SPB Git

Repository Public

models.test.ts 6.3 KB 78 lines typescript

1import { describe, expect, it } from 'vitest';2import {3  AccountStateSchema, BookSchema, CandleSchema, DecStrSchema, FillSchema, FundingPaymentSchema, InstrumentSchema, OrderRequestSchema, OrderSchema,4  PositionSchema, PublicTradeSchema, TickerSchema, VenueError, VENUE_ERROR_CODES,5  type Capabilities, type Instrument, type Order, type OrderRequest, type VenueAdapter,6} from '../src/index.ts';78const instrument: Instrument = {9  venue: 'hyperliquid', symbol: 'BTC', id: 'hyperliquid:BTC', base: 'BTC', quote: 'USD', kind: 'perpetual', collateral: 'USDC',10  settlement: 'USDC, continuous', tickSize: '1', lotSize: '0.00001', minOrderSize: null, minNotional: '10', maxLeverage: '40',11  marginModes: ['cross', 'isolated'], fundingIntervalSec: 3600, markPriceMethod: 'median of oracle, book and CEX perps', status: 'active',12};13const request: OrderRequest = { instrument: 'hyperliquid:BTC', side: 'buy', type: 'limit', size: '0.01', price: '60000', triggerPrice: null, reduceOnly: false, tif: 'gtc', clientId: 'c-1' };1415describe('models', () => {16  it('DecStr accepts canonical decimals only', () => {17    for (const ok of ['0', '-0', '1', '-1.5', '0.001', '123456789.000000001', '9'.repeat(60)]) expect(DecStrSchema.safeParse(ok).success, ok).toBe(true);18    for (const bad of ['', '+1', '1e5', 'NaN', '.5', '1.', '01', ' 1', '1,5', '9'.repeat(61), 1, null, undefined, 1.5]) expect(DecStrSchema.safeParse(bad).success, String(bad)).toBe(false);19  });2021  it('parses every model and strips unknown fields', () => {22    expect(InstrumentSchema.parse({ ...instrument, extra: 1 })).toEqual(instrument);23    expect(TickerSchema.parse({ instrument: 'hyperliquid:BTC', mark: '60000', index: null, mid: '60000.5', funding: '0.0000125', openInterest: '1000', volume24h: null, change24h: '-0.01', ts: 1 }).mark).toBe('60000');24    expect(BookSchema.parse({ instrument: 'hyperliquid:BTC', bids: [['59999', '1.5']], asks: [['60001', '2']], ts: 1 }).bids[0]).toEqual(['59999', '1.5']);25    expect(PublicTradeSchema.parse({ instrument: 'x', price: '1', size: '2', side: 'sell', ts: 1, id: 't' }).side).toBe('sell');26    expect(CandleSchema.parse({ ts: 0, open: '1', high: '2', low: '0.5', close: '1.5', volume: '10' }).high).toBe('2');27    expect(AccountStateSchema.parse({ venue: 'dydx', account: 'dydx1abc/0', equity: '100', collateral: '100', freeCollateral: '90', marginUsed: '10', maintenanceMargin: null, withdrawable: null, ts: 5, source: 'venue' }).source).toBe('venue');28    expect(PositionSchema.parse({ instrument: 'x', side: 'short', size: '1', entryPrice: '2', markPrice: '2', liquidationPrice: null, leverage: '3', marginMode: 'isolated', margin: '1', unrealizedPnl: '-0.5', realizedPnl: null, fundingPaid: null, source: 'paper' }).side).toBe('short');29    expect(OrderRequestSchema.parse(request)).toEqual(request);30    const order: Order = { ...request, venueOrderId: null, status: 'pending', filled: '0', avgFillPrice: null, createdAt: 1, updatedAt: 1, rejectReason: null };31    expect(OrderSchema.parse(order)).toEqual(order);32    expect(FillSchema.parse({ instrument: 'x', orderId: null, clientId: null, side: 'buy', price: '1', size: '1', fee: '0.001', feeAsset: 'USDC', closedPnl: null, ts: 1, id: 'f', liquidation: false }).liquidation).toBe(false);33    expect(FundingPaymentSchema.parse({ instrument: 'x', amount: '-0.01', rate: '0.0001', size: '1', ts: 1 }).amount).toBe('-0.01');34  });3536  it('rejects missing or mistyped fields', () => {37    const { tickSize: _t, ...noTick } = instrument;38    expect(InstrumentSchema.safeParse(noTick).success).toBe(false);39    expect(InstrumentSchema.safeParse({ ...instrument, tickSize: 0.5 }).success).toBe(false);40    expect(InstrumentSchema.safeParse({ ...instrument, id: 'dydx:BTC' }).success).toBe(false);41    expect(InstrumentSchema.safeParse({ ...instrument, kind: 'future' }).success).toBe(false);42    expect(InstrumentSchema.safeParse({ ...instrument, venue: 'binance' }).success).toBe(false);43    expect(OrderRequestSchema.safeParse({ ...request, size: '1e-2' }).success).toBe(false);44    expect(OrderRequestSchema.safeParse({ ...request, side: 'long' }).success).toBe(false);45    expect(OrderRequestSchema.safeParse({ ...request, tif: 'fok' }).success).toBe(false);46    expect(OrderRequestSchema.safeParse({ ...request, clientId: '' }).success).toBe(false);47    expect(OrderRequestSchema.safeParse({ ...request, price: undefined }).success).toBe(false);48    expect(BookSchema.safeParse({ instrument: 'x', bids: [['1']], asks: [], ts: 1 }).success).toBe(false);49    expect(BookSchema.safeParse({ instrument: 'x', bids: [[1, 2]], asks: [], ts: 1 }).success).toBe(false);50    expect(TickerSchema.safeParse({ instrument: 'x', mark: '1', ts: 1 }).success).toBe(false);51    expect(TickerSchema.safeParse({ instrument: 'x', mark: '1', index: null, mid: null, funding: null, openInterest: null, volume24h: null, change24h: null, ts: 1.5 }).success).toBe(false);52  });5354  it('VenueError carries a code', () => {55    const e = new VenueError('bad_answer', 'missing field', { venue: 'hyperliquid', detail: { path: 'x' } });56    expect(e).toBeInstanceOf(Error);57    expect(e).toBeInstanceOf(VenueError);58    expect(e.name).toBe('VenueError');59    expect(e.code).toBe('bad_answer');60    expect(e.venue).toBe('hyperliquid');61    expect(e.message).toBe('missing field');62    expect(new VenueError('stale', 'old').venue).toBeNull();63    expect([...VENUE_ERROR_CODES].sort()).toEqual(['bad_answer', 'rate_limited', 'rejected', 'stale', 'unavailable', 'unsupported']);64  });6566  it('the venue contract is implementable (compile-time check)', () => {67    const capabilities: Capabilities = { marketData: true, account: false, orders: false, orderTypes: ['limit'], tif: ['gtc'], modify: false, leverage: false, marginModes: ['cross'], subaccounts: false, testnet: true, stream: false, notes: [] };68    const none = async (): Promise<never> => { throw new VenueError('unsupported', 'not implemented'); };69    const adapter: VenueAdapter = {70      id: 'hyperliquid', network: 'testnet', capabilities,71      instruments: async () => [instrument], tickers: none, book: none, trades: none, candles: none, fundingHistory: none,72      account: none, positions: none, openOrders: none, orderHistory: none, fills: none, funding: none,73    };74    expect(adapter.trading).toBeUndefined();75    return expect(adapter.tickers()).rejects.toMatchObject({ code: 'unsupported' });76  });77});78
Files main
Menu

Theme