Add Redis rolling stats for flow packets and z-score driven classifiers

This commit is contained in:
dirtydishes 2025-12-30 13:24:48 -05:00
parent fc7065792f
commit 163ab1039e
10 changed files with 389 additions and 32 deletions

View file

@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import type { FlowPacket } from "@islandflow/types";
import { evaluateClassifiers, type ClassifierConfig } from "../src/classifiers";
const baseConfig: ClassifierConfig = {
sweepMinPremium: 40_000,
sweepMinCount: 3,
sweepMinPremiumZ: 2,
spikeMinPremium: 20_000,
spikeMinSize: 400,
spikeMinPremiumZ: 2.5,
spikeMinSizeZ: 2,
zMinSamples: 12
};
const buildPacket = (
overrides: Record<string, string | number | boolean>
): FlowPacket => {
return {
source_ts: 1,
ingest_ts: 1,
seq: 1,
trace_id: "trace",
id: "packet",
members: ["m1"],
features: {
option_contract_id: "SPY-2025-01-17-450-C",
count: 3,
total_premium: 1000,
total_size: 20,
first_price: 1,
last_price: 1.01,
window_ms: 500,
...overrides
},
join_quality: {}
};
};
describe("classifier z-score behavior", () => {
test("spike hit triggers on z-score even when absolute thresholds fail", () => {
const packet = buildPacket({
total_premium_z: 3.2,
total_premium_baseline_n: 20,
total_size_z: 0.4,
total_size_baseline_n: 20
});
const hits = evaluateClassifiers(packet, baseConfig);
expect(hits.some((hit) => hit.classifier_id === "unusual_contract_spike")).toBe(true);
});
test("sweep hit triggers on premium z-score when baseline is ready", () => {
const packet = buildPacket({
total_premium_z: 2.4,
total_premium_baseline_n: 20
});
const hits = evaluateClassifiers(packet, baseConfig);
expect(hits.some((hit) => hit.classifier_id === "large_bullish_call_sweep")).toBe(true);
});
test("sweep hit does not trigger when baseline is insufficient", () => {
const packet = buildPacket({
total_premium_z: 3,
total_premium_baseline_n: 4
});
const hits = evaluateClassifiers(packet, baseConfig);
expect(hits.some((hit) => hit.classifier_id === "large_bullish_call_sweep")).toBe(false);
});
});

View file

@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test";
import { computeSnapshot, computeStats } from "../src/rolling-stats";
describe("rolling stats helpers", () => {
test("computeStats handles empty baseline", () => {
const stats = computeStats([]);
expect(stats.count).toBe(0);
expect(stats.mean).toBe(0);
expect(stats.stddev).toBe(0);
});
test("computeStats calculates mean and stddev", () => {
const stats = computeStats([10, 12, 14]);
expect(stats.count).toBe(3);
expect(stats.mean).toBe(12);
expect(stats.stddev).toBeCloseTo(1.633, 3);
});
test("computeSnapshot calculates z-score against baseline", () => {
const snapshot = computeSnapshot([10, 12, 14], 15);
expect(snapshot.baselineCount).toBe(3);
expect(snapshot.zscore).toBeCloseTo(1.84, 2);
});
});