Add equity candle aggregation pipeline

This commit is contained in:
dirtydishes 2026-01-07 09:51:54 -05:00
parent f889a2597b
commit a87df21baa
13 changed files with 1188 additions and 10 deletions

View file

@ -0,0 +1,81 @@
import { describe, expect, test } from "bun:test";
import type { EquityPrint } from "@islandflow/types";
import { CandleAggregator } from "../src/aggregator";
const buildPrint = (overrides: Partial<EquityPrint> = {}): EquityPrint => {
const ts = overrides.ts ?? 0;
return {
source_ts: overrides.source_ts ?? ts,
ingest_ts: overrides.ingest_ts ?? ts,
seq: overrides.seq ?? 0,
trace_id: overrides.trace_id ?? `print:${overrides.seq ?? 0}`,
ts,
underlying_id: overrides.underlying_id ?? "AAPL",
price: overrides.price ?? 0,
size: overrides.size ?? 1,
exchange: overrides.exchange ?? "TEST",
offExchangeFlag: overrides.offExchangeFlag ?? false
};
};
describe("CandleAggregator", () => {
test("emits candle with correct OHLC and volume", () => {
const aggregator = new CandleAggregator({ intervalsMs: [1000], maxLateMs: 0 });
const first = buildPrint({ ts: 1000, price: 10, size: 100, seq: 1 });
const second = buildPrint({ ts: 1500, price: 12, size: 50, seq: 2 });
const third = buildPrint({ ts: 2500, price: 11, size: 10, seq: 3 });
expect(aggregator.ingest(first).emitted).toHaveLength(0);
expect(aggregator.ingest(second).emitted).toHaveLength(0);
const result = aggregator.ingest(third);
expect(result.emitted).toHaveLength(1);
const candle = result.emitted[0];
expect(candle.ts).toBe(1000);
expect(candle.open).toBe(10);
expect(candle.high).toBe(12);
expect(candle.low).toBe(10);
expect(candle.close).toBe(12);
expect(candle.volume).toBe(150);
expect(candle.trade_count).toBe(2);
expect(candle.seq).toBe(2);
expect(candle.source_ts).toBe(1000);
expect(candle.ingest_ts).toBe(1500);
});
test("respects open and close order with out-of-order prints", () => {
const aggregator = new CandleAggregator({ intervalsMs: [1000], maxLateMs: 2000 });
const late = buildPrint({ ts: 1500, price: 15, size: 10, seq: 2 });
const early = buildPrint({ ts: 1200, price: 11, size: 20, seq: 1 });
aggregator.ingest(late);
aggregator.ingest(early);
const [candle] = aggregator.drain();
expect(candle.open).toBe(11);
expect(candle.close).toBe(15);
expect(candle.trade_count).toBe(2);
expect(candle.seq).toBe(2);
expect(candle.source_ts).toBe(1200);
expect(candle.ingest_ts).toBe(1500);
});
test("drops late prints once window is closed", () => {
const aggregator = new CandleAggregator({ intervalsMs: [1000], maxLateMs: 0 });
const first = buildPrint({ ts: 1000, price: 10, size: 100, seq: 1 });
const next = buildPrint({ ts: 3000, price: 14, size: 50, seq: 2 });
const late = buildPrint({ ts: 1500, price: 9, size: 25, seq: 3 });
aggregator.ingest(first);
const flush = aggregator.ingest(next);
expect(flush.emitted).toHaveLength(1);
const lateResult = aggregator.ingest(late);
expect(lateResult.emitted).toHaveLength(0);
expect(lateResult.droppedLate).toBe(1);
});
});