Skip to main content
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):
ModelTest accuracy
Random Forest41.8%
XGBoost43.0%
LightGBM42.4%
Ensemble42.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:
FilterConfig keyDescription
ATR floorrisk_management.min_atr_thresholdBlocks trades when ATR < $15 (dead market).
News blocknews_block_minutesSuppresses signals N minutes before/after high-impact news.
EMA trend filterenable_ema_trend_filterIf enabled: BUY only above EMA200, SELL only below.
Circuit breakermax_consecutive_lossesStops trading after N consecutive losses. Resets at midnight local time.
Stops levelAutomatically 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.