A model-agnostic classification foundation for futures markets — any pretrained time-series classification backbone learns market structure from raw OHLCV, then thin per-strategy heads finetune on top, all held to an honest-ruler walk-forward.
Contents: Quick Start · Philosophy · Overview · Self-Supervised Pretraining (3 stages) · The Classifier Seam · Finetuning Pipeline · The Training Loop · Add a Strategy · Data · Project Structure
pip install -e .
# + the package for your chosen classification backboneFFM separates learning the market from deciding a trade. A 2-stage self-supervised pipeline (masked modeling → candle forecasting) progressively refines the backbone on raw OHLCV; a thin per-strategy classifier then finetunes on top, validated on the honest ruler.
from futures_foundation.finetune import ssl, wf, produce
# 1) PRETRAIN — 2-stage self-supervised (mask → forecast), GPU/Colab.
# Each stage warm-starts the next; output = a refined backbone checkpoint.
ssl.loop_ssl(data_dir='…', out_path='ssl_ohlcv.pt', pretext='mask') # then 'forecast'
# 2) VALIDATE — walk-forward honest ruler with overfit→Optuna; classifier-agnostic.
verdict = wf.loop_streamed(make_labeler, streams,
clf_kwargs={'backbone_ckpt': 'ssl_ohlcv.pt'})
# 3) PRODUCE — only if it generalizes; trains on all data minus a holdout → ONNX bundle.
if verdict['generalizes']:
produce.train_final_streamed(make_labeler, streams)→ Labeler contract: Add a Strategy · How validation catches & fixes overfitting: The Training Loop
Separate "understanding market context" from "making strategy-specific decisions."
Just as BERT learns language structure from unlabeled text before being finetuned for sentiment or Q&A, the FFM backbone learns regime, structure, and volatility from unlabeled futures OHLCV before any strategy logic runs. A strategy then adds only what the backbone cannot derive — setup geometry, entry distance, risk sizing — and finetunes a light classification head. Market-context knowledge is learned once and shared across every strategy.
Two principles shape everything below:
- The backbone is a pretrained foundation model designed for classification — and ingests multivariate raw OHLCV (price + volume + range), so it can encode participation and volatility, the raw material of momentum compression → expansion.
- The backbone is swappable. FFM commits to an interface, not a model. Any pretrained classification foundation model plugs in behind the same seam without touching a single strategy.
The flow is a self-supervised pretraining pipeline over one shared backbone, then a thin per-strategy head:
raw OHLCV (multi-ticker × multi-timeframe)
│
▼ SELF-SUPERVISED PRETRAINING — 2 progressive stages (finetune/ssl.py, GPU/Colab)
1) masked modeling → regime / structure / volatility
2) candle forecasting → forward price-action dynamics ──► refined backbone checkpoint
│ each stage WARM-STARTS from the previous; anti-forgetting + crash-safe resume
▼ DOWNSTREAM FINETUNE (finetune/wf.py → produce.py)
strategy labeler + light classifier head ──► ONNX bundle the bot loads
- Honest by construction. Every result passes the honest ruler: walk-forward × {REAL, SHUFFLE, RANDOM} with an overfit→Optuna loop and a pre-registered PASS/FAIL auto-verdict. A number is believed only if REAL clearly beats every control, fold after fold.
- 2026 is a reserved out-of-sample year — excluded from both pretraining and the rolling walk-forward, so the final OOS is never contaminated.
- Causal by contract. Every feature/window is strictly causal (streaming == batch, per bar); the leak audit is mandatory.
futures_foundation/finetune/ssl.py orchestrates a 2-stage self-supervised pipeline that progressively refines the backbone on raw OHLCV. Each stage warm-starts from the previous checkpoint, so the foundation compounds — regime understanding → forward dynamics — before any strategy sees it. Pretext tasks are pluggable (finetune/pretext/): a new pretraining objective is one module (its own reserve / train / gate); the orchestrator never changes.
Stage 1 — Masked modeling (learn regime / structure / volatility). A random fraction of bars in each window is masked, and the encoder reconstructs them from context (MSE on masked positions only). To fill a gap it must know what normally comes next given the local regime → it learns volatility, structure, and compression→expansion. (Masked bars are noise-filled, not zeroed, so the backbone's per-patch instance-norm never divides by zero.)
Stage 2 — Candle forecasting (learn forward price-action dynamics). Warm-started from stage 1, the encoder predicts the future candle (OHLCV) at multiple horizons from variable-length context, expressed as a move from "now" (so "copy the last bar" = predict-zero, and is punished). Predicting where price goes across near and far horizons forces multi-scale forward dynamics into the embedding — the raw material for telling a developing move from noise. Reported per-horizon so the far horizons are visibly learning.
Shared discipline across every stage:
| Guardrail | What it does |
|---|---|
| Warm-start chain | stage 2 starts from stage 1 — the foundation compounds rather than restarts |
| Anti-forgetting | later refines can freeze the tokenizer + early backbone layers and use a gentle LR, so a new objective sharpens the representation without erasing earlier stages (the same layer-freeze technique used for downstream partial-finetuning) |
| Crash-safe save + resume | the best checkpoint is written progressively (atomic, every val improvement) with a resume path — a disconnected GPU run never loses progress |
| Time-split val early-stop | generalizes forward in time; 2026 is excluded from every stage (inputs and targets) so the downstream OOS is never contaminated |
| Apples-to-apples controls (opt-in) | REAL vs time-SHUFFLE vs RANDOM input with the target held fixed — REAL must beat both, certifying the stage learned from genuine temporal order, not a shortcut |
| Linear probe (soft) | reads regime / vol / structure off the frozen embedding — informative but non-discriminating alone (generic domain-adaptation can pass it); the downstream walk-forward A/B is the decisive judge |
Runs GPU-maximized (data resident on GPU, large batch, AMP where applicable). Every stage outputs an adapted backbone checkpoint consumed downstream via backbone_ckpt.
futures_foundation.finetune.classifier is the swap point: a Classifier ABC + a get_classifier(name, **cfg) registry. A strategy pipeline references a classifier by name; the backbone behind it can change with no strategy edits.
from futures_foundation.finetune.classifier import get_classifier
clf = get_classifier(BACKBONE, backbone_ckpt='ssl_ohlcv.pt', ft_mode='partial')Two ways to attach the backbone — both initialize from the SSL checkpoint via backbone_ckpt, both run torch in an isolated subprocess (the parent stays torch-free, so torch never collides with other native libraries in one process):
- End-to-end fine-tune — foundation model + per-strategy channel adapter + light head, all trained together. Maximum capacity; the backbone specializes to the task.
- Frozen head-only — embed each window once through the frozen encoder, then train a cheap logistic or MLP head per fold on the cached embedding (optionally concatenated with hand-crafted geometry features). This is the "embed once → head per fold" pattern: fast enough to iterate on local hardware, and a clean linear/shallow probe of what the representation actually carries.
- Cross-run embedding cache — the frozen embedding is deterministic in
(backbone_ckpt, bars, window spec), so it's cached to disk keyed on exactly those. The expensive embed cost is paid once per backbone: reruns, head swaps (logistic↔MLP), and interpretability checks reuse the cached vectors instead of re-embedding.EMBED_CACHE=0disables;EMBED_CACHE_DIRrelocates it.
- Cross-run embedding cache — the frozen embedding is deterministic in
Currently supported: one pretrained classification backbone (installed via its own package), in both attach modes; additional pretrained classification foundation models are planned behind the same interface.
logistic— a torch-free baseline / test vehicle for the whole pipeline.- Add your own backbone by implementing
featurize()+fit_predict()and registering it — the walk-forward, produce, and ONNX paths are all classifier-agnostic.
futures_foundation/finetune/ — the strategy-pluggable harness: streamed walk-forward evaluator with honest-ruler controls, production trainer, ONNX export. Every strategy goes through it; nothing about it is tied to a specific backbone.
What it does: a strategy labeler defines event candidates (any rule-based setup); for each event a multivariate context window → the classifier predicts P(take), scored on realized R via the strategy's own evaluator. Validation runs the overfit-driven training loop on a rolling train / validate / test walk-forward with REAL / SHUFFLE / RANDOM controls and a pre-registered PASS/FAIL auto-verdict. The production trainer then fits one head on the full corpus minus the holdout and saves a single bundle + ONNX the bot loads.
| Component | Role |
|---|---|
wf.py |
Streamed walk-forward (run_streamed, loop_streamed) — featurize once across all streams (bounded RAM), rolling folds, VAL-selected operating point + VAL→TEST generalization gate, REAL/SHUFFLE/RANDOM, overfit→Optuna loop, PASS/FAIL verdict. 2026 excluded as OOS. |
produce.py |
Production training: one fit on the full corpus minus an N-month holdout; scores the 2026 OOS; emits the deployment bundle + signal contract + ONNX. |
tune.py |
Optuna search with a generalization-robust objective + held-out guard, auto-falling back to defaults unless the tuned config beats them. |
loop.py |
The overfit-driven loop: default WF → generalize check → Optuna only if it overfits → rerun → repeat → final full WF. |
_memmap.py |
Featurize-to-disk + streaming so full multi-timeframe, all-ticker runs fit in bounded RAM. |
classifier.py / classifiers/ |
The model-agnostic seam (above) + backbone implementations. |
loop_streamed(...) runs the whole process as one self-correcting loop. Optuna fires only when overfitting is detected — a config that already generalizes is left untouched:
- Walk-forward with the default classifier config.
- Generalizes? (VAL→TEST gap within tolerance, REAL beats controls fold-after-fold) → keep defaults, done.
- Overfit? → Optuna for a config that generalizes (objective rewards cross-fold stability; auto-falls back to defaults unless the tuned config beats them on a held-out guard).
- Rerun; repeat until it passes (capped — if nothing generalizes, the model is flagged).
Two guardrails keep it honest: the VAL→TEST gate (operating point chosen on validation, reported on test; an edge that decays is rejected) and tuning/selection that sees train+validation only — test is never consulted.
class MyLabeler:
n_classes = 2 # binary selection (take / skip)
def calendar(self): ... # ticker × timestamp
def build(self, lo, hi, test_start):
# → (contexts, labels, keys) — keys carry realized-R per target
...
def mv_contexts(self, keys): # → [N, C, seq] multivariate windows
...
def evaluate(self, keys, preds): # → per-trade realized-R array
...from futures_foundation.finetune import wf, produce
verdict = wf.loop_streamed(make_labeler, streams,
clf_kwargs={'backbone_ckpt': 'ssl_ohlcv.pt'})
if verdict['generalizes']:
produce.train_final_streamed(make_labeler, streams, export_onnx=True)The labeler's final run() (in finetune.base.StrategyLabeler) applies a session-calibrated TP≥SL triple barrier (entry = next-bar open) and emits signal_label / max_rr / sl_distance / direction, centralizing the entry-after-signal / orientation bug class once for every strategy. FoldHealthMonitor flags per-fold pathologies (val/test gap, N-collapse, confidence-flat, zero-signal-fold); realized-R economics report PF / WR / mean-R / maxDD under a trailing exit (not optimistic MFE).
9 instruments: ES, NQ, RTY, YM (equity indices), GC, SI (metals), CL (energy), ZB, ZN (rates) — each at 1 / 3 / 5 / 15min.
data/
├── ES_3min.csv # datetime, open, high, low, close, volume
├── ES_5min.csv
└── ...
databento/build_continuous.py resamples raw 1-min bars to any timeframe; databento/append_update.py splices new exports into data/ continuously. A configurable data_dir (e.g. a Google-Drive mount on Colab) lets pretraining and finetuning read the same CSVs anywhere.
Raw OHLCV is the backbone's input. For strategies that fuse hand-crafted geometry, futures_foundation.features.derive_features produces instrument-agnostic (ATR-normalized), strictly causal features (bar anatomy, returns/momentum, volume dynamics, volatility, session context, market structure, HTF context) — every feature held to the no-look-ahead causal-parity rule (streaming == batch, per bar).
Futures-Foundation-Model/
├── futures_foundation/ # Foundation package (torch-free to import)
│ ├── finetune/ # ★ The model-agnostic classification pipeline
│ │ ├── ssl.py / ssl_data.py # SSL orchestrator (2-stage pretraining) + data assembly
│ │ ├── pretext/ # pluggable pretext tasks: mask / forecast
│ │ │ ├── base.py # PretextTask interface (reserve / train / gate)
│ │ │ └── _torch/ # per-stage GPU trainers + shared BaseTrainer (save/resume/freeze)
│ │ ├── _ssl_torch.py # back-compat shim → re-exports pretext/_torch (frozen embed, ONNX)
│ │ ├── ssl_probe.py # linear probe: regime / vol / structure (soft signal)
│ │ ├── classifier.py # Classifier ABC + get_classifier registry (the seam)
│ │ ├── classifiers/ # end-to-end FT + frozen head-only (cached embeddings) + logistic
│ │ ├── wf.py # streamed walk-forward honest ruler + overfit→Optuna
│ │ ├── produce.py # production trainer + 2026 OOS + ONNX + contract
│ │ ├── tune.py / loop.py # Optuna search + overfit-driven loop
│ │ ├── _memmap.py # featurize-to-disk streaming (bounded RAM)
│ │ └── base.py / health.py # StrategyLabeler + FoldHealthMonitor
│ ├── extractors/ # Pluggable backbone interface (FeatureExtractor)
│ ├── features.py / primitives/ # OHLCV → causal features; indicators/barriers
│ └── prepare.py # raw CSVs → features parquet
├── colab/ # ★ Colab runners: clone → install → Drive paths → run
├── databento/ # Continuous-contract build + incremental update
├── tests/ # Unit tests (pre-commit gated; torch-free by contract)
└── data/ # Raw OHLCV CSVs (gitignored)
Apache 2.0 — See LICENSE for details.
This software is for research and educational purposes only. It does not constitute financial advice. Trading futures involves substantial risk of loss. Past performance of any model does not guarantee future results.