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 <noreply@anthropic.com>
This commit is contained in:
chrisnov-it
2026-06-23 15:39:44 +08:00
parent 7b576d8a2c
commit 24e66e39ae
2 changed files with 52 additions and 2 deletions
+43
View File
@@ -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)
+9 -2
View File
@@ -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)