When the master changes an account type with "New accounts only", it now serves the type's old rules as kept rule sets (firm.keptAccountRules), each for accounts dated inside [openedAt, closedAt). This build applies them: - getAccountConfig, the one rules lookup trading reads, puts a kept set's rule fields over the live type when the account's rules date falls in its window. Id, label, prefix, size and the projection fields stay the live type's, so every report still names the live type. - account_assignments gains rules_date and rules_date_manual. A new row is dated when the machine first sees the account, on the master's clock when this one runs behind and never before the catalog's last change. Auto dates come down to the broker's evidence: the end of the first fund day in Chicago, or a DXtrade registration instant. Evidence is applied before an account's first trade. Rows from before the migration get its time; unpinning keeps the row and its date. Backups carry both columns. - An account on a kept set stays on its type while its prefix and cached size still match, so a tie-break flip can't move it off its old rules. - Kept entries parse through the same normalizer as live types (now normalizeAccountType in lib/kept-rules.ts); a malformed one rejects the sync like a malformed type. The cached catalog records the parser that stored it, and a cache from an older parser is fetched in full rather than trusted with ?since= (this also brings in maxConcurrentAccounts). - Reports add keptRuleIds per firm and live counts per kept set per type. - The dashboard row and the account page show the rules date next to the account name, with an "old rules" chip saying what differs. Clicking the date edits it inline (PUT /api/accounts/:id/rules-date), previewing which rules the date lands on. Also: the master's per-firm cap on accounts traded at once (maxConcurrentAccounts), applied on top of the machine's own max concurrent when a batch is drawn and in copy trading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
9.2 KiB
TypeScript
179 lines
9.2 KiB
TypeScript
import type { FirmPlatform } from './lib/master-protocol';
|
|
import type { KeptRulesWindow } from './lib/kept-rules';
|
|
|
|
/** When one of an account type's old rule sets applies: accounts whose rules
|
|
* date is inside [openedAt, closedAt) keep those rules (openedAt null = no
|
|
* lower bound). `id` is the kept set's own, never an account type id. */
|
|
export type { KeptRulesWindow };
|
|
|
|
export interface AccountConfig {
|
|
/** Master's account type id — what a manual assignment pins to. */
|
|
id: string;
|
|
label: string;
|
|
prefix: string;
|
|
profitTarget: number;
|
|
consistency: number;
|
|
minDayPnL: number;
|
|
minTradingDays: number;
|
|
accountSize: number;
|
|
maxLoss: number; // static max drawdown below the starting size (0 = none); with a hard daily loss, decides last stands
|
|
maxPositionSize: number; // 0 = no limit; positive = max contracts per trade
|
|
targetSameEquity: boolean; // if true, reduce profitTarget by priorProfit after withdrawal
|
|
splitEnabled: boolean; // false = big days trade as one unsplit leg instead of random ≥$500 pieces
|
|
minLegs: number; // big days (> $1,700) must carve at least this many legs (0 = however the carve falls)
|
|
firstDayStop: boolean; // first cycle day carries a $1,000 account stop - books red and ends
|
|
minDayStop: boolean; // min-day trades carry a 2x-target stop; next day after a loss rides stopless
|
|
combineCmeHolidays: boolean; // true = CME holiday sessions count with the next full trading day (Apex-style)
|
|
withdrawalStages: { profit: number; consistency: number; minTradingDays: number; drain?: boolean; minDayPnL?: number; withdrawalPct?: number; payoutCap?: number }[]; // per-stage fresh profit + consistency (empty = use base values); drain = spam-withdraw stage
|
|
/** Payout model, from the master's payout settings (absent on old masters):
|
|
* withdraw withdrawalPct of balance (capped at payoutCap) per cycle,
|
|
* receive profitSplit of the gross; trailingDrawdown feeds the ruin
|
|
* probabilities. payoutable=false means completing the target just
|
|
* PASSES the account (an eval) rather than paying out. */
|
|
payoutable?: boolean;
|
|
withdrawalPct?: number;
|
|
payoutCap?: number;
|
|
profitSplit?: number;
|
|
trailingDrawdown?: number;
|
|
/** Hard daily loss limit in dollars (0/absent = none): every leg carries a
|
|
* stop that keeps the firm's day inside it, less a reserve and fees. */
|
|
dailyLossLimit?: number;
|
|
/** The firm trades every calendar day, weekends and CME holidays included. */
|
|
tradesEveryDay?: boolean;
|
|
}
|
|
|
|
/** An account type as the firm listing (/api/firms) carries it: the current
|
|
* rules, plus the windows of the old rule sets it keeps, so the dashboard can
|
|
* preview which rules a rules date lands on. */
|
|
export interface FirmAccountType extends AccountConfig {
|
|
/** Sorted by closedAt; empty when every account is on the current rules. */
|
|
kept: KeptRulesWindow[];
|
|
}
|
|
|
|
export interface FirmConfig {
|
|
/** Master's firm id (uuid). */
|
|
id: string;
|
|
firm: string;
|
|
/** Which platform the firm's accounts trade on — decides both which client
|
|
* drives it and which credential fields the dashboard asks for. */
|
|
platform: FirmPlatform;
|
|
/** Blank when this machine has no credentials for the firm. */
|
|
username: string;
|
|
/** True once the platform's credentials are stored locally — a username +
|
|
* password (Tradovate, DXTrade) or a TopstepX API key. Firms without them
|
|
* are shown greyed out with an inline credential form. */
|
|
hasCredentials: boolean;
|
|
/** What the firm trades (lib/instruments.ts): CME roots for futures
|
|
* firms, its own symbols for DXtrade. */
|
|
instruments: string[];
|
|
/** 'master' = the catalog's list; 'legacy' = a master from before the
|
|
* lists, read the old way (this machine's enabled set less bans). */
|
|
instrumentsSource: 'master' | 'legacy';
|
|
/** The firm's account types, straight from the catalog. Doubles as the
|
|
* option list for a manual account-type override. */
|
|
accounts: FirmAccountType[];
|
|
catalogUpdatedAt: string;
|
|
}
|
|
|
|
export interface AccountState {
|
|
id: number;
|
|
name: string;
|
|
active: boolean;
|
|
amount: number;
|
|
realizedPnL: number;
|
|
daysTraded: number;
|
|
positionDirection: 'long' | 'short' | null;
|
|
/** Balance floor from Tradovate's auto-liquidation profile (0 = not set) */
|
|
autoLiqThreshold: number;
|
|
/** Sum of all historical daily P&L entries */
|
|
totalProfit: number;
|
|
/** True when today's realizedPnL has met or exceeded the computed daily target */
|
|
targetHit: boolean;
|
|
/** True when a stop-enabled account booked its day red via its own stop —
|
|
* the personal DLL. It stands down until the next session's make-back. */
|
|
personalDll: boolean;
|
|
/** True when today's trading finished the current cycle — profit target,
|
|
* consistency and minimum days all met (cycleCompletion 'today'). Only
|
|
* the final day; the firm may lock or remove the account afterwards. */
|
|
completed: boolean;
|
|
/** Current stage = 1 + number of withdrawals (negative fund transactions). Stage 1 = fresh account. */
|
|
stage: number;
|
|
/** True when the current stage is a drain stage (spam-withdraw mode). */
|
|
stageDrain: boolean;
|
|
/** Next trading day's target, computed server-side. null when account is dead or config is missing. */
|
|
dailyTarget: { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } | null;
|
|
/** Dollars of price movement the next trade may lose before its stop
|
|
* fires — the tighter of the obfuscation stop and the hard daily-loss
|
|
* stop, before fees. null = the next trade rides stopless (options off,
|
|
* big non-first day, or a make-back day after a loss, with no daily
|
|
* loss limit). */
|
|
nextStop: number | null;
|
|
/** What the firm's day may still lose before a hard daily-loss breach.
|
|
* null = no daily loss limit (or its day-start balance isn't known yet). */
|
|
dllRoom: number | null;
|
|
/** DXtrade: how the next leg would be sized and how long it is expected to
|
|
* hold, from today's measured volatility of the last-picked instrument.
|
|
* null on other platforms, or before the day's first pick. */
|
|
holdEstimate: {
|
|
hours: number;
|
|
symbol: string;
|
|
notional: number;
|
|
/** Notional ÷ account size. */
|
|
multiplier: number;
|
|
volatilityPerDay: number;
|
|
winProbability: number | null;
|
|
/** The same leg with no commission, spread or swap. */
|
|
winProbabilityNoFees: number | null;
|
|
/** Expected swap a winning leg pays, added to its take-profit. */
|
|
swapReserve: number;
|
|
/** The broker's leverage on the symbol (5 = 5:1); null when unstated. */
|
|
leverage: number | null;
|
|
/** Expected spread, commissions and swap for the leg. */
|
|
cost: number;
|
|
} | null;
|
|
/** The next leg rides without a daily-loss stop: a stop-out would leave
|
|
* the account too close to its max-drawdown floor to trade on. */
|
|
lastStand: boolean;
|
|
/** Actual profit target to hit — max(stage profit target, consistency realTarget = maxDay/consistency). */
|
|
effectiveProfitTarget: number | null;
|
|
/** Current cycle daily P&L — used for target calculations. */
|
|
dailyPnL: { date: string; pnl: number }[];
|
|
/** Full P&L history across all cycles — used for calendar and equity curve display. */
|
|
fullDailyPnL: { date: string; pnl: number }[];
|
|
/** Fund transactions (deposits/withdrawals) — used by the calendar and equity curve. */
|
|
fundTransactions: { date: string; amount: number }[];
|
|
/** Master account type this account resolved to. null = Unassigned. */
|
|
accountTypeId: string | null;
|
|
accountTypeLabel: string | null;
|
|
assignmentSource: 'manual' | 'auto' | 'unassigned';
|
|
/** The resolved config inlined, so client components need no second lookup. */
|
|
accountType: AccountConfig | null;
|
|
/** Size from the initial funding transaction; null until reports return. */
|
|
inferredAccountSize: number | null;
|
|
/** True when more than one account type matched and updatedAt decided it. */
|
|
accountTypeAmbiguous: boolean;
|
|
/** ISO. Decides which of the type's rule sets the account trades on: one
|
|
* dated before a firm's rule change keeps the old rules. Defaults to when
|
|
* the account started (broker's first deposit / registration), else when
|
|
* this machine first saw it. null = no assignment row yet. */
|
|
rulesDate: string | null;
|
|
/** True when the rules date was set by hand — broker evidence never moves it. */
|
|
rulesDateManual: boolean;
|
|
/** The old rule set the account trades on; null = the type's current
|
|
* rules. accountType above already carries these rules. */
|
|
keptRules: (KeptRulesWindow & {
|
|
/** Rule fields where the kept set differs from the type's current
|
|
* rules — raw values; the dashboard formats them. */
|
|
differs: { field: string; kept: unknown; now: unknown }[];
|
|
}) | null;
|
|
}
|
|
|
|
export interface FirmState {
|
|
/** Master's firm id — join state to config on this, not on the name, which
|
|
* changes the moment someone renames a firm on the master. */
|
|
firmId: string;
|
|
firm: string;
|
|
connected: boolean;
|
|
accounts: AccountState[];
|
|
}
|