diff --git a/.context/PROJECT.md b/.context/PROJECT.md index 7779a58..c713104 100644 --- a/.context/PROJECT.md +++ b/.context/PROJECT.md @@ -1,13 +1,80 @@ -# hostexecutor +# jepa-fx-risk ## Identity -- **Name**: hostexecutor +- **Name**: jepa-fx-risk - **Owner**: Mathias -- **Client**: personal -- **Repo**: gitea.d-ma.be/mathias/hostexecutor +- **Client**: personal / research +- **Repo**: gitea.d-ma.be/mathias/jepa-fx-risk - **Status**: active +- **Brain wing**: `jepa-fx` (`wiki/jepa-fx/`) + +## Purpose + +Research project. Not a product. The "user" is future-self and research readers. Success is a reproducible, publishable result — not deployment. ## Stack -Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. +- **Primary language**: Go 1.24+ (pipeline, eval harness, CLI, experiment runner) +- **ML layer**: Python 3.12 + PyTorch (TS-JEPA training loop only — isolated in `model/`) +- **Build**: Task (Taskfile.yml) +- **Target infra**: koala (Arch + Blackwell GPU, training), iguana (Mac Studio M2, dev) +- **MCP**: brain (knowledge), gitea (version control) + +## Research context + +**Primary hypothesis:** +> JEPA embeddings trained on FX time-series will produce latent market-state representations structurally separable by regime without explicit labels, measurable by silhouette score > 0.35 on k-means clusters vs. realised-volatility regime labels, on held-out data including at least one structural break. + +**Current phase:** Phase 0 — SSL Feasibility Gate + +**Training cutoff:** 2023-01-01 (hard — never look at post-2023 data during development) + +**Out of scope:** options pricing, directional alpha, live/paper trading, exotic pairs (Phases 1–3) + +**Brain wing for prior decisions and failure modes:** `brain_query wing=jepa-fx` + +## Conventions + +### Scientific discipline +- Every experiment has a spec in `specs/` before any code runs +- Hypotheses are falsifiable and have quantitative acceptance criteria +- Null results are recorded and published — not discarded +- Training cutoff is sacred — post-2023 data never informs any design decision +- Results reported with baselines; no cherry-picking + +### Code style +- Go: `gofumpt`, `golangci-lint` with project config; table-driven tests; `testify` +- Errors: `fmt.Errorf("context: %w", err)` — no naked returns +- Python: `ruff` for lint; type hints throughout; `pytest` for tests +- No Jupyter notebooks for anything reproducible — notebooks are EDA scratch only + +### Git +- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `experiment:`, `result:` +- Branch: `feat/`, `experiment/phase-N-description`, `fix/` +- Every experiment run gets a git tag: `exp/YYYYMMDD-short-description` +- PRs: one concern per PR; description explains *why* not *what* + +### Experiment discipline +- One spec per phase in `specs/` — written before any implementation +- Each run recorded in `experiments/YYYYMMDD-HHMMSS-description/` +- Metric summaries committed to `results/summaries/` — large outputs gitignored +- `task check` must pass before any commit + +### Security / data +- No raw FX data committed (gitignored) — see `data/README.md` for reproducible download +- No API keys or tokens in code — env vars only +- Training data and results stay local — nothing to cloud unless explicitly decided + +## Agent instructions + +When acting as a coding agent on this project: + +1. Read this file and all `SKILL.md` files in `.skills/` before starting work +2. Run `brain_query wing=jepa-fx` to load current decisions and failure modes +3. Run `task check` before every commit (lint + vet + test) +4. Check `DECISIONS.md` before making any architecture or methodology choice +5. Every experiment needs a spec in `specs/` — no specless experiments +6. Never touch post-2023 data during development; it is sealed +7. Record null results honestly — do not iterate until metrics pass without noting it +8. When adding a Python dependency, justify it; prefer pure Go alternatives diff --git a/.context/mcp.json b/.context/mcp.json index cd0f1bc..c967ef1 100644 --- a/.context/mcp.json +++ b/.context/mcp.json @@ -1,8 +1,18 @@ { "mcpServers": { - "knowledge": { - "url": "http://localhost:3100/mcp", - "description": "Project knowledge base — vector + graph retrieval" + "brain": { + "type": "http", + "url": "https://brain-mcp.d-ma.be/mcp", + "headers": { + "Authorization": "Bearer ${BRAIN_MCP_TOKEN}" + } + }, + "gitea": { + "type": "http", + "url": "https://git-mcp.d-ma.be/mcp", + "headers": { + "Authorization": "Bearer ${GITEA_MCP_TOKEN}" + } } } } diff --git a/.gitignore b/.gitignore index 902b7e7..12e335d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,39 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` +# Go build artifacts +bin/ *.test - -# Output of the go coverage tool, specifically when used with LiteIDE *.out -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file +# Go workspace go.work go.work.sum -# env file +# Environment .env -# Project-specific -bin/ -*.templ.go +# Python +model/.venv/ +model/__pycache__/ +model/**/__pycache__/ +model/**/*.pyc +model/.pytest_cache/ +model/**/.pytest_cache/ +model/.ruff_cache/ + +# Data — never commit raw or processed FX data +data/raw/ +data/processed/ +data/cache/ + +# Experiment outputs — commit summaries only (results/summaries/) +experiments/*/embeddings/ +experiments/*/checkpoints/ +experiments/*/logs/ + +# Large results — commit metric tables and figures only +results/raw/ + +# Notebooks — never commit outputs +notebooks/**/.ipynb_checkpoints/ + +# OS +.DS_Store diff --git a/.skills/README.md b/.skills/README.md new file mode 100644 index 0000000..0f5e349 --- /dev/null +++ b/.skills/README.md @@ -0,0 +1,13 @@ +This directory contains skill symlinks for this project. + +Agents: load all SKILL.md files in subdirectories before starting work. + +Active skills: +- experiment-spec — write experiment specs before running any phase +- feature-spec — write component specs before implementing within a phase +- tdd — acceptance criteria map to tests; red-green-refactor +- grill-me — stress-test specs and hypotheses before committing +- session-retrospective — after each phase, surface learnings for the brain +- clean-code — Go and Python style conventions +- planning — break phases into trackable tasks +- debug — systematic debugging approach diff --git a/AGENTS.md b/AGENTS.md index 7779a58..29db286 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,5 @@ -# hostexecutor +# jepa-fx-risk — Agent context +# Auto-generated from .context/PROJECT.md by `task context:sync` +# Do not edit directly. -## Identity - -- **Name**: hostexecutor -- **Owner**: Mathias -- **Client**: personal -- **Repo**: gitea.d-ma.be/mathias/hostexecutor -- **Status**: active - -## Stack - -Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. +See .context/PROJECT.md for the canonical source. diff --git a/CLAUDE.md b/CLAUDE.md index 7779a58..29db286 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,5 @@ -# hostexecutor +# jepa-fx-risk — Agent context +# Auto-generated from .context/PROJECT.md by `task context:sync` +# Do not edit directly. -## Identity - -- **Name**: hostexecutor -- **Owner**: Mathias -- **Client**: personal -- **Repo**: gitea.d-ma.be/mathias/hostexecutor -- **Status**: active - -## Stack - -Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. +See .context/PROJECT.md for the canonical source. diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..8c391d0 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,120 @@ +# DECISIONS.md + +Architecture and methodology decisions for `jepa-fx-risk`. Every non-obvious choice lives here with its rationale. Agents: read this before making any design decision. + +Last updated: 2026-05-27 + +--- + +## Language split: Go-first, Python-minimal + +**Decision:** Data pipeline, evaluation harness, CLI, and experiment runner in Go 1.24+. Python 3.12 + PyTorch only for the TS-JEPA model training loop, isolated in `model/`. + +**Rationale:** Python's dependency ecosystem is a reliability risk for a multi-year project (conflicting CUDA versions, transitive breakage, environment drift). Go produces a single static binary, has excellent CSV/Parquet support, is fast enough for all non-training workloads, and keeps the reproducible parts of the project dependency-free. The training loop genuinely requires PyTorch — that is the one place Python is unavoidable. + +**Boundary:** `model/` is the Python perimeter. Nothing outside it imports Python. + +--- + +## Primary hypothesis + +**Decision:** Regime detection via embedding structural separability is the primary hypothesis. Distributional VaR forecasting is secondary and only pursued if Phase 1 and 2 succeed. + +**Rationale:** Regime detection is testable in Phase 1 without regulatory-quality outputs. If embeddings don't show regime structure, distributional VaR will also fail. Choosing a primary hypothesis prevents the project from retreating from one to the other on failure. + +**Success criterion (Phase 1):** Silhouette score > 0.35 on k-means clusters (k=3–5) vs. realised-volatility regime label (rolling 30-day HV percentile, high/low), computed on held-out test data including at least one structural break. + +--- + +## Training data: 2008–2022, all G10 pairs + +**Decision:** Train on 15 years of all G10 FX pairs from DUKASCopy, 2008–2022. Do not start with a single 5-year EUR/USD window. + +**Rationale:** A single 5-year window (e.g. 2019–2024) is dominated by one or two regimes and gives the encoder insufficient regime diversity to learn regime-sensitive representations. Training on 2008–2022 ensures the encoder sees: GFC (2008), EUR sovereign debt (2011–2012), SNB cap removal (2015), COVID (2020), USD rate cycle (2022). Multi-pair training also allows cross-currency transfer (Phase 4). + +**Alternative considered:** Start simple with EUR/USD only, expand later. Rejected because regime diversity in training is a structural requirement, not a nice-to-have. Retrofitting it in Phase 2 would require retraining from scratch. + +--- + +## Hard training cutoff: 2023-01-01 + +**Decision:** All data from 2023-01-01 onward is sealed. No architecture, hyperparameter, or methodology decision may be informed by post-2023 data. Post-2023 test set opened only once, for final evaluation. + +**Rationale:** Out-of-sample integrity is essential for publishability and honest self-assessment. The held-out window (2023–2026) includes: 2023 US regional bank stress, 2024 JPY intervention episodes. These are the test of genuine generalisation. + +**Enforcement:** `data/raw/` is gitignored. The download script hard-stops at 2022-12-31 for training splits. Any deviation requires a DECISIONS.md entry explaining why. + +--- + +## Phase 0: SSL feasibility gate before JEPA work + +**Decision:** Before any JEPA-specific implementation, run a Phase 0 experiment: masked autoencoder (MAE) baseline on EUR/USD hourly data. If MAE silhouette < 0.20, SSL-based regime detection is likely not feasible on this data — stop and investigate before proceeding to JEPA. + +**Rationale:** JEPA's complexity is only justified if the core SSL premise (that latent representations capture regime structure) holds for FX data. A failed MAE experiment tells us this in 2 weeks rather than 4 months. Added after Full Grill session (2026-05-27). + +**Go/no-go threshold:** MAE silhouette < 0.20 on held-out 2023 data → pause, investigate, do not proceed to Phase 1. + +--- + +## Architecture: TS-JEPA as starting implementation + +**Decision:** Use TS-JEPA (Ennadir et al., 2025) as the starting JEPA implementation. MTS-JEPA (He et al., 2026) is the upgrade path if multi-resolution proves necessary. + +**Rationale:** TS-JEPA is simpler. Validate the concept before adding multi-resolution complexity. If Phase 1 succeeds with TS-JEPA, MTS-JEPA is an ablation, not a prerequisite. + +**Risk:** Both are preprints. Code reproducibility is unconfirmed. First task of Phase 0 is reproducing TS-JEPA on the paper's own benchmark — if this takes > 2 weeks, contact authors or fall back to implementing JEPA masking from scratch using V-JEPA codebase as reference. + +--- + +## Input features: minimal for Phase 0/1 + +**Decision:** Phase 0 and Phase 1 use three features only: log-return (hourly), rolling 20-period realised volatility (hourly), VIX (daily, interpolated to hourly). + +**Rationale:** Too many input features in early phases makes it impossible to distinguish "JEPA learned regime structure" from "JEPA learned to encode a feature that correlates with regime." Minimal features reduce confounding. + +**Expansion path:** Add DXY, G10 vol surface, yield spreads in Phase 2 if Phase 1 succeeds. + +--- + +## Collapse diagnostic + +**Decision:** If PC1 of JEPA embeddings correlates > 0.85 with rolling 30-day HV, treat Phase 1 as a partial failure — the encoder learned volatility level, not regime structure. This is a useful finding but not the hypothesis. + +**Rationale:** EUR/USD hourly returns are strongly heteroskedastic. A JEPA encoder trained to predict future embeddings will strongly tend to encode current volatility as its primary latent dimension. This is predictable, not regime-sensitive. The linear probe and PC1 diagnostic together distinguish "learned volatility" from "learned regime." + +--- + +## Linear probe as mandatory Phase 1 exit gate + +**Decision:** Train a linear model on frozen JEPA embeddings to predict realised volatility decile. R² < 0.4 → embeddings are not encoding useful risk structure → do not proceed to Phase 2. + +**Rationale:** If a simple linear model cannot extract volatility regime from the embeddings, the representations are not useful for risk management purposes regardless of their silhouette score. Interpretability-by-linear-probe is the minimum bar for any downstream use. + +--- + +## Explicit out-of-scope (Phases 1–3) + +The following are explicitly out of scope for this research programme and go to a parking lot if they arise: + +- Options / derivatives pricing +- Directional alpha generation +- Live or paper trading +- Exotic pairs beyond G10 +- Institutional deployment +- Real-time inference systems + +--- + +## Experiment spec required before any experiment + +**Decision:** Every experiment phase must have a written spec in `specs/` before any code runs. No specless experiments. + +**Rationale:** Consistent with spec-driven-dev way of working. Specs force falsifiable hypothesis statement, quantitative acceptance criteria, and explicit null-result protocol before results are known — preventing post-hoc rationalisation. + +--- + +## Null results are results + +**Decision:** Null results (hypothesis rejected) are recorded in `results/summaries/` and treated as valid research outputs, not failures to be iterated away silently. + +**Rationale:** A confirmed null result (e.g. "SSL cannot find regime structure in FX hourly data") is publishable and scientifically valuable. Iterating hyperparameters until metrics pass without recording the failed attempts is p-hacking. Humble attitude; scientific approach. diff --git a/README.md b/README.md index 9ed663b..10903c3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,66 @@ -# hostexecutor +# jepa-fx-risk -> Generated from `mathias/template-go-web`. +Research project exploring JEPA (Joint Embedding Predictive Architecture) as a framework for latent representation learning applied to FX trading risk management. -## Bootstrap +**Primary hypothesis:** JEPA embeddings trained on FX time-series will produce latent market-state representations that are structurally separable by regime without explicit regime labels, measurable by silhouette score on k-means clusters validated against held-out realised-volatility regime labels. -After creating from template, run: +**Current phase:** Phase 0 — SSL Feasibility Gate (not yet started) + +**Brain wing:** `jepa-fx` — query via `brain_query wing=jepa-fx` + +--- + +## Architecture + +This is a **Go-first, Python-minimal** research project. + +| Layer | Language | Rationale | +|---|---|---| +| Data pipeline | Go | Type-safe, fast, no dependency hell | +| Experiment runner | Go | CLI tooling, reproducible invocations | +| Evaluation harness | Go | Silhouette, linear probe, collapse diagnostics | +| Model training | Python + PyTorch | TS-JEPA requires it; kept minimal and isolated | + +## Structure + +``` +specs/ experiment specs (one per phase) +src/ Go packages: pipeline, eval, cmd +model/ Python: TS-JEPA training loop only +experiments/ one directory per run (gitignored except summaries) +results/ tracked: metric tables, key figures +data/ gitignored; see data/README.md for download instructions +notebooks/ EDA only; outputs stripped before commit +docs/ ADRs and research notes +``` + +## Quick start ```bash -go mod tidy # regenerate go.sum with real module path -task generate # generate templ files -task build # build the binary +task data:fetch # download DUKASCopy G10 tick data +task pipeline:build # build Go data pipeline +task check # lint + vet + test +task experiment:run -- --spec specs/phase-0-ssl-feasibility.md ``` + +## Phases + +| Phase | Goal | Status | +|---|---|---| +| 0 | SSL feasibility gate — MAE baseline on FX data | 🔲 Not started | +| 1 | JEPA representation PoC — silhouette > 0.35 | 🔲 Not started | +| 2 | Regime detection validation — recall > 70%, lead > 5d | 🔲 Not started | +| 3 | Distributional forecasting — VaR passes Kupiec | 🔲 Not started | +| 4 | Multi-pair transfer — exotic pair improvement | 🔲 Not started | + +## Key constraints + +- **Training cutoff:** 2023-01-01 — all post-2023 data is sealed for final evaluation +- **Training data:** 2008–2022, all G10 pairs, DUKASCopy +- **Out of scope:** options pricing, directional alpha, live trading, exotic pairs (Phases 1–3) +- **End-state:** publishable research result — not institutional deployment + +## Related + +- Brain wing: `wiki/jepa-fx/` — decisions, hypotheses, failure modes +- Synthesis document: `JEPA_FX_Risk_Synthesis.docx` (session 2026-05-27) diff --git a/Taskfile.yml b/Taskfile.yml index fbca41b..fe5c7f5 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,37 +1,79 @@ version: '3' tasks: - generate: - desc: Run templ generate - cmds: [templ generate] - build: - desc: Build the binary - deps: [generate] - cmds: [go build -o bin/hostexecutor ./cmd/hostexecutor] - run: - deps: [build] - cmds: [./bin/hostexecutor] - test: - desc: Run all tests - deps: [generate] - cmds: [go test ./... -race] - lint: - cmds: [golangci-lint run ./...] + + # ── Quality gate ────────────────────────────────────────────────────────── + check: - desc: Lint, vet, and test (used by CI) - deps: [generate] + desc: "Full quality gate: lint + vet + test (run before every commit)" cmds: - - golangci-lint run ./... - - go vet ./... - - go test ./... -race -count=1 + - golangci-lint run ./src/... + - go vet ./src/... + - go test ./src/... -race -count=1 + - cd model && python -m pytest tests/ -q + + test: + desc: Run Go tests only + cmds: [go test ./src/... -race] + + lint: + desc: Lint Go code + cmds: [golangci-lint run ./src/...] + + # ── Data ───────────────────────────────────────────────────────────────── + + data:fetch: + desc: "Download G10 tick data from DUKASCopy (2003–2022)" + cmds: [go run ./src/cmd/fetch --config data/config.yaml] + + data:process: + desc: "Resample ticks → hourly OHLCV + features (log-return, rolling HV)" + cmds: [go run ./src/cmd/process --input data/raw --output data/processed] + + data:validate: + desc: "Validate processed data: gap detection, outlier report, regime coverage" + cmds: [go run ./src/cmd/validate --input data/processed] + + # ── Experiment ─────────────────────────────────────────────────────────── + + experiment:run: + desc: "Run an experiment from a spec file. Usage: task experiment:run -- --spec specs/phase-0-ssl-feasibility.md" + cmds: [go run ./src/cmd/experiment {{.CLI_ARGS}}] + + experiment:list: + desc: List all recorded experiment runs + cmds: [ls -lt experiments/ | head -20] + + # ── Evaluation ─────────────────────────────────────────────────────────── + + eval:silhouette: + desc: "Compute silhouette score on embedding output. Usage: task eval:silhouette -- --run experiments/RUNID" + cmds: [go run ./src/cmd/eval silhouette {{.CLI_ARGS}}] + + eval:probe: + desc: "Run linear probe on frozen embeddings vs. realised-vol decile" + cmds: [go run ./src/cmd/eval probe {{.CLI_ARGS}}] + + eval:collapse: + desc: "Check collapse diagnostic: PC1 correlation with rolling HV" + cmds: [go run ./src/cmd/eval collapse {{.CLI_ARGS}}] + + # ── Model (Python) ─────────────────────────────────────────────────────── + + model:train: + desc: "Train TS-JEPA model. Usage: task model:train -- --config model/configs/phase0.yaml" + dir: model + cmds: [python train.py {{.CLI_ARGS}}] + + model:setup: + desc: "Create Python venv and install model dependencies (uv)" + dir: model + cmds: + - uv venv .venv + - uv pip install -r requirements.txt + + # ── Context ────────────────────────────────────────────────────────────── context:sync: - desc: Regenerate all harness-specific context files - cmds: - - bash scripts/context-sync.sh - context:sync:claude: - cmds: [bash scripts/context-sync.sh claude] - context:sync:agents: - cmds: [bash scripts/context-sync.sh agents] - context:sync:cursor: - cmds: [bash scripts/context-sync.sh cursor] + desc: "Regenerate CLAUDE.md and AGENTS.md from .context/PROJECT.md" + cmds: [bash scripts/context-sync.sh] diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..5485b07 --- /dev/null +++ b/data/README.md @@ -0,0 +1,36 @@ +# Data + +Raw and processed FX data is **gitignored** — never committed. + +## Download: DUKASCopy G10 tick data + +Use the provided fetch task: + +```bash +task data:fetch # downloads raw tick data → data/raw/ +task data:process # resamples to hourly OHLCV + features → data/processed/ +task data:validate # gap detection, outlier report, regime coverage check +``` + +## Expected structure (local only) + +``` +data/ + raw/ G10 pairs, tick OHLCV, 2003–present (gitignored) + processed/ Hourly log-returns, rolling HV, VIX-merged (gitignored) + cache/ Intermediate artefacts (gitignored) + config.yaml Download configuration (committed) +``` + +## Pairs + +EUR/USD, GBP/USD, USD/JPY, USD/CHF, AUD/USD, USD/CAD, NZD/USD, EUR/GBP, EUR/JPY, EUR/CHF + +## Splits + +| Split | Date range | Purpose | +|---|---|---| +| Train | 2008-01-01 – 2022-12-31 | Model training only | +| Held-out test | 2023-01-01 – 2023-12-31 | Final evaluation — do not open until evaluation | + +**Training cutoff is hard: 2023-01-01. No post-2022 data informs any design decision.** diff --git a/experiments/README.md b/experiments/README.md new file mode 100644 index 0000000..2076098 --- /dev/null +++ b/experiments/README.md @@ -0,0 +1,32 @@ +# Experiments + +One directory per experiment run. Large outputs (embeddings, checkpoints, logs) are gitignored. Only metric summaries are committed to `results/summaries/`. + +## Naming convention + +``` +YYYYMMDD-HHMMSS-phase-N-short-description/ + config.yaml parameters used for this run (committed via results/summaries/) + metrics.json final metric snapshot (committed via results/summaries/) + embeddings/ (gitignored — large) + checkpoints/ (gitignored — large) + logs/ (gitignored — large) +``` + +## Git tag convention + +Every run that produces reportable metrics gets a tag: + +``` +exp/YYYYMMDD-phase-N-description +``` + +Example: `exp/20260601-phase-0-mae-baseline` + +## Starting a run + +```bash +task experiment:run -- --spec specs/phase-0-ssl-feasibility.md --tag my-run-description +``` + +The runner creates the directory, writes config, runs the experiment, and outputs metrics.json. diff --git a/model/README.md b/model/README.md new file mode 100644 index 0000000..5db9c82 --- /dev/null +++ b/model/README.md @@ -0,0 +1,31 @@ +# model + +Python + PyTorch perimeter. This is the only directory in the project that uses Python. + +## Contents + +``` +model/ + train.py TS-JEPA training entry point + configs/ YAML configs per phase + phase0-mae.yaml + phase1-tsjepa.yaml + tsjepa/ TS-JEPA model implementation (adapted from paper) + tests/ pytest tests for model components + requirements.txt pinned Python dependencies + .venv/ (gitignored — created by `task model:setup`) +``` + +## Setup + +```bash +task model:setup # creates .venv and installs requirements via uv +``` + +## Dependency policy + +Every Python dependency must be justified in a comment in `requirements.txt`. Prefer Go implementations for anything outside the training loop. When adding a new dependency, add an entry to DECISIONS.md explaining why a Go alternative wasn't sufficient. + +## Python version + +3.12 (pinned in `.python-version`) diff --git a/notebooks/README.md b/notebooks/README.md new file mode 100644 index 0000000..7091ae1 --- /dev/null +++ b/notebooks/README.md @@ -0,0 +1,11 @@ +# Notebooks + +Exploratory Data Analysis (EDA) scratch space only. + +## Rules + +- **Never use notebooks for anything reproducible.** Training, evaluation, and metric computation belong in `src/` with tests. +- **Strip all outputs before committing.** Use `nbstripout` or equivalent. +- **Label every notebook with a phase prefix:** `phase0-eda-eurusd-distribution.ipynb` + +Notebooks are thinking tools, not research artifacts. If a finding from a notebook is worth keeping, it goes into a spec, a decision in `DECISIONS.md`, or a result in `results/summaries/` — not the notebook itself. diff --git a/results/README.md b/results/README.md new file mode 100644 index 0000000..b9e40a3 --- /dev/null +++ b/results/README.md @@ -0,0 +1,32 @@ +# Results + +Tracked outputs from experiment runs. Large raw outputs are gitignored — only summaries are committed. + +## Structure + +``` +results/ + summaries/ metric tables, key figures, per-phase result records (committed) + raw/ full embedding outputs, backtest CSVs (gitignored) +``` + +## Per-phase result record format + +Each concluded phase produces a result record in `summaries/`: + +``` +summaries/ + phase-0-[pass|null].md + phase-1-[pass|null].md + ... +``` + +Each record must include: +- Phase name and hypothesis +- Key metrics (silhouette score, R², etc.) with confidence intervals where applicable +- Baseline comparison +- Verdict: PASS / NULL RESULT +- If null: what was investigated, what was found, next step taken +- Link to experiment git tag + +**Null results are valid research outputs and must be committed, not discarded.** diff --git a/specs/phase-0-ssl-feasibility.md b/specs/phase-0-ssl-feasibility.md new file mode 100644 index 0000000..9f4a2b6 --- /dev/null +++ b/specs/phase-0-ssl-feasibility.md @@ -0,0 +1,84 @@ +# Experiment Spec: Phase 0 — SSL Feasibility Gate + +## Hypothesis + +> We believe that a masked autoencoder (MAE) trained on FX hourly time-series will +> produce latent embeddings that show structural separability by volatility regime +> without explicit regime labels, measurable by silhouette score > 0.20 on k-means +> clusters evaluated against a held-out realised-volatility regime label on 2023 data. + +This hypothesis is FALSE if silhouette score ≤ 0.20 on the held-out evaluation. + +## Background + +Before investing in JEPA-specific machinery, we need to confirm that self-supervised +representation learning can find regime structure in FX time-series at all. If the +simplest SSL method (MAE) cannot find structure, JEPA will not either — and the root +cause needs to be understood before proceeding. + +Also validates that TS-JEPA code is reproducible: first task is running TS-JEPA on +the paper's own benchmark, not on FX data. If reproduction takes > 2 weeks, contact +authors or fall back to implementing JEPA masking from V-JEPA codebase. + +Added post Full Grill (2026-05-27). See DECISIONS.md: "Phase 0: SSL feasibility gate." + +## Design + +### Data +- **Source:** DUKASCopy, EUR/USD hourly OHLCV +- **Train:** 2008-01-01 – 2022-12-31 +- **Held-out test:** 2023-01-01 – 2023-12-31 (sealed until final evaluation) +- **Features:** log-return (hourly), rolling 20-period realised HV, VIX (daily → hourly interpolation) +- **Regime label (evaluation only):** rolling 30-day HV percentile; binary: top 50% = high-vol, bottom 50% = low-vol + +### Model +- **Architecture:** 1D temporal Masked Autoencoder + - Encoder: 3-layer 1D CNN + positional encoding + - Decoder: 2-layer MLP reconstructing masked segment +- **Masking:** contiguous temporal block (target); context window = 120h (5 days) +- **Loss:** MSE reconstruction on masked segment + +### TS-JEPA reproduction task (runs in parallel / before MAE) +- Reproduce TS-JEPA paper results on the authors' benchmark dataset +- Go/no-go: if reproduction fails within 2 weeks → contact authors or pivot to V-JEPA adaptation + +### Baseline +- **PCA** on raw feature vectors (same 120h context window, flattened) +- Tests whether any dimensionality reduction finds regime structure; confirms SSL is adding something + +### Ablations +- Masking horizon K ∈ {8h, 24h, 72h} — does context length affect embedding quality? + +## Acceptance Criteria + +- [ ] MAE silhouette score > 0.20 on held-out 2023 data (k-means k=3, vs. binary HV regime label) +- [ ] MAE silhouette exceeds PCA baseline silhouette +- [ ] Rerun ×3 within ±10% of reported silhouette (reproducibility) +- [ ] PC1 / rolling-HV correlation < 0.95 (encoder is not purely encoding volatility level) +- [ ] TS-JEPA reproduced on paper's benchmark within 2 weeks of start + +## Out of Scope + +- JEPA implementation (Phase 1) +- Multi-pair training (Phase 1+) +- VaR, ES, or any risk metric computation +- Any data after 2022-12-31 (training); 2023 test set opened only for final evaluation +- Hyperparameter search beyond the three masking horizons defined above + +## Null Result Protocol + +If MAE silhouette ≤ 0.20 on held-out data: +1. Conclude: SSL-based regime detection is not straightforwardly feasible on EUR/USD hourly data with these three features +2. Investigate in order: (a) try daily resolution instead of hourly, (b) expand feature set to 6 features (add DXY, yield spread), (c) try 4-class regime label (HV quartiles) instead of binary +3. If all three investigations fail: conclude SSL regime detection is not viable on FX data; document and consider pivoting the primary hypothesis to distributional forecasting directly +4. Record result in `results/summaries/phase-0-null.md` +5. Write failure mode to brain: `brain_write wing=jepa-fx hall=failures` + +## Risks + +| Risk | Canary | Mitigation | +|---|---|---| +| TS-JEPA code unreproducible | Benchmark result doesn't match paper within 2 weeks | Contact authors; fall back to V-JEPA adaptation | +| MAE encoder collapses | Reconstruction loss plateau in first 10 epochs; all embeddings near-identical | Add batch normalisation; reduce LR; check masking ratio | +| Regime label too coarse | Silhouette low even with visually structured embeddings | Also evaluate with 4-class HV quartile label | +| PC1 is just volatility | PC1/HV correlation > 0.95 | Useful finding; record it; do not declare success | diff --git a/specs/phase-1-representation-poc.md b/specs/phase-1-representation-poc.md new file mode 100644 index 0000000..64fd6c8 --- /dev/null +++ b/specs/phase-1-representation-poc.md @@ -0,0 +1,82 @@ +# Experiment Spec: Phase 1 — JEPA Representation PoC + +## Hypothesis + +> We believe that a TS-JEPA encoder trained on G10 FX hourly data (2008–2022) will +> produce latent market-state embeddings that are structurally separable by volatility +> regime without explicit regime labels, measurable by silhouette score > 0.35 on +> k-means clusters evaluated against realised-volatility regime labels on held-out +> 2023 data including at least one structural break. + +This hypothesis is FALSE if silhouette score ≤ 0.35 OR linear probe R² ≤ 0.40 on +held-out evaluation. + +**Prerequisites:** Phase 0 passed (MAE silhouette > 0.20, TS-JEPA reproduced). + +## Background + +Phase 0 confirmed that SSL-based representation learning can find regime structure in +FX time-series. Phase 1 tests whether JEPA's specific inductive bias (predict target +embeddings from context embeddings, never reconstruct raw data) produces richer +representations than a simple MAE — and whether those representations are useful for +risk management tasks (measurable via linear probe). + +## Design + +### Data +- **Source:** DUKASCopy, all G10 pairs (EUR/USD, GBP/USD, USD/JPY, USD/CHF, AUD/USD, USD/CAD, NZD/USD, EUR/GBP, EUR/JPY, EUR/CHF), hourly +- **Train:** 2008-01-01 – 2022-12-31 (all 10 pairs, jointly) +- **Held-out test:** 2023-01-01 – 2023-12-31 (sealed until final evaluation) +- **Features:** log-return, rolling 20-period HV, VIX (daily → hourly) +- **Regime label (evaluation only):** rolling 30-day HV percentile; binary + 4-class (quartiles) + +### Model +- **Architecture:** TS-JEPA (Ennadir et al., 2025) + - Context encoder: maps observed window → latent embedding + - Target encoder: EMA of context encoder (momentum β ≈ 0.996); stop-gradient + - Predictor: shallow MLP bridging context → target embedding +- **Masking horizons:** ablate K ∈ {1h, 8h, 24h}; context window = 120h +- **Training:** multi-pair joint training (one model, all G10 pairs) + +### Baseline +- Phase 0 MAE (best masking horizon from Phase 0) +- PCA on raw features (Phase 0 baseline) + +### Ablations +1. TS-JEPA vs MAE — is JEPA's no-reconstruction inductive bias adding value? +2. Single-pair (EUR/USD only) vs. multi-pair — does joint training improve representations? +3. Masking horizon K: {1h, 8h, 24h} + +## Acceptance Criteria + +- [ ] Silhouette score > 0.35 on held-out 2023 data (k-means k=3–5, binary HV label) +- [ ] Linear probe R² > 0.40 on frozen embeddings vs. realised-vol decile +- [ ] PC1 / rolling-HV correlation < 0.85 (encoder learning more than volatility level) +- [ ] TS-JEPA silhouette exceeds Phase 0 MAE silhouette by > 5% +- [ ] Rerun ×3 within ±10% of reported silhouette + +## Out of Scope + +- VaR, ES, distributional forecasting (Phase 3) +- Exotic pairs beyond G10 +- Options pricing, alpha generation, live trading +- Any data after 2022-12-31 for training; test set opened only for final evaluation +- Regime detection backtesting (Phase 2 — embedding drift as early warning) + +## Null Result Protocol + +If primary criteria not met: +1. If silhouette > 0.20 but ≤ 0.35: JEPA shows partial structure; not sufficient for Phase 2. Investigate whether multi-pair training, longer context window, or additional features close the gap. One retry permitted with documented rationale. +2. If silhouette ≤ 0.20: regression from Phase 0; investigate JEPA training stability (collapse risk). Do not proceed. +3. If linear probe R² ≤ 0.40 despite good silhouette: embeddings are structured but not encoding risk-relevant information. Record as a finding; reconsider feature set. +4. Record all results in `results/summaries/phase-1-[pass|null].md` +5. Write findings to brain: `brain_write wing=jepa-fx hall=failures` + +## Risks + +| Risk | Canary | Mitigation | +|---|---|---| +| EMA encoder collapses | All embeddings converge to near-zero; loss goes to ~0 early | Verify EMA momentum schedule; check stop-gradient implementation | +| Encoder learns only EUR/USD vol | PC1 dominated by EUR/USD HV even in multi-pair model | Evaluate per-pair silhouette; if EUR/USD dominates, weight loss by pair | +| Phase 0 silhouette was data-split artefact | Phase 1 MAE baseline doesn't reproduce Phase 0 numbers | Fix random seeds; document split methodology in Phase 0 | +| Insufficient regime diversity (2008–2022) | Embedding clusters don't separate 2023 structural break | Verify 2023 test includes high-vol episode; add 4-class label as backup |