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