From 3443fac75cf52bb35d37d1efd7a7f33fcef54300 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 8 Sep 2026 21:21:09 -0500 Subject: [PATCH] Revert "Attribute evening-session Cash History rows to the next trade date" This reverts commit 57846d2b5fe9b0d0d4bd24e6fa763f78fb9cfe79. --- lib/db.ts | 17 --------------- lib/tradovate-class.ts | 47 +++++++----------------------------------- 2 files changed, 7 insertions(+), 57 deletions(-) diff --git a/lib/db.ts b/lib/db.ts index c8beeda..3fa1bdf 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -234,23 +234,6 @@ export function saveDailyPnL(accountId: number, accountName: string, entries: { txn(); } -/** - * Replace all rows on/after fromDate with the given entries, keeping older rows. - * Used when a full Cash History report succeeded: it is authoritative for its window, - * so stale rows there (e.g. saved under a pre-roll calendar date) must not survive. - */ -export function replaceDailyPnLWindow(accountId: number, accountName: string, fromDate: string, entries: { date: string; pnl: number }[]): void { - const del = db.prepare('DELETE FROM daily_pnl WHERE account_id = ? AND date >= ?'); - const ins = db.prepare('INSERT OR REPLACE INTO daily_pnl (account_id, account_name, date, pnl) VALUES (?, ?, ?, ?)'); - const txn = db.transaction(() => { - del.run(accountId, fromDate); - 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 }[]); } diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index a7a4a34..4058ff3 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -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, replaceDailyPnLWindow, loadDailyPnL, saveAccountMeta, loadAccountMeta, saveFundTransactions, loadFundTransactions } from './db'; +import { saveDailyPnL, loadDailyPnL, saveAccountMeta, loadAccountMeta, saveFundTransactions, loadFundTransactions } from './db'; /** * Entity types Tradovate sends over the user-sync socket. Keep this as the @@ -412,21 +412,6 @@ export class TradovateClient { return parseInt(match[1], 10) < 9; } - /** - * Cash History rows are stamped with the calendar date, but trades in the evening - * session (≥ 5:00 PM CT) belong to the NEXT trade date — matching currentTradeDateCT - * and when Tradovate resets realizedPnL. Roll those rows forward so the same session - * never counts as both a prior day and today's realized PnL. - */ - private static rollEveningToNextTradeDate(date: string, timestamp: string | undefined): string { - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return date; - const m = timestamp?.match(/\d{2}\/\d{2}\/\d{4}\s+(\d{2}):\d{2}:\d{2}/); - if (!m || parseInt(m[1], 10) < 17) return date; - const next = new Date(`${date}T00:00:00Z`); - next.setUTCDate(next.getUTCDate() + 1); - return next.toISOString().slice(0, 10); - } - /** * Filters daily PnL entries for the current cycle based on fund date and withdrawal timing. * - Deposit: include the fund date (trading can start same day) @@ -469,10 +454,6 @@ export class TradovateClient { const day = String(d.getDate()).padStart(2, '0'); return `${m}/${day}/${d.getFullYear()}`; }; - // First date the Cash History report covers — within [windowStart, ∞) a successful - // report is authoritative, so cached rows there that the report no longer produces - // (e.g. saved under the pre-roll calendar date) must be dropped, not merged. - const windowStart = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}-${String(start.getDate()).padStart(2, '0')}`; if (!this.lastFetchRaw) this.lastFetchRaw = {}; if (!this.lastFetchErrors) this.lastFetchErrors = {}; @@ -517,9 +498,6 @@ export class TradovateClient { )).data; pollAttempts++; } - // A report still pending is NOT an empty report — treating it as '[]' - // would let the caller mistake "no data yet" for "no trades". - if (reportData?.['p-ticket']) throw new Error(`${name} report for ${accountName} not ready after ${pollAttempts} polls`); return typeof reportData?.data === 'string' ? reportData.data : '[]'; } catch (err) { lastErr = err; @@ -590,12 +568,11 @@ export class TradovateClient { } const delta = parseFloat((row['Delta'] ?? '0').replace(/,/g, '')); if (isNaN(delta)) continue; - const tradeDate = TradovateClient.rollEveningToNextTradeDate(row['Date'], row['Timestamp']); - dailyMap[tradeDate] = (dailyMap[tradeDate] ?? 0) + delta; - // Track earliest trade timestamp per trade date + dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta; + // Track earliest trade timestamp per date const ts = row['Timestamp']; - if (ts && (!firstTradeTsMap[tradeDate] || ts < firstTradeTsMap[tradeDate])) { - firstTradeTsMap[tradeDate] = ts; + if (ts && (!firstTradeTsMap[row['Date']] || ts < firstTradeTsMap[row['Date']])) { + firstTradeTsMap[row['Date']] = ts; } } @@ -623,14 +600,7 @@ export class TradovateClient { .map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 })) .sort((a, b) => a.date.localeCompare(b.date)); - // A report with rows is authoritative for its window: drop cached in-window rows - // it no longer produces (e.g. saved under the pre-roll calendar date). An empty - // report deletes nothing — it can't distinguish "no trades" from "no data". - const freshDates = new Set(fresh.map((e) => e.date)); - const liveCached = rows.length > 0 - ? cached.filter((e) => e.date < windowStart || freshDates.has(e.date)) - : cached; - const merged = mergePnL(liveCached, fresh); + const merged = mergePnL(cached, fresh); const lastFundAmt = fundDate ? (fundMap[fundDate] ?? null) : null; const isWithdrawal = lastFundAmt !== null && lastFundAmt < 0; @@ -644,10 +614,7 @@ export class TradovateClient { this.fullDailyPnL[account.id] = merged; this.dailyPnL[account.id] = active; this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length; - // Authoritative for the report window — drops stale in-window rows; older rows - // preserved. Only when the report returned actual rows: an empty report can't - // distinguish "no trades" from "no data", so it must never delete anything. - if (rows.length > 0) replaceDailyPnLWindow(account.id, account.name, windowStart, fresh); + saveDailyPnL(account.id, account.name, fresh); // upsert only fresh entries — preserves older cached rows delete this.lastFetchErrors[account.name]; continue; } catch {