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) <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-04-20 20:30:41 -05:00
co-authored by Claude Opus 4.6
parent 91a448750c
commit fa2aad38cb
+28 -12
View File
@@ -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. * meaning that day's trades belong to the NEW cycle.
* Timestamp format from Tradovate: "MM/DD/YYYY HH:MM:SS" in Central Time. * 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 { private static isWithdrawalBeforeFirstTrade(
if (!timestamp) return false; fundTimestamp: string | null,
const match = timestamp.match(/\d{2}\/\d{2}\/\d{4}\s+(\d{2}):\d{2}:\d{2}/); 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; if (!match) return false;
return parseInt(match[1], 10) < 9; 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. * 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) * - 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 before first trade: 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 during/after first trade: exclude the fund date (day's trades are old cycle)
*/ */
private static filterActivePnL( private static filterActivePnL(
entries: { date: string; pnl: number }[], entries: { date: string; pnl: number }[],
fundDate: string | null, fundDate: string | null,
isWithdrawal: boolean, isWithdrawal: boolean,
fundTimestamp: string | null fundTimestamp: string | null,
firstTradeTimestamp: string | null = null
): { active: { date: string; pnl: number }[]; prior: { date: string; pnl: number }[] } { ): { active: { date: string; pnl: number }[]; prior: { date: string; pnl: number }[] } {
if (!fundDate) return { active: entries, prior: [] }; if (!fundDate) return { active: entries, prior: [] };
// Withdrawal before trading → day belongs to NEW cycle (use >=) // Withdrawal before first trade → day belongs to NEW cycle (use >=)
// Withdrawal during/after trading → day belongs to OLD cycle (use >) // Withdrawal during/after first trade → day belongs to OLD cycle (use >)
// Deposit → always include the day (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 active = entries.filter((d) => excludeFundDate ? d.date > fundDate : d.date >= fundDate);
const prior = 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 fundMap: { [date: string]: number } = {};
const fundTimestampMap: { [date: string]: string } = {}; const fundTimestampMap: { [date: string]: string } = {};
const dailyMap: { [date: string]: number } = {}; const dailyMap: { [date: string]: number } = {};
const firstTradeTsMap: { [date: string]: string } = {};
for (const row of rows) { for (const row of rows) {
const changeType = (row['Cash Change Type'] ?? '').trim(); const changeType = (row['Cash Change Type'] ?? '').trim();
if (changeType === 'Fund Transaction' || changeType === 'Manual Adjustment') { if (changeType === 'Fund Transaction' || changeType === 'Manual Adjustment') {
@@ -535,6 +545,11 @@ export class TradovateClient {
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;
// 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 // 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 lastFundAmt = fundDate ? (fundMap[fundDate] ?? null) : null;
const isWithdrawal = lastFundAmt !== null && lastFundAmt < 0; const isWithdrawal = lastFundAmt !== null && lastFundAmt < 0;
const lastFundTs = fundDate ? (fundTimestampMap[fundDate] ?? loadAccountMeta(account.id, 'last_fund_timestamp')) : null; 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( 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; this.priorProfit[account.id] = Math.round(priorEntries.reduce((s, d) => s + d.pnl, 0) * 100) / 100;