src/numbers/margin.test.ts
v0.2.1 · 4.3 KB
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);
});
});