diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index fb8bbff..47b2298 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -3307,10 +3307,29 @@ class CryptoAgent: tp_price=tp_price, sl_price=sl_price, ) - if tp_sl_result.get('success'): + tp_set = tp_sl_result.get('tp_set', False) + sl_set = tp_sl_result.get('sl_set', False) + + if tp_set and sl_set: logger.info(f"[Bitget] ✅ TP/SL 补设成功: {symbol} TP={tp_price} SL={sl_price}") + elif tp_set or sl_set: + # 部分成功:更新 pending 只保留缺失的 + missing_tp = tp_price if not tp_set else None + missing_sl = sl_price if not sl_set else None + if missing_tp or missing_sl: + self._bg_pending_tp_sl[order_id] = { + **info, + 'tp_price': missing_tp, + 'sl_price': missing_sl, + } + set_text = "TP" if tp_set else "SL" + fail_text = "TP" if not tp_set else "SL" + logger.warning(f"[Bitget] ⚠️ TP/SL 部分成功: {symbol} {set_text}已设, {fail_text}待下轮补设") + continue # 不删除,下轮继续 else: - logger.warning(f"[Bitget] ⚠️ TP/SL 补设失败: {tp_sl_result.get('error')}") + logger.warning(f"[Bitget] ⚠️ TP/SL 补设失败: {tp_sl_result.get('errors')}") + continue # 不删除,下轮继续重试 + del self._bg_pending_tp_sl[order_id] except Exception as e: logger.error(f"[Bitget] 检查挂单 TP/SL 补设异常: {e}") @@ -3378,10 +3397,17 @@ class CryptoAgent: sl_price=set_sl, ) - if tp_sl_result.get('success'): - logger.info(f"[Bitget] ✅ 补救成功: {symbol} {missing_desc}") + tp_set = tp_sl_result.get('tp_set', False) + sl_set = tp_sl_result.get('sl_set', False) + if tp_set or sl_set: + set_parts = [] + if tp_set: + set_parts.append(f"TP={set_tp}") + if sl_set: + set_parts.append(f"SL={set_sl}") + logger.info(f"[Bitget] ✅ 补救成功: {symbol} {' & '.join(set_parts)}") else: - logger.warning(f"[Bitget] ⚠️ 补救失败: {tp_sl_result.get('error')}") + logger.warning(f"[Bitget] ⚠️ 补救失败: {tp_sl_result.get('errors')}") except Exception as e: logger.error(f"[Bitget] 止盈止损兜底检查异常: {e}") diff --git a/backend/app/crypto_agent/executor/bitget_executor.py b/backend/app/crypto_agent/executor/bitget_executor.py index 1a070d7..fb5df76 100644 --- a/backend/app/crypto_agent/executor/bitget_executor.py +++ b/backend/app/crypto_agent/executor/bitget_executor.py @@ -68,20 +68,63 @@ class BitgetExecutor(BaseExecutor): order_status = result.get('order_status', 'filled') # 设置止盈止损 - # 策略:总是记录到 pending,由定期检查机制处理 - # 原因:Bitget 的持仓确认可能有延迟,立即设置可能失败 if stop_loss or take_profit: - # 返回给 crypto_agent 的 pending_tp_sl 格式 - # crypto_agent 会合并: {symbol, is_long, contracts, **pending_tp_sl} - result['pending_tp_sl'] = { - 'tp_price': take_profit, - 'sl_price': stop_loss - } - # 同时记录 contracts 到 result,供 crypto_agent 使用 - result['contracts'] = contracts + is_buy = (action == 'buy') - logger.info(f" 📌 已记录 TP/SL 到待处理列表: TP=${take_profit}, SL=${stop_loss}") - logger.info(f" 📌 将由定期检查机制在持仓确认后自动设置") + if order_status == 'filled': + # 市价单已成交,直接设置 TP/SL + try: + tp_sl_result = self.bitget.set_tp_sl( + symbol=symbol, + is_long=is_buy, + size=contracts, + tp_price=take_profit, + sl_price=stop_loss + ) + tp_set = tp_sl_result.get('tp_set', False) + sl_set = tp_sl_result.get('sl_set', False) + + if tp_set and sl_set: + logger.info(f" ✅ 止盈止损已设置: TP={take_profit}, SL={stop_loss}") + elif tp_set or sl_set: + # 部分成功:记录缺失侧到 pending + missing_tp = take_profit if not tp_set else None + missing_sl = stop_loss if not sl_set else None + result['pending_tp_sl'] = { + 'tp_price': missing_tp, + 'sl_price': missing_sl + } + result['contracts'] = contracts + set_text = "TP" if tp_set else "SL" + fail_text = "TP" if not tp_set else "SL" + logger.warning(f" ⚠️ 止盈止损部分成功: {set_text}已设, {fail_text}待补设") + result['tp_sl_warning'] = f"{fail_text}设置失败,已加入待补设列表" + else: + # 全部失败:记录到 pending 等待补救 + result['pending_tp_sl'] = { + 'tp_price': take_profit, + 'sl_price': stop_loss + } + result['contracts'] = contracts + errors = tp_sl_result.get('errors', []) + logger.warning(f" ⚠️ 止盈止损设置失败,已加入待补设列表: {errors}") + result['tp_sl_warning'] = f"TP/SL设置失败: {'; '.join(errors)}" + except Exception as tp_sl_err: + logger.error(f" ⚠️ 止盈止损设置异常: {tp_sl_err}") + result['pending_tp_sl'] = { + 'tp_price': take_profit, + 'sl_price': stop_loss + } + result['contracts'] = contracts + result['tp_sl_warning'] = str(tp_sl_err) + else: + # 限价单未成交,延迟到持仓确认后设置 + result['pending_tp_sl'] = { + 'tp_price': take_profit, + 'sl_price': stop_loss + } + result['contracts'] = contracts + logger.info(f" 📌 限价单待成交,TP/SL 将在成交后自动设置: TP={take_profit}, SL={stop_loss}") logger.info(f" ✅ 开仓成功: {symbol} {contracts}张 @ ${order_type}") @@ -314,7 +357,8 @@ class BitgetExecutor(BaseExecutor): logger.info(f" ✅ 移动止损成功: {symbol} → ${new_stop_loss:.2f}") return {'success': True, 'message': f'移动止损成功: {new_stop_loss:.2f}'} else: - return {'success': False, 'message': result.get('error', result.get('message', '移动止损失败'))} + errors = result.get('errors', []) + return {'success': False, 'message': '; '.join(errors) if errors else '移动止损失败'} except Exception as e: logger.error(f"Bitget 移动止损失败: {e}") diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index 51aab24..a93c365 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -425,28 +425,28 @@ class BitgetLiveTradingService: 设置止盈止损 Returns: - {"success": bool, "results": [...], "error"?: str} + {"success": bool, "tp_set": bool, "sl_set": bool, "errors": [...]} """ try: - success = self.trading_api.modify_sl_tp( + result = self.trading_api.modify_sl_tp( symbol=symbol, stop_loss=sl_price, take_profit=tp_price, ) - if success: + if result.get("success"): logger.info(f"✅ Bitget TP/SL 设置成功: {symbol} TP={tp_price} SL={sl_price}") - return { - "success": True, - "results": [ - {"type": "take_profit", "price": tp_price}, - {"type": "stop_loss", "price": sl_price}, - ] - } else: - return {"success": False, "error": "modify_sl_tp 返回 False", "results": []} + errors = result.get("errors", []) + tp_set = result.get("tp_set", False) + sl_set = result.get("sl_set", False) + if tp_set or sl_set: + logger.warning(f"⚠️ Bitget TP/SL 部分成功: {symbol} tp_set={tp_set} sl_set={sl_set} errors={errors}") + else: + logger.error(f"❌ Bitget TP/SL 设置失败: {symbol} errors={errors}") + return result except Exception as e: logger.error(f"❌ Bitget 设置 TP/SL 失败: {symbol} {e}") - return {"success": False, "error": str(e), "results": []} + return {"success": False, "tp_set": False, "sl_set": False, "errors": [str(e)]} def get_tp_sl_prices(self, symbol: str) -> Dict[str, Optional[float]]: """ diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py index e66ab88..749a97c 100644 --- a/backend/app/services/bitget_trading_api_sdk.py +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -403,7 +403,7 @@ class BitgetTradingAPI: return False def modify_sl_tp(self, symbol: str, stop_loss: float = None, - take_profit: float = None) -> bool: + take_profit: float = None) -> Dict: """ 修改持仓的止损止盈 @@ -416,8 +416,11 @@ class BitgetTradingAPI: take_profit: 止盈价格 Returns: - 是否成功 + {"success": bool, "tp_set": bool, "sl_set": bool, "errors": [...]} + success=True 仅当所有请求的都设置成功 """ + result = {"success": False, "tp_set": False, "sl_set": False, "errors": []} + try: ccxt_symbol = self._standardize_symbol(symbol) @@ -425,7 +428,8 @@ class BitgetTradingAPI: positions = self.get_position(symbol) if not positions: logger.warning(f"没有找到 {symbol} 的持仓") - return False + result["errors"].append("没有找到持仓") + return result # 查找有持仓的仓位 position = None @@ -436,7 +440,8 @@ class BitgetTradingAPI: if not position: logger.warning(f"{symbol} 持仓数量为 0") - return False + result["errors"].append("持仓数量为 0") + return result contracts = float(position.get('contracts', 0)) pos_side = position.get('side') @@ -444,21 +449,32 @@ class BitgetTradingAPI: logger.info(f"当前持仓: {symbol} {pos_side} contracts={contracts}, 标记价={mark_price}") - # 验证价格 + # 验证价格(不合理的跳过,不影响另一个) + valid_sl = stop_loss + valid_tp = take_profit + if pos_side == 'long': if stop_loss and stop_loss >= mark_price: - logger.error(f"做多止损必须低于当前价格: SL={stop_loss}, Mark={mark_price}") - return False + logger.warning(f"做多止损必须低于当前价格: SL={stop_loss}, Mark={mark_price},跳过止损") + result["errors"].append(f"止损价 {stop_loss} >= 标记价 {mark_price}") + valid_sl = None if take_profit and take_profit <= mark_price: - logger.error(f"做多止盈必须高于当前价格: TP={take_profit}, Mark={mark_price}") - return False + logger.warning(f"做多止盈必须高于当前价格: TP={take_profit}, Mark={mark_price},跳过止盈") + result["errors"].append(f"止盈价 {take_profit} <= 标记价 {mark_price}") + valid_tp = None else: if stop_loss and stop_loss <= mark_price: - logger.error(f"做空止损必须高于当前价格: SL={stop_loss}, Mark={mark_price}") - return False + logger.warning(f"做空止损必须高于当前价格: SL={stop_loss}, Mark={mark_price},跳过止损") + result["errors"].append(f"止损价 {stop_loss} <= 标记价 {mark_price}") + valid_sl = None if take_profit and take_profit >= mark_price: - logger.error(f"做空止盈必须低于当前价格: TP={take_profit}, Mark={mark_price}") - return False + logger.warning(f"做空止盈必须低于当前价格: TP={take_profit}, Mark={mark_price},跳过止盈") + result["errors"].append(f"止盈价 {take_profit} >= 标记价 {mark_price}") + valid_tp = None + + if not valid_sl and not valid_tp: + logger.warning(f"⚠️ 止盈止损价格均不合理,无法设置") + return result # 使用独立的止损/止盈计划订单 # 注意:这种方式需要在平仓时也取消这些计划订单 @@ -469,17 +485,13 @@ class BitgetTradingAPI: # 精度处理:使用动态精度 btc_amount = self._floor_amount(ccxt_symbol, btc_amount) - orders_created = [] - # 止损单 - if stop_loss: + if valid_sl: sl_side = 'sell' if pos_side == 'long' else 'buy' try: if self.use_unified_account: - # UTA 模式:用 stopLossPrice 触发策略订单路由 - # CCXT 通过 stopLossPrice 判断是否走 privateUtaPostV3TradePlaceStrategyOrder sl_params = { - 'stopLossPrice': stop_loss, + 'stopLossPrice': valid_sl, 'hedged': True, 'reduceOnly': True, 'marginCoin': 'USDT', @@ -493,7 +505,7 @@ class BitgetTradingAPI: ) else: sl_params = { - 'stopPrice': stop_loss, + 'stopPrice': valid_sl, 'triggerBy': 'mark_price', 'tdMode': 'cross', 'marginCoin': 'USDT', @@ -507,20 +519,19 @@ class BitgetTradingAPI: price=None, params=sl_params, ) - orders_created.append(('止损', sl_order)) - logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} BTC @ ${stop_loss}") + result["sl_set"] = True + logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} @ ${valid_sl}") except Exception as e: logger.warning(f"下止损单失败: {e}") + result["errors"].append(f"止损单下单失败: {e}") # 止盈单 - if take_profit: + if valid_tp: tp_side = 'sell' if pos_side == 'long' else 'buy' try: if self.use_unified_account: - # UTA 模式:用 takeProfitPrice 触发策略订单路由 - # CCXT 通过 takeProfitPrice 判断是否走 privateUtaPostV3TradePlaceStrategyOrder tp_params = { - 'takeProfitPrice': take_profit, + 'takeProfitPrice': valid_tp, 'hedged': True, 'reduceOnly': True, 'marginCoin': 'USDT', @@ -533,7 +544,6 @@ class BitgetTradingAPI: params=self._with_account_mode_params(tp_params), ) else: - # 经典账户:普通限价止盈单 tp_params = { 'tdMode': 'cross', 'marginCoin': 'USDT', @@ -544,30 +554,45 @@ class BitgetTradingAPI: type='limit', side=tp_side, amount=btc_amount, - price=take_profit, + price=valid_tp, params=tp_params, ) - orders_created.append(('止盈', tp_order)) - logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} @ ${take_profit}") + result["tp_set"] = True + logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} @ ${valid_tp}") except Exception as e: logger.warning(f"下止盈单失败: {e}") + result["errors"].append(f"止盈单下单失败: {e}") - if orders_created: + # 判断整体成功:请求设置的都成功了 + requested_sl = valid_sl is not None + requested_tp = valid_tp is not None + all_ok = (not requested_sl or result["sl_set"]) and (not requested_tp or result["tp_set"]) + result["success"] = all_ok + + if all_ok: logger.info(f"✅ 止损止盈设置完成: SL={stop_loss}, TP={take_profit}") logger.info(f" 注意: 止损止盈以独立订单形式存在,平仓时需要同时取消") - return True else: - logger.warning(f"⚠️ 未能成功下任何止损止盈单") - return False + set_parts = [] + fail_parts = [] + if requested_sl: + (set_parts if result["sl_set"] else fail_parts).append(f"SL={valid_sl}") + if requested_tp: + (set_parts if result["tp_set"] else fail_parts).append(f"TP={valid_tp}") + logger.warning(f"⚠️ 止盈止损部分成功: 成功=[{', '.join(set_parts)}] 失败=[{', '.join(fail_parts)}]") + + return result except ccxt.BaseError as e: logger.error(f"❌ 修改止损止盈失败: {e}") - return False + result["errors"].append(str(e)) + return result except Exception as e: logger.error(f"❌ 修改止损止盈异常: {e}") import traceback logger.debug(traceback.format_exc()) - return False + result["errors"].append(str(e)) + return result # ==================== 查询操作 ==================== diff --git a/backend/tests/test_bitget_live_integration.py b/backend/tests/test_bitget_live_integration.py index a73d5dd..eb1cd0c 100644 --- a/backend/tests/test_bitget_live_integration.py +++ b/backend/tests/test_bitget_live_integration.py @@ -225,6 +225,17 @@ def test_sdk_market_order_flow(api: BitgetTradingAPI, r: TestResult): """SDK: [UTA V3] 市价开仓 → 验证持仓 → 止盈止损 → 市价平仓""" opened = False try: + # 0. 先清理已有持仓和挂单,避免方向冲突 + try: + api.cancel_all_orders(TEST_SYMBOL) + except: + pass + try: + api.close_position(TEST_SYMBOL) + except: + pass + time.sleep(1) + # 1. 获取当前价格 ticker = api.exchange.fetch_ticker('BTC/USDT:USDT') price = ticker['last'] @@ -256,12 +267,19 @@ def test_sdk_market_order_flow(api: BitgetTradingAPI, r: TestResult): ) r.record("验证持仓存在", has_pos) - # 4. 设置止盈止损 + # 4. 设置止盈止损(根据实际持仓方向计算价格) + # 上面开的是做多,所以 TP > 当前价,SL < 当前价 tp_price = round(price * 1.02, 1) # +2% sl_price = round(price * 0.98, 1) # -2% try: - tp_sl_ok = api.modify_sl_tp(TEST_SYMBOL, stop_loss=sl_price, take_profit=tp_price) - r.record("UTA 止盈止损", tp_sl_ok, f"TP=${tp_price:,.1f}, SL=${sl_price:,.1f}") + tp_sl_result = api.modify_sl_tp(TEST_SYMBOL, stop_loss=sl_price, take_profit=tp_price) + tp_set = tp_sl_result.get('tp_set', False) + sl_set = tp_sl_result.get('sl_set', False) + tp_sl_ok = tp_set and sl_set + detail = f"TP=${tp_price:,.1f}({'✅' if tp_set else '❌'}), SL=${sl_price:,.1f}({'✅' if sl_set else '❌'})" + if tp_sl_result.get('errors'): + detail += f" errors={tp_sl_result['errors']}" + r.record("UTA 止盈止损", tp_sl_ok, detail) except Exception as e: r.record("UTA 止盈止损", False, str(e)) diff --git a/backend/tests/test_bitget_live_trading_service.py b/backend/tests/test_bitget_live_trading_service.py index 0099e96..ba4b6a1 100644 --- a/backend/tests/test_bitget_live_trading_service.py +++ b/backend/tests/test_bitget_live_trading_service.py @@ -367,32 +367,34 @@ class TestSetTpSl: def test_tp_and_sl_success(self): service, mock_api = make_service() - mock_api.modify_sl_tp.return_value = True + mock_api.modify_sl_tp.return_value = {"success": True, "tp_set": True, "sl_set": True, "errors": []} result = service.set_tp_sl('BTC', is_long=True, size=1, tp_price=55000.0, sl_price=47000.0) assert result['success'] is True + assert result['tp_set'] is True + assert result['sl_set'] is True mock_api.modify_sl_tp.assert_called_once_with( symbol='BTC', stop_loss=47000.0, take_profit=55000.0 ) def test_only_sl(self): service, mock_api = make_service() - mock_api.modify_sl_tp.return_value = True + mock_api.modify_sl_tp.return_value = {"success": True, "tp_set": False, "sl_set": True, "errors": []} result = service.set_tp_sl('ETH', is_long=False, size=2, tp_price=None, sl_price=3200.0) assert result['success'] is True + assert result['sl_set'] is True - def test_modify_sl_tp_returns_false(self): + def test_modify_sl_tp_returns_failure(self): service, mock_api = make_service() - mock_api.modify_sl_tp.return_value = False + mock_api.modify_sl_tp.return_value = {"success": False, "tp_set": False, "sl_set": False, "errors": ["持仓数量为 0"]} result = service.set_tp_sl('BTC', is_long=True, size=1, tp_price=55000.0, sl_price=47000.0) assert result['success'] is False - assert 'error' in result def test_api_exception(self): service, mock_api = make_service() mock_api.modify_sl_tp.side_effect = Exception("order rejected") result = service.set_tp_sl('BTC', is_long=True, size=1, tp_price=55000.0, sl_price=47000.0) assert result['success'] is False - assert 'order rejected' in result['error'] + assert 'order rejected' in result['errors'][0] # ==================== TestCancelAllOrders ====================