Skip to main content

Model overview

ModelTypeAlgorithmOutput
SignalClassifierRF + XGB + LGB (majority vote)BUY / SELL / HOLD + confidence
PositionClassifierRF + XGB + LGB (majority vote)HOLD / EXIT / ADD
SL/TPRegressorLightGBMSL multiplier, TP multiplier
RiskRegressorLightGBMRisk multiplier [0.10, 1.25]
Training order is strict: Signal β†’ Position β†’ SLTP β†’ Risk. The Risk model depends on a backtest that requires trained SLTP models.

Signal model

File: ml/ensemble_trainer.py / ml/ensemble_predictor.py The signal model answers: should we trade this bar?

Architecture

Three independent classifiers trained on the same 59-feature vector with ATR-aware labeling. Majority vote determines the final prediction. When two or more models agree, confidence is that fraction’s vote strength.
RF vote: BUY
XGB vote: BUY
LGB vote: SELL
β†’ Result: BUY (2/3), confidence = 0.67
If confidence falls below confidence_threshold (default 0.633), the signal is treated as HOLD.

Training

python train_ml_model.py --ensemble
Hyperparameters are tuned with Optuna (ml/tune/hyperparams.py). The tuner optimizes a custom objective that balances precision, recall, and drawdown on an OOS validation split.

Inference

Inference priority: calibrated pkl β†’ ONNX β†’ raw pkl After training, calibrate_models() wraps each classifier with CalibratedClassifierCV to produce well-calibrated probability estimates. The calibrated model is used at runtime.
ONNX export (ml/model_trainer.py) runs after training but before calibration. ONNX cannot wrap CalibratedClassifierCV, so the ONNX fallback is always the uncalibrated version.

Model paths

"ensemble_paths": {
  "rf": "models/rf_signal.pkl",
  "xgb": "models/xgb_signal.pkl",
  "lgb": "models/lgb_signal.pkl",
  "rf_calibrated": "models/rf_signal_calibrated.pkl",
  "xgb_calibrated": "models/xgb_signal_calibrated.pkl",
  "lgb_calibrated": "models/lgb_signal_calibrated.pkl",
  "rf_onnx": "models/rf_signal.onnx",
  "xgb_onnx": "models/xgb_signal.onnx",
  "lgb_onnx": "models/lgb_signal.onnx"
}

Position model

File: ml/position_trainer.py / ml/position_predictor.py The position model answers: should we hold, exit early, or add to this open position?

Architecture

Same three-classifier majority-vote ensemble. Trained on labeled historical positions using the same 59-feature vector, but labeling captures in-trade state rather than entry signals. Output classes:
  • HOLD β€” keep position open
  • EXIT β€” close now (reversal signal)
  • ADD β€” increase size (continuation signal β€” currently conservative)

Training

python train_ml_model.py --position
Position labeling is handled by ml/position_labeling.py. Labels are generated from historical trades where early exit would have improved the outcome.

Tuning

Optuna tuning: ml/tune/position.py

Dynamic SL/TP model

File: ml/sltp_trainer.py / ml/sltp_predictor.py The SL/TP model answers: given current market conditions, what SL and TP distance (in ATR units) maximizes risk-adjusted return?

Architecture

LightGBM regressor. Outputs two values:
  • sl_multiplier β€” SL as a multiple of ATR
  • tp_multiplier β€” TP as a multiple of ATR
Constraint: tp_multiplier > sl_multiplier is enforced at inference time (Phase 16 fix β€” previously a bug allowed tp < sl).

Training

python train_ml_model.py --sltp

Applied at inference

sl_pts = sl_multiplier Γ— ATR Γ— atr_multiplier
tp_pts = tp_multiplier Γ— ATR Γ— atr_multiplier
# Equity-aware cap applied after:
cap = (max_risk_pct Γ— equity) / (lot Γ— pip_value)
sl_pts = min(sl_pts, cap)

Risk multiplier model

File: ml/risk_trainer.py / ml/risk_predictor.py The risk model answers: given the current state of the account, how aggressively should we size this trade?

Architecture

LightGBM regressor. Input: 7 equity-state features. Output: a multiplier in [0.10, 1.25].

Features

These features are fixed. They must stay in sync across ml/risk_trainer.py, ml/risk_predictor.py, and ml_config.json β†’ risk_model.features.
RISK_FEATURES = [
    "drawdown_pct",       # current drawdown from peak
    "equity_ratio",       # equity / starting_balance
    "win_rate_recent",    # win rate over last N trades
    "consecutive_losses", # current consecutive loss streak
    "volatility_20",      # 20-bar rolling volatility
    "atr_14",             # 14-period ATR
    "hour"                # UTC hour (session context)
]

Training

The Risk model is trained after SLTP models are saved, because its training uses a backtest that requires SL/TP predictions:
python train_ml_model.py --sltp   # must run first
python train_ml_model.py --risk
Or train all four in order:
python train_ml_model.py --ensemble --position --sltp --risk

Kelly-inspired design

The risk multiplier is loosely inspired by Kelly criterion: scale up when the edge is high (low drawdown, high recent win rate) and scale down when conditions deteriorate. The model learns this relationship from labeled historical trade data augmented with account-state features.

Model storage

Models are stored in two places:
LocationWhatUsed for
models/Metadata JSON + compatibility checkGit-tracked state
Hugging Face HubAll .pkl and .onnx binariesDistribution to trading machines

Pushing and pulling

python ml/hf_hub.py --push     # after retrain: upload new models
python ml/hf_hub.py --pull     # on trading machine: download latest

Compatibility check

Before go-live after any retrain, verify model compatibility:
# models/model_compat.json must match ml_config.json
python -c "from ml.ensemble_predictor import EnsemblePredictor; EnsemblePredictor().load()"
If features or model paths diverge, the predictor raises a ModelCompatibilityError at startup.