From 1e844178de2421f48bd7816bf43966ca2beed896 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 27 Oct 2026 01:27:07 -0500 Subject: [PATCH] Trades: record every firm's trades, not just Vest's, and list them on each TopstepX and Tradovate report fills, so a trade is built from them, flat to flat (lib/fill-trades.ts), off the window each sync already reads: TopstepX's two days of Trade/search, Tradovate's sync snapshot. Neither says who placed an order, so every order the bot places is noted in own_orders, and a trade is the bot's when its opening fill's order is one; its stop or take-profit closing it reads as such. A contract is written only when the position before its window comes out the same two passes running, so a fill landing between the position and history reads can't build the wrong trades. DXtrade reports positions: its record groups the last two days' filled orders by position code, with each close's SETTLEMENT and COMMISSION. The bot's own orders carry af- codes, so nothing is kept locally. Only accounts with something going on are read, at most every 5 minutes. The Trades button shows on every firm with a login, and the account page lists any account's trades. Co-Authored-By: Claude Opus 5.5 --- app/api/trades/route.ts | 9 +- app/components/TradesTable.tsx | 9 +- app/page.tsx | 2 +- lib/db.ts | 108 ++++++++++++++- lib/dxtrade-class.ts | 133 +++++++++++++++++- lib/dxtrade-helpers.ts | 153 +++++++++++++++++++++ lib/fill-trades.ts | 243 +++++++++++++++++++++++++++++++++ lib/topstep-fees.ts | 11 ++ lib/topstepx-class.ts | 62 ++++++++- lib/trade-recorder.ts | 101 ++++++++++++++ lib/tradovate-class.ts | 142 ++++++++++++++++++- package.json | 2 +- scripts/check-trades.mjs | 190 ++++++++++++++++++++++++++ 13 files changed, 1149 insertions(+), 16 deletions(-) create mode 100644 lib/fill-trades.ts create mode 100644 lib/trade-recorder.ts create mode 100644 scripts/check-trades.mjs diff --git a/app/api/trades/route.ts b/app/api/trades/route.ts index 5bb3cb9..e6cbc9e 100644 --- a/app/api/trades/route.ts +++ b/app/api/trades/route.ts @@ -10,7 +10,10 @@ import type { TradeView } from '@/types'; * Who opened and closed each one: the bot marks its own orders as it sends * them, so after marking began (TRADES_SINCE_SETTING) anything unmarked was * someone else's. A close Vest calls "user_requested" is an order on the - * account — the bot's if it marked it, else outside. + * account — the bot's if it marked it, else outside. Trades built from + * futures fills (lib/trade-recorder.ts, DXtrade's record) carry their marks + * already: 'bot', 'outside', or 'unknown' from before the bot noted its own + * orders. */ export async function GET(req: Request) { const url = new URL(req.url); @@ -30,7 +33,7 @@ export async function GET(req: Request) { const closedBy = open ? null : t.closedBy === 'bot' ? 'bot' : t.closeReason === 'user_requested' ? (t.closedBy === 'outside' || tracked(t.closedAt ?? t.openedAt) ? 'outside' : 'user_requested') - : t.closeReason ?? 'unknown'; + : t.closeReason ?? (t.closedBy === 'outside' ? 'outside' : t.closedBy === 'unknown' ? 'user_requested' : 'unknown'); const net = open || t.pnl === null ? null : Math.round((t.pnl - Math.abs(t.fee ?? 0) + (t.funding ?? 0)) * 100) / 100; return { @@ -48,7 +51,7 @@ export async function GET(req: Request) { net, openedAt: t.openedAt, closedAt: t.closedAt, - openedBy: t.openedBy === 'bot' ? 'bot' : t.openedBy === 'outside' || tracked(t.openedAt) ? 'outside' : null, + openedBy: t.openedBy === 'bot' ? 'bot' : t.openedBy === 'unknown' ? null : t.openedBy === 'outside' || tracked(t.openedAt) ? 'outside' : null, closedBy, }; }); diff --git a/app/components/TradesTable.tsx b/app/components/TradesTable.tsx index 6dc105b..c1ca4d7 100644 --- a/app/components/TradesTable.tsx +++ b/app/components/TradesTable.tsx @@ -6,7 +6,7 @@ import type { TradeView } from '@/types'; /** * Recorded positions from /api/trades — every trade on the accounts, the * bot's and anyone else's, with who opened it and how it closed. On the - * dashboard under a Vest firm (inline) and on the account page (as a card, + * dashboard under each firm (inline) and on the account page (as a card, * hidden while there's nothing to show). */ @@ -16,7 +16,7 @@ const PILL = 'inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold white const CLOSED_BY: Record = { bot: { label: 'Bot', cls: 'bg-violet-100 text-violet-700', title: "The bot's own close order" }, - outside: { label: 'Outside', cls: 'bg-amber-100 text-amber-700', title: 'Closed by an order the bot did not send — by hand on Vest, or another tool' }, + outside: { label: 'Outside', cls: 'bg-amber-100 text-amber-700', title: 'Closed by an order the bot did not send — by hand on the platform, another tool, or the firm’s own liquidation' }, stop_loss: { label: 'Stop loss', cls: 'bg-red-100 text-red-700', title: 'Its stop-loss triggered' }, take_profit: { label: 'Take profit', cls: 'bg-green-100 text-green-700', title: 'Its take-profit triggered' }, liquidation: { label: 'Liquidation', cls: 'bg-red-200 text-red-800', title: 'Liquidated by Vest' }, @@ -24,6 +24,7 @@ const CLOSED_BY: Record = temporary_leverage_limit: { label: 'Leverage limit', cls: 'bg-orange-100 text-orange-700', title: 'Reduced by Vest’s temporary (weekend) leverage limit' }, corporate_action: { label: 'Corporate action', cls: 'bg-slate-100 text-slate-600', title: 'Closed by a corporate action' }, user_requested: { label: 'Self-requested', cls: 'bg-slate-100 text-slate-600', title: 'An order on the account closed it — from before the bot marked its own closes, so whose is unknown' }, + unknown: { label: 'Unknown', cls: 'bg-slate-100 text-slate-500', title: 'Closed while the platform’s history wasn’t being read — when and how aren’t on record' }, }; function money(v: number): string { @@ -44,7 +45,7 @@ function when(ms: number): string { function OpenedBy({ by }: { by: TradeView['openedBy'] }) { if (by === 'bot') return Bot; - if (by === 'outside') return Outside; + if (by === 'outside') return Outside; return —; } @@ -105,7 +106,7 @@ export function TradesTable({ query, title, showAccount = false, formatAccount = const table = !trades ?
{failed ? 'Couldn’t load trades' : 'Loading trades…'}
: trades.length === 0 - ?
No trades recorded yet — they appear after the next history read (within the hour, or a minute after a position closes)
+ ?
No trades recorded yet — they appear a few minutes after a fill, once the platform’s history is read
: (
diff --git a/app/page.tsx b/app/page.tsx index 9b81c1a..45694e7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -652,7 +652,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo )}
- {firm.platform === 'vest' && hasCredentials && ( + {hasCredentials && (