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
+11 -1
View File
@@ -18,6 +18,7 @@
*/
import { isFirmPlatform } from './master-protocol';
import { sanitizeInstruments } from './instrument-specs';
import type {
CatalogAccountType, CatalogFirm, FirmCatalogPayload, FirmPlatform, SymbolMap, SymbolMapCandidate,
} from './master-protocol';
@@ -241,6 +242,9 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
id: firm.id,
name: firm.name,
platform,
// The master's list when it sends one; absent from an older
// master, whose bannedSymbols / symbolMap then apply.
...(Array.isArray(firm.instruments) ? { instruments: sanitizeInstruments(platform, firm.instruments) } : {}),
bannedSymbols: Array.isArray(firm.bannedSymbols) ? (firm.bannedSymbols as string[]) : [],
accountTypes,
serverUrl: typeof firm.serverUrl === 'string' ? firm.serverUrl : '',
@@ -302,8 +306,12 @@ export async function syncCatalog(): Promise<{ ok: boolean; changed: boolean; ve
// Only the URL is required — deliberately not instance_name, unlike the
// reporter. A freshly imaged machine has to be able to pull its catalog
// before anyone has got round to naming it.
// `since` only against the master the cache came from: two masters'
// version numbers can collide, and an 'unchanged' from the wrong one
// would keep the other's catalog.
const hasCache = getCachedCatalog().length > 0;
if (hasCache) url.searchParams.set('since', String(version));
const source = url.origin;
if (hasCache && getSetting('catalog_source_url') === source) url.searchParams.set('since', String(version));
try {
const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
@@ -311,6 +319,7 @@ export async function syncCatalog(): Promise<{ ok: boolean; changed: boolean; ve
const body = await res.json();
if (body && typeof body === 'object' && (body as Record<string, unknown>).unchanged === true) {
setSetting('catalog_source_url', source);
setSetting('catalog_last_sync_at', new Date().toISOString());
setSetting('catalog_last_error', '');
return { ok: true, changed: false, version };
@@ -342,6 +351,7 @@ export async function syncCatalog(): Promise<{ ok: boolean; changed: boolean; ve
setSetting('catalog_payload', JSON.stringify(payload));
setSetting('catalog_version', String(payload.version));
setSetting('catalog_source_url', source);
setSetting('catalog_updated_at', payload.updatedAt);
setSetting('catalog_last_sync_at', new Date().toISOString());
setSetting('catalog_last_error', suspicious.length > 0