Files
autofirmer-expanded/lib/master-protocol.ts
T
Brandon LiandClaude Opus 5.5 db1d12327a Live futures spreads off one client, and the master's max entry spread
One quote feed per machine: a TopstepX client's market hub, else a
Tradovate client's market-data socket, subscribed to every listed root's
front month and the drain micro. A supervisor keeps it on a working client,
dropping one that fails or goes quiet while the market trades.

The Auto draw weighs each instrument by its live spread (the typical tick
when no fresh quote is at hand). No futures entry pays more than the
master's max entry spread ($15 until set) to cross the book: the draw
leaves such an instrument out, an explicit symbol sits the tick out, and
fireLeg and the extra-day trade check it last before sending. The Settings
page and the master's stats show the live spreads and the feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:06:44 -05:00

386 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Wire protocol for the control link to the master dashboard.
*
* Mirror of the block at the bottom of `autotrader-master/types.ts`, which is
* the source of truth. The two repos deploy independently, so change both
* together — and only ever by adding, since an instance and a master can be
* running different versions of this file at the same time.
*/
export interface AccountTypeStats {
accountTypeId: string | null;
label: string;
accountSize: number;
totalAccounts: number;
accountsTraded: number;
inTrade: number;
/** Of the accounts that finished trading today (traded, no open position):
* how many ended with the day's profit target hit, in profit but short of
* the target, or at a realized loss (an account locked out at the daily
* loss limit counts as a loss). Optional so an older master simply
* ignores them. */
targetHit?: number;
partial?: number;
loss?: number;
/** Of the finished accounts, how many booked the day via their own
* obfuscation stop (personal DLL) rather than a genuine loss. */
personalDll?: number;
/** Of targetHit: accounts whose day finished the current cycle — profit
* target, consistency and minimum days all met once today's P&L booked
* (the final day). Optional: older instances omit it. */
completed?: number;
/** The accounts behind `completed`, by name, so the master can keep them
* after a firm erases a passed account. date = the trade date it
* completed on; stage = 1 + withdrawals taken (its meaning depends on the
* account type); kind: 'pass' = an eval passing, 'payout' = a funded
* cycle ready to withdraw, 'drain' = a drain stage's days booked;
* cycle = the date of the fund transaction that started the cycle ('' for
* the account's first cycle) — firm + name + cycle identify one
* completed cycle. The list also carries caught-up entries: for up to 7
* days after a fund transaction starts a new cycle, it includes the cycle
* that just ended if that cycle had met the rule. Caught-up entries are
* not counted in `completed`. Optional: older instances omit it. */
completedAccounts?: { name: string; date: string; stage: number; kind: 'pass' | 'payout' | 'drain'; cycle: string }[];
/** Live accounts by trading days completed toward the current target:
* {daysTraded: accountCount}. Feeds the master's payout calendar, so
* accounts that are dead, DLL-locked, or in the red are left out — the
* calendar only projects accounts still on the winning path. Optional so
* an older master simply ignores it. */
daysHist?: Record<number, number>;
/** Live accounts by days traded in the current cycle — the instance
* dashboard's Days Traded number (minDayPnL-qualifying days, today
* included once booked) —
* with how many of them hit today's target; completed = how many of those
* hits finished the cycle. Lets the master show "Day 1 3/5 · Day 2 1/4".
* Optional: older instances omit it. */
dayStats?: Record<number, { total: number; targetHit: number; completed?: number }>;
/** One entry per live account (negative ones included, dead ones not):
* its actual progress toward the current target, so the master can
* simulate the real required profit instead of assuming the winning
* path. A withdrawal resets the path, so days/maxDay/sum cover only the
* current cycle (client.dailyPnL is already the active-cycle slice),
* while profit stays the full equity profit. withdrawals > 0 tells the
* master the trail locked long ago; target/consistency/minDays carry
* this cycle's effective values from resolveEffectiveConfig. Optional so
* an older master simply ignores it. */
payoutStates?: {
days: number; profit: number; maxDay: number; sum: number; high: number;
withdrawals?: number; target?: number; consistency?: number; minDays?: number;
/** Trade date (YYYY-MM-DD) of the account's most recent traded day —
* after the 5 PM CT roll a day booked on tomorrow's trade date is
* already counted in `days`. */
lastDay?: string;
/** This account's projected payout chain on the perfect path: one
* entry per future withdrawal, with the gross amount
* (min(withdrawalPct × balance, cap)), the net received
* (gross × profitSplit), and prob = the CUMULATIVE probability of
* reaching that payout under the fair-walk ruin model. Optional so
* an older master simply ignores it. */
futurePayouts?: {
date: string; gross: number; net: number; prob: number;
stage: number; drain?: boolean;
}[];
}[];
}
export interface FirmStats {
firm: string;
totalAccounts: number;
accountsTraded: number;
inTrade: number;
/** Day outcomes, as on AccountTypeStats. */
targetHit?: number;
partial?: number;
loss?: number;
personalDll?: number;
/** Of targetHit: accounts that finished their cycle today. Optional. */
completed?: number;
/** Every account in the firm's broker list right now, dead ones included,
* so the master can tell a saved account the firm erased from one still
* listed, and spot a newly arrived funded account. size = the account
* type's size (0 without one); payoutable = the type pays out — its
* payoutable flag or withdrawal stages, the test that reports its
* completions as 'payout' — null while the account has no type. Absent
* when the firm isn't synced (unknown, not empty). Optional: older
* instances omit it.
*
* withdrawals = how many times the balance has gone down for a payout
* (negative fund transactions), which tells the master a completed
* cycle's money is out. Omitted while the account's fund history hasn't
* loaded, so a restart can't read as zero withdrawals. */
accounts?: { name: string; size: number; payoutable: boolean | null; withdrawals?: number }[];
/** Per account type, summing to the firm's own totals. Optional so a master
* on an older build simply ignores it. */
accountTypes?: AccountTypeStats[];
/** DXtrade firms: one account's next leg sized on every symbol-map
* candidate, best first — what the master's Firms page shows beside each
* symbol. Optional. */
symbolStats?: SymbolStats;
/** Futures firms: each CME root's entry cost, movement, expected hold and
* chance in the Auto draw right now. Optional. */
instrumentStats?: InstrumentStats;
}
/** A futures firm's instruments as the Auto draw sees them. */
export interface InstrumentStats {
/** ISO time computed. */
at: string;
/** Whose next leg the holds are sized for; null when no account has one. */
account: string | null;
leg: number | null;
/** 0 at the session's open, 1 at its last entry; null outside it. */
sessionProgress: number | null;
/** Holds at or over this are disabled. */
maxHoldHours: number;
/** Entries paying more than this to cross the spread are skipped (0 = no limit). */
maxEntrySpreadDollars?: number;
/** The firm whose client feeds the live quotes; null = none, typical spreads. */
quoteSource?: string | null;
instruments: {
symbol: string;
listed: boolean;
contract: string | null;
/** Crossing the spread on one contract, in dollars: live when
* `spreadLive`, else the typical one tick. */
spreadDollars: number;
spreadLive?: boolean;
/** Over the entry limit right now: out of the draw, not traded. */
spreadBlocked?: boolean;
/** One contract's typical daily move (1σ), in dollars. */
dailyMoveDollars: number | null;
holdHours: number | null;
disabled: boolean;
/** Share of Auto draws right now (0–1); null when not in the draw. */
chance: number | null;
}[];
}
/** How one account's next leg sizes on each candidate (see planLegSize). */
export interface SymbolStats {
account: string;
/** The leg's dollar target, before fees and swap. */
leg: number;
targetHoldHours: number;
/** The leg rides without a daily-loss stop (see lastStand). */
lastStand: boolean;
/** ISO time the stats were computed. */
at: string;
symbols: {
symbol: string;
notional: number;
/** Notional ÷ account size. */
multiplier: number;
/** The broker's leverage on the symbol (5 = 5:1); null when unstated. */
maxMultiplier: number | null;
holdHours: number;
/** Expected spread, commissions and swap. */
cost: number;
winProbability: number | null;
/** The same leg and daily room with no commission, spread or swap —
* what the costs take off the win chance. Optional. */
winProbabilityNoFees?: number | null;
volatilityPerDay: number;
spreadFraction: number;
}[];
}
export interface SchedulerState {
running: boolean;
action: 'Buy' | 'Sell' | 'Auto';
symbol: string;
lastRun: string | null;
intervalSeconds: number;
stopAfterAll?: boolean;
/** Today's random session-open delay in minutes (1–20), drawn once per
* Central-time day. Optional so an older master simply ignores it. */
sessionOpenDelayMin?: number;
}
/** The build this instance is running — its checkout's HEAD commit. The
* commit time is what orders two different versions. Optional in every
* message so an older instance or master simply ignores it. */
export interface BuildVersion {
commit: string;
time: string;
}
export interface AutomationSummary {
key: string;
label: string;
firm: string;
inputs: { id: string; label: string; min: number; max: number; default: number }[];
}
export interface InstanceCapabilities {
symbols: string[];
automations: AutomationSummary[];
runnerOnline: boolean;
}
// ── Firm catalog ─────────────────────────────────────────────────────────────
// Served by the master's public GET /api/firms/catalog and cached locally by
// lib/firm-catalog.ts. Credentials are per-machine and absent from all of it.
/** One account type as published by the master.
*
* The field names match `AccountConfig` in types.ts on purpose — that is what
* lets the trading code consume a catalog account type wherever it used to
* read a local account_configs row. */
export interface CatalogAccountType {
id: string;
firmId: string;
label: string;
prefix: string;
accountSize: number;
profitTarget: number;
consistency: number;
minDayPnL: number;
minTradingDays: number;
maxLoss: number;
maxPositionSize: number;
targetSameEquity: boolean;
/** False = big days trade as one unsplit leg instead of random ≥$500 pieces. */
splitEnabled: boolean;
/** Big days (> the $1,700 split threshold) must carve at least this many
* legs (0 = however the carve falls). Smaller days always trade as one
* leg; clamped to what ≥$500 pieces make possible. */
minLegs: number;
/** Obfuscation, part 1: the first trading day of a cycle carries a $1,000
* account stop — the day books red and ends instead of riding. */
firstDayStop: boolean;
/** Obfuscation, part 2: min-day (small) trades carry a 2×-target stop; a
* stopped-out day books, and the next day rides stopless. */
minDayStop: boolean;
/** True = CME holiday sessions count with the next full trading day, the way
* firms that follow CME clearing report them (e.g. Apex: Sun/Mon/Tue around
* a Monday holiday is ONE trading day, recorded on the Tuesday). */
combineCmeHolidays: boolean;
/** Per-stage config after the Nth withdrawal; the last stage repeats.
* drain=true marks a spam-withdraw stage: no profit target — book
* minTradingDays qualifying days of ~minDayPnL (default $5, randomized
* $5–$15, traded as tiny MNQ legs), then wait for the next withdrawal.
* withdrawalPct/payoutCap override the type's payout defaults per stage. */
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number; withdrawalPct?: number; payoutCap?: number }[];
/** Payout-projection settings, owned by the master's payout calendar but
* consumed here to compute each account's future payout chain. Optional:
* an older master doesn't send them. */
payoutable?: boolean;
/** Trailing drawdown in dollars for ruin-model probabilities. 0 = unset. */
trailingDrawdown?: number;
/** Legacy static payout estimate — superseded by the computed chain. */
expectedPayout?: number;
/** Share of the balance withdrawn when a cycle completes (default 0.5). */
withdrawalPct?: number;
/** Cap on the gross withdrawal in dollars. 0 = no cap. */
payoutCap?: number;
/** Share of the gross the person actually receives (default 0.9). */
profitSplit?: number;
/** Hard daily loss limit in dollars (0 = none). Every leg then carries a
* stop that keeps the firm's day from losing more than this, less a 10%
* reserve and fees. Optional: an older master doesn't send it. */
dailyLossLimit?: number;
/** Copied from the firm by the catalog sync, so every cfg-based path sees
* it: the firm trades every calendar day (24/7). */
tradesEveryDay?: boolean;
/** ISO. Ties between two matching account types are broken by this. */
updatedAt: string;
}
/** Where a firm's accounts trade. A build only knows the platforms listed
* here; the catalog sync skips a firm on any other rather than guess. */
export const FIRM_PLATFORMS = ['tradovate', 'topstepx', 'dxtrade'] as const;
export type FirmPlatform = (typeof FIRM_PLATFORMS)[number];
export function isFirmPlatform(value: unknown): value is FirmPlatform {
return typeof value === 'string' && (FIRM_PLATFORMS as readonly string[]).includes(value);
}
/** One symbol a DXTrade firm may trade for a CME root. Position size comes
* from volatility and the firm's target hold, and the commission is the
* firm's (an older master's notionalPerContract or per-symbol commissionPct
* is ignored). */
export interface SymbolMapCandidate {
/** The platform's symbol, e.g. "BTC/USD". */
symbol: string;
}
/** CME root the batch drew (NQ, MNQ, GC…) or '*' for any root not listed →
* the candidates to choose from, cheapest per unit of volatility wins. */
export type SymbolMap = Record<string, SymbolMapCandidate[]>;
export interface CatalogFirm {
id: string;
name: string;
/** Which platform the firm's accounts trade on. Older masters omit the
* field — treated as 'tradovate'; a value this build doesn't know makes
* the sync skip the firm. */
platform: FirmPlatform;
/** What the firm trades, in order: CME roots for futures firms, the firm's
* own symbols for DXtrade (see lib/instruments.ts). Absent from a master
* from before the lists — then bannedSymbols and symbolMap apply. */
instruments?: string[];
/** Older masters' way to exclude roots; newer ones derive it from
* `instruments`. */
bannedSymbols: string[];
accountTypes: CatalogAccountType[];
/** DXTrade REST base, e.g. https://dx.tradeify247.co/dxsca-web ('' otherwise). */
serverUrl?: string;
/** DXTrade login domain ('' = 'default'). */
loginDomain?: string;
/** Commission as % of notional per side (e.g. 0.04). 0 = the platform's
* per-contract fees, measured off each fill. */
commissionPct?: number;
/** Trades every calendar day, weekends and CME holidays included. */
tradesEveryDay?: boolean;
/** DXTrade, older masters: which symbols each CME root may trade as.
* Newer ones derive it from `instruments`. */
symbolMap?: SymbolMap;
/** DXTrade sizing: aim each leg's expected hold at this many hours
* (0 = DEFAULT_TARGET_HOLD_HOURS, 12). */
targetHoldHours?: number;
/** Overnight financing the firm charges, % of notional per day (0 = none). */
financingPctPerDay?: number;
updatedAt: string;
}
/** Settings the master applies to every instance (its Settings page). */
export interface CatalogTradingSettings {
/** The most an entry may pay to cross the spread, per contract, in
* dollars (0 = no limit). */
maxEntrySpreadDollars: number;
}
export interface FirmCatalogPayload {
version: number;
updatedAt: string;
firms: CatalogFirm[];
/** Absent from a master from before its Settings page. */
trading?: CatalogTradingSettings;
}
export interface FirmCatalogUnchanged {
version: number;
updatedAt: string;
unchanged: true;
}
export type Command =
| { kind: 'trade'; action: 'Buy' | 'Sell' | 'Auto'; symbol: string; stopAfterAll?: boolean }
| { kind: 'stop-trading' }
| { kind: 'autobuyer-run'; automationId: string; inputs?: Record<string, number> }
| { kind: 'report-now' }
/** Push a fresh settings/credentials backup right now. */
| { kind: 'backup-now' };
export type MasterMessage =
/** personId is the person's stable uuid; the instance stores it and
* presents it on later hellos, which is what makes a rename follow the
* person. */
| { type: 'welcome'; personId?: string }
| { type: 'command'; id: string; command: Command };
export type InstanceMessage =
| { type: 'hello'; instance: string; id?: string; capabilities: InstanceCapabilities; version?: BuildVersion }
| { type: 'report'; firms: FirmStats[]; scheduler: SchedulerState; capabilities?: InstanceCapabilities; version?: BuildVersion }
| { type: 'ack'; id: string; ok: boolean; result?: unknown; error?: string };