diff --git a/pom.xml b/pom.xml
index edd55598..bb4ef358 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,7 +38,7 @@
3.6.1
- 1.18.30
+ 1.18.36
2.19.2
3.0.2
@@ -249,6 +249,12 @@
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
@@ -259,11 +265,15 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.13.0
${java.version}
${java.version}
true
+ full
+
+ -proc:full
+
org.projectlombok
diff --git a/services/am-analysis/pom.xml b/services/am-analysis/pom.xml
index 8058b660..cbe18358 100644
--- a/services/am-analysis/pom.xml
+++ b/services/am-analysis/pom.xml
@@ -133,7 +133,6 @@
org.projectlombok
lombok
- provided
@@ -181,6 +180,7 @@
+
org.springframework.boot
spring-boot-maven-plugin
diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java b/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java
index 763e6491..36564b9b 100644
--- a/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java
+++ b/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java
@@ -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 getPortfolioOverviews(String userId) {
@@ -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());
@@ -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;
@@ -503,6 +496,24 @@ public void publishDashboardMovers(String userId, Map 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
diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java b/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java
index 3e3fb56f..0c1f5643 100644
--- a/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java
+++ b/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java
@@ -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 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) {
diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java b/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java
index 9fec0c2a..02bbbbaf 100644
--- a/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java
+++ b/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java
@@ -468,9 +468,9 @@ public Map fetchLiveTicksForEntities(List
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);
}
}
}
@@ -491,13 +491,13 @@ public Map fetchLiveTicksForEntities(List
Map 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()
@@ -512,12 +512,12 @@ public Map fetchLiveTicksForEntities(List
}
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);
}
@@ -534,6 +534,10 @@ public Map fetchLiveTicksForEntities(List
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 resolveIsinDisplayTickers(List entities) {
if (entities == null || entities.isEmpty()) {
return Map.of();
@@ -565,11 +569,35 @@ public Map resolveIsinDisplayTickers(List entiti
}
}
- private String resolveFallbackTicker(String isin, List 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 resolveIsinToTickerMap(List symbols) {
+ if (symbols == null || symbols.isEmpty()) {
+ return Map.of();
+ }
+ List 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();
+ }
}
/**
diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java b/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java
index 8955c3d3..abb5839d 100644
--- a/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java
+++ b/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java
@@ -274,9 +274,9 @@ private Map 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);
}
}
}
@@ -303,13 +303,13 @@ private Map 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) {
@@ -329,8 +329,8 @@ private Map 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,
@@ -345,30 +345,6 @@ private Map buildTicksFromLiveQuotes(
return result;
}
- private String resolveFallbackTicker(String isin, List 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 portfolios,
String isinKey, String tickerKey,
diff --git a/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java b/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java
index a8e14d7e..4cea4c11 100644
--- a/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java
+++ b/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java
@@ -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 {
@@ -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 ticks = Map.of("ITC", new LivePriceTick(289.28, 289.85));