Skip to main content

Trading loop overview

trading.py runs in a ~60-second loop. On every iteration it:
  1. Fetches the latest BTCUSD M15 candles from the MT5 REST API
  2. Engineers 59 features from the raw OHLCV data
  3. Scores the bar through the signal ensemble
  4. Applies filters and gates
  5. Sizes the position with the dynamic SL/TP and risk multiplier models
  6. Executes via the MT5 REST API
  7. Manages open positions through the position model

Stage 1 β€” Feature engineering

ml/feature_engineering.py transforms raw OHLCV candles into the 59-feature vector that all four models consume. Feature families:
  • Price structure: Bollinger Bands, ATR, range position, high/low ratios
  • Trend: EMA crossovers, MACD signal, trend strength, price vs EMA200
  • Momentum: RSI, RSI divergence, momentum over 5 and 10 bars
  • Volatility: Normalized volatility, ATR-to-price ratio
  • Volume: Volume ratio vs rolling average, volume trend
  • Session/news: London, NY, Asian session flags; news proximity and risk window
  • Account state: Current drawdown %, equity ratio, recent win rate, consecutive losses
The feature vector is identical at training time and inference time. The order is fixed β€” any change requires full retraining.

Stage 2 β€” Signal model

ml/ensemble_predictor.py runs a majority-vote ensemble of Random Forest, XGBoost, and LightGBM classifiers. Each model outputs a probability for BUY, SELL, or HOLD. The ensemble aggregates by majority vote, then applies a confidence threshold. Trades below confidence_threshold (default 0.633) are discarded as HOLD.
# simplified inference
rf_pred = rf_model.predict_proba(features)
xgb_pred = xgb_model.predict_proba(features)
lgb_pred = lgb_model.predict_proba(features)
signal, confidence = majority_vote([rf_pred, xgb_pred, lgb_pred])
if confidence < threshold:
    return HOLD
Model inference order: calibrated pkl β†’ ONNX β†’ raw pkl. CalibratedClassifierCV cannot be converted to ONNX, so the ONNX fallback uses the uncalibrated model.

Stage 3 β€” Filters and circuit breakers

Before any order is placed, the signal passes through a chain of gates:
GateTriggers whenAction
ATR floorCurrent ATR < min_atrSkip β€” market too quiet
Circuit breakerConsecutive losses β‰₯ max_consecutive_lossesPause trading
Weekly drawdownWeekly loss β‰₯ max_weekly_drawdown_pctPause for the week
Hard haltTotal equity loss β‰₯ hard_halt_pctsys.exit(99) β€” manual restart required
News blockis_news_near = 1 and news_block_minutes > 0Skip β€” high-impact news window
The hard halt never restarts automatically. Before restarting after a halt, update starting_balance_usd in config.json to your current equity.

Stage 4 β€” Dynamic SL/TP

ml/sltp_predictor.py uses a LightGBM regression model to predict optimal SL and TP multipliers relative to ATR.
sl_pts = sl_multiplier Γ— ATR Γ— config.atr_multiplier
tp_pts = tp_multiplier Γ— ATR Γ— config.atr_multiplier
# tp_pts must always be > sl_pts (Phase 16 fix)
The equity-aware SL cap then applies:
cap_pts = (max_risk_pct Γ— equity) / (lot Γ— pip_value)
sl_pts = min(sl_pts, cap_pts)
This ensures that even if the model predicts a wide SL, actual risk never exceeds your profile’s limit.

Stage 5 β€” Risk multiplier

ml/risk_predictor.py runs a LightGBM model that takes 7 equity-state features and outputs a risk multiplier in [0.10, 1.25]. Features: drawdown_pct, equity_ratio, win_rate_recent, consecutive_losses, volatility_20, atr_14, hour The multiplier scales the base risk percentage:
effective_risk = base_risk_percent Γ— risk_multiplier
When the account is healthy and recent trades are profitable, the multiplier trends toward 1.25 (Kelly-style upscaling). During drawdown or after losses, it reduces toward 0.10.

Stage 6 β€” Position sizing

Lot size is computed from the effective risk and the SL distance:
risk_usd = equity Γ— effective_risk_pct / 100
lot = risk_usd / (sl_pts Γ— pip_value)
lot = max(min_lot, min(lot, max_lot))
For cent accounts (pip_value = 100 USC/lot/point), risk_usd is in raw USC.

Stage 7 β€” Position management

After order execution, the position model (ml/position_predictor.py) evaluates every open position on each loop iteration. Same 59-feature vector. Outputs:
  • HOLD β€” keep the position, wait for TP or SL
  • EXIT β€” close early (strong reversal signal)
  • ADD β€” increase position size (strong continuation signal; currently conservative)
The position model reduces the number of trades stopped out at SL by closing earlier when momentum reverses.

Weekly optimization pipeline

Every Sunday at 2 am UTC, scripts/weekly_optimize.py runs autonomously. Key invariants:
  • The sweep uses only the first 70% of the OOS window β€” the last 30% is a true holdout
  • Models are always trained in order: Signal β†’ Position β†’ SLTP β†’ Risk
  • If the new score doesn’t beat baseline by β‰₯ 2%, everything rolls back
See Optimization pipeline for the full 13-phase breakdown.