fix: login mutex, zero-qty realised PnL, market cache-on-error, UI preserve cards

This commit is contained in:
Manohar 2026-05-12 04:23:50 +00:00
parent 882d55adad
commit 6f06755e11
4 changed files with 18 additions and 10 deletions

View file

@ -6,6 +6,7 @@ import { AngelAuthResponse } from './types.js';
let jwtToken: string | null = null; let jwtToken: string | null = null;
let refreshToken: string | null = null; let refreshToken: string | null = null;
let tokenExpiry: Date | null = null; let tokenExpiry: Date | null = null;
let loginMutex: Promise<void> | null = null; // prevents concurrent logins
const BASE_URL = 'https://apiconnect.angelbroking.com'; const BASE_URL = 'https://apiconnect.angelbroking.com';
@ -87,9 +88,12 @@ export async function login(): Promise<void> {
export async function getToken(): Promise<string> { export async function getToken(): Promise<string> {
const now = new Date(); const now = new Date();
// No token yet, or past expiry
if (!jwtToken || !tokenExpiry || now >= tokenExpiry) { if (!jwtToken || !tokenExpiry || now >= tokenExpiry) {
await login(); // Mutex: wait for in-progress login instead of starting a second one
if (!loginMutex) {
loginMutex = login().finally(() => { loginMutex = null; });
}
await loginMutex;
} }
return jwtToken!; return jwtToken!;

View file

@ -54,8 +54,9 @@ export async function fetchHoldings(): Promise<AngelHolding[]> {
*/ */
function normalisePosition(p: AngelPosition): Position | null { function normalisePosition(p: AngelPosition): Position | null {
const netqty = parseFloat(p.netqty); const netqty = parseFloat(p.netqty);
// Skip positions with zero net qty (fully closed intraday) const realisedPnl = parseFloat(p.realised) || 0;
if (netqty === 0) return null; // Only skip if qty=0 AND no realised PnL (truly empty row)
if (netqty === 0 && realisedPnl === 0) return null;
const ltp = parseFloat(p.ltp) || 0; const ltp = parseFloat(p.ltp) || 0;
const unrealised = parseFloat(p.unrealised) || 0; const unrealised = parseFloat(p.unrealised) || 0;

View file

@ -93,13 +93,14 @@ export async function fetchMarketData(): Promise<MarketQuote[]> {
try { try {
const live = await fetchFromAngel(); const live = await fetchFromAngel();
if (live.length > 0) { if (live.length > 0) {
saveToCache(live); // update cache whenever we get fresh data saveToCache(live);
return live; return live;
} }
// Empty from Angel (market closed) — serve cache
return loadFromCache();
} catch (err) { } catch (err) {
console.error('[market] Angel fetch error:', err instanceof Error ? err.message : err); // Auth/network error — always serve last known cache so UI never goes blank
console.error('[market] fetch error, serving cache:', err instanceof Error ? err.message : err);
return loadFromCache();
} }
// Market closed or error — serve cached data
const cached = loadFromCache();
return cached; // empty array if never cached (first run before market open)
} }

View file

@ -109,7 +109,9 @@ async function evaluateAlerts(positions: Position[], today: string): Promise<voi
@delta_abs, @delta_pct, @direction, @ltp, @netqty, datetime('now')) @delta_abs, @delta_pct, @direction, @ltp, @netqty, datetime('now'))
`); `);
for (const pos of positions) { // Only evaluate alerts for positions that are still open (netqty != 0)
const openPositions = positions.filter(p => p.netqty !== 0);
for (const pos of openPositions) {
let state = getBandState.get(pos.key) as BandState | undefined; let state = getBandState.get(pos.key) as BandState | undefined;
// First time seeing this position, or new trading day → initialise anchor // First time seeing this position, or new trading day → initialise anchor