diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 368aa02..52d11db 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -25,6 +25,7 @@ import { getSetting, setSetting } from './db'; import type { LaneTick, TickReason } from './master-protocol'; import { POINT_VALUES, housekeepingRoot, MAX_HOLD_HOURS } from './instrument-specs'; import { firmInstruments, drawFuturesInstrument, spreadBlock, type LegShape } from './instruments'; +import { recordEntry } from './trade-mix'; import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client'; // ── process-wide state ─────────────────────────────────────────────────────── @@ -1082,6 +1083,7 @@ async function runTradeLocked( if (!mnqContract) throw new Error(`${microRoot} contract not found for extra-day trade`); const fill = await client.sendOrder(acc.id, mnqContract.name, 1, resolvedAction, 'Market'); + recordEntry(item.firmName, microRoot, resolvedAction, 1, mnqContract.id); const exitAction: 'Buy' | 'Sell' = resolvedAction === 'Buy' ? 'Sell' : 'Buy'; const exitFill = await client.sendOrder(acc.id, mnqContract.name, 1, exitAction, 'Market'); @@ -1318,6 +1320,16 @@ async function fireLeg( const { sizing, obfuscation } = plan; const fill = await sendEntry(tag, client, acc, legContract.name, contracts, action, tradeDate, legAmount); + // For the master's trade mix: a futures leg is its root (the drain + // micro on a drain day), a DXtrade one its symbol. + const dx = (legContract.stepNotional ?? 0) > 0; + recordEntry( + item.firmName, + dx ? String(legContract.name).split('|')[0] : (item.drain ? housekeepingFor(item.firm) : symbolLabel), + action, + dx ? contracts * (Number(String(legContract.name).split('|')[1]) || 1) : contracts, + (legContract as { id?: unknown }).id, + ); const exitAction: 'Buy' | 'Sell' = action === 'Buy' ? 'Sell' : 'Buy'; // The entry fee. Percent firms: straight off the fill. Otherwise wait diff --git a/lib/master-protocol.ts b/lib/master-protocol.ts index 998f2ae..75f84a3 100644 --- a/lib/master-protocol.ts +++ b/lib/master-protocol.ts @@ -119,6 +119,23 @@ export interface FirmStats { /** Futures firms: each CME root's entry cost, movement, expected hold and * chance in the Auto draw right now. Optional. */ instrumentStats?: InstrumentStats; + /** What the firm is trading, by instrument: its open positions and + * today's entries (lib/trade-mix.ts). Optional. */ + mix?: TradeMix; +} + +/** One instrument's share of a firm's trading: accounts (open) or entries + * (today) on each side, and the contracts in them — units on DXtrade. */ +export interface InstrumentCount { + instrument: string; + long: number; + short: number; + contracts: number; +} + +export interface TradeMix { + open: InstrumentCount[]; + today: InstrumentCount[]; } /** A futures firm's instruments as the Auto draw sees them. */ diff --git a/lib/reporter.ts b/lib/reporter.ts index e03e0fc..7263df6 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -14,6 +14,7 @@ import { getSetting, loadEndedCycleStart } from './db'; import { pushReport } from './master-link'; import { getBuildVersion } from './version'; +import { tradeMixFor } from './trade-mix'; import { updaterStatus } from './updater-status'; import { getClients } from './clients'; import { getTradableFirms } from './firm-catalog'; @@ -392,6 +393,7 @@ function collectFirmStats() { accountTypes, ...symbolStatsFor(f, client, rawToday), ...instrumentStatsBlock(f, client, rawToday), + mix: tradeMixFor(f, client), }; }); } diff --git a/lib/topstepx-class.ts b/lib/topstepx-class.ts index db7ed84..ea28766 100644 --- a/lib/topstepx-class.ts +++ b/lib/topstepx-class.ts @@ -1758,6 +1758,12 @@ export class TopstepXClient implements PlatformClient { // ── lifecycle ──────────────────────────────────────────────────────────── + /** Contract id → the root it was resolved for, for the reporter's trade + * mix (lib/trade-mix.ts). */ + public contractRoots(): Map { + return new Map([...this.contractByProduct].map(([root, { contract }]) => [String(contract.id), root])); + } + /** The session token, for the live quote feed's market hub * (lib/live-quotes.ts); null while logged out. */ public quoteToken(): string | null { diff --git a/lib/trade-mix.ts b/lib/trade-mix.ts new file mode 100644 index 0000000..71e17ff --- /dev/null +++ b/lib/trade-mix.ts @@ -0,0 +1,101 @@ +/** + * 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), then the resolver's cache and TopstepX's own. + * 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 { getAllCachedContracts } from './contract-resolver'; +import { errorMessage } from './platform-client'; +import type { InstrumentCount, TradeMix } 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 { + const names = new Map(); + for (const [root, contract] of Object.entries(getAllCachedContracts())) { + if (contract) names.set(String(contract.id), root); + } + 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 => (firm.platform === 'dxtrade' + ? String(contractId).split('|')[0] + : names.get(String(contractId)) ?? `#${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 })), + }; +}