/** * TradingEngine — composes Portfolio + RiskEngine + ExchangeClient into the * single surface the Franklin capabilities call into. * * Order flow for a buy (sells mirror it): * 1. Fee-less local risk pass — malformed or obviously over-limit orders are * refused before a future adapter spends money or rate-limit capacity on * a fee quote. * 2. `exchange.estimateFee()` — a quote failure is a `blocked` outcome of * kind `fee-quote`, never a thrown error. * 3. Fee-aware risk pass — `notional + fee` must fit cash and the caps. * 4. `exchange.placeOrder()` — a throw here means "not executed" (see the * ExchangeClient contract) and is a `blocked` outcome of kind `venue`. * 5. Record-then-flag. Once `placeOrder` resolves, the venue has executed: * the fill is ALWAYS booked into the Portfolio. Anything that disagrees * with what the risk check approved — a fee above the ceiling, a * quantity or price that drifted, a cap the canonical fill now breaches, * a negative cash balance — is reported in `warnings` and journaled, not * used to discard the fill. A post-execution rejection would leave real * money at the venue that the local book does not know about, and the * resulting tool error invites the model to buy again. * * The one post-execution throw left is an UNBOOKABLE fill — a side that is * not buy/sell, a symbol for another market, a NaN quantity. Nothing sane * can be recorded from that; the error names the clientOrderId to reconcile. * * `openPosition` and `closePosition` are serialised per engine: two calls in * flight against the same Portfolio would both pass risk on the same cash * snapshot and both settle. * * The engine holds no state itself beyond the injected dependencies and the * serialisation queue; that keeps the class easy to unit-test and lets us * swap the ExchangeClient for a real adapter without touching capability * plumbing. */ import type { ExchangeClient } from './exchange.js'; import type { Portfolio } from './portfolio.js'; import type { RiskEngine } from './risk.js'; export interface OpenPositionRequest { symbol: string; qty: number; priceUsd: number; } export interface CloseRequest { symbol: string; qty?: number; } /** * Why an order did not reach the venue. The tool layer chooses its advice * to the model from this — "try a smaller qty" is right for `risk` and wrong * for everything else. */ export type BlockKind = 'risk' | 'fee-quote' | 'price' | 'venue'; export interface FilledOutcome { status: 'filled'; fill: { symbol: string; qty: number; priceUsd: number; feeUsd: number; clientOrderId: string; }; /** Post-execution disagreements with what risk approved. Empty when the venue delivered exactly. */ warnings: string[]; } export type Outcome = FilledOutcome | { status: 'blocked'; kind: BlockKind; reason: string; } | { status: 'noop'; reason: string; }; export interface TradingEngineDeps { portfolio: Portfolio; risk: RiskEngine; exchange: ExchangeClient; } export declare class TradingEngine { private deps; private queue; constructor(deps: TradingEngineDeps); /** Run `fn` after every previously queued order has settled. */ private serialize; openPosition(req: OpenPositionRequest): Promise; closePosition(req: CloseRequest): Promise; private openPositionNow; private closePositionNow; private quoteFee; private place; private assertBookable; private unbookable; private book; }