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
+33 -9
View File
@@ -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}`);