Tradovate: yesterday's realized P&L no longer reads as today's after the roll
Tradovate rolls each account's cash record to the new trade date on its own schedule (18 minutes after the 17:00 CT open on 2026-09-24, or not until the account trades), and until then the latest record still carries the ended day's realizedPnL. Plans sized legs off it, one-and-done accounts read as already traded, the reporter scored every account partial/loss, and an entry across the record roll took a whole day's P&L as its "commission", parking the take-profit out of reach. - Cash records keep their trade date; realized P&L from an earlier trade date reads as 0 (judged when read, so a record stored before the roll counts as ended after it). Archived or older records never overwrite a newer one, whichever order the roll pair arrives in. - The entry-commission delta falls back to $2.50/contract when it exceeds any real fee ($10/contract). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
77d8c2ecdb
commit
e49640498a
+67
-18
@@ -3,7 +3,7 @@
|
||||
import axios from 'axios';
|
||||
import type { AccountItem, AuthLoginResponse, Contract } from './tradovate-helpers';
|
||||
import { computeSec, randomUUIDV4 } from './tradovate-helpers';
|
||||
import { POINT_VALUES } from './trading-logic';
|
||||
import { POINT_VALUES, currentTradeDateCT } from './trading-logic';
|
||||
import { getCachedContract, resolveContracts } from './contract-resolver';
|
||||
import { saveDailyPnL, loadDailyPnL, saveAccountMeta, loadAccountMeta, saveFundTransactions, loadFundTransactions, saveEndedCycleStart } from './db';
|
||||
import { brokerWebSocket } from './outbound-proxy';
|
||||
@@ -117,6 +117,62 @@ function reconnectBackoffMs(failures: number): number {
|
||||
return Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, failures - 1), BACKOFF_MAX_MS);
|
||||
}
|
||||
|
||||
/** A Tradovate cashBalance entity, as the sync snapshot and socket carry it. */
|
||||
interface CashBalanceEntity {
|
||||
accountId: number;
|
||||
amount: number;
|
||||
realizedPnL: number;
|
||||
tradeDate?: { year: number; month: number; day: number } | null;
|
||||
archived?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An account's cash as the plan must read it. Tradovate rolls each account's
|
||||
* cash record to the new trade date on its own schedule — archiving the old
|
||||
* record and opening one with realizedPnL 0 — well after the 17:00 CT roll
|
||||
* (18 minutes on 2026-09-24), or not until the account first trades. Until
|
||||
* then the latest record still carries the ended day's realizedPnL, which the
|
||||
* plan would read as today's: legs sized off yesterday, one-and-done accounts
|
||||
* read as already traded, the entry-commission delta swallowing a whole day's
|
||||
* P&L. That day is over, and its P&L is already in amount: nothing is
|
||||
* realized today yet.
|
||||
*/
|
||||
function todaysCash(cb: CashBalanceEntity): { amount: number; realizedPnL: number; tradeDate: string | null; archived: boolean } {
|
||||
const t = cb.tradeDate;
|
||||
const tradeDate = t?.year
|
||||
? `${t.year}-${String(t.month).padStart(2, '0')}-${String(t.day).padStart(2, '0')}`
|
||||
: null;
|
||||
const recorded = cb.realizedPnL;
|
||||
return {
|
||||
amount: cb.amount,
|
||||
tradeDate,
|
||||
archived: cb.archived === true,
|
||||
// Judged when read, not when stored: a record stored at 16:59 has
|
||||
// ended by 17:01, whether or not a sync has come in since.
|
||||
get realizedPnL(): number {
|
||||
return tradeDate !== null && tradeDate < currentTradeDateCT() ? 0 : recorded;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a cash record unless the one already held is newer. At the roll the
|
||||
* snapshot and the socket deliver the ended day's record (archived) and the
|
||||
* new one together, in either order; the older must never win back.
|
||||
*/
|
||||
function storeCash(
|
||||
into: { [accountId: number]: { amount: number; realizedPnL: number; tradeDate?: string | null; archived?: boolean } },
|
||||
cb: CashBalanceEntity,
|
||||
): void {
|
||||
const next = todaysCash(cb);
|
||||
const prev = into[cb.accountId];
|
||||
if (prev?.tradeDate && next.tradeDate) {
|
||||
if (next.tradeDate < prev.tradeDate) return;
|
||||
if (next.tradeDate === prev.tradeDate && next.archived && !prev.archived) return;
|
||||
}
|
||||
into[cb.accountId] = next;
|
||||
}
|
||||
|
||||
/** ±20%, so several clients on one IP that failed together don't retry in lockstep. */
|
||||
function jitter(ms: number): number {
|
||||
return Math.round(ms * (0.8 + Math.random() * 0.4));
|
||||
@@ -146,7 +202,12 @@ export class TradovateClient {
|
||||
public accountCashBalances: {
|
||||
[accountId: number]: {
|
||||
amount: number;
|
||||
/** Today's realized P&L — see todaysCash. */
|
||||
realizedPnL: number;
|
||||
/** The trade date of the Tradovate record it came from (YYYY-MM-DD). */
|
||||
tradeDate?: string | null;
|
||||
/** That record was superseded at a roll. */
|
||||
archived?: boolean;
|
||||
};
|
||||
} = {};
|
||||
public positions: {
|
||||
@@ -575,13 +636,8 @@ export class TradovateClient {
|
||||
|
||||
// Update cash balances from WebSocket cashBalance events
|
||||
if (response.d?.entityType === 'cashBalance' && response.d?.entity) {
|
||||
const cb = response.d.entity;
|
||||
if (cb.accountId) {
|
||||
this.accountCashBalances[cb.accountId] = {
|
||||
amount: cb.amount,
|
||||
realizedPnL: cb.realizedPnL,
|
||||
};
|
||||
}
|
||||
const cb = response.d.entity as CashBalanceEntity;
|
||||
if (cb.accountId) storeCash(this.accountCashBalances, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,16 +893,9 @@ export class TradovateClient {
|
||||
return account;
|
||||
});
|
||||
|
||||
this.accountCashBalances = (response.cashBalances ?? []).reduce(
|
||||
(
|
||||
acc: { [accountId: number]: { amount: number; realizedPnL: number } },
|
||||
item: { accountId: number; amount: number; realizedPnL: number }
|
||||
) => {
|
||||
acc[item.accountId] = { amount: item.amount, realizedPnL: item.realizedPnL };
|
||||
return acc;
|
||||
},
|
||||
{} as { [accountId: number]: { amount: number; realizedPnL: number } }
|
||||
);
|
||||
const balances: TradovateClient['accountCashBalances'] = {};
|
||||
for (const item of (response.cashBalances ?? []) as CashBalanceEntity[]) storeCash(balances, item);
|
||||
this.accountCashBalances = balances;
|
||||
|
||||
this.positions = (response.positions ?? [])
|
||||
.filter((item: any) => item.netPos !== 0)
|
||||
|
||||
Reference in New Issue
Block a user