Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<kafka.version>3.6.1</kafka.version>

<!-- Utilities -->
<lombok.version>1.18.30</lombok.version>
<lombok.version>1.18.36</lombok.version>
<jackson.version>2.19.2</jackson.version>
<validation-api.version>3.0.2</validation-api.version>

Expand Down Expand Up @@ -249,6 +249,12 @@
</distributionManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
Expand All @@ -259,11 +265,15 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<parameters>true</parameters>
<proc>full</proc>
<compilerArgs>
<arg>-proc:full</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
Expand Down
2 changes: 1 addition & 1 deletion services/am-analysis/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- Testing -->
Expand Down Expand Up @@ -181,6 +180,7 @@

<build>
<plugins>

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,12 @@ public class DashboardAnalysisService {
private final AnalysisBusinessMetrics businessMetrics;

public DashboardSummary getSummary(String userId) {
return snapshotService.load(userId, DashboardWidgetType.SUMMARY, DashboardSummary.class)
.orElseGet(() -> {
log.info("[Summary] Snapshot miss for user {}, computing live", userId);
DashboardSummary summary = aggregator.getOverallSummary(userId);
if (summary != null) {
snapshotService.persist(userId, DashboardWidgetType.SUMMARY, summary);
}
return summary;
});
log.info("[Summary] Computing live summary for user {}", userId);
DashboardSummary summary = aggregator.getOverallSummary(userId);
if (summary != null) {
snapshotService.persist(userId, DashboardWidgetType.SUMMARY, summary);
}
return summary;
}

public List<PortfolioOverview> getPortfolioOverviews(String userId) {
Expand Down Expand Up @@ -205,10 +202,6 @@ private ActivityItem mapHoldingToActivity(AnalysisHolding holding, String portfo
: holding.getIdentity().getName();
}

String companyName = StringUtils.hasText(holding.getIdentity().getCompanyName())
? holding.getIdentity().getCompanyName()
: holding.getIdentity().getName();

String symbol = storedSymbol;
if (LivePriceOverlayHelper.looksLikeIsin(storedSymbol) && isinToTicker != null) {
String ticker = isinToTicker.get(storedSymbol.trim().toUpperCase());
Expand All @@ -217,10 +210,10 @@ private ActivityItem mapHoldingToActivity(AnalysisHolding holding, String portfo
}
if (StringUtils.hasText(ticker)) {
symbol = ticker;
} else if (StringUtils.hasText(companyName)) {
symbol = companyName;
}
}

String companyName = resolveActivityCompanyName(holding, symbol);
String exchange = holding.getIdentity().getExchange();
String sector = holding.getClassification() != null ? holding.getClassification().getSector() : null;

Expand Down Expand Up @@ -503,6 +496,24 @@ public void publishDashboardMovers(String userId, Map<String, LivePriceTick> liv
}
}

private String resolveActivityCompanyName(AnalysisHolding holding, String symbol) {
if (holding == null || holding.getIdentity() == null) {
return null;
}
if (StringUtils.hasText(holding.getIdentity().getCompanyName())
&& !AnalysisAggregator.looksLikeIsin(holding.getIdentity().getCompanyName())) {
return holding.getIdentity().getCompanyName();
}
if (StringUtils.hasText(holding.getIdentity().getName())
&& !AnalysisAggregator.looksLikeIsin(holding.getIdentity().getName())) {
return holding.getIdentity().getName();
}
if (symbol != null && !AnalysisAggregator.looksLikeIsin(symbol)) {
return symbol;
}
return null;
}

@lombok.Data
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ public static boolean looksLikeIsin(String value) {
return trimmed.length() == 12 && trimmed.matches("[A-Z]{2}[A-Z0-9]{10}");
}

/**
* Trading symbol stored on the holding in Mongo (portfolio/watchlist), not a name guess.
* Used when the quote key is an ISIN and identity.symbol is already the NSE ticker.
*/
public static String storedTradingSymbolFromHoldings(String isinOrSymbol, Collection<AnalysisEntity> entities) {
if (isinOrSymbol == null || entities == null || !looksLikeIsin(isinOrSymbol)) {
return null;
}
String target = isinOrSymbol.trim();
for (AnalysisEntity entity : entities) {
if (entity == null || entity.getHoldings() == null) {
continue;
}
for (AnalysisHolding h : entity.getHoldings()) {
if (h == null || h.getIdentity() == null) {
continue;
}
String holdingSymbol = h.getIdentity().getSymbol();
String holdingIsin = h.getIdentity().getIsin();
boolean matches = target.equalsIgnoreCase(holdingSymbol)
|| (holdingIsin != null && target.equalsIgnoreCase(holdingIsin));
if (!matches) {
continue;
}
if (holdingSymbol != null && !looksLikeIsin(holdingSymbol)) {
return holdingSymbol.trim().toUpperCase(Locale.ROOT);
}
}
}
return null;
}

/** Infer average buy when Mongo holdings omit averagePrice. */
public static double inferAveragePrice(InvestmentStats inv) {
if (inv == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,9 @@ public Map<String, LivePriceTick> fetchLiveTicksForEntities(List<AnalysisEntity>
tickersToFetch.add(resolved);
} else {
tickersToFetch.add(sym);
String fallback = resolveFallbackTicker(sym, entities);
if (fallback != null) {
tickersToFetch.add(fallback);
String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, entities);
if (storedTicker != null) {
tickersToFetch.add(storedTicker);
}
}
}
Expand All @@ -491,13 +491,13 @@ public Map<String, LivePriceTick> fetchLiveTicksForEntities(List<AnalysisEntity>
Map<String, LivePriceTick> result = new HashMap<>();
for (String sym : symbols) {
String ticker = isinToTicker.getOrDefault(sym, sym);
String fallback = resolveFallbackTicker(sym, entities);
String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, entities);

Object quoteData = quotesMap.get(ticker);
if (quoteData == null)
quoteData = quotesMap.get(sym);
if (quoteData == null && fallback != null)
quoteData = quotesMap.get(fallback);
if (quoteData == null && storedTicker != null)
quoteData = quotesMap.get(storedTicker);

if (quoteData instanceof Map<?, ?> qData) {
Double price = qData.get("lastPrice") != null ? ((Number) qData.get("lastPrice")).doubleValue()
Expand All @@ -512,12 +512,12 @@ public Map<String, LivePriceTick> fetchLiveTicksForEntities(List<AnalysisEntity>
}
if (price != null && price > 0) {
LivePriceTick liveTick = new LivePriceTick(price, prev);
log.info("[Aggregator] Put liveTick for sym={}, ticker={}, fallback={} -> {}", sym, ticker,
fallback, liveTick);
log.info("[Aggregator] Put liveTick for sym={}, ticker={}, storedTicker={} -> {}", sym, ticker,
storedTicker, liveTick);
result.put(sym, liveTick);
result.put(ticker, liveTick);
if (fallback != null)
result.put(fallback, liveTick);
if (storedTicker != null)
result.put(storedTicker, liveTick);
} else {
log.warn("[Aggregator] Price was null or <= 0 for sym={}", sym);
}
Expand All @@ -534,6 +534,10 @@ public Map<String, LivePriceTick> fetchLiveTicksForEntities(List<AnalysisEntity>
return Map.of();
}

/**
* Resolves ISINs extracted from a list of complex AnalysisEntity objects (by searching through
* their nested holdings) to their corresponding readable stock ticker symbols (e.g. INFY).
*/
public Map<String, String> resolveIsinDisplayTickers(List<AnalysisEntity> entities) {
if (entities == null || entities.isEmpty()) {
return Map.of();
Expand Down Expand Up @@ -565,11 +569,35 @@ public Map<String, String> resolveIsinDisplayTickers(List<AnalysisEntity> entiti
}
}

private String resolveFallbackTicker(String isin, List<AnalysisEntity> entities) {
// All ETFs, bonds, and standard securities are now resolved dynamically via
// MongoDB and Upstox ISIN matching.
// We no longer need hardcoded mappings inside the Java code.
return null;

/**
* Checks if a symbol string matches the standard 12-character alphanumeric ISIN format (e.g., INE009A01021).
*/
public static boolean looksLikeIsin(String symbol) {
return symbol != null && symbol.length() == 12 && symbol.matches("[A-Z]{2}[A-Z0-9]{10}");
}

/**
* Resolves a flat list of raw String symbols (filtering out those that are not ISINs)
* to their corresponding readable stock ticker symbols.
*/
public Map<String, String> resolveIsinToTickerMap(List<String> symbols) {
if (symbols == null || symbols.isEmpty()) {
return Map.of();
}
List<String> isins = symbols.stream()
.filter(AnalysisAggregator::looksLikeIsin)
.distinct()
.toList();
if (isins.isEmpty()) {
return Map.of();
}
try {
return marketDataClientService.resolveIsinsToTickers(isins);
} catch (Exception e) {
log.warn("[Aggregator] ISIN resolution failed: {}", e.getMessage());
return Map.of();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,9 @@ private Map<String, LivePriceTick> buildTicksFromLiveQuotes(
tickersToFetch.add(resolved);
} else {
tickersToFetch.add(sym);
String fallback = resolveFallbackTicker(sym, portfolios);
if (fallback != null) {
tickersToFetch.add(fallback);
String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, portfolios);
if (storedTicker != null) {
tickersToFetch.add(storedTicker);
}
}
}
Expand All @@ -303,13 +303,13 @@ private Map<String, LivePriceTick> buildTicksFromLiveQuotes(
// Step 4: Build ticks and apply dayChange% directly to holdings
for (String originalSym : holdingSymbols) {
String ticker = isinToTicker.getOrDefault(originalSym, originalSym);
String fallback = resolveFallbackTicker(originalSym, portfolios);
String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(originalSym, portfolios);
Object quoteObj = quotes.get(ticker);
if (quoteObj == null) {
quoteObj = quotes.get(originalSym);
}
if (quoteObj == null && fallback != null) {
quoteObj = quotes.get(fallback);
if (quoteObj == null && storedTicker != null) {
quoteObj = quotes.get(storedTicker);
}

if (quoteObj instanceof Map) {
Expand All @@ -329,8 +329,8 @@ private Map<String, LivePriceTick> buildTicksFromLiveQuotes(
LivePriceTick tick = new LivePriceTick(lastPrice, prevClose != null ? prevClose : lastPrice);
result.put(originalSym, tick);
result.put(ticker, tick);
if (fallback != null) {
result.put(fallback, tick);
if (storedTicker != null) {
result.put(storedTicker, tick);
}
// Directly stamp dayChange% and live prices onto holdings
applyDayChangeToHoldings(portfolios, originalSym, ticker,
Expand All @@ -345,30 +345,6 @@ private Map<String, LivePriceTick> buildTicksFromLiveQuotes(
return result;
}

private String resolveFallbackTicker(String isin, List<AnalysisEntity> portfolios) {
if (isin == null || !isin.startsWith("IN") || portfolios == null) return null;
for (AnalysisEntity entity : portfolios) {
if (entity.getHoldings() == null) continue;
for (com.am.analysis.adapter.model.AnalysisHolding h : entity.getHoldings()) {
if (h.getIdentity() != null && isin.equalsIgnoreCase(h.getIdentity().getSymbol())) {
String name = h.getIdentity().getCompanyName();
if (name != null) {
if (name.contains("GOLDBONDS2029SR-VIII") || name.contains("GOLDBONDS")) return "SGBD29VIII";
if (name.contains("- HEALTHY")) return "HEALTHY";
if (name.contains("- GROWWDEFNC")) return "GROWWDEFNC";
if (name.contains("- GROWWRAIL")) return "GROWWRAIL";
if (name.contains("- MOHEALTH")) return "MOHEALTH";
if (name.contains("GOLD BEES") || name.contains("GOLDBEES")) return "GOLDBEES";
if (name.contains("NIFTY BEES") || name.contains("NIFTYBEES")) return "NIFTYBEES";
if (name.contains("VODAFONE IDEA") || name.contains("VODAFONE IDEA-EQ")) return "IDEA";
if (name.contains("RAIL VIKAS") || name.contains("RVNL")) return "RVNL";
}
}
}
}
return null;
}

/** Stamps live market stats directly onto matched holdings (by ISIN or ticker). */
private void applyDayChangeToHoldings(List<AnalysisEntity> portfolios,
String isinKey, String tickerKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

class LivePriceOverlayHelperTest {

Expand All @@ -24,6 +25,25 @@ void looksLikeIsin_detectsStandardIsin() {
assertEquals(false, LivePriceOverlayHelper.looksLikeIsin(null));
}

@Test
void storedTradingSymbolFromHoldings_usesMongoTickerNotCompanyName() {
AnalysisHolding holding = AnalysisHolding.builder()
.identity(HoldingIdentity.builder()
.symbol("GROWWDEFNC")
.isin("INF666M01IO8")
.companyName("should-not-be-parsed")
.build())
.build();
AnalysisEntity entity = AnalysisEntity.builder()
.type(AnalysisEntityType.PORTFOLIO)
.holdings(List.of(holding))
.build();

assertEquals("GROWWDEFNC",
LivePriceOverlayHelper.storedTradingSymbolFromHoldings("INF666M01IO8", List.of(entity)));
assertNull(LivePriceOverlayHelper.storedTradingSymbolFromHoldings("INF666M01IO8", List.of()));
}

@Test
void resolveTick_matchesPrefixedSymbol() {
Map<String, LivePriceTick> ticks = Map.of("ITC", new LivePriceTick(289.28, 289.85));
Expand Down
Loading