/** * What each firm is trading, by instrument — its open positions now and its * entries today — for the master's fleet-wide mix. * * Open positions come off the client's position map, each contract named by * the instrument it is: a DXtrade position carries its symbol; a futures one * is looked up among the contracts this machine has entered (recorded at the * fill, kept across restarts), TopstepX's own, and every contract the * resolver has resolved in this process (never expiring). * Entries are counted as they fill and kept in a setting per trade date, so * a restart doesn't lose the day. */ import { getSetting, setSetting } from './db'; import { currentTradeDateCT } from './trading-logic'; import { rootOfContract } from './contract-resolver'; import { errorMessage } from './platform-client'; import type { InstrumentCount, TradeMix } from './master-protocol'; import { isNotionalPlatform } from './master-protocol'; const TODAY_KEY = 'trade_mix_today'; const NAMES_KEY = 'trade_mix_contracts'; /** Contract ids remembered at most — the front months of a few roots. */ const MAX_NAMES = 200; type Tally = Record; interface Today { date: string; byFirm: Record; } function readJson(key: string): T | null { try { return JSON.parse(getSetting(key) ?? 'null') as T | null; } catch { return null; } } function today(date: string): Today { const saved = readJson(TODAY_KEY); return saved?.date === date && saved.byFirm && typeof saved.byFirm === 'object' ? saved : { date, byFirm: {} }; } /** * Count one filled entry toward the firm's mix for today, and remember which * instrument its contract is. Bookkeeping: it never throws into a trade. */ export function recordEntry(firm: string, instrument: string, side: 'Buy' | 'Sell', contracts: number, contractId?: unknown): void { try { const t = today(currentTradeDateCT()); const tally = (t.byFirm[firm] ??= {}); const entry = (tally[instrument] ??= { long: 0, short: 0, contracts: 0 }); if (side === 'Buy') entry.long++; else entry.short++; entry.contracts += Number.isFinite(contracts) ? contracts : 0; setSetting(TODAY_KEY, JSON.stringify(t)); if (contractId !== undefined && contractId !== null && contractId !== '') { const names = readJson>(NAMES_KEY) ?? {}; names[String(contractId)] = instrument; const kept = Object.entries(names).slice(-MAX_NAMES); setSetting(NAMES_KEY, JSON.stringify(Object.fromEntries(kept))); } } catch (err) { console.warn(`[trade-mix] could not record an entry: ${errorMessage(err)}`); } } /** The firm's open positions and today's entries, by instrument. */ export function tradeMixFor( firm: { name: string; platform: string }, // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any, ): TradeMix { // Recorded at the fill, then TopstepX's own contracts, then every // contract this process resolved (Tradovate's, however long ago). const names = new Map(); const own = typeof client.contractRoots === 'function' ? (client.contractRoots() as Map) : null; for (const [id, root] of own ?? []) names.set(id, root); for (const [id, instrument] of Object.entries(readJson>(NAMES_KEY) ?? {})) names.set(id, instrument); const instrumentOf = (contractId: unknown): string => (isNotionalPlatform(firm.platform) ? String(contractId).split('|')[0] : names.get(String(contractId)) ?? rootOfContract(contractId as number | string) ?? `#${String(contractId)}`); const open = new Map(); for (const acc of (client.accountList ?? []) as { id: number }[]) { const pos = client.positions?.[acc.id] as { contractId: unknown; netPos: number } | undefined; if (!pos || !pos.netPos) continue; const instrument = instrumentOf(pos.contractId); const entry = open.get(instrument) ?? { instrument, long: 0, short: 0, contracts: 0 }; if (pos.netPos > 0) entry.long++; else entry.short++; entry.contracts += Math.abs(pos.netPos); open.set(instrument, entry); } const tally = today(currentTradeDateCT()).byFirm[firm.name] ?? {}; return { open: [...open.values()], today: Object.entries(tally).map(([instrument, t]) => ({ instrument, long: t.long, short: t.short, contracts: t.contracts })), }; }