This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Master policy file. Subsystem-specific rules live in subdirectory CLAUDE.md files:
| Area | File |
|---|---|
| FastAPI server, routes, WS hub | src/api/CLAUDE.md |
| Domain layer: signals, regime, strategy, events | src/domain/CLAUDE.md |
| Adapters, persistence, stores | src/infrastructure/CLAUDE.md |
| Backtest engine, optimization — FROZEN | src/backtest/CLAUDE.md |
| Test suite layout and rules | tests/CLAUDE.md |
| Config YAML management | config/CLAUDE.md |
Apex — a streaming technical-analysis signal service and livewire data-lake API. It reads bars from the livewire Parquet lake and live ticks from xenon, computes TA-Lib indicators + rule-engine signals + market regime, and serves them over REST + WebSocket, plus a read-only lake MCP. Downstream readers are argon (daily bars for its own technicals), helium (returns and bars) and signal-lab (bars and indicators). Not a broker terminal — no order placement, and nothing live connects to IB (only the frozen backtest's IB historical feed remains).
Apex was pivoted out of a risk-monitoring + backtesting monolith. The streaming path and the chart/data-lake read surface are the live product. The legacy backtest, strategy-playbook and screener code is still on disk pending the Phase 6 strip-down — treat it as frozen (rule 10). The IB/Futu risk-monitor daemon was removed in v0.1.15.
# Install
uv pip install -e ".[dev,observability,api]"
# Run — scripts/serve.sh loads .env, runs preflight, then execs the server
scripts/serve.sh
uv run --env-file .env python -m src.api.server # equivalent, env loaded by you
make api-server # REST + WS API on :8322
make mcp-server # read-only lake MCP on :8333 (needs APEX_MCP_API_KEY)
scripts/mcp_tailnet.sh up IMAGE [PORT] # run the MCP on the docker host, tailnet-only (check/down too)
make dev # API server via main.py (the production image's entry point)
# Test
make test # pytest tests/unit/ -v
uv run pytest tests/unit/ --no-cov # faster, no coverage gate
uv run pytest -k "test_rule" # pattern match
make validate-fast # PR gate (validation_runner, 10 symbols)
make coverage # full suite + HTML coverage
# Code quality (run before every commit)
make format # black + isort
make lint # black + isort + flake8
make type-check # mypy src/ tests/
make quality # lint + type-check + dead-code + complexity
# Adjusted-mode preflight — read-only raw-vs-adjusted Livewire canary. No make target.
uv run python scripts/check_silver_canary.py
# Release — bumps VERSION + pyproject in lockstep; never edit either by hand
scripts/release/cut.sh
# Database
make db-init # Create PG schema
make db-reset # Drop + recreate PG schemaLegacy make targets (momentum, pead, strategy-compare, strategy-verify, r2-*, quantitative-moment*, jobs-*) belong to the frozen subsystems — see rule 10 before running or changing them.
src/api/ FastAPI app factory, routes, WS hub, JobManager, payload builders
src/mcp_server/ read-only lake MCP (20 tools over src/application/lake); separate process, no PG/streaming
src/application/ lake service layer, chart service, subscriptions, revision watcher, TASignalService
src/domain/ signals, indicators, regime, events, strategy, interfaces
src/infrastructure/ adapters (livewire, xenon, fmp, ib, yahoo, earnings), persistence, stores, observability
src/services/ bar loading for the verifiers and the frozen screeners (historical_data_manager, bar_loader, momentum/PEAD/earnings/market-cap services)
src/runners/ CLI runners (momentum, pead, strategy_compare, optimize, validation)
src/verification/ signal + regime verifiers; run as their own CI job
src/utils/ helpers
The repo-root services/ directory is not src/services/ — it is a separate tree (compute/ market_data/ shared/ web/); do not confuse them.
Signal pipeline: tick (xenon WS) → BarAggregator → IndicatorEngine → RuleEngine → PostgreSQL → REST + WS (/v1/equity/{symbol}/signals, /ws/signals).
Apex reads two lakes. Bronze (APEX_LIVEWIRE_ROOT) is livewire's raw per-ticker Hive tree, asset_class=<class>/symbol=<encode_symbol(SYM)>/<tf>.parquet, covering six asset classes with differing timeframe ladders. Silver (APEX_LIVEWIRE_SILVER_ROOT) is equity-only, split/dividend adjusted, and published atomically as numbered revisions — apex validates revisions/current.json when it pins a revision and SHA-256-verifies each artifact when that artifact is read (existence probes skip the hash; full verification is the explicit SilverRevision.verify_artifacts()). APEX_LIVEWIRE_PRICE_MODE selects raw (default) or adjusted; a long-running service polls for new revisions and reseeds only the affected subscriptions. Reads go through per-request in-memory DuckDB — apex only reads, livewire writes. Details: src/infrastructure/CLAUDE.md.
Every data source is env-gated: apex boots regardless, and each unset source makes its endpoints return 503. It degrades, never crashes.
| Variable | Default | Enables |
|---|---|---|
APEX_LIVEWIRE_ROOT |
unset | bars/indicators reads and the streaming warmup seed |
APEX_LIVEWIRE_SILVER_ROOT |
unset | Silver revision watcher, adjusted daily/factor artifacts |
APEX_LIVEWIRE_PRICE_MODE |
raw |
raw Bronze or adjusted Silver/factor-joined Bronze |
APEX_LIVEWIRE_REVISION_POLL_SECONDS |
30 |
Silver revision poll interval |
APEX_LIVEWIRE_COVERAGE_DB |
unset | the coverage catalog behind /v1/instruments, /v1/lake/coverage |
APEX_LIVEWIRE_LAKE_ROOT |
unset | index membership, security master — /v1/membership/*, /v1/equity/{symbol}/delisting, /v1/security/{symbol} |
APEX_LIVEWIRE_DELISTED_ROOT |
unset | listing=delisted/any bars from bronze-delisted (raw) |
APEX_LIVEWIRE_REPAIRS_ROOT |
unset | gap-engine repair-report evidence on /v1/{asset_class}/{symbol}/gaps; unset leaves repairs.state=not_configured and gaps still work |
APEX_LAKE_QUERY_TIMEOUT_SECONDS |
30 |
per-query deadline on every parquet read (LakeDb); expiry interrupts only that query and answers 504 query_timeout (bulk bars: that symbol goes to missing) |
APEX_PG_URL |
unset | signal backfill, confluence, signal persistence |
APEX_XENON_WS_URL |
ws://127.0.0.1:8765 |
live ticks → live WS signal frames |
APEX_XENON_API_KEY |
unset | sends X-API-Key (and no Origin) on the xenon WS upgrade; required once xenon's relay enforces upgrade auth. Secret: keep it in .env, never log it |
APEX_TIMEFRAMES |
1d |
timeframes the streaming pipeline subscribes/warms |
APEX_API_PORT |
8322 |
listen port |
APEX_MCP_API_KEY |
unset | required by the MCP server (Bearer; it refuses to boot without one) |
APEX_MCP_HOST / APEX_MCP_PORT |
127.0.0.1 / 8333 |
MCP listen address |
APEX_MCP_CALL_TIMEOUT_SECONDS |
60 |
whole-call deadline for one MCP tool call (query_timeout on expiry) |
APEX_MCP_ALLOWED_HOSTS |
loopback:port | Host header values MCP clients may send (DNS-rebinding guard) |
FMP (FMP_API_KEY or config/secrets.yaml) serves the frozen screener pipelines only, never the live read path.
- uv only —
uv run pytest, never barepython,pip, or activated venvs - Never Yahoo Finance as a live source — bulk backfill only
- No naked shorts — defined-risk only
- Never commit without explicit user request — draft first, wait
- Always open a PR before merging to master — never
git push origin masterdirectly /review-cyclebefore finalizing any substantial change — it runs/tribunal-reviewas its cross-model pass. (Supersedes the old/codex-review.)- Module size budget — target <500 lines; split on a responsibility seam, not a layer.
src/application/lake/guards.pyis the precedent: it carries "is this request coherent, and which artifact would it read?" out ofchart.py(originally split out assrc/api/routes/_chart_guards.py, then moved intosrc/application/lake/so REST and MCP share it), leaving routes and response assembly behind. - Fix every issue you spot — no "pre-existing" dismissals
- Wire all features — never leave code dead/unconnected; no accumulating dead code behind flags
- Backtest is frozen — do not modify
src/backtest/orsrc/domain/backtest/unless explicitly asked; they are frozen for the Phase 6 strip-down VERSION==pyproject.toml [project].version— enforced by the CI Version Sync job (scripts/release/version_sync_check.py). Bump only viascripts/release/cut.sh; never hand-edit either file.- Adjusted mode never silently falls back to raw — missing or incomplete Silver raises
AdjustedDataUnavailable. Do not add a raw fallback.
CI installs black/isort/flake8/bandit unpinned (.github/workflows/ci.yml), so a locally-clean make lint can still fail CI after an upstream release. Re-check against CI output, not local exit codes.
- Python 3.13, 4-space indent, 100-char line length
- Type hints required on all functions (mypy strict)
- Use
ib_async(notib_insync), TA-Lib for indicators try/exceptalways logs the error- Typed dataclasses for return values (not
Dict[str, Any]) - Naming:
get_*(from cache),fetch_*(network/disk),load_*(deserialize from file/DB)