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
+131
-2
@@ -36,7 +36,7 @@ import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, rankForTarget } from './tradin
|
||||
import { sizingHoldHours } from './person-size';
|
||||
import {
|
||||
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
|
||||
saveEndedCycleStart,
|
||||
saveEndedCycleStart, loadFillTrades, saveFillTrades, closeTradesUnseen, type FillTradeRow,
|
||||
} from './db';
|
||||
import { getAccountConfig } from './account-type-resolver';
|
||||
import { getCachedFirm } from './firm-catalog';
|
||||
@@ -48,13 +48,19 @@ import {
|
||||
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
|
||||
lastUtcReset, ctTradeDayStart, firmDayOf, timeOf, parseDurationMs, ciNum, isFundType, signedFundValue, newOrderCode,
|
||||
inTradingHours, realizedVolPerHour, scoreCandidate, candidateRejection, parseTransfers, marginRateOf,
|
||||
parseHistoryOrder, dxTrades,
|
||||
} from './dxtrade-helpers';
|
||||
import type { DxTxn } from './dxtrade-helpers';
|
||||
import type { DxTxn, DxHistoryOrder, DxKnownPosition } from './dxtrade-helpers';
|
||||
import { hyperliquidCoins, hyperliquidCoinFor, hyperliquidQuote, hyperliquidHourlyCloses } from './hyperliquid-feed';
|
||||
|
||||
// ── timings ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const SYNC_INTERVAL_MS = 60_000;
|
||||
/** The trades record reads an active account's orders this often — sooner
|
||||
* once its positions or the day's cash show something happened. */
|
||||
const TRADES_RECORD_MS = 5 * 60_000;
|
||||
/** How far back the trades record reads orders and cash. */
|
||||
const TRADES_WINDOW_MS = 2 * 86_400_000;
|
||||
const SYNC_WEDGED_MS = 3 * 60_000;
|
||||
const WATCHDOG_INTERVAL_MS = 60_000;
|
||||
/** Logged in longer than this with no good sync for as long → re-login. */
|
||||
@@ -257,6 +263,12 @@ export class DXTradeClient implements PlatformClient {
|
||||
private recentTxns = new Map<number, Map<string, DxTxn>>();
|
||||
/** Accounts whose balance and day's transactions came back this pass. */
|
||||
private dayFresh = new Set<number>();
|
||||
/** The trades record: when each account was last read, and what its
|
||||
* positions and day's cash looked like then. */
|
||||
private tradesRecordedAt = new Map<number, { at: number; sig: string }>();
|
||||
private tradesRecordInFlight = false;
|
||||
/** The order history refused a time-from: read the latest orders instead. */
|
||||
private historyTakesNoTimeFrom = false;
|
||||
/** When each open position was first seen, for hold-time logging. */
|
||||
private seenOpenAt = new Map<number, { positionCode: string; at: number; naggedAt: number }>();
|
||||
|
||||
@@ -685,6 +697,7 @@ export class DXTradeClient implements PlatformClient {
|
||||
await this.reapStrayOrders();
|
||||
this.checkNakedPositions();
|
||||
if (superseded()) return;
|
||||
if (fresh) void this.recordTrades();
|
||||
|
||||
console.log(`${this.tag} sync — ${list.length} account(s), ${Object.keys(this.positions).length} open position(s)`);
|
||||
this.maybeFetchDaysTraded();
|
||||
@@ -1330,6 +1343,122 @@ export class DXTradeClient implements PlatformClient {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── trades record ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Every position on the accounts, the bot's and anyone else's, in the
|
||||
* trades table the dashboard lists — off the last two days' filled orders
|
||||
* and their SETTLEMENT and COMMISSION. The bot's orders carry its af-
|
||||
* codes, so who opened and closed each needs nothing kept locally. Only
|
||||
* an account with something going on is read (an open position, the
|
||||
* day's cash, a trade still open in the table), each at most every few
|
||||
* minutes. Never fails the sync.
|
||||
*/
|
||||
private async recordTrades(): Promise<void> {
|
||||
if (this.tradesRecordInFlight || !this.token || this.intentionalDisconnect) return;
|
||||
const now = Date.now();
|
||||
const due: { acc: PlatformAccount; code: string; sig: string; stored: FillTradeRow[] }[] = [];
|
||||
for (const acc of this.accountList) {
|
||||
const code = this.codeById.get(acc.id);
|
||||
if (!code || !this.dayFresh.has(acc.id)) continue;
|
||||
const positions = (this.positionsDetail.get(acc.id) ?? []).map((p) => p.positionCode).sort().join(',');
|
||||
const trading = [...(this.recentTxns.get(acc.id)?.values() ?? [])].filter((t) => !isFundType(t.type)).length;
|
||||
const sig = positions || trading > 0 ? `${positions}|${trading}` : '';
|
||||
const stored = loadFillTrades(acc.id, 'dx');
|
||||
const last = this.tradesRecordedAt.get(acc.id);
|
||||
const storedOpen = stored.some((t) => t.closedAt === null && t.closeReason === null);
|
||||
if (sig === '' && !storedOpen && (!last || last.sig === '')) continue;
|
||||
if (last && last.sig === sig && now - last.at < TRADES_RECORD_MS) continue;
|
||||
due.push({ acc, code, sig, stored });
|
||||
}
|
||||
if (due.length === 0) return;
|
||||
|
||||
this.tradesRecordInFlight = true;
|
||||
try {
|
||||
const from = new Date(now - TRADES_WINDOW_MS).toISOString();
|
||||
const txns = (await this.perAccounts<unknown>('/accounts/transfers', due.map((d) => d.code), (d) => [d],
|
||||
{ 'time-from': from, limit: 1000 }, 'history')).flatMap((body) => parseTransfers(body));
|
||||
for (const { acc, code, sig, stored } of due) {
|
||||
if (this.intentionalDisconnect) return;
|
||||
const orders = await this.historyOrders(code, from);
|
||||
if (orders === null) continue;
|
||||
this.saveTrades(acc, orders, txns.filter((t) => t.account === code), stored);
|
||||
this.tradesRecordedAt.set(acc.id, { at: Date.now(), sig });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`${this.tag} trades record failed: ${errorMessage(err)}`);
|
||||
} finally {
|
||||
this.tradesRecordInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** An account's filled orders since `from`; null when they couldn't be read. */
|
||||
private async historyOrders(code: string, from: string): Promise<DxHistoryOrder[] | null> {
|
||||
const path = `/accounts/${encodeURIComponent(code)}/orders/history`;
|
||||
try {
|
||||
let data: { orders?: unknown[] } | undefined;
|
||||
try {
|
||||
({ data } = await this.request<{ orders?: unknown[] }>('history', 'GET', path, {
|
||||
params: this.historyTakesNoTimeFrom ? { limit: 500 } : { 'time-from': from, limit: 500 },
|
||||
}));
|
||||
} catch (err) {
|
||||
if (!(err instanceof DxApiError) || err.status !== 400 || this.historyTakesNoTimeFrom) throw err;
|
||||
this.historyTakesNoTimeFrom = true;
|
||||
({ data } = await this.request<{ orders?: unknown[] }>('history', 'GET', path, { params: { limit: 500 } }));
|
||||
}
|
||||
const since = Date.parse(from);
|
||||
return (data?.orders ?? []).map(parseHistoryOrder).filter((o): o is DxHistoryOrder => o !== null && o.at >= since);
|
||||
} catch (err) {
|
||||
console.warn(`${this.tag} order history for ${accountDisplayName(code)} failed: ${errorMessage(err)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private saveTrades(acc: PlatformAccount, orders: DxHistoryOrder[], txns: DxTxn[], stored: FillTradeRow[]): void {
|
||||
const prefix = `dx:${acc.id}:`;
|
||||
const open = new Map<string, DxKnownPosition>();
|
||||
for (const p of this.positionsDetail.get(acc.id) ?? []) {
|
||||
open.set(p.positionCode, {
|
||||
symbol: p.symbol, side: p.side === 'BUY' ? 'long' : 'short', quantity: p.quantity,
|
||||
openPrice: Number.isFinite(p.openPrice) ? p.openPrice : null, openedAt: p.openTime,
|
||||
});
|
||||
}
|
||||
const known = new Map<string, DxKnownPosition>(stored.map((t) => [t.positionId.slice(prefix.length), {
|
||||
symbol: t.symbol, side: t.side, quantity: t.quantity, openPrice: t.openPrice, openedAt: t.openedAt,
|
||||
}]));
|
||||
const ours = (code: string | null) => code !== null && code.startsWith('af-');
|
||||
const rows: FillTradeRow[] = dxTrades(orders, txns, open, known)
|
||||
.filter((t) => Number.isFinite(t.openedAt))
|
||||
.map((t) => {
|
||||
const closed = t.closedAt !== null;
|
||||
const exit = closed && ours(t.closeCode) ? t.closeType : null;
|
||||
return {
|
||||
positionId: `${prefix}${t.positionCode}`,
|
||||
accountId: acc.id,
|
||||
accountName: acc.name,
|
||||
symbol: t.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: exit === 'STOP' ? 'stop_loss' : exit === 'LIMIT' ? 'take_profit' : null,
|
||||
openedBy: t.openCode === null ? null : ours(t.openCode) ? 'bot' : 'outside',
|
||||
closedBy: !closed || exit === 'STOP' || exit === 'LIMIT' ? null : ours(t.closeCode) ? 'bot' : 'outside',
|
||||
};
|
||||
});
|
||||
saveFillTrades(rows);
|
||||
// Open in the table, gone from the account, and nothing in the
|
||||
// history closed it: closed out of sight.
|
||||
const seen = new Set(rows.map((r) => r.positionId));
|
||||
closeTradesUnseen(stored
|
||||
.filter((t) => t.closedAt === null && t.closeReason === null && !seen.has(t.positionId) && !open.has(t.positionId.slice(prefix.length)))
|
||||
.map((t) => t.positionId));
|
||||
}
|
||||
|
||||
// ── orders ───────────────────────────────────────────────────────────────
|
||||
|
||||
private async freshPositions(account: string): Promise<DxPosition[]> {
|
||||
|
||||
Reference in New Issue
Block a user