Instruments: each firm's list from the master, and a session-aware futures draw

- lib/instrument-specs.ts (pure, tested): the CME specs — point value,
  tick size, one-tick spread, Yahoo ticker — replacing POINT_VALUES; the
  draw's maths; the 12-hour hold cap; the micro for drain and extra-day
  legs; reading a firm's list (the master's, or the old rules against an
  older master: this machine's enabled set less bans, the symbol map).
- lib/instruments.ts: the one module the trading paths ask. Measures every
  future's daily move from Yahoo's hourly candles once per trade date, and
  shows entry cost (a tick of spread plus Tradovate's fee parameters or
  TopstepX's last measured fill) without ever choosing by it.
- Futures Auto draws one root per batch from the batch firm's list: movement
  per dollar of spread early in the session with a 10% floor each, sliding
  to the fastest movers by the last entry; nothing expected to hold 12h or
  more; micros only if nothing else is listed. Every draw logs its odds.
- The batch is chosen before its instrument, and a group that can't trade
  anything yields to the next. Copy trade matches the held contract against
  the listed roots and won't copy an instrument it can't identify. Manual
  legs, manual orders and drain/extra-day legs follow the lists.
- DXtrade firms rank their own list whatever root was drawn.
- Capabilities, the dashboard's trade bar and contract resolves follow the
  lists; the trade bar and scheduler default to Auto. The master link
  reconnects when master_dashboard_url changes, and the catalog drops
  ?since after a switch so two masters' versions can't collide.
- Reports carry each futures firm's instrumentStats for the master's Firms
  page; GET /api/instruments shows them (?simulate=N runs the draw, no
  orders); the Settings page shows the lists read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 19:47:32 -05:00
co-authored by Claude Opus 5.5
parent 302e9b4911
commit 02544fbdf5
25 changed files with 1337 additions and 277 deletions
+16 -15
View File
@@ -14,9 +14,9 @@
* Differences from the futures platforms that shape this file:
* - Account codes are strings ("clearing:account"); ids are a stable hash of
* them (dxAccountId) so every numeric account table keeps working.
* - There are no CME products. The firm's symbol map (master Firms page) says
* which DXtrade symbols a CME root may trade as, and findFrontMonthContract
* picks the cheapest per unit of volatility right now (scoreCandidate).
* - There are no CME products. The firm's instrument list (master Firms page)
* says which DXtrade symbols it trades, whatever root a batch drew, and
* findFrontMonthContract picks among them (rankForTarget / scoreCandidate).
* One engine "contract" is one quantity step of the symbol, and the step
* rides in the contract name, so the engine's target/stop math is unchanged.
* - Quantities are units, and positions are closed by positionCode
@@ -38,6 +38,7 @@ import {
} from './db';
import { getAccountConfig } from './account-type-resolver';
import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments';
import type { SymbolMapCandidate } from './master-protocol';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
@@ -1162,24 +1163,26 @@ export class DXTradeClient implements PlatformClient {
}
/**
* The contract a CME root trades as on this firm right now, from the
* symbol map. With a pick context (the batch's leg), each candidate is
* The contract this firm trades right now, from its instrument list —
* the root is ignored. With a pick context (the batch's leg), each candidate is
* sized as the leg would be — at its own leverage — and the cheapest one
* that reaches the target hold wins (else the quickest); without one, the
* cheapest per unit of volatility (see scoreCandidate). Sticky while an
* account holds one of the candidates; a batch's pick is reused briefly
* so its other lookups (drain legs) agree.
*/
public async findFrontMonthContract(root: string, ctx?: PickContext): Promise<PlatformContract | null> {
public async findFrontMonthContract(_root: string, ctx?: PickContext): Promise<PlatformContract | null> {
if (!this.token) return null;
// The firm's own list: whatever root the batch drew, a DXtrade firm
// chooses among its instruments. One cache key for every root.
const root = '*';
const firm = getCachedFirm(this.cfg.firmId);
const map = firm?.symbolMap ?? {};
const candidates = map[root.toUpperCase()] ?? map['*'] ?? [];
const candidates: SymbolMapCandidate[] = firm ? firmInstruments(firm).map((symbol) => ({ symbol })) : [];
const now = Date.now();
if (candidates.length === 0) {
if (now - (this.unmappedLoggedAt.get(root) ?? 0) > 10 * 60_000) {
this.unmappedLoggedAt.set(root, now);
console.log(`${this.tag} no symbol mapped for ${root} (nor '*') — skipping; set the symbol map on the master Firms page`);
console.log(`${this.tag} no instruments listed — skipping; add them on the master Firms page`);
}
return null;
}
@@ -1194,14 +1197,12 @@ export class DXTradeClient implements PlatformClient {
return pick;
}
/** Every mapped symbol, looked at afresh whatever the accounts hold: the
/** Every listed symbol, looked at afresh whatever the accounts hold: the
* day's volatility, and the candidates the dashboard ranks. */
private async surveyCandidates(): Promise<void> {
const all = new Map<string, SymbolMapCandidate>();
for (const list of Object.values(getCachedFirm(this.cfg.firmId)?.symbolMap ?? {})) {
for (const c of list) if (!all.has(c.symbol)) all.set(c.symbol, c);
}
if (all.size > 0) await this.pickCandidate('survey', [...all.values()], false);
const firm = getCachedFirm(this.cfg.firmId);
const list = firm ? firmInstruments(firm) : [];
if (list.length > 0) await this.pickCandidate('survey', list.map((symbol) => ({ symbol })), false);
}
private async pickCandidate(root: string, candidates: SymbolMapCandidate[], sticky: boolean, ctx?: PickContext): Promise<PlatformContract | null> {