Skip to content
markpaper

src/orders/resting.ts

v0.2.1 · 4.7 KB

Download file
// 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);
}
All files