Files
Brandon LiandClaude Opus 5.5 1e844178de 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>
2026-10-27 01:27:07 -05:00

102 lines
4.9 KiB
TypeScript

import { byFillTime, fillsToTrades, startingNet, type Fill, type CarriedTrade } from './fill-trades';
import {
loadOwnOrders, ownOrdersSince, loadFillTrades, saveFillTrades, deleteTrades, closeTradesUnseen,
type FillTradeRow,
} from './db';
/**
* Records the trades a futures platform's fills make (lib/fill-trades.ts) in
* the trades table the dashboard lists, with who opened and closed each: the
* bot when the fill's order is one it placed (own_orders), and how — its
* stop, its take-profit, or a market close.
*
* Each pass hands over a window of fills (TopstepX: two days; Tradovate: the
* session's) with the account's position now, and the window's trades are
* rebuilt whole. A history and a position read a moment apart can disagree —
* a fill landing between the two calls — and that pass would rebuild the
* wrong trades, so a contract is written only when the position before its
* window comes out the same two passes running.
*/
export interface ContractFills {
/** As shown: "NQZ6". */
symbol: string;
/** Dollars per point per contract, for P&L the platform didn't give. */
pointValue: number | null;
fills: Fill[];
/** Signed contracts held now. */
currentNet: number;
}
/** The position before each window, last pass: prefix:account:symbol → net. */
const lastStart = new Map<string, number>();
export function recordFillTrades(prefix: string, account: { id: number; name: string }, contracts: ContractFills[]): void {
let own: Map<string, string> | null = null;
for (const c of contracts) {
const fills = [...c.fills].sort(byFillTime);
const startNet = startingNet(fills, c.currentNet);
const key = `${prefix}:${account.id}:${c.symbol}`;
const steady = lastStart.get(key) === startNet;
lastStart.set(key, startNet);
if (!steady) continue;
const stored = loadFillTrades(account.id, prefix, c.symbol);
const isOpen = (t: FillTradeRow) => t.closedAt === null && t.closeReason === null;
const windowStart = fills.length > 0 ? fills[0].at : Infinity;
if (fills.length === 0) {
// Nothing in the window: an open trade stays open while the
// position does, and is closed out of sight once it's gone.
if (startNet === 0) closeTradesUnseen(stored.filter(isOpen).map((t) => t.positionId));
continue;
}
const carryRow = stored
.filter((t) => isOpen(t) && t.openedAt < windowStart)
.sort((a, b) => b.openedAt - a.openedAt)[0];
const carry: CarriedTrade | null = carryRow
? { id: carryRow.positionId, side: carryRow.side, quantity: carryRow.quantity, openPrice: carryRow.openPrice, fee: carryRow.fee, openedAt: carryRow.openedAt }
: null;
const trades = fillsToTrades(fills, { startNet, pointValue: c.pointValue, carry });
own ??= loadOwnOrders(account.id);
const since = ownOrdersSince();
const rows: FillTradeRow[] = trades.map((t) => {
const closeKind = t.closeOrderId === null ? undefined : own!.get(t.closeOrderId);
const closed = t.closedAt !== null;
return {
positionId: t.carried ? t.id : `${prefix}:${t.id}`,
accountId: account.id,
accountName: account.name,
symbol: c.symbol,
side: t.side,
quantity: t.quantity,
openPrice: t.openPrice,
closePrice: t.closePrice,
pnl: t.pnl,
fee: t.fee,
openedAt: t.openedAt,
closedAt: t.closedAt,
closeReason: !closed ? null : closeKind === 'stop' ? 'stop_loss' : closeKind === 'limit' ? 'take_profit' : null,
openedBy: t.carried ? null
: t.openOrderId !== null && own!.has(t.openOrderId) ? 'bot'
: t.openedAt >= since ? 'outside' : 'unknown',
// A trade opened before own orders were noted had its exits
// placed then too: whose close it was is unknown, not outside.
closedBy: !closed || closeKind === 'stop' || closeKind === 'limit' ? null
: closeKind === 'market' ? 'bot'
: t.openedAt >= since ? 'outside' : 'unknown',
};
});
saveFillTrades(rows);
const ids = new Set(rows.map((r) => r.positionId));
// Built last time off a window that has since moved under it.
deleteTrades(stored.filter((t) => t.openedAt >= windowStart && !ids.has(t.positionId)).map((t) => t.positionId));
// Open from before the window, and the window doesn't carry it on:
// it closed somewhere the history no longer reaches.
closeTradesUnseen(stored.filter((t) => isOpen(t) && t.openedAt < windowStart && !ids.has(t.positionId)).map((t) => t.positionId));
}
}