True-streaming anomaly terminal: event-time MVs (RisingWave) + drift-adaptive detection, backtested - #13
Open
puneethkotha wants to merge 6 commits into
Open
True-streaming anomaly terminal: event-time MVs (RisingWave) + drift-adaptive detection, backtested#13puneethkotha wants to merge 6 commits into
puneethkotha wants to merge 6 commits into
Conversation
- bench/generate_corpus.py: seeded generator, 11,700 minute bars across 6 symbols x 5 sessions with per-symbol heteroskedastic volatility; injects 10 labeled events (earnings gaps, drawdowns, halts, shocks). - bench/replay_corpus.jsonl, bench/labels.csv: committed corpus and labels. - bench/baseline.py: runs the shipped AlertEngine over the corpus, deterministic across two passes. Static price rules fire 47 alert-bars (9.4/day); the volume rule fires 1641 (331.8/day). - .gitignore for OS files, pycache, engine state, logs.
- docker-compose.yaml: replace the nine-container stack with Redpanda (Kafka API, no Zookeeper), RisingWave single-node (stream processing + Postgres-wire serving), plus profiled producer/detector services. Topic names and the 4-partition layout are unchanged so the producer contract holds. - sql/00_sources.sql: append-only raw_bars table over the Kafka topic with event-time WATERMARK (date - 2 min); stock_info metadata table folds the old Postgres service into RisingWave. - bench/: reproducible image (Dockerfile + pinned requirements), feed_kafka.py bulk loader, producer_live.py paced replay source. - Delete consumer/consumer.py, consumer/InfluxDBWriter.py, grafana_dashboard.json, the Spark postgres jar, InfluxDB state, and stale __pycache__. - Drop findspark/influxdb-client from deps; producer bootstrap is env-driven. Verified: producer -> Redpanda -> RisingWave ingests all 11,700 corpus bars; SELECT count(*) FROM raw_bars = 11700 (exact).
- sql/10_features.sql: returns_1m (event-time log returns via lag), features (per-minute VWAP/SMA/rolling realized-vol/volume moments over bounded OVER windows), vwap_5m_hop (5-min hop, WATERMARK-driven EMIT ON WINDOW CLOSE). - bench/verify_mv.py: diffs every MV column against an independent pandas/numpy reference over the corpus (np.isclose rtol=1e-6, atol=1e-9). - bench/measure_latency.py: engine ingest->feature latency probe. Verified (real runs, bench/mv_correctness_report.json, latency_report.json): SMA/VWAP rel err ~1e-15, rvol rel err 1.1e-12 (abs 7.7e-11), volume stats exact; hop VWAP rel err 4.9e-16 over 11,741 windows; 42 windows correctly held back by the watermark. Engine latency produce->features p50 17.7ms / p95 27.1ms (raw_bars p50 8.1ms).
- detector/detector.py: per-symbol ensemble. Robust MAD z-score on log returns (primary), River HalfSpaceTrees on a symbol-relative feature vector (corroborating), River ADWIN + Page-Hinkley on a |return| volatility proxy for drift; on drift the EW band re-baselines fast for a few bars. Only knobs are K and the EW half-life (detector/config.py, config.yaml). - detector/service.py: live sidecar polling the features MV over pg-wire, writing one decision row per (symbol, window) to anomalies; composite (ts,stock) cursor gives exact continuation and restart-resume. - sql/20_anomalies.sql: decision + drawn-band sink, PK (stock, ts). - bench/features.py: pandas features matching the MVs; bench/detector_checks.py; bench/restart_test.sh. Verified (real runs): detector deterministic across two runs; band width vs rvol_5m Pearson r=0.68 (>0.6); all 4 regime-change events (all 10 events) trigger drift; 53 adaptive fires vs 47 static-price fires over the corpus. Crash-recovery: deleted 4,680 tail rows, resumed, final 11,700 rows / 11,700 distinct keys / 0 gap (no duplicate, no gap across the restart boundary).
- bench/backtest.py: runs both detectors on identical feature rows; range-based (existence) precision/recall with merged alert ranges; sweeps K (ensemble and robust-primary); reports static baseline, adaptive matched-recall + F1-optimal operating points, false-alert reduction, and AUC-PR over the primary sweep. - Finalize the corpus with realistic per-symbol volatility heterogeneity (a global 3% threshold floods on volatile names) and overnight gap_only + borderline subtle_shock events (structurally invisible to single-bar rules), giving a genuine precision/recall tradeoff. 17 labeled events. Refresh all corpus-derived reports (baseline, MV correctness, detector checks). Real backtest (bench/backtest_report.md): static (5%/3%): recall 0.53, precision 0.45, 6.0 false alerts/day, F1 0.49 adaptive : recall 0.76, precision 0.88, 0.6 false alerts/day, F1 0.82 => 10x fewer false alerts at higher recall and near-double precision; the adaptive detector dominates the static baseline on every axis. AUC-PR (primary robust-z sweep) 0.15, pulled down by the deliberately near-noise-floor subtle events.
- docs/demo/: client-side replay terminal (canvas). Animates the recorded corpus with the real detector output - price + breathing adaptive band vs the flat static reference, anomaly markers, event shading, rvol strip, live readout, anomaly log, and the real backtest scoreboard. No backend. Data precomputed by bench/build_demo_data.py. - api/: FastAPI read layer over the RisingWave pg-wire (features, anomalies, backtest, WebSocket). Smoke-tested against the running cluster. - deploy/: Compose + Caddy for a live VM (four processes behind TLS); config validated. - docs/index.html: correct the unsupported '10K+ events/sec' / '<100ms' claims to measured figures (p50 18ms / p95 27ms engine ingest->feature; 10x fewer false alerts) and reframe around the new architecture; link the live replay. - README + CHANGELOG rewritten; remove legacy Grafana/architecture images and the old pipeline SVG. Verified: replay demo renders and animates in-browser with no console errors; API /health, /symbols, /backtest, /anomalies return correct data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rearchitects StockStream from a Spark micro-batch ETL into a true-streaming,
backtested market anomaly terminal. The Spark
foreachBatch+ per-rowcollect()consumer and the InfluxDB aggregation store are gone; RisingWave nowcomputes VWAP, moving averages, and rolling realized volatility as event-time
incremental materialized views and serves them over the Postgres wire, and a
drift-adaptive online detector (River) replaces the four static single-bar
thresholds. Everything runs here — the streaming path, the MV-correctness check,
and the backtest — and the numbers below are from real runs, pasted verbatim.
The nine-container stack (Kafka+Zookeeper, Spark master/worker/submit, InfluxDB,
Postgres, Grafana) collapses to four processes: Redpanda, RisingWave, the
detector, the UI/API.
Phase-by-phase
(
bench/generate_corpus.py), 11,700 minute bars / 6 symbols / 5 sessions, 17labeled events (earnings gaps, drawdowns, halts, shocks). Static baseline via
the shipped
AlertEngine, deterministic across two passes.docker-compose.yaml;sql/00_sources.sql(watermarkedraw_bars). Producer→ Redpanda → RisingWave ingests all 11,700 bars;
count(*) = 11700exact.sql/10_features.sql:returns_1m,features(per-minute VWAP/SMA/rvol/volume moments),vwap_5m_hop(
EMIT ON WINDOW CLOSE). Verified equal to a numpy/pandas reference.detector/(robust z/MAD +Half-Space Trees + ADWIN/Page-Hinkley);
sql/20_anomalies.sql; live sidecarwith exactly-once coverage across a crash/restart.
bench/backtest.py, range-basedprecision/recall; adaptive vs static on identical inputs.
GitHub Pages (
docs/demo/), FastAPI read API (api/), Caddy/VM deploy(
deploy/), and corrected landing-page claims.Evidence / metrics (real output)
Materialized-view correctness (
bench/verify_mv.py, vs numpy/pandas,np.isclosertol 1e-6 / atol 1e-9):Backtest, adaptive vs static (
bench/backtest.py, range-based, 17 labeledevents / 6 symbols / 5 trading days):
At recall 0.76 (≥ the static baseline's 0.53) the adaptive detector fires 10x
fewer false alerts/day (0.6 vs 6.0) with near-double precision — it dominates
the static baseline on recall, precision, and false-alert rate. AUC-PR over the
primary robust-z sweep is 0.15 (pulled down by deliberately near-noise-floor
subtle events). The static rules also miss overnight gaps entirely (single-bar
open/close cannot see a cross-bar move).
Engine latency (
bench/measure_latency.py, ingest → feature, 120 bars):p50 17.7 ms, p95 27.1 ms (produce →
raw_barsp50 8.1 ms). Scoped to enginelatency; the live yfinance producer's 3s poll of 15-min bars is reported
separately.
Exactly-once coverage (
bench/restart_test.sh): deleted 4,680 tail rows tosimulate a crash; the detector resumed from its last
(ts, stock)and refilled —final 11,700 rows, 11,700 distinct keys (no duplicate), 0 gap.
Detector checks (
bench/detector_checks.py): deterministic across two runs;band-width vs
rvol_5mPearson r = 0.68 (> 0.6); all 4 regime-change events(all 10 non-subtle events) trigger drift.
Needs your environment
(
bench/producer_live.py). The real yfinance producer needs network access +open market hours; its bootstrap is now env-driven (
KAFKA_BOOTSTRAP).deploy/docker-compose.yml+deploy/Caddyfileneed a domain anda small always-on VM (e.g. Hetzner CX22, ~$5/mo) for TLS + the live URL. The
compose config is validated here; it is not stood up (no domain). The zero-infra
GitHub Pages replay needs nothing.
pages.ymlon merge tomain; the demo lives at/demo/.Run instructions
Notes
in
CHANGELOG.md.history for specific past events); it models realistic per-symbol volatility
heterogeneity and labeled events. Every metric above is a real measurement over
that corpus, reproducible with one command.