/** * 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; }[]; }[]; } 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[]; } 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; /** Cap on one position's notional in dollars (0 = none), where the * platform prices positions (DXTrade). */ maxNotional?: 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, never from the map (an older * master's notionalPerContract is ignored). */ export interface SymbolMapCandidate { /** The platform's symbol, e.g. "BTC/USD". */ symbol: string; /** Per-side commission % for this symbol; absent = the firm's. */ commissionPct?: number; } /** 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; 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: which symbols each CME root may trade as. */ 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; } export interface FirmCatalogPayload { version: number; updatedAt: string; firms: CatalogFirm[]; } 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' }; 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 };