diff --git a/lib/contract-resolver.ts b/lib/contract-resolver.ts index df6986d..e82e259 100644 --- a/lib/contract-resolver.ts +++ b/lib/contract-resolver.ts @@ -37,8 +37,13 @@ const PROBE_WINDOW = 6; // route handlers (settings page, manual trades) never see. const _g = globalThis as typeof globalThis & { __contractCache?: Map; + __contractRoots?: Map; }; const cache = (_g.__contractCache ??= new Map()); +/** Contract id → the root it was resolved for. Unlike the cache it never + * expires: a position opened hours ago still needs its instrument's name + * (lib/trade-mix.ts). Front months are few, so it stays small. */ +const roots = (_g.__contractRoots ??= new Map()); const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes export function getCachedContract(symbol: string): ResolvedContract | null { @@ -51,6 +56,12 @@ export function getCachedContract(symbol: string): ResolvedContract | null { return entry.contract; } +/** The root a resolved contract id belongs to, however long ago it was + * resolved; null for one this process never resolved. */ +export function rootOfContract(contractId: number | string): string | null { + return roots.get(String(contractId)) ?? null; +} + export function getAllCachedContracts(): Record { const result: Record = {}; for (const [symbol, entry] of cache) { @@ -205,6 +216,7 @@ export async function resolveContracts( }; results[sym] = contract; cache.set(sym, { contract, resolvedAt: Date.now() }); + roots.set(String(contract.id), sym); if (rolledFrom) { console.log(`[contract-resolver] ${sym}: ${rolledFrom} → ${match.name} ROLLED (by volume)`); diff --git a/lib/trade-mix.ts b/lib/trade-mix.ts index 71e17ff..96191fc 100644 --- a/lib/trade-mix.ts +++ b/lib/trade-mix.ts @@ -5,14 +5,15 @@ * 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. + * 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 { getAllCachedContracts } from './contract-resolver'; +import { rootOfContract } from './contract-resolver'; import { errorMessage } from './platform-client'; import type { InstrumentCount, TradeMix } from './master-protocol'; @@ -71,16 +72,15 @@ export function tradeMixFor( // 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(); - 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)}`); + : names.get(String(contractId)) ?? rootOfContract(contractId as number | string) ?? `#${String(contractId)}`); const open = new Map(); for (const acc of (client.accountList ?? []) as { id: number }[]) {