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
+60
-2
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user