diff --git a/apps/web/package.json b/apps/web/package.json
index edab5bd..b61eb2e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -9,6 +9,7 @@
},
"dependencies": {
"@islandflow/types": "workspace:*",
+ "lightweight-charts": "^4.2.0",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
diff --git a/bun.lock b/bun.lock
index 557deeb..0408e06 100644
--- a/bun.lock
+++ b/bun.lock
@@ -9,6 +9,7 @@
"name": "@islandflow/web",
"dependencies": {
"@islandflow/types": "workspace:*",
+ "lightweight-charts": "^4.2.0",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -205,10 +206,14 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+ "fancy-canvas": ["fancy-canvas@2.1.0", "", {}, "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ=="],
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+ "lightweight-charts": ["lightweight-charts@4.2.3", "", { "dependencies": { "fancy-canvas": "2.1.0" } }, "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw=="],
+
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
From a95bb1375dfb3fd0ae970685b9ac98aad87aa0f8 Mon Sep 17 00:00:00 2001
From: dirtydishes <35477874+dirtydishes@users.noreply.github.com>
Date: Wed, 7 Jan 2026 16:57:54 -0500
Subject: [PATCH 005/237] Document environment configuration
---
README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 91 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 9860a69..d163311 100644
--- a/README.md
+++ b/README.md
@@ -110,13 +110,97 @@ Run just the web app (fixed to port 3000):
Run just the API:
- `bun --cwd services/api run dev`
-Adapter selection (env):
-- Options: `OPTIONS_INGEST_ADAPTER` (defaults to `synthetic`; supported: `synthetic`, `alpaca`, `ibkr`, `databento`)
-- Equities: `EQUITIES_INGEST_ADAPTER` (defaults to `synthetic`)
-- Compute: `COMPUTE_DELIVER_POLICY` (`new` default), `COMPUTE_CONSUMER_RESET` (force skip backlog)
-- Rolling stats: `REDIS_URL`, `ROLLING_WINDOW_SIZE`, `ROLLING_TTL_SEC`
-- Classifier tuning: `CLASSIFIER_SWEEP_MIN_PREMIUM_Z`, `CLASSIFIER_SPIKE_MIN_PREMIUM_Z`, `CLASSIFIER_SPIKE_MIN_SIZE_Z`, `CLASSIFIER_Z_MIN_SAMPLES`
-- Aggressor gating: `CLASSIFIER_MIN_NBBO_COVERAGE`, `CLASSIFIER_MIN_AGGRESSOR_RATIO`
+## Environment Configuration
+
+All runtime configuration is driven by `.env`. Start by copying `.env.example` and edit the values you need. Defaults below match `.env.example` unless otherwise noted.
+
+### Core infrastructure
+
+These define how services connect to the event bus and storage backends. Documentation links are provided for convenience.
+
+- `NATS_URL` (default `nats://localhost:4222`) — NATS JetStream endpoint. See [NATS](https://nats.io/) and [JetStream](https://docs.nats.io/nats-concepts/jetstream).
+- `CLICKHOUSE_URL` (default `http://localhost:8123`) — ClickHouse HTTP endpoint. See [ClickHouse](https://clickhouse.com/).
+- `CLICKHOUSE_DATABASE` (default `default`) — ClickHouse database name.
+- `REDIS_URL` (default `redis://localhost:6379`) — Redis endpoint for rolling stats. See [Redis](https://redis.io/).
+
+### Adapter selection
+
+- `OPTIONS_INGEST_ADAPTER` (default `synthetic`) — options ingest adapter: `synthetic`, `alpaca`, `ibkr`, `databento`.
+- `EQUITIES_INGEST_ADAPTER` (default `synthetic`) — equities ingest adapter.
+- `EMIT_INTERVAL_MS` (default `1000`) — synthetic equities emit cadence.
+
+### Alpaca options adapter (dev-only)
+
+Provider links: [Alpaca](https://alpaca.markets/), [Alpaca Market Data API](https://alpaca.markets/docs/api-references/market-data-api/).
+
+- `ALPACA_KEY_ID`, `ALPACA_SECRET_KEY` — credentials.
+- `ALPACA_REST_URL` (default `https://data.alpaca.markets`) — REST endpoint.
+- `ALPACA_WS_BASE_URL` (default `wss://stream.data.alpaca.markets/v1beta1`) — streaming endpoint.
+- `ALPACA_FEED` (default `indicative`) — use `opra` when you have a subscription.
+- `ALPACA_UNDERLYINGS` (default `SPY,NVDA,AAPL`) — comma-separated list of symbols.
+- `ALPACA_STRIKES_PER_SIDE` (default `8`) — strikes per side around ATM.
+- `ALPACA_MAX_DTE_DAYS` (default `30`) — expiry horizon.
+- `ALPACA_MONEYNESS_PCT` (default `0.06`) — ATM band for strike selection.
+- `ALPACA_MONEYNESS_FALLBACK_PCT` (default `0.1`) — fallback band if strikes are sparse.
+- `ALPACA_MAX_QUOTES` (default `200`) — subscription size guardrail.
+
+### Databento historical replay adapter
+
+Provider links: [Databento](https://databento.com/), [Databento API](https://databento.com/docs/api-reference).
+
+- `DATABENTO_API_KEY` — API key.
+- `DATABENTO_DATASET` (default `OPRA.PILLAR`) — dataset.
+- `DATABENTO_SCHEMA` (default `trades`) — schema.
+- `DATABENTO_START` — ISO date/time start for replay.
+- `DATABENTO_END` — ISO date/time end (optional).
+- `DATABENTO_SYMBOLS` (default `SPY.OPT`) — comma list or dataset symbols.
+- `DATABENTO_STYPE_IN` (default `parent`) — input symbology type.
+- `DATABENTO_STYPE_OUT` (default `instrument_id`) — output symbology type.
+- `DATABENTO_LIMIT` (default `0`) — record cap (0 means no cap).
+- `DATABENTO_PRICE_SCALE` (default `1`) — divide raw price by this value.
+- `DATABENTO_PYTHON_BIN` (default `py/.venv/bin/python`) — Python executable for replay sidecar.
+
+### IBKR options adapter (Python sidecar)
+
+Provider links: [Interactive Brokers](https://www.interactivebrokers.com/), [IBKR API docs](https://interactivebrokers.github.io/).
+
+- `IBKR_HOST` (default `127.0.0.1`) — TWS/Gateway host.
+- `IBKR_PORT` (default `7497`) — TWS/Gateway port.
+- `IBKR_CLIENT_ID` (default `0`) — API client ID.
+- `IBKR_SYMBOL` (default `SPY`) — underlying symbol.
+- `IBKR_EXPIRY` (default `20250117`) — expiry in `YYYYMMDD`.
+- `IBKR_STRIKE` (default `450`) — strike price.
+- `IBKR_RIGHT` (default `C`) — option right (`C` or `P`).
+- `IBKR_EXCHANGE` (default `SMART`) — exchange route.
+- `IBKR_CURRENCY` (default `USD`) — currency.
+- `IBKR_PYTHON_BIN` (default `python3`) — Python executable for sidecar.
+
+### Compute + market-structure tuning
+
+- `COMPUTE_DELIVER_POLICY` (default `new`) — consumer start behavior (`new` or `all`).
+- `COMPUTE_CONSUMER_RESET` (default `false`) — force consumer reset (skip backlog).
+- `NBBO_MAX_AGE_MS` (default `1000`) — max allowed NBBO age for joins.
+- `NEXT_PUBLIC_NBBO_MAX_AGE_MS` (default `1000`) — UI-visible NBBO age for display gating.
+- `ROLLING_WINDOW_SIZE` (default `50`) — rolling stats window length.
+- `ROLLING_TTL_SEC` (default `86400`) — rolling stats TTL in seconds.
+
+### Classifier thresholds
+
+- `CLASSIFIER_SWEEP_MIN_PREMIUM` (default `40000`) — absolute sweep premium floor.
+- `CLASSIFIER_SWEEP_MIN_COUNT` (default `3`) — minimum leg count for sweeps.
+- `CLASSIFIER_SWEEP_MIN_PREMIUM_Z` (default `2`) — sweep premium z-score threshold.
+- `CLASSIFIER_SPIKE_MIN_PREMIUM` (default `20000`) — absolute spike premium floor.
+- `CLASSIFIER_SPIKE_MIN_SIZE` (default `400`) — absolute spike size floor.
+- `CLASSIFIER_SPIKE_MIN_PREMIUM_Z` (default `2.5`) — spike premium z-score threshold.
+- `CLASSIFIER_SPIKE_MIN_SIZE_Z` (default `2`) — spike size z-score threshold.
+- `CLASSIFIER_Z_MIN_SAMPLES` (default `12`) — minimum samples before z-scores apply.
+- `CLASSIFIER_MIN_NBBO_COVERAGE` (default `0.5`) — NBBO coverage ratio gate.
+- `CLASSIFIER_MIN_AGGRESSOR_RATIO` (default `0.55`) — aggressor ratio gate.
+
+### Testing + throttling
+
+- `TESTING_MODE` (default `false`) — enable ingest throttling for local dev.
+- `TESTING_THROTTLE_MS` (default `200`) — minimum spacing between emitted prints.
Testing mode (throttles ingest to reduce CPU):
- `TESTING_MODE=true` enables throttling
From 1583a5041210970ac9c10ee9d269da1a4864fcc9 Mon Sep 17 00:00:00 2001
From: dirtydishes
Date: Fri, 9 Jan 2026 15:29:41 -0500
Subject: [PATCH 006/237] Improve local defaults and replay candle fetch
---
.env.example | 6 +-
apps/web/app/page.tsx | 77 ++++++++++++---
packages/storage/src/clickhouse.ts | 3 +-
scripts/dev.ts | 95 ++++++++++++++++++-
services/api/src/index.ts | 8 +-
services/candles/src/index.ts | 12 +--
services/compute/src/index.ts | 8 +-
.../ingest-equities/src/adapters/synthetic.ts | 8 +-
services/ingest-equities/src/index.ts | 6 +-
.../ingest-options/src/adapters/synthetic.ts | 6 +-
services/ingest-options/src/index.ts | 6 +-
11 files changed, 193 insertions(+), 42 deletions(-)
diff --git a/.env.example b/.env.example
index 508bd81..36bc452 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,7 @@
-NATS_URL=nats://localhost:4222
-CLICKHOUSE_URL=http://localhost:8123
+NATS_URL=nats://127.0.0.1:4222
+CLICKHOUSE_URL=http://127.0.0.1:8123
CLICKHOUSE_DATABASE=default
-REDIS_URL=redis://localhost:6379
+REDIS_URL=redis://127.0.0.1:6379
# Options ingest
OPTIONS_INGEST_ADAPTER=synthetic
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 3828c78..01dd36c 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -20,9 +20,8 @@ const NBBO_MAX_AGE_MS_SAFE =
Number.isFinite(NBBO_MAX_AGE_MS) && NBBO_MAX_AGE_MS > 0 ? NBBO_MAX_AGE_MS : 1000;
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
const CANDLE_INTERVALS = [
- { label: "1s", ms: 1000 },
- { label: "5s", ms: 5000 },
- { label: "1m", ms: 60000 }
+ { label: "1m", ms: 60000 },
+ { label: "5m", ms: 300000 }
];
type CandlestickSeries = ReturnType;
@@ -63,6 +62,23 @@ const toChartCandle = (candle: EquityCandle): ChartCandle => {
};
};
+const readErrorDetail = async (response: Response): Promise => {
+ const text = await response.text();
+ if (!text) {
+ return "";
+ }
+ try {
+ const payload = JSON.parse(text) as {
+ detail?: string;
+ error?: string;
+ message?: string;
+ };
+ return payload.detail ?? payload.error ?? payload.message ?? text;
+ } catch {
+ return text;
+ }
+};
+
type WsStatus = "connecting" | "connected" | "disconnected";
type TapeMode = "live" | "replay";
@@ -1218,15 +1234,28 @@ type CandleChartProps = {
ticker: string;
intervalMs: number;
mode: TapeMode;
+ replayTime?: number | null;
};
-const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
+const CandleChart = ({ ticker, intervalMs, mode, replayTime = null }: CandleChartProps) => {
const containerRef = useRef(null);
const chartRef = useRef(null);
const seriesRef = useRef(null);
const socketRef = useRef(null);
const reconnectRef = useRef(null);
const lastCandleRef = useRef<{ time: UTCTimestamp; seq: number } | null>(null);
+ const replayBucket = useMemo(() => {
+ if (mode !== "replay" || replayTime === null) {
+ return null;
+ }
+ return Math.floor(replayTime / intervalMs);
+ }, [mode, replayTime, intervalMs]);
+ const replayEndTs = useMemo(() => {
+ if (replayBucket === null) {
+ return null;
+ }
+ return (replayBucket + 1) * intervalMs - 1;
+ }, [replayBucket, intervalMs]);
const [ready, setReady] = useState(false);
const [status, setStatus] = useState(mode === "live" ? "connecting" : "connected");
const [lastUpdate, setLastUpdate] = useState(null);
@@ -1307,6 +1336,16 @@ const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
return;
}
+ if (mode === "replay" && replayBucket === null) {
+ setError(null);
+ setHasData(false);
+ setLastUpdate(null);
+ lastCandleRef.current = null;
+ seriesRef.current.setData([]);
+ setStatus("connected");
+ return;
+ }
+
let active = true;
setError(null);
setHasData(false);
@@ -1322,9 +1361,15 @@ const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
url.searchParams.set("interval_ms", intervalMs.toString());
url.searchParams.set("limit", "300");
url.searchParams.set("cache", "1");
+ if (mode === "replay" && replayEndTs !== null) {
+ url.searchParams.set("end_ts", replayEndTs.toString());
+ }
const response = await fetch(url.toString());
if (!response.ok) {
- throw new Error(`Candle fetch failed (${response.status})`);
+ const detail = await readErrorDetail(response);
+ throw new Error(
+ `Candle fetch failed (${response.status})${detail ? `: ${detail}` : ""}`
+ );
}
const payload = (await response.json()) as { data?: EquityCandle[] };
if (!active || !seriesRef.current) {
@@ -1361,7 +1406,7 @@ const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
return () => {
active = false;
};
- }, [ready, ticker, intervalMs, mode]);
+ }, [ready, ticker, intervalMs, mode, replayBucket, replayEndTs]);
useEffect(() => {
if (!ready || mode !== "live" || !seriesRef.current) {
@@ -1471,6 +1516,13 @@ const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
}, [intervalMs]);
const statusText = statusLabel(status, false, mode);
+ const intervalLabel = formatIntervalLabel(intervalMs);
+ const emptyLabel =
+ mode === "live"
+ ? status === "connected"
+ ? `No candles yet. First ${intervalLabel} candle appears after the window closes.`
+ : "Chart offline. Start candles service."
+ : "No candles for this replay window.";
return (
@@ -1487,11 +1539,7 @@ const CandleChart = ({ ticker, intervalMs, mode }: CandleChartProps) => {
{error ? (
Chart error: {error}
) : !hasData ? (
-
- {mode === "live"
- ? "No candles yet. Start candles service."
- : "No candles for this replay window."}
-
+
{emptyLabel}
) : null}
);
@@ -2280,7 +2328,12 @@ export default function HomePage() {
Charting {chartTicker}
)}