src/orders/resting.ts
v0.2.1 · 4.7 KB
// Waiting for a resting order to be reflected by the book (knowledge base: orders.md §5.4, §7.2).
//
// The placement response has no order_index. The order shows up in `accountActiveOrders` seconds later
// next to your client_order_index. "Not found within the timeout" is NOT "no order": it can be an IoC
// without remainder, a rejection, or a lag longer than the wait - reconcile on the next tick.
/** Default wait: ~5 s in 700 ms steps. */
export const DEFAULT_RESTING_TIMEOUT_MS = 5_000;
export const DEFAULT_RESTING_POLL_INTERVAL_MS = 700;
export interface RestingOrderLike {
orderId: string;
clientOrderIndex: number | null;
marketIndex: number;
}
export interface ResolveRestingInput<T extends RestingOrderLike> {
/** Full read of active orders (e.g. `rest.accountActiveOrders(accountIndex)`). */
readOpenOrders: () => Promise<ReadonlyArray<T>>;
clientOrderIndex: number;
marketIndex: number;
/** Default {@link DEFAULT_RESTING_TIMEOUT_MS}. */
timeoutMs?: number;
/** Default {@link DEFAULT_RESTING_POLL_INTERVAL_MS}. */
intervalMs?: number;
sleep?: (ms: number) => Promise<void>;
now?: () => number;
}
export type ResolveRestingResult<T> =
| { found: true; orderId: string; order: T; reads: number }
/** Not reflected within the timeout. Not proof of absence. */
| { found: false; reads: number };
/** Polls the book until an order with the given `client_order_index` on `marketIndex` appears. */
export async function resolveResting<T extends RestingOrderLike>(
input: ResolveRestingInput<T>,
): Promise<ResolveRestingResult<T>> {
const timeoutMs = input.timeoutMs ?? DEFAULT_RESTING_TIMEOUT_MS;
const intervalMs = input.intervalMs ?? DEFAULT_RESTING_POLL_INTERVAL_MS;
const sleep = input.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
const now = input.now ?? Date.now;
const deadline = now() + timeoutMs;
let reads = 0;
for (;;) {
reads++;
const orders = await input.readOpenOrders();
const hit = orders.find(
(o) => o.marketIndex === input.marketIndex && o.clientOrderIndex === input.clientOrderIndex,
);
if (hit) return { found: true, orderId: hit.orderId, order: hit, reads };
if (now() >= deadline) return { found: false, reads };
await sleep(intervalMs);
}
}
export interface WaitForOrderKeyInput {
/** Placement keys (`pending.placeKey`) of the orders currently in the book. */
readKeys: () => Promise<ReadonlySet<string>>;
key: string;
timeoutMs?: number;
intervalMs?: number;
sleep?: (ms: number) => Promise<void>;
now?: () => number;
/** Called once when the key appears (e.g. `memory.forgetPlaced(key)`). */
onFound?: (key: string) => void;
}
/**
* Waits for an order with the placement key (coin + side + price) to appear in the book.
* `true` = appeared; `false` = not within the timeout, which is not "the order does not exist".
*/
export async function waitForOrderKeyInBook(input: WaitForOrderKeyInput): Promise<boolean> {
const timeoutMs = input.timeoutMs ?? DEFAULT_RESTING_TIMEOUT_MS;
const intervalMs = input.intervalMs ?? DEFAULT_RESTING_POLL_INTERVAL_MS;
const sleep = input.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
const now = input.now ?? Date.now;
const deadline = now() + timeoutMs;
for (;;) {
if ((await input.readKeys()).has(input.key)) {
input.onFound?.(input.key);
return true;
}
if (now() >= deadline) return false;
await sleep(intervalMs);
}
}
/** Send order within a tick: cancels first, then IoC, then reduceOnly GTC (protection), then ordinary GTC. */
export type SendClass = 'cancel' | 'ioc' | 'gtcReduceOnly' | 'gtc';
/** Rank for {@link sortForSending}. @experimental the IoC -> GTC RO -> GTC ordering of placements was designed but not verified live. */
export const SEND_ORDER: Record<SendClass, number> = { cancel: 0, ioc: 1, gtcReduceOnly: 2, gtc: 3 };
/** Classifies an outgoing action for {@link sortForSending}. */
export function sendClassOf(
action: { kind: 'cancel' } | { kind: 'place'; tif: 'Gtc' | 'Ioc'; reduceOnly: boolean },
): SendClass {
if (action.kind === 'cancel') return 'cancel';
if (action.tif === 'Ioc') return 'ioc';
return action.reduceOnly ? 'gtcReduceOnly' : 'gtc';
}
/**
* Stable sort of a tick's actions: cancels, then IoC, then reduceOnly GTC, then ordinary GTC. With a
* write window, more entry orders than the window holds would otherwise starve the protection behind them.
* Cancels always go first; the placement order is @experimental (see {@link SEND_ORDER}).
*/
export function sortForSending<T>(actions: readonly T[], classOf: (a: T) => SendClass): T[] {
return actions
.map((a, i) => ({ a, i, rank: SEND_ORDER[classOf(a)] }))
.sort((x, y) => x.rank - y.rank || x.i - y.i)
.map((x) => x.a);
}