src/account/parse.test.ts
v0.2.0 · 7.7 KB
import { describe, expect, it } from 'vitest';
import { decimalToX18, X18 } from '../numbers/x18.js';
import { subaccountInfoPayload, UNIVERSE } from './fixtures.js';
import {
amountsUnchanged,
nonZeroAmounts,
parseSubaccountInfo,
positionAmountX18,
SubaccountInfoParseError,
} from './parse.js';
import { equityFromInfo, positionsFromInfo } from './positions.js';
describe('parseSubaccountInfo', () => {
it('decodes exists, unweighted health, balances and products', () => {
const info = parseSubaccountInfo(subaccountInfoPayload());
expect(info.exists).toBe(true);
expect(info.accountValueX18).toBe(1234n * X18 + X18 / 2n);
expect(info.accountValue).toBe(1234.5);
expect(info.initialHealthX18).toBe(1000n * X18);
expect(info.maintenanceHealthX18).toBe(1100n * X18);
expect(info.balances.get(2)).toEqual({ productId: 2, amountX18: decimalToX18('0.5'), vQuoteX18: -33000n * X18 });
expect(info.balances.get(8)?.amountX18).toBe(0n);
expect(info.products.get(2)).toEqual({
productId: 2,
oraclePriceX18: 66120n * X18,
longWeightInitialX18: 950000000000000000n,
});
expect(positionAmountX18(info, 2)).toBe(decimalToX18('0.5'));
expect(positionAmountX18(info, 999)).toBe(0n);
expect([...nonZeroAmounts(info).keys()]).toEqual([2]);
});
it('a non-existent subaccount has account value 0 even if healths carry numbers', () => {
const info = parseSubaccountInfo(subaccountInfoPayload({ exists: false }));
expect(info.accountValueX18).toBe(0n);
expect(info.accountValue).toBe(0);
});
it('tolerates missing risk / balances / products arrays', () => {
const info = parseSubaccountInfo({
exists: true,
healths: [{ health: '0' }, { health: '0' }, { health: '5000000000000000000' }],
});
expect(info.accountValue).toBe(5);
expect(info.balances.size).toBe(0);
const noRisk = parseSubaccountInfo(
subaccountInfoPayload({ perp_products: [{ product_id: 2, oracle_price_x18: '1000000000000000000' }] }),
);
expect(noRisk.products.get(2)?.longWeightInitialX18).toBeNull();
});
it('fails closed on every malformed field', () => {
const bad = (o: Record<string, unknown>) => () => parseSubaccountInfo(subaccountInfoPayload(o));
expect(bad({ exists: 'yes' })).toThrow(SubaccountInfoParseError);
expect(bad({ healths: [{ health: '1' }, { health: '1' }] })).toThrow(/healths/);
expect(bad({ healths: [{ health: '1' }, { health: '1' }, { health: '1e18' }] })).toThrow(/health/);
expect(bad({ healths: [{ health: '1' }, { health: '1' }, null] })).toThrow(/healths\[2\]/);
expect(bad({ perp_balances: 'x' })).toThrow(/perp_balances/);
expect(bad({ perp_balances: [{ product_id: '2', balance: { amount: '1', v_quote_balance: '1' } }] })).toThrow(
/product_id/,
);
expect(bad({ perp_balances: [{ product_id: 2 }] })).toThrow(/no balance/);
expect(bad({ perp_balances: [{ product_id: 2, balance: { amount: '1.5', v_quote_balance: '1' } }] })).toThrow(
/amount/,
);
expect(
bad({
perp_balances: [
{ product_id: 2, balance: { amount: '1', v_quote_balance: '1' } },
{ product_id: 2, balance: { amount: '2', v_quote_balance: '1' } },
],
}),
).toThrow(/duplicate perp balance/);
expect(bad({ perp_products: [{ product_id: 2, oracle_price_x18: 'x' }] })).toThrow(/oracle_price_x18/);
expect(bad({ perp_products: [{ product_id: -1, oracle_price_x18: '1' }] })).toThrow(/product_id/);
expect(
bad({
perp_products: [
{ product_id: 2, oracle_price_x18: '1' },
{ product_id: 2, oracle_price_x18: '1' },
],
}),
).toThrow(/duplicate/);
expect(() => parseSubaccountInfo(null)).toThrow(SubaccountInfoParseError);
expect(() => parseSubaccountInfo([])).toThrow(SubaccountInfoParseError);
});
});
describe('amountsUnchanged (stability fence)', () => {
it('compares non-zero amounts only', () => {
const a = parseSubaccountInfo(subaccountInfoPayload());
const same = parseSubaccountInfo(subaccountInfoPayload());
expect(amountsUnchanged(a, same)).toBe(true);
const filled = parseSubaccountInfo(
subaccountInfoPayload({
perp_balances: [{ product_id: 2, balance: { amount: decimalToX18('0.6').toString(), v_quote_balance: '0' } }],
}),
);
expect(amountsUnchanged(a, filled)).toBe(false);
const flat = parseSubaccountInfo(subaccountInfoPayload({ perp_balances: [] }));
expect(amountsUnchanged(a, flat)).toBe(false);
const zeroEntryAdded = parseSubaccountInfo(
subaccountInfoPayload({
perp_balances: [
{ product_id: 2, balance: { amount: decimalToX18('0.5').toString(), v_quote_balance: '1' } },
{ product_id: 188, balance: { amount: '0', v_quote_balance: '0' } },
],
}),
);
expect(amountsUnchanged(a, zeroEntryAdded)).toBe(true);
});
});
describe('positionsFromInfo / equityFromInfo', () => {
it('computes side, entry, value and uPnL with the knowledge-base formulas', () => {
const info = parseSubaccountInfo(subaccountInfoPayload());
const { positions, ok, degraded } = positionsFromInfo(info, UNIVERSE.byProductId);
expect(ok).toBe(true);
expect(degraded).toEqual([]);
expect(positions).toHaveLength(1);
const p = positions[0];
expect(p).toMatchObject({
coin: 'BTC',
productId: 2,
side: 'long',
szi: 0.5,
size: 0.5,
vQuote: -33000,
oraclePx: 66120,
});
expect(p?.entryPx).toBe(66000);
expect(p?.positionValue).toBe(33060);
expect(p?.unrealizedPnl).toBe(60); // 0.5 * 66120 - 33000
expect(p?.longWeightInitial).toBe(0.95); // from perp_products.risk, not the 0.98 in symbols
expect(p?.leverage).toBe(20);
expect(p?.sizeX18).toBe(decimalToX18('0.5'));
});
it('falls back to the symbols weight and handles shorts', () => {
const info = parseSubaccountInfo(
subaccountInfoPayload({
perp_balances: [
{
product_id: 2,
balance: { amount: (-decimalToX18('0.25')).toString(), v_quote_balance: (16600n * X18).toString() },
},
],
perp_products: [{ product_id: 2, oracle_price_x18: (66120n * X18).toString() }],
}),
);
const p = positionsFromInfo(info, UNIVERSE.byProductId).positions[0];
expect(p).toMatchObject({
side: 'short',
szi: -0.25,
size: 0.25,
entryPx: 66400,
longWeightInitial: 0.98,
leverage: 50,
});
expect(p?.unrealizedPnl).toBeCloseTo(-0.25 * 66120 + 16600, 6);
});
it('degrades on an unknown product or a missing oracle instead of treating the coin as flat', () => {
const unknown = parseSubaccountInfo(
subaccountInfoPayload({
perp_balances: [
{ product_id: 2, balance: { amount: decimalToX18('0.5').toString(), v_quote_balance: '0' } },
{ product_id: 777, balance: { amount: '1000000000000000000', v_quote_balance: '0' } },
],
}),
);
const r1 = positionsFromInfo(unknown, UNIVERSE.byProductId);
expect(r1.ok).toBe(false);
expect(r1.degraded).toEqual([{ productId: 777, reason: 'unknown-product' }]);
expect(r1.positions.map((p) => p.coin)).toEqual(['BTC']);
const noOracle = parseSubaccountInfo(subaccountInfoPayload({ perp_products: [] }));
const r2 = positionsFromInfo(noOracle, UNIVERSE.byProductId);
expect(r2.ok).toBe(false);
expect(r2.degraded).toEqual([{ productId: 2, reason: 'no-oracle' }]);
});
it('equity: account value and position totals', () => {
const info = parseSubaccountInfo(subaccountInfoPayload());
const eq = equityFromInfo(info, UNIVERSE.byProductId);
expect(eq).toEqual({
accountValue: 1234.5,
exists: true,
unrealizedPnl: 60,
grossPositionValue: 33060,
});
});
});