From 24e66e39ae0df0ef5c6fbce3604097843a14a0dd Mon Sep 17 00:00:00 2001 From: chrisnov-it Date: Tue, 23 Jun 2026 15:39:44 +0800 Subject: [PATCH] Add risk management: spread guard, break-even SL, ATR multiplier cap - Spread guard: skip entry when spread > 60 pts (3x normal for XM) - Break-even: auto-move SL to entry price when profit exceeds 1x ATR - ATR multiplier cap: SL capped at max 5x ATR, TP at max 10x ATR (prevents absurd SL/TP from misconfigured bots) Co-Authored-By: Claude --- core/bots/trading_bot.py | 43 ++++++++++++++++++++++++++++++++++++++++ core/mt5/trade.py | 11 ++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index 96d850b..33bc433 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -119,6 +119,49 @@ class TradingBot(threading.Thread): current_position = self._get_open_position() + # --- Spread Guard (check even without position) --- + try: + symbol_info = mt5.symbol_info(self.market_for_mt5) + if symbol_info and symbol_info.spread > 60: + logger.info(f"Bot {self.id} [{self.strategy_name}] - Spread {symbol_info.spread} terlalu lebar. Skip entry.") + if not current_position: + time.sleep(self.check_interval) + continue + except Exception: + pass + + # --- Break-even: move SL to entry when profit > 1x ATR --- + if current_position: + try: + from core.utils.mt5 import get_rates_mt5 + atr_df = get_rates_mt5(self.market_for_mt5, self.tf_map.get(self.timeframe, mt5.TIMEFRAME_H1), 20) + if atr_df is not None and len(atr_df) > 14: + import pandas_ta as ta + atr_val = ta.atr(atr_df['high'], atr_df['low'], atr_df['close'], length=14).iloc[-1] + if atr_val and atr_val > 0: + if current_position.type == mt5.ORDER_TYPE_BUY: + tick = mt5.symbol_info_tick(self.market_for_mt5) + if tick: + profit = tick.bid - current_position.price_open + if profit > atr_val and current_position.sl < current_position.price_open: + req = {"action": mt5.TRADE_ACTION_SLTP, "position": current_position.ticket, + "symbol": self.market_for_mt5, "sl": current_position.price_open, + "tp": current_position.tp, "magic": self.id} + mt5.order_send(req) + logger.info(f"Bot {self.id} [{self.strategy_name}] - Break-even BUY di {current_position.price_open}") + elif current_position.type == mt5.ORDER_TYPE_SELL: + tick = mt5.symbol_info_tick(self.market_for_mt5) + if tick: + profit = current_position.price_open - tick.ask + if profit > atr_val and (current_position.sl == 0 or current_position.sl > current_position.price_open): + req = {"action": mt5.TRADE_ACTION_SLTP, "position": current_position.ticket, + "symbol": self.market_for_mt5, "sl": current_position.price_open, + "tp": current_position.tp, "magic": self.id} + mt5.order_send(req) + logger.info(f"Bot {self.id} [{self.strategy_name}] - Break-even SELL di {current_position.price_open}") + except Exception as e: + logger.debug(f"Bot {self.id} - Break-even check non-fatal: {e}") + # Check if market is open before handling trade signal if self._is_market_open_for_symbol(): self._handle_trade_signal(signal, current_position) diff --git a/core/mt5/trade.py b/core/mt5/trade.py index b37ba3f..797fb21 100644 --- a/core/mt5/trade.py +++ b/core/mt5/trade.py @@ -89,8 +89,15 @@ def place_trade(symbol, order_type, risk_percent, sl_atr_multiplier, tp_atr_mult # --- 2. Tentukan harga & level SL/TP --- price = mt5.symbol_info_tick(symbol).ask if order_type == mt5.ORDER_TYPE_BUY else mt5.symbol_info_tick(symbol).bid - sl_distance = atr * sl_atr_multiplier - tp_distance = atr * tp_atr_multiplier + + # Cap ATR multiplier to safe bounds (default 2x SL, 4x TP if absurd values) + sl_mult = max(1, min(sl_atr_multiplier, 5)) + tp_mult = max(2, min(tp_atr_multiplier, 10)) + if sl_mult != sl_atr_multiplier or tp_mult != tp_atr_multiplier: + logger.warning(f"ATR multiplier capped: SL {sl_atr_multiplier}→{sl_mult}, TP {tp_atr_multiplier}→{tp_mult}") + + sl_distance = atr * sl_mult + tp_distance = atr * tp_mult sl_level = round(price - sl_distance if order_type == mt5.ORDER_TYPE_BUY else price + sl_distance, digits) tp_level = round(price + tp_distance if order_type == mt5.ORDER_TYPE_BUY else price - tp_distance, digits)