fix bug
This commit is contained in:
parent
ef4b7790cf
commit
44fc91dfc2
@ -732,6 +732,9 @@ class CryptoAgent:
|
||||
paper_decision = self.execute_signal_with_rules(
|
||||
trading_signal, 'PaperTrading', paper_account, paper_positions, paper_pending
|
||||
)
|
||||
paper_decision = self._normalize_execution_decision(
|
||||
paper_decision, paper_positions, paper_pending
|
||||
)
|
||||
# 不发送决策通知(因为是基于硬编码规则的执行,不是 LLM 决策)
|
||||
# await self._send_trading_decision_notification(
|
||||
# paper_decision, market_signal, current_price, prefix="[模拟盘]"
|
||||
@ -747,6 +750,9 @@ class CryptoAgent:
|
||||
hl_decision = self.execute_signal_with_rules(
|
||||
trading_signal, 'Hyperliquid', hl_account, hl_positions, hl_pending
|
||||
)
|
||||
hl_decision = self._normalize_execution_decision(
|
||||
hl_decision, hl_positions, hl_pending
|
||||
)
|
||||
# 不发送决策通知(因为是基于硬编码规则的执行,不是 LLM 决策)
|
||||
# await self._send_trading_decision_notification(
|
||||
# hl_decision, market_signal, current_price, prefix="[Hyperliquid]"
|
||||
@ -762,6 +768,9 @@ class CryptoAgent:
|
||||
bg_decision = self.execute_signal_with_rules(
|
||||
trading_signal, 'Bitget', bg_account, bg_positions, bg_pending
|
||||
)
|
||||
bg_decision = self._normalize_execution_decision(
|
||||
bg_decision, bg_positions, bg_pending
|
||||
)
|
||||
# 不发送决策通知(因为是基于硬编码规则的执行,不是 LLM 决策)
|
||||
# await self._send_trading_decision_notification(
|
||||
# bg_decision, market_signal, current_price, prefix="[Bitget]"
|
||||
@ -904,23 +913,28 @@ class CryptoAgent:
|
||||
if order.get('status') == 'open' and order.get('filled_price'):
|
||||
# 已成交的订单作为持仓
|
||||
position_list.append({
|
||||
'order_id': order.get('order_id'),
|
||||
'symbol': order.get('symbol'),
|
||||
'side': order.get('side'),
|
||||
'side': 'buy' if order.get('side') == 'long' else 'sell',
|
||||
'holding': order.get('quantity', 0),
|
||||
'entry_price': order.get('filled_price') or order.get('entry_price'),
|
||||
'unrealized_pnl_pct': order.get('pnl_percent', 0),
|
||||
'stop_loss': order.get('stop_loss'),
|
||||
'take_profit': order.get('take_profit')
|
||||
'take_profit': order.get('take_profit'),
|
||||
'opened_at': order.get('opened_at'),
|
||||
'created_at': order.get('created_at'),
|
||||
})
|
||||
elif order.get('status') == 'pending':
|
||||
# 未成交的订单作为挂单
|
||||
pending_orders.append({
|
||||
'order_id': order.get('order_id'),
|
||||
'symbol': order.get('symbol'),
|
||||
'side': order.get('side'),
|
||||
'side': 'buy' if order.get('side') == 'long' else 'sell',
|
||||
'entry_price': order.get('entry_price'),
|
||||
'quantity': order.get('quantity', 0),
|
||||
'entry_type': order.get('entry_type', 'market'),
|
||||
'confidence': order.get('confidence', 0)
|
||||
'confidence': order.get('confidence', 0),
|
||||
'created_at': order.get('created_at'),
|
||||
})
|
||||
|
||||
return position_list, account, pending_orders
|
||||
@ -986,11 +1000,12 @@ class CryptoAgent:
|
||||
pending_orders.append({
|
||||
'order_id': order.get('order_id'),
|
||||
'symbol': f"{order['symbol']}USDT", # 转换格式
|
||||
'side': 'buy' if order.get('side') == 'B' else 'sell',
|
||||
'side': order.get('side'),
|
||||
'entry_price': order.get('price'),
|
||||
'quantity': order.get('size'),
|
||||
'entry_type': 'limit',
|
||||
'is_reduce_only': order.get('is_reduce_only', False)
|
||||
'is_reduce_only': order.get('is_reduce_only', False),
|
||||
'created_at': order.get('created_at'),
|
||||
})
|
||||
|
||||
return position_list, account, pending_orders
|
||||
@ -999,6 +1014,105 @@ class CryptoAgent:
|
||||
logger.error(f"获取 Hyperliquid 状态失败: {e}")
|
||||
return [], {}, []
|
||||
|
||||
def _normalize_symbol(self, symbol: str) -> str:
|
||||
"""统一交易对格式为 BTCUSDT"""
|
||||
if not symbol:
|
||||
return symbol
|
||||
return symbol if symbol.endswith('USDT') else f"{symbol}USDT"
|
||||
|
||||
def _build_follow_up_open_decision(self, decision: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""为复合动作构建二段式开仓决策"""
|
||||
follow_up = dict(decision)
|
||||
follow_up.pop('next_decision', None)
|
||||
follow_up['decision'] = 'OPEN'
|
||||
follow_up['action'] = decision.get('signal_action', decision.get('action'))
|
||||
follow_up['symbol'] = self._normalize_symbol(decision.get('symbol', ''))
|
||||
return follow_up
|
||||
|
||||
def _normalize_execution_decision(self,
|
||||
decision: Dict[str, Any],
|
||||
positions: List[Dict[str, Any]],
|
||||
pending_orders: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""将复合动作归一化为执行器可落地的动作"""
|
||||
if not decision:
|
||||
return decision
|
||||
|
||||
decision_type = decision.get('decision', 'HOLD')
|
||||
if decision_type in {'OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING', 'HOLD'}:
|
||||
return decision
|
||||
|
||||
symbol = self._normalize_symbol(decision.get('symbol', ''))
|
||||
signal_action = decision.get('signal_action', decision.get('action'))
|
||||
opposite_side = 'sell' if signal_action == 'buy' else 'buy'
|
||||
|
||||
same_positions = [
|
||||
pos for pos in positions
|
||||
if self._normalize_symbol(pos.get('symbol', '')) == symbol and pos.get('side') == signal_action
|
||||
]
|
||||
opposite_positions = [
|
||||
pos for pos in positions
|
||||
if self._normalize_symbol(pos.get('symbol', '')) == symbol and pos.get('side') == opposite_side
|
||||
]
|
||||
opposite_pending = [
|
||||
order for order in pending_orders
|
||||
if self._normalize_symbol(order.get('symbol', '')) == symbol and order.get('side') == opposite_side
|
||||
]
|
||||
|
||||
def build_close(target_positions: List[Dict[str, Any]], reason: str) -> Dict[str, Any]:
|
||||
close_decision = dict(decision)
|
||||
close_decision['decision'] = 'CLOSE'
|
||||
close_decision['action'] = 'CLOSE'
|
||||
close_decision['symbol'] = symbol
|
||||
close_decision['reason'] = reason
|
||||
close_decision['reasoning'] = reason
|
||||
order_ids = [pos.get('order_id') for pos in target_positions if pos.get('order_id')]
|
||||
if order_ids:
|
||||
close_decision['orders_to_close'] = order_ids
|
||||
return close_decision
|
||||
|
||||
def build_cancel(target_orders: List[Dict[str, Any]], reason: str) -> Dict[str, Any]:
|
||||
cancel_decision = dict(decision)
|
||||
cancel_decision['decision'] = 'CANCEL_PENDING'
|
||||
cancel_decision['action'] = 'CANCEL_PENDING'
|
||||
cancel_decision['symbol'] = symbol
|
||||
cancel_decision['reason'] = reason
|
||||
cancel_decision['reasoning'] = reason
|
||||
cancel_decision['orders_to_cancel'] = [
|
||||
order.get('order_id') for order in target_orders if order.get('order_id')
|
||||
]
|
||||
return cancel_decision
|
||||
|
||||
if decision_type in {'FLIP', 'CLOSE_OPPOSITE'}:
|
||||
if opposite_positions:
|
||||
normalized = build_close(opposite_positions, decision.get('reasoning', decision.get('reason', '反向仓位先平仓')))
|
||||
normalized['next_decision'] = self._build_follow_up_open_decision(decision)
|
||||
return normalized
|
||||
if opposite_pending:
|
||||
normalized = build_cancel(opposite_pending, decision.get('reasoning', decision.get('reason', '先撤销反向挂单')))
|
||||
normalized['next_decision'] = self._build_follow_up_open_decision(decision)
|
||||
return normalized
|
||||
return self._build_follow_up_open_decision(decision)
|
||||
|
||||
if decision_type == 'CANCEL_AND_OPEN':
|
||||
if opposite_pending:
|
||||
normalized = build_cancel(opposite_pending, decision.get('reasoning', decision.get('reason', '先撤销反向挂单')))
|
||||
normalized['next_decision'] = self._build_follow_up_open_decision(decision)
|
||||
return normalized
|
||||
return self._build_follow_up_open_decision(decision)
|
||||
|
||||
if decision_type == 'ROLL':
|
||||
if same_positions:
|
||||
normalized = build_close(same_positions, decision.get('reasoning', decision.get('reason', '滚仓先平旧仓')))
|
||||
normalized['next_decision'] = self._build_follow_up_open_decision(decision)
|
||||
return normalized
|
||||
return self._build_follow_up_open_decision(decision)
|
||||
|
||||
logger.warning(f"未知复合决策类型,降级为 HOLD: {decision_type}")
|
||||
fallback = dict(decision)
|
||||
fallback['decision'] = 'HOLD'
|
||||
fallback['reasoning'] = fallback.get('reasoning', fallback.get('reason', f'未支持的决策类型: {decision_type}'))
|
||||
return fallback
|
||||
|
||||
async def _execute_decisions(self, paper_decision: Dict[str, Any],
|
||||
hyperliquid_decision: Dict[str, Any],
|
||||
bitget_decision: Dict[str, Any],
|
||||
@ -1039,6 +1153,7 @@ class CryptoAgent:
|
||||
"""执行模拟盘决策(使用执行器)"""
|
||||
try:
|
||||
decision_type = decision.get('decision', 'HOLD')
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
@ -1077,6 +1192,8 @@ class CryptoAgent:
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ 平仓成功")
|
||||
await self._send_signal_notification(market_signal, decision, current_price)
|
||||
if next_decision:
|
||||
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
error = result.get('error', '平仓失败')
|
||||
logger.error(f" ❌ 平仓失败: {error}")
|
||||
@ -1096,8 +1213,12 @@ class CryptoAgent:
|
||||
if success_count > 0:
|
||||
logger.info(f" ✅ 成功取消 {success_count} 个挂单")
|
||||
await self._send_signal_notification(market_signal, decision, current_price)
|
||||
if next_decision:
|
||||
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
logger.warning(f" ⚠️ 没有成功取消任何挂单")
|
||||
else:
|
||||
logger.warning(f" ⚠️ 模拟盘暂不支持的执行动作: {decision_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 模拟盘执行异常: {e}")
|
||||
@ -2128,6 +2249,7 @@ class CryptoAgent:
|
||||
'quantity': order.get('size'),
|
||||
'entry_type': 'limit',
|
||||
'is_reduce_only': order.get('is_reduce_only', False),
|
||||
'created_at': order.get('created_at'),
|
||||
})
|
||||
|
||||
return position_list, account, pending_orders
|
||||
@ -2474,7 +2596,14 @@ class CryptoAgent:
|
||||
"decision": final_action,
|
||||
"action": final_action,
|
||||
"reason": final_reason,
|
||||
"reasoning": final_reason
|
||||
"reasoning": final_reason,
|
||||
"signal_action": signal.get('action'),
|
||||
"symbol": signal.get('symbol'),
|
||||
"entry_price": signal.get('entry_price'),
|
||||
"stop_loss": signal.get('stop_loss'),
|
||||
"take_profit": signal.get('take_profit'),
|
||||
"confidence": signal.get('confidence', 0),
|
||||
"grade": signal.get('grade', 'C'),
|
||||
}
|
||||
|
||||
async def _execute_bitget_decisions(self, decision: Dict[str, Any],
|
||||
@ -2484,6 +2613,7 @@ class CryptoAgent:
|
||||
try:
|
||||
decision_type = decision.get('decision', 'HOLD')
|
||||
symbol = decision.get('symbol', 'UNKNOWN')
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
@ -2541,6 +2671,8 @@ class CryptoAgent:
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ Bitget 平仓成功")
|
||||
await self._send_signal_notification(market_signal, decision, current_price, prefix="[Bitget]")
|
||||
if next_decision:
|
||||
await self._execute_bitget_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
error = result.get('error', '未知错误')
|
||||
logger.error(f" ❌ Bitget 平仓失败: {error}")
|
||||
@ -2562,10 +2694,14 @@ class CryptoAgent:
|
||||
|
||||
if success_count > 0:
|
||||
logger.info(f" ✅ Bitget 取消成功: {success_count} 个挂单")
|
||||
if next_decision:
|
||||
await self._execute_bitget_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
error = "没有成功取消任何挂单"
|
||||
logger.error(f" ❌ Bitget 取消失败: {error}")
|
||||
await self._notify_bitget_error(symbol, "取消挂单", error)
|
||||
else:
|
||||
logger.warning(f" ⚠️ Bitget 暂不支持的执行动作: {decision_type}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Bitget 执行异常: {e}")
|
||||
@ -2816,6 +2952,7 @@ class CryptoAgent:
|
||||
try:
|
||||
decision_type = decision.get('decision', 'HOLD')
|
||||
symbol = decision.get('symbol', 'UNKNOWN')
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
@ -2854,6 +2991,8 @@ class CryptoAgent:
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ Hyperliquid 平仓成功")
|
||||
await self._send_signal_notification(market_signal, decision, current_price, prefix="[Hyperliquid]")
|
||||
if next_decision:
|
||||
await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
error = result.get('error', '未知错误')
|
||||
logger.error(f" ❌ Hyperliquid 平仓失败: {error}")
|
||||
@ -2868,12 +3007,16 @@ class CryptoAgent:
|
||||
result = await executor.execute_cancel(order_id, symbol)
|
||||
if result.get('success'):
|
||||
success_count += 1
|
||||
if success_count > 1:
|
||||
if success_count > 0:
|
||||
logger.info(f" ✅ Hyperliquid 取消成功: {success_count} 个")
|
||||
if next_decision:
|
||||
await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price)
|
||||
else:
|
||||
error = "没有成功取消任何挂单"
|
||||
logger.error(f" ❌ Hyperliquid 取消失败: {error}")
|
||||
await self._notify_hyperliquid_error(symbol, "取消挂单", error)
|
||||
else:
|
||||
logger.warning(f" ⚠️ Hyperliquid 暂不支持的执行动作: {decision_type}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Hyperliquid 执行异常: {e}")
|
||||
await self._notify_hyperliquid_error(symbol, decision_type, str(e))
|
||||
@ -3558,7 +3701,7 @@ class CryptoAgent:
|
||||
platforms_to_check.append(('模拟盘', self.paper_trading))
|
||||
|
||||
# 添加 Bitget 实盘
|
||||
if self.bitget and self.settings.bitget_use_testnet:
|
||||
if self.bitget:
|
||||
platforms_to_check.append(('Bitget', self.bitget))
|
||||
|
||||
# 添加 Hyperliquid 实盘
|
||||
@ -3570,6 +3713,8 @@ class CryptoAgent:
|
||||
# 获取账户状态
|
||||
if hasattr(platform_service, 'get_account_state'):
|
||||
account_state = platform_service.get_account_state()
|
||||
elif hasattr(platform_service, 'get_account_status'):
|
||||
account_state = platform_service.get_account_status()
|
||||
elif hasattr(platform_service, 'get_balance'):
|
||||
account_state = platform_service.get_balance()
|
||||
else:
|
||||
@ -3579,8 +3724,9 @@ class CryptoAgent:
|
||||
# 获取当前余额(统一字段名)
|
||||
current_balance = (
|
||||
account_state.get('current_balance') or
|
||||
account_state.get('account_value') or
|
||||
account_state.get('balance') or
|
||||
account_state.get('available_balance', 0)
|
||||
0
|
||||
)
|
||||
|
||||
if current_balance <= 0:
|
||||
@ -3665,6 +3811,21 @@ class CryptoAgent:
|
||||
|
||||
logger.info(f"[{platform_name}] 需要平仓 {len(positions)} 个持仓")
|
||||
|
||||
if hasattr(platform_service, 'market_close_all'):
|
||||
result = platform_service.market_close_all()
|
||||
success_items = result.get('results') or result.get('result') or []
|
||||
closed_count = sum(1 for item in success_items if item.get('success', True))
|
||||
|
||||
await self._send_alert_notification(
|
||||
f"🚨 [{platform_name}] 紧急平仓完成",
|
||||
f"触发原因: 账户回撤超过 {self.settings.account_max_drawdown*100:.0f}%\n"
|
||||
f"平仓数量: {closed_count}/{len(positions)}\n\n"
|
||||
f"⚠️ 交易系统已停止,请人工检查账户!"
|
||||
)
|
||||
|
||||
logger.info(f"🚨 [{platform_name}] 紧急平仓完成: {closed_count}/{len(positions)}")
|
||||
return
|
||||
|
||||
# 逐个平仓
|
||||
closed_count = 0
|
||||
for pos in positions:
|
||||
@ -3753,7 +3914,7 @@ class CryptoAgent:
|
||||
# 达到目标盈利,平仓
|
||||
decision = {
|
||||
'decision': 'CLOSE',
|
||||
'symbol': symbol + 'USDT',
|
||||
'symbol': self._normalize_symbol(symbol),
|
||||
'reason': reason
|
||||
}
|
||||
result = await executor.execute_close(decision, current_prices.get(symbol, 0))
|
||||
@ -3768,7 +3929,7 @@ class CryptoAgent:
|
||||
# 持仓超时,平仓
|
||||
decision = {
|
||||
'decision': 'CLOSE',
|
||||
'symbol': symbol + 'USDT',
|
||||
'symbol': self._normalize_symbol(symbol),
|
||||
'reason': reason
|
||||
}
|
||||
result = await executor.execute_close(decision, current_prices.get(symbol, 0))
|
||||
@ -3874,7 +4035,7 @@ class CryptoAgent:
|
||||
try:
|
||||
# 飞书
|
||||
if self.feishu:
|
||||
await self.feishu.send_message(f"{title}\n\n{message}")
|
||||
await self.feishu.send_text(f"{title}\n\n{message}")
|
||||
|
||||
# 钉钉
|
||||
if self.dingtalk:
|
||||
@ -3895,4 +4056,3 @@ def get_crypto_agent() -> 'CryptoAgent':
|
||||
"""获取加密货币智能体单例"""
|
||||
# 直接使用类单例,不使用全局变量(避免 reload 时重置)
|
||||
return CryptoAgent()
|
||||
|
||||
|
||||
@ -120,37 +120,24 @@ class BitgetExecutor(BaseExecutor):
|
||||
symbol = decision.get('symbol', '').replace('USDT', '')
|
||||
orders_to_close = decision.get('orders_to_close', [])
|
||||
|
||||
if not orders_to_close:
|
||||
# 平掉所有持仓
|
||||
result = self.bitget.market_close_all()
|
||||
logger.info(f" ✅ 平仓所有持仓")
|
||||
# Bitget 持仓是按 symbol 聚合管理,不能按 order_id 精确平仓。
|
||||
result = self.bitget.market_close_position(symbol)
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ 平仓成功: {symbol}")
|
||||
else:
|
||||
logger.warning(f" ⚠️ 平仓失败: {symbol} - {result.get('error', '未知错误')}")
|
||||
|
||||
# 发送飞书通知
|
||||
await self.send_execution_notification(
|
||||
operation='CLOSE',
|
||||
symbol=symbol,
|
||||
result=result
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# 平掉指定订单
|
||||
results = []
|
||||
for order_id in orders_to_close:
|
||||
# 这里需要根据 order_id 找到对应的持仓
|
||||
# Bitget 的持仓管理需要优化
|
||||
results.append({'order_id': order_id, 'success': True})
|
||||
|
||||
success_result = {'success': True, 'results': results}
|
||||
if orders_to_close:
|
||||
result['requested_order_ids'] = orders_to_close
|
||||
|
||||
# 发送飞书通知
|
||||
await self.send_execution_notification(
|
||||
operation='CLOSE',
|
||||
symbol=symbol,
|
||||
result=success_result
|
||||
result=result
|
||||
)
|
||||
|
||||
return success_result
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bitget 平仓失败: {e}")
|
||||
@ -169,7 +156,8 @@ class BitgetExecutor(BaseExecutor):
|
||||
"""执行撤单"""
|
||||
try:
|
||||
result = self.bitget.cancel_order(symbol.replace('USDT', ''), order_id)
|
||||
logger.info(f" ✅ 撤单成功: {order_id}")
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ 撤单成功: {order_id}")
|
||||
|
||||
# 发送飞书通知
|
||||
await self.send_execution_notification(
|
||||
@ -306,17 +294,24 @@ class BitgetExecutor(BaseExecutor):
|
||||
{'success': bool, 'message': str}
|
||||
"""
|
||||
try:
|
||||
# Bitget 使用 modify_sl_tp 方法
|
||||
success = self.bitget.modify_sl_tp(
|
||||
position = self.bitget.get_position_for_symbol(symbol)
|
||||
if not position:
|
||||
return {'success': False, 'message': f'找不到 {symbol} 的持仓'}
|
||||
|
||||
tp_sl_prices = self.bitget.get_tp_sl_prices(symbol.replace('USDT', ''))
|
||||
result = self.bitget.set_tp_sl(
|
||||
symbol=symbol.replace('USDT', ''),
|
||||
stop_loss=new_stop_loss
|
||||
is_long=position['size'] > 0,
|
||||
size=abs(position['size']),
|
||||
tp_price=tp_sl_prices.get('take_profit'),
|
||||
sl_price=new_stop_loss
|
||||
)
|
||||
|
||||
if success:
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ 移动止损成功: {symbol} → ${new_stop_loss:.2f}")
|
||||
return {'success': True, 'message': f'移动止损成功: {new_stop_loss:.2f}'}
|
||||
else:
|
||||
return {'success': False, 'message': '移动止损失败'}
|
||||
return {'success': False, 'message': result.get('error', result.get('message', '移动止损失败'))}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bitget 移动止损失败: {e}")
|
||||
|
||||
@ -129,39 +129,23 @@ class HyperliquidExecutor(BaseExecutor):
|
||||
symbol = decision.get('symbol', '').replace('USDT', '')
|
||||
orders_to_close = decision.get('orders_to_close', [])
|
||||
|
||||
if not orders_to_close:
|
||||
# 平掉所有持仓
|
||||
result = self.hyperliquid.market_close_all()
|
||||
logger.info(f" ✅ 平仓所有持仓")
|
||||
result = self.hyperliquid.market_close_position(symbol)
|
||||
if result.get('success'):
|
||||
logger.info(f" ✅ 平仓成功: {symbol}")
|
||||
else:
|
||||
logger.warning(f" ⚠️ 平仓失败: {symbol} - {result.get('error', '未知错误')}")
|
||||
|
||||
# 发送飞书通知
|
||||
await self.send_execution_notification(
|
||||
operation='CLOSE',
|
||||
symbol=symbol,
|
||||
result=result
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# 平掉指定订单
|
||||
results = []
|
||||
for order_id in orders_to_close:
|
||||
close_result = self.hyperliquid.close_position(symbol, order_id)
|
||||
results.append({
|
||||
'order_id': order_id,
|
||||
'success': close_result.get('success', False)
|
||||
})
|
||||
|
||||
success_result = {'success': True, 'results': results}
|
||||
if orders_to_close:
|
||||
result['requested_order_ids'] = orders_to_close
|
||||
|
||||
# 发送飞书通知
|
||||
await self.send_execution_notification(
|
||||
operation='CLOSE',
|
||||
symbol=symbol,
|
||||
result=success_result
|
||||
result=result
|
||||
)
|
||||
|
||||
return success_result
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Hyperliquid 平仓失败: {e}")
|
||||
@ -320,9 +304,16 @@ class HyperliquidExecutor(BaseExecutor):
|
||||
{'success': bool, 'message': str}
|
||||
"""
|
||||
try:
|
||||
# Hyperliquid 使用 set_tp_sl 方法(只传 sl_price)
|
||||
position = self.hyperliquid.get_position_for_symbol(symbol)
|
||||
if not position:
|
||||
return {'success': False, 'message': f'找不到 {symbol} 的持仓'}
|
||||
|
||||
tp_sl_prices = self.hyperliquid.get_tp_sl_prices(symbol.replace('USDT', ''))
|
||||
result = self.hyperliquid.set_tp_sl(
|
||||
symbol=symbol.replace('USDT', ''),
|
||||
is_long=position['size'] > 0,
|
||||
size=abs(position['size']),
|
||||
tp_price=tp_sl_prices.get('take_profit'),
|
||||
sl_price=new_stop_loss
|
||||
)
|
||||
|
||||
@ -330,7 +321,7 @@ class HyperliquidExecutor(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('message', '移动止损失败')}
|
||||
return {'success': False, 'message': result.get('error', result.get('message', '移动止损失败'))}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Hyperliquid 移动止损失败: {e}")
|
||||
|
||||
@ -5,6 +5,7 @@ Bitget 实盘交易服务
|
||||
供 crypto_agent.py 的决策执行层使用。
|
||||
"""
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from app.config import get_settings
|
||||
@ -91,6 +92,7 @@ class BitgetLiveTradingService:
|
||||
|
||||
return {
|
||||
"account_value": account_value,
|
||||
"current_balance": account_value,
|
||||
"total_margin_used": frozen,
|
||||
"available_balance": available,
|
||||
}
|
||||
@ -170,14 +172,25 @@ class BitgetLiveTradingService:
|
||||
|
||||
side = pos.get('side', 'long')
|
||||
size = coin_amount if side == 'long' else -coin_amount
|
||||
tp_sl_prices = self.get_tp_sl_prices(coin)
|
||||
opened_at_raw = info.get('cTime') or pos.get('timestamp')
|
||||
if isinstance(opened_at_raw, (int, float)):
|
||||
opened_at = datetime.fromtimestamp(opened_at_raw / 1000).isoformat()
|
||||
else:
|
||||
opened_at = pos.get('datetime') or opened_at_raw
|
||||
|
||||
result.append({
|
||||
"coin": coin,
|
||||
"symbol": f"{coin}USDT",
|
||||
"side": "buy" if size > 0 else "sell",
|
||||
"size": size,
|
||||
"entry_price": float(pos.get('entryPrice', 0) or 0),
|
||||
"unrealized_pnl": float(pos.get('unrealizedPnl', 0) or 0),
|
||||
"leverage": int(float(pos.get('leverage', 1) or 1)),
|
||||
"liquidation_price": float(pos.get('liquidationPrice', 0) or 0) or None,
|
||||
"stop_loss": tp_sl_prices.get('stop_loss'),
|
||||
"take_profit": tp_sl_prices.get('take_profit'),
|
||||
"opened_at": opened_at,
|
||||
"position": pos,
|
||||
})
|
||||
return result
|
||||
@ -409,6 +422,11 @@ class BitgetLiveTradingService:
|
||||
contracts = float(order.get('amount', 0) or 0)
|
||||
contract_size = self.get_contract_size(coin)
|
||||
size_in_coins = contracts # ccxt amount 已是币数量
|
||||
created_at_raw = order.get('timestamp')
|
||||
if isinstance(created_at_raw, (int, float)):
|
||||
created_at = datetime.fromtimestamp(created_at_raw / 1000).isoformat()
|
||||
else:
|
||||
created_at = order.get('datetime') or created_at_raw
|
||||
|
||||
result.append({
|
||||
"order_id": str(order.get('id', '')),
|
||||
@ -418,9 +436,27 @@ class BitgetLiveTradingService:
|
||||
"price": float(order.get('price', 0) or 0),
|
||||
"is_reduce_only": bool(order.get('reduceOnly', False)),
|
||||
"order_type": order.get('type', ''),
|
||||
"created_at": created_at,
|
||||
})
|
||||
return result
|
||||
|
||||
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
撤销单个挂单
|
||||
|
||||
Returns:
|
||||
{"success": bool, "order_id": str, "error"?: str}
|
||||
"""
|
||||
try:
|
||||
success = self.trading_api.cancel_order(symbol=symbol, order_id=order_id)
|
||||
if success:
|
||||
logger.info(f"✅ Bitget 单笔撤单成功: {symbol} #{order_id}")
|
||||
return {"success": True, "order_id": str(order_id), "symbol": symbol}
|
||||
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)}
|
||||
|
||||
def cancel_all_orders(self, symbol: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
撤销指定币种的所有挂单
|
||||
@ -493,47 +529,57 @@ class BitgetLiveTradingService:
|
||||
|
||||
def market_close_all(self) -> Dict[str, Any]:
|
||||
"""市价平仓所有持仓"""
|
||||
import math
|
||||
results = []
|
||||
positions = self.get_open_positions()
|
||||
for pos in positions:
|
||||
coin = pos['coin']
|
||||
is_long = pos['size'] > 0
|
||||
coin_amount = abs(pos['size'])
|
||||
|
||||
# 精度处理:向下取整到 0.0001(Bitget 最小精度)
|
||||
coin_amount = math.floor(coin_amount * 10000) / 10000
|
||||
|
||||
if coin_amount < 0.0001:
|
||||
logger.warning(f"{coin} 持仓过小 ({coin_amount}),跳过")
|
||||
continue
|
||||
|
||||
# 直接使用币数量平仓,不经过合约转换
|
||||
try:
|
||||
ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT')
|
||||
side = 'sell' if is_long else 'buy'
|
||||
order = self.trading_api.exchange.create_market_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=coin_amount,
|
||||
params={
|
||||
'reduceOnly': True,
|
||||
'tdMode': 'cross',
|
||||
'marginCoin': 'USDT',
|
||||
}
|
||||
)
|
||||
if order:
|
||||
logger.info(f"✅ Bitget 平仓成功: {coin} {side} {coin_amount}")
|
||||
results.append({"success": True, "coin": coin, "size": coin_amount})
|
||||
else:
|
||||
results.append({"success": False, "coin": coin, "error": "返回空"})
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Bitget 平仓失败: {coin} {e}")
|
||||
results.append({"success": False, "coin": coin, "error": str(e)})
|
||||
results.append(self.market_close_position(pos['coin']))
|
||||
|
||||
all_ok = all(r.get('success') for r in results)
|
||||
return {"success": all_ok, "results": results}
|
||||
|
||||
def market_close_position(self, symbol: str) -> Dict[str, Any]:
|
||||
"""按交易对市价平仓单个持仓"""
|
||||
import math
|
||||
|
||||
coin = symbol.replace('USDT', '').replace('/', '').upper()
|
||||
position = self.get_position_for_symbol(coin)
|
||||
if not position:
|
||||
return {"success": False, "coin": coin, "error": "未找到持仓"}
|
||||
|
||||
is_long = position['size'] > 0
|
||||
coin_amount = math.floor(abs(position['size']) * 10000) / 10000
|
||||
if coin_amount < 0.0001:
|
||||
logger.warning(f"{coin} 持仓过小 ({coin_amount}),跳过")
|
||||
return {"success": True, "coin": coin, "size": 0}
|
||||
|
||||
self.cancel_tp_sl_orders(coin)
|
||||
|
||||
try:
|
||||
ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT')
|
||||
side = 'sell' if is_long else 'buy'
|
||||
order = self.trading_api.exchange.create_market_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=coin_amount,
|
||||
params={
|
||||
'reduceOnly': True,
|
||||
'tdMode': 'cross',
|
||||
'marginCoin': 'USDT',
|
||||
}
|
||||
)
|
||||
if order:
|
||||
logger.info(f"✅ Bitget 单币种平仓成功: {coin} {side} {coin_amount}")
|
||||
return {
|
||||
"success": True,
|
||||
"coin": coin,
|
||||
"size": coin_amount,
|
||||
"order_id": str(order.get('id', '')),
|
||||
}
|
||||
return {"success": False, "coin": coin, "error": "返回空"}
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Bitget 单币种平仓失败: {coin} {e}")
|
||||
return {"success": False, "coin": coin, "error": str(e)}
|
||||
|
||||
|
||||
# ==================== 单例工厂 ====================
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ class HyperliquidTradingService:
|
||||
|
||||
return {
|
||||
"account_value": account_value,
|
||||
"current_balance": account_value,
|
||||
"total_margin_used": total_margin_used,
|
||||
"available_balance": account_value - total_margin_used,
|
||||
"positions": state.get("assetPositions", []),
|
||||
@ -324,7 +325,9 @@ class HyperliquidTradingService:
|
||||
"order_type": order.get("orderType", {}),
|
||||
"timestamp": order.get("timestamp"),
|
||||
"original_size": float(order.get("origSz", 0)),
|
||||
"raw_side": side
|
||||
"raw_side": side,
|
||||
"created_at": datetime.fromtimestamp(order.get("timestamp", 0) / 1000).isoformat()
|
||||
if order.get("timestamp") else None,
|
||||
})
|
||||
|
||||
return orders
|
||||
@ -476,6 +479,8 @@ class HyperliquidTradingService:
|
||||
def cancel_order(self, symbol: str, order_id: int) -> Dict[str, Any]:
|
||||
"""取消订单"""
|
||||
try:
|
||||
if isinstance(order_id, str):
|
||||
order_id = int(order_id)
|
||||
result = self.exchange.cancel(symbol, order_id)
|
||||
logger.info(f"取消订单: {symbol} #{order_id}")
|
||||
return {"success": True, "result": result}
|
||||
@ -515,29 +520,46 @@ class HyperliquidTradingService:
|
||||
for pos in positions:
|
||||
position_data = pos.get("position", {})
|
||||
coin = position_data.get("coin")
|
||||
size = float(position_data.get("szi", 0))
|
||||
|
||||
if size == 0:
|
||||
if not coin:
|
||||
continue
|
||||
results.append(self.market_close_position(coin))
|
||||
|
||||
# 取消该币种的所有挂单(包括止盈止损)
|
||||
self.cancel_all_orders(coin)
|
||||
|
||||
is_long = size > 0
|
||||
result = self.place_market_order(
|
||||
symbol=coin,
|
||||
is_buy=not is_long, # 平多头=卖出,平空头=买入
|
||||
size=abs(size),
|
||||
reduce_only=True
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
all_ok = all(result.get("success") for result in results)
|
||||
logger.info(f"🚨 紧急平仓完成,共平仓 {len(results)} 个持仓")
|
||||
return {"success": True, "closed_positions": len(results), "results": results}
|
||||
return {"success": all_ok, "closed_positions": len(results), "results": results}
|
||||
except Exception as e:
|
||||
logger.error(f"紧急平仓失败: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def market_close_position(self, symbol: str) -> Dict[str, Any]:
|
||||
"""按交易对市价平仓单个持仓"""
|
||||
position = self.get_position_for_symbol(symbol)
|
||||
if not position:
|
||||
return {"success": False, "symbol": symbol, "error": "未找到持仓"}
|
||||
|
||||
coin = position.get("coin", symbol.replace('USDT', '').replace('/', '').upper())
|
||||
size = abs(float(position.get("size", 0)))
|
||||
if size <= 0:
|
||||
return {"success": True, "symbol": coin, "size": 0}
|
||||
|
||||
self.cancel_all_orders(coin)
|
||||
|
||||
is_long = position.get("size", 0) > 0
|
||||
result = self.place_market_order(
|
||||
symbol=coin,
|
||||
is_buy=not is_long,
|
||||
size=size,
|
||||
reduce_only=True
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
result["symbol"] = coin
|
||||
return result
|
||||
|
||||
def close_position(self, symbol: str, order_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""兼容旧调用:按交易对平仓,忽略 order_id"""
|
||||
return self.market_close_position(symbol)
|
||||
|
||||
def get_open_positions(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有持仓"""
|
||||
try:
|
||||
@ -552,13 +574,21 @@ class HyperliquidTradingService:
|
||||
if size == 0:
|
||||
continue
|
||||
|
||||
tp_sl_prices = self.get_tp_sl_prices(coin)
|
||||
|
||||
positions.append({
|
||||
"coin": coin,
|
||||
"symbol": f"{coin}USDT",
|
||||
"side": "buy" if size > 0 else "sell",
|
||||
"size": size, # 正数=多头,负数=空头
|
||||
"entry_price": float(position_data.get("entryPx", 0)),
|
||||
"unrealized_pnl": float(position_data.get("unrealizedPnl", 0)),
|
||||
"leverage": position_data.get("leverage", {}).get("value"),
|
||||
"liquidation_price": position_data.get("liquidationPx"),
|
||||
"stop_loss": tp_sl_prices.get("stop_loss"),
|
||||
"take_profit": tp_sl_prices.get("take_profit"),
|
||||
"opened_at": datetime.fromtimestamp(position_data.get("timestamp", 0) / 1000).isoformat()
|
||||
if position_data.get("timestamp") else None,
|
||||
"position": position_data # 保留原始数据
|
||||
})
|
||||
|
||||
@ -569,9 +599,10 @@ class HyperliquidTradingService:
|
||||
|
||||
def get_position_for_symbol(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取指定币种的持仓"""
|
||||
normalized_symbol = symbol.replace('USDT', '').replace('/', '').upper()
|
||||
positions = self.get_open_positions()
|
||||
for pos in positions:
|
||||
if pos["coin"] == symbol:
|
||||
if pos["coin"] == normalized_symbol:
|
||||
return pos
|
||||
return None
|
||||
|
||||
|
||||
@ -1604,6 +1604,38 @@ class PaperTradingService:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""兼容执行层接口:返回未成交挂单列表"""
|
||||
orders = self.get_active_orders(symbol)
|
||||
return [order for order in orders if order.get('status') == OrderStatus.PENDING.value]
|
||||
|
||||
def get_open_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""兼容执行层接口:返回已成交持仓列表"""
|
||||
orders = self.get_active_orders(symbol)
|
||||
positions = []
|
||||
|
||||
for order in orders:
|
||||
if order.get('status') != OrderStatus.OPEN.value:
|
||||
continue
|
||||
|
||||
side = order.get('side')
|
||||
positions.append({
|
||||
'order_id': order.get('order_id'),
|
||||
'symbol': order.get('symbol'),
|
||||
'side': 'buy' if side == 'long' else 'sell',
|
||||
'entry_price': order.get('filled_price') or order.get('entry_price') or 0,
|
||||
'filled_price': order.get('filled_price'),
|
||||
'quantity': order.get('quantity', 0),
|
||||
'margin': order.get('margin', 0),
|
||||
'stop_loss': order.get('stop_loss'),
|
||||
'take_profit': order.get('take_profit'),
|
||||
'unrealized_pnl_pct': order.get('pnl_percent', 0),
|
||||
'opened_at': order.get('opened_at'),
|
||||
'created_at': order.get('created_at'),
|
||||
})
|
||||
|
||||
return positions
|
||||
|
||||
def get_order_by_id(self, order_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取订单"""
|
||||
# 先从缓存查找
|
||||
@ -1849,6 +1881,10 @@ class PaperTradingService:
|
||||
'total_pnl_percent': round((realized_pnl / self.initial_balance * 100), 2) if self.initial_balance > 0 else 0
|
||||
}
|
||||
|
||||
def get_account_state(self) -> Dict[str, Any]:
|
||||
"""兼容执行层接口:账户状态别名"""
|
||||
return self.get_account_status()
|
||||
|
||||
def _calculate_grade_statistics(self, orders: List[PaperOrder]) -> Dict[str, Any]:
|
||||
"""按信号等级统计"""
|
||||
result = {}
|
||||
|
||||
158
backend/tests/test_execution_safety_fixes.py
Normal file
158
backend/tests/test_execution_safety_fixes.py
Normal file
@ -0,0 +1,158 @@
|
||||
"""
|
||||
执行链安全修复回归测试
|
||||
|
||||
覆盖重点:
|
||||
- Bitget 单币种平仓,不再误触发全仓平仓
|
||||
- Bitget 单笔撤单接口可用
|
||||
- Bitget 移动止损走 set_tp_sl,而不是不存在的方法
|
||||
"""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
|
||||
def make_bitget_service():
|
||||
from app.services.bitget_live_trading_service import BitgetLiveTradingService
|
||||
|
||||
mock_api = MagicMock()
|
||||
mock_exchange = MagicMock()
|
||||
mock_api.exchange = mock_exchange
|
||||
mock_api._standardize_symbol = lambda s: f"{s.replace('USDT', '')}/USDT:USDT"
|
||||
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bitget_max_total_leverage = 10.0
|
||||
mock_settings.bitget_max_single_position = 1000.0
|
||||
mock_settings.hyperliquid_circuit_breaker_drawdown = 0.10
|
||||
|
||||
service = BitgetLiveTradingService.__new__(BitgetLiveTradingService)
|
||||
service.settings = mock_settings
|
||||
service.max_total_leverage = mock_settings.bitget_max_total_leverage
|
||||
service.max_single_position = mock_settings.bitget_max_single_position
|
||||
service.circuit_breaker_drawdown = mock_settings.hyperliquid_circuit_breaker_drawdown
|
||||
service.trading_api = mock_api
|
||||
service.initial_balance = 10000.0
|
||||
|
||||
return service, mock_api
|
||||
|
||||
|
||||
def load_bitget_executor_class():
|
||||
"""按文件加载执行器,避免触发 app.crypto_agent.__init__ 的重依赖"""
|
||||
executor_dir = Path(__file__).resolve().parents[1] / 'app' / 'crypto_agent' / 'executor'
|
||||
|
||||
if 'app.crypto_agent' not in sys.modules:
|
||||
crypto_pkg = types.ModuleType('app.crypto_agent')
|
||||
crypto_pkg.__path__ = [str(executor_dir.parent)]
|
||||
sys.modules['app.crypto_agent'] = crypto_pkg
|
||||
|
||||
if 'app.crypto_agent.executor' not in sys.modules:
|
||||
executor_pkg = types.ModuleType('app.crypto_agent.executor')
|
||||
executor_pkg.__path__ = [str(executor_dir)]
|
||||
sys.modules['app.crypto_agent.executor'] = executor_pkg
|
||||
|
||||
if 'app.crypto_agent.executor.base_executor' not in sys.modules:
|
||||
base_spec = importlib.util.spec_from_file_location(
|
||||
'app.crypto_agent.executor.base_executor',
|
||||
executor_dir / 'base_executor.py',
|
||||
)
|
||||
base_module = importlib.util.module_from_spec(base_spec)
|
||||
sys.modules[base_spec.name] = base_module
|
||||
base_spec.loader.exec_module(base_module)
|
||||
|
||||
if 'app.crypto_agent.executor.bitget_executor' not in sys.modules:
|
||||
executor_spec = importlib.util.spec_from_file_location(
|
||||
'app.crypto_agent.executor.bitget_executor',
|
||||
executor_dir / 'bitget_executor.py',
|
||||
)
|
||||
executor_module = importlib.util.module_from_spec(executor_spec)
|
||||
sys.modules[executor_spec.name] = executor_module
|
||||
executor_spec.loader.exec_module(executor_module)
|
||||
|
||||
return sys.modules['app.crypto_agent.executor.bitget_executor'].BitgetExecutor
|
||||
|
||||
|
||||
def test_bitget_market_close_position_only_closes_requested_symbol():
|
||||
service, mock_api = make_bitget_service()
|
||||
mock_api.get_position.return_value = [
|
||||
{
|
||||
'symbol': 'BTC/USDT:USDT',
|
||||
'side': 'long',
|
||||
'entryPrice': 50000.0,
|
||||
'unrealizedPnl': 100.0,
|
||||
'leverage': 5,
|
||||
'liquidationPrice': 45000.0,
|
||||
'info': {'available': '0.02'},
|
||||
},
|
||||
{
|
||||
'symbol': 'ETH/USDT:USDT',
|
||||
'side': 'long',
|
||||
'entryPrice': 3000.0,
|
||||
'unrealizedPnl': 50.0,
|
||||
'leverage': 5,
|
||||
'liquidationPrice': 2500.0,
|
||||
'info': {'available': '0.5'},
|
||||
},
|
||||
]
|
||||
mock_api.get_open_orders.return_value = []
|
||||
mock_api.cancel_all_orders.return_value = True
|
||||
mock_api.exchange.create_market_order.return_value = {'id': 'close-btc'}
|
||||
|
||||
result = service.market_close_position('BTCUSDT')
|
||||
|
||||
assert result['success'] is True
|
||||
assert result['coin'] == 'BTC'
|
||||
kwargs = mock_api.exchange.create_market_order.call_args.kwargs
|
||||
assert kwargs['symbol'] == 'BTC/USDT:USDT'
|
||||
assert kwargs['side'] == 'sell'
|
||||
|
||||
|
||||
def test_bitget_cancel_order_delegates_to_sdk():
|
||||
service, mock_api = make_bitget_service()
|
||||
mock_api.cancel_order.return_value = True
|
||||
|
||||
result = service.cancel_order('BTC', 'oid-1')
|
||||
|
||||
assert result['success'] is True
|
||||
mock_api.cancel_order.assert_called_once_with(symbol='BTC', order_id='oid-1')
|
||||
|
||||
|
||||
def test_bitget_executor_close_uses_symbol_close_not_close_all():
|
||||
BitgetExecutor = load_bitget_executor_class()
|
||||
|
||||
executor = BitgetExecutor.__new__(BitgetExecutor)
|
||||
executor.bitget = MagicMock()
|
||||
executor.send_execution_notification = AsyncMock()
|
||||
executor.bitget.market_close_position.return_value = {'success': True, 'coin': 'BTC'}
|
||||
|
||||
result = asyncio.run(executor.execute_close({'symbol': 'BTCUSDT'}, 50000.0))
|
||||
|
||||
assert result['success'] is True
|
||||
executor.bitget.market_close_position.assert_called_once_with('BTC')
|
||||
executor.bitget.market_close_all.assert_not_called()
|
||||
|
||||
|
||||
def test_bitget_executor_move_stop_loss_uses_set_tp_sl():
|
||||
BitgetExecutor = load_bitget_executor_class()
|
||||
|
||||
executor = BitgetExecutor.__new__(BitgetExecutor)
|
||||
executor.bitget = MagicMock()
|
||||
executor.bitget.get_position_for_symbol.return_value = {'size': 0.02}
|
||||
executor.bitget.get_tp_sl_prices.return_value = {'take_profit': 55000.0}
|
||||
executor.bitget.set_tp_sl.return_value = {'success': True}
|
||||
|
||||
result = asyncio.run(executor.move_stop_loss('BTCUSDT', 49000.0))
|
||||
|
||||
assert result['success'] is True
|
||||
executor.bitget.set_tp_sl.assert_called_once_with(
|
||||
symbol='BTC',
|
||||
is_long=True,
|
||||
size=0.02,
|
||||
tp_price=55000.0,
|
||||
sl_price=49000.0,
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user