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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 01:27:07 -05:00
co-authored by Claude Opus 5.5
parent 6f6660fdcc
commit 1e844178de
13 changed files with 1149 additions and 16 deletions
+60 -2
View File
@@ -27,9 +27,11 @@ import * as signalR from '@microsoft/signalr';
import { currentTradeDateCT, FUND_EVENT_MIN, fundReconcileStep, type FundSighting } from './trading-logic';
import { getCachedContract } from './contract-resolver';
import { signalrWebSocket } from './outbound-proxy';
import { saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions, saveEndedCycleStart, dropFundTransaction } from './db';
import { saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions, saveEndedCycleStart, dropFundTransaction, recordOwnOrder } from './db';
import { getAccountConfig } from './account-type-resolver';
import { topstepxProductCode, topstepxRoot, topstepCommission, TOPSTEP_COMMISSION_FALLBACK } from './topstep-fees';
import { topstepxProductCode, topstepxRoot, topstepxSymbol, topstepCommission, TOPSTEP_COMMISSION_FALLBACK } from './topstep-fees';
import { recordFillTrades } from './trade-recorder';
import type { Fill } from './fill-trades';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
@@ -844,6 +846,44 @@ export class TopstepXClient implements PlatformClient {
return (t.fees ?? 0) + topstepCommission(root, t.size);
}
/** The trades the last two days' fills make, in the trades table the
* dashboard lists (lib/trade-recorder.ts). Never fails the sync. */
private recordTrades(acc: PlatformAccount, trades: TsxTrade[], open: { contractId: string; type: number; size: number }[]): void {
try {
const learned = this.learnedProductRoots();
const byContract = new Map<string, Fill[]>();
for (const p of open) byContract.set(p.contractId, []);
for (const t of trades) {
if (t.voided || !(t.size > 0)) continue;
const at = Date.parse(t.creationTimestamp);
if (!Number.isFinite(at)) continue;
const fills = byContract.get(t.contractId) ?? [];
fills.push({
id: String(t.id),
orderId: String(t.orderId),
at,
side: t.side === SIDE_BUY ? 'buy' : 'sell',
qty: t.size,
price: t.price,
fee: this.tradeFees(t, learned),
pnl: t.profitAndLoss,
});
byContract.set(t.contractId, fills);
}
recordFillTrades('tsx', acc, [...byContract].map(([contractId, fills]) => {
const p = open.find((x) => x.contractId === contractId);
return {
symbol: topstepxSymbol(contractId, learned),
pointValue: null,
fills,
currentNet: !p ? 0 : p.type === 1 ? p.size : -p.size,
};
}));
} catch (err) {
console.error(`[topstepx] trade record failed for ${acc.name}: ${errorMessage(err)}`);
}
}
/** Product code → root, off the contracts resolved this run
* ("CON.F.US.GCE.Z26" resolved for GC → GCE is GC). */
private learnedProductRoots(): Map<string, string> {
@@ -900,12 +940,15 @@ export class TopstepXClient implements PlatformClient {
for (const acc of this.accountList) {
const balance = (accRes.accounts ?? []).find((a) => a.id === acc.id)?.balance ?? 0;
let accountFresh = true;
/** This pass's open positions, for the trades record. */
let openNow: { contractId: string; type: number; size: number }[] | null = null;
try {
const posRes = await this.post<{ positions: { accountId: number; contractId: string; type: number; size: number; averagePrice: number; creationTimestamp: string }[] }>(
'/api/Position/searchOpen', { accountId: acc.id });
if (superseded()) return;
const open = (posRes.positions ?? []).filter((p) => p.size > 0);
openNow = open;
if (open.length > 0) {
this.applyPosition(open[0]);
} else {
@@ -941,6 +984,7 @@ export class TopstepXClient implements PlatformClient {
map.set(t.id, t);
}
this.todayTrades[acc.id] = map;
if (openNow !== null) this.recordTrades(acc, tradeRes.trades ?? [], openNow);
} catch (err) {
accountFresh = false;
console.error(`[topstepx] trade sync failed for ${acc.name}: ${errorMessage(err)}`);
@@ -1602,9 +1646,11 @@ export class TopstepXClient implements PlatformClient {
trailPrice: null,
customTag: null,
});
this.noteOwnOrder(accountId, res.orderId, type);
return res.orderId;
} catch (err) {
if (err instanceof GatewayRefusedError) {
if (err.orderId !== undefined) this.noteOwnOrder(accountId, err.orderId, type);
const ambiguous = err.orderId !== undefined
|| (err.errorCode !== undefined && AMBIGUOUS_PLACE_ERRORS.has(err.errorCode));
if (!ambiguous) throw new OrderNotPlacedError(`TopstepX refused the order: ${err.message}`);
@@ -1614,6 +1660,7 @@ export class TopstepXClient implements PlatformClient {
const status = response?.status;
// Same rule as a refusal: an answer that names an order means it exists.
const namesOrder = typeof response?.data?.orderId === 'number' && response.data.orderId > 0;
if (namesOrder) this.noteOwnOrder(accountId, response?.data?.orderId, type);
if (typeof status === 'number' && REFUSED_BEFORE_HANDLER.has(status) && !namesOrder) {
throw new OrderNotPlacedError(`TopstepX rejected the order request: ${errorMessage(err)}`);
}
@@ -1621,6 +1668,17 @@ export class TopstepXClient implements PlatformClient {
}
}
/** Notes an order as the bot's, so the trade its fill opens or closes is
* credited to the bot (lib/trade-recorder.ts). */
private noteOwnOrder(accountId: number, orderId: unknown, type: number): void {
if (typeof orderId !== 'number' || !(orderId > 0)) return;
try {
recordOwnOrder(accountId, orderId, type === TYPE_STOP ? 'stop' : type === TYPE_LIMIT ? 'limit' : 'market');
} catch (err) {
console.error(`[topstepx] could not note order ${orderId} as the bot's: ${errorMessage(err)}`);
}
}
/** See PlatformClient: an account whose last order's outcome is unknown
* counts as holding a working order until a sync's Order/searchOpen shows
* none. */