This box no longer decides what firms exist. It caches the master's catalog
and stores one thing of its own: the Tradovate login for each firm. Creating
firms locally is gone — app/add, POST /api/firms, the account-config routes
and the banned-symbol route with it.
The cache is the point, not an optimisation. The scheduler needs every
account's target and size on each 60s tick, so fetching them per tick would
stop trading with positions open the first time the master rebooted. It is
one JSON blob in settings, replaced only by a complete valid response, and a
response with zero firms is refused outright: a misconfigured FIRMS_DB serves
{"firms":[]} as a healthy 200, and mirroring that would idle the whole fleet
at once.
Accounts bind to an account type by prefix AND size, the size taken from the
opening funding transaction. That does not arrive with the first sync, so an
account sits Unassigned and the resolver tries again next sync rather than
guessing. A type picked by hand is never overwritten by an auto pass. This
replaces the longest-prefix helper that had been copy-pasted into five files,
which could not tell two types with the same prefix apart at all.
firm_credentials and account_assignments deliberately have no foreign key to
the cached catalog. foreign_keys is ON, so a cascade would erase the Tradovate
passwords the moment a firm was removed on the master — routine, not
exceptional. They survive it, and are re-keyed by name if the master's database
is ever rebuilt with fresh ids.
The legacy firms/account_configs/firm_banned_symbols tables are left on disk,
unreferenced. firms is the only table that ever held plaintext credentials and
dropping it would make a rollback unrecoverable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
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; // 0 = no limit; positive = account is "Dead" when loss exceeds this
|
|
maxPositionSize: number; // 0 = no limit; positive = max contracts per trade
|
|
targetSameEquity: boolean; // if true, reduce profitTarget by priorProfit after withdrawal
|
|
withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[]; // per-stage fresh profit + consistency (empty = use base values)
|
|
}
|
|
|
|
export interface FirmConfig {
|
|
/** Master's firm id (uuid). */
|
|
id: string;
|
|
firm: string;
|
|
/** Blank when this machine has no credentials for the firm. */
|
|
username: string;
|
|
/** True once a Tradovate username and password are stored locally. Firms
|
|
* without them are shown greyed out with an inline credential form. */
|
|
hasCredentials: boolean;
|
|
bannedSymbols: string[];
|
|
/** The firm's account types, straight from the catalog. Doubles as the
|
|
* option list for a manual account-type override. */
|
|
accounts: AccountConfig[];
|
|
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;
|
|
/** Current stage = 1 + number of withdrawals (negative fund transactions). Stage 1 = fresh account. */
|
|
stage: number;
|
|
/** 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;
|
|
/** 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;
|
|
}
|
|
|
|
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[];
|
|
}
|