/** * 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; /** 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; /** 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; }[]; }[]; /** Live accounts of this slice on each kept rule set — the slice itself * stays keyed by the live account type, whose id is the only one ever * reported as an accountTypeId. Optional. */ keptRules?: { id: string; accounts: number }[]; } 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. * * The rest come from statusVersion builds (platform-client.ts * accountStatus), each left out when null or at its default. status: * unknown when the balance is missing (reason cash_missing — never dead), * dead on the floor test, history_pending, locked when the firm stopped * it trading (reason says how), else live. balance and floor (0 = none) * in dollars; accountTypeId = the resolved catalog type (absent = * unassigned); firstFundDate = the earliest positive fund row, YYYY-MM-DD * (DXtrade: the registration date when there is none); positiveFunds * counts those rows, absent until the fund history loads; brokerStatus = * the broker's own flags (Tradovate closed/archived/restricted, DXtrade * accountStatus, TopstepX canTrade:false). */ accounts?: { name: string; size: number; payoutable: boolean | null; withdrawals?: number; status?: 'live' | 'locked' | 'dead' | 'history_pending' | 'unknown'; reason?: string; balance?: number; floor?: number; accountTypeId?: string; firstFundDate?: string; positiveFunds?: number; brokerStatus?: string; }[]; /** 1 on every firm from a build that sends the account status fields, * including one with no accounts. Absent = an older build: its accounts * are unknown, never dead. An empty accounts[] from a statusVersion build * is a synced firm with no accounts. Optional. */ statusVersion?: 1; /** The catalog firm's uuid (statusVersion builds), so the master needn't * resolve the name. Optional. */ firmId?: string; /** Kept rule set ids this machine has loaded for the firm. Present (even * []) = this build applies kept rules and its cached catalog was parsed * by a parser that understands them. Absent = old build or stale cache. */ keptRuleIds?: string[]; /** ISO time the firm's last good sync finished (statusVersion builds), sent * once synced. The statuses count only while it is recent. Optional. */ syncedAt?: string; /** 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; /** Answers the 'update-now' command; the master offers it only then. */ updateNow: 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; } /** Old rules an account type keeps for its accounts whose rules date is * inside [openedAt, closedAt) — set when the owner changes the type's rules * for new accounts only. Built as: live type X's fields, overwritten by the * snapshot's RULE fields, plus these. Never an account type itself: nothing * is pinned or reported by its id as an accountTypeId (lib/kept-rules.ts). */ export interface KeptAccountRules extends CatalogAccountType { /** This kept set's own uuid — never a live account type id. */ id: string; /** The live account type id (X). */ keptFrom: string; /** ISO; inclusive lower bound on the rules date; null = none. */ openedAt: string | null; /** ISO; exclusive upper bound. */ closedAt: 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; 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; /** The firm's website ('' = none; absent from older masters). The master * matches the extension's tab against it to find a person's 2FA code. */ siteUrl?: string; /** The most of the firm's accounts this machine trades at once, on top of * its own max_concurrent_accounts (0 = no firm cap; absent from older * masters). */ maxConcurrentAccounts?: number; /** Older masters' way to exclude roots; newer ones derive it from * `instruments`. */ bannedSymbols: string[]; accountTypes: CatalogAccountType[]; /** Old rule sets the firm's account types keep for older accounts. * Optional; old builds drop it. */ keptAccountRules?: KeptAccountRules[]; /** 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; /** Instance-local, never sent by the master: the catalog parser version * (lib/firm-catalog.ts CATALOG_PARSER_VERSION) that produced a cached * payload. An older parser dropped fields it didn't know, so a cache * without it, or below the current one, isn't trusted to hold them. */ parser?: number; } 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 } | { kind: 'report-now' } /** Push a fresh settings/credentials backup right now. */ | { kind: 'backup-now' } /** Heal this machine's auto-updater if it stopped, then check for an * update at once, skipping a failed update's retry wait. */ | { kind: 'update-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 };