SPB Git

Repository Public

worker.ts 4.7 KB 89 lines typescript

1// Paper worker: every few seconds, for each paper account with a position or a resting order, feed the latest ticker and book2// of its instruments to the paper engine, apply funding at the venue's funding times with the venue's published rate, record3// margin warnings, simulated liquidations and loss-limit breaches, and snapshot equity once a minute.4//5// The step keeps nothing in memory between runs: every decision is made from the database row of the account (engine state,6// policy, funding cursors) inside one transaction per account. Running it twice on the same inputs, or restarting the process7// between two steps, changes nothing — a book already seen is ignored by the engine, a funding time already applied is behind8// the cursor, fills and funding payments are keyed by their engine ids.9import type { DecStr, Instrument } from '@spw/derivatives';10import { loop } from '../worker.ts';11import { derivatives, type Derivatives } from './index.ts';12import type { MarketFeed, PaperService } from './paper.ts';1314export const PAPER_STEP_MS = 3_000;15/** a ticker older than this is not fed to an account (the step waits for fresh data instead) */16export const FEED_MAX_AGE_MS = 15_000;17/** a published funding rate is matched to a funding time when it is stamped within this distance of it */18const FUNDING_MATCH_MS = 5 * 60_000;1920export interface StepSummary { accounts: number; fed: number; fills: number; funding: number; liquidations: number; halts: number; errors: string[] }2122/** rates the venue applied since `cursor`, keyed by the funding time they belong to */23async function fundingRates(svc: PaperService, inst: Instrument, cursor: number): Promise<Map<number, DecStr> | null> {24  const iv = inst.fundingIntervalSec * 1000;25  try {26    const out = new Map<number, DecStr>();27    for (const r of await svc.data.fundingSince(inst, cursor + 1)) {28      const t = Math.round(r.ts / iv) * iv;29      if (Math.abs(r.ts - t) <= FUNDING_MATCH_MS && t > cursor) out.set(t, r.rate);30    }31    return out;32  } catch { return null; }                                           // unreadable now: funding waits, nothing is skipped33}3435export async function paperStep(svc: PaperService, now: number = svc.now()): Promise<StepSummary> {36  const summary: StepSummary = { accounts: 0, fed: 0, fills: 0, funding: 0, liquidations: 0, halts: 0, errors: [] };37  const accounts = await svc.active();38  if (accounts.length === 0) return summary;                         // no exposure anywhere: no venue is called39  summary.accounts = accounts.length;40  const note = (what: string, e: unknown): void => { summary.errors.push(`${what}: ${e instanceof Error ? e.message.slice(0, 160) : 'error'}`); };4142  // market data once per instrument, whatever the number of accounts on it43  const live = new Map<string, Omit<MarketFeed, 'fundingRates'>>();44  await Promise.all([...new Set(accounts.flatMap((a) => a.instruments))].map(async (id) => {45    try {46      const inst = await svc.data.instrument(id);47      const [ticker, book] = await Promise.all([svc.data.ticker(inst), svc.data.book(inst)]);48      if (now - ticker.ts > FEED_MAX_AGE_MS) throw new Error(`ticker is ${Math.round((now - ticker.ts) / 1000)} s old`);49      live.set(id, { inst, ticker, book });50    } catch (e) { note(id, e); }51  }));5253  const rates = new Map<string, Promise<Map<number, DecStr> | null>>();54  for (const a of accounts) {55    const feeds: MarketFeed[] = [];56    for (const id of a.instruments) {57      const f = live.get(id);58      if (!f) continue;59      const cursor = a.cursors[id];60      let ratesOf: Map<number, DecStr> | null = null;61      if (cursor !== undefined && cursor + f.inst.fundingIntervalSec * 1000 <= now) {62        const key = `${id}:${cursor}`;63        if (!rates.has(key)) rates.set(key, fundingRates(svc, f.inst, cursor));64        ratesOf = await rates.get(key)!;65      }66      feeds.push({ ...f, fundingRates: ratesOf });67    }68    if (feeds.length === 0) continue;69    try {70      const r = await svc.tick(a.id, feeds, now);71      summary.fed += 1; summary.fills += r.fills; summary.funding += r.funding;72      if (r.liquidated) summary.liquidations += 1;73      if (r.halted) summary.halts += 1;74    } catch (e) { note(`account ${a.id}`, e); }75  }76  return summary;77}7879export function startDerivativesWorker(d: Derivatives = derivatives()): void {80  let lastError = '';81  loop('deriv-paper', async () => {82    const s = await paperStep(d.paper);83    if (s.fills || s.funding || s.liquidations || s.halts) console.log(`[derivatives] paper: ${s.fed} account(s), ${s.fills} fill(s), ${s.funding} funding payment(s), ${s.liquidations} liquidation(s), ${s.halts} halt(s)`);84    const err = s.errors[0] ?? '';85    if (err && err !== lastError) console.log(`[derivatives] paper: ${s.errors.length} error(s) (${err})`);86    lastError = err;87  }, PAPER_STEP_MS);88}89
Files main
Menu

Theme