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.
|
* 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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user