From 5bdf7fbffec34d6aabc4c0dff41184f6cb4b9b41 Mon Sep 17 00:00:00 2001 From: Manohar Date: Sat, 9 May 2026 03:50:11 +0000 Subject: [PATCH] fix: market cache in SQLite for after-hours; collapsible sections with localStorage state --- public/index.html | 28 ++++++++-- src/angel/market.ts | 130 +++++++++++++++++++------------------------- src/db/client.ts | 11 ++++ 3 files changed, 91 insertions(+), 78 deletions(-) diff --git a/public/index.html b/public/index.html index adfc2f3..9dd780f 100644 --- a/public/index.html +++ b/public/index.html @@ -85,7 +85,13 @@ .btn-save{padding:8px 22px;border-radius:9px;background:var(--coral);border:none;color:#fff;font-size:.78rem;font-weight:600;cursor:pointer;transition:all var(--tr)} .btn-save:hover{background:#e55a3a;transform:translateY(-1px)} .save-msg{font-size:.72rem;color:var(--green);display:none;margin-left:10px} - + + .collapsible .card-head{cursor:pointer;user-select:none} + .collapsible .card-head::after{content:"\u2303";font-size:.8rem;color:var(--text3);transition:transform .2s;margin-left:8px} + .collapsible.collapsed .card-head::after{transform:rotate(180deg)} + .collapse-body{overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1);max-height:4000px} + .collapsible.collapsed .collapse-body{max-height:0} +
@@ -130,11 +136,11 @@
-
No history yet — populates after first poll cycle
+
No history yet — populates after first poll cycle
Option Positions
Live
- +
@@ -142,7 +148,7 @@
Alert History
-
SymbolQtyLTPAvg CostUnrealisedRealisedTotal P&L
Loading…
+
SymbolP&L at AlertAnchorMoveLTPTime (IST)
No alerts yet
@@ -152,7 +158,7 @@ Alert Settings Changes apply immediately
-
+
Alert when P&L moves this % from anchor
Guard for near-zero P&L positions
@@ -253,6 +259,18 @@ async function saveOverride(key){ await fetch('/api/config/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alert_threshold_pct:pct?parseFloat(pct):null,muted_until:mute?new Date(mute).toISOString():null})}); } async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket()]);} + +function toggleCard(id){ + const el=document.getElementById(id); + el.classList.toggle('collapsed'); + const s=JSON.parse(localStorage.getItem('collapse')||'{}'); + s[id]=el.classList.contains('collapsed'); + localStorage.setItem('collapse',JSON.stringify(s)); +} +(function(){ + const s=JSON.parse(localStorage.getItem('collapse')||'{}'); + Object.entries(s).forEach(([id,c])=>{if(c)document.getElementById(id)?.classList.add('collapsed')}); +})(); loadConfig();refresh(); setInterval(refresh,60000); setInterval(loadMarket,15000); diff --git a/src/angel/market.ts b/src/angel/market.ts index 304739e..68882e9 100644 --- a/src/angel/market.ts +++ b/src/angel/market.ts @@ -1,81 +1,86 @@ import axios from 'axios'; import { getAuthHeaders } from './auth.js'; +import { db } from '../db/client.js'; const BASE = 'https://apiconnect.angelbroking.com'; export interface MarketQuote { - key: string; - label: string; - price: number; - change: number; - changePct: number; - unit: string; - stale: boolean; // true = after-hours data from Yahoo fallback + key: string; label: string; price: number; + change: number; changePct: number; unit: string; + stale: boolean; cachedAt?: string; } -// Fixed Angel tokens for major indices const INDEX_TOKENS = [ + { key: 'SENSEX', exchange: 'BSE', token: '1', label: 'SENSEX', unit: 'pts' }, { key: 'NIFTY50', exchange: 'NSE', token: '26000', label: 'NIFTY 50', unit: 'pts' }, { key: 'BANKNIFTY', exchange: 'NSE', token: '26009', label: 'BANK NIFTY', unit: 'pts' }, { key: 'INDIAVIX', exchange: 'NSE', token: '26017', label: 'INDIA VIX', unit: '%' }, - { key: 'SENSEX', exchange: 'BSE', token: '1', label: 'SENSEX', unit: 'pts' }, ]; -// Yahoo Finance symbols for fallback (after hours / Angel empty) -const YAHOO_SYMBOLS: Record = { - NIFTY50: { label: 'NIFTY 50', unit: 'pts', yahooSym: '^NSEI' }, - BANKNIFTY: { label: 'BANK NIFTY', unit: 'pts', yahooSym: '^NSEBANK' }, - INDIAVIX: { label: 'INDIA VIX', unit: '%', yahooSym: '^INDIAVIX' }, - SENSEX: { label: 'SENSEX', unit: 'pts', yahooSym: '^BSESN' }, - CRUDEOIL: { label: 'Crude Oil', unit: '\u20b9/bbl', yahooSym: 'CL=F' }, - USDINR: { label: 'INR/USD', unit: '\u20b9', yahooSym: 'USDINR=X' }, -}; +async function searchToken(exchange: string, query: string): Promise { + try { + const headers = await getAuthHeaders(); + const r = await axios.post(BASE + '/rest/secure/angelbroking/order/v1/searchScrip', + { exchange, searchscrip: query }, { headers, timeout: 5000 }); + return r.data?.data?.[0]?.symboltoken ?? null; + } catch { return null; } +} + +function saveToCache(quotes: MarketQuote[]): void { + const upsert = db.prepare(`INSERT INTO market_cache (key,label,price,change_val,change_pct,unit,cached_at) + VALUES (@key,@label,@price,@change_val,@change_pct,@unit,datetime('now')) + ON CONFLICT(key) DO UPDATE SET label=excluded.label,price=excluded.price, + change_val=excluded.change_val,change_pct=excluded.change_pct,unit=excluded.unit,cached_at=excluded.cached_at`); + for (const q of quotes) { + upsert.run({ key: q.key, label: q.label, price: q.price, + change_val: q.change, change_pct: q.changePct, unit: q.unit }); + } +} + +function loadFromCache(): MarketQuote[] { + const rows = db.prepare('SELECT * FROM market_cache ORDER BY key').all() as any[]; + return rows.map(r => ({ + key: r.key, label: r.label, price: r.price, + change: r.change_val, changePct: r.change_pct, unit: r.unit, + stale: true, cachedAt: r.cached_at, + })); +} async function fetchFromAngel(): Promise { const headers = await getAuthHeaders(); - // Step 1: search for front-month crude and INR/USD tokens - async function search(exchange: string, query: string): Promise { - try { - const r = await axios.post( - BASE + '/rest/secure/angelbroking/order/v1/searchScrip', - { exchange, searchscrip: query }, - { headers, timeout: 5000 } - ); - return r.data?.data?.[0]?.symboltoken ?? null; - } catch { return null; } - } - const [crudeToken, usdinrToken] = await Promise.all([ - search('MCX', 'CRUDEOIL'), - search('CDS', 'USDINR'), + searchToken('MCX', 'CRUDEOIL'), + searchToken('CDS', 'USDINR'), ]); - // Step 2: build token map const tokenMap: Record = { NSE: [], BSE: [] }; const tokenIndex: Record = {}; - for (const idx of INDEX_TOKENS) { - tokenMap[idx.exchange] = tokenMap[idx.exchange] || []; tokenMap[idx.exchange].push(idx.token); tokenIndex[idx.token] = idx.key; } if (crudeToken) { tokenMap['MCX'] = [crudeToken]; tokenIndex[crudeToken] = 'CRUDEOIL'; } if (usdinrToken) { tokenMap['CDS'] = [usdinrToken]; tokenIndex[usdinrToken] = 'USDINR'; } - // Step 3: fetch quotes - const r = await axios.post( - BASE + '/rest/secure/angelbroking/market/v1/quote/', - { mode: 'FULL', exchangeTokens: tokenMap }, - { headers, timeout: 8000 } - ); + const r = await axios.post(BASE + '/rest/secure/angelbroking/market/v1/quote/', + { mode: 'FULL', exchangeTokens: tokenMap }, { headers, timeout: 8000 }); const fetched: any[] = r.data?.data?.fetched ?? []; if (!fetched.length) return []; + const LABELS: Record = { + SENSEX: { label: 'SENSEX', unit: 'pts' }, + NIFTY50: { label: 'NIFTY 50', unit: 'pts' }, + BANKNIFTY: { label: 'BANK NIFTY', unit: 'pts' }, + INDIAVIX: { label: 'INDIA VIX', unit: '%' }, + CRUDEOIL: { label: 'Crude Oil', unit: '\u20b9/bbl' }, + USDINR: { label: 'INR/USD', unit: '\u20b9' }, + }; + return fetched.map((q: any) => { - const key = tokenIndex[q.symbolToken] ?? q.tradingSymbol; - const meta = YAHOO_SYMBOLS[key] ?? { label: q.tradingSymbol, unit: '' }; + const key = tokenIndex[q.symbolToken] ?? q.tradingSymbol; + const meta = LABELS[key] ?? { label: q.tradingSymbol, unit: '' }; const ltp = parseFloat(q.ltp) || 0; const close = parseFloat(q.close) || 0; const change = parseFloat(q.netChange) || (ltp - close); @@ -84,38 +89,17 @@ async function fetchFromAngel(): Promise { }); } -async function fetchFromYahoo(): Promise { - const syms = Object.entries(YAHOO_SYMBOLS).map(([k, v]) => v.yahooSym).join(','); - const url = 'https://query1.finance.yahoo.com/v7/finance/quote?symbols=' + syms; - const r = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 8000 }); - const quotes: any[] = r.data?.quoteResponse?.result ?? []; - - const yahooKeyMap: Record = {}; - for (const [k, v] of Object.entries(YAHOO_SYMBOLS)) yahooKeyMap[v.yahooSym] = k; - - return quotes.map((q: any) => { - const key = yahooKeyMap[q.symbol] ?? q.symbol; - const meta = YAHOO_SYMBOLS[key] ?? { label: q.shortName ?? key, unit: '' }; - return { - key, label: meta.label, - price: q.regularMarketPrice ?? 0, - change: q.regularMarketChange ?? 0, - changePct: q.regularMarketChangePercent ?? 0, - unit: meta.unit, - stale: true, // Yahoo = previous session close after hours - }; - }); -} - export async function fetchMarketData(): Promise { try { - const angelData = await fetchFromAngel(); - if (angelData.length > 0) return angelData; - // Angel returned empty (market closed) — fall back to Yahoo - return await fetchFromYahoo(); + const live = await fetchFromAngel(); + if (live.length > 0) { + saveToCache(live); // update cache whenever we get fresh data + return live; + } } catch (err) { - // Angel auth/network error — try Yahoo - try { return await fetchFromYahoo(); } - catch { return []; } + console.error('[market] Angel fetch error:', err instanceof Error ? err.message : err); } + // Market closed or error — serve cached data + const cached = loadFromCache(); + return cached; // empty array if never cached (first run before market open) } diff --git a/src/db/client.ts b/src/db/client.ts index 1157407..7512d10 100644 --- a/src/db/client.ts +++ b/src/db/client.ts @@ -78,6 +78,17 @@ export function initDb(): void { -- Indexes CREATE INDEX IF NOT EXISTS idx_alerts_position ON alerts(position_key); CREATE INDEX IF NOT EXISTS idx_alerts_alerted_at ON alerts(alerted_at DESC); + + -- Market data cache (persists last known values for after-hours display) + CREATE TABLE IF NOT EXISTS market_cache ( + key TEXT PRIMARY KEY, + label TEXT NOT NULL, + price REAL NOT NULL, + change_val REAL NOT NULL, + change_pct REAL NOT NULL, + unit TEXT NOT NULL, + cached_at TEXT NOT NULL + ); CREATE INDEX IF NOT EXISTS idx_positions_closed ON positions(is_closed); -- Portfolio-level P&L snapshots (recorded every poll tick)