UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

94 lines 5.55 kB
/** * # The IBKR PAPER **DRY-RUN ORDER TICKET** (kestrel-7o2.8) — env-gated, TRANSMITS NOTHING * * Not a test (it is not a `*.test.ts` and never runs under `bun test`): a hand-run script that drives * the whole paper order face against a LIVE, client-launched IB Gateway and **prints the order ticket * a human then reviews** — the contract identity, the side/qty, the `@fair`-anchored limit price WITH * ITS RECEIPT, the observed bid/ask (a HEALTH SIGNAL, never a price), the max loss, the budget, and * the L0 clamp verdict. * * ## THE HARD RULE: THE DEFAULT PATH TRANSMITS NOTHING * It resolves the contract, pulls the quotes, computes the price, runs the L0 clamp, prints the * ticket — and STOPS. `IbkrBroker.preflight` is what it calls: the full guard chain WITHOUT the * `placeOrder`. Transmission happens ONLY if `KESTREL_IBKR_TRANSMIT=1` is explicitly set, which is a * separate, deliberate, HUMAN act. The dry run is the default and says so loudly. * * ## What it is (and is not) allowed to be * - The instrument is **SPY** — a generic, publication-safe ticker. Nothing here reveals an * application or a strategy (ARCHITECTURE §7). * - The action is a **DEFINED-RISK LONG**: BUY 1 option contract. Max loss = the premium, so * never-naked holds trivially and the ticket can be checked by eye. * - `mode` is **paper**. The order face refuses `live` at construction. * * ## The quote source is INJECTED * kestrel-7o2.7 (the `ibkrFeed` FeedSource) is a parallel bead and does not exist on this branch, so * this script pulls its two-sided quotes directly through the transport's shared socket * ({@link reqMktDataQuotes}). That path is behind an injected {@link QuoteSource}, so the real feed * drops in later without touching this script. * * ## Run it * ```bash * KESTREL_IBKR_PAPER_E2E=1 KESTREL_IBKR_CLIENT_ID=14 bun run src/adapters/broker/ibkr/order-dryrun.ts * ``` */ import { EventName } from "@stoqey/ib"; import type { Contract } from "@stoqey/ib"; import { IbkrTransport } from "./transport.ts"; import type { PriceAnchor, QuoteFreshness } from "./broker.ts"; /** * A two-sided quote for one contract, WITH ITS PROVENANCE. * * `bid`/`ask` are a HEALTH SIGNAL (is this thing tradeable?) — never a value, and never a price on * their own. But they are not decoration either: they BOUND `@fair`, which is the only thing that * prices an order. And `@fair` may only be computed from LIVE input, so `freshness` travels WITH the * numbers — a quote that cannot say where it came from cannot anchor anything (kestrel-7o2.8). */ export interface TwoSidedQuote { readonly bid: number | null; readonly ask: number | null; readonly last: number | null; /** What the VENUE served. Only `live` may ANCHOR a price; the rest are health signals only. */ readonly freshness: QuoteFreshness; } /** * Where the dry run gets its quotes (kestrel-7o2.8). INJECTED so the real `ibkrFeed` FeedSource * (kestrel-7o2.7, a parallel bead) substitutes for {@link reqMktDataQuotes} later without this script * changing at all. */ export interface QuoteSource { quote(contract: Contract): Promise<TwoSidedQuote>; } /** * The default {@link QuoteSource}: `reqMktData` over the transport's ONE shared socket. It collects * `tickPrice` until BOTH sides are present or the deadline lapses, then cancels the subscription. * * ## It asks for LIVE, and it REPORTS what it actually got (kestrel-7o2.8, blocker 4a) * The shipped version asked for `DELAYED_FROZEN` and fed the result to `@fair` as a PRICE ANCHOR. That * is the defect: a price anchor must never accept delayed/frozen input. So we request **LIVE** (type * 1) — and because IB SUBSTITUTES rather than refusing when the account lacks a live subscription, we * also LISTEN: the venue's own `marketDataType` callback, and the arrival of the DELAYED tick ids * (66/67/68), each stamp the quote's {@link TwoSidedQuote.freshness}. A delayed quote is still returned * — it is an honest health signal, and a MISSING one would look like a dark book — but it is returned * WEARING ITS LABEL, and everything that PRICES an order refuses it (the broker's price-anchor wall). */ export declare function reqMktDataQuotes(transport: IbkrTransport, timeoutMs?: number): QuoteSource; /** * Build the broker's {@link PriceAnchor} from an OBSERVED quote and the `@fair` derived from it — * fail-closed: a quote that is not LIVE and two-sided yields NO anchor at all, and the broker's * price-anchor wall then refuses the order. This is the ONE place a script turns a quote into * something that may price an order, and it is deliberately unable to launder a stale one. */ export declare function priceAnchorFrom(quote: TwoSidedQuote, fair: number): PriceAnchor | undefined; /** * The order-id minter. The sequence is the GATEWAY's (`nextValidId`) — never ours to invent, and never * an RNG. It is drawn ONLY on the transmit path; the dry run never reaches it. If the gateway never * issued a seed, a transmit FAILS CLOSED rather than guessing an id that might collide with a live one. * * EXPORTED (kestrel-7o2.12) so the paper session driver's venue composition draws its order ids from * this ONE rule rather than a second copy of it — a rule that exists twice is a rule that diverges. */ export declare function mintOrderId(client: { on(event: EventName, listener: (...args: never[]) => void): unknown; reqIds(n?: number): unknown; }): () => number; //# sourceMappingURL=order-dryrun.d.ts.map