A firm can cap how many contracts of an instrument its accounts may hold (the catalog's instrumentLimits, set on the master's Firms page). A leg trades one contract per $1,000, capped by the account type's max contracts and that limit. The Auto draw sizes each instrument's hold with the capped count, so a limit that slows a leg past 12 hours takes it out of the draw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
452 lines
20 KiB
TypeScript
452 lines
20 KiB
TypeScript
/**
|
||
* 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;
|
||
/** What the firm is trading, by instrument: its open positions and
|
||
* today's entries (lib/trade-mix.ts). Optional. */
|
||
mix?: TradeMix;
|
||
}
|
||
|
||
/** One instrument's share of a firm's trading: accounts (open) or entries
|
||
* (today) on each side, and the contracts in them — units on DXtrade. */
|
||
export interface InstrumentCount {
|
||
instrument: string;
|
||
long: number;
|
||
short: number;
|
||
contracts: number;
|
||
}
|
||
|
||
export interface TradeMix {
|
||
open: InstrumentCount[];
|
||
today: InstrumentCount[];
|
||
}
|
||
|
||
/** 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 (0–180, after 5:45 PM
|
||
* CT), drawn once per Central-time day. Optional so an older master
|
||
* simply ignores it. */
|
||
sessionOpenDelayMin?: number;
|
||
/** What the last tick did in each lane, and why accounts sat it out.
|
||
* Optional so an older master simply ignores it. */
|
||
lastTicks?: LaneTick[];
|
||
}
|
||
|
||
/** Accounts that sat a tick out for one reason (dollar amounts folded, so
|
||
* like reasons group), with a few of their names. */
|
||
export interface TickReason {
|
||
reason: string;
|
||
count: number;
|
||
accounts: string[];
|
||
}
|
||
|
||
/** What the scheduler's last tick did in one lane. */
|
||
export interface LaneTick {
|
||
/** "futures", or the every-day firm's name. */
|
||
lane: string;
|
||
/** ISO time of the tick. */
|
||
at: string;
|
||
/** fired, no-eligible, positions-open, closed, holding-for-sync,
|
||
* skipped-in-flight, or "error: …". */
|
||
outcome: string;
|
||
/** One line on what happened: what fired, or what stopped it. */
|
||
detail: string;
|
||
/** Why the lane's accounts weren't in the batch, most common first. */
|
||
reasons?: TickReason[];
|
||
}
|
||
|
||
/** The auto-updater on this machine (scripts/update-check.mjs), read from
|
||
* the files it keeps and from git. */
|
||
export interface UpdaterStatus {
|
||
/** Its last check (it runs every minute under PM2); null = never ran here. */
|
||
lastCheckAt: string | null;
|
||
/** What that check found, e.g. "up to date at 1a2b3c4d" or "ERROR: …". */
|
||
lastCheck: string | null;
|
||
/** An update that failed and waits to retry: at which step, how often. */
|
||
failed: { target: string; step: string; at: string; attempts: number } | null;
|
||
/** The commit the running build was made from. */
|
||
builtCommit: string | null;
|
||
/** Commits on origin this checkout doesn't have, as of the updater's
|
||
* last fetch; null when git can't say. */
|
||
behind: number | null;
|
||
/** Tracked files edited on this machine — they can block the pull. */
|
||
localChanges: string[];
|
||
}
|
||
|
||
/** 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[];
|
||
/** Futures firms: the most contracts the firm allows on an instrument,
|
||
* by root, for every account type (absent = no limit beyond the type's
|
||
* max contracts). */
|
||
instrumentLimits?: Record<string, number>;
|
||
/** 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; updater?: UpdaterStatus }
|
||
| { type: 'ack'; id: string; ok: boolean; result?: unknown; error?: string };
|