diff --git a/backend/app/crypto_agent/executor/base_executor.py b/backend/app/crypto_agent/executor/base_executor.py index d8a4016..f4f916c 100644 --- a/backend/app/crypto_agent/executor/base_executor.py +++ b/backend/app/crypto_agent/executor/base_executor.py @@ -696,12 +696,15 @@ class BaseExecutor(ABC): success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', result.get('message', '')) + already_closed = result.get('already_closed', False) if success: - title = f"✅ [{target_key or self.platform_name}] 撤单成功 - {symbol}" + title = f"✅ [{target_key or self.platform_name}] 撤单已完成 - {symbol}" if already_closed else f"✅ [{target_key or self.platform_name}] 撤单成功 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "订单ID", order_id) + if already_closed: + self._append_notification_detail(content_parts, "状态", "订单已不在挂单列表,按已完成处理") if details and 'reason' in details: self._append_notification_detail(content_parts, "撤单原因", details['reason']) diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index 5b2b891..16a8403 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -581,7 +581,22 @@ class BitgetLiveTradingService: if success: logger.info(f"✅ Bitget 单笔撤单成功: {symbol} #{order_id}") return {"success": True, "order_id": normalized_order_id, "symbol": symbol} - return {"success": False, "order_id": str(order_id), "error": "cancel_order 返回 False"} + + # 某些场景下,交易所会返回 False,但订单实际上已经成交/撤销/失效。 + # 为了避免误报,再做一次挂单列表确认;若已不在 open orders 中,视为幂等成功。 + refreshed_open_orders = self.get_open_orders(symbol) + still_exists = any(str(o.get('order_id', '')) == normalized_order_id for o in refreshed_open_orders) + if not still_exists: + logger.info(f"ℹ️ Bitget 撤单返回 False,但订单已不在挂单列表,视为完成: {symbol} #{order_id}") + return { + "success": True, + "order_id": normalized_order_id, + "symbol": symbol, + "already_closed": True, + "message": "订单撤单请求后已不在挂单列表,可能已成交、已撤销或已失效", + } + + return {"success": False, "order_id": str(order_id), "error": "cancel_order 返回 False,且订单仍存在于挂单列表"} except Exception as e: logger.error(f"❌ Bitget 单笔撤单失败: {symbol} #{order_id} {e}") return {"success": False, "order_id": str(order_id), "error": str(e)} diff --git a/backend/tests/test_bitget_live_trading_service.py b/backend/tests/test_bitget_live_trading_service.py index 68ae6af..07f46c9 100644 --- a/backend/tests/test_bitget_live_trading_service.py +++ b/backend/tests/test_bitget_live_trading_service.py @@ -250,6 +250,46 @@ class TestGetPositionForSymbol: assert pos['coin'] == 'ETH' +class TestCancelOrder: + + def test_cancel_order_returns_success_when_order_missing_before_cancel(self): + service, mock_api = make_service() + service.get_open_orders = MagicMock(return_value=[]) + + result = service.cancel_order('SOL', '123') + + assert result['success'] is True + assert result['already_closed'] is True + mock_api.cancel_order.assert_not_called() + + def test_cancel_order_returns_success_when_exchange_false_but_order_disappears(self): + service, mock_api = make_service() + service.get_open_orders = MagicMock(side_effect=[ + [{'order_id': '123', 'symbol': 'SOL'}], + [], + ]) + mock_api.cancel_order.return_value = False + + result = service.cancel_order('SOL', '123') + + assert result['success'] is True + assert result['already_closed'] is True + assert '可能已成交' in result['message'] + + def test_cancel_order_returns_failure_when_exchange_false_and_order_still_exists(self): + service, mock_api = make_service() + service.get_open_orders = MagicMock(side_effect=[ + [{'order_id': '123', 'symbol': 'SOL'}], + [{'order_id': '123', 'symbol': 'SOL'}], + ]) + mock_api.cancel_order.return_value = False + + result = service.cancel_order('SOL', '123') + + assert result['success'] is False + assert '仍存在于挂单列表' in result['error'] + + # ==================== TestPlaceMarketOrder ==================== class TestPlaceMarketOrder: