Model overview
| Model | Type | Algorithm | Output |
|---|
| Signal | Classifier | RF + XGB + LGB (majority vote) | BUY / SELL / HOLD + confidence |
| Position | Classifier | RF + XGB + LGB (majority vote) | HOLD / EXIT / ADD |
| SL/TP | Regressor | LightGBM | SL multiplier, TP multiplier |
| Risk | Regressor | LightGBM | Risk 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.
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:
| Location | What | Used for |
|---|
models/ | Metadata JSON + compatibility check | Git-tracked state |
| Hugging Face Hub | All .pkl and .onnx binaries | Distribution 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.