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 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-06 13:36:46 +07:00
co-authored by Claude Opus 4.6
parent 0d25548ed5
commit 6e0db5274b
2 changed files with 75 additions and 43 deletions
+1 -1
View File
@@ -1220,7 +1220,7 @@ class TradingBot:
regime=regime, regime=regime,
volatility=volatility, volatility=volatility,
session=session_status.get("session", "unknown"), 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 atr=0, # ATR calculated in main loop, not available here
smc_signal=signal.signal_type, smc_signal=signal.signal_type,
smc_confidence=signal.confidence, smc_confidence=signal.confidence,
+74 -42
View File
@@ -639,70 +639,102 @@ class MT5Connector:
volume: Optional[float] = None, volume: Optional[float] = None,
deviation: int = 20, deviation: int = 20,
magic: int = 123456, magic: int = 123456,
max_retries: int = 3,
) -> OrderResult: ) -> OrderResult:
""" """
Close an open position. Close an open position with retry logic.
Args: Args:
ticket: Position ticket ticket: Position ticket
volume: Volume to close (None for full close) volume: Volume to close (None for full close)
deviation: Maximum price deviation deviation: Maximum price deviation
magic: Magic number magic: Magic number
max_retries: Maximum retry attempts
Returns: Returns:
OrderResult with execution details OrderResult with execution details
""" """
if not mt5: if not mt5:
return OrderResult(success=False, comment="MT5 not available") return OrderResult(success=False, comment="MT5 not available")
# Get position info # Get position info
position = mt5.positions_get(ticket=ticket) position = mt5.positions_get(ticket=ticket)
if not position: if not position:
return OrderResult(success=False, comment="Position not found") return OrderResult(success=False, comment="Position not found")
position = position[0] position = position[0]
symbol = position.symbol symbol = position.symbol
pos_volume = volume or position.volume pos_volume = volume or position.volume
# Determine close direction result = None
if position.type == mt5.POSITION_TYPE_BUY: for attempt in range(max_retries):
close_type = mt5.ORDER_TYPE_SELL # Re-fetch price each attempt for accuracy
price = mt5.symbol_info_tick(symbol).bid tick = mt5.symbol_info_tick(symbol)
else: if not tick:
close_type = mt5.ORDER_TYPE_BUY logger.warning(f"Close attempt {attempt + 1}: No tick data for {symbol}")
price = mt5.symbol_info_tick(symbol).ask time.sleep(0.5)
continue
request = {
"action": mt5.TRADE_ACTION_DEAL, if position.type == mt5.POSITION_TYPE_BUY:
"symbol": symbol, close_type = mt5.ORDER_TYPE_SELL
"volume": float(pos_volume), price = tick.bid
"type": close_type, else:
"position": ticket, close_type = mt5.ORDER_TYPE_BUY
"price": price, price = tick.ask
"deviation": deviation,
"magic": magic, request = {
"comment": "AI Bot Close", "action": mt5.TRADE_ACTION_DEAL,
"type_time": mt5.ORDER_TIME_GTC, "symbol": symbol,
"type_filling": mt5.ORDER_FILLING_IOC, "volume": float(pos_volume),
} "type": close_type,
"position": ticket,
result = mt5.order_send(request) "price": price,
"deviation": deviation,
if result and result.retcode == self.RETCODE_DONE: "magic": magic,
logger.info(f"Position {ticket} closed") "comment": "AI Bot Close",
return OrderResult( "type_time": mt5.ORDER_TIME_GTC,
success=True, "type_filling": mt5.ORDER_FILLING_IOC,
order_id=result.order, }
retcode=result.retcode,
comment=result.comment, result = mt5.order_send(request)
price=result.price,
volume=result.volume, 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( return OrderResult(
success=False, success=False,
retcode=result.retcode if result else None, 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( def get_open_positions(