From a22dfe459c2dec4011c4b99c3e0029fb5a60fd54 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 22 Mar 2026 11:42:25 +0800 Subject: [PATCH] feat: Add Hyperliquid trading integration with ClawFi - Add hyperliquid_trading_service.py with position management and TP/SL - Implement dual-track trading (paper trading + Hyperliquid) - Add position size calculation based on available margin - Support net position mode (Hyperliquid) vs order mode (paper) - Add risk controls: 10% circuit breaker, 10x max leverage - Add test script for Hyperliquid SDK validation --- backend/HYPERLIQUID_REVIEW.md | 100 ++++ backend/app/config.py | 13 + backend/app/crypto_agent/crypto_agent.py | 511 +++++++++++++++--- .../services/hyperliquid_trading_service.py | 498 +++++++++++++++++ backend/requirements.txt | 4 + backend/test_hyperliquid.py | 307 +++++++++++ 6 files changed, 1347 insertions(+), 86 deletions(-) create mode 100644 backend/HYPERLIQUID_REVIEW.md create mode 100644 backend/app/services/hyperliquid_trading_service.py create mode 100644 backend/test_hyperliquid.py diff --git a/backend/HYPERLIQUID_REVIEW.md b/backend/HYPERLIQUID_REVIEW.md new file mode 100644 index 0000000..8699d12 --- /dev/null +++ b/backend/HYPERLIQUID_REVIEW.md @@ -0,0 +1,100 @@ +# Hyperliquid 集成代码 Review + +## 核心差异总结 + +### 1. 仓位模式 +- **Hyperliquid**: 净持仓模式(Position Netting)- 同币种订单自动合并 +- **模拟盘**: 订单模式(Order-based)- 每个订单独立 + +### 2. 止盈止损 +- **Hyperliquid**: 独立订单(开仓后单独设置,reduce_only=True) +- **模拟盘**: 订单属性(创建时设置) + +### 3. 需要修正的问题 + +#### 问题 1: `_get_hyperliquid_trading_state()` 需要查询止盈止损订单 +```python +def _get_hyperliquid_trading_state(self) -> tuple: + # 需要额外查询挂单,找出 reduce_only 的止盈止损订单 + # 并关联到对应的持仓 +``` + +#### 问题 2: `_execute_hyperliquid_trade()` 需要设置止盈止损 +```python +async def _execute_hyperliquid_trade(...): + # 1. 开仓 + result = self.hyperliquid.place_market_order(...) + + # 2. 立即设置止盈止损(新增) + if result.get('success'): + await self._set_hyperliquid_tp_sl(decision) +``` + +#### 问题 3: 加仓需要重新计算止盈止损 +```python +# 加仓时: +# 1. 取消旧的止盈止损订单 +# 2. 执行加仓 +# 3. 根据新的平均入场价重新设置止盈止损 +``` + +#### 问题 4: 平仓需要先取消止盈止损订单 +```python +async def _execute_hyperliquid_close(...): + # 1. 取消该币种的所有止盈止损订单(新增) + # 2. 市价平仓 +``` + +#### 问题 5: 不支持同时多空 +```python +# Hyperliquid 同一币种只能有一个方向的净持仓 +# 如果决策是反向开仓,会自动平掉现有持仓并反向 +# 需要在决策器中考虑这个限制 +``` + +## 修正方案 + +### 新增方法到 `hyperliquid_trading_service.py` + +```python +def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: + """获取挂单(包括止盈止损订单)""" + +def get_tp_sl_orders(self, symbol: str) -> Dict[str, Optional[float]]: + """获取指定币种的止盈止损价格""" + # 返回 {'take_profit': price, 'stop_loss': price} + +def set_tp_sl(self, symbol: str, is_long: bool, size: float, + tp_price: Optional[float], sl_price: Optional[float]): + """设置止盈止损""" + +def cancel_tp_sl_orders(self, symbol: str): + """取消指定币种的所有止盈止损订单""" +``` + +### 修改 `crypto_agent.py` + +```python +async def _execute_hyperliquid_trade(...): + # 1. 检查是否有反向持仓(Hyperliquid 会自动平仓) + # 2. 执行开仓 + # 3. 设置止盈止损 + # 4. 如果是加仓,需要重新计算止盈止损 +``` + +## 决策器需要考虑的差异 + +1. **加仓决策**: Hyperliquid 会合并仓位,入场价变成加权平均 +2. **反向开仓**: Hyperliquid 会自动平掉现有持仓 +3. **止盈止损调整**: 加仓后需要重新设置止盈止损 + +## 建议 + +1. **先实现基础功能**: 开仓 + 止盈止损 + 平仓 +2. **再实现高级功能**: 加仓、减仓、调整止盈止损 +3. **测试验证**: 在测试网充分测试后再启用实盘 +4. **风控优先**: 确保 10% 熔断和杠杆限制正确工作 + +## Sources +- [Bybit Copy Trading Settlement Guide](https://www.bybit.nl/en/help-center/article/A-Comprehensive-Guide-to-Copy-Trading-Settlement) +- [Hyperliquid Fees and Margin Guide](https://publish0x.com/toxi-trading-bot-short-review/how-to-trade-perpetuals-on-hyperliquid-fees-margin-liquidati-xrplyqn) diff --git a/backend/app/config.py b/backend/app/config.py index 1cbd10a..fea9dcf 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -207,6 +207,19 @@ class Settings(BaseSettings): pullback_select_time: str = "09:00" # 选股时间(24小时制) pullback_sectors_to_check: int = 5 # 检查板块数量 + # ========== Hyperliquid 交易配置(ClawFi 集成)========== + # Hyperliquid 交易开关 + hyperliquid_trading_enabled: bool = False # Hyperliquid 实盘交易开关(默认关闭) + + # Hyperliquid 环境变量(由 clawfi-hyperliquid-skill 安装脚本注入) + # CLAWFI_WALLET_ADDRESS - 主钱包地址 + # CLAWFI_PRIVATE_KEY - Agent 专用私钥 + + # Hyperliquid 风险控制 + hyperliquid_max_total_leverage: float = 10.0 # 总杠杆上限(≤10x,ClawFi 强制规则) + hyperliquid_circuit_breaker_drawdown: float = 0.10 # 10% 熔断阈值(ClawFi 强制规则) + hyperliquid_max_single_position: float = 1000 # 单笔最大持仓金额 (USD) + class Config: env_file = find_env_file() case_sensitive = False diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 93b2220..3701b62 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -60,6 +60,15 @@ class CryptoAgent: max_total_leverage=self.paper_trading.max_total_leverage ) + # Hyperliquid 实盘服务(可选) + from app.services.hyperliquid_trading_service import get_hyperliquid_service + self.hyperliquid = get_hyperliquid_service() + + if self.hyperliquid: + logger.info(f"🔥 Hyperliquid 实盘交易: 已启用") + else: + logger.info(f"📊 Hyperliquid 实盘交易: 未启用(仅模拟盘)") + # 状态管理 self.last_signals: Dict[str, Dict[str, Any]] = {} self.signal_cooldown: Dict[str, datetime] = {} @@ -81,6 +90,7 @@ class CryptoAgent: monitor.update_config("crypto_agent", { "symbols": self.symbols, "auto_trading_enabled": True, # 模拟交易始终启用 + "hyperliquid_enabled": self.hyperliquid is not None, "analysis_interval": "每5分钟整点" }) @@ -595,34 +605,46 @@ class CryptoAgent: await self._send_market_signal_notification(market_signal, current_price) # ============================================================ - # 第二阶段:交易决策(信号 + 仓位 + 账户状态) - # 模拟交易和实盘交易分别进行独立决策 + # 第二阶段:交易决策(双轨独立) + # 模拟交易和 Hyperliquid 实盘分别进行独立决策 # ============================================================ logger.info(f"\n🤖 【第二阶段:交易决策】") - # 获取配置 - paper_trading_enabled = self.settings.paper_trading_enabled + paper_decision = None + hyperliquid_decision = None - # 交易决策 - if paper_trading_enabled: - logger.info(f"\n📊 【交易决策】") - positions, account, pending_orders = self._get_trading_state() - # 过滤:只传递当前symbol的挂单给决策器,避免LLM搞混 - pending_orders_for_symbol = [o for o in pending_orders if o.get('symbol') == symbol] - decision = await self.decision_maker.make_decision( - market_signal, positions, account, current_price, pending_orders_for_symbol + # 2.1 模拟盘决策 + if self.settings.paper_trading_enabled: + logger.info(f"\n📊 【模拟盘决策】") + paper_positions, paper_account, paper_pending = self._get_paper_trading_state() + paper_pending_for_symbol = [o for o in paper_pending if o.get('symbol') == symbol] + + paper_decision = await self.decision_maker.make_decision( + market_signal, paper_positions, paper_account, current_price, paper_pending_for_symbol ) - self._log_trading_decision(decision) - # 发送交易决策通知 - await self._send_trading_decision_notification(decision, market_signal, current_price) + logger.info(f" 模拟盘决策: {paper_decision.get('action')} - {paper_decision.get('reasoning', '')}") + await self._send_trading_decision_notification(paper_decision, market_signal, current_price, prefix="[模拟盘]") else: - logger.info(f"⏸️ 交易未启用") - decision = None + logger.info(f"⏸️ 模拟盘交易未启用") + + # 2.2 Hyperliquid 实盘决策(独立) + if self.hyperliquid: + logger.info(f"\n🔥 【Hyperliquid 决策】") + hl_positions, hl_account, hl_pending = self._get_hyperliquid_trading_state() + hl_pending_for_symbol = [o for o in hl_pending if o.get('symbol') == symbol] + + hyperliquid_decision = await self.decision_maker.make_decision( + market_signal, hl_positions, hl_account, current_price, hl_pending_for_symbol + ) + logger.info(f" Hyperliquid 决策: {hyperliquid_decision.get('action')} - {hyperliquid_decision.get('reasoning', '')}") + await self._send_trading_decision_notification(hyperliquid_decision, market_signal, current_price, prefix="[Hyperliquid]") + else: + logger.info(f"⏸️ Hyperliquid 实盘交易未启用") # ============================================================ - # 第三阶段:执行交易决策 + # 第三阶段:执行交易决策(双轨独立) # ============================================================ - await self._execute_decisions(decision, market_signal, current_price) + await self._execute_decisions(paper_decision, hyperliquid_decision, market_signal, current_price) except Exception as e: logger.error(f"❌ 分析 {symbol} 出错: {e}") @@ -735,9 +757,9 @@ class CryptoAgent: if risk: logger.info(f" 风险: {risk}") - def _get_trading_state(self) -> tuple: + def _get_paper_trading_state(self) -> tuple: """ - 获取交易状态(持仓和账户) + 获取模拟盘交易状态(持仓和账户) Returns: (positions, account, pending_orders) - 持仓列表、账户状态、挂单列表 @@ -774,9 +796,83 @@ class CryptoAgent: return position_list, account, pending_orders - async def _execute_decisions(self, decision: Dict[str, Any], + def _get_hyperliquid_trading_state(self) -> tuple: + """ + 获取 Hyperliquid 实盘交易状态(持仓和账户) + + Returns: + (positions, account, pending_orders) - 持仓列表、账户状态、挂单列表 + """ + try: + # 获取账户状态 + hl_state = self.hyperliquid.get_account_state() + + # 转换持仓格式 + position_list = [] + for pos in hl_state["positions"]: + position_data = pos.get("position", {}) + coin = position_data.get("coin") + size = float(position_data.get("szi", 0)) + + if size != 0: + entry_price = float(position_data.get("entryPx", 0)) + unrealized_pnl = float(position_data.get("unrealizedPnl", 0)) + + # 获取止盈止损价格(从挂单中查询) + tp_sl_prices = self.hyperliquid.get_tp_sl_prices(coin) + + position_list.append({ + 'symbol': f"{coin}USDT", # BTC → BTCUSDT + 'side': 'buy' if size > 0 else 'sell', + 'holding': abs(size), + 'entry_price': entry_price, + 'unrealized_pnl': unrealized_pnl, + 'stop_loss': tp_sl_prices.get('stop_loss'), + 'take_profit': tp_sl_prices.get('take_profit') + }) + + # 转换账户格式(匹配模拟盘格式) + account = { + 'current_balance': hl_state["account_value"], + 'initial_balance': self.hyperliquid.initial_balance, + 'used_margin': hl_state["total_margin_used"], + 'available_balance': hl_state["available_balance"], + 'total_position_value': sum(abs(float(p.get("position", {}).get("szi", 0)) * + float(p.get("position", {}).get("entryPx", 0))) + for p in hl_state["positions"]), + 'max_total_leverage': self.hyperliquid.max_total_leverage + } + + # 计算当前总杠杆 + if account['current_balance'] > 0: + account['current_total_leverage'] = account['total_position_value'] / account['current_balance'] + else: + account['current_total_leverage'] = 0 + + # 获取挂单(包括止盈止损) + all_orders = self.hyperliquid.get_open_orders() + pending_orders = [] + for order in all_orders: + pending_orders.append({ + 'order_id': order.get('order_id'), + 'symbol': f"{order['symbol']}USDT", # 转换格式 + 'side': 'buy' if order.get('side') == 'B' else 'sell', + 'entry_price': order.get('price'), + 'quantity': order.get('size'), + 'entry_type': 'limit', + 'is_reduce_only': order.get('is_reduce_only', False) + }) + + return position_list, account, pending_orders + + except Exception as e: + logger.error(f"获取 Hyperliquid 状态失败: {e}") + return [], {}, [] + + async def _execute_decisions(self, paper_decision: Dict[str, Any], + hyperliquid_decision: Dict[str, Any], market_signal: Dict[str, Any], current_price: float): - """执行交易决策""" + """执行交易决策(双轨独立)""" # 选择最佳信号用于保存 best_signal = self._get_best_signal_from_market(market_signal) @@ -788,67 +884,74 @@ class CryptoAgent: signal_to_save['current_price'] = current_price self.signal_db.add_signal(signal_to_save) - # 获取配置 - paper_trading_enabled = self.settings.paper_trading_enabled + # ============================================================ + # 执行模拟盘决策 + # ============================================================ + if paper_decision: + await self._execute_paper_decisions(paper_decision, market_signal, current_price) # ============================================================ - # 执行交易决策 + # 执行 Hyperliquid 决策 # ============================================================ - if paper_trading_enabled and decision: - decision_type = decision.get('decision', 'HOLD') + if hyperliquid_decision and self.hyperliquid: + await self._execute_hyperliquid_decisions(hyperliquid_decision, market_signal, current_price) - if decision_type == 'HOLD': - reasoning = decision.get('reasoning', '观望') - logger.info(f"\n📊 交易决策: {reasoning}") - # HOLD决策的理由已在交易决策通知中说明,无需单独通知 - else: - logger.info(f"\n📊 【执行交易】") + async def _execute_paper_decisions(self, decision: Dict[str, Any], + market_signal: Dict[str, Any], + current_price: float): + """执行模拟盘决策""" + decision_type = decision.get('action', 'HOLD') - if decision_type in ['OPEN', 'ADD']: - # 先执行交易 - logger.info(f" 准备执行交易...") - result = await self._execute_paper_trade(decision, market_signal, current_price) - - # 检查是否成功执行 - order = result.get('order') if result else None - logger.info(f" 订单创建检查: order={'存在' if order else '不存在'}, result_key={'order' in (result or {})}") - - if order: - # 验证订单对象的有效性 - if hasattr(order, 'order_id') and order.order_id: - logger.info(f" 订单验证通过: {order.order_id}") - # 只有成功创建订单后才发送通知 - await self._send_signal_notification(market_signal, decision, current_price) - else: - logger.error(f" ❌ 订单对象无效: 缺少order_id属性") - # 订单创建失败,理由已在日志中记录,无需单独通知 - else: - # 订单创建失败,理由已在日志中记录,无需单独通知 - reason = result.get('message', '订单创建失败') if result else '订单创建失败' - logger.warning(f" ⚠️ 交易未执行: {reason}") - elif decision_type == 'CLOSE': - close_success = await self._execute_close(decision, current_price) - # CLOSE 操作也发送执行通知 - if close_success: - await self._send_signal_notification(market_signal, decision, current_price) - else: - logger.warning(f" ⚠️ 平仓未成功执行,跳过通知") - elif decision_type == 'CANCEL_PENDING': - cancel_success = await self._execute_cancel_pending(decision) - # CANCEL_PENDING 操作也发送执行通知 - if cancel_success: - await self._send_signal_notification(market_signal, decision, current_price) - else: - logger.warning(f" ⚠️ 取消挂单未成功执行,跳过通知") - elif decision_type == 'REDUCE': - reduce_success = await self._execute_reduce(decision) - # REDUCE 操作也发送执行通知 - if reduce_success: - await self._send_signal_notification(market_signal, decision, current_price) - else: - logger.warning(f" ⚠️ 减仓未成功执行,跳过通知") + if decision_type == 'HOLD': + reasoning = decision.get('reasoning', '观望') + logger.info(f"\n📊 交易决策: {reasoning}") + # HOLD决策的理由已在交易决策通知中说明,无需单独通知 else: - logger.info(f"\n⏸️ 交易未启用或决策为空") + logger.info(f"\n📊 【执行交易】") + + if decision_type in ['OPEN', 'ADD']: + # 先执行交易 + logger.info(f" 准备执行交易...") + result = await self._execute_paper_trade(decision, market_signal, current_price) + + # 检查是否成功执行 + order = result.get('order') if result else None + logger.info(f" 订单创建检查: order={'存在' if order else '不存在'}, result_key={'order' in (result or {})}") + + if order: + # 验证订单对象的有效性 + if hasattr(order, 'order_id') and order.order_id: + logger.info(f" 订单验证通过: {order.order_id}") + # 只有成功创建订单后才发送通知 + await self._send_signal_notification(market_signal, decision, current_price) + else: + logger.error(f" ❌ 订单对象无效: 缺少order_id属性") + # 订单创建失败,理由已在日志中记录,无需单独通知 + else: + # 订单创建失败,理由已在日志中记录,无需单独通知 + reason = result.get('message', '订单创建失败') if result else '订单创建失败' + logger.warning(f" ⚠️ 交易未执行: {reason}") + elif decision_type == 'CLOSE': + close_success = await self._execute_close(decision, current_price) + # CLOSE 操作也发送执行通知 + if close_success: + await self._send_signal_notification(market_signal, decision, current_price) + else: + logger.warning(f" ⚠️ 平仓未成功执行,跳过通知") + elif decision_type == 'CANCEL_PENDING': + cancel_success = await self._execute_cancel_pending(decision) + # CANCEL_PENDING 操作也发送执行通知 + if cancel_success: + await self._send_signal_notification(market_signal, decision, current_price) + else: + logger.warning(f" ⚠️ 取消挂单未成功执行,跳过通知") + elif decision_type == 'REDUCE': + reduce_success = await self._execute_reduce(decision) + # REDUCE 操作也发送执行通知 + if reduce_success: + await self._send_signal_notification(market_signal, decision, current_price) + else: + logger.warning(f" ⚠️ 减仓未成功执行,跳过通知") def _get_best_signal_from_market(self, market_signal: Dict[str, Any]) -> Dict[str, Any]: """从市场信号中获取最佳信号""" @@ -1032,14 +1135,15 @@ class CryptoAgent: async def _send_trading_decision_notification(self, decision: Dict[str, Any], market_signal: Dict[str, Any], - current_price: float): + current_price: float, + prefix: str = ""): """发送交易决策通知(第二阶段)""" try: decision_type = decision.get('decision', 'HOLD') symbol = market_signal.get('symbol') # 账户类型标识 - account_type = "📊 交易" + account_type = f"{prefix} 📊 交易" if prefix else "📊 交易" # 决策类型映射 decision_map = { @@ -1165,7 +1269,8 @@ class CryptoAgent: logger.debug(traceback.format_exc()) async def _send_signal_notification(self, market_signal: Dict[str, Any], - decision: Dict[str, Any], current_price: float): + decision: Dict[str, Any], current_price: float, + prefix: str = ""): """发送交易执行通知(第三阶段)""" try: decision_type = decision.get('decision', 'HOLD') @@ -1176,6 +1281,9 @@ class CryptoAgent: # 构建消息 - 使用旧格式风格 symbol = market_signal.get('symbol') + + # 添加前缀到标题 + title_prefix = f"{prefix} " if prefix else "" action = decision.get('action', '') reasoning = decision.get('reasoning', '') risk_analysis = decision.get('risk_analysis', '') @@ -1222,22 +1330,22 @@ class CryptoAgent: # 挂单时标题显示"挂单",现价单时显示"开仓"/"平仓"等 if decision_type == 'OPEN': decision_title = '挂单' if entry_type == 'limit' else '开仓' - title = f"[执行] {account_type} {symbol} {decision_title}" + title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "green" elif decision_type == 'CLOSE': decision_title = '挂单' if entry_type == 'limit' else '平仓' - title = f"[执行] {account_type} {symbol} {decision_title}" + title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "orange" elif decision_type == 'ADD': decision_title = '挂单' if entry_type == 'limit' else '加仓' - title = f"[执行] {account_type} {symbol} {decision_title}" + title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "green" elif decision_type == 'REDUCE': decision_title = '挂单' if entry_type == 'limit' else '减仓' - title = f"[执行] {account_type} {symbol} {decision_title}" + title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "orange" else: - title = f"[执行] {account_type} {symbol} 交易执行" + title = f"{title_prefix}[执行] {account_type} {symbol} 交易执行" color = "blue" # 构建卡片内容 @@ -1723,6 +1831,237 @@ class CryptoAgent: logger.error(f"执行减仓失败: {e}") return False + # ============================================================ + # Hyperliquid 执行方法 + # ============================================================ + + async def _execute_hyperliquid_decisions(self, decision: Dict[str, Any], + market_signal: Dict[str, Any], + current_price: float): + """执行 Hyperliquid 决策""" + decision_type = decision.get('action', 'HOLD') + + if decision_type == 'HOLD': + reasoning = decision.get('reasoning', '观望') + logger.info(f" Hyperliquid 决策: {reasoning}") + return + + try: + if decision_type in ['OPEN', 'ADD']: + logger.info(f" 准备执行 Hyperliquid 交易...") + result = await self._execute_hyperliquid_trade(decision, market_signal, current_price) + + if result.get('success'): + logger.info(f" ✅ Hyperliquid 交易成功") + await self._send_signal_notification(market_signal, decision, current_price, prefix="[Hyperliquid]") + else: + logger.error(f" ❌ Hyperliquid 交易失败: {result.get('error')}") + + elif decision_type == 'CLOSE': + logger.info(f" 准备 Hyperliquid 平仓...") + result = await self._execute_hyperliquid_close(decision, current_price) + + if result.get('success'): + logger.info(f" ✅ Hyperliquid 平仓成功") + await self._send_signal_notification(market_signal, decision, current_price, prefix="[Hyperliquid]") + else: + logger.error(f" ❌ Hyperliquid 平仓失败: {result.get('error')}") + + elif decision_type == 'CANCEL_PENDING': + logger.info(f" 准备取消 Hyperliquid 挂单...") + result = await self._execute_hyperliquid_cancel(decision) + + if result.get('success'): + logger.info(f" ✅ Hyperliquid 取消成功") + else: + logger.error(f" ❌ Hyperliquid 取消失败: {result.get('error')}") + + except Exception as e: + logger.error(f" ❌ Hyperliquid 执行异常: {e}") + + async def _execute_hyperliquid_trade(self, decision: Dict[str, Any], + market_signal: Dict[str, Any], + current_price: float) -> Dict[str, Any]: + """执行 Hyperliquid 开仓/加仓""" + try: + symbol = decision.get('symbol', '').replace('USDT', '') # BTCUSDT → BTC + side = decision.get('side') + entry_type = decision.get('entry_type', 'market') + entry_price = decision.get('entry_price', current_price) + + # 计算仓位大小(基于可用保证金和风控) + size = self._calculate_hyperliquid_position_size(decision, current_price) + + # 检查保证金是否充足 + if size <= 0: + return {"success": False, "error": "保证金不足,无法开仓"} + + # 更新杠杆 + leverage = min(decision.get('leverage', 10), 10) + self.hyperliquid.update_leverage(symbol, leverage) + + # 如果是加仓,先取消旧的止盈止损 + if decision.get('action') == 'ADD': + self.hyperliquid.cancel_tp_sl_orders(symbol) + logger.info(f" 取消旧的止盈止损订单") + + # 执行交易 + if entry_type == 'market': + result = self.hyperliquid.place_market_order( + symbol=symbol, + is_buy=(side == 'buy'), + size=size + ) + else: # limit + result = self.hyperliquid.place_limit_order( + symbol=symbol, + is_buy=(side == 'buy'), + size=size, + price=entry_price + ) + + # 如果开仓成功,设置止盈止损 + if result.get('success'): + tp_price = decision.get('take_profit') + sl_price = decision.get('stop_loss') + + if tp_price or sl_price: + # 判断方向 + is_long = (side == 'buy') + + # 设置止盈止损 + tp_sl_result = self.hyperliquid.set_tp_sl( + symbol=symbol, + is_long=is_long, + size=size, + tp_price=tp_price, + sl_price=sl_price + ) + + if not tp_sl_result.get('success'): + logger.warning(f" ⚠️ 设置止盈止损失败: {tp_sl_result.get('error')}") + + return result + + except Exception as e: + logger.error(f"Hyperliquid 交易执行失败: {e}") + return {"success": False, "error": str(e)} + + async def _execute_hyperliquid_close(self, decision: Dict[str, Any], + current_price: float) -> Dict[str, Any]: + """执行 Hyperliquid 平仓""" + try: + symbol = decision.get('symbol', '').replace('USDT', '') + + # 先取消所有止盈止损订单 + self.hyperliquid.cancel_tp_sl_orders(symbol) + logger.info(f" 取消止盈止损订单") + + # 获取当前持仓 + position = self.hyperliquid.get_position_for_symbol(symbol) + + if not position: + return {"success": False, "error": "未找到持仓"} + + size = abs(position["size"]) + is_long = position["size"] > 0 + + # 平仓(方向相反) + result = self.hyperliquid.place_market_order( + symbol=symbol, + is_buy=not is_long, + size=size, + reduce_only=True + ) + + return result + + except Exception as e: + logger.error(f"Hyperliquid 平仓失败: {e}") + return {"success": False, "error": str(e)} + + async def _execute_hyperliquid_cancel(self, decision: Dict[str, Any]) -> Dict[str, Any]: + """执行 Hyperliquid 取消挂单""" + try: + symbol = decision.get('symbol', '').replace('USDT', '') + result = self.hyperliquid.cancel_all_orders(symbol) + return result + except Exception as e: + logger.error(f"Hyperliquid 取消挂单失败: {e}") + return {"success": False, "error": str(e)} + + def _calculate_hyperliquid_position_size(self, decision: Dict[str, Any], current_price: float) -> float: + """ + 计算 Hyperliquid 仓位大小(基于可用保证金和风控限制) + + Args: + decision: 交易决策 + current_price: 当前价格 + + Returns: + 可开仓数量(币的数量,如 BTC = 0.01) + """ + try: + # 获取账户状态 + account_state = self.hyperliquid.get_account_state() + current_balance = account_state["account_value"] + used_margin = account_state["total_margin_used"] + available_balance = account_state["available_balance"] + + # 获取当前所有持仓的总价值 + total_position_value = 0 + positions = self.hyperliquid.get_open_positions() + for pos in positions: + size = abs(pos["size"]) + entry_price = pos["entry_price"] + total_position_value += size * entry_price + + # 当前总杠杆 + current_total_leverage = total_position_value / current_balance if current_balance > 0 else 0 + + # 获取杠杆配置 + leverage = min(decision.get('leverage', 5), 10) # 最大 10x + + # 计算最大可开仓金额(考虑多个限制) + max_by_config = self.hyperliquid.max_single_position # 配置的单笔限制 + max_by_available = available_balance * leverage # 可用保证金 × 杠杆 + max_by_total_leverage = (current_balance * self.hyperliquid.max_total_leverage - total_position_value) # 总杠杆限制 + + # 取最小值作为最大可开仓金额 + max_position_usd = min(max_by_config, max_by_available, max_by_total_leverage) + + # 风控检查:不能超过可用余额的 50%(保守策略) + max_position_usd = min(max_position_usd, current_balance * 0.5) + + # 如果计算出的最大值 <= 0,说明保证金不足 + if max_position_usd <= 0: + logger.warning(f"⚠️ 可用保证金不足,无法开仓") + logger.warning(f" 账户价值: ${current_balance:.2f}") + logger.warning(f" 可用余额: ${available_balance:.2f}") + logger.warning(f" 总持仓价值: ${total_position_value:.2f}") + logger.warning(f" 当前总杠杆: {current_total_leverage:.2f}x") + return 0 + + # 根据当前价格计算数量 + size = max_position_usd / current_price + + logger.info(f"💰 仓位计算:") + logger.info(f" 账户价值: ${current_balance:.2f}") + logger.info(f" 可用余额: ${available_balance:.2f}") + logger.info(f" 总持仓价值: ${total_position_value:.2f}") + logger.info(f" 当前总杠杆: {current_total_leverage:.2f}x") + logger.info(f" 计划杠杆: {leverage}x") + logger.info(f" 最大可开仓金额: ${max_position_usd:.2f} (限制: min(配置${max_by_config:.0f}, 可用${max_by_available:.0f}, 杠杆${max_by_total_leverage:.0f}))") + logger.info(f" 计算数量: {size:.6f} @ ${current_price:.2f}") + + # 四舍五入到合理精度 + return round(size, 6) + + except Exception as e: + logger.error(f"计算仓位大小失败: {e}") + # 发生错误时返回 0,不开仓 + return 0 + def _convert_to_paper_signal(self, symbol: str, signal: Dict[str, Any], current_price: float) -> Dict[str, Any]: """转换 LLM 信号格式为模拟交易格式""" diff --git a/backend/app/services/hyperliquid_trading_service.py b/backend/app/services/hyperliquid_trading_service.py new file mode 100644 index 0000000..63447a0 --- /dev/null +++ b/backend/app/services/hyperliquid_trading_service.py @@ -0,0 +1,498 @@ +""" +Hyperliquid 交易服务 - ClawFi 集成 +""" +import os +from typing import Dict, Any, Optional, List +from datetime import datetime + +from app.config import get_settings +from app.utils.logger import logger + +try: + from hyperliquid.info import Info + from hyperliquid.exchange import Exchange + from eth_account import Account + HYPERLIQUID_AVAILABLE = True +except ImportError: + HYPERLIQUID_AVAILABLE = False + logger.warning("Hyperliquid SDK 未安装,请运行: npx clawfi-hyperliquid-skill") + + +class HyperliquidTradingService: + """Hyperliquid 交易服务(ClawFi 集成)""" + + def __init__(self): + """初始化 Hyperliquid 交易服务""" + if not HYPERLIQUID_AVAILABLE: + raise ImportError("Hyperliquid SDK 未安装") + + self.settings = get_settings() + + # 从环境变量加载认证信息 + self.wallet_address = os.getenv("CLAWFI_WALLET_ADDRESS") + self.private_key = os.getenv("CLAWFI_PRIVATE_KEY") + + if not self.wallet_address or not self.private_key: + raise ValueError( + "缺少 Hyperliquid 认证信息。请运行: " + "npx clawfi-hyperliquid-skill --wallet=0x... --key=0x..." + ) + + # 风控配置 + self.max_total_leverage = self.settings.hyperliquid_max_total_leverage + self.circuit_breaker_drawdown = self.settings.hyperliquid_circuit_breaker_drawdown + self.max_single_position = self.settings.hyperliquid_max_single_position + + # 初始化 SDK + self.info = Info(base_url="https://api.hyperliquid.xyz") + account = Account.from_key(self.private_key) + self.exchange = Exchange(account, base_url="https://api.hyperliquid.xyz", + account_address=self.wallet_address) + + # 初始账户价值(用于熔断检查) + self.initial_balance: Optional[float] = None + self._initialize_account() + + logger.info(f"Hyperliquid 交易服务初始化完成") + logger.info(f" 钱包地址: {self.wallet_address}") + logger.info(f" 总杠杆上限: {self.max_total_leverage}x") + logger.info(f" 熔断阈值: {self.circuit_breaker_drawdown * 100}%") + + def _initialize_account(self): + """初始化账户信息""" + try: + state = self.get_account_state() + self.initial_balance = state["account_value"] + logger.info(f" 初始账户价值: ${self.initial_balance:,.2f}") + except Exception as e: + logger.error(f"初始化账户失败: {e}") + raise + + def get_account_state(self) -> Dict[str, Any]: + """获取账户状态""" + try: + state = self.info.user_state(self.wallet_address) + margin_summary = state.get("marginSummary", {}) + + account_value = float(margin_summary.get("accountValue", 0)) + total_margin_used = float(margin_summary.get("totalMarginUsed", 0)) + + return { + "account_value": account_value, + "total_margin_used": total_margin_used, + "available_balance": account_value - total_margin_used, + "positions": state.get("assetPositions", []), + "margin_summary": margin_summary + } + except Exception as e: + logger.error(f"获取账户状态失败: {e}") + raise + + def check_risk_limits(self) -> Dict[str, Any]: + """ + 检查风险限制(ClawFi 强制规则) + + Returns: + 风险检查结果 + """ + state = self.get_account_state() + current_value = state["account_value"] + + # 计算回撤 + if self.initial_balance is None: + self.initial_balance = current_value + + drawdown = (self.initial_balance - current_value) / self.initial_balance if self.initial_balance > 0 else 0 + + # 10% 熔断检查 + circuit_breaker_triggered = drawdown >= self.circuit_breaker_drawdown + + if circuit_breaker_triggered: + logger.error(f"🚨 触发 10% 熔断!当前回撤: {drawdown * 100:.2f}%") + # 平掉所有持仓 + self.market_close_all() + raise Exception(f"触发 10% 熔断 - 所有持仓已平仓(回撤: {drawdown * 100:.2f}%)") + + return { + "initial_balance": self.initial_balance, + "current_value": current_value, + "drawdown": drawdown, + "drawdown_percent": drawdown * 100, + "circuit_breaker_triggered": circuit_breaker_triggered, + "safe_to_trade": not circuit_breaker_triggered + } + + def update_leverage(self, symbol: str, leverage: int): + """ + 更新杠杆(必须在开仓前调用) + + Args: + symbol: 交易对(如 "BTC") + leverage: 杠杆倍数(≤10) + """ + if leverage > 10: + raise ValueError(f"杠杆不能超过 10x(ClawFi 规则),当前: {leverage}x") + + try: + result = self.exchange.update_leverage(leverage, symbol, is_cross=False) + logger.info(f"更新杠杆: {symbol} → {leverage}x") + return result + except Exception as e: + logger.error(f"更新杠杆失败: {e}") + raise + + def place_market_order( + self, + symbol: str, + is_buy: bool, + size: float, + reduce_only: bool = False + ) -> Dict[str, Any]: + """ + 下市价单 + + Args: + symbol: 交易对(如 "BTC") + is_buy: True=做多,False=做空 + size: 数量 + reduce_only: 是否仅平仓 + """ + # 风险检查 + self.check_risk_limits() + + try: + result = self.exchange.market_open(symbol, is_buy, size, reduce_only=reduce_only) + + side = "买入" if is_buy else "卖出" + logger.info(f"✅ Hyperliquid 市价单: {side} {symbol} {size}") + + return { + "success": True, + "symbol": symbol, + "side": "buy" if is_buy else "sell", + "size": size, + "result": result + } + except Exception as e: + logger.error(f"下单失败: {e}") + return { + "success": False, + "error": str(e) + } + + def place_limit_order( + self, + symbol: str, + is_buy: bool, + size: float, + price: float, + reduce_only: bool = False + ) -> Dict[str, Any]: + """下限价单""" + self.check_risk_limits() + + try: + result = self.exchange.order(symbol, is_buy, size, price, + {"limit": {"tif": "Gtc"}}, + reduce_only=reduce_only) + + side = "买入" if is_buy else "卖出" + logger.info(f"✅ Hyperliquid 限价单: {side} {symbol} {size} @ ${price}") + + return { + "success": True, + "symbol": symbol, + "side": "buy" if is_buy else "sell", + "size": size, + "price": price, + "result": result + } + except Exception as e: + logger.error(f"下单失败: {e}") + return { + "success": False, + "error": str(e) + } + + def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: + """ + 获取所有挂单(包括止盈止损订单) + + Args: + symbol: 可选,指定币种 + + Returns: + 挂单列表 + """ + try: + # Hyperliquid 没有直接的获取挂单 API,需要通过 user_state + # 注意:这个方法可能需要根据实际 API 调整 + state = self.info.user_state(self.wallet_address) + open_orders = state.get("openOrders", []) + + orders = [] + for order in open_orders: + coin = order.get("coin") + if symbol and coin != symbol: + continue + + orders.append({ + "order_id": order.get("oid"), + "symbol": coin, + "side": order.get("side"), + "size": float(order.get("totalSz", 0)), + "price": float(order.get("limitPx", 0)), + "is_reduce_only": order.get("reduceOnly", False), + "order_type": order.get("orderType", {}) + }) + + return orders + except Exception as e: + logger.error(f"获取挂单失败: {e}") + return [] + + def get_tp_sl_prices(self, symbol: str) -> Dict[str, Optional[float]]: + """ + 获取指定币种的止盈止损价格 + + Args: + symbol: 币种(如 "BTC") + + Returns: + {'take_profit': price, 'stop_loss': price} + """ + try: + orders = self.get_open_orders(symbol) + tp_price = None + sl_price = None + + for order in orders: + if not order.get("is_reduce_only"): + continue + + order_type = order.get("order_type", {}) + + # 止盈:限价单 + if "limit" in order_type and order["price"] > 0: + tp_price = order["price"] + + # 止损:触发单 + if "trigger" in order_type: + trigger_px = order_type.get("trigger", {}).get("triggerPx") + if trigger_px: + sl_price = float(trigger_px) + + return { + "take_profit": tp_price, + "stop_loss": sl_price + } + except Exception as e: + logger.error(f"获取止盈止损价格失败: {e}") + return {"take_profit": None, "stop_loss": None} + + def set_tp_sl( + self, + symbol: str, + is_long: bool, + size: float, + tp_price: Optional[float] = None, + sl_price: Optional[float] = None + ) -> Dict[str, Any]: + """ + 设置止盈止损(开仓后调用) + + Args: + symbol: 币种(如 "BTC") + is_long: 是否多头 + size: 数量 + tp_price: 止盈价格(可选) + sl_price: 止损价格(可选) + + Returns: + 执行结果 + """ + try: + results = [] + close_is_buy = not is_long # 平多头=卖出,平空头=买入 + + # 设置止盈(限价单) + if tp_price: + tp_result = self.exchange.order( + symbol, close_is_buy, size, tp_price, + {"limit": {"tif": "Gtc"}}, + reduce_only=True + ) + results.append({"type": "take_profit", "result": tp_result}) + logger.info(f"✅ 设置止盈: {symbol} @ ${tp_price}") + + # 设置止损(触发单) + if sl_price: + # 触发价格需要稍微偏离(避免滑点问题) + exec_px = sl_price * 0.999 if close_is_buy else sl_price * 1.001 + + sl_result = self.exchange.order( + symbol, close_is_buy, size, exec_px, + {"trigger": {"triggerPx": sl_price, "isMarket": True, "tpsl": "sl"}}, + reduce_only=True + ) + results.append({"type": "stop_loss", "result": sl_result}) + logger.info(f"✅ 设置止损: {symbol} @ ${sl_price}(触发)") + + return { + "success": True, + "results": results + } + + except Exception as e: + logger.error(f"设置止盈止损失败: {e}") + return { + "success": False, + "error": str(e) + } + + def cancel_tp_sl_orders(self, symbol: str) -> Dict[str, Any]: + """ + 取消指定币种的所有止盈止损订单 + + Args: + symbol: 币种(如 "BTC") + + Returns: + 取消结果 + """ + try: + orders = self.get_open_orders(symbol) + cancelled_count = 0 + + for order in orders: + if order.get("is_reduce_only"): + result = self.exchange.cancel(symbol, order["order_id"]) + if result.get("status") == "ok": + cancelled_count += 1 + + logger.info(f"✅ 取消 {symbol} 的止盈止损订单: {cancelled_count} 个") + + return { + "success": True, + "cancelled_count": cancelled_count + } + + except Exception as e: + logger.error(f"取消止盈止损订单失败: {e}") + return { + "success": False, + "error": str(e) + } + + def cancel_order(self, symbol: str, order_id: int) -> Dict[str, Any]: + """取消订单""" + try: + result = self.exchange.cancel(symbol, order_id) + logger.info(f"取消订单: {symbol} #{order_id}") + return {"success": True, "result": result} + except Exception as e: + logger.error(f"取消订单失败: {e}") + return {"success": False, "error": str(e)} + + def cancel_all_orders(self, symbol: Optional[str] = None) -> Dict[str, Any]: + """取消所有订单""" + try: + result = self.exchange.cancel_all_orders(symbol) + logger.info(f"取消所有订单: {symbol or '全部'}") + return {"success": True, "result": result} + except Exception as e: + logger.error(f"取消所有订单失败: {e}") + return {"success": False, "error": str(e)} + + def market_close_all(self) -> Dict[str, Any]: + """紧急平仓所有持仓(熔断时使用)""" + try: + state = self.get_account_state() + positions = state["positions"] + + results = [] + for pos in positions: + position_data = pos.get("position", {}) + coin = position_data.get("coin") + size = float(position_data.get("szi", 0)) + + if size == 0: + continue + + # 取消该币种的所有挂单(包括止盈止损) + 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) + + logger.info(f"🚨 紧急平仓完成,共平仓 {len(results)} 个持仓") + return {"success": True, "closed_positions": len(results), "results": results} + except Exception as e: + logger.error(f"紧急平仓失败: {e}") + return {"success": False, "error": str(e)} + + def get_open_positions(self) -> List[Dict[str, Any]]: + """获取所有持仓""" + try: + state = self.get_account_state() + positions = [] + + for pos in state["positions"]: + position_data = pos.get("position", {}) + coin = position_data.get("coin") + size = float(position_data.get("szi", 0)) + + if size == 0: + continue + + positions.append({ + "coin": coin, + "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"), + "position": position_data # 保留原始数据 + }) + + return positions + except Exception as e: + logger.error(f"获取持仓失败: {e}") + return [] + + def get_position_for_symbol(self, symbol: str) -> Optional[Dict[str, Any]]: + """获取指定币种的持仓""" + positions = self.get_open_positions() + for pos in positions: + if pos["coin"] == symbol: + return pos + return None + + +# 单例 +_hyperliquid_service_instance = None + +def get_hyperliquid_service() -> Optional[HyperliquidTradingService]: + """获取 Hyperliquid 交易服务单例""" + global _hyperliquid_service_instance + + settings = get_settings() + + # 如果未启用,返回 None + if not settings.hyperliquid_trading_enabled: + return None + + if _hyperliquid_service_instance is None: + try: + _hyperliquid_service_instance = HyperliquidTradingService() + except Exception as e: + logger.error(f"初始化 Hyperliquid 服务失败: {e}") + return None + + return _hyperliquid_service_instance diff --git a/backend/requirements.txt b/backend/requirements.txt index 5289341..4f802bd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -35,3 +35,7 @@ lxml>=4.9.0 akshare>=1.12.0 apscheduler>=3.10.0 # 定时任务 +# Hyperliquid 交易依赖(ClawFi 集成) +hyperliquid-python-sdk>=0.22.0 +eth-account>=0.10.0 + diff --git a/backend/test_hyperliquid.py b/backend/test_hyperliquid.py new file mode 100644 index 0000000..e769096 --- /dev/null +++ b/backend/test_hyperliquid.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +""" +Hyperliquid SDK 测试脚本 +用于验证 Hyperliquid 集成是否正常工作 + +⚠️ 警告:此脚本仅执行查询操作,不会执行任何交易 +""" +import os +import sys +from typing import Dict, Any + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + +def test_sdk_import(): + """测试 SDK 导入""" + print("\n" + "="*60) + print("📦 测试 1: SDK 导入") + print("="*60) + + try: + from hyperliquid.info import Info + from hyperliquid.exchange import Exchange + from eth_account import Account + print("✅ SDK 导入成功") + return True + except ImportError as e: + print(f"❌ SDK 导入失败: {e}") + print("\n请运行以下命令安装 SDK:") + print(" npx clawfi-hyperliquid-skill --wallet=YOUR_WALLET --key=YOUR_KEY") + return False + + +def test_env_variables(): + """测试环境变量""" + print("\n" + "="*60) + print("🔑 测试 2: 环境变量") + print("="*60) + + wallet = os.getenv("CLAWFI_WALLET_ADDRESS") + private_key = os.getenv("CLAWFI_PRIVATE_KEY") + + if not wallet: + print("❌ CLAWFI_WALLET_ADDRESS 未设置") + print("\n请运行以下命令设置环境变量:") + print(" npx clawfi-hyperliquid-skill --wallet=YOUR_WALLET --key=YOUR_KEY") + return False + + if not private_key: + print("❌ CLAWFI_PRIVATE_KEY 未设置") + print("\n请运行以下命令设置环境变量:") + print(" npx clawfi-hyperliquid-skill --wallet=YOUR_WALLET --key=YOUR_KEY") + return False + + print(f"✅ CLAWFI_WALLET_ADDRESS: {wallet}") + print(f"✅ CLAWFI_PRIVATE_KEY: {private_key[:10]}...{private_key[-4:]}") + return True + + +def test_connection(wallet: str, private_key: str): + """测试连接和基础查询""" + print("\n" + "="*60) + print("🔗 测试 3: 连接 Hyperliquid") + print("="*60) + + try: + from hyperliquid.info import Info + from hyperliquid.exchange import Exchange + from eth_account import Account + + # 初始化 + account = Account.from_key(private_key) + info = Info(base_url="https://api.hyperliquid.xyz", skip_ws=True) + exchange = Exchange(account, base_url="https://api.hyperliquid.xyz", + account_address=wallet) + + print(f"✅ Agent 地址: {account.address}") + print(f"✅ 目标钱包: {wallet}") + + # 测试查询账户状态 + print("\n📊 查询账户状态...") + user_state = info.user_state(wallet) + + if not user_state: + print("❌ 无法获取账户状态") + return False + + # 账户价值 + margin_summary = user_state.get("marginSummary", {}) + account_value = float(margin_summary.get("accountValue", 0)) + withdrawable = float(margin_summary.get("withdrawable", 0)) + total_margin_used = float(margin_summary.get("totalMarginUsed", 0)) + + print(f"✅ 账户价值: ${account_value:,.2f}") + print(f"✅ 可提取: ${withdrawable:,.2f}") + print(f"✅ 已用保证金: ${total_margin_used:,.2f}") + + # 查询持仓 + print("\n📈 查询持仓...") + positions = user_state.get("assetPositions", []) + + if not positions: + print("✅ 无持仓") + else: + open_positions = [] + for pos in positions: + p = pos.get("position", {}) + size = float(p.get("szi", 0)) + if size != 0: + open_positions.append({ + "coin": p.get("coin"), + "size": size, + "entry_px": float(p.get("entryPx", 0)), + "unrealized_pnl": float(p.get("unrealizedPnl", 0)) + }) + + if not open_positions: + print("✅ 无活跃持仓") + else: + print(f"✅ 活跃持仓数: {len(open_positions)}") + for pos in open_positions: + side = "做多" if pos["size"] > 0 else "做空" + print(f" {pos['coin']}: {side} {abs(pos['size'])} @ ${pos['entry_px']:,.2f} | " + f"PnL: ${pos['unrealized_pnl']:,.2f}") + + # 查询挂单 + print("\n📋 查询挂单...") + open_orders = user_state.get("openOrders", []) + + if not open_orders: + print("✅ 无挂单") + else: + print(f"✅ 挂单数: {len(open_orders)}") + for order in open_orders: + coin = order.get("coin") + side = order.get("side") + size = float(order.get("totalSz", 0)) + limit_px = float(order.get("limitPx", 0)) + print(f" {coin}: {side} {size} @ ${limit_px:,.2f}") + + return True + + except Exception as e: + print(f"❌ 连接失败: {e}") + import traceback + traceback.print_exc() + return False + + +def test_tick_size(): + """测试获取 tick size""" + print("\n" + "="*60) + print("📏 测试 4: 获取 Tick Size") + print("="*60) + + try: + from hyperliquid.info import Info + + info = Info(base_url="https://api.hyperliquid.xyz", skip_ws=True) + + # 获取元数据 + meta = info.meta() + universe = meta.get("universe", []) + + print(f"✅ 可交易币种数: {len(universe)}") + + # 显示前几个币种的 tick size + for asset in universe[:5]: + name = asset.get("name") + tick_size = float(asset.get("tickSize", 1.0)) + sz_decimals = asset.get("szDecimals", 5) + print(f" {name}: tick_size={tick_size}, sz_decimals={sz_decimals}") + + return True + + except Exception as e: + print(f"❌ 获取 tick size 失败: {e}") + return False + + +def test_service_initialization(): + """测试服务初始化""" + print("\n" + "="*60) + print("🔧 测试 5: 服务初始化") + print("="*60) + + try: + from app.services.hyperliquid_trading_service import get_hyperliquid_service + + # 尝试获取服务(如果未启用会返回 None) + service = get_hyperliquid_service() + + if service is None: + print("⚠️ Hyperliquid 服务未启用(hyperliquid_trading_enabled=False)") + print(" 要启用服务,请在 .env 文件中设置: hyperliquid_trading_enabled=true") + return False + + print("✅ Hyperliquid 服务初始化成功") + print(f" 钱包地址: {service.wallet_address}") + print(f" 最大杠杆: {service.max_total_leverage}x") + print(f" 熔断阈值: {service.circuit_breaker_drawdown * 100}%") + print(f" 单笔最大持仓: ${service.max_single_position}") + + # 测试获取账户状态 + print("\n📊 测试获取账户状态...") + state = service.get_account_state() + print(f"✅ 账户价值: ${state['account_value']:,.2f}") + print(f"✅ 可用余额: ${state['available_balance']:,.2f}") + + # 测试获取持仓 + print("\n📈 测试获取持仓...") + positions = service.get_open_positions() + if positions: + print(f"✅ 活跃持仓: {len(positions)} 个") + for pos in positions: + side = "做多" if pos["size"] > 0 else "做空" + print(f" {pos['coin']}: {side} {abs(pos['size'])}") + else: + print("✅ 无活跃持仓") + + # 测试获取止盈止损价格 + print("\n🛡️ 测试获取止盈止损...") + if positions: + for pos in positions: + tp_sl = service.get_tp_sl_prices(pos["coin"]) + print(f" {pos['coin']}: TP={tp_sl['take_profit']}, SL={tp_sl['stop_loss']}") + else: + # 测试 BTC 的止盈止损(即使没有持仓) + tp_sl = service.get_tp_sl_prices("BTC") + print(f" BTC: TP={tp_sl['take_profit']}, SL={tp_sl['stop_loss']}") + + return True + + except Exception as e: + print(f"❌ 服务初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + """主测试流程""" + print("\n" + "🦅"*30) + print(" Hyperliquid 集成测试") + print("🦅"*30) + + results = {} + + # 测试 1: SDK 导入 + results["sdk_import"] = test_sdk_import() + if not results["sdk_import"]: + print("\n❌ SDK 导入失败,无法继续测试") + return False + + # 测试 2: 环境变量 + results["env_vars"] = test_env_variables() + if not results["env_vars"]: + print("\n❌ 环境变量未设置,无法继续测试") + return False + + # 获取环境变量 + wallet = os.getenv("CLAWFI_WALLET_ADDRESS") + private_key = os.getenv("CLAWFI_PRIVATE_KEY") + + # 测试 3: 连接 + results["connection"] = test_connection(wallet, private_key) + if not results["connection"]: + print("\n⚠️ 连接测试失败,但继续测试其他功能") + + # 测试 4: Tick size + results["tick_size"] = test_tick_size() + + # 测试 5: 服务初始化 + results["service"] = test_service_initialization() + + # 总结 + print("\n" + "="*60) + print("📊 测试结果总结") + print("="*60) + + for test_name, result in results.items(): + status = "✅ 通过" if result else "❌ 失败" + print(f" {test_name}: {status}") + + all_passed = all(results.values()) + + if all_passed: + print("\n🎉 所有测试通过!Hyperliquid 集成正常工作") + else: + print("\n⚠️ 部分测试失败,请检查上述错误信息") + + return all_passed + + +if __name__ == "__main__": + try: + success = main() + sys.exit(0 if success else 1) + except KeyboardInterrupt: + print("\n\n⚠️ 测试被用户中断") + sys.exit(1) + except Exception as e: + print(f"\n\n❌ 测试过程中发生异常: {e}") + import traceback + traceback.print_exc() + sys.exit(1)