SPB Git

Repository Public

models.ts 7.2 KB 182 lines typescript

1/**2 * Normalised models of the derivatives module (contract: docs/v4/DERIVATIVES-ARCHITECTURE.md, "models.ts").3 * Every model is a zod schema (`XSchema`) exported with its inferred type (`X`). Unknown fields are tolerated (stripped);4 * missing or mistyped fields fail validation. All prices, sizes and amounts are canonical decimal strings.5 */6import { z } from 'zod';7import { DEC_RE, MAX_DIGITS } from './decimal.ts';89/** canonical decimal string: /^-?(0|[1-9]\d*)(\.\d+)?$/, no exponent, at most MAX_DIGITS digits */10export const DecStrSchema = z.string().max(MAX_DIGITS + 2).regex(DEC_RE, 'not a canonical decimal string')11  .refine((x) => x.replace(/[-.]/g, '').length <= MAX_DIGITS, `more than ${MAX_DIGITS} digits`);12const Ts = z.number().int().nonnegative();          // milliseconds since the Unix epoch1314export const VENUE_IDS = ['hyperliquid', 'dydx', 'gmx'] as const;15export const VenueIdSchema = z.enum(VENUE_IDS);16export type VenueId = z.infer<typeof VenueIdSchema>;1718/** position side; an order side is 'buy' | 'sell' */19export const SideSchema = z.enum(['long', 'short']);20export type Side = z.infer<typeof SideSchema>;21export const OrderSideSchema = z.enum(['buy', 'sell']);22export type OrderSide = z.infer<typeof OrderSideSchema>;23export const MarginModeSchema = z.enum(['cross', 'isolated']);24export type MarginMode = z.infer<typeof MarginModeSchema>;25export const SourceSchema = z.enum(['venue', 'paper']);2627export const InstrumentSchema = z.object({28  venue: VenueIdSchema,29  symbol: z.string().min(1),                        // venue's own symbol, e.g. 'BTC' on Hyperliquid, 'BTC-USD' on dYdX30  id: z.string().min(1),                            // `${venue}:${symbol}`31  base: z.string().min(1),32  quote: z.string().min(1),33  kind: z.literal('perpetual'),34  collateral: z.string().min(1),35  settlement: z.string(),                           // how PnL settles, free text from the venue's docs36  tickSize: DecStrSchema,37  lotSize: DecStrSchema,38  minOrderSize: DecStrSchema.nullable(),39  minNotional: DecStrSchema.nullable(),40  maxLeverage: DecStrSchema,41  marginModes: z.array(MarginModeSchema),42  fundingIntervalSec: z.number().int().positive(),43  markPriceMethod: z.string(),44  status: z.enum(['active', 'reduce_only', 'halted']),45}).refine((i) => i.id === `${i.venue}:${i.symbol}`, { message: 'id must be `${venue}:${symbol}`', path: ['id'] });46export type Instrument = z.infer<typeof InstrumentSchema>;4748export const TickerSchema = z.object({49  instrument: z.string().min(1),50  mark: DecStrSchema,51  index: DecStrSchema.nullable(),52  mid: DecStrSchema.nullable(),53  funding: DecStrSchema.nullable(),                 // per funding interval54  openInterest: DecStrSchema.nullable(),55  volume24h: DecStrSchema.nullable(),56  change24h: DecStrSchema.nullable(),57  ts: Ts,58});59export type Ticker = z.infer<typeof TickerSchema>;6061export const BookLevelSchema = z.tuple([DecStrSchema, DecStrSchema]);     // [price, size]62export const BookSchema = z.object({63  instrument: z.string().min(1),64  bids: z.array(BookLevelSchema),                   // best (highest) first65  asks: z.array(BookLevelSchema),                   // best (lowest) first66  ts: Ts,67});68export type Book = z.infer<typeof BookSchema>;6970export const PublicTradeSchema = z.object({71  instrument: z.string().min(1), price: DecStrSchema, size: DecStrSchema, side: OrderSideSchema, ts: Ts, id: z.string(),72});73export type PublicTrade = z.infer<typeof PublicTradeSchema>;7475export const CandleSchema = z.object({76  ts: Ts, open: DecStrSchema, high: DecStrSchema, low: DecStrSchema, close: DecStrSchema, volume: DecStrSchema,77});78export type Candle = z.infer<typeof CandleSchema>;7980export const AccountStateSchema = z.object({81  venue: VenueIdSchema,82  account: z.string().min(1),83  equity: DecStrSchema,84  collateral: DecStrSchema,85  freeCollateral: DecStrSchema,86  marginUsed: DecStrSchema,87  maintenanceMargin: DecStrSchema.nullable(),88  withdrawable: DecStrSchema.nullable(),89  ts: Ts,90  source: SourceSchema,91});92export type AccountState = z.infer<typeof AccountStateSchema>;9394export const PositionSchema = z.object({95  instrument: z.string().min(1),96  side: SideSchema,97  size: DecStrSchema,                               // positive; the direction is `side`98  entryPrice: DecStrSchema,99  markPrice: DecStrSchema,100  liquidationPrice: DecStrSchema.nullable(),101  leverage: DecStrSchema,102  marginMode: MarginModeSchema,103  margin: DecStrSchema,104  unrealizedPnl: DecStrSchema,105  realizedPnl: DecStrSchema.nullable(),106  fundingPaid: DecStrSchema.nullable(),107  source: SourceSchema,108});109export type Position = z.infer<typeof PositionSchema>;110111export const ORDER_TYPES = ['limit', 'market', 'stop_market', 'stop_limit', 'take_profit_market', 'take_profit_limit'] as const;112export const OrderTypeSchema = z.enum(ORDER_TYPES);113export type OrderType = z.infer<typeof OrderTypeSchema>;114export const TifSchema = z.enum(['gtc', 'ioc', 'alo']);        // alo = post only115export type Tif = z.infer<typeof TifSchema>;116117export const OrderRequestSchema = z.object({118  instrument: z.string().min(1),119  side: OrderSideSchema,120  type: OrderTypeSchema,121  size: DecStrSchema,122  price: DecStrSchema.nullable(),                   // limit price; for market orders the worst acceptable price (slippage bound), required123  triggerPrice: DecStrSchema.nullable(),124  reduceOnly: z.boolean(),125  tif: TifSchema,126  clientId: z.string().min(1),                      // idempotency key, unique per account127});128export type OrderRequest = z.infer<typeof OrderRequestSchema>;129130export const ORDER_STATUSES = ['pending', 'open', 'filled', 'partially_filled', 'cancelled', 'rejected', 'triggered'] as const;131export const OrderStatusSchema = z.enum(ORDER_STATUSES);132export type OrderStatus = z.infer<typeof OrderStatusSchema>;133134export const OrderSchema = OrderRequestSchema.extend({135  venueOrderId: z.string().nullable(),136  status: OrderStatusSchema,137  filled: DecStrSchema,138  avgFillPrice: DecStrSchema.nullable(),139  createdAt: Ts,140  updatedAt: Ts,141  rejectReason: z.string().nullable(),142});143export type Order = z.infer<typeof OrderSchema>;144145export const FillSchema = z.object({146  instrument: z.string().min(1),147  orderId: z.string().nullable(),148  clientId: z.string().nullable(),149  side: OrderSideSchema,150  price: DecStrSchema,151  size: DecStrSchema,152  fee: DecStrSchema,153  feeAsset: z.string(),154  closedPnl: DecStrSchema.nullable(),155  ts: Ts,156  id: z.string().min(1),157  liquidation: z.boolean(),158});159export type Fill = z.infer<typeof FillSchema>;160161export const FundingPaymentSchema = z.object({162  instrument: z.string().min(1), amount: DecStrSchema, rate: DecStrSchema, size: DecStrSchema, ts: Ts,163});164export type FundingPayment = z.infer<typeof FundingPaymentSchema>;165166export const VENUE_ERROR_CODES = ['rate_limited', 'unavailable', 'bad_answer', 'rejected', 'unsupported', 'stale'] as const;167export type VenueErrorCode = (typeof VENUE_ERROR_CODES)[number];168169/** thrown by venue adapters */170export class VenueError extends Error {171  readonly code: VenueErrorCode;172  readonly venue: VenueId | null;173  readonly detail: unknown;174  constructor(code: VenueErrorCode, message: string, o: { venue?: VenueId | null; detail?: unknown; cause?: unknown } = {}) {175    super(message, o.cause === undefined ? undefined : { cause: o.cause });176    this.name = 'VenueError';177    this.code = code;178    this.venue = o.venue ?? null;179    this.detail = o.detail;180  }181}182
Files main
Menu

Theme