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:
co-authored by
Claude Opus 5.5
parent
6f6660fdcc
commit
1e844178de
@@ -0,0 +1,101 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user