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:
Brandon Li
2026-09-24 17:22:43 -05:00
co-authored by Claude Opus 5.5
parent 77d8c2ecdb
commit e49640498a
2 changed files with 92 additions and 20 deletions
+25 -2
View File
@@ -353,6 +353,29 @@ async function legInstrument(
return { contract: mnq, pointValue: POINT_VALUES['MNQ'] };
}
/** An entry's fee when the balance update hasn't shown it yet — at least one
* extra tick on the exit. */
const FALLBACK_COMMISSION_PER_CONTRACT = 2.5;
/** No real entry fee comes near this per contract. */
const MAX_COMMISSION_PER_CONTRACT = 10;
/**
* The entry fee, read off the realized-P&L delta around the fill. Anything
* past a real per-contract fee is something else moving realized P&L — a
* broker record rolling to the new trade date under the entry swapped a whole
* day's P&L into the "fee", and doubled into the exit target that parked the
* take-profit out of reach — so it falls back like a missing update does.
*/
function entryCommissionFrom(before: number, after: number, contracts: number): number {
const delta = Math.abs(after - before);
if (delta > 0 && delta <= MAX_COMMISSION_PER_CONTRACT * contracts) return delta;
if (delta > 0) {
console.warn(`[auto-trade] realized P&L moved $${Math.round(delta * 100) / 100} around a ${contracts}-contract entry`
+ ` — not a fee; using $${FALLBACK_COMMISSION_PER_CONTRACT}/contract`);
}
return FALLBACK_COMMISSION_PER_CONTRACT * contracts;
}
// ── per-account trade evaluation (shared by runTrade / copyTrade / hasRemaining…) ──
/** The ineligible reason for an account whose history isn't loaded yet — still
@@ -776,7 +799,7 @@ async function runTradeLocked(
// After entry, realizedPnL drops by the entry commission; the delta from the
// pre-entry value is the entry fee paid (pre-entry may be nonzero mid-day).
// Fall back to $2.50/contract if the WS hasn't updated yet (guarantees at least 1 extra tick).
const entryCommission = Math.abs(updatedCash.realizedPnL - item.cash.realizedPnL) || (2.5 * contracts);
const entryCommission = entryCommissionFrom(item.cash.realizedPnL, updatedCash.realizedPnL, contracts);
const totalCommission = entryCommission * 2; // entry + exit round-trip
const grossTarget = legAmount + totalCommission;
@@ -1064,7 +1087,7 @@ async function copyTradeLocked(lease: number): Promise<unknown[]> {
await new Promise(r => setTimeout(r, 1000));
const updatedCash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
const entryCommission = Math.abs(updatedCash.realizedPnL - item.cash.realizedPnL) || (2.5 * contracts);
const entryCommission = entryCommissionFrom(item.cash.realizedPnL, updatedCash.realizedPnL, contracts);
const totalCommission = entryCommission * 2;
const grossTarget = legAmount + totalCommission;