/** * The surface a platform trading client must expose. TradovateClient grew this * shape organically; TopstepXClient implements the same one so the scheduler, * reporter, state route and resolver can drive either platform without caring * which is behind the map entry. * * Contract ids are `number` on Tradovate and `string` (e.g. * "CON.F.US.ENQ.Z25") on TopstepX — everything that compares them only ever * compares within one client, so the union is safe. */ export interface PlatformAccount { id: number; name: string; active: boolean; } export interface PlatformContract { id: number | string; name: string; tickSize: number; } export interface PlatformClient { accountList: PlatformAccount[]; accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number } }; positions: { [accountId: number]: { contractId: number | string; netPos: number; netPrice: number; timestamp: Date; }; }; daysTraded: { [accountId: number]: number }; dailyPnL: { [accountId: number]: { date: string; pnl: number }[] }; fullDailyPnL: { [accountId: number]: { date: string; pnl: number }[] }; lastFundDates: { [accountId: number]: string | null }; fundTransactions: { [accountId: number]: { date: string; amount: number }[] }; priorProfit: { [accountId: number]: number }; autoLiqThresholds: { [accountId: number]: number }; fetchDaysComplete: boolean; syncComplete: boolean; lastFetchErrors: Record; lastFetchRaw: Record; recentEntityEvents: { entityType: string; eventType: string; entity: unknown; ts: number }[]; products: { id: number; name: string }[]; fetchDaysTraded(onlyAccountIds?: number[]): Promise<{ failedAccounts: number }>; findFrontMonthContract(productName: string): Promise; fetchInstrumentFees(symbols: string[]): Promise<{ [symbol: string]: number }>; sendOrder( accountId: number, contractSymbol: string, quantity: number, action: 'Buy' | 'Sell', orderType: 'Market' | 'Limit', price?: number ): Promise>; placeOrderNoWait( accountId: number, contractSymbol: string, quantity: number, action: 'Buy' | 'Sell', orderType: 'Market' | 'Limit', price?: number ): Promise<{ orderId?: number }>; placeOcoExit( accountId: number, contractSymbol: string, quantity: number, exitAction: 'Buy' | 'Sell', limitPrice: number, stopPrice: number, ): Promise<{ orderId?: number }>; disconnect(): void; onFill(orderId: number, callback: (fill: Record) => void): void; } /** lastFetchErrors note for a Tradovate account whose Cash History is * unavailable (passed accounts) and whose rows came from the Fills report — * a fresh fetch, not a failure. */ export const FILLS_FALLBACK_NOTE = 'cash history unavailable (using fills fallback)'; /** * Can this account's daily P&L be trusted for a verdict the master keeps * for good (cycleCompletion 'today')? Not while a fetch run is in flight — * at startup Tradovate serves the DB cache before each account's fetch — * and not when the account's last fetch failed and left cached rows behind. */ export function historyFresh(client: PlatformClient, account: PlatformAccount): boolean { if (!client.fetchDaysComplete) return false; const error = client.lastFetchErrors?.[account.name]; return !error || error === FILLS_FALLBACK_NOTE; }