Files
foxhunt/docs/plans/2026-02-21-full-stack-ml-integration-design.md

7.8 KiB

Full-Stack ML Integration Design

Date: 2026-02-21 Goal: Take the ML codebase from "4 models that train independently" to "paper-trading pipeline producing simulated P&L from a 4-model ensemble."

Architecture Overview

Market Data (Databento MBP10/OHLCV)
        │
        ▼
┌──────────────────┐
│ Feature Extraction│ ← 51-dim FeatureVector (43 market + 8 OFI)
└────────┬─────────┘
         │
         ▼
┌──────────────────────────────────────────┐
│           Ensemble Coordinator            │
│  ┌─────┐ ┌─────┐ ┌────────┐ ┌─────┐    │
│  │ DQN │ │ PPO │ │ Mamba2 │ │ TFT │    │
│  └──┬──┘ └──┬──┘ └───┬────┘ └──┬──┘    │
│     └───┬───┘        │         │        │
│         └────────┬───┘─────────┘        │
│              Weighted Vote               │
│      direction: f64, confidence: f64     │
└────────┬─────────────────────────────────┘
         │
         ▼
┌──────────────────┐
│  TradeSignal     │ ← action + size + confidence
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ PaperBroker      │ ← simulates fills against real prices
│ PnL Tracker      │ ← tracks cumulative returns, drawdown, Sharpe
└──────────────────┘

Phase 1: Codebase Cleanup

Problem: 6+ test files use stale imports (foxhunt_ml::, WorkingDQN), several modules have unstaged changes, and cargo test -p ml fails to compile all integration tests.

Actions:

  1. Delete or fix stale test files: tft_int8_integration_test.rs, dqn_full_gradient_flow_integration_test.rs, debug_target_network_init.rs, epsilon_greedy_softmax_test.rs, dqn_target_update_frequency_bug9_test.rs, trending_test.rs
  2. Commit unstaged Mamba2 changes (bilinear discretization, loss.rs, mod.rs)
  3. Commit unstaged ensemble changes (adapters/ module with DQN + PPO adapters)
  4. Verify SQLX_OFFLINE=true cargo test -p ml --lib compiles cleanly
  5. Verify SQLX_OFFLINE=true cargo test -p ml --tests compiles cleanly (integration tests)

Success criteria: Zero compilation errors across all test targets.

Phase 2: Complete Ensemble Inference Adapters

Problem: Only DQN and PPO have ModelInferenceAdapter implementations. Mamba2 and TFT are missing.

Existing trait (ml/src/ensemble/inference_adapter.rs):

pub trait ModelInferenceAdapter: Send + Sync {
    fn predict(&self, features: &FeatureVector) -> MLResult<EnsemblePrediction>;
    fn is_ready(&self) -> bool;
    fn model_name(&self) -> &str;
}

Mamba2 Inference Adapter

  • Input: Mamba2 is a sequence model — needs a sliding window of recent FeatureVectors, not just the current bar. The adapter holds an internal ring buffer of the last N feature vectors (configurable, default 50).
  • Forward pass: Stack the window into a [seq_len, feature_dim] tensor, run through Mamba2SSM::forward(), take the last timestep prediction.
  • Output: The final prediction is a scalar (next-bar return prediction). Convert to directional signal: direction = prediction.signum(), confidence = prediction.abs().min(1.0).
  • Checkpoint loading: Mamba2 checkpoint save/load works (safetensors format via VarMap).

TFT Inference Adapter

  • Challenge: TFT checkpoint save is stubbed (saves empty placeholder). The model weights cannot be restored from disk.
  • Solution: Two options:
    • (A) Fix TFT to expose its VarMap for proper checkpoint save/load (requires TFT refactoring)
    • (B) Use TFT as an in-memory model only — train it, keep it alive, inference from the same instance
  • Recommendation: Option B for now — skip checkpoint loading, construct TFT from config and train before use. Defer VarMap refactoring.
  • Input: TFT needs past_features: [past_len, feat_dim] and known_futures: [future_len, known_dim]. The adapter handles windowing and known-future construction (time features: hour, day-of-week, etc.).
  • Output: Quantile predictions → median as point estimate → directional signal.

Ensemble Coordinator

  • Wire all 4 adapters into the existing EnsembleCoordinator or WeightedVoting system.
  • Graceful degradation: if a model's is_ready() returns false, skip it and re-weight the remaining models.
  • Default equal weights (0.25 each), with API to set custom weights.

Success criteria: EnsembleCoordinator::predict(features) returns a weighted consensus from all available models.

Phase 3: Production Hyperopt Campaign

Problem: Models use default/conservative hyperparameters. No systematic search has been run on the full dataset.

Components:

  1. Hyperopt runner script — CLI entrypoint that configures and runs hyperopt:
    • Input: model type (DQN/PPO), dataset path, trial budget, early stopping strategy (SHA/Hyperband), GPU memory limit
    • Output: trial history CSV, best params JSON, best checkpoint .safetensors
  2. Results persistence — save to ml/hyperopt_results/{model}/{timestamp}/
  3. Automated validation — after hyperopt, take best params → full train → ValidationHarness → verdict
  4. OOM guard — clamp batch size based on available VRAM (RTX 3050 Ti = 4GB, ~230 max batch size for DQN)

Campaign plan:

  • DQN QR-DQN on 361-file 6E.FUT dataset, 50 trials, SHA with η=3
  • PPO on same dataset, 30 trials, Hyperband (max_resource=81, η=3)
  • Validate top-3 param sets through harness, pick the one with best DSR

Success criteria: At least one model achieves ValidationVerdict::Pass (DSR p < 0.05, PBO < 0.25).

Phase 4: Paper Trading Pipeline

Problem: trading_engine has no dependency on ml. There's no path from market data to ML-driven orders.

MLSignalService

  • Lives in trading_engine/src/ml_signal.rs
  • Subscribes to market data feed (Databento or replayed historical data)
  • Extracts 51-dim FeatureVector per bar (43 market features + 8 OFI from MBP10)
  • Calls EnsembleCoordinator::predict(features)EnsemblePrediction
  • Converts to TradeSignal { action: Buy/Sell/Hold, size: f64, confidence: f64 }
  • Emits signals to the order management system

PaperBrokerAdapter

  • Implements the existing broker adapter trait but simulates fills
  • Fill price = current market price + simulated slippage
  • Tracks simulated positions, P&L, commissions
  • No real network calls

PaperPnLTracker

  • Tracks cumulative returns, max drawdown, rolling Sharpe (252-bar window)
  • Periodic reporting to stdout/log
  • Optional: write results to CSV for post-analysis

Integration Test

  • Replay 1 week of historical 6E.FUT OHLCV data through the full pipeline
  • Verify: signals generated, positions opened/closed, P&L computed
  • Assert no panics, all values finite

Success criteria: Full pipeline runs on replayed data, produces finite P&L and Sharpe.

Constraints

  • GPU: RTX 3050 Ti, 4GB VRAM — batch sizes must be bounded
  • Concurrent sessions: Other Claude sessions may be active — use worktrees for isolation
  • Clippy denials: unwrap_used, expect_used, panic, indexing_slicing — all code must use safe patterns
  • SQLX_OFFLINE=true: Required for all cargo commands

Non-Goals (YAGNI)

  • Live broker integration (paper-trading only for now)
  • Multi-asset support (6E.FUT only)
  • Web dashboard or UI
  • Cloud deployment
  • Model versioning / MLOps