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.
547 lines • 39 kB
TypeScript
/**
* # adapters/broker — the venue-agnostic broker/feed adapter SEAM (kestrel-7o2.4, increment 1)
*
* The charter claim this module proves: **`sim | paper | live` is ONE Session path where only the
* gate behind the seam differs** (CONTEXT: Mode — "one engine path; only the gate differs"; sim.ts
* §"One code path, one gate"). This file names the two seams that make that true and the mode-keyed
* factory that selects a gate — WITHOUT any IBKR code, any network, any npm dependency, or any
* real-money path (that is a later increment).
*
* ## The execution seam is ALREADY the Gate (engine/plans.ts)
* The engine hands the gate an {@link OrderIntent} that is **fully resolved before it arrives**: a
* numeric `px` (a silent mid is forbidden, RUNTIME §4), never-naked enforced, the price-resolution
* receipt (`sourceAnnotation`) attached, the directional-guard evidence (`moneyness`/`covered`)
* threaded. So a broker adapter is a **TRANSMITTER, never a re-pricer** — it routes the intent to a
* venue and reports back what the venue did. A {@link BrokerAdapter} IS a {@link Gate}.
*
* ## How the reference gate (SimGate, session/sim.ts) surfaces ORDER events — the pattern to MIRROR
* `SimGate.submit` is SYNCHRONOUS: it rests the intent in the {@link SimFillEngine} (the judge) and
* returns a `string` ref. The fill engine APPENDS its typed ORDER events (`place | fill | reject |
* cancel`) to a cumulative `events` buffer; an injected `drain()` closure slices the fresh ones onto
* the emitted bus (stamping `seq`). The engine learns of fills ONLY by OBSERVING `ORDER fill` bus
* events — `SessionCore.step` feeds each drained fill back via `engine.onEvent(fillEvent)` (the
* fills-before-sweep ordering, RUNTIME §7). The engine never simulates a fill itself.
*
* A {@link BrokerAdapter} mirrors this EXACTLY (it does not invent a divergent event path): it holds
* an `events` buffer of seq-less {@link NewBusEvent}s, its `submit`/`cancel` append `place | fill |
* reject | cancel` records to it, and the SAME injected `drain()` surfaces them + feeds fills back to
* the engine. The {@link OrderAction} set is CLOSED (`place | cancel | fill | reject`, bus/types.ts,
* guarded by `isValidStreamType`) — an adapter stays within it.
*
* ## The FEED seam is the bus itself
* There is NO abstract feed interface in the runtime today: `SessionCore.step(ev: BusEvent)` consumes
* the {@link BusEvent} union directly (a `META` header + `TICK/BOOK` carrying the real
* `OptionQuote[]` legs + `underlier_px` + `TICK/SPOT` carrying `px`/`bid`/`ask`). A {@link FeedSource}
* is therefore exactly a **producer of that SAME union** — the mock replays a deterministic in-memory
* sequence; a live feed would translate a venue's market-data stream into the identical shapes. No new
* event type is introduced, so `SessionCore.step` folds a FeedSource's output with zero changes.
*
* ## Mode is first-class, and LIVE fails closed
* `Mode` is `sim | paper | live` (bus/types.ts); the record layer already branches on it (`fidelityOf`,
* `instanceIdentityOf`). The protocol authorization scopes DELIBERATELY EXCLUDE `broker`/`live` from
* {@link WALLET_SIGNABLE_SCOPES} (protocol/index.ts) — **live authority requires a human signature, never
* a wallet/config alone**. {@link makeGate} encodes exactly that boundary: `sim` returns a SimGate
* (byte-identical to today), `paper` returns a {@link BrokerAdapter}-backed gate, and `live` FAILS CLOSED
* with a typed {@link LiveGateRefused} UNLESS an explicit, human-authorized {@link LiveArm} is present.
* **This increment provides NEITHER a live arm NOR live routing** — so the only correct outcome for `live`
* here is the typed refusal.
*
* @remarks Increment 1 is the SEAM + the fail-closed factory. The green implementation (the reference
* paper adapter's fill model, the SimGate construction, the live arm's signature verification) lands
* behind these shapes; the stubs below throw {@link NotImplemented} until then.
*/
import type { Gate, OrderIntent } from "../engine/index.ts";
import type { BusEvent, Mode, NewBusEvent, Right } from "../bus/index.ts";
import type { SimFillEngine, SpotFillEngine } from "../fill/index.ts";
import { SimGate } from "../session/sim.ts";
/**
* A venue-agnostic broker adapter (kestrel-7o2.4). A BrokerAdapter **IS a {@link Gate}** — the one
* execution seam (engine/plans.ts): `submit` transmits an ALREADY-RESOLVED {@link OrderIntent} to the
* venue and returns the ref the engine correlates later `ORDER` events against; `cancel` pulls a
* resting order. Because the intent arrives fully priced + never-naked-checked + receipt-bearing, the
* adapter is a **transmitter, never a re-pricer** (it must not touch `intent.px`).
*
* ORDER events (`place | fill | reject | cancel`) it produces are surfaced back to the engine EXACTLY
* as {@link SimFillEngine} surfaces the sim judge's: appended to {@link events} (seq-less
* {@link NewBusEvent}s), then an injected `drain()` closure stamps them onto the emitted bus and feeds
* `fill`s back via `engine.onEvent(fillEvent)`. The adapter never invents a divergent event path, and
* every event it emits inhabits the CLOSED `OrderAction` vocabulary.
*/
export interface BrokerAdapter extends Gate {
/** The injected clock (epoch ms) the driver pins before each event — the gate seam is `now`-less
* (RUNTIME §0); the driver owns the clock and pins it, exactly as `SessionCore` pins `SimGate.now`.
* Stamped onto the ORDER events this adapter emits. */
now: number;
/** Transmit a resolved intent to the venue; return the venue ref (the engine correlates fills
* against it). MUST NOT re-price — `intent.px` is the resolved limit. */
submit(intent: OrderIntent): string;
/** Pull a resting order by its ref. A no-op on an unknown/already-terminal ref (fail-closed, never
* a crash). */
cancel(ref: string): void;
/** The cumulative typed ORDER events this adapter has produced, in order — seq-less
* {@link NewBusEvent}s mirroring {@link SimFillEngine.events}. The owner (the emitted stream via the
* injected `drain`) stamps `seq`; a `fill` here is what the engine OBSERVES through
* `engine.onEvent`. Read-only. */
readonly events: readonly NewBusEvent[];
/** The venue's AUTHORITATIVE position PULL (kestrel-7o2.9) — a queryable snapshot of the broker's
* own reported net position per {@link PositionKey}, the source of truth the reconciliation-trip
* compares the engine's EXPECTED positions against (ADR-0034 §4: "reconciliation must anchor to the
* broker's AUTHORITATIVE PULL, not only push"). OPTIONAL: the reference sim/paper mock exposes it
* (an in-memory net-of-fills query); a push-only transport that cannot pull is fail-closed at
* reconcile time. Never re-prices — a read-only report. */
positions?(): PositionSnapshot;
/** The venue's TRUE per-intent contract multiplier — dollars per point per contract (kestrel-7o2.8,
* defense-in-depth). OPTIONAL: a venue that knows each contract's multiplier (the IBKR paper face
* resolves it from its {@link BrokerAdapter} ContractBook — an option `100`, an equity `1`) exposes
* it so the seam's {@link makeLiveClamp L0 clamp} computes `notional = px·qty·multiplier` on the REAL
* per-contract notional, NOT a flat `1×` that is 100× too loose for a 100× option. `makeGate("paper")`
* reads it STRUCTURALLY (the seam never imports a venue transport) and threads it into the clamp. A
* venue that cannot resolve a multiplier omits this; the clamp then keeps its configured flat one. */
multiplierOf?(intent: OrderIntent): number;
}
/** Construction deps for the reference **mock/paper** {@link BrokerAdapter} (kestrel-7o2.4). Mirrors
* how {@link SimFillEngine} + `SessionCore.#drain` are wired: `drain` surfaces this adapter's fresh
* {@link BrokerAdapter.events} onto the emitted stream (and feeds `fill`s back to the engine) after
* every `submit`/`cancel`, exactly as the SimGate closure does. Zero network. */
export interface PaperBrokerDeps {
/** Surface this adapter's fresh ORDER events — the SimGate `() => void drain()` analogue. */
readonly drain: () => void;
/** Contract multiplier (dollars per point per contract). Carried for parity with the fill engine;
* the paper venue's deterministic fill model uses the intent's own `px` as the transmitted limit. */
readonly multiplier?: number;
}
/**
* The reference **mock/paper** broker adapter (kestrel-7o2.4): a deterministic, in-memory,
* ZERO-NETWORK venue that transmits an intent and reports a deterministic paper fill back as an
* `ORDER fill` event — the full paper loop with no IBKR code.
*/
export declare function makePaperBroker(deps: PaperBrokerDeps): BrokerAdapter;
/**
* A venue-agnostic **feed** (kestrel-7o2.4). The runtime has no abstract feed interface: `SessionCore`
* folds the {@link BusEvent} union directly. A FeedSource is exactly a producer of that SAME union — a
* `META` header, `TICK/BOOK` with real `OptionQuote` legs, `TICK/SPOT` with `px`/`bid`/`ask` — so
* `SessionCore.step` consumes it unchanged. The mock replays a deterministic in-memory sequence; a live
* feed would translate a venue's market-data stream into the identical shapes.
*/
export interface FeedSource {
/** The bus events this source produces, in order — the SAME union `SessionCore.step` consumes. */
events(): Iterable<BusEvent>;
}
export declare function replayFeed(events: readonly BusEvent[]): FeedSource;
/** The gate mode (kestrel-7o2.4) — the same closed {@link Mode} vocabulary the bus/record layer keys
* on (`sim | paper | live`). */
export type GateMode = Mode;
/**
* The **risk limits** one {@link LiveArm} authorizes (kestrel-7o2.9) — a plain typed config carried ON
* the arm, checked by the {@link makeLiveClamp L0 pre-transmit clamp} BEFORE any order reaches the wire
* (ADR-0034 §4). All three are hard ceilings; over ANY of them ⇒ a typed {@link ClampRefused}, never a
* transmit (fail-closed / bounded-risk, RUNTIME §8). The broker's own margin check is NOT the risk
* boundary — L0 sits ABOVE the adapter (ADR-0034 §7).
*/
export interface RiskLimits {
/** Max single-order size (contracts/shares) — `intent.qty` may not exceed it. */
readonly maxOrderQty: number;
/** Max ABSOLUTE net position per {@link PositionKey} AFTER the order (given current positions). */
readonly maxPositionQty: number;
/** Max notional per order in dollars: `px × qty × multiplier` may not exceed it. */
readonly maxNotionalUsd: number;
}
/** The UNBOUNDED paper-clamp limits (kestrel-7o2.21) — used when `makeGate("paper", …)` is called with
* NO {@link PaperGateDeps.limits} config. Every ceiling is `+Infinity`, so no order is ever refused for
* size/position/notional, yet the paper gate is STILL a composed {@link LiveClamp} (the kill-switch and
* reconciliation-trip remain reachable) rather than the bare adapter. This keeps the pre-clamp paper
* path's emitted bytes unchanged while making the L0 envelope a real, reachable call site. */
export declare const UNBOUNDED_PAPER_LIMITS: RiskLimits;
/**
* A **human-signed grant** of live authority over a specific {@link RiskLimits} (kestrel-7o2.9). Live
* is NOT wallet-signable — the protocol excludes `broker`/`live` from {@link WALLET_SIGNABLE_SCOPES}
* (`src/protocol/index.ts`), so a wallet/config alone can never arm live; a human signature is
* required. This is the RAW claim a caller presents; {@link armLive} verifies its signature through an
* {@link AuthoritySigner} and only then mints the unforgeable {@link LiveArm}. A grant is a plain
* object — it is NOT itself authority; presenting one proves nothing until the signature verifies.
*/
export interface LiveAuthorityGrant {
/** Must be the non-wallet-signable `"live"` scope (protocol/index.ts). */
readonly scope: "live";
/** The risk limits this human signature authorizes — the signature is OVER these limits, so the
* limits cannot be widened after signing without invalidating the signature. */
readonly limits: RiskLimits;
/** Opaque handle to the human signature over `limits` (never a key/routine — mirrors the protocol
* `CertifiedReceipt.signatureRef` open/closed seam). */
readonly signatureRef: string;
}
/**
* Verifies the human signature on a {@link LiveAuthorityGrant} (kestrel-7o2.9) — the open/closed seam
* that keeps the live-authority proof out of this module. **Production** wires a real signature
* verifier (the human wallet/HSM signature over the canonical limits); **tests** wire a mock signer
* over a deterministic {@link sha256} of the limits. {@link armLive} calls `verify` and fails closed if
* it returns `false` — a grant whose signature does not verify can NEVER mint an arm.
*/
export interface AuthoritySigner {
verify(grant: LiveAuthorityGrant): boolean;
}
/**
* The explicit, human-authorized Kestrel-level **LIVE arm** (kestrel-7o2.4 declared it; kestrel-7o2.9
* makes it REAL and UNFORGEABLE). It is an OPAQUE, BRANDED token — the exported name is a type alias to
* a class ({@link LiveArmToken}) with a private `#` brand, and the class value is NOT exported. Two
* walls make it unforgeable:
*
* 1. **Nominal brand (compile-time).** The private `#brand` field makes a plain object literal NOT
* structurally assignable to `LiveArm` — a forger must reach for `as unknown as LiveArm`, which the
* runtime wall then catches.
* 2. **Construction guard (run-time).** The only mint is {@link armLive}, which verifies a human
* signature ({@link AuthoritySigner}) before `new`-ing the token. Nothing else can construct it, so
* {@link isLiveArm} (a `#brand in x` check) rejects every fabricated value.
*
* The arm CARRIES the {@link RiskLimits} it authorizes (read via {@link armLimits}); those limits are
* what the human signed over, so a caller cannot self-grant wider limits by hand-rolling an arm. Live
* authority is NOT wallet-signable ({@link WALLET_SIGNABLE_SCOPES} excludes `broker`/`live`).
*/
export type LiveArm = LiveArmToken;
/** The unforgeable {@link LiveArm} token (kestrel-7o2.9). NOT exported — the private `#brand` field
* blocks structural forgery AND the class value is unreachable outside this module, so {@link armLive}
* (signature-verified) is the sole mint. */
declare class LiveArmToken {
#private;
/** The risk limits the human signature authorized. */
readonly limits: RiskLimits;
/** The verified grant this arm was minted from (its `signatureRef` is the authority receipt). */
readonly grant: LiveAuthorityGrant;
constructor(
/** The risk limits the human signature authorized. */
limits: RiskLimits,
/** The verified grant this arm was minted from (its `signatureRef` is the authority receipt). */
grant: LiveAuthorityGrant);
/** The forgery-proof runtime brand check — true iff `x` was minted by {@link armLive}. */
static has(x: unknown): x is LiveArmToken;
}
/** The sim-gate ingredients {@link makeGate} needs to build the reference SimGate for `sim` mode —
* the SAME pieces `SessionCore` hands `new SimGate(...)` today, so the `sim` path stays byte-identical.
* (`spotAllowed` = only the strict-cross family has a spot judge; `drain` surfaces fills, RUNTIME §7.) */
export interface SimGateDeps {
readonly fill: SimFillEngine;
readonly spotFill: SpotFillEngine;
readonly spotAllowed: boolean;
readonly drain: () => void;
}
/**
* The paper-mode L0 clamp config (kestrel-7o2.21) — the carrier for the {@link makeGate}(`"paper"`)
* safety envelope. It supplies the {@link RiskLimits} (and multiplier/tolerance/kill-switch/position
* readers) the paper clamp enforces, sourced from CONFIG rather than a human signature — paper is
* deliberately NOT human-signable. `makeGate` mints a {@link PaperArm} from these limits (or uses the
* supplied {@link arm}) and wraps `deps.broker` in a {@link makeLiveClamp L0 clamp}. Absent ⇒ the paper
* gate is still CLAMPED (never the bare adapter) but with UNBOUNDED limits, so its bytes are unchanged
* from the pre-clamp paper path while the kill-switch/reconciliation-trip remain reachable.
*/
export interface PaperGateDeps {
/** Config-supplied risk ceilings the paper clamp enforces. Absent ⇒ unbounded (no size/position/
* notional refusal), but the clamp is still composed (kill-switch + reconciliation reachable). */
readonly limits?: RiskLimits;
/** A pre-minted {@link PaperArm} (from {@link makePaperArm}); overrides {@link limits} if present.
* Deliberately NOT a {@link LiveArm} — a paper gate can never carry live authority. */
readonly arm?: PaperArm;
/** Contract multiplier for `notional = px·qty·multiplier`. Absent ⇒ `1`. */
readonly multiplier?: number;
/** Reconciliation tolerance (absolute qty per key). Absent ⇒ `0`. */
readonly tolerance?: number;
/** The kill-switch to consult/trip. Absent ⇒ the clamp mints a fresh one. */
readonly killSwitch?: KillSwitch;
/** Engine/Blotter EXPECTED positions for the max-position check + reconcile. Absent ⇒ the broker's
* own PULL ({@link BrokerAdapter.positions}) if it exposes one, else empty. */
readonly positions?: () => PositionSnapshot;
/** The broker's AUTHORITATIVE position PULL for the reconciliation-trip. Absent ⇒ the broker's own
* {@link BrokerAdapter.positions} if it exposes one, else empty. */
readonly brokerPositions?: () => PositionSnapshot;
}
/** Everything the mode-keyed factory may need. Exactly one branch is exercised per mode; the others
* are absent (a construction-order convenience, like `ComposedProvider`). */
export interface GateDeps {
/** The SimGate ingredients — required for `sim`. */
readonly sim?: SimGateDeps;
/** The venue adapter — required for `paper` (and, in a later increment, `live`). */
readonly broker?: BrokerAdapter;
/** The paper L0 clamp config (kestrel-7o2.21) — the {@link RiskLimits}/multiplier/tolerance the
* `paper` gate's {@link makeLiveClamp clamp} enforces. Absent ⇒ an unbounded-but-still-composed clamp
* (the paper gate is NEVER the bare adapter). NOT human-signable — paper authority is config, not a
* signature. */
readonly paper?: PaperGateDeps;
/** The explicit human-authorized live arm — the ONLY thing that could unlock `live`. Absent in this
* increment ⇒ `makeGate("live", …)` fails closed with {@link LiveGateRefused}. Accepts an EXPLICIT
* `undefined` (not just omission): "no arm" is a first-class fail-closed input a caller may state
* outright, and it must refuse identically to omitting it (exactOptionalPropertyTypes). */
readonly liveArm?: LiveArm | undefined;
/** The PAPER VENUE to serve this gate from (kestrel-7o2.8) — the name a venue adapter registered
* itself under via {@link registerPaperVenue} (e.g. `"ibkr"`). An alternative to handing
* {@link broker} directly: it lets `makeGate("paper", …)` reach a venue face WITHOUT this module
* importing that venue's transport (so no npm/socket dependency ever lands on the `sim` path). An
* UNREGISTERED name is refused, never a silent fallthrough. */
readonly venue?: PaperVenue | undefined;
}
/** The name a paper venue adapter registers itself under (e.g. `"ibkr"`). */
export type PaperVenue = string;
/** How {@link makeGate} reaches a registered paper venue — a THUNK, so the venue's adapter (and its
* socket/npm dependencies) is only touched when a `paper` gate actually asks for it. */
export type PaperVenueFactory = () => BrokerAdapter;
/**
* Register a venue adapter as the {@link BrokerAdapter} `makeGate("paper", { venue })` serves
* (kestrel-7o2.8). The venue's own module calls this at its composition root, so THIS module never
* imports a venue transport — `sim` keeps its zero-dependency path, byte-identically. Registration is
* PAPER-ONLY by construction: nothing here can unlock `live`, which still requires the human-signed
* {@link LiveArm} and refuses with {@link LiveGateRefused} regardless of what is registered.
*/
export declare function registerPaperVenue(venue: PaperVenue, make: PaperVenueFactory): void;
/** The paper venues registered so far, sorted (a stable, loggable list — never an RNG/insertion-order
* leak into a diagnostic). */
export declare function registeredPaperVenues(): readonly PaperVenue[];
/**
* The typed, fail-closed refusal {@link makeGate} raises for a `live` gate requested without an explicit
* {@link LiveArm} (kestrel-7o2.4). A DISTINCT error class (not a bare `Error`) so a caller can catch the
* live-authority refusal specifically and never mistake it for an unrelated failure — and so a `live`
* request can NEVER silently fall through to a routing gate. Mirrors the protocol boundary: `live` is not
* in {@link WALLET_SIGNABLE_SCOPES}; live authority requires a human signature (RUNTIME §8, fail-closed).
*/
export declare class LiveGateRefused extends Error {
readonly mode: "live";
constructor(reason: string);
}
/** Thrown by the reference stubs until the green implementation lands (kestrel-7o2.4 increment 1 is the
* SEAM + the fail-closed factory; the behavior behind the shapes is a later increment). Distinct from
* {@link LiveGateRefused} so a RED test can tell "not yet built" from "live refused by design". */
export declare class NotImplemented extends Error {
constructor(what: string);
}
/**
* Select the execution gate for a mode (kestrel-7o2.4) — the ONE line the hardwired
* `this.gate = new SimGate(...)` (session/sim.ts) becomes, proving `sim | paper | live` is one Session
* path where only the gate differs:
*
* - `sim` → the reference SimGate over {@link GateDeps.sim} — BYTE-IDENTICAL to today.
* - `paper` → the {@link BrokerAdapter} in {@link GateDeps.broker} (a BrokerAdapter IS a Gate).
* - `live` → FAILS CLOSED with {@link LiveGateRefused} unless {@link GateDeps.liveArm} is present.
* This increment provides no arm and no routing, so `live` ALWAYS refuses here.
*
* The `sim` overload returns the concrete {@link SimGate} (its `now` is pinned by the driver each event),
* so SessionCore keeps its `readonly gate: SimGate` field and this stays a pure refactor.
*
* ## The `paper` gate's L0 GUARANTEE — self-sufficient, never delegated (ADR-0034 §4, kestrel-ct9m)
*
* `makeGate("paper", …)` NEVER returns the bare adapter: it returns the {@link makeLiveClamp L0 clamp}
* wrapped around it. The clamp's contract holds over a transport with NO walls of its own (the reference
* mock today; a future live transport with no venue-side validation tomorrow) — "a bare seam over an
* adapter with no WALL 5 still refuses". Concretely, BEFORE anything is transmitted, `submit` refuses with
* a typed {@link ClampRefused} when ANY of these holds:
*
* - the {@link KillSwitch} is tripped ⇒ `"killed"`
* - `qty` or `px` is NON-FINITE (NaN/±Infinity) ⇒ `"not-a-number"`
* - `qty` is not a POSITIVE INTEGER, or `px` is not POSITIVE ⇒ `"malformed-intent"`
* - the contract multiplier is non-finite or non-positive ⇒ `"multiplier"`
* - `qty` exceeds `maxOrderQty` ⇒ `"order-size"`
* - `|projected net position|` exceeds `maxPositionQty` ⇒ `"position"`
* - `px·qty·multiplier` exceeds `maxNotionalUsd` ⇒ `"notional"`
*
* The WELL-FORMEDNESS rows are load-bearing, not hygiene: every ceiling above them is a `>` comparison,
* and a `>` comparison against a NaN, a negative, or a zero is `false` — i.e. it PASSES. Without those
* rows a `qty = -1_000_000` sell clears the size ceiling, drives the notional negative past
* `maxNotionalUsd`, and sign-inverts the projected position. Validity is checked HERE and not deferred to
* the venue face's own WALL 1, because on a bare seam there is no WALL 1 to defer to.
*/
export declare function makeGate(mode: "sim", deps: GateDeps): SimGate;
export declare function makeGate(mode: GateMode, deps: GateDeps): Gate;
/** A net-position key (kestrel-7o2.9): `` `${instrument}|${strike}|${right}` `` for an option leg,
* bare `instrument` for a spot/equity leg (ADR-0017 — no fictional strike). The unit the L0
* max-position check and the reconciliation-trip both key on. */
export type PositionKey = string;
/** A snapshot of SIGNED net position per {@link PositionKey} (kestrel-7o2.9) — buy `+`, sell `−`. Two
* readings are compared at reconcile: the engine/Blotter EXPECTED snapshot vs the broker's
* AUTHORITATIVE PULL ({@link BrokerAdapter.positions}). */
export interface PositionSnapshot {
readonly [key: PositionKey]: number;
}
/** The {@link PositionKey} for an order/leg (kestrel-7o2.9) — pure, so the clamp, the engine-expected
* snapshot, and the broker PULL all key identically (no skew). */
export declare function positionKeyOf(o: {
readonly instrument: string;
readonly strike?: number;
readonly right?: Right;
}): PositionKey;
/**
* Mint an unforgeable {@link LiveArm} from a human-signed {@link LiveAuthorityGrant} (kestrel-7o2.9) —
* the SOLE construction path. Verifies the grant's signature through the injected {@link AuthoritySigner}
* and, only if it verifies, `new`s the branded token carrying the signed {@link RiskLimits}. FAIL-CLOSED:
* a grant whose signature does not verify (a wallet/config forgery, a widened-after-signing limit set)
* throws {@link LiveGateRefused} — it can NEVER mint an arm. Live is not wallet-signable (protocol
* excludes `broker`/`live` from {@link WALLET_SIGNABLE_SCOPES}); this is the human-signature wall.
*/
export declare function armLive(grant: LiveAuthorityGrant, signer: AuthoritySigner): LiveArm;
/** The forgery-proof runtime brand check (kestrel-7o2.9) — true iff `arm` was minted by {@link armLive}.
* A plain object literal (even one shaped like a grant, even cast `as unknown as LiveArm`) returns
* `false`: the private `#brand` is unreachable except through the sole mint. REAL (not a stub) — a
* brand read has no behavior to defer, and the clamp/factory rely on it to reject fabricated arms. */
export declare function isLiveArm(arm: unknown): arm is LiveArm;
/** The {@link RiskLimits} an {@link LiveArm} authorizes (kestrel-7o2.9) — the exact limits the human
* signed over, read off the opaque token. REAL (not a stub). */
export declare function armLimits(arm: LiveArm): RiskLimits;
/**
* A **PAPER arm** (kestrel-7o2.21) — a distinct, BRANDED authority that carries config-supplied
* {@link RiskLimits} for the paper venue WITHOUT any human signature. It is the paper-mode sibling of
* {@link LiveArm}: same "an unforgeable token that carries limits" shape, but minted by a plain config
* mint ({@link makePaperArm}) rather than the signature-verified {@link armLive}. This is what lets the
* {@link makeLiveClamp L0 clamp} wrap the PAPER broker — the clamp becomes a REAL call site of the L0
* envelope instead of dead code — WITHOUT fabricating live authority.
*
* The wall that keeps this safe: a PaperArm is NOT a {@link LiveArm} ({@link isLiveArm} returns `false`
* for it, its `#paperBrand` is a different private field than {@link LiveArmToken}'s `#brand`), so it can
* NEVER authorize a live gate — `makeGate("live", …)` rejects it fail-closed, and a clamp built from a
* PaperArm records `provenance: "paper"`. Paper is deliberately not human-signable; a PaperArm proves
* only paper authority, never live. {@link armLive} remains the SOLE mint of {@link LiveArm}.
*/
export type PaperArm = PaperArmToken;
/** The branded {@link PaperArm} token (kestrel-7o2.21). NOT exported — the private `#paperBrand` field
* (distinct from {@link LiveArmToken}'s `#brand`) blocks structural forgery AND makes it un-confusable
* with a LiveArm, so {@link makePaperArm} is the sole mint and {@link isPaperArm} is a total check. */
declare class PaperArmToken {
#private;
/** The config-supplied risk limits this paper arm authorizes (NOT a human signature). */
readonly limits: RiskLimits;
constructor(
/** The config-supplied risk limits this paper arm authorizes (NOT a human signature). */
limits: RiskLimits);
/** True iff `x` was minted by {@link makePaperArm}. */
static has(x: unknown): x is PaperArmToken;
}
/**
* Mint a {@link PaperArm} from plain config {@link RiskLimits} (kestrel-7o2.21) — the paper-mode
* counterpart to {@link armLive}, but with NO signature verification, because paper carries no
* real-money risk and is deliberately NOT human-signable. The minted arm is a branded token, so a
* forged/literal object still cannot pass as authority (the {@link makeLiveClamp} construction guard
* rejects anything neither {@link isLiveArm} nor {@link isPaperArm}). It authorizes ONLY paper: it is
* not a {@link LiveArm} and can never unlock `makeGate("live", …)`.
*/
export declare function makePaperArm(limits: RiskLimits): PaperArm;
/** The forgery-proof runtime brand check for a {@link PaperArm} (kestrel-7o2.21) — true iff `arm` was
* minted by {@link makePaperArm}. A plain literal (even cast `as unknown as PaperArm`) returns `false`,
* and a genuine {@link LiveArm} returns `false` too (distinct brand): paper and live authority never
* cross-authorize. REAL (not a stub). */
export declare function isPaperArm(arm: unknown): arm is PaperArm;
/** The authority a {@link makeLiveClamp L0 clamp} may be built on (kestrel-7o2.21): a signature-verified
* {@link LiveArm} OR a config-minted {@link PaperArm}. Both are BRANDED tokens — a raw/forged literal is
* neither, and the clamp constructor rejects it fail-closed. The clamp records which one built it
* ({@link LiveClamp.provenance}) so a PaperArm-built clamp is paper-only. */
export type ClampArm = LiveArm | PaperArm;
/** Which L0 boundary a {@link ClampRefused} tripped (kestrel-7o2.9) — a closed, typed reason so a
* caller can tell an over-limit refusal from a killed one and log the exact wall.
*
* `"not-a-number"` and `"multiplier"` (kestrel-7o2.10, A3) are the WELL-FORMEDNESS boundaries that must
* clear BEFORE any ceiling comparison, because a ceiling comparison on a corrupt number FAILS OPEN
* rather than closed:
* - `"not-a-number"` — a non-finite `qty`/`px`. `NaN > ceiling` is `false` for EVERY ceiling, so a NaN
* silently satisfies all three limits and transmits. Fail-closed demands the inverse: an unorderable
* number is REFUSED, never waved through.
* - `"multiplier"` — a non-finite or non-positive contract multiplier. `notional = px·qty·multiplier`,
* so a `0` multiplier computes EVERY notional as `0` and the `maxNotionalUsd` ceiling becomes inert
* (a `NaN`/negative one corrupts it just as silently). The ceiling is only as real as its inputs.
* - `"malformed-intent"` (kestrel-ct9m) — a FINITE but unorderable `qty`/`px`: a qty that is not a
* POSITIVE INTEGER (negative, zero, fractional) or a px that is not POSITIVE. Same fail-open shape as
* the NaN one, one step removed: `-1_000_000 > maxOrderQty` is `false`, `px · -1_000_000 · mult >
* maxNotionalUsd` is `false` (the notional goes NEGATIVE), and `projected = current + (buy ? qty :
* -qty)` SIGN-INVERTS, so a million-lot sell clears every ceiling and transmits. A `0`/negative px
* collapses or inverts the notional the same way. The L0 seam may not delegate its own validity to a
* venue face's WALL 1 (ADR-0034 §4: a bare seam over a WALL-less adapter still refuses). */
export type ClampLimit = "order-size" | "position" | "notional" | "killed" | "not-a-number" | "multiplier" | "malformed-intent";
/**
* The typed, fail-closed refusal the {@link makeLiveClamp L0 clamp} raises when an order is over a
* {@link RiskLimits} ceiling OR the {@link KillSwitch} is tripped (kestrel-7o2.9). A DISTINCT error
* class (not a bare `Error`, distinct from {@link LiveGateRefused}) so a caller catches an L0 refusal
* specifically. The order is NEVER transmitted — the clamp throws BEFORE it calls the underlying
* adapter's `submit` (ADR-0034 §4/§7, bounded-risk / never-naked). Carries the tripped {@link ClampLimit}
* and the refused order's `ref` for the logged reason.
*/
export declare class ClampRefused extends Error {
readonly limit: ClampLimit;
readonly ref: string;
constructor(limit: ClampLimit, ref: string, reason: string);
}
/**
* The **kill-switch** (kestrel-7o2.9, ADR-0034 §4) — a stateful, per-process halt the {@link makeLiveClamp
* clamp} consults before EVERY transmit. A single operator action (or an adapter-detected degradation:
* dead feed, lost heartbeat, reconciliation break) `trip`s it; once tripped ALL live transmission is
* refused ({@link ClampRefused}, `"killed"`), fail-closed, with the logged reason. STAND_DOWN is always
* reachable. It only ever latches ON — there is no un-trip on the seam (re-arming is a fresh human act,
* out of band). Shared by injection so a reconciliation-trip and an operator halt hit the same switch.
*/
export interface KillSwitch {
/** Has the switch been tripped? Once `true`, stays `true` (latch). */
readonly tripped: boolean;
/** The reason logged at the trip, or `null` while un-tripped. */
readonly reason: string | null;
/** Trip the switch (idempotent — first reason wins). All subsequent transmits refuse. */
trip(reason: string): void;
}
/** Build a fresh, un-tripped {@link KillSwitch} (kestrel-7o2.9). Latches ON — `trip` is idempotent
* (first reason wins) and there is NO un-trip on the seam; re-arming is a fresh human act, out of band. */
export declare function makeKillSwitch(): KillSwitch;
/**
* The **L0 live clamp** (kestrel-7o2.9) — a {@link BrokerAdapter} wrapper at the live-gate boundary that
* the underlying adapter CANNOT bypass. Its `submit`, BEFORE delegating to the underlying adapter,
* checks the resolved {@link OrderIntent} against the {@link LiveArm}'s {@link RiskLimits} (max order
* size, max position given current positions, max notional `= px·qty·multiplier`) AND the
* {@link KillSwitch}. Over ANY limit, OR killed ⇒ a typed {@link ClampRefused} and the order is NEVER
* transmitted (fail-closed). It also exposes {@link reconcile}, the reconciliation-trip.
*/
export interface LiveClamp extends BrokerAdapter {
/** The kill-switch this clamp consults (the injected one, or a fresh one) — trip it to halt all
* transmission; read {@link KillSwitch.tripped} to observe a reconciliation-trip. */
readonly killSwitch: KillSwitch;
/** Which authority built this clamp (kestrel-7o2.21): `"live"` iff a signature-verified
* {@link LiveArm}, `"paper"` iff a config-minted {@link PaperArm}. Recorded so a PaperArm-built clamp
* is PAPER-ONLY — it can never be presented as a live gate (a paper clamp never wraps a live
* transport; `makeGate("live", …)` refuses a PaperArm at the factory, before any clamp is built). */
readonly provenance: "live" | "paper";
/**
* The **reconciliation-trip** (kestrel-7o2.9, ADR-0034 §4). Pull the broker's AUTHORITATIVE position
* snapshot and compare it, per {@link PositionKey}, against the engine/Blotter EXPECTED snapshot. Any
* divergence beyond `tolerance` (a broker fill the engine did not originate, or an engine order with
* no broker terminal state) ⇒ `trip` the {@link KillSwitch} with a logged reason, halting all further
* transmission. Never a silent divergence. Anchors to the broker's PULL, not the push stream.
*/
reconcile(): void;
}
/** Construction deps for {@link makeLiveClamp} (kestrel-7o2.9). */
export interface LiveClampDeps {
/** The venue adapter the clamp wraps — the reference mock/paper {@link BrokerAdapter} in tests; a live
* transport in production (later beads, behind this envelope). */
readonly underlying: BrokerAdapter;
/** The authority the clamp is built on (kestrel-7o2.21): a signature-verified {@link LiveArm} OR a
* config-minted {@link PaperArm} — both carry the authorized {@link RiskLimits}. A FORGED/literal arm
* (neither {@link isLiveArm} nor {@link isPaperArm}) is REJECTED at construction with a
* {@link LiveGateRefused} — the clamp can never be built on fabricated authority. The clamp records
* which one built it in {@link LiveClamp.provenance}; a PaperArm makes the clamp paper-only. */
readonly arm: ClampArm;
/** The kill-switch to consult/trip. Absent ⇒ the clamp mints a fresh one ({@link LiveClamp.killSwitch}). */
readonly killSwitch?: KillSwitch;
/** The engine/Blotter EXPECTED positions (signed net per {@link PositionKey}) — read for the
* max-position check and the reconciliation-trip's expected side. */
readonly positions: () => PositionSnapshot;
/** The broker's AUTHORITATIVE position PULL for the reconciliation-trip (ADR-0034 §4). In production
* this wires to `underlying.positions()`; injected here so the pull point is explicit and testable. */
readonly brokerPositions: () => PositionSnapshot;
/** Contract multiplier for `notional = px·qty·multiplier` (kestrel-7o2.8). Either a flat `number`
* (one instrument class) OR a per-intent resolver `(intent) => number` — the shape `makeGate("paper")`
* threads for a venue that knows each contract's TRUE multiplier (the IBKR face: option `100`, equity
* `1`), so the notional ceiling is computed on real per-contract notional, never a flat `1×` that is
* 100× too loose for a 100× option. */
readonly multiplier: number | ((intent: OrderIntent) => number);
/** Reconciliation tolerance (absolute qty per key). Absent ⇒ `0` (exact match required). */
readonly tolerance?: number;
}
/**
* Build the {@link LiveClamp L0 pre-transmit risk clamp} over an underlying {@link BrokerAdapter}
* (kestrel-7o2.9). FAIL-CLOSED at construction: a forged/literal {@link LiveArm} (one {@link isLiveArm}
* rejects) throws {@link LiveGateRefused} — the envelope can never be armed on fabricated authority.
* The returned clamp IS a BrokerAdapter (a Gate), so it drops into the same execution seam; its `submit`
* enforces the arm's {@link RiskLimits} + the {@link KillSwitch} above the underlying adapter, and
* {@link LiveClamp.reconcile} trips the switch on a broker-vs-engine position break.
*/
export declare function makeLiveClamp(deps: LiveClampDeps): LiveClamp;
export {};
//# sourceMappingURL=broker.d.ts.map