From 6e0db5274bcf5bae5f9ca2cc2805bce457cc4730 Mon Sep 17 00:00:00 2001 From: GifariKemal Date: Fri, 6 Feb 2026 13:36:46 +0700 Subject: [PATCH] fix: spread dict access and close_position retry logic - Fix 'dict' has no attribute 'spread' by using .get("spread", 0) - Add 3-retry loop to close_position() with fresh price each attempt - Match retry pattern from send_order() for consistency Co-Authored-By: Claude Opus 4.6 --- main_live.py | 2 +- src/mt5_connector.py | 116 +++++++++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/main_live.py b/main_live.py index 57858ff..061d2f8 100644 --- a/main_live.py +++ b/main_live.py @@ -1220,7 +1220,7 @@ class TradingBot: regime=regime, volatility=volatility, session=session_status.get("session", "unknown"), - spread=self.mt5.get_symbol_info(self.config.symbol).spread if hasattr(self.mt5, 'get_symbol_info') else 0, + spread=self.mt5.get_symbol_info(self.config.symbol).get("spread", 0) if hasattr(self.mt5, 'get_symbol_info') else 0, atr=0, # ATR calculated in main loop, not available here smc_signal=signal.signal_type, smc_confidence=signal.confidence, diff --git a/src/mt5_connector.py b/src/mt5_connector.py index 385a7c8..773e6ef 100644 --- a/src/mt5_connector.py +++ b/src/mt5_connector.py @@ -639,70 +639,102 @@ class MT5Connector: volume: Optional[float] = None, deviation: int = 20, magic: int = 123456, + max_retries: int = 3, ) -> OrderResult: """ - Close an open position. - + Close an open position with retry logic. + Args: ticket: Position ticket volume: Volume to close (None for full close) deviation: Maximum price deviation magic: Magic number - + max_retries: Maximum retry attempts + Returns: OrderResult with execution details """ if not mt5: return OrderResult(success=False, comment="MT5 not available") - + # Get position info position = mt5.positions_get(ticket=ticket) if not position: return OrderResult(success=False, comment="Position not found") - + position = position[0] symbol = position.symbol pos_volume = volume or position.volume - - # Determine close direction - if position.type == mt5.POSITION_TYPE_BUY: - close_type = mt5.ORDER_TYPE_SELL - price = mt5.symbol_info_tick(symbol).bid - else: - close_type = mt5.ORDER_TYPE_BUY - price = mt5.symbol_info_tick(symbol).ask - - request = { - "action": mt5.TRADE_ACTION_DEAL, - "symbol": symbol, - "volume": float(pos_volume), - "type": close_type, - "position": ticket, - "price": price, - "deviation": deviation, - "magic": magic, - "comment": "AI Bot Close", - "type_time": mt5.ORDER_TIME_GTC, - "type_filling": mt5.ORDER_FILLING_IOC, - } - - result = mt5.order_send(request) - - if result and result.retcode == self.RETCODE_DONE: - logger.info(f"Position {ticket} closed") - return OrderResult( - success=True, - order_id=result.order, - retcode=result.retcode, - comment=result.comment, - price=result.price, - volume=result.volume, - ) - + + result = None + for attempt in range(max_retries): + # Re-fetch price each attempt for accuracy + tick = mt5.symbol_info_tick(symbol) + if not tick: + logger.warning(f"Close attempt {attempt + 1}: No tick data for {symbol}") + time.sleep(0.5) + continue + + if position.type == mt5.POSITION_TYPE_BUY: + close_type = mt5.ORDER_TYPE_SELL + price = tick.bid + else: + close_type = mt5.ORDER_TYPE_BUY + price = tick.ask + + request = { + "action": mt5.TRADE_ACTION_DEAL, + "symbol": symbol, + "volume": float(pos_volume), + "type": close_type, + "position": ticket, + "price": price, + "deviation": deviation, + "magic": magic, + "comment": "AI Bot Close", + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_IOC, + } + + result = mt5.order_send(request) + + if result is None: + error = mt5.last_error() + logger.warning(f"Close attempt {attempt + 1} failed (None): {error}") + time.sleep(0.5) + continue + + if result.retcode == self.RETCODE_DONE: + logger.info(f"Position {ticket} closed @ {result.price}") + return OrderResult( + success=True, + order_id=result.order, + retcode=result.retcode, + comment=result.comment, + price=result.price, + volume=result.volume, + ) + + # Non-retryable errors + if result.retcode in [ + self.RETCODE_INVALID_VOLUME, + self.RETCODE_INVALID_STOPS, + self.RETCODE_TRADE_DISABLED, + ]: + return OrderResult( + success=False, + retcode=result.retcode, + comment=result.comment, + ) + + # Retryable: requote, reject, invalid price, invalid request + logger.warning(f"Close attempt {attempt + 1} for #{ticket}: {result.retcode} - {result.comment}") + time.sleep(0.5) + return OrderResult( success=False, retcode=result.retcode if result else None, - comment=result.comment if result else "Close failed", + comment=result.comment if result else "Max retries exceeded", ) def get_open_positions(