Files
autofirmer-expanded/lib/trade-mix.ts
T
Brandon LiandClaude Opus 5.5 b125257d4e Trade mix: name positions by a contract index that never expires
A Tradovate position opened before this build (so its entry wasn't recorded)
was named from the contract cache, whose entries expire after 30 minutes:
Majd's CL longs showed as #3273057. The resolver now also keeps a lasting
contract id → root index, which the mix reads after its recorded entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:04:42 -05:00

102 lines
4.5 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), 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';
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 {
// Recorded at the fill, then TopstepX's own contracts, then every
// contract this process resolved (Tradovate's, however long ago).
const names = new Map<string, string>();
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)) ?? rootOfContract(contractId as number | string) ?? `#${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 })),
};
}