This commit is contained in:
aaron 2026-04-06 19:17:03 +08:00
parent 582b8e1f02
commit 61ed81c9b6
6 changed files with 190 additions and 75 deletions

View File

@ -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}")

View File

@ -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}")

View File

@ -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]]:
"""

View File

@ -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
# ==================== 查询操作 ====================

View File

@ -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))

View File

@ -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 ====================