Reset days traded counter from last fund transaction date

Tracks Fund Transaction entries in the Cash History report to find the
most recent account funding/reset date. Only trading days on or after
that date count toward daysTraded and the daily target calculation.
The last fund date is persisted in SQLite so it survives beyond the
28-day Tradovate report window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-03-21 04:10:02 -05:00
co-authored by Claude Sonnet 4.6
parent 6b299f5359
commit 6d960ceb7b
2 changed files with 53 additions and 9 deletions
+20
View File
@@ -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 }[]); 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 ─────────────────────────────────────────────────────── // ── Firm banned symbols ───────────────────────────────────────────────────────
db.exec(` db.exec(`
+33 -9
View File
@@ -5,7 +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'; import { saveDailyPnL, loadDailyPnL, saveAccountMeta, loadAccountMeta } from './db';
export class TradovateClient { export class TradovateClient {
private name: string; private name: string;
@@ -31,6 +31,8 @@ export class TradovateClient {
public daysTraded: { [accountId: number]: number } = {}; public daysTraded: { [accountId: number]: number } = {};
public dailyPnL: { [accountId: number]: { date: string; pnl: 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) */ /** Balance floor set by the prop firm — account is blown when amount <= this value (0 = not set) */
public autoLiqThresholds: { [accountId: number]: number } = {}; public autoLiqThresholds: { [accountId: number]: number } = {};
@@ -406,11 +408,16 @@ export class TradovateClient {
}; };
for (const account of this.accountList) { 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 // Load cache first — serves as both the startup baseline and the fallback if API fails
const cached = loadDailyPnL(account.id); const cached = loadDailyPnL(account.id);
if (cached.length > 0) { if (cached.length > 0) {
this.dailyPnL[account.id] = cached; const active = storedFundDate ? cached.filter((d) => d.date >= storedFundDate) : cached;
this.daysTraded[account.id] = cached.filter((d) => d.pnl !== 0).length; 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) --- // --- 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); 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)'; this.lastFetchRaw[account.name] = rows.length > 0 ? JSON.stringify(rows[0]) : '(empty)';
const fundDates: string[] = [];
const dailyMap: { [date: string]: number } = {}; const dailyMap: { [date: string]: number } = {};
for (const row of rows) { 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, '')); const delta = parseFloat((row['Delta'] ?? '0').replace(/,/g, ''));
if (isNaN(delta)) continue; if (isNaN(delta)) continue;
dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta; 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) 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));
const merged = mergePnL(cached, fresh); const merged = mergePnL(cached, fresh);
this.dailyPnL[account.id] = merged; const active = fundDate ? merged.filter((d) => d.date >= fundDate) : merged;
this.daysTraded[account.id] = merged.filter((d) => d.pnl !== 0).length; 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 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;
@@ -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) 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));
const merged = mergePnL(cached, fresh); const merged = mergePnL(cached, fresh);
this.dailyPnL[account.id] = merged; const active = fundDate ? merged.filter((d) => d.date >= fundDate) : merged;
this.daysTraded[account.id] = merged.filter((d) => d.pnl !== 0).length; 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 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);
// 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) { if (cached.length > 0) {
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`; 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}`); console.log(`[fetchDaysTraded] using ${cached.length} cached entries from DB for ${account.name}`);