Files
autofirmer-expanded/lib/trade-mix.ts
T
Brandon LiandClaude Opus 5.5 5696f2b036 Report each firm's trading mix: open positions and today's entries by instrument
The master knew how many accounts were in a trade, not in what. Each firm's
report now carries its open positions by instrument (accounts long and
short, contracts held) and today's entries the same way.

A position's contract is named by its instrument: DXtrade's carries the
symbol; a futures one is looked up among the contracts this machine has
entered (recorded at the fill and kept), then the resolver's cache and
TopstepX's own. Entries are counted as they fill, per trade date, in a
setting, so a restart doesn't lose the day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:50:56 -05:00

102 lines
4.4 KiB
TypeScript

/**
* 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<string, { long: number; short: number; contracts: number }>;
interface Today {
date: string;
byFirm: Record<string, Tally>;
}
function readJson<T>(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>(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<Record<string, string>>(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<string, string>();
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<string, string>) : null;
for (const [id, root] of own ?? []) names.set(id, root);
for (const [id, instrument] of Object.entries(readJson<Record<string, string>>(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<string, InstrumentCount>();
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 })),
};
}