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;
|
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 ───────────────────────────────────────────────────────
|
// ── Firm banned symbols ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
|
|||||||
+41
-11
@@ -5,6 +5,7 @@ import type { AccountItem, AuthLoginResponse, Contract } from './tradovate-helpe
|
|||||||
import { computeSec, randomUUIDV4 } from './tradovate-helpers';
|
import { computeSec, randomUUIDV4 } from './tradovate-helpers';
|
||||||
import { POINT_VALUES } from './trading-logic';
|
import { POINT_VALUES } from './trading-logic';
|
||||||
import { getCachedContract, resolveContracts } from './contract-resolver';
|
import { getCachedContract, resolveContracts } from './contract-resolver';
|
||||||
|
import { saveDailyPnL, loadDailyPnL } from './db';
|
||||||
|
|
||||||
export class TradovateClient {
|
export class TradovateClient {
|
||||||
private name: string;
|
private name: string;
|
||||||
@@ -391,9 +392,28 @@ export class TradovateClient {
|
|||||||
return typeof reportData?.data === 'string' ? reportData.data : '[]';
|
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) {
|
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) ---
|
// --- Try Cash History first (true after-fee daily P&L) ---
|
||||||
let usedFallback = false;
|
|
||||||
try {
|
try {
|
||||||
const raw = await requestReport('Cash History', account.name);
|
const raw = await requestReport('Cash History', account.name);
|
||||||
const fixed = raw.replace(/"Date":\s*(\d{4}-\d{2}-\d{2})/g, '"Date": "$1"');
|
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;
|
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 }))
|
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
this.dailyPnL[account.id] = entries;
|
const merged = mergePnL(cached, fresh);
|
||||||
this.daysTraded[account.id] = entries.filter((d) => d.pnl !== 0).length;
|
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];
|
delete this.lastFetchErrors[account.name];
|
||||||
continue;
|
continue;
|
||||||
} catch {
|
} catch {
|
||||||
usedFallback = true;
|
// fall through to Fills fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fallback: Fills report + FIFO (slightly pre-fee, used for passed accounts) ---
|
// --- 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 }))
|
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
this.dailyPnL[account.id] = entries;
|
const merged = mergePnL(cached, fresh);
|
||||||
this.daysTraded[account.id] = entries.filter((d) => d.pnl !== 0).length;
|
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)';
|
this.lastFetchErrors[account.name] = 'cash history unavailable (using fills fallback)';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg);
|
console.error(`[fetchDaysTraded] both reports failed for ${account.name}:`, msg);
|
||||||
this.lastFetchErrors[account.name] = msg;
|
// Cache already loaded at top of loop — just log the error
|
||||||
this.dailyPnL[account.id] ??= [];
|
if (cached.length > 0) {
|
||||||
this.daysTraded[account.id] ??= 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;
|
this.fetchDaysComplete = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user