Revert "Attribute evening-session Cash History rows to the next trade date"
This reverts commit 57846d2b5f.
This commit is contained in:
@@ -234,23 +234,6 @@ export function saveDailyPnL(accountId: number, accountName: string, entries: {
|
|||||||
txn();
|
txn();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all rows on/after fromDate with the given entries, keeping older rows.
|
|
||||||
* Used when a full Cash History report succeeded: it is authoritative for its window,
|
|
||||||
* so stale rows there (e.g. saved under a pre-roll calendar date) must not survive.
|
|
||||||
*/
|
|
||||||
export function replaceDailyPnLWindow(accountId: number, accountName: string, fromDate: string, entries: { date: string; pnl: number }[]): void {
|
|
||||||
const del = db.prepare('DELETE FROM daily_pnl WHERE account_id = ? AND date >= ?');
|
|
||||||
const ins = db.prepare('INSERT OR REPLACE INTO daily_pnl (account_id, account_name, date, pnl) VALUES (?, ?, ?, ?)');
|
|
||||||
const txn = db.transaction(() => {
|
|
||||||
del.run(accountId, fromDate);
|
|
||||||
for (const e of entries) {
|
|
||||||
ins.run(accountId, accountName, e.date, e.pnl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
txn();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadDailyPnL(accountId: number): { date: string; pnl: number }[] {
|
export function loadDailyPnL(accountId: number): { date: string; pnl: number }[] {
|
||||||
return (db.prepare('SELECT date, pnl FROM daily_pnl WHERE account_id = ? ORDER BY date').all(accountId) as { date: string; pnl: number }[]);
|
return (db.prepare('SELECT date, pnl FROM daily_pnl WHERE account_id = ? ORDER BY date').all(accountId) as { date: string; pnl: number }[]);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-40
@@ -5,7 +5,7 @@ import type { AccountItem, AuthLoginResponse, Contract } from './tradovate-helpe
|
|||||||
import { computeSec, randomUUIDV4 } from './tradovate-helpers';
|
import { computeSec, randomUUIDV4 } from './tradovate-helpers';
|
||||||
import { POINT_VALUES } from './trading-logic';
|
import { POINT_VALUES } from './trading-logic';
|
||||||
import { getCachedContract, resolveContracts } from './contract-resolver';
|
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
|
* 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;
|
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.
|
* 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)
|
||||||
@@ -469,10 +454,6 @@ export class TradovateClient {
|
|||||||
const day = String(d.getDate()).padStart(2, '0');
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
return `${m}/${day}/${d.getFullYear()}`;
|
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.lastFetchRaw) this.lastFetchRaw = {};
|
||||||
if (!this.lastFetchErrors) this.lastFetchErrors = {};
|
if (!this.lastFetchErrors) this.lastFetchErrors = {};
|
||||||
@@ -517,9 +498,6 @@ export class TradovateClient {
|
|||||||
)).data;
|
)).data;
|
||||||
pollAttempts++;
|
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 : '[]';
|
return typeof reportData?.data === 'string' ? reportData.data : '[]';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastErr = err;
|
lastErr = err;
|
||||||
@@ -590,12 +568,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;
|
||||||
const tradeDate = TradovateClient.rollEveningToNextTradeDate(row['Date'], row['Timestamp']);
|
dailyMap[row['Date']] = (dailyMap[row['Date']] ?? 0) + delta;
|
||||||
dailyMap[tradeDate] = (dailyMap[tradeDate] ?? 0) + delta;
|
// Track earliest trade timestamp per date
|
||||||
// Track earliest trade timestamp per trade date
|
|
||||||
const ts = row['Timestamp'];
|
const ts = row['Timestamp'];
|
||||||
if (ts && (!firstTradeTsMap[tradeDate] || ts < firstTradeTsMap[tradeDate])) {
|
if (ts && (!firstTradeTsMap[row['Date']] || ts < firstTradeTsMap[row['Date']])) {
|
||||||
firstTradeTsMap[tradeDate] = ts;
|
firstTradeTsMap[row['Date']] = ts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,14 +600,7 @@ export class TradovateClient {
|
|||||||
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
.map(([date, pnl]) => ({ date, pnl: Math.round(pnl * 100) / 100 }))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
// A report with rows is authoritative for its window: drop cached in-window rows
|
const merged = mergePnL(cached, fresh);
|
||||||
// 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 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;
|
||||||
@@ -644,10 +614,7 @@ export class TradovateClient {
|
|||||||
this.fullDailyPnL[account.id] = merged;
|
this.fullDailyPnL[account.id] = merged;
|
||||||
this.dailyPnL[account.id] = active;
|
this.dailyPnL[account.id] = active;
|
||||||
this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length;
|
this.daysTraded[account.id] = active.filter((d) => d.pnl !== 0).length;
|
||||||
// Authoritative for the report window — drops stale in-window rows; older rows
|
saveDailyPnL(account.id, account.name, fresh); // upsert only fresh entries — preserves older cached 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);
|
|
||||||
delete this.lastFetchErrors[account.name];
|
delete this.lastFetchErrors[account.name];
|
||||||
continue;
|
continue;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user