From fa2aad38cb8f69bdb18d2f3348d3ba2f4ef1ccbc Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Mon, 20 Apr 2026 20:30:41 -0500 Subject: [PATCH] Use actual first-trade timestamp for same-day withdrawal filtering Instead of the 9 AM CT heuristic, compare the withdrawal timestamp against the day's earliest trade timestamp. If trades happened AFTER the withdrawal, those trades count toward the new cycle. Falls back to the 9 AM heuristic when first-trade timestamp is unavailable (e.g., cache-only fallback path). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/tradovate-class.ts | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index df72272..0587f8e 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -391,14 +391,22 @@ export class TradovateClient { } /** - * Returns true when a withdrawal happened before the trading session started, + * Returns true when a withdrawal happened before the first trade of the day, * meaning that day's trades belong to the NEW cycle. * Timestamp format from Tradovate: "MM/DD/YYYY HH:MM:SS" in Central Time. - * Cutoff: before 9:00 AM CT → "before trading". + * Falls back to 9:00 AM CT cutoff when firstTradeTimestamp is not available. */ - private static isWithdrawalBeforeTrading(timestamp: string | null): boolean { - if (!timestamp) return false; - const match = timestamp.match(/\d{2}\/\d{2}\/\d{4}\s+(\d{2}):\d{2}:\d{2}/); + private static isWithdrawalBeforeFirstTrade( + fundTimestamp: string | null, + firstTradeTimestamp: string | null + ): boolean { + if (!fundTimestamp) return false; + if (firstTradeTimestamp) { + // Direct comparison — withdrawal before the day's first trade + return fundTimestamp < firstTradeTimestamp; + } + // Fallback heuristic: before 9:00 AM CT + const match = fundTimestamp.match(/\d{2}\/\d{2}\/\d{4}\s+(\d{2}):\d{2}:\d{2}/); if (!match) return false; return parseInt(match[1], 10) < 9; } @@ -406,21 +414,22 @@ export class TradovateClient { /** * 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) - * - Withdrawal before trading session: include the fund date (day's trades are new cycle) - * - Withdrawal during/after trading: exclude the fund date (day's trades are old cycle) + * - Withdrawal before first trade: include the fund date (day's trades are new cycle) + * - Withdrawal during/after first trade: exclude the fund date (day's trades are old cycle) */ private static filterActivePnL( entries: { date: string; pnl: number }[], fundDate: string | null, isWithdrawal: boolean, - fundTimestamp: string | null + fundTimestamp: string | null, + firstTradeTimestamp: string | null = null ): { active: { date: string; pnl: number }[]; prior: { date: string; pnl: number }[] } { if (!fundDate) return { active: entries, prior: [] }; - // Withdrawal before trading → day belongs to NEW cycle (use >=) - // Withdrawal during/after trading → day belongs to OLD cycle (use >) + // Withdrawal before first trade → day belongs to NEW cycle (use >=) + // Withdrawal during/after first trade → day belongs to OLD cycle (use >) // Deposit → always include the day (use >=) - const excludeFundDate = isWithdrawal && !TradovateClient.isWithdrawalBeforeTrading(fundTimestamp); + const excludeFundDate = isWithdrawal && !TradovateClient.isWithdrawalBeforeFirstTrade(fundTimestamp, firstTradeTimestamp); const active = entries.filter((d) => excludeFundDate ? d.date > fundDate : d.date >= fundDate); const prior = entries.filter((d) => excludeFundDate ? d.date <= fundDate : d.date < fundDate); @@ -522,6 +531,7 @@ export class TradovateClient { const fundMap: { [date: string]: number } = {}; const fundTimestampMap: { [date: string]: string } = {}; const dailyMap: { [date: string]: number } = {}; + const firstTradeTsMap: { [date: string]: string } = {}; for (const row of rows) { const changeType = (row['Cash Change Type'] ?? '').trim(); if (changeType === 'Fund Transaction' || changeType === 'Manual Adjustment') { @@ -535,6 +545,11 @@ export class TradovateClient { const delta = parseFloat((row['Delta'] ?? '0').replace(/,/g, '')); if (isNaN(delta)) continue; dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta; + // Track earliest trade timestamp per date + const ts = row['Timestamp']; + if (ts && (!firstTradeTsMap[row['Date']] || ts < firstTradeTsMap[row['Date']])) { + firstTradeTsMap[row['Date']] = ts; + } } // Persist fund transactions so they survive beyond the 28-day window @@ -566,8 +581,9 @@ export class TradovateClient { const lastFundAmt = fundDate ? (fundMap[fundDate] ?? null) : null; const isWithdrawal = lastFundAmt !== null && lastFundAmt < 0; const lastFundTs = fundDate ? (fundTimestampMap[fundDate] ?? loadAccountMeta(account.id, 'last_fund_timestamp')) : null; + const firstTradeTs = fundDate ? (firstTradeTsMap[fundDate] ?? null) : null; const { active, prior: priorEntries } = TradovateClient.filterActivePnL( - merged, fundDate, isWithdrawal, lastFundTs + merged, fundDate, isWithdrawal, lastFundTs, firstTradeTs ); this.priorProfit[account.id] = Math.round(priorEntries.reduce((s, d) => s + d.pnl, 0) * 100) / 100;