Skip to content
markpaper

src/account/reliability.test.ts

v0.3.0 · 6.1 KB

Download file
import { describe, expect, it } from 'vitest';
import {
  createEquitySmoother,
  createLastGoodCache,
  isEquityImplausible,
  ledgerExplainsDrop,
  median,
} from './reliability.js';

describe('isEquityImplausible — spec table from the knowledge base', () => {
  const prev = { accountValue: 1000, totalNtlPos: 5000 };
  const cases: Array<[string, { accountValue: number; totalNtlPos: number }, typeof prev | null, boolean]> = [
    ['av −20%, ntl same (price move)', { accountValue: 800, totalNtlPos: 5000 }, prev, false],
    ['av +19%, ntl same', { accountValue: 1190, totalNtlPos: 5000 }, prev, false],
    ['av ×48, ntl same', { accountValue: 48000, totalNtlPos: 5000 }, prev, true],
    ['av ÷9, ntl same', { accountValue: 111, totalNtlPos: 5002 }, prev, true],
    ['av and ntl fell together (real exit)', { accountValue: 100, totalNtlPos: 500 }, prev, false],
    ['ntl −50%, av −84% (partial exit)', { accountValue: 160, totalNtlPos: 2500 }, prev, false],
    ['av ×48 and ntl ×5', { accountValue: 48000, totalNtlPos: 25000 }, prev, false],
    ['prevAv = 0', { accountValue: 48000, totalNtlPos: 5000 }, { accountValue: 0, totalNtlPos: 5000 }, false],
    ['prevNtl = 0', { accountValue: 48000, totalNtlPos: 5000 }, { accountValue: 1000, totalNtlPos: 0 }, false],
    ['no previous reading', { accountValue: 1, totalNtlPos: 1 }, null, false],
    ['stable isolated-only account', { accountValue: 300, totalNtlPos: 900 }, { accountValue: 300, totalNtlPos: 900 }, false],
  ];
  for (const [name, cur, p, expected] of cases) {
    it(name, () => expect(isEquityImplausible(cur, p)).toBe(expected));
  }

  it('non-finite current readings are implausible', () => {
    expect(isEquityImplausible({ accountValue: Number.NaN, totalNtlPos: 1 }, prev)).toBe(true);
  });

  it('thresholds are configurable', () => {
    expect(isEquityImplausible({ accountValue: 850, totalNtlPos: 5000 }, prev, { lowerRatio: 0.9 })).toBe(true);
  });
});

describe('median', () => {
  it('odd, even (upper median) and empty', () => {
    expect(median([3, 1, 2])).toBe(2);
    expect(median([4, 1, 3, 2])).toBe(3);
    expect(() => median([])).toThrow(RangeError);
  });
});

describe('createEquitySmoother', () => {
  it('filters single and double phantoms: [100, 101, 4050, 100, 4850, 99, 100] → 100', () => {
    const s = createEquitySmoother();
    let out = 0;
    for (const v of [100, 101, 4050, 100, 4850, 99, 100]) out = s.push(v, true);
    expect(out).toBe(100);
  });

  it('untrusted readings do not enter the window; empty window → 0; first trusted returns itself', () => {
    const s = createEquitySmoother();
    expect(s.push(5000, false)).toBe(0);
    expect(s.push(100, true)).toBe(100);
    expect(s.push(9999, false)).toBe(100);
    expect(s.size()).toBe(1);
  });

  it('keeps a bounded window and can be reset', () => {
    const s = createEquitySmoother({ window: 3 });
    for (const v of [1, 2, 3, 100, 100]) s.push(v, true);
    expect(s.size()).toBe(3);
    expect(s.value()).toBe(100);
    s.reset();
    expect(s.value()).toBe(0);
    expect(() => createEquitySmoother({ window: 0 })).toThrow(RangeError);
  });
});

describe('ledgerExplainsDrop', () => {
  const since = 1_000;
  it('explains a drop when moved funds cover at least 50% of the shortfall', () => {
    expect(ledgerExplainsDrop([{ time: 2_000, delta: { usdc: '-500' } }], since, 1000)).toBe(true);
    expect(ledgerExplainsDrop([{ time: 2_000, delta: { usdc: '-499' } }], since, 1000)).toBe(false);
  });

  it('ignores entries before the episode start and falls back to amount', () => {
    expect(
      ledgerExplainsDrop(
        [
          { time: 500, delta: { usdc: '10000' } },
          { time: 1_500, delta: { amount: '300' } },
        ],
        since,
        1000,
      ),
    ).toBe(false);
    expect(ledgerExplainsDrop([{ time: 1_500, delta: { amount: '600' } }], since, 1000)).toBe(true);
  });

  it('empty ledger or non-positive shortfall explains nothing', () => {
    expect(ledgerExplainsDrop([], since, 100)).toBe(false);
    expect(ledgerExplainsDrop(null, since, 100)).toBe(false);
    expect(ledgerExplainsDrop([{ time: 2_000, delta: { usdc: '1000' } }], since, 0)).toBe(false);
  });
});

describe('createLastGoodCache', () => {
  function clock(start = 0) {
    let t = start;
    return { now: () => t, advance: (ms: number) => (t += ms) };
  }

  it('fresh trusted positive readings update the cache', () => {
    const c = clock();
    const cache = createLastGoodCache({ now: c.now });
    expect(cache.update({ trusted: true, value: 100 })).toEqual({ ok: true, value: 100, fresh: true, ageMs: 0 });
    expect(cache.peek()).toEqual({ value: 100, at: 0 });
  });

  it('degraded readings get the cache within TTL, marked stale', () => {
    const c = clock();
    const cache = createLastGoodCache({ now: c.now, ttlMs: 600_000 });
    cache.update({ trusted: true, value: 100 });
    c.advance(60_000);
    expect(cache.update({ trusted: false, value: 3 })).toEqual({ ok: true, value: 100, fresh: false, ageMs: 60_000 });
    c.advance(600_000);
    expect(cache.update({ trusted: false, value: null })).toMatchObject({ ok: false, value: 0 });
  });

  it('no cache → ok false', () => {
    expect(createLastGoodCache().update({ trusted: false, value: null })).toMatchObject({ ok: false });
  });

  it('an honest trusted 0 deletes the cache: 100 → 0 → error must not return 100', () => {
    const cache = createLastGoodCache();
    cache.update({ trusted: true, value: 100 });
    expect(cache.update({ trusted: true, value: 0 })).toMatchObject({ ok: true, value: 0, fresh: true });
    expect(cache.update({ trusted: false, value: null })).toMatchObject({ ok: false, value: 0 });
  });

  it('hard-invalid values are never replaced by the cache', () => {
    const cache = createLastGoodCache();
    cache.update({ trusted: true, value: 100 });
    expect(cache.update({ trusted: false, value: -5 })).toMatchObject({ ok: false, value: 0 });
    expect(cache.update({ trusted: true, value: Number.NaN })).toMatchObject({ ok: false, value: 0 });
    expect(cache.peek()?.value).toBe(100);
    cache.clear();
    expect(cache.peek()).toBeNull();
  });
});
All files