This commit is contained in:
aaron 2026-04-25 15:36:28 +08:00
parent ef0d40dbd1
commit 8cb4ad9762
3 changed files with 60 additions and 2 deletions

View File

@ -696,12 +696,15 @@ class BaseExecutor(ABC):
success = result.get('success', False) success = result.get('success', False)
order_id = result.get('order_id', '') order_id = result.get('order_id', '')
error_msg = result.get('error', result.get('message', '')) error_msg = result.get('error', result.get('message', ''))
already_closed = result.get('already_closed', False)
if success: 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) content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "订单ID", order_id) self._append_notification_detail(content_parts, "订单ID", order_id)
if already_closed:
self._append_notification_detail(content_parts, "状态", "订单已不在挂单列表,按已完成处理")
if details and 'reason' in details: if details and 'reason' in details:
self._append_notification_detail(content_parts, "撤单原因", details['reason']) self._append_notification_detail(content_parts, "撤单原因", details['reason'])

View File

@ -581,7 +581,22 @@ class BitgetLiveTradingService:
if success: if success:
logger.info(f"✅ Bitget 单笔撤单成功: {symbol} #{order_id}") logger.info(f"✅ Bitget 单笔撤单成功: {symbol} #{order_id}")
return {"success": True, "order_id": normalized_order_id, "symbol": symbol} 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: except Exception as e:
logger.error(f"❌ Bitget 单笔撤单失败: {symbol} #{order_id} {e}") logger.error(f"❌ Bitget 单笔撤单失败: {symbol} #{order_id} {e}")
return {"success": False, "order_id": str(order_id), "error": str(e)} return {"success": False, "order_id": str(order_id), "error": str(e)}

View File

@ -250,6 +250,46 @@ class TestGetPositionForSymbol:
assert pos['coin'] == 'ETH' 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 ==================== # ==================== TestPlaceMarketOrder ====================
class TestPlaceMarketOrder: class TestPlaceMarketOrder: