/** * Tracks opens this runtime has already submitted but cannot yet see in a clearing-house * snapshot, so the slot gate, the dedupe check and the free-margin budget count them. * * Why it has to exist: a clearing-house read is a 1–5 s old picture of the book, signals * arrive in pairs milliseconds apart (they are serialized behind the reconcile mutex), and * nothing tells the runtime when a submit becomes a position. Without a record of what was * just sent, two consecutive signals on a one-slot strategy both read "0 open" from the same * lagging snapshot and both open — the venue is the only thing left enforcing the cap. * * Entries are keyed by the normalized `(coin, dex)` pair so a snapshot row, a DSL position * and a submit all collapse to the same key (`xyz:GOLD` on the main book and `GOLD` on `xyz` * are one market, not two). * * Every entry is a claim about an order whose fate is not settled, so each kind expires on * its own TTL: a reservation that outlives its window is released rather than blocking the * strategy forever. This is a bounded-lifetime cache, NOT a source of truth — a fetched * snapshot always wins, and `reconcile` drops anything the exchange has confirmed. */ import type { OpenPosition } from "../types/strategy.js"; /** * What is known about a submitted open: * - `filled` — the venue accepted it and reported a fill; only the snapshot lags. * - `resting` — a maker order is live on the book, unfilled. It holds no position yet but * will consume a slot and margin the moment it fills. * - `ambiguous` — the outcome is unknown (timeout, transport throw) or not yet determined * (the provisional kind a reservation starts as, before `createPosition` returns). */ export type PendingOpenKind = "filled" | "resting" | "ambiguous"; /** One submitted-but-unconfirmed open. */ export interface PendingOpenEntry { /** Normalized `coin|dex` key — the same key a snapshot row reduces to. */ readonly key: string; readonly coin: string; readonly dex: string; /** Margin the submit committed, subtracted from the free-margin budget while unseen. */ readonly marginAmount: number; readonly kind: PendingOpenKind; /** When the entry last changed kind (the TTL clock). */ readonly ts: number; readonly orderId?: string; } /** * A filled open only has to survive until the next snapshot carries it. Clearing-house * snapshot age runs to roughly 4.6 s at p99, so a minute is two orders of magnitude of * headroom — long enough that no realistic lag drops the entry early, short enough that a * fill the venue silently rolled back does not wedge the strategy. */ export declare const PENDING_FILLED_TTL_MS = 60000; /** * An ambiguous open (timeout, transport throw) may still land server-side long after the * client stopped waiting. The window has to outlast the slowest submit that can still * execute: the MCP ALO ceiling is 330 s, plus the trading engine's own server-side ceiling * behind it. Ten minutes covers both with margin; past that, holding the slot costs more * than the duplicate it prevents. */ export declare const PENDING_AMBIGUOUS_TTL_MS: number; /** * A resting maker order can sit on the book indefinitely — nothing in this runtime cancels * one. Mirroring {@link PENDING_OPEN_STALE_MS} keeps this TTL in step with the other place * the runtime gives up on an unobserved open (the position-id registry). It is a stopgap: * the real fix is reading open orders, not aging a guess out. */ export declare const PENDING_RESTING_TTL_MS: number; export interface PendingOpensLedger { /** * Record an open about to be submitted. Call this SYNCHRONOUSLY before awaiting * `createPosition`: the gap between submit and outcome is exactly the window a second * signal can slip through. Returns the entry key, for `settle`/`release`. */ reserve(coin: string, dex: string | undefined, marginAmount: number): string; /** Record the submit's outcome (and its order id, when the venue gave one). No-op if the key is gone. */ settle(key: string, kind: PendingOpenKind, orderId?: string): void; /** Drop a reservation the venue rejected outright — it holds neither slot nor margin. */ release(key: string): void; /** Drop every entry the snapshot now confirms, plus anything past its TTL. */ reconcile(positions: readonly OpenPosition[] | undefined): void; /** Live entries this snapshot does NOT show — what the caller must add to the snapshot's own count. */ unseen(positions: readonly OpenPosition[] | undefined): PendingOpenEntry[]; } export interface PendingOpensLedgerOptions { /** Injectable clock (tests). */ now?: () => number; /** Per-kind TTL overrides (tests); anything unset uses the documented default. */ ttlMsByKind?: Partial>; } /** * The ledger key for a market: base coin + dex bucket, with `main` and the `xyz:` coin * prefix normalized away so one market never occupies two keys. */ export declare function pendingOpenKey(coin: string, dex?: string): string; export declare function createPendingOpensLedger(options?: PendingOpensLedgerOptions): PendingOpensLedger; //# sourceMappingURL=pending-opens-ledger.d.ts.map