Persist daily P&L beyond 28-day Tradovate window
Adds a daily_pnl SQLite table that accumulates trade history indefinitely. On each hourly fetch, fresh API data is upserted (not replaced) so entries older than Tradovate's 28-day limit are preserved. Cache is loaded at startup and used as fallback when both report requests fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
131c9bca9f
commit
6b299f5359
@@ -201,6 +201,32 @@ export function setInstrumentEnabled(symbol: string, enabled: boolean): boolean
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
// ── Daily P&L Cache ──────────────────────────────────────────────────────────
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS daily_pnl (
|
||||
account_id INTEGER NOT NULL,
|
||||
account_name TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
pnl REAL NOT NULL,
|
||||
PRIMARY KEY (account_id, date)
|
||||
);
|
||||
`);
|
||||
|
||||
export function saveDailyPnL(accountId: number, accountName: string, entries: { date: string; pnl: number }[]): void {
|
||||
const ins = db.prepare('INSERT OR REPLACE INTO daily_pnl (account_id, account_name, date, pnl) VALUES (?, ?, ?, ?)');
|
||||
const txn = db.transaction(() => {
|
||||
for (const e of entries) {
|
||||
ins.run(accountId, accountName, e.date, e.pnl);
|
||||
}
|
||||
});
|
||||
txn();
|
||||
}
|
||||
|
||||
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 }[]);
|
||||
}
|
||||
|
||||
// ── Firm banned symbols ───────────────────────────────────────────────────────
|
||||
|
||||
db.exec(`
|
||||
|
||||
+41
-11
@@ -5,6 +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';
|
||||
|
||||
export class TradovateClient {
|
||||
private name: string;
|
||||
@@ -391,9 +392,28 @@ export class TradovateClient {
|
||||
return typeof reportData?.data === 'string' ? reportData.data : '[]';
|
||||
};
|
||||
|
||||
// Merge fresh API entries on top of cached historical entries (fresh takes precedence for overlapping dates)
|
||||
const mergePnL = (
|
||||
cached: { date: string; pnl: number }[],
|
||||
fresh: { date: string; pnl: number }[]
|
||||
): { date: string; pnl: number }[] => {
|
||||
const map = new Map<string, number>();
|
||||
for (const e of cached) map.set(e.date, e.pnl);
|
||||
for (const e of fresh) map.set(e.date, e.pnl);
|
||||
return Array.from(map.entries())
|
||||
.map(([date, pnl]) => ({ date, pnl }))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
};
|
||||
|
||||
for (const account of this.accountList) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// --- Try Cash History first (true after-fee daily P&L) ---
|
||||
let usedFallback = false;
|
||||
try {
|
||||
const raw = await requestReport('Cash History', account.name);
|
||||
const fixed = raw.replace(/"Date":\s*(\d{4}-\d{2}-\d{2})/g, '"Date": "$1"');
|
||||
@@ -408,16 +428,18 @@ export class TradovateClient {
|
||||
dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta;
|
||||
}
|
||||
|
||||
const entries = Object.entries(dailyMap)
|
||||
const fresh = Object.entries(dailyMap)
|
||||
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
this.dailyPnL[account.id] = entries;
|
||||
this.daysTraded[account.id] = entries.filter((d) => d.pnl !== 0).length;
|
||||
const merged = mergePnL(cached, fresh);
|
||||
this.dailyPnL[account.id] = merged;
|
||||
this.daysTraded[account.id] = merged.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;
|
||||
} catch {
|
||||
usedFallback = true;
|
||||
// fall through to Fills fallback
|
||||
}
|
||||
|
||||
// --- Fallback: Fills report + FIFO (slightly pre-fee, used for passed accounts) ---
|
||||
@@ -462,19 +484,27 @@ export class TradovateClient {
|
||||
}
|
||||
}
|
||||
|
||||
const entries = Object.entries(dailyMap)
|
||||
const fresh = Object.entries(dailyMap)
|
||||
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
this.dailyPnL[account.id] = entries;
|
||||
this.daysTraded[account.id] = entries.filter((d) => d.pnl !== 0).length;
|
||||
const merged = mergePnL(cached, fresh);
|
||||
this.dailyPnL[account.id] = merged;
|
||||
this.daysTraded[account.id] = merged.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);
|
||||
this.lastFetchErrors[account.name] = msg;
|
||||
this.dailyPnL[account.id] ??= [];
|
||||
this.daysTraded[account.id] ??= 0;
|
||||
// Cache already loaded 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}`);
|
||||
} else {
|
||||
this.dailyPnL[account.id] ??= [];
|
||||
this.daysTraded[account.id] ??= 0;
|
||||
this.lastFetchErrors[account.name] = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.fetchDaysComplete = true;
|
||||
|
||||
Reference in New Issue
Block a user