Every ~60 seconds, the bot generates a trade signal. Here’s exactly what happens.
The trading cycle
1. Fetch 250 BTCUSD M15 candles
2. Engineer 56 features (M15 + M30/H1/H4/D1 multi-timeframe)
3. Inject Live news features from Forex Factory calendar
4. Vote RF + XGBoost + LightGBM → BUY / SELL / HOLD
5. Gate Need ≥2/3 agreement + conf ≥60% + prob_diff ≥10%
6. Filter ATR floor · circuit breaker · stops_level · digit precision
7. Execute BUY/SELL at 2% equity risk, SL=0.8×ATR, TP=0.8×ATR
Ensemble voting
Three models vote independently. The final signal requires majority agreement (≥2 of 3):
| Model | Test accuracy |
|---|
| Random Forest | 41.8% |
| XGBoost | 43.0% |
| LightGBM | 42.4% |
| Ensemble | 42.6% |
42.6% signal accuracy still produces high win-rate behavior in live-like OOS runs (recent weekly snapshot: 78.5%). Here’s why: ATR-aware labels produce ~33% random baseline (equal BUY/SELL/HOLD distribution). The models achieve 47–50% precision on BUY/SELL predictions. Combined with a tight TP=0.8×ATR at high-confidence threshold (≥0.60), this creates positive expectancy — most high-confidence signals hit TP before SL.
Confidence gating
Even with majority vote, a signal is suppressed if:
- Confidence < 60%:
prediction.confidence_threshold = 0.60 in ml_config.json
- Probability gap < 10%: The top-2 class probabilities must differ by at least
min_probability_diff = 0.10
Both thresholds are tunable in ml_config.json without retraining. See Optimization for sweep scripts.
Filter pipeline
After the ensemble votes, signals pass through these filters in order:
| Filter | Config key | Description |
|---|
| ATR floor | risk_management.min_atr_threshold | Blocks trades when ATR < $15 (dead market). |
| News block | news_block_minutes | Suppresses signals N minutes before/after high-impact news. |
| EMA trend filter | enable_ema_trend_filter | If enabled: BUY only above EMA200, SELL only below. |
| Circuit breaker | max_consecutive_losses | Stops trading after N consecutive losses. Resets at midnight local time. |
| Stops level | — | Automatically enforced: SL/TP must be stops_level points away from current price. |
Position sizing
Lot size is calculated using the Kelly criterion adjusted for confidence level:
risk_amount = equity × risk_percent / 100
sl_distance = atr × sl_atr_multiplier (in $)
lot = risk_amount / sl_distance
lot = min(lot, max_lot)
Default: 2% equity risk, max 1.0 lot.
Execution
Orders are placed via POST /orders on the MT5 REST API:
{
"symbol": "BTCUSD",
"type": "BUY",
"volume": 0.01,
"sl": 84200.0,
"tp": 85000.0,
"comment": "NOVOSKY BUY conf=0.67"
}
SL and TP are ATR-based:
SL = entry_price − ATR × sl_atr_multiplier (for BUY)
TP = entry_price + ATR × tp_atr_multiplier (for BUY)
Live news injection
The news features (is_news_near, news_minutes_away, news_count_today, etc.) show near-zero SHAP values at training time. This is expected — the Forex Factory calendar only covers the current week, so 99.9% of the 2-year training history sees zeros.
At inference time, trading.py overwrites these features with live values every cycle. They are also used as hard execution filters.
Never drop the session or news features based on SHAP analysis alone. They are protected by design. See Three-File Rule for the full list.