Repository Public
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