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:
co-authored by
Claude Sonnet 4.6
parent
6b299f5359
commit
6d960ceb7b
@@ -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
@@ -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}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user