diff --git a/lib/db.ts b/lib/db.ts index d9fff5d..da2246c 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -227,6 +227,26 @@ export function loadDailyPnL(accountId: number): { date: string; pnl: number }[] return (db.prepare('SELECT date, pnl FROM daily_pnl WHERE account_id = ? ORDER BY date').all(accountId) as { date: string; pnl: number }[]); } +// ── Account Meta ───────────────────────────────────────────────────────────── + +db.exec(` + CREATE TABLE IF NOT EXISTS account_meta ( + account_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (account_id, key) + ); +`); + +export function saveAccountMeta(accountId: number, key: string, value: string): void { + db.prepare('INSERT OR REPLACE INTO account_meta (account_id, key, value) VALUES (?, ?, ?)').run(accountId, key, value); +} + +export function loadAccountMeta(accountId: number, key: string): string | null { + const row = db.prepare('SELECT value FROM account_meta WHERE account_id = ? AND key = ?').get(accountId, key) as { value: string } | undefined; + return row?.value ?? null; +} + // ── Firm banned symbols ─────────────────────────────────────────────────────── db.exec(` diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index 672fb32..cd2fa64 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -5,7 +5,7 @@ import type { AccountItem, AuthLoginResponse, Contract } from './tradovate-helpe import { computeSec, randomUUIDV4 } from './tradovate-helpers'; import { POINT_VALUES } from './trading-logic'; import { getCachedContract, resolveContracts } from './contract-resolver'; -import { saveDailyPnL, loadDailyPnL } from './db'; +import { saveDailyPnL, loadDailyPnL, saveAccountMeta, loadAccountMeta } from './db'; export class TradovateClient { private name: string; @@ -31,6 +31,8 @@ export class TradovateClient { public daysTraded: { [accountId: number]: number } = {}; public dailyPnL: { [accountId: number]: { date: string; pnl: number }[] } = {}; + /** Date of the last fund transaction per account — days traded are counted from this date onwards */ + public lastFundDates: { [accountId: number]: string | null } = {}; /** Balance floor set by the prop firm — account is blown when amount <= this value (0 = not set) */ public autoLiqThresholds: { [accountId: number]: number } = {}; @@ -406,11 +408,16 @@ export class TradovateClient { }; for (const account of this.accountList) { + // Load last known fund date from DB — used to filter days traded to the current challenge period + const storedFundDate = loadAccountMeta(account.id, 'last_fund_date'); + this.lastFundDates[account.id] = storedFundDate; + // Load cache first — serves as both the startup baseline and the fallback if API fails const cached = loadDailyPnL(account.id); if (cached.length > 0) { - this.dailyPnL[account.id] = cached; - this.daysTraded[account.id] = cached.filter((d) => d.pnl !== 0).length; + const active = storedFundDate ? cached.filter((d) => d.date >= storedFundDate) : cached; + this.dailyPnL[account.id] = active; + this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length; } // --- Try Cash History first (true after-fee daily P&L) --- @@ -420,21 +427,34 @@ export class TradovateClient { const rows: { Date: string; Delta: string; 'Cash Change Type': string; [k: string]: any }[] = JSON.parse(fixed); this.lastFetchRaw[account.name] = rows.length > 0 ? JSON.stringify(rows[0]) : '(empty)'; + const fundDates: string[] = []; const dailyMap: { [date: string]: number } = {}; for (const row of rows) { - if ((row['Cash Change Type'] ?? '').trim() === 'Fund Transaction') continue; + if ((row['Cash Change Type'] ?? '').trim() === 'Fund Transaction') { + fundDates.push(row['Date']); + continue; + } const delta = parseFloat((row['Delta'] ?? '0').replace(/,/g, '')); if (isNaN(delta)) continue; dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta; } + // Use the most recent fund transaction as the reset point — persist it so it survives beyond the 28-day window + const lastFundDate = fundDates.sort().pop() ?? null; + if (lastFundDate) { + saveAccountMeta(account.id, 'last_fund_date', lastFundDate); + this.lastFundDates[account.id] = lastFundDate; + } + const fundDate = this.lastFundDates[account.id]; + const fresh = Object.entries(dailyMap) .map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 })) .sort((a, b) => a.date.localeCompare(b.date)); const merged = mergePnL(cached, fresh); - this.dailyPnL[account.id] = merged; - this.daysTraded[account.id] = merged.filter((d) => d.pnl !== 0).length; + const active = fundDate ? merged.filter((d) => d.date >= fundDate) : merged; + this.dailyPnL[account.id] = active; + this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length; saveDailyPnL(account.id, account.name, fresh); // upsert only fresh entries — preserves older cached rows delete this.lastFetchErrors[account.name]; continue; @@ -484,19 +504,23 @@ export class TradovateClient { } } + // Fills report has no fund transaction data — use stored fund date + const fundDate = this.lastFundDates[account.id]; + const fresh = Object.entries(dailyMap) .map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 })) .sort((a, b) => a.date.localeCompare(b.date)); const merged = mergePnL(cached, fresh); - this.dailyPnL[account.id] = merged; - this.daysTraded[account.id] = merged.filter((d) => d.pnl !== 0).length; + const active = fundDate ? merged.filter((d) => d.date >= fundDate) : merged; + this.dailyPnL[account.id] = active; + this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length; saveDailyPnL(account.id, account.name, fresh); // upsert only fresh entries — preserves older cached rows this.lastFetchErrors[account.name] = 'cash history unavailable (using fills fallback)'; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg); - // Cache already loaded at top of loop — just log the error + // Cache already loaded and filtered at top of loop — just log the error if (cached.length > 0) { this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`; console.log(`[fetchDaysTraded] using ${cached.length} cached entries from DB for ${account.name}`);