Skip to content
markpaper

src/numbers/margin.test.ts

v0.2.1 · 4.3 KB

Download file
import { describe, expect, it } from 'vitest';
import { LighterNumbersError } from './errors.js';
import {
  clampLeverage,
  imfFromAccountFraction,
  imfFromMarketFraction,
  leverageFromAccountFraction,
  MARGIN_FRACTION_SCALES,
  marginIdentity,
  marketFractionForLeverage,
  maxLeverageFromImf,
} from './margin.js';

describe('two fraction scales', () => {
  it('documents both scales', () => {
    expect(MARGIN_FRACTION_SCALES.market.divisor).toBe(10_000);
    expect(MARGIN_FRACTION_SCALES.account.divisor).toBe(100);
  });

  it('market fraction is hundredths of a percent', () => {
    expect(imfFromMarketFraction('1000')).toBe(0.1);
    expect(imfFromMarketFraction(5000)).toBe(0.5);
  });

  it('account fraction is percent', () => {
    expect(imfFromAccountFraction('50.00')).toBe(0.5);
    expect(imfFromAccountFraction('20.00')).toBe(0.2);
  });

  it('the trap: dividing the account fraction by 10 000 gives 200x instead of 2x', () => {
    const wrong = Math.round(1 / (Number('50.00') / 10_000));
    expect(wrong).toBe(200);
    expect(leverageFromAccountFraction('50.00')).toBe(2);
  });

  it('rejects garbage in the converters', () => {
    expect(() => imfFromMarketFraction('abc')).toThrow(LighterNumbersError);
    expect(() => imfFromAccountFraction(Number.NaN)).toThrow(LighterNumbersError);
  });
});

describe('leverage helpers', () => {
  it('maxLeverageFromImf floors 10000 / raw and never returns Infinity', () => {
    expect(maxLeverageFromImf('1000')).toBe(10);
    expect(maxLeverageFromImf('200')).toBe(50);
    expect(maxLeverageFromImf('3333')).toBe(3);
    expect(maxLeverageFromImf('0')).toBe(1);
    expect(maxLeverageFromImf('')).toBe(1);
    expect(maxLeverageFromImf('nope')).toBe(1);
  });

  it('leverageFromAccountFraction rounds and clamps to 1x', () => {
    expect(leverageFromAccountFraction('50.00')).toBe(2);
    expect(leverageFromAccountFraction('20.00')).toBe(5);
    expect(leverageFromAccountFraction('10.00')).toBe(10);
    expect(leverageFromAccountFraction('100.00')).toBe(1);
    expect(leverageFromAccountFraction('0')).toBe(1);
    expect(leverageFromAccountFraction('x')).toBe(1);
  });

  it('marketFractionForLeverage inverts the cap formula', () => {
    expect(marketFractionForLeverage(10)).toBe(1000);
    expect(marketFractionForLeverage(2)).toBe(5000);
    expect(() => marketFractionForLeverage(0.5)).toThrow(LighterNumbersError);
  });

  it('clampLeverage keeps [1, cap] as an integer', () => {
    expect(clampLeverage(5, 10)).toBe(5);
    expect(clampLeverage(25, 10)).toBe(10);
    expect(clampLeverage(0.2, 10)).toBe(1);
    expect(clampLeverage(3.6, 10)).toBe(4);
    expect(clampLeverage(3, 0)).toBe(1);
  });
});

describe('marginIdentity', () => {
  it('holds when the account fraction is read in percent', () => {
    // Synthetic account in the documented shape: three cross positions at 50% / 20% / 10%.
    const positions = [
      { positionValue: '1000.00', initialMarginFractionPercent: '50.00' },
      { positionValue: '2500.00', initialMarginFractionPercent: '20.00' },
      { positionValue: '800.00', initialMarginFractionPercent: '10.00' },
    ];
    const expected = 500 + 500 + 80;
    const r = marginIdentity({
      positions,
      crossInitialMarginRequirement: String(expected),
      totalAssetValue: '3000.00',
      availableBalance: String(3000 - expected),
    });
    expect(r.expected).toBe(expected);
    expect(r.fromBalances).toBe(expected);
    expect(r.holds).toBe(true);
  });

  it('breaks by two orders of magnitude when the market scale is applied to account fractions', () => {
    const wrongScale = [{ positionValue: '1000.00', initialMarginFractionPercent: 50 / 100 }];
    const r = marginIdentity({ positions: wrongScale, crossInitialMarginRequirement: '500.00' });
    expect(r.holds).toBe(false);
    expect(r.expected).toBeCloseTo(5, 9);
  });

  it('flags a balance witness that disagrees', () => {
    const r = marginIdentity({
      positions: [{ positionValue: 1000, initialMarginFractionPercent: '50.00' }],
      crossInitialMarginRequirement: 500,
      totalAssetValue: 3000,
      availableBalance: 2000,
    });
    expect(r.fromBalances).toBe(1000);
    expect(r.holds).toBe(false);
  });

  it('holds trivially on a flat account', () => {
    expect(marginIdentity({ positions: [], crossInitialMarginRequirement: '0' }).holds).toBe(true);
  });
});
All files