1
This commit is contained in:
parent
582b8e1f02
commit
61ed81c9b6
@ -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}")
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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]]:
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
# ==================== 查询操作 ====================
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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 ====================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user