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:
co-authored by
Claude Opus 4.6
parent
91a448750c
commit
fa2aad38cb
+28
-12
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user