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
TypeScript
/**
* # 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