Skip to content

Commit 259180a

Browse files
committed
Add UI.
1 parent 3b289b3 commit 259180a

131 files changed

Lines changed: 16900 additions & 882 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ pytest-output.txt
3535
venv
3636
env
3737

38+
# --- Node deps (the ui-builder stage runs its own ``npm ci``) ---
39+
node_modules
40+
ui/node_modules
41+
ui/dist
42+
ui/.vite
43+
3844
# --- Local secrets and runtime state ---
3945
.env
4046
.env.local

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ apps/**/src-tauri/target/
3434
apps/**/src-tauri/gen/schemas/
3535
apps/operator-desktop/apps/
3636

37+
# Operator console SPA (ui/, ADR-013) — build artifacts, not source
38+
ui/node_modules/
39+
ui/dist/
40+
ui/.vite/
41+
3742
# Typecheck, lint, test, coverage
3843
.mypy_cache/
3944
.pytest_cache/

Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
# ── Stage 0: build the operator console SPA ──────────────────────────────────
2+
#
3+
# The browser console (ADR-013) is a static Vite/React bundle in ``ui/``. We
4+
# build it inside the image so a deploy needs no host Node toolchain. The built
5+
# ``/ui/dist`` is copied into the runtime stage and served by the operator API.
6+
FROM node:22-slim AS ui-builder
7+
8+
WORKDIR /ui
9+
10+
# Install against the lockfile first (cached unless deps change), then build.
11+
COPY ui/package.json ui/package-lock.json ./
12+
RUN npm ci --no-audit --no-fund
13+
14+
COPY ui/ ./
15+
RUN npm run build
16+
117
# ── Stage 1: build dependencies ──────────────────────────────────────────────
218
#
319
# Pin to a specific Python 3.11 patch version (matching the CI runner
@@ -49,6 +65,12 @@ COPY --chown=quant:quant alembic.ini ./alembic.ini
4965
COPY --chown=quant:quant scripts ./scripts
5066
COPY --chown=quant:quant infra ./infra
5167

68+
# Operator console (ADR-013): the prebuilt SPA from the ui-builder stage.
69+
# ``QP__API__CONSOLE_DIST_DIR`` makes the path explicit so the API serves it
70+
# regardless of where the package is imported from inside the image.
71+
COPY --from=ui-builder --chown=quant:quant /ui/dist ./ui/dist
72+
ENV QP__API__CONSOLE_DIST_DIR=/app/ui/dist
73+
5274
USER quant
5375

5476
# OCI labels — populated at build time via ``docker build --build-arg

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PYTHON ?= python3
22

3-
.PHONY: setup lint format-check typecheck test test-cov diff-check verify verify-online migrate-check docker-build-test backup-drill
3+
.PHONY: setup lint format-check typecheck test test-cov diff-check verify verify-online migrate-check docker-build-test backup-drill deploy
44

55
setup:
66
$(PYTHON) -m pip install --upgrade pip
@@ -47,5 +47,10 @@ migrate-check:
4747
docker-build-test:
4848
docker build --target runtime -t quant-platform:ci-test .
4949

50+
# One-command full-stack deploy (backend + console). See scripts/deploy.sh.
51+
# On Windows use: pwsh scripts/deploy.ps1
52+
deploy:
53+
bash scripts/deploy.sh
54+
5055
backup-drill:
5156
bash scripts/local_backup_drill.sh

constraints/py311.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ starlette==0.46.2
7575
structlog==25.5.0
7676
tenacity==9.1.4
7777
toolz==1.1.0
78+
torch==2.11.0
7879
typing-inspection==0.4.2
7980
typing_extensions==4.15.0
8081
tzdata==2026.2

docker-compose.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ x-quant-platform-env: &quant-platform-env
99
QP__STORAGE__REDIS_URL: redis://redis:6379/0
1010
QP__STORAGE__EVENT_BUS_BACKEND: redis_streams
1111
QP__STORAGE__OBJECT_STORE_ROOT: /app/data/parquet
12+
# Operator console SPA location inside the image (baked by the Dockerfile;
13+
# restated here so it is explicit at the compose layer). See ADR-013.
14+
QP__API__CONSOLE_DIST_DIR: /app/ui/dist
1215
QP__BROKER__HOST: ${QP__DOCKER_BROKER_HOST:-host.docker.internal}
1316
QP__BROKER__PORT: ${QP__DOCKER_BROKER_PORT:-7497}
1417
QP__BROKER__CLIENT_ID: ${QP__BROKER__CLIENT_ID:-985}
@@ -85,7 +88,11 @@ services:
8588
retries: 5
8689

8790
quant-platform-api:
88-
build: .
91+
build:
92+
context: .
93+
args:
94+
QP_GIT_COMMIT: ${QP_GIT_COMMIT:-unknown}
95+
QP_BUILD_DATE: ${QP_BUILD_DATE:-unknown}
8996
restart: unless-stopped
9097
ports:
9198
- "8000:8000"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ADR-012 — Arm D (learned-PCA) live production port
2+
3+
**Status:** Accepted (in progress) — 2026-05-29
4+
**Context:** [ADR-011](adr-011-live-pv-formulaic-feature-port.md) (pv+formulaic live port), [ADR-004](adr-004-per-category-eligibility-thresholds.md) (gate v3), `project_dollar_volume_scoring_defect`.
5+
6+
## Context
7+
8+
After the dollar-volume scoring fix (ADR-011) and the gate v3 redesign (ADR-004), the corrected walk-forward shows **no portable *linear* arm clears the Sharpe≥1.0 gate** — F/G sit at ~0.85, and adding orthogonal fundamentals (Arm P) lifts IC +20% but leaves Sharpe flat. The only arms that clear the gate are non-linear: **D (learned-PCA linear ranker, Sharpe 1.09)** and **N (GRU, 1.12)**. Operator decision (2026-05-29): build **D** as the first production non-linear port (N/GRU as a follow-up once D is stable in paper soak); parallelize IC→Sharpe construction research only after D's deployable path is unblocked.
9+
10+
D = `long_only_top30_pv_formulaic_learnedpca` (under v3: `learnedpca_streakdial` variants D/E). Construction: the pv+formulaic features (27 pv + 9 formulaic = 36) are projected through a **PCA artifact** (8 components + reconstruction error = 9 learned features) fit on the warmup window; the linear IC-weighted ranker (with the ADR-011 rank-normalized scoring) ranks on those 9 learned features; long-only top-30 + streak dial.
11+
12+
## Why this is tractable (de-risked 2026-05-29)
13+
14+
The PCA artifact is **pure data**: `PCAArtifact` is a frozen dataclass of `feature_names`, `mean`, `scale`, `components`, `explained_variance` (tuples of floats). Serialization exists (`loader.save_pca_artifact`/`load_pca_artifact`, JSON; schema `pca-artifact-v2`). The transform (`compute_learned_features`) is **deterministic matmul** (center → scale → project), no sklearn/pickle at inference. The fit is **static** (warmup-only, not per-fold rolling). Verified end-to-end: a persisted artifact round-trips exactly and reproduces the transform exactly (`np.allclose`). So "manifest/artifact loading" and "deterministic replay" are sound by construction — the inference path has no stochastic or unportable component.
15+
16+
## Requirements (operator-specified deliverables)
17+
18+
1. **Live-inference path** — live bars → pv+formulaic (ADR-011, done) → PCA transform → 9 learned features → score with D's frozen weights → top-30.
19+
2. **Manifest/artifact loading** — a versioned, persisted production PCA artifact + manifest the live engine loads (not re-fit live).
20+
3. **Deterministic replay** — same bars + same artifact + same weights ⇒ identical holdings.
21+
4. **Paper-trading adapter** — D routes through the existing IB-paper execution path (ADR-011 increment-3 plugin framework + risk limits).
22+
5. **Gate monitoring** — live IC / drawdown / streak tracked against the v3 gate so degradation surfaces.
23+
24+
## Architecture constraint (the layering crux, same as ADR-011)
25+
26+
The learned-PCA code lives in `quant_platform.research.features.learned` (composition layer). The live engine is in the inner layers (`services`/`engines`), which **must not import `quant_platform.research`** (`check_import_boundaries.py`). So the **inference** modules must be kernel-extracted to the inner layer; the **sklearn trainer stays in research** (offline fitting is a research concern). This mirrors ADR-011's "compute moves to the kernel, fit/register stays in research" split exactly.
27+
28+
- **Move to `services/research_service/features/kernel/learned/`:** `artifact.py` (PCAArtifact + schema), `loader.py` (JSON save/load), `features.py` (`compute_learned_features` — the transform), `config.py` (`LearnedConfig`). All depend only on kernel contracts/transforms + numpy/pandas/json — pure, extractable.
29+
- **Stays in `research.features.learned`:** `trainer.py` (`fit_pca_artifact`, sklearn). Re-export shims at the old `research.features.learned.*` paths preserve every importer (the backtest, tests).
30+
31+
## Incremental sequence (each its own verified-green commit)
32+
33+
- **Increment 1 — artifact + manifest foundation (DONE 2026-05-29).** `scripts/build_live_pca_artifact.py` fits the production PCA artifact on all available history (the live warmup) over universe-300 pv+formulaic, and persists it + a manifest (artifact schema, source feature names + version, the 9 learned output names, n_components, fit window, bars fingerprint, git commit, timestamp) under **`infra/artifacts/learned_pca/`** (a *tracked* location — `data/` is gitignored, but the live engine must load a versioned artifact that travels with the repo for deterministic deploy). The builder reuses the backtest's exact pv+formulaic compute (no divergent copy) and self-checks the round-trip before writing. Re-run quarterly to mint a new version. Produced: 8 components, 36 source features → 9 learned features, fit window 2021-12-27 → 2026-05-22, 330 instruments. Proven: persisted artifact round-trips + reproduces the transform exactly.
34+
- **Increment 2 — kernel-extract the learned inference modules (DONE 2026-05-29).** `git mv` of `artifact.py` (pure, verbatim), `config.py`, `loader.py`, `features.py``services/research_service/features/kernel/learned/`, with their `research.features.{contracts,transforms,learned.*}` imports rewritten to `kernel.*`. Re-export shims left at the four `research.features.learned.*` paths (object-identical: `shim.compute_learned_features is kernel.compute_learned_features`). The `__init__` (`register_family(MANIFEST)`) and `trainer.py` (sklearn `fit_pca_artifact`) stay in research and import through the shims. Verified: `mypy src` clean (978), import-boundaries clean, ruff clean; family still registers (`learned/learned-representations-v1`), trainer imports, both scripts import, 47 learned + 733 research-feature tests green. The live learned family (increment 3) can now import the transform from the inner kernel without crossing `services → research`.
35+
- **Increment 3 — live D feature family + bundle.** A `learned_pv_formulaic` family: live bars → pv+formulaic (ADR-011 kernel) → load the manifest artifact → `compute_learned_features``FeatureBundle` of the 9 learned features (latest row per instrument). Golden-master parity vs the backtest's D features on identical bars.
36+
- **Increment 4 — D strategy plugin + wiring.** `arm_d` `BuiltInStrategyPlugin` (`feature_set_version` = the learned family version, `required_features` = the 9 learned names, `default_factor_weights` = D's frozen promoted weights, top_n=30); `--engine` choice; session risk limits (reuse ADR-011's `QP__RISK__MAX_GROSS_EXPOSURE=0.22`).
37+
- **Increment 5 — simulated-backend validation + gate monitoring.** Reconcile live D top-30 + weights vs the D backtest (the increment-4-style parity check that caught the dollar-volume defect); wire live gate monitoring. On a clean reconcile → ib-paper soak.
38+
- **Promotion.** Re-run the gate on D's corrected evidence, promote D to the registry (supersede — the registry currently has no active G after the 2026-05-29 demotion), with the artifact manifest pinned in the model metadata.
39+
40+
## Trade-offs / revisit
41+
42+
- The artifact is fit on a fixed warmup. Live, it should be **periodically re-fit** (e.g. quarterly) as new history accrues — a scheduled offline job that produces a new manifest version; the live engine pins a version for deterministic replay. Increment 1's builder is that job's core.
43+
- D carries a reconstruction-error feature; confirm it is in D's promoted weight vector (it is a learned feature, weight ≥ 0 under non-negative IC).
44+
- If the kernel extraction surfaces a research-only entanglement in `features.py` (e.g. a registry side-effect like the pv/formulaic `__init__`), apply the same registration-split as ADR-011 (compute moves, `register_family` stays).
45+
46+
## Related
47+
- [ADR-011](adr-011-live-pv-formulaic-feature-port.md) — the pv+formulaic port this extends (its kernel + plugin framework are reused).
48+
- [ADR-004](adr-004-per-category-eligibility-thresholds.md) — the v3 gate D is evaluated against.
49+
- `scripts/build_live_pca_artifact.py` — the production-artifact builder (increment 1).

0 commit comments

Comments
 (0)