src/account/reliability.test.ts
v0.3.0 · 6.1 KB
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();
});
});