> ## Documentation Index
> Fetch the complete documentation index at: https://docs.novosky.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Models

> NOVOSKY uses four ML models: a signal ensemble, a position model, a dynamic SL/TP regressor, and a risk multiplier. This page covers each model's design, training inputs, and outputs.

## 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 62-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

```bash theme={null}
python scripts/retrain.py   # trains all 4 models together
```

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.

<Note>
  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.
</Note>

### Model paths

```json theme={null}
"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 62-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

```bash theme={null}
python scripts/retrain.py   # always trains all 4 models together
```

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

```bash theme={null}
python scripts/retrain.py   # always trains all 4 models together
```

### Applied at inference

```python theme={null}
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`.

```python theme={null}
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:

```bash theme={null}
python scripts/retrain.py   # trains all 4 models in the correct order
```

### 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 differently depending on whether you are using the Python bot or the Go binary.

### Python bot

| Location         | What                                | Used for                         |
| ---------------- | ----------------------------------- | -------------------------------- |
| `models/`        | Metadata JSON + compatibility check | Git-tracked state                |
| Hugging Face Hub | All `.pkl` and `.onnx` binaries     | Distribution to trading machines |

```bash theme={null}
python ml/hf_hub.py --push     # after retrain: upload new models
python ml/hf_hub.py --pull     # on trading machine: download latest
```

### Go binary

All model files are **embedded inside the binary** at compile time. Customers do not receive a `models/` directory or a Hugging Face Hub connection.

| Embedded file              | Format              | Go inference                                     |
| -------------------------- | ------------------- | ------------------------------------------------ |
| `ensemble_rf/xgb/lgb.onnx` | ONNX classification | `input="X"`, `outputs=["label","probabilities"]` |
| `position_rf/xgb/lgb.onnx` | ONNX classification | Same                                             |
| `risk_lgb.onnx`            | ONNX regression     | `input="float_input"`, `output="variable"`       |
| `sltp_sl/tp_lgb.onnx`      | ONNX regression     | Same                                             |
| `*_scaler.json`            | StandardScaler JSON | Applied before ONNX inference                    |
| `*_metadata.json`          | Feature names       | Read at startup — never hardcoded                |

After every retrain, run:

```bash theme={null}
python scripts/export_scalers.py          # PKL scalers → JSON
python scripts/export_onnx_regression.py  # risk/SLTP .txt → .onnx
bash scripts/pack_embedded.sh             # copy into internal/embedded/
go build -o novosky_go ./cmd/novosky/     # rebuild binary
```

See [Go binary](/developer/go-binary) for the full post-retrain workflow.

### Compatibility check (Python bot)

Before go-live after any retrain, verify model compatibility:

```bash theme={null}
# 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.
