/** * RiskEngine — pre-trade guardrails the agent must clear before an order * touches the exchange. Pure function style: the engine holds only config; * Portfolio state is passed in per call so the same engine is reusable. * * Guardrails enforced: * - Input validation: qty, price and notional must be finite and positive; * a buy's estimated fee must be finite and non-negative. Fails CLOSED — * NaN can never pass a `>` comparison and slip through a cap. * - Cash sufficiency for buys: notional + estimated exchange fee must fit * the cash balance (compared at CASH_EPSILON_USD precision). * - Per-position cap (USD notional any single symbol may hold) * - Total exposure cap (sum of all open positions' notional) * - Sells: the position must exist and the sale must not exceed it; an * estimated fee, when supplied, must not consume the whole proceeds. * * Exit orders (sells of existing positions) bypass exposure caps — a paranoid * cap could otherwise trap the agent in a losing position it wants to exit. * * This is the documented last line of defense (docs/CONVICTIONS.md #5): the * LLM-facing tool validates too, but every order through TradingEngine — * buys AND sells — is re-evaluated here by code. */ import type { Portfolio } from './portfolio.js'; import type { ExchangeOrder } from './exchange.js'; export interface RiskConfig { maxPositionUsd: number; maxTotalExposureUsd: number; } /** * A buy MUST carry the estimated fee — omitting it is a type error, not a * silent fee-blind check. A sell MAY carry one (the engine quotes it) so the * "fee eats the whole sale" case is caught before the order is placed. */ export type OrderRequest = (Omit & { side: 'buy'; feeUsd: number; }) | (Omit & { side: 'sell'; feeUsd?: number; }); export interface RiskDecision { allowed: boolean; reason?: string; } export declare class RiskEngine { private config; constructor(config: RiskConfig); check(portfolio: Portfolio, order: OrderRequest): RiskDecision; private checkSell; private checkBuy; }