Revert "Attribute evening-session Cash History rows to the next trade date"

This reverts commit 57846d2b5f.
This commit is contained in:
Brandon Li
2026-09-08 21:21:09 -05:00
parent f07721019b
commit 3443fac75c
2 changed files with 7 additions and 57 deletions
+7 -40
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, 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 {