import { Component, ElementRef, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { DashboardService } from './dashboard.service'; import { take } from 'rxjs/operators'; import { lastValueFrom } from 'rxjs'; type IndexItem = { code: string; price: number; chg: number; miniPath?: string; isUp?: boolean }; type Company = { sym: string; ltp: number; chg: number; high: number; low: number }; type Sector = { name: string; picks: string[] }; type MarketCard = { title: string; value: string; chg: string; dir: 'up' | 'down' | 'neutral' }; type GlobalIndex = { id: string; name: string; country: string; region: string; price: number; change: number; changePct: number; sparkline: number[]; miniPath?: string; isUp?: boolean }; @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule, HttpClientModule], // IndexCardComponent], templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { // Demo data (used as fallback until live data loads) indices: IndexItem[] = [ { code: 'NIFTY 50', price: 24480.55, chg: +0.35 }, { code: 'BANK NIFTY', price: 51880.20, chg: -0.12 }, { code: 'SENSEX', price: 80820.40, chg: +0.28 }, { code: 'NIFTY MIDCAP', price: 53000.10, chg: +0.45 }, { code: 'NIFTY SMALLCAP', price: 17350.60, chg: -0.18 }, ]; // Market overview cards (populated live when API responds) // initialize placeholders to em dash so UI doesn't show raw 0 or empty strings marketCards: MarketCard[] = [ { title: 'Gold', value: '—', chg: '—', dir: 'neutral' }, { title: 'Silver', value: '—', chg: '—', dir: 'neutral' }, { title: 'Crude Oil (Brent)', value: '—', chg: '—', dir: 'neutral' }, { title: 'Crude Oil (WTI)', value: '—', chg: '—', dir: 'neutral' }, { title: 'Natural Gas', value: '—', chg: '—', dir: 'neutral' }, { title: 'USD/INR', value: '—', chg: '—', dir: 'neutral' }, { title: 'EUR/USD', value: '—', chg: '—', dir: 'neutral' }, { title: 'GBP/USD', value: '—', chg: '—', dir: 'neutral' }, { title: 'Bitcoin', value: '—', chg: '—', dir: 'neutral' }, { title: 'Ethereum', value: '—', chg: '—', dir: 'neutral' }, { title: 'S&P 500', value: '—', chg: '—', dir: 'neutral' }, { title: 'NASDAQ', value: '—', chg: '—', dir: 'neutral' }, { title: 'DAX', value: '—', chg: '—', dir: 'neutral' }, { title: 'Nikkei', value: '—', chg: '—', dir: 'neutral' }, { title: 'Copper', value: '—', chg: '—', dir: 'neutral' } ]; // expose duplicated list for seamless loop in template get marketCardsRepeat(): MarketCard[] { return [...this.marketCards, ...this.marketCards]; } companiesByIndex: Record = { 'NIFTY 50': [ { sym: 'RELIANCE', ltp: 2950.30, chg: +0.85, high: 2972.0, low: 2920.0 }, { sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 }, { sym: 'INFY', ltp: 1622.40, chg: +0.42, high: 1631.5, low: 1605.2 }, { sym: 'HDFCBANK', ltp: 1548.10, chg: +0.12, high: 1556.0, low: 1531.5 }, { sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 }, ], 'BANK NIFTY': [ { sym: 'SBIN', ltp: 884.30, chg: +0.70, high: 892.0, low: 876.2 }, { sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 }, { sym: 'KOTAKBANK', ltp: 1744.50, chg: +0.15, high: 1752.0, low: 1732.0 }, { sym: 'PNB', ltp: 119.80, chg: +1.25, high: 121.3, low: 118.4 }, { sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 }, ], 'SENSEX': [ { sym: 'LT', ltp: 3822.40, chg: +0.34, high: 3839.0, low: 3788.5 }, { sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 }, { sym: 'ITC', ltp: 496.70, chg: +0.48, high: 499.5, low: 492.6 }, { sym: 'HCLTECH', ltp: 1822.10, chg: +0.22, high: 1833.0, low: 1808.0 }, { sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 }, ], 'NIFTY MIDCAP': [ { sym: 'TATAELXSI', ltp: 9175.00, chg: +0.90, high: 9250.0, low: 9080.0 }, { sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 }, { sym: 'INDHOTEL', ltp: 692.10, chg: +0.55, high: 699.0, low: 684.5 }, { sym: 'TVSMOTOR', ltp: 2088.20, chg: +0.20, high: 2105.0, low: 2068.0 }, { sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 }, ], 'NIFTY SMALLCAP': [ { sym: 'TANLA', ltp: 1115.10, chg: +1.12, high: 1134.0, low: 1101.0 }, { sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 }, { sym: 'KEI', ltp: 4965.00, chg: +0.44, high: 5012.0, low: 4920.0 }, { sym: 'PNCINFRA', ltp: 475.75, chg: +0.18, high: 482.0, low: 470.0 }, { sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 }, ], }; // Create demo US companies per index so Companies table and sectors show US firms when 'US' selected private createUSCompaniesByIndex(): Record { return { 'S&P 500': [ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 }, { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 }, { sym: 'AMZN', ltp: 140.25, chg: +0.8, high: 142.0, low: 138.0 }, { sym: 'GOOGL', ltp: 128.70, chg: +0.4, high: 130.0, low: 127.0 }, { sym: 'META', ltp: 310.45, chg: -0.9, high: 316.0, low: 308.1 }, ], 'Dow Jones': [ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 }, { sym: 'UNH', ltp: 480.22, chg: +0.3, high: 482.0, low: 475.0 }, { sym: 'V', ltp: 230.12, chg: +0.6, high: 231.5, low: 228.0 }, { sym: 'JPM', ltp: 140.50, chg: -0.2, high: 142.0, low: 139.0 }, { sym: 'GS', ltp: 360.75, chg: +0.1, high: 362.0, low: 358.0 }, ], 'Nasdaq Composite': [ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 }, { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 }, { sym: 'TSLA', ltp: 210.44, chg: +2.5, high: 215.0, low: 205.5 }, { sym: 'NVDA', ltp: 420.55, chg: +3.1, high: 430.2, low: 410.0 }, { sym: 'ADBE', ltp: 485.60, chg: -0.6, high: 490.0, low: 482.0 }, ], 'S&P MidCap 400': [ { sym: 'ODFL', ltp: 150.12, chg: +0.9, high: 152.0, low: 148.0 }, { sym: 'LVS', ltp: 80.34, chg: -0.4, high: 82.0, low: 79.0 }, { sym: 'UAL', ltp: 52.10, chg: +1.5, high: 53.0, low: 50.5 }, { sym: 'NWL', ltp: 30.22, chg: -0.2, high: 31.0, low: 29.5 }, { sym: 'EXPD', ltp: 115.44, chg: +0.7, high: 117.0, low: 114.0 }, ], 'S&P SmallCap 600': [ { sym: 'AEO', ltp: 25.12, chg: +0.5, high: 25.8, low: 24.5 }, { sym: 'CNC', ltp: 45.22, chg: -0.3, high: 46.0, low: 44.0 }, { sym: 'FLEX', ltp: 12.34, chg: +2.0, high: 12.8, low: 11.9 }, { sym: 'BOKF', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 }, { sym: 'PRXL', ltp: 8.90, chg: +0.2, high: 9.2, low: 8.5 }, ] }; } // Create demo India companies per index so Companies table and sectors show India firms when 'India' selected private createIndiaCompaniesByIndex(): Record { return { 'NIFTY 50': [ { sym: 'RELIANCE', ltp: 2950.30, chg: 0.85, high: 2972.0, low: 2920.0 }, { sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 }, { sym: 'INFY', ltp: 1622.40, chg: 0.42, high: 1631.5, low: 1605.2 }, { sym: 'HDFCBANK', ltp: 1548.10, chg: 0.12, high: 1556.0, low: 1531.5 }, { sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 }, ], 'BANK NIFTY': [ { sym: 'SBIN', ltp: 884.30, chg: 0.70, high: 892.0, low: 876.2 }, { sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 }, { sym: 'KOTAKBANK', ltp: 1744.50, chg: 0.15, high: 1752.0, low: 1732.0 }, { sym: 'PNB', ltp: 119.80, chg: 1.25, high: 121.3, low: 118.4 }, { sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 }, ], 'SENSEX': [ { sym: 'LT', ltp: 3822.40, chg: 0.34, high: 3839.0, low: 3788.5 }, { sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 }, { sym: 'ITC', ltp: 496.70, chg: 0.48, high: 499.5, low: 492.6 }, { sym: 'HCLTECH', ltp: 1822.10, chg: 0.22, high: 1833.0, low: 1808.0 }, { sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 }, ], 'NIFTY MIDCAP': [ { sym: 'TATAELXSI', ltp: 9175.00, chg: 0.90, high: 9250.0, low: 9080.0 }, { sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 }, { sym: 'INDHOTEL', ltp: 692.10, chg: 0.55, high: 699.0, low: 684.5 }, { sym: 'TVSMOTOR', ltp: 2088.20, chg: 0.20, high: 2105.0, low: 2068.0 }, { sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 }, ], 'NIFTY SMALLCAP': [ { sym: 'TANLA', ltp: 1115.10, chg: 1.12, high: 1134.0, low: 1101.0 }, { sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 }, { sym: 'KEI', ltp: 4965.00, chg: 0.44, high: 5012.0, low: 4920.0 }, { sym: 'PNCINFRA', ltp: 475.75, chg: 0.18, high: 482.0, low: 470.0 }, { sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 }, ], }; } // Create sector picks for US so sector grid shows US companies when 'US' selected private createUSSectors(): Sector[] { return [ { name: 'Technology', picks: ['AAPL', 'MSFT', 'GOOGL', 'NVDA'] }, { name: 'Financials', picks: ['JPM', 'GS', 'BAC', 'C'] }, { name: 'Consumer Discretionary', picks: ['AMZN', 'TSLA', 'MCD'] }, { name: 'Healthcare', picks: ['UNH', 'JNJ', 'PFE'] }, { name: 'Industrials', picks: ['UNP', 'HON', 'CAT'] }, ]; } sectors: Sector[] = [ { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] }, { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] }, { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] }, { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&M', 'TVSMOTOR'] }, { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] }, { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] }, ]; get indexCodes(): string[] { return Object.keys(this.companiesByIndex); } activeIndex: string = this.indexCodes[0]; // currently selected company symbol for chart highlighting selectedCompany: string | null = null; gainers: Company[] = []; losers: Company[] = []; // New global indices lists globalIndices: GlobalIndex[] = []; indiaIndices: GlobalIndex[] = []; // All indices cache (includes India and others) private allGlobalIndices: GlobalIndex[] = []; // Country filter // default countries shown immediately (will be replaced by live data when available) countries: string[] = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia']; selectedCountry = 'India'; // common alias map so UI labels like 'US' or 'Uk' match backend country names private countryAliases: Record = { 'us': ['united states', 'usa', 'us', 'u.s.a', 'u.s.'], 'uk': ['united kingdom', 'britain', 'uk', 'u.k.'], 'german': ['germany', 'german'], 'russia': ['russia', 'russian federation'], 'india': ['india'], 'sweden': ['sweden'] }; // demo list for India indices private defaultIndiaIndexNames: string[] = [ 'Nifty 50', 'Nifty Next 50', 'Nifty 100', 'Nifty 500', 'Nifty Bank', 'Nifty IT', 'Nifty Pharma', 'Nifty FMCG', 'Nifty Midcap 100', 'Nifty Smallcap 100' ]; // avoid repeated fetches in quick succession private globalFetched = false; // last updated timestamp shown in the header lastUpdated = '—'; // chart symbol and timer handle chartSymbol = 'RELIANCE'; private tickHandle?: number; public quotesIntervalHandle?: number; @ViewChild('chartCanvas', { static: false }) private canvasRef?: ElementRef; @ViewChild('chartContainer', { static: false }) private chartContainerRef?: ElementRef; @ViewChild('tickerTrack', { static: false }) private tickerTrackRef?: ElementRef; private marqueeAnimationId?: number; private marqueeResizeObserver?: ResizeObserver | null = null; private marqueeMutationObserver?: MutationObserver | null = null; private marqueeMeasureTimeout?: any; private lastDurationSec?: number | null = null; // priority lists of important indices per country (used to order and pick indices from live payload) private importantIndicesMap: Record = { 'india': [ 'Nifty 50', 'Nifty Bank', 'SENSEX', 'Nifty Next 50', 'Nifty 100', 'Nifty 500', 'Nifty IT', 'Nifty Pharma', 'Nifty FMCG', 'Nifty Midcap 100', 'Nifty Smallcap 100' ], 'us': ['S&P 500', 'Dow Jones', 'Nasdaq Composite', 'S&P MidCap 400', 'S&P SmallCap 600'], 'uk': ['FTSE 100', 'FTSE 250', 'FTSE 350'], 'german': ['DAX', 'MDAX', 'TecDAX'], 'sweden': ['OMX Stockholm 30'], 'russia': ['MOEX Russia', 'RTS Index'] }; private uniqueByName(list: GlobalIndex[]): GlobalIndex[] { const seen = new Set(); const out: GlobalIndex[] = []; for (const it of list) { const name = (it.name || '').toString().trim(); const key = name.toLowerCase(); if (!key) continue; if (seen.has(key)) continue; seen.add(key); out.push(it); } return out; } // Build a prioritized list of indices for a country based on importantIndicesMap and available live entries private buildCountryIndices(countryKey: string): GlobalIndex[] { if (!countryKey) return []; const key = countryKey.toLowerCase().trim(); const important = this.importantIndicesMap[key] || []; // gather live matches (case-insensitive name match) const live = (this.allGlobalIndices || []).filter(g => this.countryMatches(key, (g.country || '').toLowerCase())); const byNameMap = new Map(); // normalize and index live by name live.forEach(g => { const n = (g.name || '').toString().trim(); if (n) byNameMap.set(n.toLowerCase(), g); }); const result: GlobalIndex[] = []; // pick in priority order for (const name of important) { const found = byNameMap.get(name.toLowerCase()); if (found) result.push(found); } // append any other live indices for the country that were not in the important list (unique) live.forEach(g => { const n = (g.name || '').toString().trim().toLowerCase(); if (!result.some(r => ((r.name || '').toLowerCase() === n))) result.push(g); }); return this.uniqueByName(result).map(g => { // ensure miniPath computed g.miniPath = this.sparkPath(g.sparkline || []); g.isUp = (g.changePct || 0) >= 0; return g; }); } constructor(private host: ElementRef, private market: DashboardService) { } ngOnInit(): void { // Initialize mini sparklines and clock this.indices.forEach((idx) => this.refreshIndexSpark(idx)); this.setClock(); this.updateGainersLosers(); // Load cached market overview so values appear immediately on reload if available try { const raw = localStorage.getItem('marketCardsCache'); if (raw) { const cached = JSON.parse(raw) as MarketCard[]; if (Array.isArray(cached) && cached.length) this.marketCards = cached; } } catch { // ignore cache errors } // --- Load cached companies/quotes so Companies table shows immediately after reload --- try { const rawComp = localStorage.getItem('companiesCacheV1'); if (rawComp) { const parsed = JSON.parse(rawComp) as any; if (parsed && parsed.companiesByIndex) { this.companiesByIndex = parsed.companiesByIndex; if (parsed.lastUpdated) this.lastUpdated = parsed.lastUpdated; this.updateGainersLosers(); // ensure activeIndex valid if (!this.indexCodes.includes(this.activeIndex)) { this.activeIndex = this.indexCodes[0]; } } } } catch { // ignore } // Fetch live data once on init (and keep demo simulation running as fallback) this.fetchLiveMarkets(); // fetch market overview cards independently so they display even if /getcompanies fails this.fetchMarketCards(); this.fetchGlobalIndices(); // Ensure demo India indices present initially if backend doesn't provide them if (this.selectedCountry && this.selectedCountry.toLowerCase().trim() === 'india' && (!this.indiaIndices || this.indiaIndices.length === 0)) { this.populateDefaultIndiaIndices(); } // Start ticks every 3s this.tickHandle = window.setInterval(() => { this.tickIndices(); this.tickCompanies(); this.updateGainersLosers(); this.setClock(); }, 3000); // Start polling live quotes every 30s this.quotesIntervalHandle = window.setInterval(() => { this.fetchLiveQuotesForActiveIndex(); }, 30000); // ensure selectedCompany defaults to first company in active index (from cache or demo) const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; // draw initial chart for selected company (try live intraday) this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ }); } } ngAfterViewInit(): void { // Initial chart const initial = this.companiesByIndex[this.activeIndex][0]; this.chartSymbol = initial?.sym ?? '—'; // draw SVG style area chart in container this.renderAreaChart(initial?.sym ?? '—', initial?.ltp ?? 100); // setup ticker marquee after view init this.setupMarquee(); } ngOnDestroy(): void { if (this.tickHandle) { clearInterval(this.tickHandle); } if (this.quotesIntervalHandle) { clearInterval(this.quotesIntervalHandle); } if (this.marqueeAnimationId) { cancelAnimationFrame(this.marqueeAnimationId); this.marqueeAnimationId = undefined; } if (this.marqueeResizeObserver) { try { this.marqueeResizeObserver.disconnect(); } catch (e) { /* ignore */ } this.marqueeResizeObserver = null; } if (this.marqueeMutationObserver) { try { this.marqueeMutationObserver.disconnect(); } catch (e) { /* ignore */ } this.marqueeMutationObserver = null; } if (this.marqueeMeasureTimeout) { clearTimeout(this.marqueeMeasureTimeout); this.marqueeMeasureTimeout = undefined; } } // Loading state shown by template overlay while quotes request running quotesLoading: boolean = false; // localStorage key for persisted companies/quotes cache private readonly companiesCacheKey = 'companiesCacheV1'; // Called after view init to set animation duration based on content width so the marquee flows evenly private setupMarquee(): void { const el = this.tickerTrackRef?.nativeElement as HTMLDivElement | undefined; if (!el) return; // measure content width and container width; duplicated items => distance is half scrollWidth const measureOnce = () => { try { const totalWidth = el.scrollWidth || 0; // width of duplicated content const distance = Math.max(1, totalWidth / 2); const pxPerSecond = 120; // control speed const durationSec = Math.max(6, distance / pxPerSecond); // avoid changing CSS var if duration unchanged (prevents animation restart) if (this.lastDurationSec == null || Math.abs((this.lastDurationSec || 0) - durationSec) > 0.5) { el.style.setProperty('--marquee-duration', `${Math.round(durationSec)}s`); this.lastDurationSec = durationSec; } } catch (e) { // ignore } }; const scheduleMeasure = () => { if (this.marqueeMeasureTimeout) clearTimeout(this.marqueeMeasureTimeout); this.marqueeMeasureTimeout = setTimeout(() => { measureOnce(); this.marqueeMeasureTimeout = undefined; }, 120); }; // initial measurement scheduleMeasure(); // observe size changes and DOM changes, but debounce actual measurement try { this.marqueeResizeObserver = new ResizeObserver(() => scheduleMeasure()); this.marqueeResizeObserver.observe(el); if (el.parentElement) this.marqueeResizeObserver.observe(el.parentElement); } catch (e) { this.marqueeResizeObserver = null; } try { this.marqueeMutationObserver = new MutationObserver(() => scheduleMeasure()); this.marqueeMutationObserver.observe(el, { childList: true, subtree: true }); } catch (e) { this.marqueeMutationObserver = null; } } private fetchGlobalIndices(): void { // avoid duplicate fetch if (this.globalFetched) return; this.globalFetched = true; this.market.getGlobalIndices().pipe(take(1)).subscribe({ next: (resp: any) => { try { console.log('fetchGlobalIndices resp:', resp); const data = resp?.data || resp; if (!Array.isArray(data)) return; // capture marketCards from response early so we can use it when deriving indices const respMarketCards = (resp as any)?.marketCards; // map all returned indices const mapped = data.map(this.mapToGlobalIndex); // If backend returned no mapped indices but provided marketCards, derive a few common indices from marketCards if ((!mapped || mapped.length === 0) && Array.isArray(respMarketCards) && respMarketCards.length) { const indexNames = new Set(['S&P 500', 'NASDAQ', 'DAX', 'Nikkei', 'Nifty 50', 'Nifty Bank', 'SENSEX']); const derived: GlobalIndex[] = []; respMarketCards.forEach((mc: any) => { if (indexNames.has(mc.title)) { const price = Number(mc.price) || (typeof mc.display === 'number' ? mc.display : NaN); const pct = mc.chgPct ?? (mc.chg && price ? (Number(mc.chg) / (price - Number(mc.chg))) * 100 : 0); const gi: GlobalIndex = { id: (mc.title || '').replace(/\s+/g, '_'), name: mc.title, country: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'United States' : mc.title === 'DAX' ? 'Germany' : mc.title === 'Nikkei' ? 'Japan' : 'India', region: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'US' : mc.title === 'DAX' ? 'German' : mc.title === 'Nikkei' ? 'Japan' : 'India', price: Number(isNaN(price) ? 0 : price), change: Number(mc.chg ?? 0), changePct: Number(pct || 0), sparkline: this.series(Number(isNaN(price) ? Math.round(Number(mc.price) || 1000) : price), 26) } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.changePct || 0) >= 0; derived.push(gi); } }); // merge derived into mapped so rest of flow continues if (derived.length) { mapped.push(...derived); } } // compute mini sparklines paths mapped.forEach((g) => { g.miniPath = this.sparkPath(g.sparkline || []); g.isUp = (g.change || 0) >= 0; }); // merge into cache (dedupe by id) const byId = new Map(); [...this.allGlobalIndices, ...mapped].forEach(i => byId.set(i.id, i)); this.allGlobalIndices = Array.from(byId.values()); // populate countries list for filter — merge live values but keep defaults if none const uniq = Array.from(new Set(this.allGlobalIndices.map((g) => g.country).filter(Boolean))); // always include common defaults so UI shows tabs even when backend data missing const defaultCountries = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia']; if (uniq.length) { // Filter live countries: exclude those that are aliases of our defaults to avoid duplicates const aliasesMap: Record = this.countryAliases; const isAliasOfDefault = (countryName: string) => { if (!countryName) return false; const lname = countryName.toLowerCase(); // check against every alias set; if any alias contains the live name, consider it a match for (const key of Object.keys(aliasesMap)) { const aliases = aliasesMap[key] || []; for (const a of aliases) { if (lname.includes(a)) return true; } } return false; }; const rest = uniq .filter(c => c.toLowerCase() !== 'india') .filter(c => !isAliasOfDefault(c)) .sort(); // merge defaults with any truly extra live countries // filter out any unwanted country labels (e.g., Russia, Japan) const hide = new Set(['russia', 'japan']); const merged = [...defaultCountries, ...rest].filter(c => !hide.has((c || '').toLowerCase())); this.countries = merged; } else { this.countries = [...defaultCountries]; } // replace marketCards with live market overview if available in response root if (Array.isArray(respMarketCards) && respMarketCards.length) { this.marketCards = respMarketCards.map((m: any) => { const display = (m.display ?? m.price); const priceNum = Number(m.price ?? display ?? NaN); // prefer explicit percent from backend, otherwise compute from point change and previous price let pct = m.chgPct; if (pct === undefined || pct === null || Number.isNaN(Number(pct))) { const chgPoint = Number(m.chg || 0); const prev = priceNum - chgPoint; pct = (prev && !Number.isNaN(prev)) ? (chgPoint / prev) * 100 : 0; } const priceStr = (display === null || display === undefined) ? '—' : (typeof display === 'number' ? display.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(display)); const chgNum = Number(pct || 0); return { title: m.title, value: priceStr, // show percent, not raw point change chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%', dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral' } as MarketCard; }); console.log('marketCards set from resp.marketCards:', this.marketCards); } else { // fallback: derive a simple market overview from some well-known indices/currencies returned in "data" const pickTitles = new Set(['Gold', 'Silver', 'Crude Oil (Brent)', 'Crude Oil (WTI)', 'Natural Gas', 'USD/INR', 'EUR/USD', 'GBP/USD', 'Bitcoin', 'Ethereum', 'S&P 500', 'NASDAQ', 'DAX', 'Nikkei']); const derived: MarketCard[] = []; // if backend provided indices with matching names, map them mapped.forEach((m: any) => { if (pickTitles.has(m.name)) { const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price)); const chgNum = Number(m.change ?? m.changePct ?? 0); derived.push({ title: m.name, value: priceStr, chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%', dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral' }); } }); // if still empty, try to pick first few indices if (derived.length === 0) { mapped.slice(0, 12).forEach((m: any) => { const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price)); const chgNum = Number(m.change ?? m.changePct ?? 0); derived.push({ title: m.name, value: priceStr, chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%', dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral' }); }); } if (derived.length) { this.marketCards = derived; console.log('marketCards derived from indices:', this.marketCards); } } // apply current selection so UI updates immediately with live data this.applySelection(this.selectedCountry || 'India'); } catch (e) { console.error('Error processing global indices', e); } }, error: (err: any) => { console.warn('Failed to fetch global indices', err); } }); } private mapToGlobalIndex(d: any): GlobalIndex { return { id: d.id || `${(d.name || '').replace(/\s+/g, '_')}_${Math.random().toString(36).slice(2, 8)}`, name: d.name, country: d.country, region: d.region, price: d.price, change: d.change, changePct: d.changePct, sparkline: d.sparkline || [], } as GlobalIndex; } // Create demo US indices list with required names so UI shows S&P 500, Dow Jones, Nasdaq, S&P MidCap 400 and S&P SmallCap 600 when 'US' selected private createUSIndices(): GlobalIndex[] { const names = [ 'S&P 500', 'Dow Jones', 'Nasdaq Composite', 'S&P MidCap 400', 'S&P SmallCap 600' ]; return names.map((name, i) => { const base = Math.round(3000 + i * 200 + (Math.random() - 0.5) * 5000); const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2)); const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'United States', region: 'US', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); } // Create demo UK indices (FTSE family) so UI shows FTSE 100, FTSE 250, FTSE 350, FTSE Small Cap when 'Uk' selected private createUKIndices(): GlobalIndex[] { const names = [ 'FTSE 100', 'FTSE 250', 'FTSE 350', 'FTSE Small Cap' ]; return names.map((name, i) => { const base = Math.round(6000 + i * 100 + (Math.random() - 0.5) * 400); const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2)); const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'United Kingdom', region: 'UK', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); } // Create demo German indices (DAX family) so UI shows DAX, MDAX, TecDAX, SDAX when 'German' selected private createGermanIndices(): GlobalIndex[] { const names = [ 'DAX', 'MDAX', 'TecDAX', 'SDAX' ]; return names.map((name, i) => { const base = Math.round(10000 + i * 200 + (Math.random() - 0.5) * 800); const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2)); const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'Germany', region: 'German', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); } // Create demo Sweden indices so UI shows OMXS30 and STOXX Sweden when 'Sweden' selected private createSwedenIndices(): GlobalIndex[] { const names = [ 'OMX Stockholm 30', 'STOXX Sweden' ]; return names.map((name, i) => { const base = Math.round(1500 + i * 50 + (Math.random() - 0.5) * 200); const change = Number(((Math.random() - 0.5) * 1.0).toFixed(2)); const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'Sweden', region: 'Sweden', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); } // Create demo Russia indices so UI shows MOEX Russia and RTS Index when 'Russia' selected private createRussiaIndices(): GlobalIndex[] { const names = ['MOEX Russia', 'RTS Index']; return names.map((name, i) => { const base = Math.round(2000 + i * 300 + (Math.random() - 0.5) * 800) * (name === 'MOEX Russia' ? 5 : 1); const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2)); const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'Russia', region: 'Russia', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); } // Helper to find a company object by symbol across all indices private findCompanyBySymbol(sym: string): Company | undefined { if (!sym) return undefined; const lists = Object.values(this.companiesByIndex || {}); for (const list of lists) { for (const c of list) { if ((c.sym || '').toString() === sym.toString()) return c; } } return undefined; } // Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection get displayIndices(): GlobalIndex[] { const sel = (this.selectedCountry || '').toLowerCase().trim(); let list: GlobalIndex[] = []; if (sel === 'india') list = this.indiaIndices || []; else list = this.globalIndices || []; // Deduplicate by name (case-insensitive) and limit visible cards to a sensible number (9) const unique = this.uniqueByName(list); // apply small display fixes: normalize long Dow Jones name and boost MOEX Russia demo value const adjusted = unique.slice(0, 14).map(g => { const copy = { ...g } as GlobalIndex; if (typeof copy.name === 'string') { // replace long backend label if present copy.name = copy.name.replace(/Dow Jones Industrial Average/ig, 'Dow Jones'); } // if MOEX Russia appears and looks small (likely demo), scale it up for visibility if (typeof copy.name === 'string' && /moex\s*russia/i.test(copy.name)) { // scale price but avoid absurdly large values: if price < 10000, multiply if (typeof copy.price === 'number' && copy.price > 0 && copy.price < 10000) { copy.price = Math.round(copy.price * 5); } } return copy; }); return adjusted.slice(0, 9); } // Fetch live markets from backend private fetchLiveMarkets(): void { this.market.getCompanies().pipe(take(1)).subscribe({ next: (data: any) => { console.log('fetchLiveMarkets - getCompanies response:', data); try { if (data.indices && Array.isArray(data.indices) && data.indices.length) { // map backend indices to IndexItem this.indices = data.indices.map((it: any) => ({ code: it.code, price: it.price, chg: it.chg })); this.indices.forEach((idx) => this.refreshIndexSpark(idx)); } if (data.companiesByIndex) { this.companiesByIndex = data.companiesByIndex; // Immediately fetch live quotes for active index now that companies are loaded try { console.log('Companies loaded for indices, fetching live quotes for', this.activeIndex); this.fetchLiveQuotesForActiveIndex(); } catch (e) { console.warn('fetchLiveQuotesForActiveIndex failed shortly after companies load', e); } } if (data.sectors) { this.sectors = data.sectors; } if (data.lastUpdated) { this.lastUpdated = data.lastUpdated; } // update derived lists and chart this.updateGainersLosers(); const initial = this.companiesByIndex[this.activeIndex]?.[0]; if (initial) this.renderAreaChart(initial.sym, initial.ltp || 100); // if backend provided marketCards in /getcompanies payload use them const respMarketCards = data.marketCards ?? data.data?.marketCards ?? data.results?.marketCards ?? data.market_cards ?? data.cards ?? null; if (Array.isArray(respMarketCards) && respMarketCards.length) { console.log('fetchLiveMarkets: found marketCards in /getcompanies payload', respMarketCards); // apply live marketCards immediately and cache them const mapped = respMarketCards.map((m: any) => { const display = m.display ?? m.price ?? m.value ?? m.last ?? m.close ?? null; const value = this.formatPrice(display); const pctRaw = m.chgPct ?? m.chg_pct ?? m.pct ?? m.changePct ?? m.change_percent ?? m.change ?? null; const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(m.chg || 0) && typeof display === 'number' ? ((Number(m.chg) / (display - Number(m.chg))) * 100) : 0); const n = Number(pct || 0); return { title: m.title ?? m.name ?? (m.symbol ?? '—'), value, chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'), dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral') } as MarketCard; }); this.marketCards = mapped; try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ } } else { console.log('fetchLiveMarkets: no marketCards found in /getcompanies payload'); } // persist companies snapshot so table shows immediately on reload try { const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated }; localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot)); } catch { /* ignore */ } } catch (e) { console.error('Error processing market data', e); } }, error: (err: any) => { console.warn('Failed to fetch live markets', err); } }); } // Fetch market overview cards from backend (uses /getmarketcards) private fetchMarketCards(): void { this.market.getMarketCards().pipe(take(1)).subscribe({ next: (cards: any[]) => { console.log('fetchLiveMarkets - /getmarketcards response:', cards); try { if (cards && Array.isArray(cards) && cards.length) { const mapped = cards.map((c: any) => { const display = c.display ?? c.price ?? c.value ?? c.last ?? c.close ?? null; const value = this.formatPrice(display); const pctRaw = c.chgPct ?? c.chg_pct ?? c.pct ?? c.changePct ?? c.change_percent ?? c.change ?? null; const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(c.chg || 0) && typeof display === 'number' ? ((Number(c.chg) / (display - Number(c.chg))) * 100) : 0); const n = Number(pct || 0); return { title: c.title ?? c.name ?? (c.symbol ?? '—'), value, chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'), dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral') } as MarketCard; }); this.marketCards = mapped; try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ } } } catch (e) { console.error('Error processing market cards', e); } }, error: (err: any) => { console.warn('Failed to fetch market cards', err); } }); } // UI helpers fmt(n: number): string { return n.toFixed(2); } random(): number { return Math.random(); } // Format a price for display. Treat null/undefined/empty as missing and return '—'./ private formatPrice(val: any): string { if (val === null || val === undefined) return '—'; if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 }); if (typeof val === 'string') return val.trim() === '' ? '—' : val; return String(val); } // Format index name for UI: remove bracketed suffixes like "(OMXS30)" public formatIndexName(name: string | undefined | null): string { if (!name) return ''; return String(name).replace(/\s*([^)]*)\)\s*/g, '').replace(/\s*\([^)]*\)\s*/g, '').trim(); } public setActiveIndex(code: string): void { this.activeIndex = code; // fetch live quotes for newly active index immediately this.fetchLiveQuotesForActiveIndex(); // update selected company to first of the newly active index const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; // draw initial chart for selected company (try live intraday) this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ }); } } // Create demo UK companies per index private createUKCompaniesByIndex(): Record { return { 'FTSE 100': [ { sym: 'HSBA', ltp: 520.10, chg: +0.8, high: 525.0, low: 515.0 }, { sym: 'BP', ltp: 310.22, chg: -0.3, high: 315.0, low: 308.0 }, { sym: 'GSK', ltp: 1380.50, chg: +0.2, high: 1395.0, low: 1370.0 }, { sym: 'BARC', ltp: 190.44, chg: +1.1, high: 192.0, low: 188.0 }, { sym: 'VOD', ltp: 120.60, chg: -0.6, high: 122.0, low: 119.0 }, ], 'FTSE 250': [ { sym: 'SMT', ltp: 45.12, chg: +0.4, high: 46.0, low: 44.0 }, { sym: 'TPK', ltp: 8.34, chg: -0.2, high: 8.8, low: 8.0 }, ] }; } private createUKSectors(): Sector[] { return [ { name: 'UK Financials', picks: ['HSBA', 'BARC', 'LLOY'] }, { name: 'Energy', picks: ['BP', 'RDSA'] }, { name: 'Telecom', picks: ['VOD', 'BT'] }, ]; } // Create demo German companies per index private createGermanCompaniesByIndex(): Record { return { 'DAX': [ { sym: 'SAP', ltp: 110.12, chg: +0.5, high: 112.0, low: 109.0 }, { sym: 'BMW', ltp: 85.34, chg: -0.4, high: 86.5, low: 84.0 }, { sym: 'DAI', ltp: 75.22, chg: +0.7, high: 76.0, low: 74.0 }, { sym: 'BAYN', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 }, ] }; } private createGermanSectors(): Sector[] { return [ { name: 'Automotive', picks: ['BMW', 'DAI', 'VOW3'] }, { name: 'Software', picks: ['SAP', 'SOW'] }, ]; } // Create demo Sweden companies per index private createSwedenCompaniesByIndex(): Record { return { 'OMX Stockholm 30': [ { sym: 'ERIC', ltp: 78.12, chg: +0.9, high: 79.5, low: 76.8 }, { sym: 'SEB', ltp: 120.34, chg: -0.3, high: 121.8, low: 119.0 }, { sym: 'VOLV', ltp: 150.44, chg: +0.4, high: 152.0, low: 149.0 }, ] }; } private createSwedenSectors(): Sector[] { return [ { name: 'Industrial', picks: ['VOLV', 'ATCO'] }, { name: 'Telecom', picks: ['ERIC'] }, ]; } // Create demo Russia companies per index private createRussiaCompaniesByIndex(): Record { return { 'MOEX Russia': [ { sym: 'SBER', ltp: 170.12, chg: +1.2, high: 172.0, low: 168.0 }, { sym: 'GAZP', ltp: 280.34, chg: -0.5, high: 285.0, low: 278.0 }, ] }; } private createRussiaSectors(): Sector[] { return [ { name: 'Energy', picks: ['GAZP', 'LKOH'] }, { name: 'Banks', picks: ['SBER', 'VTBR'] }, ]; } private applySelection(country: string): void { const sel = (country || '').toLowerCase().trim(); if (!sel) { this.globalIndices = []; return; } if (sel === 'india') { // Prefer prioritized live India indices; fallback to demo if none const built = this.buildCountryIndices('india'); if (built && built.length) this.indiaIndices = built; else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices(); this.globalIndices = []; // Restore India companies/sectors and reset active index so UI reflects India after switching back this.companiesByIndex = this.createIndiaCompaniesByIndex(); this.sectors = [ { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] }, { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] }, { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] }, { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&M', 'TVSMOTOR'] }, { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] }, { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] }, ]; const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } // update derived lists this.updateGainersLosers(); return; } // try to find live entries in cache const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india'); if (matched && matched.length) { this.globalIndices = matched; // Even when we have live index entries we may still want to populate demo companies/sectors // for certain countries so the "Companies by Index", sector picks and toppers/losers update. if (sel === 'us') { this.companiesByIndex = this.createUSCompaniesByIndex(); this.sectors = this.createUSSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'uk') { this.companiesByIndex = this.createUKCompaniesByIndex(); this.sectors = this.createUKSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'german' || sel === 'germany') { this.companiesByIndex = this.createGermanCompaniesByIndex(); this.sectors = this.createGermanSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'sweden') { this.companiesByIndex = this.createSwedenCompaniesByIndex(); this.sectors = this.createSwedenSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'russia') { this.companiesByIndex = this.createRussiaCompaniesByIndex(); this.sectors = this.createRussiaSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } // For live data we don't have a mapping of companies per index; keep existing companiesByIndex return; } // no live data for this country — create demo indices for known countries so UI shows names immediately let demo: GlobalIndex[] = []; if (sel === 'us') demo = this.createUSIndices(); else if (sel === 'uk') demo = this.createUKIndices(); else if (sel === 'german') demo = this.createGermanIndices(); else if (sel === 'sweden') demo = this.createSwedenIndices(); else if (sel === 'russia') demo = this.createRussiaIndices(); this.globalIndices = demo; // merge demo into cache to improve subsequent lookups if (demo && demo.length) { const byId = new Map(); [...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i)); this.allGlobalIndices = Array.from(byId.values()); } // --- NEW: populate demo companies & sectors for this country so related cards update --- if (sel === 'us') { this.companiesByIndex = this.createUSCompaniesByIndex(); this.sectors = this.createUSSectors(); // reset active index to first available and refresh chart/quotes const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } // try to fetch live quotes for US symbols (backend may support them) this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } // update gainers/losers immediately from demo data this.updateGainersLosers(); } else if (sel === 'uk') { this.companiesByIndex = this.createUKCompaniesByIndex(); this.sectors = this.createUKSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'german' || sel === 'germany') { this.companiesByIndex = this.createGermanCompaniesByIndex(); this.sectors = this.createGermanSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'sweden') { this.companiesByIndex = this.createSwedenCompaniesByIndex(); this.sectors = this.createSwedenSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } else if (sel === 'russia') { this.companiesByIndex = this.createRussiaCompaniesByIndex(); this.sectors = this.createRussiaSectors(); const firstKey = Object.keys(this.companiesByIndex)[0]; if (firstKey) { this.activeIndex = firstKey; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.renderAreaChart(first.sym, first.ltp || 100); } this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ }); } this.updateGainersLosers(); } } // New: load full constituents from backend for active index (uses /getcompanies?code=) public async loadAllConstituents(): Promise { try { const code = this.activeIndex || this.indexCodes[0]; if (!code) return; const resp: any = await lastValueFrom(this.market.getConstituents(code)); if (!resp) return; // backend payload uses 'constituents' array of {symbol, company} const rows = resp.constituents || resp.results || resp.data || []; if (!Array.isArray(rows) || rows.length === 0) return; // convert to Company[] using yfinance symbol mapping for India (append .NS) const companies: Company[] = rows.map((r: any) => { const s = (r.symbol || r.Symbol || r.symbol || '').toString().trim(); const sym = s || (r.company || r.Company || '').toString().trim(); // turn into NSE ticker if needed const isIndia = (code || '').toLowerCase().includes('nifty') || (code || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india'; let symT = sym; if (isIndia && sym && !sym.includes('.') && !sym.includes(':')) symT = `${sym}.NS`; return { sym: symT.replace('.NS', ''), ltp: 0, chg: 0, high: 0, low: 0 } as Company; }); // store under active code name (keep original key format) this.companiesByIndex[code] = companies; // fetch live quotes for new list await this.fetchLiveQuotesForActiveIndex(); this.updateGainersLosers(); } catch (e) { console.warn('loadAllConstituents failed', e); } } // Ensure selectedCompany is valid for current activeIndex and draw chart synchronously private ensureSelectedCompanyAndDraw(): void { try { const keys = Object.keys(this.companiesByIndex || {}); if (keys.length === 0) return; if (!keys.includes(this.activeIndex)) this.activeIndex = keys[0]; const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.chartSymbol = first.sym; // Draw chart synchronously to ensure UI reflects selection immediately // Use a short timeout so that template updates (table) have rendered setTimeout(() => { this.renderAreaChart(first.sym, first.ltp); }, 20); } } catch (e) { // ignore } } // Fetch live quotes for companies in the active index and update table private async fetchLiveQuotesForActiveIndex(): Promise { // show spinner in table this.quotesLoading = true; try { const tickers = this.buildTickersForIndex(this.activeIndex || ''); if (!tickers || tickers.length === 0) { this.quotesLoading = false; return; } // helper: sleep const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); // helper: fetch with retries + exponential backoff const fetchWithRetries = async (batch: string[], maxRetries = 3, baseDelay = 500): Promise => { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const obs = this.market.getQuotes(batch); const res = await lastValueFrom(obs); // treat empty result as failure (backend may return [] on error) if (Array.isArray(res) && res.length > 0) return res; // if got empty but this was final attempt, consider failure } catch (err) { // fall through to retry console.warn('fetchWithRetry attempts Gi failed', err); } const delay = baseDelay * Math.pow(2, attempt); await sleep(delay); } return null; }; // batch tickers to avoid very long query strings; adjust size as needed const batchSize = 10; const batches: string[][] = []; for (let i = 0; i < tickers.length; i += batchSize) { batches.push(tickers.slice(i, i + batchSize)); } const allQuotes: any[] = []; for (const b of batches) { const batchRes = await fetchWithRetries(b, 3, 400); if (batchRes === null) { // At least one batch failed after retries – fallback to demo ticks console.warn('Quotes batch failed after retries, falling back to demo ticks'); this.quotesLoading = false; // Apply one demo tick step immediately so UI updates this.tickCompanies(); this.updateGainersLosers(); return; } allQuotes.push(...batchRes); } // If we reach here, we have fetched quotes for all batches const list = this.companiesByIndex[this.activeIndex] || []; const byReq = new Map(); allQuotes.forEach((q: any) => { if (!q) return; const s = (q.symbol || '').toString(); byReq.set(s.toUpperCase(), q); }); list.forEach((c) => { const reqSym = (this.selectedCountry.toLowerCase() === 'india' && !c.sym.includes('.') && !c.sym.includes(':')) ? `${c.sym}.NS`.toUpperCase() : c.sym.toUpperCase(); const q = byReq.get(reqSym) || byReq.get(c.sym.toUpperCase()); if (!q) return; if (q.price !== undefined && q.price !== null) c.ltp = Number(q.price); // backend provides chgPct as percentage; use it if present if (q.chgPct !== undefined && q.chgPct !== null) c.chg = Number(q.chgPct); else if (q.chg !== undefined && q.chg !== null && c.ltp) { // approximate percent from absolute change const prev = c.ltp - Number(q.chg); if (prev && !Number.isNaN(prev)) c.chg = (Number(q.chg) / prev) * 100; } if (q.high !== undefined && q.high !== null) c.high = Number(q.high); if (q.low !== undefined && q.low !== null) c.low = Number(q.low); }); this.updateGainersLosers(); this.lastUpdated = new Date().toLocaleString(); // persist updated quotes so reload shows latest values immediately try { const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated }; localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot)); } catch { /* ignore */ } } catch (e) { console.warn('fetchLiveQuotesForActiveIndex unexpected error', e); // fallback to demo ticks this.tickCompanies(); this.updateGainersLosers(); } finally { this.quotesLoading = false; } } // Build yfinance-compatible tickers for a company symbol depending on country private buildTickersForIndex(indexCode: string): string[] { const list = this.companiesByIndex[indexCode] || []; const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india'; return list.map(c => { const sym = (c.sym || '').trim(); if (!sym) return ''; // For India, append NSE suffix if not already present if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`; return sym; }).filter(Boolean); } // Create demo India indices with sparklines and miniPath for UI when live data not available private populateDefaultIndiaIndices(): void { const now = Date.now(); const items: GlobalIndex[] = this.defaultIndiaIndexNames.map((name: string, i: number) => { const base = Math.round(20000 + i * 1000 + (Math.random() - 0.5) * 2000); const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2)); const changePct = Number((change / base * 100).toFixed(2)); const spark = this.series(base, 26); const gi: GlobalIndex = { id: `${name.replace(/\s+/g, '_')}_${i}`, name, country: 'India', region: 'India', price: base, change, changePct, sparkline: spark, } as GlobalIndex; gi.miniPath = this.sparkPath(gi.sparkline || []); gi.isUp = (gi.change || 0) >= 0; return gi; }); this.indiaIndices = items; // also ensure cache contains these so other logic can find India entries const byId = new Map(); [...this.allGlobalIndices, ...items].forEach(i => byId.set(i.id, i)); this.allGlobalIndices = Array.from(byId.values()); } private countryMatches(selected: string, actual: string): boolean { if (!selected || !actual) return false; if (selected === actual) return true; // check aliases for selected label const aliases = this.countryAliases[selected]; if (aliases) { for (const a of aliases) { if (actual.includes(a)) return true; } } // check if actual has a known alias that matches selected const key = Object.keys(this.countryAliases).find(k => this.countryAliases[k].some((a: string) => actual.includes(a))); if (key && key === selected) return true; // fallback: partial match return actual.includes(selected) || selected.includes(actual); } // Clock private setClock(): void { this.lastUpdated = new Date().toLocaleString(); } // Indices sparkline generation private series(base: number, pts = 26): number[] { const arr = [base]; for (let i = 1; i < pts; i++) { const step = (Math.random() - 0.5) * (base * 0.002); // ±0.2% arr.push(Math.max(1, arr[i - 1] + step)); } return arr; } private sparkPath(values: number[], w = 240, h = 28, pad = 2): string { if (!values || values.length === 0) return ''; const max = Math.max(...values); const min = Math.min(...values); const norm = (v: number) => h - pad - ((v - min) / Math.max(1e-6, max - min)) * (h - 2 * pad); const step = (w - 2 * pad) / (values.length - 1 || 1); let d = ''; values.forEach((v, i) => { const x = pad + i * step; const y = norm(v); d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`; }); return d; } private refreshIndexSpark(idx: IndexItem): void { const vals = this.series(idx.price, 26); idx.isUp = vals[vals.length - 1] >= vals[0]; idx.miniPath = this.sparkPath(vals); } private tickIndices(): void { this.indices.forEach((idx) => { const delta = (Math.random() - 0.5) * (idx.price * 0.0008); idx.price = Math.max(1, idx.price + delta); idx.chg += (Math.random() - 0.5) * 0.04; // drift this.refreshIndexSpark(idx); }); } // Companies random walk tick private tickCompanies(): void { Object.values(this.companiesByIndex).forEach((list) => { list.forEach((c) => { const base = c.ltp; const move = (Math.random() - 0.5) * (base * 0.0025); // ±0.25% c.ltp = Math.max(1, base + move); const ref = base / (1 + c.chg / 100); c.chg = ((c.ltp - ref) / ref) * 100; c.high = Math.max(c.high, c.ltp); c.low = Math.min(c.low, c.ltp); }); }); } private flattenCompanies(): Company[] { return Object.values(this.companiesByIndex).flat().map((c) => ({ ...c })); } private updateGainersLosers(): void { const all = this.flattenCompanies(); const sorted = [...all].sort((a, b) => b.chg - a.chg); this.gainers = sorted.slice(0, 5); this.losers = sorted.slice(-5).reverse(); } // Chart private makeIntraday(base = 100, points = 90): number[] { const arr = [base]; for (let i = 1; i < points; i++) { const step = (Math.random() - 0.5) * (base * 0.004); arr.push(Math.max(1, arr[i - 1] + step)); } return arr; } // Compatibility wrapper for template calls — delegate to SVG renderer public drawTodayChart(symbol = '—', base = 100): void { this.renderAreaChart(symbol, base); } // Render an SVG area chart into the chart container. If `series` is provided it is used // as the plotted points (array of numbers). Otherwise a simulated series is generated. private renderAreaChart(symbol: string, base = 100, series?: number[]): void { const container = this.chartContainerRef?.nativeElement; if (!container) return; // ensure chartSymbol reflects what we're drawing this.chartSymbol = symbol; // clear container.innerHTML = ''; // use device pixel width for responsiveness const rect = container.getBoundingClientRect(); const width = Math.max(600, Math.round(rect.width || 700)); const height = 260; const pts = series && series.length ? series : this.makeIntraday(base, 90); const padLeft = 40; const padRight = 40; const padTop = 16; const padBottom = 36; const min = Math.min(...pts); const max = Math.max(...pts); const toX = (i: number) => padLeft + (i * (width - padLeft - padRight)) / (pts.length - 1 || 1); const toY = (v: number) => padTop + ((max - v) / Math.max(1e-6, max - min)) * (height - padTop - padBottom); // SVG root const ns = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(ns, 'svg'); svg.setAttribute('width', String(width)); svg.setAttribute('height', String(height)); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.style.width = '100%'; svg.style.height = '260px'; svg.style.display = 'block'; // grid lines for (let g = 0; g < 4; g++) { const y = padTop + (g * (height - padTop - padBottom)) / 3; const line = document.createElementNS(ns, 'line'); line.setAttribute('x1', String(padLeft)); line.setAttribute('y1', String(y)); line.setAttribute('x2', String(width - padRight)); line.setAttribute('y2', String(y)); line.setAttribute('stroke', '#e6ecff'); line.setAttribute('stroke-opacity', '0.06'); line.setAttribute('stroke-width', '1'); svg.appendChild(line); } // build path string let d = ''; let area = ''; pts.forEach((v, i) => { const x = toX(i); const y = toY(v); if (i === 0) { d += `M ${x} ${y}`; area += `M ${x} ${y}`; } else { d += ` L ${x} ${y}`; area += ` L ${x} ${y}`; } }); // close area to baseline const lastX = toX(pts.length - 1); area += ` L ${lastX} ${height - padBottom} L ${padLeft} ${height - padBottom} Z`; // area fill const areaPath = document.createElementNS(ns, 'path'); areaPath.setAttribute('d', area); areaPath.setAttribute('fill', '#0f9d58'); areaPath.setAttribute('fill-opacity', '0.08'); svg.appendChild(areaPath); // line path const path = document.createElementNS(ns, 'path'); path.setAttribute('d', d); path.setAttribute('fill', 'none'); path.setAttribute('stroke', '#0f9d58'); path.setAttribute('stroke-width', '2'); path.setAttribute('stroke-linejoin', 'round'); path.setAttribute('stroke-linecap', 'round'); svg.appendChild(path); // last point marker const lastYVal = pts[pts.length - 1]; const lastCx = toX(pts.length - 1); const lastCy = toY(lastYVal); const circ = document.createElementNS(ns, 'circle'); circ.setAttribute('cx', String(lastCx)); circ.setAttribute('cy', String(lastCy)); circ.setAttribute('r', '4'); circ.setAttribute('fill', '#0f9d58'); svg.appendChild(circ); // y-axis labels (right side) const labelRight = (val: number, y: number) => { const txt = document.createElementNS(ns, 'text'); txt.setAttribute('x', String(width - padRight + 8)); txt.setAttribute('y', String(y + 4)); txt.setAttribute('fill', '#fff'); txt.setAttribute('font-size', '12'); txt.setAttribute('opacity', '0.9'); txt.textContent = val.toFixed(2); svg.appendChild(txt); }; labelRight(max, toY(max)); labelRight(min, toY(min)); // x-axis labels (bottom) const ticks = [0, Math.floor(pts.length / 4), Math.floor(pts.length / 2), Math.floor((3 * pts.length) / 4), pts.length - 1]; ticks.forEach((ti) => { const x = toX(ti); const txt = document.createElementNS(ns, 'text'); txt.setAttribute('x', String(x)); txt.setAttribute('y', String(height - 8)); txt.setAttribute('fill', '#999'); txt.setAttribute('font-size', '11'); txt.setAttribute('text-anchor', 'middle'); // fake times: show HH:MM using index const minutes = (ti * 5) % 60; const hour = 9 + Math.floor(ti / 12); txt.textContent = `${hour}:${String(minutes).padStart(2, '0')} AM`; svg.appendChild(txt); }); container.appendChild(svg); // expose small tooltip on hover (basic) svg.addEventListener('mousemove', (ev) => { const rect = svg.getBoundingClientRect(); const x = ev.clientX - rect.left; // find closest index let closest = 0; let bestDist = Infinity; for (let i = 0; i < pts.length; i++) { const dx = Math.abs(toX(i) - x); if (dx < bestDist) { bestDist = dx; closest = i; } } // simple tooltip: set title on container const v = pts[closest]; svg.setAttribute('title', `Index: ${symbol}\nValue: ${v.toFixed(2)}`); }); } // Fetch intraday series for symbol and draw chart. Falls back to simulated when live data present private async fetchAndDrawChart(sym: string, base = 100): Promise { try { // try backend intraday endpoint (via MarketService.getIntraday) const obs = this.market.getIntraday(sym, '1d', '1m'); const resp: any = await lastValueFrom(obs); // expected resp: { symbol, timestamps: string[], closes: number[] } if (resp && Array.isArray(resp.closes) && resp.closes.length > 0) { this.chartSymbol = sym; this.drawSeriesChart(resp.closes, sym); return; } } catch (e) { // ignore and fallback } // fallback: simulate intraday this.chartSymbol = sym; this.renderAreaChart(sym, base); } // Draw given numeric series on canvas (replaces simulated draw when live data present) private drawSeriesChart(values: number[], symbol = '—'): void { this.chartSymbol = symbol; const canvas = this.canvasRef?.nativeElement; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const w = canvas.width; const h = canvas.height; ctx.clearRect(0, 0, w, h); const min = Math.min(...values); const max = Math.max(...values); const padX = 28; const padY = 16; const toX = (i: number) => padX + (i * (w - padX * 2)) / (values.length - 1 || 1); const toY = (v: number) => h - padY - ((v - min) / Math.max(1e-6, max - min)) * (h - padY * 2); // grid ctx.globalAlpha = 0.4; ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1; for (let g = 0; g < 4; g++) { const gy = padY + (g * (h - padY * 2)) / 3; ctx.beginPath(); ctx.moveTo(padX, gy); ctx.lineTo(w - padX, gy); ctx.stroke(); } ctx.globalAlpha = 1; const up = values[values.length - 1] >= values[0]; const styles = getComputedStyle(this.host.nativeElement); const stroke = styles.getPropertyValue(up ? '--up' : '--down').trim() || (up ? '#12c48b' : '#ff5b6b'); ctx.strokeStyle = stroke; ctx.lineWidth = 2; ctx.beginPath(); values.forEach((v, i) => { const x = toX(i); const y = toY(v); ctx.lineTo(x, y); }); ctx.stroke(); // last price marker const lastX = toX(values.length - 1); const lastY = toY(values[values.length - 1]); ctx.fillStyle = stroke; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill(); } public selectCountry(country: string): void { this.selectedCountry = country; // always apply selection immediately so UI updates synchronously this.applySelection(country); // if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives) if (!this.globalFetched) this.fetchGlobalIndices(); // Ensure activeIndex points to a valid index for the newly selected companies list const keys = Object.keys(this.companiesByIndex || {}); if (keys.length) { if (!keys.includes(this.activeIndex)) { this.activeIndex = keys[0]; } const first = this.companiesByIndex[this.activeIndex]?.[0]; if (first) { this.selectedCompany = first.sym; this.chartSymbol = first.sym; // draw chart for the first company (prefer live intraday) this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { this.renderAreaChart(first.sym, first.ltp); }); return; } } // If no company available for activeIndex, try to keep current selectedCompany if present if (this.selectedCompany) { const comp = this.findCompanyBySymbol(this.selectedCompany); const base = comp?.ltp ?? 100; this.chartSymbol = this.selectedCompany; this.fetchAndDrawChart(this.selectedCompany, base).catch(() => { this.renderAreaChart(this.selectedCompany as string, base); }); } } // expose as a public property (arrow) so Angular template type-checker recognizes it // (removed duplicate - use the method defined later in file) // Handle company row / button clicks from template public onCompanyClick(e: Event, c: Company): void { if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (!c) return; this.selectedCompany = c.sym; // try to fetch live intraday series and draw; fallback to simulated when not available this.fetchAndDrawChart(c.sym, c.ltp).catch(() => { this.renderAreaChart(c.sym, c.ltp); }); } }