diff --git a/backend/HYPERLIQUID_REVIEW.md b/backend/HYPERLIQUID_REVIEW.md deleted file mode 100644 index 8699d12..0000000 --- a/backend/HYPERLIQUID_REVIEW.md +++ /dev/null @@ -1,100 +0,0 @@ -# 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/api/hyperliquid.py b/backend/app/api/hyperliquid.py deleted file mode 100644 index 13c9f4b..0000000 --- a/backend/app/api/hyperliquid.py +++ /dev/null @@ -1,383 +0,0 @@ -""" -Hyperliquid 交易 API -提供 Hyperliquid 实盘交易数据接口 -""" -from fastapi import APIRouter, HTTPException, Query -from typing import Optional -from datetime import datetime -from pydantic import BaseModel - -from app.services.hyperliquid_trading_service import get_hyperliquid_service -from app.utils.logger import logger - - -router = APIRouter(prefix="/api/hyperliquid", tags=["Hyperliquid"]) - - -class AccountStatusResponse(BaseModel): - """账户状态响应""" - success: bool - enabled: bool - message: str - data: Optional[dict] = None - - -@router.get("/account") -async def get_account_status(): - """ - 获取 Hyperliquid 账户状态 - - 返回: - - account_value: 账户总价值 - - available_balance: 可用余额 - - total_margin_used: 已用保证金 - - total_position_value: 总持仓价值 - - current_total_leverage: 当前总杠杆 - - max_total_leverage: 最大总杠杆限制 - - initial_balance: 初始余额(用于计算回撤) - - drawdown_percent: 回撤百分比 - """ - try: - service = get_hyperliquid_service() - - if service is None: - return AccountStatusResponse( - success=True, - enabled=False, - message="Hyperliquid 服务未启用。请在 .env 中设置 hyperliquid_trading_enabled=true", - data=None - ) - - # 获取账户状态 - state = service.get_account_state() - - # 计算回撤 - if service.initial_balance and service.initial_balance > 0: - drawdown = (service.initial_balance - state["account_value"]) / service.initial_balance - else: - drawdown = 0 - - # 获取持仓 - positions = service.get_open_positions() - total_position_value = sum( - abs(pos["size"]) * pos["entry_price"] - for pos in positions - ) - - # 计算当前总杠杆 - if state["account_value"] > 0: - current_total_leverage = total_position_value / state["account_value"] - else: - current_total_leverage = 0 - - return AccountStatusResponse( - success=True, - enabled=True, - message="Hyperliquid 服务正常", - data={ - "account_value": state["account_value"], - "available_balance": state["available_balance"], - "total_margin_used": state["total_margin_used"], - "total_position_value": total_position_value, - "current_total_leverage": current_total_leverage, - "max_total_leverage": service.max_total_leverage, - "initial_balance": service.initial_balance, - "drawdown_percent": drawdown * 100, - "wallet_address": service.wallet_address, - "leverage_limit": 10.0, # Hyperliquid 强制限制 - "circuit_breaker_threshold": service.circuit_breaker_drawdown * 100 - } - ) - except Exception as e: - logger.error(f"获取 Hyperliquid 账户状态失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/positions") -async def get_positions( - symbol: Optional[str] = Query(None, description="币种筛选,如 BTC") -): - """ - 获取 Hyperliquid 持仓信息 - - 返回所有净持仓(Position Netting 模式) - """ - try: - service = get_hyperliquid_service() - - if service is None: - return { - "success": True, - "enabled": False, - "message": "Hyperliquid 服务未启用", - "positions": [] - } - - # 获取持仓 - all_positions = service.get_open_positions() - - # 筛选指定币种 - if symbol: - symbol = symbol.replace("USDT", "") # 兼容输入 - all_positions = [p for p in all_positions if p["coin"] == symbol] - - # 获取每个持仓的止盈止损价格 - positions_data = [] - for pos in all_positions: - coin = pos["coin"] - size = pos["size"] - - # 获取止盈止损 - tp_sl = service.get_tp_sl_prices(coin) - - positions_data.append({ - "symbol": f"{coin}USDT", - "side": "long" if size > 0 else "short", - "size": abs(size), - "entry_price": pos["entry_price"], - "unrealized_pnl": pos["unrealized_pnl"], - "leverage": (pos.get("leverage") or {}).get("value", "N/A") if isinstance(pos.get("leverage"), dict) else (pos.get("leverage") or "N/A"), - "liquidation_price": pos.get("liquidation_price"), - "take_profit": tp_sl.get("take_profit"), - "stop_loss": tp_sl.get("stop_loss"), - "position": pos.get("position") # 原始数据 - }) - - return { - "success": True, - "enabled": True, - "count": len(positions_data), - "positions": positions_data - } - except Exception as e: - logger.error(f"获取 Hyperliquid 持仓失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/orders") -async def get_orders( - symbol: Optional[str] = Query(None, description="币种筛选,如 BTC") -): - """ - 获取 Hyperliquid 挂单信息 - - 包括: - - 限价单(未成交的开仓/平仓单) - - 止盈止损单(reduce_only=True) - """ - try: - service = get_hyperliquid_service() - - if service is None: - return { - "success": True, - "enabled": False, - "message": "Hyperliquid 服务未启用", - "orders": [] - } - - # 获取所有挂单 - all_orders = service.get_open_orders() - - # 筛选指定币种 - if symbol: - symbol = symbol.replace("USDT", "") # 兼容输入 - all_orders = [o for o in all_orders if o["symbol"] == symbol] - - # 分类挂单 - entry_orders = [] - tp_sl_orders = [] - - for order in all_orders: - order_data = { - "order_id": order.get("order_id"), - "symbol": f"{order['symbol']}USDT", - "side": order.get("side"), - "size": order.get("size"), - "price": order.get("price"), - "is_reduce_only": order.get("is_reduce_only", False), - "order_type": order.get("order_type", {}) - } - - if order.get("is_reduce_only"): - tp_sl_orders.append(order_data) - else: - entry_orders.append(order_data) - - return { - "success": True, - "enabled": True, - "counts": { - "entry_orders": len(entry_orders), - "tp_sl_orders": len(tp_sl_orders), - "total": len(all_orders) - }, - "entry_orders": entry_orders, - "tp_sl_orders": tp_sl_orders - } - except Exception as e: - logger.error(f"获取 Hyperliquid 挂单失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/summary") -async def get_summary(): - """ - 获取 Hyperliquid 交易摘要 - - 一次性获取账户、持仓、订单的概要信息 - """ - try: - service = get_hyperliquid_service() - - if service is None: - return { - "success": True, - "enabled": False, - "message": "Hyperliquid 服务未启用" - } - - # 获取账户状态 - account_state = service.get_account_state() - - # 获取持仓 - positions = service.get_open_positions() - total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in positions) - - # 获取挂单 - orders = service.get_open_orders() - - # 计算杠杆 - if account_state["account_value"] > 0: - current_leverage = total_position_value / account_state["account_value"] - else: - current_leverage = 0 - - # 计算回撤 - if service.initial_balance and service.initial_balance > 0: - drawdown = (service.initial_balance - account_state["account_value"]) / service.initial_balance - else: - drawdown = 0 - - return { - "success": True, - "enabled": True, - "data": { - "account": { - "account_value": account_state["account_value"], - "available_balance": account_state["available_balance"], - "total_margin_used": account_state["total_margin_used"] - }, - "positions": { - "count": len(positions), - "total_value": total_position_value - }, - "orders": { - "count": len(orders), - "entry_orders": len([o for o in orders if not o.get("is_reduce_only", False)]), - "tp_sl_orders": len([o for o in orders if o.get("is_reduce_only", False)]) - }, - "risk": { - "current_leverage": current_leverage, - "max_leverage": service.max_total_leverage, - "leverage_utilization": (current_leverage / service.max_total_leverage * 100) if service.max_total_leverage > 0 else 0, - "drawdown": drawdown * 100, - "circuit_breaker_threshold": service.circuit_breaker_drawdown * 100 - } - } - } - except Exception as e: - logger.error(f"获取 Hyperliquid 摘要失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/orders/cancel") -async def cancel_orders( - symbol: str = Query(..., description="币种,如 BTC") -): - """ - 取消指定币种的所有挂单 - - 包括开仓单和止盈止损单 - """ - try: - service = get_hyperliquid_service() - - if service is None: - return { - "success": False, - "message": "Hyperliquid 服务未启用" - } - - # 取消该币种的所有订单 - result = service.cancel_all_orders(symbol.replace("USDT", "")) - - if result.get("success"): - return { - "success": True, - "message": f"已取消 {symbol} 的所有挂单" - } - else: - return { - "success": False, - "message": result.get("error", "取消失败") - } - except Exception as e: - logger.error(f"取消 Hyperliquid 挂单失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/positions/close") -async def close_position( - symbol: str = Query(..., description="币种,如 BTC") -): - """ - 平掉指定币种的所有持仓(市价平仓) - - ⚠️ 警告:此操作会立即以市价平仓,请谨慎使用 - """ - try: - service = get_hyperliquid_service() - - if service is None: - return { - "success": False, - "message": "Hyperliquid 服务未启用" - } - - # 获取持仓 - position = service.get_position_for_symbol(symbol.replace("USDT", "")) - - if not position: - return { - "success": False, - "message": f"未找到 {symbol} 的持仓" - } - - # 取消所有挂单(包括止盈止损) - service.cancel_tp_sl_orders(symbol.replace("USDT", "")) - - # 市价平仓 - size = abs(position["size"]) - is_long = position["size"] > 0 - - result = service.place_market_order( - symbol=symbol.replace("USDT", ""), - is_buy=not is_long, - size=size, - reduce_only=True - ) - - if result.get("success"): - return { - "success": True, - "message": f"已平仓 {symbol} {size} @ 市价" - } - else: - return { - "success": False, - "message": result.get("error", "平仓失败") - } - except Exception as e: - logger.error(f"Hyperliquid 平仓失败: {e}") - raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/api/system.py b/backend/app/api/system.py index 376e08a..88fe25e 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -11,7 +11,6 @@ from app.crypto_agent.crypto_agent import get_crypto_agent from app.services.signal_database_service import get_signal_db_service from app.services.paper_trading_service import get_paper_trading_service from app.services.bitget_live_trading_service import get_bitget_live_service -from app.services.hyperliquid_trading_service import get_hyperliquid_service router = APIRouter() @@ -355,44 +354,6 @@ async def get_console_snapshot(): }, } - hyperliquid_service = get_hyperliquid_service() - hyperliquid_summary = {"enabled": False} - if hyperliquid_service is not None: - hl_account = hyperliquid_service.get_account_state() - hl_positions = hyperliquid_service.get_open_positions() - hl_orders = hyperliquid_service.get_open_orders() - hl_total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in hl_positions) - hl_drawdown = 0.0 - if hyperliquid_service.initial_balance and hyperliquid_service.initial_balance > 0: - hl_drawdown = (hyperliquid_service.initial_balance - hl_account["account_value"]) / hyperliquid_service.initial_balance * 100 - - hyperliquid_summary = { - "enabled": True, - "account": { - "account_value": hl_account.get("account_value", 0), - "available_balance": hl_account.get("available_balance", 0), - "total_margin_used": hl_account.get("total_margin_used", 0), - "initial_balance": hyperliquid_service.initial_balance, - }, - "positions": { - "count": len(hl_positions), - "total_value": hl_total_position_value, - "items": hl_positions[:8], - }, - "orders": { - "count": len(hl_orders), - "entry_orders": len([o for o in hl_orders if not o.get("is_reduce_only")]), - "tp_sl_orders": len([o for o in hl_orders if o.get("is_reduce_only")]), - "items": hl_orders[:8], - }, - "risk": { - "current_leverage": hl_total_position_value / hl_account["account_value"] if hl_account.get("account_value", 0) > 0 else 0, - "max_leverage": hyperliquid_service.max_total_leverage, - "drawdown_percent": hl_drawdown, - "circuit_breaker_threshold": hyperliquid_service.circuit_breaker_drawdown * 100, - }, - } - recent_cutoff = now - timedelta(minutes=30) recent_signal_count = sum( 1 @@ -418,22 +379,13 @@ async def get_console_snapshot(): for order in (bg_orders[:12] if bitget_service is not None else []) ] - hyperliquid_position_items = [ - _normalize_platform_position("hyperliquid", pos) - for pos in (hl_positions[:12] if hyperliquid_service is not None else []) - ] - hyperliquid_order_items = [ - _normalize_platform_order("hyperliquid", order) - for order in (hl_orders[:12] if hyperliquid_service is not None else []) - ] - unified_positions = sorted( - paper_position_items + bitget_position_items + hyperliquid_position_items, + paper_position_items + bitget_position_items, key=lambda item: _parse_signal_timestamp(item.get("opened_at")) or datetime.min, reverse=True, ) unified_orders = sorted( - paper_order_items + bitget_order_items + hyperliquid_order_items, + paper_order_items + bitget_order_items, key=lambda item: _parse_signal_timestamp(item.get("created_at")) or datetime.min, reverse=True, ) @@ -466,7 +418,6 @@ async def get_console_snapshot(): }, }, "bitget": bitget_summary, - "hyperliquid": hyperliquid_summary, } execution_events = crypto_agent.get_recent_execution_events(limit=40) diff --git a/backend/app/config.py b/backend/app/config.py index 71e41f9..6477218 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -217,19 +217,6 @@ class Settings(BaseSettings): # ========== Bitget 实盘交易配置 ========== bitget_trading_enabled: bool = False # Bitget 实盘交易开关(默认关闭) - # ========== Hyperliquid 交易配置(ClawFi 集成)========== - # Hyperliquid 交易开关 - hyperliquid_trading_enabled: bool = False # Hyperliquid 实盘交易开关(默认关闭) - - # Hyperliquid 环境变量(由 clawfi-hyperliquid-skill 安装脚本注入) - clawfi_wallet_address: Optional[str] = None # 主钱包地址 - clawfi_private_key: Optional[str] = None # 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 c90212d..fcf2beb 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -63,20 +63,11 @@ class CryptoAgent: 'PaperTrading': { 'min_margin': {}, # 无最小限制 'max_margin_pct': 0.25, # 单笔最大25%(与实盘一致) - }, - 'Hyperliquid': { - 'min_margin': { - 'BTC': 50, # Hyperliquid 最小约 $50 - 'ETH': 20, - 'SOL': 10, - }, - 'max_margin_pct': 0.25, # 单笔最大25% } } PLATFORM_SIGNAL_PRIORITY = { 'PaperTrading': ['short_term', 'medium_term'], - 'Hyperliquid': ['short_term', 'medium_term'], 'Bitget': ['medium_term', 'short_term'], } @@ -167,15 +158,6 @@ class CryptoAgent: # 模拟交易服务(始终启用) self.paper_trading = get_paper_trading_service() - # 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 实盘交易: 未启用(仅模拟盘)") - # Bitget 实盘服务(可选) from app.services.bitget_live_trading_service import get_bitget_live_service self.bitget = get_bitget_live_service() @@ -186,7 +168,7 @@ class CryptoAgent: logger.info(f"📊 Bitget 实盘交易: 未启用(仅模拟盘)") # 初始化平台执行器 - from app.crypto_agent.executor import PaperTradingExecutor, BitgetExecutor, HyperliquidExecutor + from app.crypto_agent.executor import PaperTradingExecutor, BitgetExecutor self.executors = {} @@ -200,11 +182,6 @@ class CryptoAgent: self.executors['Bitget'] = BitgetExecutor() logger.info(f" 🔥 Bitget 执行器: 已初始化") - # Hyperliquid 执行器 - if self.hyperliquid: - self.executors['Hyperliquid'] = HyperliquidExecutor() - logger.info(f" 🔥 Hyperliquid 执行器: 已初始化") - # 状态管理 self.last_signals: Dict[str, Dict[str, Any]] = {} self.last_execution_preview: Dict[str, Dict[str, Any]] = {} @@ -245,7 +222,6 @@ class CryptoAgent: # 挂单 TP/SL 追踪:挂单成交后自动补设止盈止损 # key=order_id, value={symbol, is_long, size/contracts, tp_price, sl_price} - self._hl_pending_tp_sl: Dict[str, Dict] = {} self._bg_pending_tp_sl: Dict[str, Dict] = {} # 配置 @@ -271,7 +247,6 @@ class CryptoAgent: monitor.update_config("crypto_agent", { "symbols": self.symbols, "auto_trading_enabled": True, # 模拟交易始终启用 - "hyperliquid_enabled": self.hyperliquid is not None, "bitget_enabled": self.bitget is not None, "analysis_interval": "每5分钟轻扫描,LLM分层冷却" }) @@ -1024,10 +999,6 @@ class CryptoAgent: # 使用执行器检查持仓管理(止盈/超时退出/移动止损) await self._check_position_management_all_platforms() - # 检查实盘挂单是否已成交,补设止盈止损 - if self.hyperliquid: - await self._check_and_set_pending_tp_sl_hyperliquid() - await self._check_hyperliquid_missing_tp_sl() if self.bitget: await self._check_and_set_pending_tp_sl_bitget() await self._check_bitget_missing_tp_sl() # 兜底:检查缺少的 TP/SL 并补救 @@ -1362,31 +1333,7 @@ class CryptoAgent: paper_decision = {"action": "IGNORE", "reason": "未启用"} 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_signal = self._select_signal_for_platform(valid_signals, 'Hyperliquid', market_state=market_signal.get('market_state', '中性'), trend_direction=market_signal.get('trend_direction', 'neutral')) - if hl_signal: - logger.info( - f" 采用信号: {hl_signal.get('timeframe', 'unknown')} | " - f"{hl_signal.get('action')} | {hl_signal.get('confidence', 0)}%" - ) - trading_signal = self._build_execution_signal(symbol, hl_signal, current_price, market_signal) - 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 - ) - else: - logger.info(" 无可执行信号") - hl_decision = {"decision": "HOLD", "action": "IGNORE", "reason": "无适配信号", "reasoning": "无适配信号"} - else: - hl_decision = {"action": "IGNORE", "reason": "未启用"} - logger.info(f"⏸️ Hyperliquid 实盘交易未启用") - - # 2.3 Bitget 实盘处理 + # 2.2 Bitget 实盘处理 if self.bitget: logger.info(f"\n🔥 【Bitget】") bg_positions, bg_account, bg_pending = self._get_bitget_trading_state() @@ -1414,14 +1361,13 @@ class CryptoAgent: 'timestamp': datetime.now().isoformat(), 'current_price': current_price, 'paper': paper_decision, - 'hyperliquid': hl_decision, 'bitget': bg_decision, } # ============================================================ # 第三阶段:执行交易动作(各平台独立) # ============================================================ - await self._execute_decisions(paper_decision, hl_decision, bg_decision, market_signal, current_price) + await self._execute_decisions(paper_decision, bg_decision, market_signal, current_price) self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat() self._analysis_monitor["last_analysis_status"] = "completed" self._analysis_monitor["last_analysis_detail"] = f"完成分析,产生 {len(valid_signals)} 个有效信号" @@ -1563,91 +1509,6 @@ class CryptoAgent: return position_list, account, pending_orders - 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"]: - if not isinstance(pos, dict): - continue - position_data = pos.get("position", {}) - if not isinstance(position_data, dict): - continue - 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)) - mark_price = float(position_data.get("markPx", 0) or position_data.get("mark_price", 0) or 0) - - # 获取止盈止损价格(从挂单中查询) - tp_sl_prices = self.hyperliquid.get_tp_sl_prices(coin) - - position = { - 'symbol': f"{coin}USDT", # BTC → BTCUSDT - 'side': 'buy' if size > 0 else 'sell', - 'holding': abs(size), - 'entry_price': entry_price, - 'mark_price': mark_price, - 'unrealized_pnl': unrealized_pnl, - '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_list.append(self._build_runtime_position_state(position)) - - # 转换账户格式(匹配模拟盘格式) - 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"], - 'available': hl_state["available_balance"], # 决策器期望的键名 - 'order_leverage': min(getattr(self.hyperliquid, 'max_total_leverage', 10), 10), - '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': order.get('side'), - 'entry_price': order.get('price'), - '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 - - except Exception as e: - logger.error(f"获取 Hyperliquid 状态失败: {e}") - return [], {}, [] - def _normalize_symbol(self, symbol: str) -> str: """统一交易对格式为 BTCUSDT""" if not symbol: @@ -1749,10 +1610,9 @@ class CryptoAgent: return fallback async def _execute_decisions(self, paper_decision: Dict[str, Any], - hyperliquid_decision: Dict[str, Any], bitget_decision: Dict[str, Any], market_signal: Dict[str, Any], current_price: float): - """执行交易决策(三轨独立)""" + """执行交易决策(模拟盘 + Bitget 独立)""" # 保存本轮所有达到阈值的可交易信号,避免分流后只落一条信号 threshold = self.settings.crypto_llm_threshold * 100 symbol = market_signal.get('symbol') @@ -1784,21 +1644,6 @@ class CryptoAgent: status="warning", ) - # ============================================================ - # 执行 Hyperliquid 决策 - # ============================================================ - if hyperliquid_decision and self.hyperliquid and not self._is_platform_halted('Hyperliquid'): - await self._execute_hyperliquid_decisions(hyperliquid_decision, market_signal, current_price) - elif hyperliquid_decision and self.hyperliquid and self._is_platform_halted('Hyperliquid'): - self._record_execution_event( - "Hyperliquid", - "platform_halted_skip", - symbol=hyperliquid_decision.get("symbol", market_signal.get("symbol", "")), - decision=hyperliquid_decision, - reason="平台已停机,跳过执行", - status="warning", - ) - # ============================================================ # 执行 Bitget 决策 # ============================================================ @@ -1819,7 +1664,6 @@ class CryptoAgent: current_price=current_price, decisions={ "PaperTrading": paper_decision, - "Hyperliquid": hyperliquid_decision, "Bitget": bitget_decision, }, ) @@ -2459,9 +2303,9 @@ class CryptoAgent: async def _send_signal_notification(self, market_signal: Dict[str, Any], decision: Dict[str, Any], current_price: float, - prefix: str = "", hl_order_status: str = None): + prefix: str = "", order_status: str = None): """发送交易执行通知(第三阶段) - hl_order_status: Hyperliquid 限价单实际状态 'resting'|'filled'|None + order_status: 限价单实际状态 'resting'|'filled'|None """ try: decision_type = decision.get('decision', 'HOLD') @@ -2518,12 +2362,12 @@ class CryptoAgent: timeframe_map = {'short_term': '短线', 'medium_term': '趋势', 'long_term': '长线'} timeframe_text = timeframe_map.get(signal_timeframe, signal_timeframe) - # 对 Hyperliquid 限价单:用实际订单状态决定显示 - # resting=真的在挂单中, filled=已立即成交, None=非HL或市价单 - if hl_order_status == 'resting': + # 对限价单:用实际订单状态决定显示 + # resting=真的在挂单中, filled=已立即成交, None=市价单或未知 + if order_status == 'resting': entry_type_text = '挂单' entry_type_icon = '⏳' - elif hl_order_status == 'filled': + elif order_status == 'filled': entry_type_text = '现价成交' entry_type_icon = '⚡' else: @@ -2534,11 +2378,11 @@ class CryptoAgent: position_map = {'heavy': '🔥 重仓', 'medium': '📊 中仓', 'light': '🌱 轻仓'} position_display = position_map.get(position_size, '🌱 轻仓') - # 构建卡片标题:Hyperliquid 限价单区分实际状态 + # 构建卡片标题:限价单区分实际状态 if decision_type == 'OPEN': - if hl_order_status == 'resting': + if order_status == 'resting': decision_title = '挂单中' - elif hl_order_status == 'filled': + elif order_status == 'filled': decision_title = '开仓(立即成交)' else: decision_title = '挂单' if entry_type == 'limit' else '开仓' @@ -2549,9 +2393,9 @@ class CryptoAgent: title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "orange" elif decision_type == 'ADD': - if hl_order_status == 'resting': + if order_status == 'resting': decision_title = '加仓挂单中' - elif hl_order_status == 'filled': + elif order_status == 'filled': decision_title = '加仓(立即成交)' else: decision_title = '挂单' if entry_type == 'limit' else '加仓' @@ -2595,7 +2439,7 @@ class CryptoAgent: content_parts.append(f"🎯 **止盈价**: ${take_profit}") # 决策理由和风险分析(挂单中不显示,等成交后再显示) - is_pending = (hl_order_status == 'resting') or \ + is_pending = (order_status == 'resting') or \ (entry_type == 'limit' and decision_type in ['OPEN', 'ADD']) if not is_pending: content_parts.append(f"") @@ -3105,26 +2949,6 @@ class CryptoAgent: logger.error(f"执行减仓失败: {e}") return False - # ============================================================ - # Hyperliquid 执行方法 - # ============================================================ - - async def _notify_hyperliquid_error(self, symbol: str, operation: str, error: str): - """发送 Hyperliquid 操作失败的飞书/钉钉/Telegram 通知""" - title = f"❌ Hyperliquid 操作失败 - {symbol}" - content = "\n".join([ - f"🔴 **操作**: {operation}", - f"⚠️ **错误**: {error}", - f"🕐 **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - ]) - logger.error(f"[Hyperliquid] {operation} 失败 | {symbol} | {error}") - if self.settings.feishu_enabled and self.feishu_error: - await self.feishu_error.send_card(title, content, "red") - if self.settings.telegram_enabled: - await self.telegram.send_message(f"{title}\n\n{content}") - if self.settings.dingtalk_enabled: - await self.dingtalk.send_action_card(title, content) - async def _notify_bitget_error(self, symbol: str, operation: str, error: str): """发送 Bitget 操作失败的飞书/钉钉/Telegram 通知""" title = f"❌ Bitget 操作失败 - {symbol}" @@ -3473,7 +3297,7 @@ class CryptoAgent: if platform_name == 'PaperTrading' and self.paper_trading: recent_orders = self.paper_trading.get_order_history(limit=max_lookback) - # Bitget/Hyperliquid 实盘暂不查询历史(API 限制),用空列表 + # Bitget 实盘暂不查询历史(API 限制),用空列表 if not recent_orders: return {'losing_streak': 0, 'should_cool_down': False, 'margin_multiplier': 1.0, 'reason': ''} @@ -3602,7 +3426,7 @@ class CryptoAgent: Args: signal: 交易信号(包含 action, symbol, confidence 等) - platform_name: 平台名称 ('Bitget', 'PaperTrading', 'Hyperliquid') + platform_name: 平台名称 ('Bitget', 'PaperTrading') account: 平台账户状态 positions: 当前持仓列表 pending_orders: 当前挂单列表 @@ -3807,7 +3631,7 @@ class CryptoAgent: await self._send_signal_notification( market_signal, decision, current_price, prefix="[Bitget]", - hl_order_status=order_status + order_status=order_status ) # TP/SL 警告 @@ -4126,346 +3950,6 @@ class CryptoAgent: logger.error(f"Bitget 计算仓位大小失败: {e}") return 0 - async def _execute_hyperliquid_decisions(self, decision: Dict[str, Any], - market_signal: Dict[str, Any], - current_price: float): - """执行 Hyperliquid 决策(使用执行器)""" - try: - decision_type = decision.get('decision', 'HOLD') - symbol = decision.get('symbol', 'UNKNOWN') - next_decision = decision.get('next_decision') - - if decision_type == 'HOLD': - hold_reason = decision.get('reason', decision.get('reasoning', '观望')) - logger.info(f" Hyperliquid 决策: {hold_reason}") - self._record_execution_event("Hyperliquid", "hold", decision=decision, reason=hold_reason, status="hold") - # 仅记录日志,不发飞书通知(避免消息过多) - return - - # 使用执行器 - executor = self.executors.get('Hyperliquid') - if not executor: - logger.warning(f" ⚠️ Hyperliquid 执行器未初始化") - return - - # 执行开仓/加仓 - if decision_type in ['OPEN', 'ADD']: - logger.info(f" 准备执行 Hyperliquid 交易...") - result = await executor.execute_open(decision, current_price) - - if result.get('success'): - order_status = result.get('order_status', 'filled') - logger.info(f" ✅ Hyperliquid 交易成功 ({order_status})") - decision['_execution_succeeded'] = True - self._record_execution_event( - "Hyperliquid", "open_success", decision=decision, status="success", - reason=decision.get('reason', decision.get('reasoning', '')), - extra={"order_status": order_status, "order_id": result.get('order_id')}, - ) - await self._send_signal_notification( - market_signal, decision, current_price, - prefix="[Hyperliquid]", - hl_order_status=order_status - ) - if result.get('pending_tp_sl'): - order_id = str(result.get('order_id') or '') - pending_tp_sl = result.get('pending_tp_sl') or {} - tracking_key = order_id or f"{symbol}:{datetime.now().timestamp()}" - signal_action = decision.get('signal_action', decision.get('action')) - self._hl_pending_tp_sl[tracking_key] = self._build_pending_tp_sl_task( - symbol=symbol.replace('USDT', ''), - is_long=signal_action == 'buy', - size=result.get('position_size') or result.get('size') or 0, - tp_price=pending_tp_sl.get('tp_price'), - sl_price=pending_tp_sl.get('sl_price'), - order_status=order_status, - has_real_order_id=bool(order_id), - ) - logger.info(f" 📌 已记录 Hyperliquid TP/SL 待补设任务 (key={tracking_key})") - if result.get('tp_sl_warning'): - await self._notify_hyperliquid_error(symbol, "设置止盈止损", result['tp_sl_warning']) - else: - error = result.get('error', '未知错误') - logger.error(f" ❌ Hyperliquid 交易失败: {error}") - self._record_execution_event("Hyperliquid", "open_failed", decision=decision, reason=error, status="error") - await self._notify_hyperliquid_error(symbol, decision_type, error) - # 执行平仓 - elif decision_type == 'CLOSE': - logger.info(f" 准备 Hyperliquid 平仓...") - result = await executor.execute_close(decision, current_price) - if result.get('success'): - logger.info(f" ✅ Hyperliquid 平仓成功") - decision['_execution_succeeded'] = True - self._record_execution_event("Hyperliquid", "close_success", decision=decision, status="success") - if next_decision: - await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price) - else: - error = result.get('error', '未知错误') - logger.error(f" ❌ Hyperliquid 平仓失败: {error}") - self._record_execution_event("Hyperliquid", "close_failed", decision=decision, reason=error, status="error") - await self._notify_hyperliquid_error(symbol, "平仓", error) - # 执行撤单 - elif decision_type == 'CANCEL_PENDING': - logger.info(f" 准备取消 Hyperliquid 挂单...") - orders_to_cancel = decision.get('orders_to_cancel', []) - success_count = 0 if orders_to_cancel else 0 - for order_info in orders_to_cancel: - order_id = order_info if isinstance(order_info, str) else order_info.get('order_id', '') - result = await executor.execute_cancel(order_id, symbol) - if result.get('success'): - success_count += 1 - if success_count > 0: - logger.info(f" ✅ Hyperliquid 取消成功: {success_count} 个") - decision['_execution_succeeded'] = True - self._record_execution_event( - "Hyperliquid", "cancel_success", decision=decision, status="success", - extra={"cancelled_count": success_count}, - ) - if next_decision: - await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price) - else: - error = "没有成功取消任何挂单" - logger.error(f" ❌ Hyperliquid 取消失败: {error}") - self._record_execution_event("Hyperliquid", "cancel_failed", decision=decision, reason=error, status="error") - await self._notify_hyperliquid_error(symbol, "取消挂单", error) - else: - logger.warning(f" ⚠️ Hyperliquid 暂不支持的执行动作: {decision_type}") - self._record_execution_event("Hyperliquid", "unsupported_decision", decision=decision, reason=f"暂不支持的执行动作: {decision_type}", status="warning") - except Exception as e: - logger.error(f" ❌ Hyperliquid 执行异常: {e}") - self._record_execution_event("Hyperliquid", "exception", decision=decision, reason=str(e), status="error") - await self._notify_hyperliquid_error(symbol, decision_type, str(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 - action = decision.get('action', '') # buy/sell - entry_type = decision.get('entry_type', 'market') - entry_price = decision.get('entry_price', current_price) - is_buy = (action == 'buy') # 修复:用 action 字段判断方向 - - # 计算仓位大小(基于可用保证金和风控) - 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=is_buy, - size=size - ) - else: # limit - result = self.hyperliquid.place_limit_order( - symbol=symbol, - is_buy=is_buy, - size=size, - price=entry_price - ) - - # 如果开仓成功,处理止盈止损 + 验证订单实际状态 - if result.get('success'): - order_status = result.get('order_status', 'filled') # market单默认filled - - # 限价单:如果立即成交(filled),验证持仓是否存在 - if entry_type == 'limit' and order_status == 'filled': - position = self.hyperliquid.get_position_for_symbol(symbol) - if position: - logger.info(f" ✅ 限价单立即成交,持仓确认: {symbol} size={position['size']}") - else: - logger.warning(f" ⚠️ 限价单显示 filled 但未查到持仓,可能已被平仓或数据延迟") - - # 限价单:如果仍在挂单中(resting),验证订单是否在挂单列表 - elif entry_type == 'limit' and order_status == 'resting': - order_id = result.get('order_id') - open_orders = self.hyperliquid.get_open_orders(symbol) - if any(o.get('order_id') == order_id for o in open_orders): - logger.info(f" ✅ 限价单已挂出并确认可见: oid={order_id}") - else: - logger.warning(f" ⚠️ 限价单 oid={order_id} 未在挂单列表中查到(可能已成交或延迟)") - - # 将实际状态写回 result,供通知层使用 - result['verified_order_status'] = order_status - - tp_price = decision.get('take_profit') - sl_price = decision.get('stop_loss') - - if tp_price or sl_price: - # 只有已成交的订单才设置止盈止损(挂单中的不设,等成交后再设) - if order_status != 'resting': - tp_sl_result = self.hyperliquid.set_tp_sl( - symbol=symbol, - is_long=is_buy, - 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')}") - result['tp_sl_warning'] = tp_sl_result.get('error', '设置止盈止损失败') - else: - # 挂单中:记录下来,等下次循环检测成交后补设 - order_id = str(result.get('order_id', '')) - if order_id: - self._hl_pending_tp_sl[order_id] = { - 'symbol': symbol, - 'is_long': is_buy, - 'size': size, - 'tp_price': tp_price, - 'sl_price': sl_price, - } - logger.info(f" 📌 [Hyperliquid] 挂单 TP/SL 已记录 (oid={order_id}),等成交后补设") - - 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', '') - - # 清理该 symbol 的挂单 TP/SL 追踪记录 - self._hl_pending_tp_sl = {k: v for k, v in self._hl_pending_tp_sl.items() if v['symbol'] != symbol} - - # 先取消所有止盈止损订单 - 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', '') - # 清理该 symbol 的挂单 TP/SL 追踪记录 - self._hl_pending_tp_sl = {k: v for k, v in self._hl_pending_tp_sl.items() if v['symbol'] != symbol} - result = self.hyperliquid.cancel_all_orders(symbol) - return result - except Exception as e: - logger.error(f"Hyperliquid 取消挂单失败: {e}") - return {"success": False, "error": str(e)} - - async def _check_and_set_pending_tp_sl_hyperliquid(self): - """检查 Hyperliquid 挂单是否已成交,若成交则补设止盈止损""" - if self._is_platform_halted('Hyperliquid'): - return - if not self._hl_pending_tp_sl: - return - try: - for order_id, info in list(self._hl_pending_tp_sl.items()): - symbol = info['symbol'] - has_real_order_id = info.get('has_real_order_id', True) - if has_real_order_id: - open_orders = self.hyperliquid.get_open_orders(symbol) - still_open = any(str(o.get('order_id')) == order_id for o in open_orders) - else: - still_open = info.get('order_status') == 'resting' - if not still_open: - # 订单已不在挂单列表 → 已成交,补设 TP/SL - tp_price = info.get('tp_price') - sl_price = info.get('sl_price') - position = self.hyperliquid.get_position_for_symbol(symbol) - if not position: - logger.info(f"[Hyperliquid] 挂单/持仓 {order_id} ({symbol}) 当前无持仓,跳过 TP/SL 补设") - del self._hl_pending_tp_sl[order_id] - continue - size = info.get('size') or abs(position.get('size', 0)) - if size <= 0: - logger.warning(f"[Hyperliquid] 挂单/持仓 {order_id} ({symbol}) 数量无效,跳过 TP/SL 补设") - del self._hl_pending_tp_sl[order_id] - continue - logger.info(f"[Hyperliquid] 挂单 {order_id} ({symbol}) 已成交,补设 TP/SL...") - tp_sl_result = self.hyperliquid.set_tp_sl( - symbol=symbol, - is_long=info['is_long'], - size=size, - tp_price=tp_price, - sl_price=sl_price, - ) - info['retry_count'] = int(info.get('retry_count', 0)) + 1 - 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"[Hyperliquid] ✅ TP/SL 补设成功: {symbol} TP={tp_price} SL={sl_price}") - elif tp_set or sl_set: - missing_tp = tp_price if not tp_set else None - missing_sl = sl_price if not sl_set else None - self._hl_pending_tp_sl[order_id] = self._build_pending_tp_sl_task( - symbol=info['symbol'], - is_long=info['is_long'], - size=size, - tp_price=missing_tp, - sl_price=missing_sl, - order_status=info.get('order_status'), - has_real_order_id=info.get('has_real_order_id', True), - retry_count=info.get('retry_count', 0), - first_seen_at=info.get('first_seen_at'), - last_alert_at=info.get('last_alert_at'), - ) - set_text = "TP" if tp_set else "SL" - fail_text = "TP" if not tp_set else "SL" - logger.warning(f"[Hyperliquid] ⚠️ TP/SL 部分成功: {symbol} {set_text}已设, {fail_text}待下轮补设") - await self._maybe_alert_tp_sl_incomplete( - "Hyperliquid", - order_id, - self._hl_pending_tp_sl[order_id], - f"{set_text}已设,{fail_text}补设失败", - ) - continue - else: - logger.warning(f"[Hyperliquid] ⚠️ TP/SL 补设失败: {tp_sl_result.get('errors') or tp_sl_result.get('error')}") - await self._maybe_alert_tp_sl_incomplete( - "Hyperliquid", - order_id, - info, - str(tp_sl_result.get('errors') or tp_sl_result.get('error') or 'TP/SL补设失败'), - ) - continue - del self._hl_pending_tp_sl[order_id] - except Exception as e: - logger.error(f"[Hyperliquid] 检查挂单 TP/SL 补设异常: {e}") - async def _check_and_set_pending_tp_sl_bitget(self): """检查 Bitget 挂单是否已成交,若成交则补设止盈止损""" if self._is_platform_halted('Bitget'): @@ -4626,163 +4110,6 @@ class CryptoAgent: except Exception as e: logger.error(f"[Bitget] 止盈止损兜底检查异常: {e}") - async def _check_hyperliquid_missing_tp_sl(self): - """定时检查 Hyperliquid 持仓是否缺少止盈止损,缺少则从最新信号补救""" - if not self.hyperliquid: - return - try: - positions = self.hyperliquid.get_open_positions() - if not positions: - return - - for pos in positions: - symbol = pos.get('symbol', '') - if not symbol: - continue - - coin = symbol.replace('USDT', '') - tp_sl = self.hyperliquid.get_tp_sl_prices(coin) - has_tp = tp_sl.get('take_profit') is not None - has_sl = tp_sl.get('stop_loss') is not None - - if has_tp and has_sl: - continue - - latest_signal = self.signal_db.get_latest_signal('crypto', symbol) - if not latest_signal: - missing = ('止盈' if not has_tp else '') + ('/' if not has_tp and not has_sl else '') + ('止损' if not has_sl else '') - logger.warning(f"[Hyperliquid] ⚠️ {symbol} 缺少{missing},且无历史信号可补救") - continue - - tp_price = latest_signal.get('take_profit') - sl_price = latest_signal.get('stop_loss') - - if not tp_price and not sl_price: - logger.warning(f"[Hyperliquid] ⚠️ {symbol} 缺少止盈止损,最近信号也无 TP/SL") - continue - - set_tp = tp_price if not has_tp else None - set_sl = sl_price if not has_sl else None - - missing_parts = [] - if not has_tp: - missing_parts.append(f"TP={set_tp}") - if not has_sl: - missing_parts.append(f"SL={set_sl}") - logger.warning(f"[Hyperliquid] 🔧 {symbol} 缺少 {' & '.join(missing_parts)},从信号补救...") - - size = abs(pos.get('size', 0)) - if size <= 0: - continue - - tp_sl_result = self.hyperliquid.set_tp_sl( - symbol=coin, - is_long=pos.get('size', 0) > 0, - size=size, - tp_price=set_tp, - sl_price=set_sl, - ) - - 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"[Hyperliquid] ✅ 补救成功: {symbol} {' & '.join(set_parts)}") - else: - logger.warning(f"[Hyperliquid] ⚠️ 补救失败: {tp_sl_result.get('errors') or tp_sl_result.get('error')}") - await self._maybe_alert_tp_sl_incomplete( - "Hyperliquid", - f"fallback:{symbol}", - self._build_pending_tp_sl_task( - symbol=coin, - is_long=pos.get('size', 0) > 0, - size=size, - tp_price=set_tp, - sl_price=set_sl, - retry_count=self.TP_SL_RETRY_ALERT_THRESHOLD, - ), - str(tp_sl_result.get('errors') or tp_sl_result.get('error') or '兜底补设失败'), - force=True, - ) - - except Exception as e: - logger.error(f"[Hyperliquid] 止盈止损兜底检查异常: {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 - - # 按 Hyperliquid 要求的精度截断(szDecimals) - # ETH=3位, BTC=5位 等,必须截断而非四舍五入,避免超出允许仓位 - sz_decimals = self.hyperliquid.get_sz_decimals(decision.get('symbol', '').replace('USDT', '')) - factor = 10 ** sz_decimals - size = math.floor(size * factor) / factor # 截断,不四舍五入 - - # 最小下单量检查 - min_size = 1 / factor - if size < min_size: - logger.warning(f"⚠️ 计算仓位 {size} 低于最小下单量 {min_size},取消开仓") - return 0 - - 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}))") @@ -4966,21 +4293,6 @@ class CryptoAgent: paper_pending, ) if paper_signal else {"decision": "HOLD", "action": "IGNORE", "reason": "无适配信号", "reasoning": "无适配信号"} - if self.hyperliquid: - hl_positions, hl_account, hl_pending = self._get_hyperliquid_trading_state() - hl_signal = self._select_signal_for_platform(valid_signals, 'Hyperliquid') - execution_preview['Hyperliquid'] = self._normalize_execution_decision( - self.execute_signal_with_rules( - self._build_execution_signal(symbol, hl_signal, current_price), - 'Hyperliquid', - hl_account, - hl_positions, - hl_pending, - ), - hl_positions, - hl_pending, - ) if hl_signal else {"decision": "HOLD", "action": "IGNORE", "reason": "无适配信号", "reasoning": "无适配信号"} - if self.bitget: bg_positions, bg_account, bg_pending = self._get_bitget_trading_state() bg_signal = self._select_signal_for_platform(valid_signals, 'Bitget') @@ -5064,8 +4376,6 @@ class CryptoAgent: pending_orders = self.paper_trading.get_open_orders() elif platform_name == 'Bitget': pending_orders = self.bitget.get_open_orders() if self.bitget else [] - elif platform_name == 'Hyperliquid': - pending_orders = self.hyperliquid.get_open_orders() if self.hyperliquid else [] else: continue @@ -5323,8 +4633,6 @@ class CryptoAgent: positions = self.paper_trading.get_open_positions() elif platform_name == 'Bitget': positions = self.bitget.get_open_positions() if self.bitget else [] - elif platform_name == 'Hyperliquid': - positions = self.hyperliquid.get_open_positions() if self.hyperliquid else [] else: continue @@ -5426,8 +4734,6 @@ class CryptoAgent: platforms_to_check.append(('PaperTrading', self.paper_trading)) if self.bitget: platforms_to_check.append(('Bitget', self.bitget)) - if self.hyperliquid: - platforms_to_check.append(('Hyperliquid', self.hyperliquid)) return platforms_to_check def _load_platform_halts(self): @@ -5500,7 +4806,7 @@ class CryptoAgent: def get_platform_halt_status(self) -> Dict[str, Any]: result = {} - for platform_name in ['PaperTrading', 'Bitget', 'Hyperliquid']: + for platform_name in ['PaperTrading', 'Bitget']: info = self._platform_halts.get(platform_name, {}) result[platform_name] = { 'halted': bool(info.get('halted')), @@ -5513,14 +4819,13 @@ class CryptoAgent: return result def resume_platform(self, platform_name: str) -> Dict[str, Any]: - valid_platforms = {'PaperTrading', 'Bitget', 'Hyperliquid'} + valid_platforms = {'PaperTrading', 'Bitget'} if platform_name not in valid_platforms: raise ValueError(f"不支持的平台: {platform_name}") platform_service = { 'PaperTrading': self.paper_trading, 'Bitget': self.bitget, - 'Hyperliquid': self.hyperliquid, }.get(platform_name) if not platform_service: diff --git a/backend/app/crypto_agent/executor/CODE_REVIEW_2026-03-28.md b/backend/app/crypto_agent/executor/CODE_REVIEW_2026-03-28.md deleted file mode 100644 index 60cedab..0000000 --- a/backend/app/crypto_agent/executor/CODE_REVIEW_2026-03-28.md +++ /dev/null @@ -1,456 +0,0 @@ -# Code Review Report - 超激进配置和账户止损 -DATE: 2026-03-28 -REVIEWER: Claude Code Agent - -## 📋 审查范围 - -1. 超激进仓位配置(20%/15%/8%) -2. 账户级止损功能(25%止损,15%警告) -3. 统一杠杆配置(10x) -4. 移动止损功能 -5. 飞书通知集成 - ---- - -## 🔴 严重问题 (Critical Issues) - -### 1. **配置不一致:A级信号实际只能用10%而非20%** - -**位置**: `crypto_agent.py:2165, 2143` - -**问题**: -```python -# Line 2143: A级信号配置 -base_margin_pct = 0.20 # A级: 20% - -# Line 2165: 平台最大限制 -max_margin_pct = rules.get('max_margin_pct', 0.1) # 10% - -# Line 2175: 实际会被截断 -if margin > max_margin: - margin = max_margin # $200 → $100 -``` - -**影响**: -- A级信号期望用20%仓位,实际被限制到10% -- B级信号15%也会被截断到10% -- **文档说明与实际不符** - -**建议修复**: -```python -# 方案1: 提高平台最大限制(超激进) -max_margin_pct = rules.get('max_margin_pct', 0.25) # 改为25% - -# 方案2: 调整基础配置与限制对齐 -if confidence >= 90: - base_margin_pct = 0.10 # A级: 10% (与限制对齐) -elif confidence >= 70: - base_margin_pct = 0.10 # B级: 10% -else: - base_margin_pct = 0.08 # C级: 8% -``` - -**优先级**: 🔴 **高** - 配置与文档不一致 - ---- - -### 2. **紧急平仓方法调用可能缺少await** - -**位置**: `crypto_agent.py:3656-3659` - -**问题**: -```python -# 这些方法可能是同步的,但在async函数中 -if hasattr(platform_service, 'market_close_position'): - result = platform_service.market_close_position(symbol) # ❌ 缺少await? -elif hasattr(platform_service, 'close_position'): - result = platform_service.close_position(symbol) # ❌ 缺少await? -``` - -**检查**: -- Bitget `market_close_position()` - 需要检查是否是async -- Hyperliquid `close_position()` - 需要检查是否是async -- PaperTrading `close_position()` - 可能是同步方法 - -**风险**: -- 如果是异步方法但没有await,会导致协程未执行 -- 紧急平仓失败,无法止损 - -**建议修复**: -```python -# 修复方案:检查并正确调用 -try: - if hasattr(platform_service, 'market_close_position'): - method = platform_service.market_close_position - if asyncio.iscoroutinefunction(method): - result = await method(symbol) - else: - result = method(symbol) - elif hasattr(platform_service, 'close_position'): - method = platform_service.close_position - if asyncio.iscoroutinefunction(method): - result = await method(symbol) - else: - result = method(symbol) -except Exception as e: - logger.error(f"调用平仓方法异常: {e}") - result = {'success': False, 'error': str(e)} -``` - -**优先级**: 🔴 **高** - 可能导致止损失败 - ---- - -### 3. **初始余额获取逻辑有缺陷** - -**位置**: `crypto_agent.py:3571` - -**问题**: -```python -initial_balance = account_state.get('initial_balance', - account_state.get('current_balance', 0)) -``` - -**场景分析**: -``` -场景1: 正常情况 -account_state = {'initial_balance': 10000, 'current_balance': 8500} -→ initial_balance = 10000 ✅ - -场景2: 没有initial_balance字段(Bitget实盘) -account_state = {'current_balance': 10000, 'available': 9000} -→ initial_balance = 10000 (fallback到current_balance) -→ current_balance = 10000 -→ drawdown = 0 ❌ 无法检测回撤! - -场景3: 第一次运行(模拟盘) -account_state = {'initial_balance': 10000, 'balance': 10000} -→ initial_balance = 10000 -→ current_balance = 0 (字段名不匹配) -→ drawdown计算失败 ❌ -``` - -**影响**: -- **Bitget/Hyperliquid实盘可能无法检测回撤** -- 首次运行时initial_balance字段可能不存在 -- 字段名不一致导致获取失败 - -**建议修复**: -```python -# 方案1: 使用配置中的初始余额(推荐) -initial_balance = account_state.get('initial_balance', - self.settings.paper_trading_initial_balance) # 从配置读取 - -# 方案2: 记录并持久化初始余额 -if not hasattr(self, '_initial_balances'): - self._initial_balances = {} - -if platform_name not in self._initial_balances: - # 第一次运行,记录初始余额 - self._initial_balances[platform_name] = current_balance - # 持久化到文件 - self._save_initial_balances() - -initial_balance = self._initial_balances[platform_name] - -# 方案3: 统一字段名获取 -initial_balance = ( - account_state.get('initial_balance') or - account_state.get('start_balance') or - self._get_persisted_initial_balance(platform_name) or - current_balance # 最后的fallback -) -``` - -**优先级**: 🔴 **高** - 账户止损可能失效 - ---- - -## ⚠️ 警告问题 (Warnings) - -### 4. **杠杆限制逻辑冗余** - -**位置**: `crypto_agent.py:2178-2188` - -**问题**: -```python -# 这里已经限制了最大保证金为balance * 10% -max_margin = balance * max_margin_pct # max_margin_pct = 0.1 -if margin > max_margin: - margin = max_margin - -# 下面又根据剩余杠杆再次限制 -max_margin_by_leverage = balance * remaining_leverage -if margin > max_margin_by_leverage: - margin = max_margin_by_leverage -``` - -**分析**: -- `max_margin_pct = 0.1` 已经限制了单笔最多10% -- `remaining_leverage = 10 - current` 最大10x -- 两层限制可能导致过度保守 - -**示例**: -``` -balance = $1000 -current_leverage = 5x -remaining = 5x - -Layer 1: max_margin = $1000 × 10% = $100 -Layer 2: max_margin_by_leverage = $1000 × 5 = $5000 - -实际: margin = min($100, $5000) = $100 - -问题: Layer 2 永远不会生效(因为Layer 1已经限制了) -``` - -**建议**: -```python -# 方案1: 只保留杠杆限制,去掉max_margin_pct -# 因为10x杠杆已经隐含了单笔最多100%的风险 - -# 方案2: 调整max_margin_pct为更合理的值 -max_margin_pct = rules.get('max_margin_pct', 0.25) # 25% - -# 方案3: 明确说明两层限制的目的 -# Layer 1: 平台风控限制(单笔不超过余额的10%) -# Layer 2: 杠杆空间限制(总杠杆不超过10x) -``` - -**优先级**: ⚠️ **中** - 逻辑冗余但不影响功能 - ---- - -### 5. **移动止损触发条件可能过于简单** - -**位置**: `base_executor.py:224-251` - -**问题**: -```python -# 规则3: 移动止损 -if pnl_pct >= 2: - current_sl = pos.get('stop_loss') - if side == 'buy' and current_sl and current_sl < entry_price: - actions.append({ - 'symbol': symbol, - 'action': 'MOVE_SL', - 'new_sl': entry_price, - 'reason': f"盈利 {pnl_pct:.1f}% >= 2%,移动止损到入场价", - 'priority': 3 - }) -``` - -**缺陷**: -1. **没有考虑波动率**: 高波动币种2%盈利很常见,低波动币种2%可能很罕见 -2. **没有考虑持仓时间**: 刚开仓就2% vs 持仓3天2%含义不同 -3. **一次性移动**: 从-3%直接移动到0%,跨度较大 -4. **未考虑市场状态**: 震荡市vs趋势市应该不同策略 - -**建议增强**: -```python -def check_position_management(self, positions, current_prices): - """持仓管理检查(增强版)""" - actions = [] - - for pos in positions: - pnl_pct = ... # 计算盈亏 - hold_hours = ... # 计算持仓时长 - volatility = self._get_symbol_volatility(symbol) # 获取波动率 - - # 根据波动率调整阈值 - if volatility > 0.05: # 高波动 - move_sl_threshold = 3.0 # 需要盈利3%才移动 - elif volatility < 0.02: # 低波动 - move_sl_threshold = 1.5 # 盈利1.5%就移动 - else: - move_sl_threshold = 2.0 # 标准阈值 - - # 分阶段移动止损 - if pnl_pct >= move_sl_threshold * 1.5: - # 大幅盈利,移动到+1% - actions.append({ - 'action': 'MOVE_SL', - 'new_sl': entry_price * 1.01, - 'reason': f"盈利 {pnl_pct:.1f}% >= {move_sl_threshold*1.5:.1f}%,移动止损到+1%" - }) - elif pnl_pct >= move_sl_threshold: - # 达到阈值,移动到保本 - actions.append({ - 'action': 'MOVE_SL', - 'new_sl': entry_price, - 'reason': f"盈利 {pnl_pct:.1f}% >= {move_sl_threshold:.1f}%,移动止损到保本" - }) -``` - -**优先级**: ⚠️ **中** - 可以后续优化 - ---- - -### 6. **飞书通知缺少失败重试** - -**位置**: `base_executor.py:457-495` - -**问题**: -```python -async def send_execution_notification(self, operation, symbol, result, details): - if not self.feishu: - return - - try: - # 直接发送,无重试 - await self._send_open_notification(...) - except Exception as e: - logger.error(f"发送执行通知失败: {e}") - # ❌ 没有重试,通知丢失 -``` - -**影响**: -- 网络抖动导致通知失败 -- 无法知道关键交易执行情况 -- 飞书API限流时通知丢失 - -**建议修复**: -```python -async def send_execution_notification(self, operation, symbol, result, details): - if not self.feishu: - return - - max_retries = 3 - for attempt in range(max_retries): - try: - await self._send_open_notification(...) - break # 成功则退出 - except Exception as e: - if attempt < max_retries - 1: - await asyncio.sleep(1 * (attempt + 1)) # 指数退避 - logger.warning(f"通知发送失败,重试 {attempt+1}/{max_retries}: {e}") - else: - # 最后一次失败,记录到本地 - logger.error(f"通知发送失败(已重试{max_retries}次): {e}") - self._save_failed_notification(operation, symbol, result, details) -``` - -**优先级**: ⚠️ **中** - 影响监控但不影响交易 - ---- - -## ✅ 优秀设计 (Good Practices) - -### 1. **账户级止损设计合理** - -**优点**: -- ✅ 统一的止损检查逻辑(所有平台共用) -- ✅ 分级告警(15%警告,25%止损) -- ✅ 紧急平仓机制 -- ✅ 完整的日志记录 - -### 2. **飞书通知设计良好** - -**优点**: -- ✅ 统一的通知入口 `send_execution_notification()` -- ✅ 根据操作类型分发到不同方法 -- ✅ 格式化的卡片消息 -- ✅ 成功/失败都有通知 - -### 3. **移动止损抽象合理** - -**优点**: -- ✅ 基类定义抽象方法 -- ✅ 各平台独立实现 -- ✅ 统一的触发逻辑 -- ✅ 优先级排序 - ---- - -## 📊 测试建议 - -### 单元测试 -```python -def test_account_stop_loss(): - """测试账户级止损""" - # 1. 测试回撤15%触发警告 - # 2. 测试回撤25%触发止损 - # 3. 测试紧急平仓 - # 4. 测试initial_balance获取 - -def test_position_sizing(): - """测试仓位计算""" - # 1. 测试A级信号20%被限制到10% - # 2. 测试杠杆限制 - # 3. 测试最小保证金限制 - # 4. 测试可用余额不足 - -def test_move_stop_loss(): - """测试移动止损""" - # 1. 测试2%盈利触发 - # 2. 测试各平台实现 - # 3. 测试飞书通知 -``` - -### 集成测试 -```python -async def test_emergency_close_integration(): - """测试紧急平仓完整流程""" - # 1. 创建测试仓位 - # 2. 触发25%回撤 - # 3. 验证平仓执行 - # 4. 验证通知发送 - # 5. 验证系统停止 -``` - ---- - -## 🔧 修复优先级 - -| 优先级 | 问题 | 影响 | 工作量 | -|-------|------|------|-------| -| 🔴 **P0** | 配置不一致(20%→10%) | 文档与实际不符 | 低 | -| 🔴 **P0** | 紧急平仓缺少await | 止损可能失败 | 中 | -| 🔴 **P0** | 初始余额获取缺陷 | 回撤检测失效 | 中 | -| ⚠️ **P1** | 杠杆限制冗余 | 逻辑混乱 | 低 | -| ⚠️ **P1** | 移动止损过于简单 | 优化空间 | 中 | -| ⚠️ **P2** | 通知缺少重试 | 监控遗漏 | 低 | - ---- - -## 📝 建议行动计划 - -### 第一阶段(立即修复)- P0问题 - -1. **修复配置不一致** - ```bash - # 选择方案1:提高max_margin_pct - # 或方案2:调整base_margin_pct与限制对齐 - ``` - -2. **修复紧急平仓await** - ```python - # 添加asyncio.iscoroutinefunction检查 - ``` - -3. **修复初始余额获取** - ```python - # 使用配置中的初始余额或持久化 - ``` - -### 第二阶段(优化)- P1问题 - -4. **简化杠杆限制逻辑** -5. **增强移动止损策略** -6. **添加通知重试机制** - -### 第三阶段(测试) - -7. **编写单元测试** -8. **编写集成测试** -9. **模拟盘验证** -10. **小资金实盘测试** - ---- - -## 📚 相关文档 - -- [超激进配置详解](./AGGRESSIVE_CONFIG.md) -- [账户止损说明](./ACCOUNT_STOP_LOSS.md) - 需要创建 -- [移动止损功能](./MOVE_STOP_LOSS_FEATURE.md) -- [飞书通知集成](./NOTIFICATION_FEATURE.md) diff --git a/backend/app/crypto_agent/executor/EXECUTOR_OPTIMIZATION_SUMMARY.md b/backend/app/crypto_agent/executor/EXECUTOR_OPTIMIZATION_SUMMARY.md deleted file mode 100644 index 604d7b4..0000000 --- a/backend/app/crypto_agent/executor/EXECUTOR_OPTIMIZATION_SUMMARY.md +++ /dev/null @@ -1,292 +0,0 @@ -# 交易执行器优化总结 - -## ✅ 完成的优化 - -### 1. 创建执行器系统架构 - -**新建文件**: -- `app/crypto_agent/executor/base_executor.py` - 执行器基类(300+ 行) -- `app/crypto_agent/executor/paper_trading_executor.py` - 模拟盘执行器 -- `app/crypto_agent/executor/bitget_executor.py` - Bitget 执行器 -- `app/crypto_agent/executor/hyperliquid_executor.py` - Hyperliquid 执行器 -- `app/crypto_agent/executor/__init__.py` - 包初始化 - -### 2. 核心优化功能 - -#### **订单类型决策** ⭐⭐⭐ -- **市价单 vs 限价单智能选择** -- 价格差 < 阈值 → 市价单(立即成交) -- 价格差 >= 阈值 → 限价单(挂单等待) - -| 平台 | 市价阈值 | -|------|---------| -| 模拟盘 | 0.15% | -| Hyperliquid | 0.1% | -| Bitget | 0.2% | - -#### **止盈止损设置** ⭐⭐⭐ -- **Hyperliquid**: 下单时直接设置 TP/SL ✅ -- **Bitget**: 成交后单独设置 TP/SL -- **模拟盘**: 下单时设置(模拟) - -#### **挂单超时管理** ⭐⭐ -- **自动取消超时挂单** -- 模拟盘: 4 小时 -- Hyperliquid: 4 小时 -- Bitget: 24 小时 - -#### **持仓管理** ⭐⭐ -- **自动止盈**: 达到目标盈利自动平仓 - - 模拟盘/Bitget: 3% - - Hyperliquid: 2.5% -- **持仓超时平仓**: 持仓过长自动平仓 - - 模拟盘/Hyperliquid: 4 小时 - - Bitget: 6 小时 -- **移动止损**: 盈利 >= 2% 时,止损移到入场价 - -#### **交易成本预留** ⭐⭐ -- **自动预留手续费** -- 计算保证金时预留开仓+平仓手续费 -- 不会因手续费不足导致失败 - -#### **API 限流感知** ⭐ -- **智能重试机制** -- 检测限流错误 → 指数退避 + 随机抖动 -- 最大重试: 5 次 -- Bitget 限流严格,特殊处理 - -#### **挂单价格优化** ⭐ -- **动态更新挂单价格** -- 新价格更优(差值 >= 0.5%)→ 取消旧单,下新单 -- 做多:价格更低更优 -- 做空:价格更高更优 - ---- - -## 📊 平台特性对比表 - -| 特性 | 模拟盘 | Hyperliquid | Bitget | -|------|--------|-------------|--------| -| **市价单阈值** | 0.15% | 0.1% | 0.2% | -| **TP/SL 设置** | ✅ 下单时 | ✅ **下单时** | ❌ 成交后 | -| **挂单超时** | 4h | 4h | 24h | -| **止盈规则** | 3% | 2.5% | 3% | -| **持仓时长** | 4h | 4h | 6h | -| **手续费率** | 0% | 0.05% | 0.06% | -| **限流等待** | - | 指数退避 | **指数+抖动** | -| **价格更新阈值** | 0.3% | 0.5% | 0.5% | -| **最大重试** | 3 | 5 | 5 | - ---- - -## 🔄 修改的核心方法 - -### `crypto_agent.py` 中的修改 - -#### **1. 初始化执行器** (`__init__`) -```python -# 初始化平台执行器 -self.executors = { - 'PaperTrading': PaperTradingExecutor(), - 'Bitget': BitgetExecutor(), - 'Hyperliquid': HyperliquidExecutor(), -} -``` - -#### **2. 简化执行方法** - -**`_execute_paper_decisions`**: -```python -# 之前: 100+ 行复杂逻辑 -# 现在: 使用执行器,30 行 -executor = self.executors.get('PaperTrading') -result = await executor.execute_open(decision, current_price) -``` - -**`_execute_bitget_decisions`**: -```python -# 之前: 150+ 行,包含合约张数计算、杠杆设置、TP/SL 等 -# 现在: 使用执行器,30 行 -executor = self.executors.get('Bitget') -result = await executor.execute_open(decision, current_price) -``` - -**`_execute_hyperliquid_decisions`**: -```python -# 之前: 80+ 行 -# 现在: 使用执行器,40 行 -executor = self.executors.get('Hyperliquid') -result = await executor.execute_open(decision, current_price) -``` - -#### **3. 主循环中添加定期检查** - -```python -# 每轮循环开始时 -async def run(self): - while self.running: - # 1. 检查挂单超时 - await self._check_pending_order_timeouts() - - # 2. 检查持仓管理(止盈/止损/移动止损) - await self._check_position_management_all_platforms() - - # 3. 原有的 TP/SL 补设 - if self.hyperliquid: - await self._check_and_set_pending_tp_sl_hyperliquid() - if self.bitget: - await self._check_and_set_pending_tp_sl_bitget() -``` - ---- - -## 🚀 部署和测试 - -### **部署步骤** - -1. **提交代码** -```bash -cd /path/to/Stock_Agent -git add backend/app/crypto_agent/ -git commit -m "feat: 交易执行器优化 - 订单类型/止盈止损/超时管理/持仓管理" -git push -``` - -2. **重启服务** -```bash -# systemctl -sudo systemctl restart stock-agent - -# 或 docker -docker-compose restart - -# 或 pm2 -pm2 restart stock-agent -``` - -### **测试计划** - -#### **第1周:模拟盘测试** -```bash -PAPER_TRADING_ENABLED=true -BITGET_TRADING_ENABLED=false -HYPERLIQUID_ENABLED=false -``` - -**验证项**: -- ✅ 订单类型决策(市价 vs 限价) -- ✅ 止盈止损自动设置 -- ✅ 挂单超时自动取消 -- ✅ 持仓自动止盈 -- ✅ 移动止损 - -#### **第2周:Bitget 小仓位测试** -```bash -BITGET_TRADING_ENABLED=true -# 使用小仓位(最小保证金) -``` - -**验证项**: -- ✅ API 限流处理 -- ✅ 成交后 TP/SL 设置 -- ✅ 手续费预留 -- ✅ 挂单价格优化 - -#### **第3周:Hyperliquid 测试** -```bash -HYPERLIQUID_ENABLED=true -``` - -**验证项**: -- ✅ 下单时设置 TP/SL -- ✅ 流动性好,市价单阈值低 - -#### **第4周:全量部署** -```bash -# 所有平台启用 -PAPER_TRADING_ENABLED=true -BITGET_TRADING_ENABLED=true -HYPERLIQUID_ENABLED=true -``` - ---- - -## 📈 预期效果 - -### **性能提升** -- ✅ **代码简化**: 执行方法从 100+ 行 → 30 行 -- ✅ **维护性**: 平台逻辑独立封装,易于维护 -- ✅ **扩展性**: 新增平台只需继承 `BaseExecutor` - -### **风控加强** -- ✅ **订单类型优化**: 避免不必要的挂单 -- ✅ **止盈止损可靠**: Hyperliquid 下单时设置,避免漏设 -- ✅ **超时管理**: 自动取消过期挂单,释放保证金 -- ✅ **持仓管理**: 自动止盈、移动止损 - -### **稳定性提升** -- ✅ **限流处理**: 智能 API 重试,避免频繁失败 -- ✅ **成本预留**: 手续费预留,避免余额不足 -- ✅ **价格优化**: 动态调整挂单价格 - ---- - -## 📝 日志示例 - -### **订单类型决策** -``` -🎯 [Bitget] 处理交易信号: buy BTCUSDT - 订单类型: 价格差 0.05% < 0.2%,使用市价单 - ✅ OPEN: 正常开仓, 保证金 $85.00 -``` - -### **挂单超时取消** -``` -⏰ [Bitget] 挂单超时 - 交易对: ETH - 订单ID: 123456789 - 挂单时长: 24.1 小时 - ✅ 已取消超时挂单: 123456789 -``` - -### **自动止盈** -``` -📊 [Bitget] BTCUSDT 盈利 3.2% >= 3% - ✅ 自动止盈成功: BTCUSDT -``` - -### **移动止损** -``` -📊 [Bitget] ETHUSDT 盈利 2.5% >= 2%,移动止损到入场价 - ✅ 建议移动止损: ETHUSDT → $3520.00 -``` - ---- - -## 🎯 总结 - -**交易执行器系统已完全集成!** - -✅ **8 个优化全部实现**: -1. ✅ 订单类型决策(市价/限价) -2. ✅ 止盈止损设置逻辑 -3. ✅ 挂单超时管理 -4. ✅ 持仓管理(止盈/超时/移动止损) -5. ✅ 交易成本预留 -6. ✅ API 限流感知 -7. ✅ 挂单价格优化 -8. ✅ 平台差异化配置 - -**代码质量**: -- ✅ 无语法错误 -- ✅ 架构清晰 -- ✅ 易于维护 -- ✅ 易于扩展 - -**建议部署顺序**: -1. 第1周:模拟盘全面测试 -2. 第2周:Bitget 小仓位测试 -3. 第3周:Hyperliquid 测试 -4. 第4周:全量部署 - -**下一步**:部署到服务器,开始第1周模拟盘测试!🚀 diff --git a/backend/app/crypto_agent/executor/FEATURE_SUMMARY.md b/backend/app/crypto_agent/executor/FEATURE_SUMMARY.md deleted file mode 100644 index a1c5478..0000000 --- a/backend/app/crypto_agent/executor/FEATURE_SUMMARY.md +++ /dev/null @@ -1,272 +0,0 @@ -# 执行器移动止损 + 飞书通知完成总结 -CREATED: 2026-03-28 - -## ✅ 已完成的功能 - -### 1. 飞书通知集成 - -所有交易执行操作现在都会自动发送飞书通知: - -#### 修改的文件 -- [base_executor.py](./base_executor.py) - 添加基类通知方法 -- [bitget_executor.py](./bitget_executor.py) - Bitget 平台集成 -- [hyperliquid_executor.py](./hyperliquid_executor.py) - Hyperliquid 平台集成 -- [paper_trading_executor.py](./paper_trading_executor.py) - 模拟盘集成 - -#### 通知类型 -| 操作 | 成功通知 | 失败通知 | -|------|---------|---------| -| **开仓** (OPEN) | ✅ 绿色卡片 | ❌ 红色卡片 | -| **平仓** (CLOSE) | ✅ 绿色卡片(含盈亏) | ❌ 红色卡片 | -| **撤单** (CANCEL) | ✅ 绿色卡片 | ❌ 红色卡片 | -| **止盈止损** (TP_SL) | ✅ 绿色卡片 | ⚠️ 橙色卡片 | -| **持仓管理** (POSITION_MANAGEMENT) | 🔵 蓝色卡片 | - | - -#### 通知内容 -``` -标题: ✅ [Bitget] 开仓成功 - BTC -内容: - 平台: Bitget - 交易对: BTC - 订单ID: 1234567890 - 数量: 1 张 - 价格: $85,000.00 - 保证金: $170.00 - 杠杆: 5x - 止损: $82,000.00 - 止盈: $88,000.00 - 订单类型: limit -``` - -详细文档: [NOTIFICATION_FEATURE.md](./NOTIFICATION_FEATURE.md) - ---- - -### 2. 移动止损功能 - -所有平台已实现智能移动止损,当持仓盈利达到 2% 时自动将止损移动到入场价。 - -#### 修改的文件 -- [base_executor.py](./base_executor.py) - 添加抽象方法 `move_stop_loss()` -- [bitget_executor.py](./bitget_executor.py) - Bitget 平台实现 -- [hyperliquid_executor.py](./hyperliquid_executor.py) - Hyperliquid 平台实现 -- [paper_trading_executor.py](./paper_trading_executor.py) - 模拟盘实现 -- [crypto_agent.py](../crypto_agent.py) - 集成移动止损调用 - -#### 触发条件 -```python -if pnl_pct >= 2%: - if side == 'buy' and current_sl < entry_price: - # 做多:移动止损到入场价 - MOVE_SL → entry_price - elif side == 'sell' and current_sl > entry_price: - # 做空:移动止损到入场价 - MOVE_SL → entry_price -``` - -#### 平台配置 -| 平台 | 目标盈利 | 最大持仓 | 移动止损触发 | 移动到 | -|------|---------|---------|------------|-------| -| **Bitget** | 3% | 6h | 盈利 >= 2% | 入场价 | -| **Hyperliquid** | 2.5% | 4h | 盈利 >= 2% | 入场价 | -| **PaperTrading** | 3% | 4h | 盈利 >= 2% | 入场价 | - -#### 执行流程 -1. **检查**: `_check_position_management_all_platforms()` (每轮循环) -2. **触发**: 盈利达到 2% 且止损未移动 -3. **执行**: 调用 `executor.move_stop_loss()` -4. **通知**: - - 日志: `"✅ 移动止损成功: BTC → $85,000.00"` - - 告警: `"🔒 [Bitget] 移动止损"` - - 飞书: 蓝色卡片通知 - -详细文档: [MOVE_STOP_LOSS_FEATURE.md](./MOVE_STOP_LOSS_FEATURE.md) - ---- - -## 📂 新增文件 - -1. **NOTIFICATION_FEATURE.md** - 飞书通知功能完整文档 -2. **MOVE_STOP_LOSS_FEATURE.md** - 移动止损功能完整文档 -3. **FEISHU_NOTIFICATION_INTEGRATION.md** (用户创建) - 飞书集成说明 - ---- - -## 🔧 技术实现 - -### 基类新增方法 - -```python -# 1. 通知发送方法 -async def send_execution_notification(operation, symbol, result, details) - -# 2. 移动止损抽象方法 -@abstractmethod -async def move_stop_loss(symbol, new_stop_loss, current_stop_loss) -``` - -### 各平台实现 - -#### Bitget -```python -async def move_stop_loss(symbol, new_stop_loss, current_stop_loss): - success = self.bitget.modify_sl_tp( - symbol=symbol.replace('USDT', ''), - stop_loss=new_stop_loss - ) - return {'success': success} -``` - -**API**: `bitget_trading_api_sdk.modify_sl_tp(symbol, stop_loss, take_profit)` - -#### Hyperliquid -```python -async def move_stop_loss(symbol, new_stop_loss, current_stop_loss): - result = self.hyperliquid.set_tp_sl( - symbol=symbol.replace('USDT', ''), - sl_price=new_stop_loss - ) - return {'success': result.get('success', False)} -``` - -**API**: `hyperliquid_trading_service.set_tp_sl(symbol, sl_price)` - -#### PaperTrading -```python -async def move_stop_loss(symbol, new_stop_loss, current_stop_loss): - orders = self.paper_trading.get_active_orders(symbol) - success_count = 0 - - for order in orders: - result = self.paper_trading.update_order( - order_id=order.order_id, - stop_loss=new_stop_loss - ) - if result.get('success'): - success_count += 1 - - return {'success': success_count > 0} -``` - -**API**: `paper_trading_service.update_order(order_id, stop_loss)` - ---- - -## 🎯 使用示例 - -### 示例 1: Bitget 开仓 + 移动止损 -``` -1. 信号: BTC 做多, 置信度 85% (A级) -2. 决策: - - 保证金: $170 (3% of $1074) - - 杠杆: 5x - - 入场价: $85,000 - - 止损: $82,000 - - 止盈: $88,000 - -3. 执行: Bitget 开仓 1 张合约 - → 📢 飞书通知: ✅ [Bitget] 开仓成功 - BTC - -4. 价格上涨到 $87,000 (盈利 2.35%) - → 系统触发移动止损 - → 执行: 移动止损到 $85,000 - → 📢 飞书通知: 🔒 [Bitget] 移动止损 - 交易对: BTC - 新止损: $85,000.00 - 原因: 盈利 2.35% >= 2%,移动止损到入场价 -``` - -### 示例 2: Hyperliquid 平仓通知 -``` -1. 持仓: ETH 做多, 入场价 $3,500 -2. 当前价: $3,600 (盈利 2.86%) -3. 触发: 持仓管理检查 -4. 执行: 自动止盈平仓 - → 📢 飞书通知: ✅ [Hyperliquid] 平仓成功 - ETH - 平台: Hyperliquid - 交易对: ETH - 盈利: $100.00 - 收益率: 2.86% - 平仓原因: 盈利 2.86% >= 2.5% -``` - ---- - -## ⚠️ 注意事项 - -### 1. 飞书 Webhook 配置 -确保配置正确的飞书 Webhook URL: -```python -# settings.py -feishu_paper_trading_webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/..." -feishu_enabled = True -``` - -### 2. 止损设置 -- Bitget 和 Hyperliquid 需要在开仓时或开仓后设置止损 -- 移动止损只会修改已存在的止损订单 -- 如果没有初始止损,移动止损可能会失败 - -### 3. 异步调用 -所有执行器方法都是异步的,需要使用 `await`: -```python -result = await executor.move_stop_loss(symbol, new_sl) -await executor.send_execution_notification(operation, symbol, result) -``` - ---- - -## 📊 监控建议 - -### 关键指标 -1. **通知成功率**: 飞书通知发送成功率 -2. **移动止损触发次数**: 统计移动止损执行频率 -3. **移动止损效果**: 移动止损后最终盈亏情况 - -### 日志监控 -```bash -# 查看移动止损日志 -grep "移动止损成功" logs/crypto_agent.log - -# 查看飞书通知日志 -grep "飞书消息发送成功" logs/crypto_agent.log -``` - ---- - -## 🚀 下一步计划 - -### 1. 测试 -- [ ] 测试各平台飞书通知是否正常 -- [ ] 测试移动止损逻辑是否正确触发 -- [ ] 测试失败场景的通知 - -### 2. 优化 -- [ ] 根据实际使用情况调整移动止损触发阈值 -- [ ] 优化飞书通知的展示格式 -- [ ] 添加通知失败重试机制 - -### 3. 扩展 -- [ ] 支持更多平台(Binance, OKX 等) -- [ ] 添加邮件/短信通知渠道 -- [ ] 实现分级移动止损(2% 移到入场价, 4% 移到 +1% 等) - ---- - -## 📖 相关文档 - -- [仓位计算逻辑](./POSITION_SIZE_LOGIC.md) -- [飞书通知功能](./NOTIFICATION_FEATURE.md) -- [移动止损功能](./MOVE_STOP_LOSS_FEATURE.md) -- [执行器优化总结](./EXECUTOR_OPTIMIZATION_SUMMARY.md) - ---- - -## 📝 变更日志 - -### 2026-03-28 -- ✅ 为所有平台添加飞书通知功能 -- ✅ 实现移动止损功能(Bitget, Hyperliquid, PaperTrading) -- ✅ 集成持仓管理自动执行(crypto_agent.py) -- ✅ 创建完整文档 -- ✅ 修复 crypto_agent.py 中的移动止损调用逻辑 diff --git a/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_COMPLETE.md b/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_COMPLETE.md deleted file mode 100644 index c2fada8..0000000 --- a/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_COMPLETE.md +++ /dev/null @@ -1,305 +0,0 @@ -# 执行器飞书通知集成 - 完成报告 - -## ✅ 完成状态 - -**100% 完成** - 所有交易执行器已成功集成飞书通知功能 - ---- - -## 📦 改动的文件(4 个) - -| 文件 | 改动类型 | 说明 | -|------|---------|------| -| [base_executor.py](backend/app/crypto_agent/executor/base_executor.py) | ✅ 新增功能 | 添加飞书通知基类方法 | -| [paper_trading_executor.py](backend/app/crypto_agent/executor/paper_trading_executor.py) | ✅ 集成 | 在所有执行方法中添加通知 | -| [bitget_executor.py](backend/app/crypto_agent/executor/bitget_executor.py) | ✅ 集成 | 在所有执行方法中添加通知 | -| [hyperliquid_executor.py](backend/app/crypto_agent/executor/hyperliquid_executor.py) | ✅ 集成 | 在所有执行方法中添加通知 | - ---- - -## 🎯 新增功能 - -### 1. BaseExecutor - 飞书通知基类 - -**初始化飞书服务**: -```python -def __init__(self, platform_name: str): - self.platform_name = platform_name - # 初始化飞书通知服务 - try: - from app.services.feishu_service import get_feishu_paper_trading_service - self.feishu = get_feishu_paper_trading_service() - except Exception as e: - logger.warning(f"[{self.platform_name}] 飞书服务初始化失败: {e}") - self.feishu = None -``` - -**通知方法**: -| 方法 | 触发场景 | -|------|---------| -| `send_execution_notification()` | 统一入口 | -| `_send_open_notification()` | 开仓成功/失败 | -| `_send_close_notification()` | 平仓成功/失败 | -| `_send_cancel_notification()` | 撤单成功/失败 | -| `_send_tp_sl_notification()` | 止盈止损设置 | -| `_send_position_management_notification()` | 持仓管理操作 | -| `_send_generic_notification()` | 通用通知(兜底) | - ---- - -### 2. 通知触发场景 - -#### ✅ 开仓(OPEN) -- **成功**:绿色卡片,包含订单ID、数量、价格、保证金、杠杆、TP/SL -- **失败**:红色卡片,包含错误信息和失败原因 - -#### ✅ 平仓(CLOSE) -- **成功**:绿色卡片,包含盈亏金额、收益率、平仓原因 -- **失败**:红色卡片,包含错误信息 - -#### ✅ 撤单(CANCEL) -- **成功**:绿色卡片,包含订单ID -- **失败**:红色卡片,包含订单ID和错误信息 - -#### ✅ 止盈止损(TP_SL) -- **成功**:绿色卡片,包含止损价、止盈价 -- **失败**:橙色卡片,包含错误信息 - -#### ✅ 持仓管理(POSITION_MANAGEMENT) -- **自动止盈**:绿色卡片 -- **移动止损**:蓝色卡片 -- **超时平仓**:橙色卡片 - ---- - -## 📊 通知格式示例 - -### 开仓成功 -``` -✅ [Bitget] 开仓成功 - BTC - -**平台**: Bitget -**交易对**: BTC -**订单ID**: 123456789 -**数量**: 1 张 -**价格**: $85,000.00 -**保证金**: $170.00 -**杠杆**: 5x -**止损**: $83,000.00 -**止盈**: $88,000.00 -**订单类型**: limit -``` - -### 开仓失败 -``` -❌ [Bitget] 开仓失败 - BTC - -**平台**: Bitget -**交易对**: BTC -**错误**: 仓位计算结果 0 张,低于最小下单量 -**原因**: 计算仓位 0 张 < 1 张 -``` - -### 平仓成功 -``` -✅ [Hyperliquid] 平仓成功 - ETH - -**平台**: Hyperliquid -**交易对**: ETH -**盈利**: $125.50 -**收益率**: 2.5% -**平仓原因**: 自动止盈 -``` - -### TP/SL 设置失败 -``` -⚠️ [Bitget] 止盈止损设置失败 - BTC - -**平台**: Bitget -**交易对**: BTC -**错误**: API 限流,请稍后重试 -``` - ---- - -## 🔧 技术实现 - -### 调用方式 - -所有执行器通过基类的统一方法发送通知: - -```python -# 开仓成功 -await self.send_execution_notification( - operation='OPEN', - symbol=symbol, - result=result, - details={ - 'size': contracts, - 'price': entry_price, - 'margin': adjusted_margin, - 'leverage': leverage, - 'stop_loss': stop_loss, - 'take_profit': take_profit, - 'order_type': order_type - } -) -``` - -### 飞书 Webhook 配置 - -所有执行器使用 `paper_trading` webhook: -```env -FEISHU_PAPER_TRADING_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxx -FEISHU_ENABLED=true -``` - ---- - -## ✅ 验证结果 - -### 语法检查 -```bash -✅ base_executor.py 语法正确 -✅ paper_trading_executor.py 语法正确 -✅ bitget_executor.py 语法正确 -✅ hyperliquid_executor.py 语法正确 -``` - -### 集成检查 -- ✅ BaseExecutor 初始化飞书服务 -- ✅ Bitget 执行器调用通知方法(7 处) -- ✅ Hyperliquid 执行器调用通知方法(7 处) -- ✅ PaperTrading 执行器调用通知方法(6 处) - ---- - -## 🎯 核心优势 - -### 1. 实时监控 -- ✅ 每次交易操作都会即时推送到飞书 -- ✅ 无需登录平台即可了解交易状态 -- ✅ 支持移动端接收通知 - -### 2. 快速定位问题 -- ✅ 失败通知包含详细错误信息 -- ✅ 便于快速排查交易失败原因 -- ✅ 支持审计和回溯 - -### 3. 统一管理 -- ✅ 所有平台使用统一的通知格式 -- ✅ 基类封装,易于维护和扩展 -- ✅ 新增平台自动继承通知功能 - -### 4. 灵活配置 -- ✅ 支持不同平台使用不同 webhook -- ✅ 通知内容可自定义 -- ✅ 支持启用/禁用通知 - ---- - -## 📝 使用说明 - -### 配置飞书 Webhook - -1. 在飞书群组中添加自定义机器人 -2. 获取 Webhook URL -3. 配置环境变量: - ```bash - # .env - FEISHU_PAPER_TRADING_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxx - FEISHU_ENABLED=true - ``` - -### 重启服务 -```bash -sudo systemctl restart stock-agent -# 或 -docker-compose restart -``` - -### 验证通知 -- 触发一次交易操作 -- 检查飞书群是否收到对应的通知卡片 -- 检查通知内容是否正确 - ---- - -## 🚀 后续优化建议 - -### 1. 通知频率控制 -```python -# 避免短时间内大量通知 -# 可添加通知聚合或限流机制 -class NotificationRateLimiter: - def __init__(self, max_per_minute=10): - self.max_per_minute = max_per_minute - # ... -``` - -### 2. 通知级别区分 -```python -# 不同级别的通知使用不同 webhook -notification_levels = { - 'INFO': 'trading_webhook', # 开仓/平仓成功 - 'WARNING': 'warning_webhook', # TP/SL 设置失败 - 'ERROR': 'error_webhook', # 交易失败 - 'CRITICAL': 'alert_webhook' # 爆仓风险 -} -``` - -### 3. 个性化配置 -```python -# 允许用户选择接收哪些类型的通知 -user_preferences = { - 'notify_on_open': True, - 'notify_on_close': True, - 'notify_on_cancel': False, - 'notify_on_failure_only': False -} -``` - ---- - -## 📋 总结 - -**✅ 完成状态**:100% 完成,所有执行器已集成飞书通知 - -**✅ 影响范围**: -- 修改文件:4 个 -- 新增方法:7 个(基类) -- 集成位置:20+ 处通知调用 - -**✅ 核心价值**: -- 实时监控交易状态 -- 快速定位问题 -- 统一通知格式 -- 易于维护扩展 - -**✅ 兼容性**: -- 向后兼容,不影响现有功能 -- 支持开关配置(FEISHU_ENABLED) -- 自动降级(飞书服务初始化失败时不影响交易) - ---- - -## 🎉 部署就绪 - -所有代码已通过语法验证,可以直接部署使用! - -**部署步骤**: -1. 配置飞书 Webhook URL -2. 重启服务 -3. 触发交易验证通知 - -**监控方式**: -- 查看日志:`journalctl -u stock-agent -f` -- 检查飞书群通知 -- 验证通知内容完整性 - ---- - -**文档位置**: -- 详细说明:[FEISHU_NOTIFICATION_INTEGRATION.md](backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_INTEGRATION.md) -- 仓位逻辑:[POSITION_SIZE_LOGIC.md](backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC.md) -- 优化总结:[EXECUTOR_OPTIMIZATION_SUMMARY.md](backend/app/crypto_agent/executor/EXECUTOR_OPTIMIZATION_SUMMARY.md) diff --git a/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_INTEGRATION.md b/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_INTEGRATION.md deleted file mode 100644 index bc8e4ef..0000000 --- a/backend/app/crypto_agent/executor/FEISHU_NOTIFICATION_INTEGRATION.md +++ /dev/null @@ -1,345 +0,0 @@ -# 执行器飞书通知集成 - -## ✅ 改动概述 - -为所有交易执行器添加了飞书通知功能,确保每次交易执行结果(开仓、平仓、撤单、止盈止损设置)都会实时推送到飞书。 - ---- - -## 📦 改动的文件 - -### 1. `backend/app/crypto_agent/executor/base_executor.py` - -**新增功能**: - -#### 1.1 初始化飞书服务 -```python -def __init__(self, platform_name: str): - self.platform_name = platform_name - # 初始化飞书通知服务 - try: - from app.services.feishu_service import get_feishu_paper_trading_service - self.feishu = get_feishu_paper_trading_service() - except Exception as e: - logger.warning(f"[{self.platform_name}] 飞书服务初始化失败: {e}") - self.feishu = None -``` - -#### 1.2 通知方法 - -| 方法 | 用途 | -|------|------| -| `send_execution_notification()` | 统一通知入口,根据操作类型分发 | -| `_send_open_notification()` | 开仓成功/失败通知 | -| `_send_close_notification()` | 平仓成功/失败通知 | -| `_send_cancel_notification()` | 撤单成功/失败通知 | -| `_send_tp_sl_notification()` | 止盈止损设置通知 | -| `_send_position_management_notification()` | 持仓管理通知(自动止盈/移动止损) | -| `_send_generic_notification()` | 通用通知(兜底) | - -#### 1.3 通知格式 - -**开仓成功**: -``` -✅ [Bitget] 开仓成功 - BTC - -**平台**: Bitget -**交易对**: BTC -**订单ID**: 123456789 -**数量**: 1 张 -**价格**: $85,000.00 -**保证金**: $170.00 -**杠杆**: 5x -**止损**: $83,000.00 -**止盈**: $88,000.00 -**订单类型**: limit -``` - -**开仓失败**: -``` -❌ [Bitget] 开仓失败 - BTC - -**平台**: Bitget -**交易对**: BTC -**错误**: 仓位计算结果 0 张,低于最小下单量 -**原因**: 计算仓位 0 张 < 1 张 -``` - -**平仓成功**: -``` -✅ [Hyperliquid] 平仓成功 - ETH - -**平台**: Hyperliquid -**交易对**: ETH -**盈利**: $125.50 -**收益率**: 2.5% -**平仓原因**: 自动止盈 -``` - -**止盈止损设置失败**: -``` -⚠️ [Bitget] 止盈止损设置失败 - BTC - -**平台**: Bitget -**交易对**: BTC -**错误**: API 限流,请稍后重试 -``` - ---- - -### 2. `backend/app/crypto_agent/executor/bitget_executor.py` - -**集成位置**: -- `execute_open()` - 开仓成功/失败后发送通知 -- `execute_close()` - 平仓成功/失败后发送通知 -- `execute_cancel()` - 撤单成功/失败后发送通知 -- `set_stop_loss_take_profit()` - TP/SL 设置成功/失败后发送通知 - -**示例**: -```python -# 开仓成功 -await self.send_execution_notification( - operation='OPEN', - symbol=symbol, - result=result, - details={ - 'size': contracts, - 'price': entry_price, - 'margin': adjusted_margin, - 'leverage': leverage, - 'stop_loss': stop_loss, - 'take_profit': take_profit, - 'order_type': order_type - } -) -``` - ---- - -### 3. `backend/app/crypto_agent/executor/paper_trading_executor.py` - -**集成位置**: -- `execute_open()` - 开仓成功/失败后发送通知 -- `execute_close()` - 平仓成功/失败后发送通知(包含盈亏信息) -- `execute_cancel()` - 撤单成功/失败后发送通知 - -**特点**: -- 平仓通知包含盈亏金额和收益率 -- 开仓通知包含订单类型(市价/限价) - ---- - -### 4. `backend/app/crypto_agent/executor/hyperliquid_executor.py` - -**集成位置**: -- `execute_open()` - 开仓成功/失败后发送通知 -- `execute_close()` - 平仓成功/失败后发送通知 -- `execute_cancel()` - 撤单成功/失败后发送通知 -- `set_stop_loss_take_profit()` - TP/SL 设置通知 - -**特点**: -- Hyperliquid 支持下单时设置 TP/SL,通知中会体现 -- 平仓支持批量操作 - ---- - -## 🎯 通知触发场景 - -| 场景 | 操作类型 | 通知颜色 | 必须字段 | -|------|---------|---------|---------| -| 开仓成功 | `OPEN` | green | 平台、交易对、订单ID、数量、价格、保证金、杠杆 | -| 开仓失败 | `OPEN` | red | 平台、交易对、错误信息 | -| 平仓成功 | `CLOSE` | green | 平台、交易对、盈亏金额、收益率 | -| 平仓失败 | `CLOSE` | red | 平台、交易对、错误信息 | -| 撤单成功 | `CANCEL` | green | 平台、交易对、订单ID | -| 撤单失败 | `CANCEL` | red | 平台、交易对、订单ID、错误信息 | -| TP/SL 设置成功 | `TP_SL` | green | 平台、交易对、止损价、止盈价 | -| TP/SL 设置失败 | `TP_SL` | orange | 平台、交易对、错误信息 | -| 自动止盈 | `POSITION_MANAGEMENT` | green | 平台、交易对、操作、原因、盈亏 | -| 移动止损 | `POSITION_MANAGEMENT` | blue | 平台、交易对、操作、原因 | - ---- - -## 📊 飞书 Webhook 配置 - -所有执行器使用 `get_feishu_paper_trading_service()` 获取飞书服务实例,对应配置: - -```python -# .env -FEISHU_PAPER_TRADING_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxx -FEISHU_ENABLED=true -``` - -**说明**: -- 所有平台的执行通知统一发送到 `paper_trading` webhook -- 这是因为执行通知属于交易操作,与 `crypto` webhook(信号通知)区分 -- 如需为不同平台配置不同 webhook,可在执行器初始化时传入不同的 `service_type` - ---- - -## ✅ 测试验证 - -### 测试场景 - -#### 1. 开仓成功通知 -```python -# 触发条件:Bitget 开仓 1 张 BTC 合约 -# 预期:飞书收到绿色卡片,包含订单详情 -``` - -#### 2. 开仓失败通知 -```python -# 触发条件:仓位计算 < 1 张 -# 预期:飞书收到红色卡片,包含失败原因 -``` - -#### 3. 平仓成功通知 -```python -# 触发条件:Hyperliquid 平仓持仓 -# 预期:飞书收到绿色卡片,包含盈亏信息 -``` - -#### 4. 撤单通知 -```python -# 触发条件:取消挂单 -# 预期:飞书收到绿色/红色卡片,包含订单ID -``` - -#### 5. TP/SL 设置通知 -```python -# 触发条件:设置止盈止损 -# 预期:飞书收到绿色/橙色卡片,包含 TP/SL 价格 -``` - ---- - -## 🔍 日志示例 - -### 开仓成功 -``` -🎯 [Bitget] 处理交易信号: buy BTCUSDT - 订单类型: 价格差 0.05% < 0.2%,使用市价单 - 仓位计算: $170.00 USD → 0.010000 BTC → 1 张 - ✅ 开仓成功: BTC 1张 @ $market - 📤 飞书通知发送成功 -``` - -### 开仓失败 -``` -🎯 [Hyperliquid] 处理交易信号: buy ETHUSDT - 订单类型: 价格差 0.05% < 0.1%,使用市价单 - ❌ 开仓失败: 仓位计算失败: 0 - 📤 飞书通知发送成功 -``` - -### 平仓成功 -``` -📊 [Bitget] 检查持仓管理 - BTCUSDT 盈利 3.2% >= 3% - ✅ 自动止盈成功: BTCUSDT - 📤 飞书通知发送成功 -``` - ---- - -## 🎯 优势 - -### 1. 实时监控 -- ✅ 每次交易操作都会即时推送到飞书 -- ✅ 无需登录平台即可了解交易状态 -- ✅ 支持移动端接收通知 - -### 2. 问题快速定位 -- ✅ 失败通知包含详细错误信息 -- ✅ 便于快速排查交易失败原因 -- ✅ 支持审计和回溯 - -### 3. 统一管理 -- ✅ 所有平台使用统一的通知格式 -- ✅ 基类封装,易于维护和扩展 -- ✅ 新增平台自动继承通知功能 - -### 4. 灵活配置 -- ✅ 支持不同平台使用不同 webhook -- ✅ 通知内容可自定义 -- ✅ 支持启用/禁用通知 - ---- - -## 📝 后续优化建议 - -### 1. 通知频率控制 -```python -# 避免短时间内大量通知 -# 可添加通知聚合或限流机制 -``` - -### 2. 通知级别区分 -```python -# 不同级别的通知使用不同 webhook -# - INFO: 开仓/平仓成功 -# - WARNING: TP/SL 设置失败 -# - ERROR: 交易失败 -# - CRITICAL: 爆仓风险 -``` - -### 3. 多语言支持 -```python -# 根据用户配置切换中英文通知 -``` - -### 4. 通知内容定制 -```python -# 允许用户选择接收哪些类型的通知 -# - 只接收失败通知 -# - 只接收平仓通知 -# - 接收所有通知 -``` - ---- - -## 🚀 部署 - -### 1. 配置飞书 Webhook -```bash -# .env -FEISHU_PAPER_TRADING_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxx -FEISHU_ENABLED=true -``` - -### 2. 重启服务 -```bash -sudo systemctl restart stock-agent -# 或 -docker-compose restart -``` - -### 3. 验证通知 -```bash -# 触发一次交易,检查飞书群是否收到通知 -``` - ---- - -## 📋 总结 - -✅ **已完成**: -- BaseExecutor 集成飞书通知基类 -- Bitget 执行器完整通知集成 -- Hyperliquid 执行器完整通知集成 -- PaperTrading 执行器完整通知集成 -- 支持开仓/平仓/撤单/TP/SL 通知 -- 成功/失败不同颜色区分 -- 详细的通知内容(包含所有关键字段) - -✅ **核心优势**: -- 实时监控交易状态 -- 快速定位问题 -- 统一通知格式 -- 易于维护扩展 - -✅ **影响范围**: -- 3 个执行器文件(Bitget, Hyperliquid, PaperTrading) -- 1 个基类文件(BaseExecutor) -- 无需修改 crypto_agent.py -- 向后兼容,不影响现有功能 diff --git a/backend/app/crypto_agent/executor/FIX_REPORT_2026-03-28.md b/backend/app/crypto_agent/executor/FIX_REPORT_2026-03-28.md deleted file mode 100644 index 56786b9..0000000 --- a/backend/app/crypto_agent/executor/FIX_REPORT_2026-03-28.md +++ /dev/null @@ -1,397 +0,0 @@ -# P0 问题修复报告 -DATE: 2026-03-28 -STATUS: ✅ 已完成 - -## 📋 修复概览 - -| 问题 | 严重性 | 状态 | 文件 | -|------|--------|------|------| -| **配置不一致** (20%→10%) | 🔴 P0 | ✅ 已修复 | `crypto_agent.py:51,56,63` | -| **紧急平仓缺少await** | 🔴 P0 | ✅ 已修复 | `crypto_agent.py:3666-3671` | -| **初始余额获取缺陷** | 🔴 P0 | ✅ 已修复 | `crypto_agent.py:144-148, 3803-3870` | - ---- - -## 🔧 修复详情 - -### 1️⃣ 配置不一致:max_margin_pct 从 10% 提高到 25% - -**问题**: -- A级信号配置为 20% 保证金 -- 但被 `max_margin_pct = 0.1` 限制到 10% -- 文档与实际不符 - -**修复前**: -```python -# crypto_agent.py:51, 56, 63 -'Bitget': { - 'max_margin_pct': 0.1, # ❌ 限制到10% -}, -'PaperTrading': { - 'max_margin_pct': 0.05, # ❌ 限制到5% -}, -'Hyperliquid': { - 'max_margin_pct': 0.1, # ❌ 限制到10% -} -``` - -**修复后**: -```python -# crypto_agent.py:51, 56, 63 -'Bitget': { - 'max_margin_pct': 0.25, # ✅ 支持超激进配置25% -}, -'PaperTrading': { - 'max_margin_pct': 0.25, # ✅ 支持超激进配置25% -}, -'Hyperliquid': { - 'max_margin_pct': 0.25, # ✅ 支持超激进配置25% -} -``` - -**效果**: -| 信号等级 | 期望保证金 | 修复前实际 | 修复后实际 | -|---------|----------|----------|----------| -| **A级** (20%) | $200 | $100 ❌ | **$200** ✅ | -| **B级** (15%) | $150 | $100 ❌ | **$150** ✅ | -| **C级** (8%) | $80 | $50 ❌ | **$80** ✅ | - -**验证**: ✅ 现在 A级信号可以使用完整的 20% 保证金 - ---- - -### 2️⃣ 紧急平仓:添加 async/await 检查 - -**问题**: -- 紧急平仓方法可能是 async,但没有 await -- 导致协程未执行,平仓失败 -- 止损失效,风险极大 - -**修复前**: -```python -# crypto_agent.py:3656-3661 -if hasattr(platform_service, 'market_close_position'): - result = platform_service.market_close_position(symbol) # ❌ 缺少await -elif hasattr(platform_service, 'close_position'): - result = platform_service.close_position(symbol) # ❌ 缺少await -``` - -**修复后**: -```python -# crypto_agent.py:3656-3671 -# 获取平仓方法 -close_method = None -if hasattr(platform_service, 'market_close_position'): - close_method = platform_service.market_close_position -elif hasattr(platform_service, 'close_position'): - close_method = platform_service.close_position -else: - logger.warning(f"[{platform_name}] 无法平仓 {symbol}: 无平仓方法") - continue - -# 检查是否是async方法并正确调用 -import asyncio -if asyncio.iscoroutinefunction(close_method): - result = await close_method(symbol) # ✅ 正确await -else: - result = close_method(symbol) # ✅ 同步调用 -``` - -**效果**: -- ✅ 自动检测方法是同步还是异步 -- ✅ 正确调用,确保平仓执行 -- ✅ 紧急止损恢复功能 - -**验证**: ✅ 无论方法是 async 还是 sync,都能正确调用 - ---- - -### 3️⃣ 初始余额:实现持久化机制 - -**问题**: -- Bitget/Hyperliquid 没有 `initial_balance` 字段 -- fallback 到 `current_balance` → drawdown = 0 -- **回撤检测完全失效** -- **账户止损失效** - -**修复前**: -```python -# crypto_agent.py:3571-3572 -initial_balance = account_state.get('initial_balance', - account_state.get('current_balance', 0)) -# ❌ Bitget/Hyperliquid 没有 initial_balance 字段 -# ❌ 导致 initial = current → drawdown = 0 -``` - -**修复后**: - -**A. 添加持久化存储** (`crypto_agent.py:144-148`): -```python -# 状态管理 -self.last_signals: Dict[str, Dict[str, Any]] = {} -self.signal_cooldown: Dict[str, datetime] = {} - -# 账户初始余额持久化(用于计算回撤) -self._initial_balances: Dict[str, float] = {} -self._load_initial_balances() # 从文件加载 -``` - -**B. 实现加载方法** (`crypto_agent.py:3803-3820`): -```python -def _load_initial_balances(self): - """从文件加载初始余额""" - try: - import json - from pathlib import Path - - file_path = Path("data/initial_balances.json") - if file_path.exists(): - with open(file_path, 'r') as f: - self._initial_balances = json.load(f) - logger.info(f"📂 已加载初始余额: {self._initial_balances}") - else: - logger.info(f"📂 初始余额文件不存在,将在首次运行时创建") - self._initial_balances = {} - except Exception as e: - logger.error(f"加载初始余额失败: {e}") - self._initial_balances = {} -``` - -**C. 实现保存方法** (`crypto_agent.py:3822-3838`): -```python -def _save_initial_balances(self): - """保存初始余额到文件""" - try: - import json - from pathlib import Path - - # 确保目录存在 - Path("data").mkdir(exist_ok=True) - - file_path = Path("data/initial_balances.json") - with open(file_path, 'w') as f: - json.dump(self._initial_balances, f, indent=2) - - logger.info(f"💾 已保存初始余额: {self._initial_balances}") - except Exception as e: - logger.error(f"保存初始余额失败: {e}") -``` - -**D. 实现获取方法** (`crypto_agent.py:3840-3858`): -```python -def _get_initial_balance(self, platform_name: str, current_balance: float) -> float: - """ - 获取或设置平台的初始余额 - - Args: - platform_name: 平台名称 - current_balance: 当前余额 - - Returns: - 初始余额 - """ - if platform_name not in self._initial_balances: - # 第一次运行,记录当前余额作为初始余额 - self._initial_balances[platform_name] = current_balance - self._save_initial_balances() - logger.info(f"✨ [{platform_name}] 记录初始余额: ${current_balance:.2f}") - - return self._initial_balances[platform_name] -``` - -**E. 修改回撤计算** (`crypto_agent.py:3872-3881`): -```python -# 获取当前余额(统一字段名) -current_balance = ( - account_state.get('current_balance') or - account_state.get('balance') or - account_state.get('available_balance', 0) -) - -if current_balance <= 0: - logger.warning(f"[{platform_name}] 当前余额无效: {current_balance}") - continue - -# 获取或记录初始余额(使用持久化机制)✅ -initial_balance = self._get_initial_balance(platform_name, current_balance) - -# 计算回撤 -drawdown = (initial_balance - current_balance) / initial_balance -``` - -**效果**: -- ✅ 首次运行时记录初始余额到文件 -- ✅ 后续运行从文件加载初始余额 -- ✅ **回撤检测恢复功能** -- ✅ **账户止损恢复功能** - -**数据文件**: -```json -// data/initial_balances.json -{ - "模拟盘": 10000.0, - "Bitget": 1074.5, - "Hyperliquid": 1000.0 -} -``` - -**验证**: ✅ 即使 Bitget/Hyperliquid 不提供 initial_balance,也能正确计算回撤 - ---- - -## ✅ 修复验证 - -### 验证 1: 配置一致性 -```bash -# 测试 A级信号 -信号: BTC 做多, 置信度 92% (A级) -账户余额: $1000 -期望保证金: 20% = $200 - -✅ 修复后实际: $200 (受25%限制,不截断) -❌ 修复前实际: $100 (被10%限制截断) -``` - -### 验证 2: 紧急平仓 -```python -# 模拟触发止损 -drawdown = 26% > 25% -→ 执行紧急平仓 - -✅ 修复后: 检测async → await close_method(symbol) -❌ 修复前: 协程未await → 平仓失败 -``` - -### 验证 3: 初始余额 -```python -# Bitget 首次运行 -account_state = {'current_balance': 1074.5} # 没有 initial_balance - -✅ 修复后: - 1. 检测没有记录 → 保存 $1074.5 到 data/initial_balances.json - 2. 下次运行加载 $1074.5 - 3. 当前 $900 → drawdown = (1074.5 - 900) / 1074.5 = 16.2% ✅ - -❌ 修复前: - 1. initial = current = $1074.5 - 2. drawdown = 0 ❌ - 3. 止损永远不触发 ❌ -``` - ---- - -## 📊 测试建议 - -### 单元测试 -```python -def test_max_margin_pct(): - """测试保证金限制""" - agent = CryptoAgent() - rules = agent.PLATFORM_RULES['Bitget'] - assert rules['max_margin_pct'] == 0.25 # ✅ 25% - -def test_emergency_close_await(): - """测试紧急平仓async检测""" - # Mock async method - async def mock_close(symbol): - return {'success': True} - - # 验证await调用 - import asyncio - assert asyncio.iscoroutinefunction(mock_close) # ✅ - -def test_initial_balance_persistence(): - """测试初始余额持久化""" - agent = CryptoAgent() - - # 第一次获取(应该记录) - initial = agent._get_initial_balance('TestPlatform', 1000.0) - assert initial == 1000.0 # ✅ - - # 修改后再次获取(应该从文件加载) - agent._initial_balances = {} - agent._load_initial_balances() - initial = agent._get_initial_balance('TestPlatform', 900.0) - assert initial == 1000.0 # ✅ 仍然是初始值 -``` - -### 集成测试 -```python -async def test_account_stop_loss_integration(): - """测试账户级止损完整流程""" - # 1. 初始化账户 - agent = CryptoAgent() - platform_name = 'PaperTrading' - - # 2. 记录初始余额 - initial_balance = 10000.0 - agent._get_initial_balance(platform_name, initial_balance) - - # 3. 模拟亏损到25%回撤 - current_balance = 7500.0 # -25% - drawdown = (initial_balance - current_balance) / initial_balance - - # 4. 验证触发止损 - assert drawdown >= 0.25 # ✅ - - # 5. 执行紧急平仓 - await agent._emergency_close_all_positions(platform_name, agent.paper_trading) - - # 6. 验证系统停止 - assert not agent.running # ✅ -``` - ---- - -## 🎯 后续行动 - -### 已完成 ✅ -- [x] 修复配置不一致(max_margin_pct 25%) -- [x] 修复紧急平仓await问题 -- [x] 修复初始余额持久化 -- [x] 创建修复文档 - -### 待完成 📋 -- [ ] 编写单元测试 -- [ ] 编写集成测试 -- [ ] 模拟盘测试验证 -- [ ] 实盘小资金测试 -- [ ] 监控实际回撤数据 - -### 优化建议 💡 -- [ ] P1: 简化杠杆限制逻辑 -- [ ] P1: 增强移动止损策略 -- [ ] P2: 添加通知重试机制 - ---- - -## 📝 相关文档 - -- [Code Review报告](./CODE_REVIEW_2026-03-28.md) -- [超激进配置详解](./AGGRESSIVE_CONFIG.md) -- [账户止损说明](./ACCOUNT_STOP_LOSS.md) - 待创建 -- [配置更新总结](./CONFIG_UPDATE_2026-03-28.md) - ---- - -## 🚀 部署检查清单 - -部署前必须确认: -- [x] `data/` 目录有写权限(用于持久化初始余额) -- [x] `max_margin_pct = 0.25` 配置生效 -- [x] 紧急平仓方法正确调用(async/sync) -- [x] 初始余额文件 `data/initial_balances.json` 会自动创建 - -部署后立即验证: -- [ ] 检查 `data/initial_balances.json` 是否创建 -- [ ] 检查日志中初始余额记录 -- [ ] 测试警告阈值(15%)是否触发 -- [ ] 测试移动止损是否正常工作 -- [ ] 监控实际保证金使用比例 - ---- - -**修复完成时间**: 2026-03-28 -**修复工程师**: Claude Code Agent -**测试状态**: ⚠️ 待测试 -**部署状态**: ⏳ 待部署 diff --git a/backend/app/crypto_agent/executor/LEVERAGE_CONFIGURATION.md b/backend/app/crypto_agent/executor/LEVERAGE_CONFIGURATION.md deleted file mode 100644 index 832a6b6..0000000 --- a/backend/app/crypto_agent/executor/LEVERAGE_CONFIGURATION.md +++ /dev/null @@ -1,280 +0,0 @@ -# 杠杆配置总结 -UPDATED: 2026-03-28 - -## 配置变更 - -### ✅ 已完成的修改 - -| 配置项 | 修改前 | 修改后 | 文件 | -|--------|--------|--------|------| -| **PaperTrading 单笔杠杆** | 20x | **10x** | `config.py:133` | -| **Bitget 默认杠杆** | 5x | **10x** | `crypto_agent.py:2580` | -| **Hyperliquid 默认杠杆** | 10x | **10x** (不变) | `crypto_agent.py:2884` | -| **总杠杆上限** | 10x | **10x** (不变) | `config.py` | - ---- - -## 当前配置 - -### 杠杆配置总览 - -| 平台 | 单笔杠杆 | 总杠杆上限 | 说明 | -|------|---------|----------|------| -| **PaperTrading** | 10x | 10x | 模拟盘统一10x杠杆 | -| **Bitget** | 10x | 10x | 实盘默认10x,可动态调整 | -| **Hyperliquid** | 10x | 10x | 实盘默认10x,最大10x | - -### 配置文件位置 - -```python -# config.py -class Settings(BaseSettings): - # 模拟交易 - paper_trading_leverage: int = 10 # ✅ 从20改为10 - paper_trading_max_total_leverage: float = 10 # 总杠杆上限 - - # Bitget 实盘 - bitget_max_total_leverage: float = 10 # 总杠杆上限 - - # Hyperliquid 实盘 - hyperliquid_max_total_leverage: float = 10 # 总杠杆上限. -``` - -### 代码实现 - -#### Bitget 开仓 -```python -# crypto_agent.py:2579-2581 -# 设置杠杆 (默认10x,最大10x) -leverage = min(decision.get('leverage', 10), 10) -self.bitget.update_leverage(symbol, leverage) -``` - -#### Hyperliquid 开仓 -```python -# crypto_agent.py:2884-2885 -leverage = min(decision.get('leverage', 10), 10) -self.hyperliquid.update_leverage(symbol, leverage) -``` - -#### PaperTrading 开仓 -```python -# paper_trading_service.py 使用配置中的杠杆 -self.leverage = settings.paper_trading_leverage # 10x -``` - ---- - -## 杠杆控制逻辑 - -### 单笔杠杆限制 - -```python -# 确保单笔杠杆不超过10x -leverage = min(decision.get('leverage', 10), 10) -``` - -### 总杠杆控制 - -系统会实时监控总杠杆使用情况: - -```python -# 计算当前总杠杆 -total_position_value = sum(all_positions_value) -current_total_leverage = total_position_value / account_balance - -# 检查是否还能开仓 -remaining_leverage = max_total_leverage - current_total_leverage - -if remaining_leverage <= 0: - # 已达最大杠杆,无法开仓 - return 0, f"已达最大杠杆 {current_leverage:.1f}x/{max_total_leverage}x" - -# 限制新开仓的杠杆 -max_new_position = account_balance * remaining_leverage -``` - ---- - -## 实际案例 - -### 案例 1: Bitget $1074 账户, A级信号 -``` -账户余额: $1,074 -可用余额: $1,074 -当前总杠杆: 0x (无持仓) - -信号置信度: 92% (A级) -保证金比例: 10% (Kelly公式型) -保证金: $1,074 × 10% = $107.40 - -单笔杠杆: 10x -持仓价值: $107.40 × 10 = $1,074 -当前总杠杆: $1,074 / $1,074 = 1.0x ✅ - -剩余可用杠杆: 10x - 1.0x = 9.0x -剩余可开仓金额: $1,074 × 9.0 = $9,666 -``` - -### 案例 2: Hyperliquid $2000 账户, 已有持仓 -``` -账户余额: $2,000 -当前持仓价值: $8,000 (已用杠杆 4x) -当前总杠杆: $8,000 / $2,000 = 4.0x - -新信号 (B级): -保证金比例: 6% (Kelly公式型) -保证金: $2,000 × 6% = $120 - -单笔杠杆: 10x -新持仓价值: $120 × 10 = $1,200 - -开仓后总杠杆: ($8,000 + $1,200) / $2,000 = 4.6x ✅ (< 10x) - -剩余可用杠杆: 10x - 4.6x = 5.4x -``` - -### 案例 3: 杠杆超限,拒绝开仓 -``` -账户余额: $1,000 -当前持仓价值: $9,500 (已用杠杆 9.5x) -当前总杠杆: $9,500 / $1,000 = 9.5x - -新信号 (A级): -保证金: $100 -期望杠杆: 10x -期望持仓价值: $1,000 - -预计总杠杆: ($9,500 + $1,000) / $1,000 = 10.5x ❌ (> 10x) - -系统拒绝: "已达最大杠杆 9.5x/10x" -建议: 先平仓部分持仓 -``` - ---- - -## Kelly公式型 + 10x杠杆的威力 - -### 资金利用效率对比 - -| 场景 | 3%仓位+5x杠杆 | 10%仓位+10x杠杆 | 倍数 | -|------|--------------|----------------|------| -| **$1000账户 A级信号** | $150 仓位 | $1000 仓位 | **6.7x** | -| **盈利 3%** | +$4.5 | +$30 | **6.7x** | -| **亏损 3%** | -$4.5 | -$30 | **6.7x** | - -### 示例: 10次交易后的差异 - -假设: -- A级信号胜率: 60% -- 平均盈利: 4% -- 平均亏损: 3% -- $1000账户 - -**保守配置 (3% + 5x)**: -``` -盈利: 6次 × $150 × 4% = $36 -亏损: 4次 × $150 × 3% = -$18 -净收益: +$18 (1.8%) -总杠杆峰值: ~1.5x (非常安全) -``` - -**Kelly配置 (10% + 10x)**: -``` -盈利: 6次 × $1000 × 4% = $240 -亏损: 4次 × $1000 × 3% = -$120 -净收益: +$120 (12%) 🚀 -总杠杆峰值: ~6x (风险可控) -``` - -**收益差异: 12% vs 1.8% = 6.7倍** - ---- - -## 风险管理 - -### 单笔最大亏损(-3%止损) - -| 信号等级 | 保证金 | 杠杆 | 持仓价值 | -3%亏损 | 占总资金% | -|---------|--------|-----|---------|---------|----------| -| **A级** | $100 | 10x | $1,000 | -$30 | **-3.0%** | -| **B级** | $60 | 10x | $600 | -$18 | **-1.8%** | -| **C级** | $20 | 10x | $200 | -$6 | **-0.6%** | - -### 最大回撤控制 - -- **单笔最大亏损**: -3% (A级信号止损) -- **连续5次亏损**: -15% (极端情况) -- **建议**: 设置账户级止损 -20% - ---- - -## 监控指标 - -### 实时监控 - -```python -# 每轮循环检查 -current_total_leverage = total_position_value / account_balance - -if current_total_leverage > 8.0: - # 警告: 接近杠杆上限 - logger.warning(f"⚠️ 总杠杆 {current_leverage:.1f}x,接近上限10x") - -if current_total_leverage >= 10.0: - # 紧急: 达到杠杆上限 - logger.error(f"🚨 已达最大杠杆 {current_leverage:.1f}x/10x") -``` - -### 告警通知 - -- **飞书通知**: 杠杆 > 8x 时发送告警 -- **日志记录**: 每次开仓记录杠杆变化 -- **自动拒绝**: 杠杆满时自动拒绝新信号 - ---- - -## 配置建议 - -### 保守型(推荐新手) -```python -paper_trading_leverage: int = 5 # 或保持10x但降低仓位比例 -base_margin_pct = 0.05 # A级5% -``` - -### 平衡型(当前配置) -```python -paper_trading_leverage: int = 10 -base_margin_pct = 0.10 # A级10% -``` - -### 激进型(仅限经验丰富) -```python -paper_trading_leverage: int = 10 -base_margin_pct = 0.15 # A级15% -max_total_leverage: float = 15 # ⚠️ 极高风险 -``` - ---- - -## 检查清单 - -- [x] PaperTrading 杠杆从20x改为10x -- [x] Bitget 默认杠杆从5x改为10x -- [x] Hyperliquid 默认杠杆保持10x -- [x] 总杠杆上限保持10x -- [x] 更新配置文档 -- [x] 创建杠杆控制文档 - -- [ ] 回测验证新配置效果 -- [ ] 实盘测试并监控 -- [ ] 根据实际表现微调 - ---- - -## 相关文档 - -- [仓位配置策略](./POSITION_SIZING_STRATEGY.md) -- [仓位计算逻辑](./POSITION_SIZE_LOGIC.md) -- [移动止损功能](./MOVE_STOP_LOSS_FEATURE.md) -- [功能完成总结](./FEATURE_SUMMARY.md) diff --git a/backend/app/crypto_agent/executor/MOVE_STOP_LOSS_FEATURE.md b/backend/app/crypto_agent/executor/MOVE_STOP_LOSS_FEATURE.md deleted file mode 100644 index ceccfbf..0000000 --- a/backend/app/crypto_agent/executor/MOVE_STOP_LOSS_FEATURE.md +++ /dev/null @@ -1,321 +0,0 @@ -# 移动止损功能文档 -CREATED: 2026-03-28 - -## 功能概述 - -所有平台(Bitget、Hyperliquid、PaperTrading)均已实现**移动止损**功能。当持仓盈利达到 2% 或以上时,系统会自动将止损价移动到入场价,锁定盈亏并降低风险。 - -## 触发条件 - -在 [base_executor.py](./base_executor.py) 的 `check_position_management()` 方法中定义: - -```python -# 规则3: 移动止损 -if pnl_pct >= 2: - current_sl = pos.get('stop_loss') - if side == 'buy' and current_sl and current_sl < entry_price: - actions.append({ - 'symbol': symbol, - 'action': 'MOVE_SL', - 'new_sl': entry_price, - 'reason': f"盈利 {pnl_pct:.1f}% >= 2%,移动止损到入场价", - 'priority': 3 - }) - elif side == 'sell' and current_sl and current_sl > entry_price: - actions.append({ - 'symbol': symbol, - 'action': 'MOVE_SL', - 'new_sl': entry_price, - 'reason': f"盈利 {pnl_pct:.1f}% >= 2%,移动止损到入场价", - 'priority': 3 - }) -``` - -### 条件详解 - -1. **盈利条件**: `pnl_pct >= 2%` (持仓盈利达到或超过2%) -2. **止损位置**: - - **做多** (`buy`): 当前止损 < 入场价 → 移动到入场价 - - **做空** (`sell`): 当前止损 > 入场价 → 移动到入场价 -3. **优先级**: 3 (低于 TAKE_PROFIT 和 TIME_EXIT) - -## 实现位置 - -### 1. 基类定义 ([base_executor.py:438](./base_executor.py#L438)) - -```python -@abstractmethod -async def move_stop_loss(self, - symbol: str, - new_stop_loss: float, - current_stop_loss: Optional[float] = None) -> Dict[str, Any]: - """ - 移动止损 - - Args: - symbol: 交易对 - new_stop_loss: 新的止损价 - current_stop_loss: 当前止损价(可选) - - Returns: - {'success': bool, 'message': str} - """ - pass -``` - -### 2. Bitget 实现 ([bitget_executor.py:300](./bitget_executor.py#L300)) - -```python -async def move_stop_loss(self, - symbol: str, - new_stop_loss: float, - current_stop_loss: Optional[float] = None) -> Dict[str, Any]: - """移动止损(Bitget)""" - try: - # Bitget 使用 modify_sl_tp 方法 - success = self.bitget.modify_sl_tp( - symbol=symbol.replace('USDT', ''), - stop_loss=new_stop_loss - ) - - if success: - logger.info(f" ✅ 移动止损成功: {symbol} → ${new_stop_loss:.2f}") - return {'success': True, 'message': f'移动止损成功: {new_stop_loss:.2f}'} - else: - return {'success': False, 'message': '移动止损失败'} - - except Exception as e: - logger.error(f"Bitget 移动止损失败: {e}") - return {'success': False, 'message': str(e)} -``` - -**API**: `bitget.modify_sl_tp(symbol, stop_loss, take_profit)` - -### 3. Hyperliquid 实现 ([hyperliquid_executor.py:295](./hyperliquid_executor.py#L295)) - -```python -async def move_stop_loss(self, - symbol: str, - new_stop_loss: float, - current_stop_loss: Optional[float] = None) -> Dict[str, Any]: - """移动止损(Hyperliquid)""" - try: - # Hyperliquid 使用 set_tp_sl 方法(只传 sl_price) - result = self.hyperliquid.set_tp_sl( - symbol=symbol.replace('USDT', ''), - sl_price=new_stop_loss - ) - - if result.get('success', False): - 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', '移动止损失败')} - - except Exception as e: - logger.error(f"Hyperliquid 移动止损失败: {e}") - return {'success': False, 'message': str(e)} -``` - -**API**: `hyperliquid.set_tp_sl(symbol, sl_price)` - -### 4. PaperTrading 实现 ([paper_trading_executor.py:263](./paper_trading_executor.py#L263)) - -```python -async def move_stop_loss(self, - symbol: str, - new_stop_loss: float, - current_stop_loss: Optional[float] = None) -> Dict[str, Any]: - """移动止损(模拟盘)""" - try: - # 查找该交易对的所有活跃订单 - orders = self.paper_trading.get_active_orders(symbol) - - if not orders: - return {'success': False, 'message': f'找不到 {symbol} 的活跃订单'} - - # 更新所有该交易对的订单止损价 - success_count = 0 - for order in orders: - update_result = self.paper_trading.update_order( - order_id=order.order_id, - stop_loss=new_stop_loss - ) - - if update_result.get('success'): - success_count += 1 - - if success_count > 0: - logger.info(f" ✅ 移动止损成功: {symbol} → ${new_stop_loss:.2f} ({success_count}/{len(orders)} 订单)") - return { - 'success': True, - 'message': f'移动止损成功: {new_stop_loss:.2f} ({success_count}/{len(orders)} 订单)' - } - else: - return {'success': False, 'message': '所有订单移动止损失败'} - - except Exception as e: - logger.error(f"模拟盘移动止损失败: {e}") - return {'success': False, 'message': str(e)} -``` - -**API**: `paper_trading.update_order(order_id, stop_loss)` - -## 执行流程 - -### 在 crypto_agent.py 中调用 - -```python -async def _check_position_management_all_platforms(self): - """检查所有平台的持仓管理""" - try: - for platform_name, executor in self.executors.items(): - # 获取持仓 - positions = await executor.get_positions() - if not positions: - continue - - # 获取当前价格 - symbols = [pos.get('symbol') for pos in positions] - current_prices = {} - for symbol in symbols: - price = await self._get_current_price(symbol) - current_prices[symbol] = price - - # 检查持仓管理 - actions = executor.check_position_management(positions, current_prices) - - # 执行建议的操作 - for action_info in actions: - symbol = action_info.get('symbol') - action = action_info.get('action') - reason = action_info.get('reason', '') - - if action == 'MOVE_SL': - new_sl = action_info.get('new_sl') - if new_sl: - # 调用执行器的移动止损方法 - move_result = await executor.move_stop_loss( - symbol=symbol, - new_stop_loss=new_sl - ) - - if move_result.get('success'): - logger.info(f" ✅ 移动止损成功: {symbol} → ${new_sl:.2f}") - await self._send_alert_notification( - f"🔒 [{platform_name}] 移动止损", - f"交易对: {symbol}\n新止损: ${new_sl:.2f}\n原因: {reason}" - ) - - # 发送飞书通知 - await executor.send_execution_notification( - operation='POSITION_MANAGEMENT', - symbol=symbol, - result={'success': True, 'action': 'MOVE_SL', 'reason': reason}, - details={'new_sl': new_sl, 'pnl_percent': pnl_pct} - ) - else: - logger.warning(f" ⚠️ 移动止损失败: {move_result.get('message')}") - - except Exception as e: - logger.error(f"检查持仓管理失败: {e}") -``` - -## 通知机制 - -### 1. 日志通知 -- ✅ 成功: `"✅ 移动止损成功: {symbol} → ${new_sl:.2f}"` -- ⚠️ 失败: `"⚠️ 移动止损失败: {message}"` - -### 2. 告警通知 -- 标题: `"🔒 [{platform_name}] 移动止损"` -- 内容: - ``` - 交易对: BTC - 新止损: $85,000.00 - 原因: 盈利 2.5% >= 2%,移动止损到入场价 - ``` - -### 3. 飞书通知 -- **操作类型**: `POSITION_MANAGEMENT` -- **操作**: `MOVE_SL` -- **颜色**: 蓝色 -- **详情**: - - 平台 - - 交易对 - - 操作: 移动止损 - - 原因 - - 新止损价 - - 盈亏百分比 - -## 配置参数 - -| 平台 | 目标盈利 | 最大持仓 | 移动止损触发 | 移动到 | -|------|---------|---------|------------|-------| -| **Bitget** | 3% | 6h | 盈利 >= 2% | 入场价 | -| **Hyperliquid** | 2.5% | 4h | 盈利 >= 2% | 入场价 | -| **PaperTrading** | 3% | 4h | 盈利 >= 2% | 入场价 | - -## 示例场景 - -### 场景 1: Bitget BTC 做多 -``` -入场价: $80,000 -当前价: $82,000 (盈利 2.5%) -当前止损: $78,000 - -触发条件: ✅ 盈利 2.5% >= 2% -执行动作: 移动止损到 $80,000 (入场价) -结果: 锁定盈亏,最坏情况保本退出 -``` - -### 场景 2: Hyperliquid ETH 做空 -``` -入场价: $3,500 -当前价: $3,400 (盈利 2.86%) -当前止损: $3,600 - -触发条件: ✅ 盈利 2.86% >= 2% -执行动作: 移动止损到 $3,500 (入场价) -结果: 锁定盈亏,最坏情况保本退出 -``` - -### 场景 3: PaperTrading SOL 做多 -``` -入场价: $140 -当前价: $145 (盈利 3.57%) -当前止损: $135 - -触发条件: ✅ 盈利 3.57% >= 2% -执行动作: 移动止损到 $140 (入场价) -结果: 锁定盈亏,最坏情况保本退出 -``` - -## 风险管理 - -### 优势 -1. **锁定盈亏**: 盈利 2% 后,最坏情况也能保本退出 -2. **降低心理压力**: 不用担心从盈利变亏损 -3. **自动化**: 无需人工干预,系统自动执行 - -### 注意事项 -1. **市场波动**: 剧烈波动可能导致止损被触发后价格反弹 -2. **止损距离**: 入场价作为止损可能较近,需结合市场情况 -3. **仓位大小**: 大仓位时移动止损更为重要 - -## 监控与调优 - -### 监控指标 -- 移动止损执行次数 -- 移动止损后价格反转次数 -- 移动止损触发后的最终盈亏 - -### 调优建议 -1. **触发阈值**: 可根据市场波动率调整(1.5% ~ 3%) -2. **移动位置**: 可移动到更保守的位置(如入场价 - 0.5%) -3. **分批移动**: 盈利 2% 移动到入场价,盈利 4% 移动到 +1% 等 - -## 相关文档 -- [仓位计算逻辑](./POSITION_SIZE_LOGIC.md) -- [飞书通知功能](./NOTIFICATION_FEATURE.md) -- [执行器优化总结](./EXECUTOR_OPTIMIZATION_SUMMARY.md) diff --git a/backend/app/crypto_agent/executor/NOTIFICATION_FEATURE.md b/backend/app/crypto_agent/executor/NOTIFICATION_FEATURE.md deleted file mode 100644 index ef023b4..0000000 --- a/backend/app/crypto_agent/executor/NOTIFICATION_FEATURE.md +++ /dev/null @@ -1,82 +0,0 @@ -# 执行器飞书通知功能 - -FIXED: 2026-03-28 - -## 功能概述 - -为所有交易执行器(Bitget、Hyperliquid、PaperTrading) 添加了飞书通知功能,每次执行交易操作时都会自动发送飞书通知。 - -## 实现位置 - -- **基类**: `backend/app/crypto_agent/executor/base_executor.py` - - 添加了飞书服务初始化 - - 添加了统一的通知发送方法 `send_execution_notification()` - - 添加了针对不同操作的通知方法: - - `_send_open_notification()` - 开仓通知 - - `_send_close_notification()` - 平仓通知 - - `_send_cancel_notification()` - 撤单通知 - - `_send_tp_sl_notification()` - 止盈止损设置通知 - - `_send_position_management_notification()` - 持仓管理通知 - - `_send_generic_notification()` - 通用通知 - -- **Bitget 执行器**: `backend/app/crypto_agent/executor/bitget_executor.py` - - 在 `execute_open()` 中添加成功/失败通知 - - 在 `execute_close()` 中添加成功/失败通知 - - 在 `execute_cancel()` 中添加成功/失败通知 - -- **Hyperliquid 执行器**: `backend/app/crypto_agent/executor/hyperliquid_executor.py` - - 在 `execute_open()` 中添加成功/失败通知 - - 在 `execute_close()` 中添加成功/失败通知 - - 在 `execute_cancel()` 中添加成功/失败通知 - -- **PaperTrading 执行器**: `backend/app/crypto_agent/executor/paper_trading_executor.py` - - 已经集成了飞书通知功能 - -## 通知类型 - -### 1. 开仓通知 (OPEN) -- **成功**: 绿色卡片,包含平台、交易对、订单ID、数量、价格、保证金、杠杆、止损、止盈、订单类型 -- **失败**: 红色卡片,包含平台、交易对、错误信息、失败原因 - -### 2. 平仓通知 (CLOSE) -- **成功**: 绿色卡片,包含平台、交易对、盈亏金额、收益率、平仓原因 -- **失败**: 红色卡片,包含平台、交易对、错误信息 - -### 3. 撤单通知 (CANCEL) -- **成功**: 绿色卡片,包含平台、交易对、订单ID、撤单原因 -- **失败**: 红色卡片,包含平台、交易对、订单ID、错误信息 - -### 4. 止盈止损通知 (TP_SL) -- **成功**: 绿色卡片,包含平台、交易对、止损价、止盈价 -- **失败**: 橙色卡片,包含平台、交易对、错误信息 - -### 5. 持仓管理通知 (POSITION_MANAGEMENT) -- **颜色**: 根据操作类型(TAKE_PROFIT=绿色, TIME_EXIT=橙色, MOVE_SL=蓝色) -- **内容**: 平台、交易对、操作类型、原因、盈亏百分比、持仓时长 - -**特别说明**: 移动止损 (MOVE_SL) 会在持仓盈利达到 2% 时自动触发,详见 [MOVE_STOP_LOSS_FEATURE.md](./MOVE_STOP_LOSS_FEATURE.md) - -## 通知格式 -使用飞书卡片消息(Interactive Card),格式如下: -``` -标题: [状态图标] [平台] 操作类型 - 交易对 -内容: -**平台**: XXX -**交易对**: XXX -**其他字段**: XXX -``` - -## 使用的服务 -- **飞书服务**: `get_feishu_paper_trading_service()` - - 使用 `paper_trading` 类型的飞书 webhook - - 确保交易通知发送到正确的飞书群组 - -## 通知时机 -- **立即发送**: 每次执行操作后立即发送通知 -- **成功/失败都发送**: 无论操作成功还是失败都会发送通知 -- **包含详情**: 尽可能包含更多执行详情,方便追踪和调试 - -## 下一步 -- 测试各个平台的通知是否正常发送 -- 磮认飞书 webhook 配置正确 -- 根据需要调整通知格式和内容 diff --git a/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC.md b/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC.md deleted file mode 100644 index d728972..0000000 --- a/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC.md +++ /dev/null @@ -1,300 +0,0 @@ -# 仓位大小计算逻辑(超激进配置) -UPDATED: 2026-03-28 - -## 📊 总体流程 - -``` -市场信号 (LLM) → 硬编码规则决策 → 保证金计算 → 平台执行器 → 下单 + 飞书通知 - ↓ ↓ ↓ ↓ - 置信度 75% 同向/反向检查 保证金金额 合约张数/币数量 -``` - ---- - -## 1️⃣ 决策层:保证金计算 (`crypto_agent.py`) - -### **超激进配置** 🔥 - -| 信号等级 | 置信度范围 | 保证金比例 | $1000账户保证金 | 10x杠杆仓位 | -|---------|----------|----------|---------------|-----------| -| **A级** | ≥90分 | **20%** | $200 | $2,000 | -| **B级** | 70-89分 | **15%** | $150 | $1,500 | -| **C级** | <70分 | **8%** | $80 | $800 | - -### **计算步骤** - -#### **Step 1: 根据信号等级确定基础保证金比例(超激进配置)** - -```python -if confidence >= 90: - base_margin_pct = 0.20 # A级: 20% (重仓出击) 🔥 -elif confidence >= 70: - base_margin_pct = 0.15 # B级: 15% (中仓跟进) 🔥 -else: - base_margin_pct = 0.08 # C级: 8% (轻仓试探) -``` - -#### **Step 2: 计算初始保证金** - -```python -margin = available * base_margin_pct - -# 示例: -# 可用余额: $1000 -# 信号等级: A级 (92分) -# margin = 1000 × 20% = $200 🔥 -``` - -#### **Step 3: 应用平台最小保证金限制** - -```python -# Bitget 最小保证金(各币种不同,考虑10x杠杆) -BTC: min_margin = $85 # 0.01 BTC × $85000 / 10x杠杆 -ETH: min_margin = $35 # 0.1 ETH × $3500 / 10x杠杆 -SOL: min_margin = $14 # 1 SOL × $140 / 10x杠杆 - -if margin < min_margin: - margin = min_margin -``` - -**示例**: -``` -计算保证金: $200 -BTC 最小保证金: $85 -调整后: margin = $200 (大于$85,不调整) ✅ -``` - -#### **Step 4: 应用最大保证金限制** - -```python -# 单笔不超过余额的 10% (所有平台) -max_margin = balance * 0.10 - -if margin > max_margin: - margin = max_margin -``` - -**示例**: -``` -计算保证金: $200 -账户余额: $1000 -最大限制: 10% -调整后: margin = $100 (不超过$100) ⚠️ -``` - -#### **Step 5: 应用杠杆限制** - -```python -remaining_leverage = max_leverage - current_leverage - -if remaining_leverage <= 0: - return 0, "已达最大杠杆" - -max_margin_by_leverage = balance * remaining_leverage -if margin > max_margin_by_leverage: - margin = max_margin_by_leverage -``` - -**示例**: -``` -计算保证金: $100 -账户余额: $1000 -当前杠杆: 0x -最大杠杆: 10x -剩余杠杆: 10x -调整后: margin = min($100, $1000 × 10 = $10000) = $100 ✅ -``` - -#### **Step 6: 确保不超过可用余额** - -```python -if margin > available: - margin = available * 0.95 # 留 5% 余量(手续费) -``` - -### **完整示例(Bitget BTC)** - -``` -输入: -- 信号置信度: 92% (A级) -- 可用余额: $1,074 -- 账户余额: $1,074 -- 当前杠杆: 0x -- 最大杠杆: 10x - -Step 1: 基础保证金比例 = 20% (A级) 🔥 -Step 2: 初始保证金 = 1074 × 20% = $214.80 -Step 3: BTC 最小保证金 = $85 → 不调整 -Step 4: 最大保证金限制 = 1074 × 10% = $107.40 → 调整为 $107.40 ⚠️ -Step 5: 剩余杠杆 = 10x → 不调整 -Step 6: $107.40 < $1074 → 不调整 - -最终保证金: $107.40 (受10%最大限制,实际10%而非20%) -原因: "信号A级(92%) → 20%保证金,受平台10%限制 → 10%保证金" -``` - ---- - -## 2️⃣ 执行层:平台特定转换 - -### **Bitget 执行器** - -#### **输入**: 保证金 `$107.40` + 杠杆 `10x` + 价格 `$85000` - -#### **Step 1: 计算持仓价值** -```python -position_value = margin × leverage - = $107.40 × 10 = $1,074 -``` - -#### **Step 2: 计算币数量** -```python -coin_amount = position_value / price - = $1,074 / $85000 - = 0.01264 BTC -``` - -#### **Step 3: 获取合约规格** -```python -# BTC 合约规格 -contract_size = 0.01 BTC/张 - -# 不同币种的合约规格 -BTC: 0.01 BTC/张 -ETH: 0.1 ETH/张 -SOL: 1 SOL/张 -``` - -#### **Step 4: 计算合约张数(向下取整)** -```python -contracts = int(coin_amount / contract_size) - = int(0.01264 / 0.01) - = 1 张 ✅ -``` - -**完整示例**: -``` -保证金: $107.40 -杠杆: 10x -持仓价值: $1,074 -BTC 数量: 0.01264 BTC -合约张数: 1 张 ✅ -``` - ---- - -### **Hyperliquid 执行器** - -#### **输入**: 保证金 `$100` + 杠杆 `10x` + 价格 `$85000` - -#### **Step 1: 计算持仓价值** -```python -position_value = margin × leverage - = $100 × 10 = $1,000 -``` - -#### **Step 2: 计算仓位大小(币数量)** -```python -position_size = position_value / price - = $1,000 / $85000 - = 0.01176 BTC -``` - -#### **Step 3: 直接下单(无合约规格限制)** -```python -# Hyperliquid 支持任意大小 -order_params = { - 'symbol': 'BTC', - 'is_buy': True, - 'size': 0.01176, # 直接使用币数量 - 'price': None, - 'order_type': 'market', -} -``` - -**优势**: Hyperliquid 无最小合约限制,可以下任意大小的订单。 - ---- - -## 📊 平台对比 - -| 项目 | Bitget | Hyperliquid | -|------|--------|-------------| -| **最小单位** | 合约张数(整数) | 币数量(任意) | -| **BTC 合约规格** | 0.01 BTC/张 | 无限制 | -| **最小保证金** (10x杠杆) | $85 | $50 | -| **仓位表示** | 张数 | 币数量 | -| **计算公式** | `(保证金×杠杆) / 价格 / 合约规格` | `(保证金×杠杆) / 价格` | - ---- - -## 🛡️ 风险控制 - -### **账户级止损** (25%) - -```python -# 每轮循环开始时检查 -initial_balance = $10,000 -current_balance = $7,500 -drawdown = ($10,000 - $7,500) / $10,000 = 25% - -if drawdown >= 25%: - # 🚨 触发账户级止损 - # 1. 立即平掉所有持仓 - # 2. 停止交易系统 - # 3. 发送紧急通知 -``` - -### **警告阈值** (15%) - -```python -if drawdown >= 15% and drawdown < 25%: - # ⚠️ 发送警告通知 - await send_alert("账户回撤警告: 15%") -``` - -### **总杠杆限制** (10x) - -```python -current_total_leverage = total_position_value / account_balance - -if current_total_leverage >= 10.0: - # 拒绝新开仓 - return "已达最大杠杆 10x/10x" -``` - ---- - -## 📝 总结 - -**决策层 (`crypto_agent.py`)**: -- ✅ 根据信号等级计算保证金(8%/15%/20%) -- ✅ 应用平台规则(最小/最大限制) -- ✅ 考虑杠杆空间 -- ✅ 账户级止损保护(25%) - -**执行层 (执行器)**: -- ✅ Bitget: 保证金 → 币数量 → 合约张数(整数) -- ✅ Hyperliquid: 保证金 → 币数量(任意) -- ✅ 预留手续费 -- ✅ 智能重试 -- ✅ **飞书通知**: 所有执行结果自动发送通知 - -**关键差异**: -- **Bitget**: 受合约规格限制,必须整数张,最小保证金较高 -- **Hyperliquid**: 无合约规格限制,任意大小,灵活性强 - -**风险管理**: -- **账户止损**: 25% 回撤自动平仓 -- **警告阈值**: 15% 回撤发送警告 -- **总杠杆限制**: 10x 上限 - ---- - -## 相关文档 - -- [超激进配置详解](./AGGRESSIVE_CONFIG.md) -- [杠杆配置说明](./LEVERAGE_CONFIGURATION.md) -- [仓位策略对比](./POSITION_SIZING_STRATEGY.md) -- [移动止损功能](./MOVE_STOP_LOSS_FEATURE.md) -- [配置更新总结](./CONFIG_UPDATE_2026-03-28.md) diff --git a/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC_OLD.md b/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC_OLD.md deleted file mode 100644 index 08ac120..0000000 --- a/backend/app/crypto_agent/executor/POSITION_SIZE_LOGIC_OLD.md +++ /dev/null @@ -1,426 +0,0 @@ -# 仓位大小计算逻辑 - -## 📊 总体流程 - -``` -市场信号 (LLM) → 硬编码规则决策 → 保证金计算 → 平台执行器 → 下单 - ↓ ↓ ↓ ↓ - 置信度 75% 同向/反向检查 保证金金额 合约张数/币数量 -``` - ---- - -## 1️⃣ 决策层:保证金计算 (`crypto_agent.py`) - -### **输入参数** -- **信号置信度** (`confidence`): 0-100 分 -- **可用余额** (`available`): 账户可用 USDT -- **账户余额** (`balance`): 账户总余额 USDT -- **当前杠杆** (`current_total_leverage`): 已使用杠杆 -- **最大杠杆** (`max_total_leverage`): 允许的最大杠杆 - -### **计算步骤** - -#### **Step 1: 根据信号等级确定基础保证金比例** - -```python -if confidence >= 90: - base_margin_pct = 0.03 # A级: 3% -elif confidence >= 70: - base_margin_pct = 0.02 # B级: 2% -else: - base_margin_pct = 0.01 # C级: 1% -``` - -#### **Step 2: 计算初始保证金** - -```python -margin = available * base_margin_pct - -# 示例: -# 可用余额: $1000 -# 信号等级: B级 (75分) -# margin = 1000 × 2% = $20 -``` - -#### **Step 3: 应用平台最小保证金限制** - -```python -# Bitget 最小保证金(各币种不同) -BTC: min_margin = $85 # 0.01 BTC × $85000 ÷ 10x杠杆 -ETH: min_margin = $35 # 0.1 ETH × $3500 ÷ 10x杠杆 -SOL: min_margin = $14 # 1 SOL × $140 ÷ 10x杠杆 - -if margin < min_margin: - margin = min_margin -``` - -**示例**: -``` -计算保证金: $20 -BTC 最小保证金: $85 -调整后: margin = $85 -``` - -#### **Step 4: 应用最大保证金限制** - -```python -# 单笔不超过余额的 10% (Bitget/Hyperliquid) 或 5% (模拟盘) -max_margin = balance * max_margin_pct - -if margin > max_margin: - margin = max_margin -``` - -**示例**: -``` -计算保证金: $150 -账户余额: $1000 -最大限制: 10% -调整后: margin = $100 -``` - -#### **Step 5: 应用杠杆限制** - -```python -remaining_leverage = max_leverage - current_leverage - -if remaining_leverage <= 0: - return 0, "已达最大杠杆" - -max_margin_by_leverage = balance * remaining_leverage -if margin > max_margin_by_leverage: - margin = max_margin_by_leverage -``` - -**示例**: -``` -计算保证金: $200 -账户余额: $1000 -当前杠杆: 8x -最大杠杆: 10x -剩余杠杆: 2x -调整后: margin = $1000 × 2 = $2000 (超过余额,限制为 $200) -``` - -#### **Step 6: 确保不超过可用余额** - -```python -if margin > available: - margin = available * 0.95 # 留 5% 余量(手续费) -``` - -### **完整示例(Bitget BTC)** - -``` -输入: -- 信号置信度: 75% (B级) -- 可用余额: $1074 -- 账户余额: $1074 -- 当前杠杆: 0x -- 最大杠杆: 10x - -Step 1: 基础保证金比例 = 2% (B级) -Step 2: 初始保证金 = 1074 × 2% = $21.48 -Step 3: BTC 最小保证金 = $85 → 调整为 $85 -Step 4: 最大保证金限制 = 1074 × 10% = $107.4 → 不调整 -Step 5: 剩余杠杆 = 10x → 不调整 -Step 6: $85 < $1074 → 不调整 - -最终保证金: $85 -原因: "信号B级(75%) → 2%保证金,应用BTC最小保证金限制$85" -``` - ---- - -## 2️⃣ 执行层:平台特定转换 - -### **Bitget 执行器** - -#### **输入**: 保证金 `$85` + 杠杆 `5x` (假设) + 价格 `$85000` - -#### **Step 1: 计算持仓价值** -```python -position_value = margin × leverage - = $85 × 5 = $425 -``` - -#### **Step 2: 计算币数量** -```python -coin_amount = position_value / price - = $425 / $85000 - = 0.005 BTC -``` - -#### **Step 3: 获取合约规格** -```python -# BTC 合约规格 -contract_size = 0.01 BTC/张 - -# 不同币种的合约规格 -BTC: 0.01 BTC/张 -ETH: 0.1 ETH/张 -SOL: 1 SOL/张 -``` - -#### **Step 4: 计算合约张数(向下取整)** -```python -contracts = int(coin_amount / contract_size) - = int(0.005 / 0.01) - = 0 张 ❌ -``` - -**问题**: `0 张 < 1 张` → 无法下单! - -#### **解决方案: 调整保证金** - -```python -# 需要至少 1 张合约 -min_contracts = 1 -min_coin_amount = 1 × 0.01 = 0.01 BTC -min_position_value = 0.01 × $85000 = $850 -min_margin = $850 / 5x = $170 - -# 之前的最小保证金 $85 是按 10x 杠杆算的 -# 如果用 5x 杠杆,需要 $170 -``` - -**完整示例**: -``` -保证金: $170 -杠杆: 5x -持仓价值: $850 -BTC 数量: 0.01 BTC -合约张数: 1 张 ✅ -``` - ---- - -### **Hyperliquid 执行器** - -#### **输入**: 保证金 `$50` + 杠杆 `5x` + 价格 `$85000` - -#### **Step 1: 计算持仓价值** -```python -position_value = margin × leverage - = $50 × 5 = $250 -``` - -#### **Step 2: 计算仓位大小(币数量)** -```python -position_size = position_value / price - = $250 / $85000 - = 0.00294 BTC -``` - -#### **Step 3: 直接下单(无合约规格限制)** -```python -# Hyperliquid 支持任意大小 -order_params = { - 'symbol': 'BTC', - 'is_buy': True, - 'size': 0.00294, # 直接使用币数量 - 'price': None, # 市价单 - 'order_type': 'market', -} -``` - -**优势**: Hyperliquid 无最小合约限制,可以下任意大小的订单。 - ---- - -## 📊 平台对比 - -| 项目 | Bitget | Hyperliquid | -|------|--------|-------------| -| **最小单位** | 合约张数(整数) | 币数量(任意) | -| **BTC 合约规格** | 0.01 BTC/张 | 无限制 | -| **最小保证金** (10x杠杆) | $85 | $50 | -| **最小保证金** (5x杠杆) | $170 | $25 | -| **仓位表示** | 张数 | 币数量 | -| **计算公式** | `(保证金 × 杠杆) / 价格 / 合约规格` | `(保证金 × 杠杆) / 价格` | - ---- - -## 🔍 实际案例 - -### **案例 1: Bitget BTC,账户 $1074** - -``` -信号: BTC 做多,置信度 75% (B级) - -Step 1 (决策层): -- 基础保证金 = $1074 × 2% = $21.48 -- 最小保证金限制 (BTC) = $85 -- 最终保证金 = $85 - -Step 2 (执行层,假设 10x 杠杆): -- 持仓价值 = $85 × 10 = $850 -- BTC 数量 = $850 / $85000 = 0.01 BTC -- 合约张数 = 0.01 / 0.01 = 1 张 ✅ - -结果: 下单 1 张 BTC 合约,保证金 $85 -``` - -### **案例 2: Bitget SOL,账户 $1074** - -``` -信号: SOL 做多,置信度 75% (B级) - -Step 1 (决策层): -- 基础保证金 = $1074 × 2% = $21.48 -- 最小保证金限制 (SOL) = $14 -- 最终保证金 = $21.48 (大于 $14,不调整) - -Step 2 (执行层,假设 5x 杠杆): -- 持仓价值 = $21.48 × 5 = $107.40 -- SOL 数量 = $107.40 / $140 = 0.767 SOL -- 合约张数 = 0.767 / 1 = 0 张 ❌ - -问题: 0 张 < 1 张 - -解决方案: 调整保证金到至少 $28 (28 × 5 / 140 / 1 = 1 张) - -实际调整: -- 最小保证金 (5x杠杆) = (1 × 1 × $140) / 5 = $28 -- 调整后保证金 = $28 -- SOL 数量 = $28 × 5 / $140 = 1 SOL -- 合约张数 = 1 / 1 = 1 张 ✅ - -结果: 下单 1 张 SOL 合约,保证金 $28 -``` - -### **案例 3: Hyperliquid BTC,账户 $1000** - -``` -信号: BTC 做多,置信度 80% (B级) - -Step 1 (决策层): -- 基础保证金 = $1000 × 2% = $20 -- 最小保证金限制 (BTC) = $50 -- 最终保证金 = $50 - -Step 2 (执行层,5x 杠杆): -- 持仓价值 = $50 × 5 = $250 -- BTC 数量 = $250 / $85000 = 0.00294 BTC -- 直接下单: 0.00294 BTC ✅ - -结果: 下单 0.00294 BTC,保证金 $50 -``` - ---- - -## ⚠️ 常见问题 - -### **问题 1: 为什么 Bitget 的仓位计算失败?** - -**原因**: Bitget 有合约规格限制,必须是整数张数。 - -**解决**: -```python -# 方法1: 提高杠杆(从 5x → 10x) -# $85 × 10 = $850 → 0.01 BTC → 1 张 ✅ - -# 方法2: 提高保证金 -# $170 × 5 = $850 → 0.01 BTC → 1 张 ✅ -``` - -### **问题 2: 为什么保证金会被调整两次?** - -**第一次调整** (决策层): -- 确保满足平台最小保证金(如 BTC $85) - -**第二次调整** (执行层): -- 确保至少能买 1 张合约 -- 考虑实际杠杆配置 - -**建议**: 统一在执行层调整,决策层只计算基础保证金。 - -### **问题 3: Hyperliquid 为什么不需要最小保证金?** - -**原因**: Hyperliquid 支持任意大小的仓位,没有合约规格限制。 - -**优势**: 可以精确控制仓位大小,无需凑整。 - ---- - -## 🎯 优化建议 - -### **1. 统一最小保证金计算** - -```python -def get_min_margin_for_symbol(symbol: str, leverage: int, platform: str) -> float: - """获取某币种的最小保证金(考虑杠杆)""" - if platform == 'Bitget': - contract_size = get_contract_size(symbol) - price = get_current_price(symbol) - min_contracts = 1 - min_position_value = min_contracts * contract_size * price - min_margin = min_position_value / leverage - return min_margin - elif platform == 'Hyperliquid': - # Hyperliquid 最小 $50 - return 50.0 - else: - return 0.0 -``` - -### **2. 在决策层预先计算** - -```python -# 在 _calculate_position_size 中 -leverage = signal.get('leverage', 5) # 使用实际杠杆 -min_margin = get_min_margin_for_symbol(symbol, leverage, platform_name) -if margin < min_margin: - margin = min_margin - logger.info(f" 调整保证金到最小值: ${margin:.2f} (杠杆 {leverage}x)") -``` - ---- - -## 📝 总结 - -**决策层 (`crypto_agent.py`)**: -- ✅ 根据信号等级计算保证金(1%/2%/3%) -- ✅ 应用平台规则(最小/最大限制) -- ✅ 考虑杠杆空间 - -**执行层 (执行器)**: -- ✅ Bitget: 保证金 → 币数量 → 合约张数(整数) -- ✅ Hyperliquid: 保证金 → 币数量(任意) -- ✅ 预留手续费 -- ✅ 智能重试 -- ✅ **飞书通知**: 所有执行结果自动发送通知 - -**关键差异**: -- **Bitget**: 受合约规格限制,必须整数张,最小保证金较高 -- **Hyperliquid**: 无合约规格限制,任意大小,灵活性强 - ---- - -## 📢 飞书通知集成 - -### 功能说明 -所有交易执行操作都会自动发送飞书通知,详见 [NOTIFICATION_FEATURE.md](./NOTIFICATION_FEATURE.md) - -### 通知时机 -- **开仓**: 执行成功/失败后立即通知 -- **平仓**: 执行成功/失败后立即通知 -- **撤单**: 执行成功/失败后立即通知 -- **止盈止损**: 设置成功/失败后立即通知 - -### 通知内容 -- **平台**: Bitget / Hyperliquid / 模拟盘 -- **交易对**: BTC / ETH / SOL 等 -- **执行结果**: 成功/失败状态 -- **详细信息**: 订单ID、数量、价格、保证金、杠杆、盈亏等 - -## 📊 持仓管理功能 - -所有平台都实现了智能持仓管理,包括: - -1. **自动止盈** (TAKE_PROFIT): 达到目标盈利时自动平仓 -2. **超时退出** (TIME_EXIT): 持仓超过最大时长时自动平仓 -3. **移动止损** (MOVE_SL): 盈利达到 2% 时,自动移动止损到入场价 - -详细说明: [MOVE_STOP_LOSS_FEATURE.md](./MOVE_STOP_LOSS_FEATURE.md) diff --git a/backend/app/crypto_agent/executor/POSITION_SIZING_STRATEGY.md b/backend/app/crypto_agent/executor/POSITION_SIZING_STRATEGY.md deleted file mode 100644 index 37fe9c6..0000000 --- a/backend/app/crypto_agent/executor/POSITION_SIZING_STRATEGY.md +++ /dev/null @@ -1,241 +0,0 @@ -# 仓位配置方案 -UPDATED: 2026-03-28 - -## 当前方案: Kelly公式型 - -### 配置详情 - -| 信号等级 | 置信度范围 | 保证金比例 | $1000账户示例 | 5x杠杆仓位 | 10x杠杆仓位 | -|---------|----------|----------|-------------|-----------|------------| -| **A级** | ≥90分 | **10%** | $100 | $500 | $1,000 | -| **B级** | 70-89分 | **6%** | $60 | $300 | $600 | -| **C级** | <70分 | **2%** | $20 | $100 | $200 | - -### 特点 -- ✅ **最大化资金利用率**: A级信号用10%仓位,充分利用高质量机会 -- ✅ **分级明确**: 不同信号等级差异明显(10% / 6% / 2%) -- ✅ **适合小资金**: $1000账户也能开有意义仓位($500-$1000) -- ⚠️ **高风险**: 单笔最大亏损可能较大,需严格止损 - -### 理论基础: Kelly公式 - -Kelly公式用于计算最优仓位大小: - -``` -f* = (p × b - q) / b - -其中: -f* = 最优仓位比例 -p = 胜率 -q = 败率 (1 - p) -b = 盈亏比 (平均盈利/平均亏损) -``` - -**假设参数**: -- A级信号胜率: 65%, 盈亏比 2.0 → Kelly建议 ~22% (我们用10%更保守) -- B级信号胜率: 55%, 盈亏比 1.5 → Kelly建议 ~10% (我们用6%) -- C级信号胜率: 50%, 盈亏比 1.2 → Kelly建议 ~3% (我们用2%) - -## 实际案例 - -### 案例 1: Bitget BTC, $1074账户, A级信号 -``` -信号置信度: 92% (A级) -账户余额: $1,074 -可用余额: $1,074 - -Step 1: 基础保证金 = $1,074 × 10% = $107.40 -Step 2: 最小保证金限制 (BTC) = $85 → 不调整 -Step 3: 最大保证金限制 = $1,074 × 10% = $107.40 → 不调整 -Step 4: 杠杆限制 (5x) → 不调整 - -最终保证金: $107.40 -杠杆: 5x -持仓价值: $537 -BTC数量: 0.00631 BTC -合约张数: 0 张 ❌ - -问题: 0.00631 BTC / 0.01 BTC/张 = 0.631张 → 不足1张 - -解决: 调整保证金到 $170 -→ 0.01 BTC = 1张 ✅ -``` - -### 案例 2: Hyperliquid ETH, $1000账户, B级信号 -``` -信号置信度: 78% (B级) -账户余额: $1,000 -可用余额: $1,000 - -Step 1: 基础保证金 = $1,000 × 6% = $60 -Step 2: 最小保证金限制 = $50 → 不调整 -Step 3: 最大保证金限制 = $1,000 × 10% = $100 → 不调整 - -最终保证金: $60 -杠杆: 5x -持仓价值: $300 -ETH数量: 0.0857 ETH ✅ (Hyperliquid支持任意大小) -``` - -### 案例 3: PaperTrading SOL, $10000账户, C级信号 -``` -信号置信度: 65% (C级) -账户余额: $10,000 -可用余额: $10,000 - -Step 1: 基础保证金 = $10,000 × 2% = $200 -Step 2: 无最小限制 (模拟盘) -Step 3: 最大保证金限制 = $10,000 × 5% = $500 → 不调整 - -最终保证金: $200 -杠杆: 5x -持仓价值: $1,000 -SOL数量: 7.14 SOL ✅ -``` - -## 风险管理 - -### 单笔最大亏损计算 - -假设止损设置为-3%: - -| 信号等级 | 保证金 | 杠杆 | 持仓价值 | -3%亏损 | 占总资金% | -|---------|--------|-----|---------|---------|----------| -| **A级** | $100 | 5x | $500 | -$15 | -1.5% | -| **A级** | $100 | 10x | $1,000 | -$30 | -3.0% | -| **B级** | $60 | 5x | $300 | -$9 | -0.9% | -| **C级** | $20 | 5x | $100 | -$3 | -0.3% | - -### 最大持仓限制 - -除了保证金比例,还有其他限制: - -1. **平台最大保证金**: 单笔不超过余额的10% (Bitget/Hyperliquid) -2. **杠杆限制**: 总杠杆不超过10x -3. **最小保证金**: - - Bitget BTC: $85 (10x) / $170 (5x) - - Hyperliquid: $50 (任意杠杆) -4. **合约规格** (Bitget): - - 必须是整数张 - - BTC: 0.01 BTC/张 - - ETH: 0.1 ETH/张 - - SOL: 1 SOL/张 - -## 与其他方案对比 - -### 方案对比表 - -| 方案 | A级 | B级 | C级 | $1000账户A级仓位(5x) | 特点 | -|------|-----|-----|-----|---------------------|------| -| **保守型** | 3% | 2% | 1% | $150 | 风险低,收益慢 | -| **平衡型** | 5% | 3% | 1% | $250 | 风险收益平衡 | -| **激进型** | 8% | 5% | 2% | $400 | 高收益,高风险 | -| **Kelly型** (当前) | 10% | 6% | 2% | $500 | 最大化资金效率 | - -### 资金曲线对比 - -假设10次A级信号,胜率60%,平均盈利4%,平均亏损3%: - -``` -保守型 (3%): -- 盈利: 6次 × $150 × 4% = $36 -- 亏损: 4次 × $150 × 3% = -$18 -- 净收益: +$18 (1.8%) - -Kelly型 (10%): -- 盈利: 6次 × $500 × 4% = $120 -- 亏损: 4次 × $500 × 3% = -$60 -- 净收益: +$60 (6.0%) -``` - -**Kelly型收益是保守型的3.3倍**,但最大回撤也会更大。 - -## 调整建议 - -### 何时调整 - -1. **降低仓位** (回到平衡型): - - 连续亏损 > 5次 - - 账户回撤 > 15% - - 市场极度波动 (VIX > 40) - -2. **提高杠杆** (保持10%保证金): - - 使用10x杠杆替代5x - - A级信号仓位: $500 → $1,000 - - 适合高置信度信号 (≥95%) - -### 动态调整策略 - -```python -def get_dynamic_position_size(confidence, account, recent_performance): - """根据近期表现动态调整仓位""" - - # 基础比例 (Kelly型) - if confidence >= 90: - base_pct = 0.10 - elif confidence >= 70: - base_pct = 0.06 - else: - base_pct = 0.02 - - # 根据近期表现调整 - win_rate = recent_performance.get('win_rate_7d', 0.5) - - if win_rate < 0.4: - # 近期表现差,降低仓位 - base_pct *= 0.7 - elif win_rate > 0.7: - # 近期表现好,提高仓位 - base_pct *= 1.2 - - return account['available'] * base_pct -``` - -## 实施检查清单 - -- [x] 修改 `crypto_agent.py` 中的 `_calculate_position_size()` 方法 -- [x] 更新文档说明新比例 -- [x] 测试A级信号仓位计算 -- [x] 测试B级信号仓位计算 -- [x] 测试C级信号仓位计算 -- [x] 验证Bitget合约张数计算 -- [x] 验证Hyperliquid任意大小支持 - -- [ ] 实盘测试并监控 -- [ ] 根据实盘表现微调 - -## 相关配置文件 - -```python -# crypto_agent.py - -# Kelly公式型配置 -if confidence >= 90: - base_margin_pct = 0.10 # A级: 10% -elif confidence >= 70: - base_margin_pct = 0.06 # B级: 6% -else: - base_margin_pct = 0.02 # C级: 2% - -# 平台限制 -PLATFORM_RULES = { - 'Bitget': { - 'max_margin_pct': 0.10, # 单笔最大10% - 'min_margin': { - 'BTC': 85, # 10x杠杆 - 'ETH': 35, - 'SOL': 14 - } - }, - 'Hyperliquid': { - 'max_margin_pct': 0.10, - 'min_margin': 50 # 通用最小$50 - } -} -``` - -## 相关文档 - -- [仓位计算逻辑](./POSITION_SIZE_LOGIC.md) -- [执行器优化总结](./EXECUTOR_OPTIMIZATION_SUMMARY.md) -- [移动止损功能](./MOVE_STOP_LOSS_FEATURE.md) diff --git a/backend/app/crypto_agent/executor/TEST_REPORT_2026-03-28.md b/backend/app/crypto_agent/executor/TEST_REPORT_2026-03-28.md deleted file mode 100644 index 5e6650b..0000000 --- a/backend/app/crypto_agent/executor/TEST_REPORT_2026-03-28.md +++ /dev/null @@ -1,187 +0,0 @@ -# P0问题修复验证测试报告 -DATE: 2026-03-28 22:53:44 -STATUS: ✅ 全部通过 - -## 📊 测试结果 - -``` -总测试数: 23 -通过: 23 ✅ -失败: 0 ❌ -通过率: 100.0% - -🎉 所有测试通过!P0问题修复验证成功! -``` - ---- - -## ✅ 测试详情 - -### 测试1: PLATFORM_RULES 配置 -**状态**: ✅ 3/3 通过 - -- ✅ Bitget max_margin_pct = 0.25 -- ✅ PaperTrading max_margin_pct = 0.25 -- ✅ Hyperliquid max_margin_pct = 0.25 - -**验证**: 所有平台的max_margin_pct已从10%提高到25% - ---- - -### 测试2: A级信号仓位计算逻辑 -**状态**: ✅ 3/3 通过 - -- ✅ A级信号保证金: $200.00 (未被截断) -- ✅ 持仓价值: $2000.00 (期望 $2000) -- ✅ 账户比例: 200% (期望 200%) - -**验证**: $1000账户,A级信号可以使用完整的$200保证金(20%) - ---- - -### 测试3: 紧急平仓async/await处理代码 -**状态**: ✅ 2/2 通过 - -- ✅ 包含 asyncio.iscoroutinefunction 检查 -- ✅ 包含 await close_method 调用 - -**验证**: 紧急平仓代码已添加async/await检测机制 - ---- - -### 测试4: 初始余额持久化方法 -**状态**: ✅ 4/4 通过 - -- ✅ 找到方法/属性: _load_initial_balances -- ✅ 找到方法/属性: _save_initial_balances -- ✅ 找到方法/属性: _get_initial_balance -- ✅ 找到方法/属性: _initial_balances - -**验证**: 初始余额持久化机制已实现 - ---- - -### 测试5: 回撤计算(各种场景) -**状态**: ✅ 4/4 通过 - -- ✅ 无回撤: 0.0% -- ✅ 15%回撤(警告): 15.0% -- ✅ 25%回撤(止损): 25.0% -- ✅ 50%回撤(严重): 50.0% - -**验证**: 回撤计算逻辑正确 - ---- - -### 测试6: async/await机制验证 -**状态**: ✅ 4/4 通过 - -- ✅ sync_close 不是协程: False -- ✅ async_close 是协程: True -- ✅ 同步调用成功: {'success': True, 'symbol': 'BTC'} -- ✅ 异步调用成功: {'success': True, 'symbol': 'ETH'} - -**验证**: async/await检测和调用机制工作正常 - ---- - -### 测试7: BaseExecutor飞书通知功能 -**状态**: ✅ 3/3 通过 - -- ✅ 找到通知方法: send_execution_notification -- ✅ 找到通知方法: _send_open_notification -- ✅ 找到通知方法: _send_close_notification - -**验证**: BaseExecutor飞书通知功能已实现 - ---- - -## 📋 修复验证清单 - -### P0-1: 配置不一致(max_margin_pct) -- [x] Bitget: 10% → 25% ✅ -- [x] PaperTrading: 5% → 25% ✅ -- [x] Hyperliquid: 10% → 25% ✅ -- [x] A级信号可以用20%保证金 ✅ -- [x] 不被截断到10% ✅ - -### P0-2: 紧急平仓async/await -- [x] 添加 `asyncio.iscoroutinefunction()` 检查 ✅ -- [x] async方法使用await调用 ✅ -- [x] sync方法直接调用 ✅ -- [x] 紧急平仓不会失败 ✅ - -### P0-3: 初始余额持久化 -- [x] 实现 `_load_initial_balances()` ✅ -- [x] 实现 `_save_initial_balances()` ✅ -- [x] 实现 `_get_initial_balance()` ✅ -- [x] 持久化到 `data/initial_balances.json` ✅ -- [x] 回撤计算正确 ✅ -- [x] 账户止损恢复功能 ✅ - ---- - -## 🎯 下一步测试建议 - -### 单元测试(可选) -```bash -# 运行完整测试(需要安装依赖) -pip install httpx ccxt -python backend/test_p0_fixes.py -``` - -### 模拟盘测试(推荐) -1. **启动系统** - ```bash - cd backend - python main.py - ``` - -2. **观察日志** - ``` - ✅ 期望看到: - 📂 已加载初始余额: {"模拟盘": 10000.0} - ✨ [模拟盘] 记录初始余额: $10000.00 - - ❌ 如果看到: - 📂 初始余额文件不存在,将在首次运行时创建 - ``` - -3. **验证保证金** - - 等待A级信号 - - 检查日志中的保证金是否为20% - - 例如: `保证金: $200.00 (账户 $1000.00 × 20%)` - -4. **验证回撤监控** - - 系统会定期输出: - ``` - 📊 [模拟盘] 账户状态: 初始 $10000.00 → 当前 $9500.00 (回撤 5.00%) - ``` - -### 实盘小资金测试(可选) -1. Bitget $100 测试 -2. 观察初始余额记录 -3. 观察实际保证金使用 - ---- - -## 📄 相关文档 - -- [修复报告](./FIX_REPORT_2026-03-28.md) -- [Code Review](./CODE_REVIEW_2026-03-28.md) -- [超激进配置详解](./AGGRESSIVE_CONFIG.md) -- [配置更新总结](./CONFIG_UPDATE_2026-03-28.md) - ---- - -## ✅ 结论 - -**所有P0问题修复验证通过!** - -- ✅ **配置一致性**: max_margin_pct 25% ✅ -- ✅ **紧急平仓**: async/await机制 ✅ -- ✅ **初始余额持久化**: 完整实现 ✅ -- ✅ **回撤计算**: 正确无误 ✅ -- ✅ **飞书通知**: 功能完整 ✅ - -**系统已准备好进行模拟盘测试!** 🚀 diff --git a/backend/app/crypto_agent/executor/__init__.py b/backend/app/crypto_agent/executor/__init__.py index 8ee9109..f7e7f43 100644 --- a/backend/app/crypto_agent/executor/__init__.py +++ b/backend/app/crypto_agent/executor/__init__.py @@ -6,11 +6,9 @@ from app.crypto_agent.executor.base_executor import BaseExecutor from app.crypto_agent.executor.paper_trading_executor import PaperTradingExecutor from app.crypto_agent.executor.bitget_executor import BitgetExecutor -from app.crypto_agent.executor.hyperliquid_executor import HyperliquidExecutor __all__ = [ 'BaseExecutor', 'PaperTradingExecutor', 'BitgetExecutor', - 'HyperliquidExecutor', ] diff --git a/backend/app/crypto_agent/executor/hyperliquid_executor.py b/backend/app/crypto_agent/executor/hyperliquid_executor.py deleted file mode 100644 index c58dc65..0000000 --- a/backend/app/crypto_agent/executor/hyperliquid_executor.py +++ /dev/null @@ -1,382 +0,0 @@ -""" -Hyperliquid 实盘交易执行器 -""" -from typing import Dict, Any, List, Optional -from app.crypto_agent.executor.base_executor import BaseExecutor -from app.services.hyperliquid_trading_service import get_hyperliquid_service -from app.utils.logger import logger - - -class HyperliquidExecutor(BaseExecutor): - """Hyperliquid 实盘交易执行器""" - - def __init__(self): - super().__init__("Hyperliquid") - self.hyperliquid = get_hyperliquid_service() - - # ==================== 核心执行方法 ==================== - - async def execute_open(self, decision: Dict[str, Any], - current_price: float) -> Dict[str, Any]: - """执行开仓""" - try: - symbol = decision.get('symbol', '').replace('USDT', '') - action = decision.get('signal_action', decision.get('action')) # buy/sell - margin = decision.get('margin', decision.get('quantity', 0)) - entry_price = decision.get('entry_price', current_price) - stop_loss = decision.get('stop_loss') - take_profit = decision.get('take_profit') - - # 决定订单类型 - order_type, order_reason = self.decide_order_type(decision, current_price) - logger.info(f" 订单类型: {order_reason}") - - # 获取账户状态 - account_state = self.hyperliquid.get_account_state() - available = account_state.get('available_balance', 0) - - leverage = min(decision.get('leverage', 10), 10) - adjusted_margin = self.calculate_effective_margin(available, margin) - - if adjusted_margin <= 0: - return { - 'success': False, - 'error': f'保证金无效: {adjusted_margin}' - } - - # 计算仓位大小 - position_size = self._calculate_position_size(symbol, adjusted_margin, entry_price, leverage) - actual_position_value = position_size * entry_price - leverage_ok, leverage_reason, effective_leverage = self.validate_effective_leverage( - decision, - adjusted_margin, - actual_position_value, - ) - - if position_size <= 0: - return { - 'success': False, - 'error': f'仓位计算失败: {position_size}' - } - if not leverage_ok: - return { - 'success': False, - 'error': leverage_reason, - 'effective_leverage': effective_leverage, - } - - # 设置杠杆 - self.hyperliquid.update_leverage(symbol, leverage) - - # 下单 - is_buy = (action == 'buy') - - if order_type == 'market': - # 市价单 - result = self.hyperliquid.place_market_order( - symbol=symbol, - is_buy=is_buy, - size=position_size, - reduce_only=False - ) - else: - # 限价单 - result = self.hyperliquid.place_limit_order( - symbol=symbol, - is_buy=is_buy, - size=position_size, - price=entry_price, - reduce_only=False - ) - - if not result.get('success'): - return result - - order_id = result.get('order_id') - order_status = result.get('order_status', 'filled') - - logger.info(f" ✅ 开仓成功: {symbol} {position_size} @ ${order_type}") - - # 设置止盈止损 - if stop_loss or take_profit: - if order_status == 'filled': - # 市价单已成交,直接设置 TP/SL - try: - tp_sl_result = self.hyperliquid.set_tp_sl( - symbol=symbol, - is_long=is_buy, - size=position_size, - 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: - # 部分成功:记录缺失侧,交给 agent 后续补设 - set_text = "TP" if tp_set else "SL" - fail_text = "TP" if not tp_set else "SL" - result['pending_tp_sl'] = { - 'tp_price': take_profit if not tp_set else None, - 'sl_price': stop_loss if not sl_set else None, - } - result['position_size'] = position_size - logger.warning(f" ⚠️ 止盈止损部分成功: {set_text}已设, {fail_text}失败") - result['tp_sl_warning'] = f"{fail_text}设置失败: {tp_sl_result.get('errors', [])}" - else: - errors = tp_sl_result.get('errors', []) - result['pending_tp_sl'] = { - 'tp_price': take_profit, - 'sl_price': stop_loss, - } - result['position_size'] = position_size - 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['position_size'] = position_size - result['tp_sl_warning'] = str(tp_sl_err) - else: - # 限价单未成交:记录下来,等成交后自动补设 - result['pending_tp_sl'] = { - 'tp_price': take_profit, - 'sl_price': stop_loss, - } - result['position_size'] = position_size - logger.info(f" 📌 限价单待成交,TP/SL 将在成交后自动设置: TP={take_profit}, SL={stop_loss}") - result['tp_sl_warning'] = "限价单未成交,TP/SL 已加入待补设列表" - - # 开仓成功通知由 crypto_agent 统一发送,避免与执行摘要重复 - - return result - - except Exception as e: - logger.error(f"Hyperliquid 开仓失败: {e}") - error_result = {'success': False, 'error': str(e)} - - # 发送失败通知 - await self.send_execution_notification( - operation='OPEN', - symbol=decision.get('symbol', ''), - result=error_result - ) - - return error_result - - async def execute_close(self, decision: Dict[str, Any], - current_price: float) -> Dict[str, Any]: - """执行平仓""" - try: - symbol = decision.get('symbol', '').replace('USDT', '') - orders_to_close = decision.get('orders_to_close', []) - - result = self.hyperliquid.market_close_position(symbol) - if result.get('success'): - logger.info(f" ✅ 平仓成功: {symbol}") - else: - logger.warning(f" ⚠️ 平仓失败: {symbol} - {result.get('error', '未知错误')}") - - if orders_to_close: - result['requested_order_ids'] = orders_to_close - - # 发送飞书通知 - await self.send_execution_notification( - operation='CLOSE', - symbol=symbol, - result=result - ) - - return result - - except Exception as e: - logger.error(f"Hyperliquid 平仓失败: {e}") - error_result = {'success': False, 'error': str(e)} - - # 发送失败通知 - await self.send_execution_notification( - operation='CLOSE', - symbol=decision.get('symbol', ''), - result=error_result - ) - - return error_result - - async def execute_cancel(self, order_id: str, symbol: str) -> Dict[str, Any]: - """执行撤单""" - try: - result = self.hyperliquid.cancel_order(symbol.replace('USDT', ''), order_id) - if not result.get('order_id'): - result['order_id'] = order_id - logger.info(f" ✅ 撤单成功: {order_id}") - - # 发送飞书通知 - await self.send_execution_notification( - operation='CANCEL', - symbol=symbol, - result=result, - details={'order_id': order_id} - ) - - return result - except Exception as e: - logger.error(f"Hyperliquid 撤单失败: {e}") - error_result = {'success': False, 'error': str(e), 'order_id': order_id} - - # 发送失败通知 - await self.send_execution_notification( - operation='CANCEL', - symbol=symbol, - result=error_result, - details={'order_id': order_id} - ) - - return error_result - - async def set_stop_loss_take_profit(self, - symbol: str, - order_id: str, - stop_loss: Optional[float], - take_profit: Optional[float], - position_size: float) -> Dict[str, Any]: - """设置止盈止损""" - try: - positions = self.hyperliquid.get_open_positions() - pos = next((p for p in positions if p.get('coin') == symbol.replace('USDT', '')), None) - - if not pos: - return {'success': False, 'message': f'找不到 {symbol} 的持仓'} - - result = self.hyperliquid.set_tp_sl( - symbol=symbol.replace('USDT', ''), - is_long=pos['size'] > 0, - size=position_size, - tp_price=take_profit, - sl_price=stop_loss - ) - - if result.get('success'): - logger.info(f" ✅ 止盈止损设置成功: SL=${stop_loss}, TP={take_profit}") - - return result - - except Exception as e: - logger.error(f"Hyperliquid 设置止盈止损失败: {e}") - return {'success': False, 'message': str(e)} - - def should_set_tp_sl_on_order(self) -> bool: - """Hyperliquid 支持在下单时设置 TP/SL""" - return True - - # ==================== 平台特定配置 ==================== - - def get_market_order_threshold(self) -> float: - """市价单阈值: 0.1% (Hyperliquid 流动性好)""" - return 0.1 - - def get_pending_order_timeout(self) -> float: - """挂单超时: 4 小时""" - return 4.0 - - def get_position_exit_rules(self) -> tuple: - """持仓退出规则: (目标盈利 2.5%, 无最大持仓时间限制)""" - return (2.5, float('inf')) - - def get_fee_rate(self) -> float: - """手续费率: 0.05% (taker)""" - return 0.0005 - - def get_max_retries(self) -> int: - """最大重试次数: 5""" - return 5 - - def is_rate_limit_error(self, error_msg: str) -> bool: - """判断是否是限流错误""" - rate_limit_indicators = [ - 'rate limit', - 'too many requests', - '429', - 'limit exceeded' - ] - return any(indicator in error_msg.lower() for indicator in rate_limit_indicators) - - def get_rate_limit_wait_time(self, error_msg: str, attempt: int) -> float: - """获取限流等待时间""" - import random - base_wait = min(2 ** attempt, 30) # 指数退避,最大 30s - jitter = random.uniform(0.5, 1.5) - return base_wait * jitter - - def get_price_update_threshold(self) -> float: - """价格更新阈值: 0.5%""" - return 0.5 - - # ==================== 辅助方法 ==================== - - def _calculate_position_size(self, symbol: str, margin: float, price: float, leverage: int) -> float: - """计算仓位大小(按 Hyperliquid szDecimals 精度截断)""" - try: - import math - - # Hyperliquid 的仓位计算 - position_value = margin * leverage - position_size = position_value / price - - # 按 Hyperliquid 要求的精度截断(szDecimals) - sz_decimals = self.hyperliquid.get_sz_decimals(symbol) - factor = 10 ** sz_decimals - position_size = math.floor(position_size * factor) / factor - - logger.info(f" 仓位计算: ${margin:.2f} × {leverage}x = ${position_value:.2f} → {position_size:.6f} {symbol} (精度: {sz_decimals}位)") - - return position_size - - except Exception as e: - logger.error(f"计算仓位大小失败: {e}") - return 0 - - # ==================== 移动止损 ==================== - - async def move_stop_loss(self, - symbol: str, - new_stop_loss: float, - current_stop_loss: Optional[float] = None) -> Dict[str, Any]: - """ - 移动止损(Hyperliquid) - - Args: - symbol: 交易对 - new_stop_loss: 新止损价 - current_stop_loss: 当前止损价(可选) - - Returns: - {'success': bool, 'message': str} - """ - try: - 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 - ) - - if result.get('success', False): - 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', '移动止损失败'))} - - except Exception as e: - logger.error(f"Hyperliquid 移动止损失败: {e}") - return {'success': False, 'message': str(e)} diff --git a/backend/app/main.py b/backend/app/main.py index 66cba4e..bc21266 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from fastapi.responses import FileResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger -from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, news, astock, hyperliquid, bitget_live +from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, news, astock, bitget_live from app.utils.error_handler import setup_global_exception_handler, init_error_notifier from app.utils.system_status import get_system_monitor import os @@ -672,7 +672,6 @@ app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"]) app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(llm.router, tags=["LLM模型"]) app.include_router(paper_trading.router, tags=["交易"]) -app.include_router(hyperliquid.router, tags=["Hyperliquid"]) app.include_router(bitget_live.router, tags=["Bitget"]) app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"]) app.include_router(astock.router, prefix="/api/astock", tags=["A股分析"]) @@ -746,14 +745,6 @@ async def console_page(): return FileResponse(page_path) return {"message": "页面不存在"} -@app.get("/hyperliquid") -async def hyperliquid_page(): - """Hyperliquid 交易监控页面""" - page_path = os.path.join(frontend_path, "hyperliquid.html") - if os.path.exists(page_path): - return FileResponse(page_path) - return {"message": "页面不存在"} - if __name__ == "__main__": import uvicorn diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index a93c365..abc711e 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -1,8 +1,7 @@ """ Bitget 实盘交易服务 -提供与 HyperliquidTradingService 一致的接口,底层调用 BitgetTradingAPI(ccxt)。 -供 crypto_agent.py 的决策执行层使用。 +封装 Bitget U 本位合约实盘交易能力,供 crypto_agent.py 的执行层使用。 """ import math from datetime import datetime @@ -37,7 +36,7 @@ class BitgetLiveTradingService: """ Bitget 实盘交易服务 - 接口与 HyperliquidTradingService 保持一致,方便 crypto_agent.py 统一调用。 + Bitget 实盘交易服务,统一处理账户、持仓、下单和保护单。 """ @staticmethod @@ -462,11 +461,45 @@ class BitgetLiveTradingService: if not order.get('reduceOnly'): continue order_side = order.get('side', '') - price = float(order.get('price', 0) or 0) + info = order.get('info') or {} + price = self._safe_float( + order.get('price'), + order.get('stopPrice'), + order.get('triggerPrice'), + info.get('triggerPrice'), + info.get('executePrice'), + info.get('stopSurplusTriggerPrice'), + info.get('stopLossTriggerPrice'), + info.get('takeProfit'), + info.get('stopLoss'), + ) order_type = order.get('type', '') + plan_type = str(info.get('planType') or info.get('strategyType') or '').lower() - # stop 类型通常是止损,limit 类型通常是止盈 - if 'stop' in order_type.lower(): + take_profit_price = self._safe_float( + info.get('takeProfit'), + info.get('tpTriggerPrice'), + info.get('stopSurplusTriggerPrice'), + ) + stop_loss_price = self._safe_float( + info.get('stopLoss'), + info.get('slTriggerPrice'), + info.get('stopLossTriggerPrice'), + ) + + if take_profit_price: + result['take_profit'] = take_profit_price + if stop_loss_price: + result['stop_loss'] = stop_loss_price + + # 兼容独立 TP/SL 策略单和经典账户 reduce-only 挂单。 + if 'loss' in plan_type or 'stop_loss' in order_type.lower() or 'stop-loss' in order_type.lower(): + result['stop_loss'] = stop_loss_price or price + elif 'profit' in plan_type or 'take_profit' in order_type.lower() or 'take-profit' in order_type.lower(): + result['take_profit'] = take_profit_price or price + elif order_type == 'uta_tpsl': + continue + elif 'stop' in order_type.lower(): result['stop_loss'] = price elif order_type == 'limit' and price: result['take_profit'] = price @@ -497,14 +530,25 @@ class BitgetLiveTradingService: else: created_at = order.get('datetime') or created_at_raw + info = order.get('info') or {} + display_price = self._safe_float( + order.get('price'), + order.get('stopPrice'), + order.get('triggerPrice'), + info.get('takeProfit'), + info.get('stopLoss'), + ) + result.append({ "order_id": str(order.get('id', '')), "symbol": coin, "side": order.get('side', ''), "size": size_in_coins, - "price": float(order.get('price', 0) or 0), + "price": display_price, "is_reduce_only": bool(order.get('reduceOnly', False)), "order_type": order.get('type', ''), + "take_profit": info.get('takeProfit') or info.get('tpTriggerPrice'), + "stop_loss": info.get('stopLoss') or info.get('slTriggerPrice'), "created_at": created_at, }) return result diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py index 156f4cf..f08484c 100644 --- a/backend/app/services/bitget_trading_api_sdk.py +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -41,6 +41,18 @@ class BitgetTradingAPI: return str(value) return default + @staticmethod + def _safe_float(*values: Any, default: float = 0.0) -> float: + """从多个候选字段中提取第一个可用浮点数。""" + for value in values: + if value in (None, ''): + continue + try: + return float(value) + except (TypeError, ValueError): + continue + return default + def __init__(self, api_key: str, api_secret: str, passphrase: str = "", use_testnet: bool = True): """ 初始化 Bitget 交易 API @@ -446,7 +458,21 @@ class BitgetTradingAPI: contracts = float(position.get('contracts', 0)) pos_side = position.get('side') - mark_price = float(position.get('markPrice', 0)) + info = position.get('info') or {} + mark_price = self._safe_float( + position.get('markPrice'), + position.get('mark_price'), + info.get('markPrice'), + info.get('mark_price'), + info.get('marketPrice'), + position.get('lastPrice'), + ) + if mark_price <= 0: + try: + ticker = self.exchange.fetch_ticker(ccxt_symbol) + mark_price = self._safe_float(ticker.get('last'), ticker.get('close')) + except Exception as e: + logger.warning(f"{symbol} 标记价缺失,获取 ticker 失败: {e}") logger.info(f"当前持仓: {symbol} {pos_side} contracts={contracts}, 标记价={mark_price}") @@ -486,42 +512,55 @@ class BitgetTradingAPI: # 精度处理:使用动态精度 btc_amount = self._floor_amount(ccxt_symbol, btc_amount) + if self.use_unified_account: + try: + tpsl_order = self._place_uta_tpsl_order( + symbol=symbol, + pos_side=pos_side, + take_profit=valid_tp, + stop_loss=valid_sl, + ) + if not tpsl_order: + raise RuntimeError("Bitget 未返回 UTA TP/SL 策略单结果") + if valid_sl: + result["sl_set"] = True + if valid_tp: + result["tp_set"] = True + result["success"] = True + logger.info( + f"✅ [UTA] Bitget 仓位 TP/SL 策略单已设置: " + f"{symbol} posSide={pos_side} TP={valid_tp} SL={valid_sl}, " + f"order={self._extract_order_id(tpsl_order)}" + ) + return result + except Exception as e: + logger.warning(f"[UTA] 设置仓位 TP/SL 策略单失败: {e}") + result["errors"].append(f"UTA TP/SL 策略单失败: {e}") + return result + # 止损单 if valid_sl: sl_side = 'sell' if pos_side == 'long' else 'buy' try: - if self.use_unified_account: - sl_params = { - 'stopLossPrice': valid_sl, - 'hedged': True, - 'reduceOnly': True, - 'marginCoin': 'USDT', - } - sl_order = self.exchange.create_order( - symbol=ccxt_symbol, - type='market', - side=sl_side, - amount=btc_amount, - params=self._with_account_mode_params(sl_params), - ) - else: - sl_params = { - 'stopPrice': valid_sl, - 'triggerBy': 'mark_price', - 'tdMode': 'cross', - 'marginCoin': 'USDT', - 'reduceOnly': True, - } - sl_order = self.exchange.create_order( - symbol=ccxt_symbol, - type='stop_market', - side=sl_side, - amount=btc_amount, - price=None, - params=sl_params, - ) + sl_params = { + 'stopPrice': valid_sl, + 'triggerBy': 'mark_price', + 'tdMode': 'cross', + 'marginCoin': 'USDT', + 'reduceOnly': True, + } + sl_order = self.exchange.create_order( + symbol=ccxt_symbol, + type='stop_market', + side=sl_side, + amount=btc_amount, + price=None, + params=sl_params, + ) + if not sl_order: + raise RuntimeError("Bitget 未返回止损单结果") result["sl_set"] = True - logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} @ ${valid_sl}") + logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} @ ${valid_sl}, order={self._extract_order_id(sl_order)}") except Exception as e: logger.warning(f"下止损单失败: {e}") result["errors"].append(f"止损单下单失败: {e}") @@ -530,36 +569,23 @@ class BitgetTradingAPI: if valid_tp: tp_side = 'sell' if pos_side == 'long' else 'buy' try: - if self.use_unified_account: - tp_params = { - 'takeProfitPrice': valid_tp, - 'hedged': True, - 'reduceOnly': True, - 'marginCoin': 'USDT', - } - tp_order = self.exchange.create_order( - symbol=ccxt_symbol, - type='market', - side=tp_side, - amount=btc_amount, - params=self._with_account_mode_params(tp_params), - ) - else: - tp_params = { - 'tdMode': 'cross', - 'marginCoin': 'USDT', - 'reduceOnly': True, - } - tp_order = self.exchange.create_order( - symbol=ccxt_symbol, - type='limit', - side=tp_side, - amount=btc_amount, - price=valid_tp, - params=tp_params, - ) + tp_params = { + 'tdMode': 'cross', + 'marginCoin': 'USDT', + 'reduceOnly': True, + } + tp_order = self.exchange.create_order( + symbol=ccxt_symbol, + type='limit', + side=tp_side, + amount=btc_amount, + price=valid_tp, + params=tp_params, + ) + if not tp_order: + raise RuntimeError("Bitget 未返回止盈单结果") result["tp_set"] = True - logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} @ ${valid_tp}") + logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} @ ${valid_tp}, order={self._extract_order_id(tp_order)}") except Exception as e: logger.warning(f"下止盈单失败: {e}") result["errors"].append(f"止盈单下单失败: {e}") @@ -597,6 +623,52 @@ class BitgetTradingAPI: # ==================== 查询操作 ==================== + def _place_uta_tpsl_order( + self, + symbol: str, + pos_side: str, + take_profit: Optional[float] = None, + stop_loss: Optional[float] = None, + ) -> Optional[Dict[str, Any]]: + """ + UTA 账户使用 Bitget 策略单接口设置仓位止盈/止损。 + + 普通 create_order + stopLossPrice/takeProfitPrice 在 UTA 下会出现字段映射不稳定, + 这里直接调用 /api/v3/trade/place-strategy-order,确保 TP 和 SL 都按仓位保护单提交。 + """ + payload = { + 'category': self.DEFAULT_PRODUCT_TYPE, + 'symbol': self._to_contract_symbol_id(symbol), + 'type': 'tpsl', + 'tpslMode': 'full', + 'posSide': pos_side, + } + if take_profit is not None: + payload.update({ + 'takeProfit': str(take_profit), + 'tpTriggerBy': 'mark', + 'tpOrderType': 'market', + }) + if stop_loss is not None: + payload.update({ + 'stopLoss': str(stop_loss), + 'slTriggerBy': 'mark', + 'slOrderType': 'market', + }) + logger.info(f"[UTA] 提交 Bitget 保护单: {payload}") + response = self.exchange.privateUtaPostV3TradePlaceStrategyOrder(payload) + if response.get('code') not in (None, '00000'): + raise RuntimeError(f"Bitget UTA 策略单失败: {response}") + return response + + def _extract_order_id(self, response: Optional[Dict[str, Any]]) -> str: + if not response: + return '-' + data = response.get('data') if isinstance(response, dict) else None + if isinstance(data, dict): + return str(data.get('orderId') or data.get('clientOid') or data.get('strategyId') or '-') + return str(response.get('id') or response.get('orderId') or '-') + def get_order(self, symbol: str, order_id: str = None, client_order_id: str = None) -> Optional[Dict]: """ 查询订单 @@ -639,20 +711,80 @@ class BitgetTradingAPI: Returns: 挂单列表 """ + orders: List[Dict[str, Any]] = [] try: ccxt_symbol = self._standardize_symbol(symbol) if symbol else None orders = self.exchange.fetch_open_orders(ccxt_symbol, None, None, self._with_account_mode_params()) - - logger.debug(f"查询到 {len(orders)} 个挂单") - return orders - except ccxt.BaseError as e: - logger.error(f"❌ 查询挂单失败: {e}") + logger.error(f"❌ 查询普通挂单失败: {e}") + except Exception as e: + logger.error(f"❌ 查询普通挂单异常: {e}") + + if self.use_unified_account: + orders.extend(self.get_open_strategy_orders(symbol)) + + logger.debug(f"查询到 {len(orders)} 个挂单") + return orders + + def get_open_strategy_orders(self, symbol: str = None) -> List[Dict]: + """查询 Bitget UTA 未触发策略单,主要用于识别仓位 TP/SL。""" + if not self.use_unified_account: + return [] + try: + payload = { + 'category': self.DEFAULT_PRODUCT_TYPE, + 'type': 'tpsl', + } + target_symbol_id = self._to_contract_symbol_id(symbol) if symbol else None + + response = self.exchange.privateUtaGetV3TradeUnfilledStrategyOrders(payload) + if response.get('code') not in (None, '00000'): + logger.warning(f"[UTA] 查询未触发策略单失败: {response}") + return [] + + data = response.get('data') or {} + raw_orders = data.get('list') if isinstance(data, dict) else data + if not isinstance(raw_orders, list): + raw_orders = [] + + normalized_orders = [self._normalize_uta_strategy_order(item) for item in raw_orders] + if target_symbol_id: + normalized_orders = [ + order for order in normalized_orders + if self._to_contract_symbol_id(order.get('symbol', '')) == target_symbol_id + ] + return normalized_orders + except ccxt.BaseError as e: + logger.warning(f"[UTA] 查询未触发策略单失败: {e}") return [] except Exception as e: - logger.error(f"❌ 查询挂单异常: {e}") + logger.warning(f"[UTA] 查询未触发策略单异常: {e}") return [] + def _normalize_uta_strategy_order(self, item: Dict[str, Any]) -> Dict[str, Any]: + plan_type = str(item.get('planType') or item.get('type') or item.get('strategyType') or '').lower() + tp_price = self._safe_float(item.get('takeProfit'), item.get('tpTriggerPrice')) + sl_price = self._safe_float(item.get('stopLoss'), item.get('slTriggerPrice')) + trigger_price = self._safe_float(item.get('triggerPrice'), item.get('executePrice'), tp_price, sl_price) + side = str(item.get('side') or '').lower() + pos_side = str(item.get('posSide') or '').lower() + if not side: + side = 'sell' if pos_side == 'long' else 'buy' if pos_side == 'short' else '' + + return { + 'id': str(item.get('orderId') or item.get('strategyId') or item.get('clientOid') or ''), + 'symbol': self._standardize_symbol(str(item.get('symbol') or '')), + 'side': side, + 'amount': self._safe_float(item.get('qty'), item.get('size'), item.get('baseVolume')), + 'price': trigger_price, + 'stopPrice': trigger_price, + 'triggerPrice': trigger_price, + 'type': 'uta_tpsl', + 'reduceOnly': True, + 'timestamp': self._safe_float(item.get('ctime'), item.get('createdTime'), item.get('uTime')), + 'info': item, + } + def get_history_orders(self, symbol: str, limit: int = 100) -> List[Dict]: """ 查询历史订单 diff --git a/backend/app/services/hyperliquid_trading_service.py b/backend/app/services/hyperliquid_trading_service.py deleted file mode 100644 index 6c60ddc..0000000 --- a/backend/app/services/hyperliquid_trading_service.py +++ /dev/null @@ -1,698 +0,0 @@ -""" -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() - - # 从 settings 加载认证信息 - self.wallet_address = self.settings.clawfi_wallet_address - self.private_key = self.settings.clawfi_private_key - - if not self.wallet_address or not self.private_key: - raise ValueError( - "缺少 Hyperliquid 认证信息。请在 .env 中设置: " - "CLAWFI_WALLET_ADDRESS 和 CLAWFI_PRIVATE_KEY" - ) - - # 风控配置 - 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, - "current_balance": 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() - - # 精度保护:确保 size 符合 szDecimals 要求 - size = self._sanitize_size(symbol, size) - - try: - if reduce_only: - # 平仓使用 market_close(不需要指定 is_buy,自动判断) - result = self.exchange.market_close(symbol, sz=size) - else: - # 开仓使用 market_open - result = self.exchange.market_open(symbol, is_buy, size) - - # 检查 API 响应状态 - if result.get("status") != "ok": - error_msg = result.get("response", "Unknown error") - logger.error(f"❌ Hyperliquid 市价单失败: {error_msg}") - return {"success": False, "error": str(error_msg), "result": result} - - # 检查单个订单状态 - statuses = result.get("response", {}).get("data", {}).get("statuses", []) - error_statuses = [s for s in statuses if "error" in s] - if error_statuses: - logger.error(f"❌ Hyperliquid 市价单错误: {error_statuses}") - return {"success": False, "error": str(error_statuses), "result": result} - - # statuses 为空 → 静默拒绝 - if not statuses: - logger.error(f"❌ Hyperliquid 市价单:返回 statuses 为空,订单未成功提交") - return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} - - side = "买入" if is_buy else "卖出" - order_type = "平仓" if reduce_only else "开仓" - logger.info(f"✅ Hyperliquid 市价单: {order_type} {side} {symbol} {size}") - - return { - "success": True, - "symbol": symbol, - "side": "buy" if is_buy else "sell", - "size": size, - "reduce_only": reduce_only, - "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() - - # 精度保护:确保 size 和 price 符合要求 - size = self._sanitize_size(symbol, size) - price = round(float(price), 5) # Hyperliquid 价格最多 5 位小数 - - try: - result = self.exchange.order(symbol, is_buy, size, price, - {"limit": {"tif": "Gtc"}}, - reduce_only=reduce_only) - - # 检查 API 响应状态 - if result.get("status") != "ok": - error_msg = result.get("response", "Unknown error") - logger.error(f"❌ Hyperliquid 限价单失败: {error_msg}") - return {"success": False, "error": str(error_msg), "result": result} - - # 检查单个订单状态 - statuses = result.get("response", {}).get("data", {}).get("statuses", []) - - # 有错误 → 失败 - error_statuses = [s for s in statuses if "error" in s] - if error_statuses: - logger.error(f"❌ Hyperliquid 限价单错误: {error_statuses}") - return {"success": False, "error": str(error_statuses), "result": result} - - # statuses 为空 → Hyperliquid 静默拒绝,视为失败 - if not statuses: - logger.error(f"❌ Hyperliquid 限价单:返回 statuses 为空,订单未成功提交") - return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} - - # 判断订单实际状态:resting(挂单中)还是 filled(立即成交) - first_status = statuses[0] - if "resting" in first_status: - order_id = first_status["resting"].get("oid") - order_status = "resting" - side = "买入" if is_buy else "卖出" - logger.info(f"✅ Hyperliquid 限价单已挂出: {side} {symbol} {size} @ ${price} (oid={order_id})") - elif "filled" in first_status: - order_status = "filled" - filled_info = first_status["filled"] - avg_px = filled_info.get("avgPx", price) - logger.info(f"✅ Hyperliquid 限价单立即成交: {symbol} {size} @ ${avg_px}") - order_id = filled_info.get("oid") - else: - # 未知状态,记录并视为成功但标记 unknown - order_status = "unknown" - order_id = None - logger.warning(f"⚠️ Hyperliquid 限价单状态未知: {first_status}") - - side = "买入" if is_buy else "卖出" - return { - "success": True, - "order_status": order_status, # "resting" | "filled" | "unknown" - "order_id": order_id, - "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: - # 使用 open_orders API 获取挂单 - orders_data = self.info.open_orders(self.wallet_address) - - orders = [] - for order in orders_data or []: - coin = order.get("coin") - if symbol and coin != symbol: - continue - - # side: "A" = ask (sell/做空), "B" = bid (buy/做多) - side = order.get("side") - is_buy = (side == "B") - - # Hyperliquid API 不直接返回 reduce_only 标记 - # 但我们可以根据其他信息判断 - # 暂时将所有订单都标记为非 reduce_only - # Hyperliquid API 返回 reduceOnly(驼峰),不是 reduce_only - is_reduce_only = order.get("reduceOnly", order.get("reduce_only", False)) - - orders.append({ - "order_id": order.get("oid"), - "symbol": coin, - "side": "buy" if is_buy else "sell", - "size": float(order.get("sz", 0)), - "price": float(order.get("limitPx", 0)), - "is_reduce_only": is_reduce_only, - "order_type": order.get("orderType", {}), - "timestamp": order.get("timestamp"), - "original_size": float(order.get("origSz", 0)), - "raw_side": side, - "created_at": datetime.fromtimestamp(order.get("timestamp", 0) / 1000).isoformat() - if order.get("timestamp") else None, - }) - - 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", {}) - # 防御性检查:确保 order_type 是 dict - if not isinstance(order_type, dict): - continue - - # 止盈:限价单 - 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: - {"success": bool, "tp_set": bool, "sl_set": bool, "errors": [...]} - success=True 仅当所有请求的都设置成功 - """ - result = {"success": False, "tp_set": False, "sl_set": False, "errors": []} - close_is_buy = not is_long # 平多头=卖出,平空头=买入 - - # 设置止盈(限价单)— 独立 try-except,失败不影响止损 - if tp_price: - try: - tp_price = round(float(tp_price), 5) - tp_result = self.exchange.order( - symbol, close_is_buy, size, tp_price, - {"limit": {"tif": "Gtc"}}, - reduce_only=True - ) - # 验证响应 - if tp_result.get("status") == "ok": - statuses = tp_result.get("response", {}).get("data", {}).get("statuses", []) - error_statuses = [s for s in statuses if "error" in s] - if error_statuses: - err_msg = error_statuses[0]["error"] - logger.warning(f"设置止盈失败: {symbol} {err_msg}") - result["errors"].append(f"止盈设置失败: {err_msg}") - else: - result["tp_set"] = True - logger.info(f"✅ 设置止盈: {symbol} @ ${tp_price}") - else: - err_msg = tp_result.get("response", str(tp_result)) - logger.warning(f"设置止盈失败: {symbol} {err_msg}") - result["errors"].append(f"止盈设置失败: {err_msg}") - except Exception as e: - logger.warning(f"设置止盈失败: {symbol} {e}") - result["errors"].append(f"止盈设置失败: {e}") - - # 设置止损(触发单)— 独立 try-except,失败不影响止盈 - if sl_price: - try: - # 买单止损:exec_px 略高于 trigger(接受更高的买入价) - # 卖单止损:exec_px 略低于 trigger(接受更低的卖出价) - exec_px = sl_price * 1.001 if close_is_buy else sl_price * 0.999 - sl_price = round(float(sl_price), 5) - exec_px = round(float(exec_px), 5) - sl_result = self.exchange.order( - symbol, close_is_buy, size, exec_px, - {"trigger": {"triggerPx": sl_price, "isMarket": True, "tpsl": "sl"}}, - reduce_only=True - ) - # 验证响应 - if sl_result.get("status") == "ok": - statuses = sl_result.get("response", {}).get("data", {}).get("statuses", []) - error_statuses = [s for s in statuses if "error" in s] - if error_statuses: - err_msg = error_statuses[0]["error"] - logger.warning(f"设置止损失败: {symbol} {err_msg}") - result["errors"].append(f"止损设置失败: {err_msg}") - else: - result["sl_set"] = True - logger.info(f"✅ 设置止损: {symbol} @ ${sl_price}(触发)") - else: - err_msg = sl_result.get("response", str(sl_result)) - logger.warning(f"设置止损失败: {symbol} {err_msg}") - result["errors"].append(f"止损设置失败: {err_msg}") - except Exception as e: - logger.warning(f"设置止损失败: {symbol} {e}") - result["errors"].append(f"止损设置失败: {e}") - - # 判断整体成功 - requested_tp = tp_price is not None - requested_sl = sl_price is not None - all_ok = (not requested_tp or result["tp_set"]) and (not requested_sl or result["sl_set"]) - result["success"] = all_ok - - if all_ok: - logger.info(f"✅ 止盈止损设置完成: {symbol} TP={tp_price} SL={sl_price}") - elif result["tp_set"] or result["sl_set"]: - logger.warning(f"⚠️ 止盈止损部分成功: {symbol} tp_set={result['tp_set']} sl_set={result['sl_set']}") - else: - logger.error(f"❌ 止盈止损设置失败: {symbol} errors={result['errors']}") - - return result - - 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: - 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} - 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: - # Hyperliquid SDK 没有 cancel_all_orders 方法,需要先查询再逐个取消 - orders = self.get_open_orders(symbol) - - results = [] - for order in orders: - order_symbol = order.get('symbol') - order_id = order.get('order_id') - try: - result = self.exchange.cancel(order_symbol, order_id) - results.append(result) - except Exception as e: - logger.warning(f"取消订单失败: {order_symbol} #{order_id} - {e}") - - logger.info(f"取消所有订单: {symbol or '全部'} ({len(results)} 个)") - return {"success": True, "result": results, "cancelled_count": len(results)} - 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") - if not coin: - continue - results.append(self.market_close_position(coin)) - - all_ok = all(result.get("success") for result in results) - logger.info(f"🚨 紧急平仓完成,共平仓 {len(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: - 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 - - 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") if isinstance(position_data.get("leverage"), dict) else position_data.get("leverage"), - "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 # 保留原始数据 - }) - - return positions - except Exception as e: - logger.error(f"获取持仓失败: {e}") - return [] - - 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"] == normalized_symbol: - return pos - return None - - def get_sz_decimals(self, symbol: str) -> int: - """ - 获取交易对的数量精度(szDecimals) - - Hyperliquid 要求订单数量必须符合各币种精度,否则报 'Order has invalid size' - 例如 ETH=3(最小 0.001),BTC=5(最小 0.00001) - """ - try: - asset = self.info.name_to_asset(symbol) - return self.info.asset_to_sz_decimals.get(asset, 3) - except Exception: - logger.warning(f"获取 {symbol} szDecimals 失败,使用默认值 3") - return 3 - - def _sanitize_size(self, symbol: str, size: float) -> float: - """ - 精度保护:确保 size 符合 Hyperliquid szDecimals 要求 - - 这是防止 float_to_wire causes rounding 错误的最后防线。 - """ - import math - try: - sz_decimals = self.get_sz_decimals(symbol) - factor = 10 ** sz_decimals - sanitized = math.floor(float(size) * factor) / factor - if sanitized != size: - logger.info(f" 精度截断: {size} → {sanitized} ({symbol} szDecimals={sz_decimals})") - return sanitized - except Exception as e: - logger.warning(f" 精度截断失败: {e},使用原值 {size}") - return size - - -# 单例 -_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/openclawfi/README.md b/backend/openclawfi/README.md deleted file mode 100644 index fc6ee73..0000000 --- a/backend/openclawfi/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# ClawFi Hyperliquid Trading Skill 🦅 - -A one-click, cross-platform installation tool designed for AI Agents operating on **ClawFi — The On-Chain Wall Street for Agents**. - -This package automatically: - -1. **Installs SDKs**: Fetches `hyperliquid-python-sdk` and `eth-account` for your Python environment. -2. **Deploys Documentation**: Injects the canonical `SKILL.md` (trading rules, integration limits, and API examples) directly into your agent's context directory (Global `~/.agents/skills` or a local `openclaw` project). -3. **Injects Variables**: Provides an interactive way to store your authorized keys. - ---- - -## ⚡ Installation - -Install and configure everything in one setup step. You can securely pass your variables natively, and we'll append them to your shell profile (`~/.zshrc`, `~/.bashrc`, or Windows Registry). - -```bash -npx clawfi-hyperliquid-skill \ - --wallet=0xYourMainWalletAddress \ - --key=0xYourAgentPrivateKey -``` - -_(You can also run without arguments and configure the variables manually later)._ - -## 🔑 Environment Variables - -To operate safely on ClawFi's API Wallet architecture, your Agent requires these variables: - -| Variable | Description | -| ----------------------- | ---------------------------------------------------------------------------- | -| `CLAWFI_WALLET_ADDRESS` | Your **main account address** that holds the actual trading balance. | -| `CLAWFI_PRIVATE_KEY` | Your **Agent Key** (Proxy API Key) private key. Never the main wallet's key! | - -## 🛡️ Core Trading Restrictions (ClawFi Rules) - -1. **Leverage is Global**: You cannot set leverage inside an order call; you must call `update_leverage()` per asset before opening positions. Leverage **must be ≤ 10x**. -2. **10% Circuit Breaker**: You must halt trading and close all open positions if total drawdown reaches 10% of the initial allocated balance. -3. **No Asset Transfers**: Your Agent Key has zero withdrawal/transfer permissions by design. -4. **No Market Manipulation**: Wash trading and spoofing are strictly forbidden. - -## 📚 What's Next? - -After installation, tell your Agent to read its new skill documentation. - -- **Global Agents**: Tell your Agent to read `~/.agents/skills/clawfi-hyperliquid/SKILL.md` -- **OpenClaw Agents**: Tell your Agent to read `[project_root]/.agents/skills/clawfi-hyperliquid/SKILL.md` - -Your Agent will learn how to initialize the connection securely, fetch account balances dynamically, respect Tick Size rounding, and place Limit/Market/TP/SL orders. diff --git a/backend/openclawfi/install.js b/backend/openclawfi/install.js deleted file mode 100755 index ad0625c..0000000 --- a/backend/openclawfi/install.js +++ /dev/null @@ -1,711 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// ═══════════════════════════════════════════════════════════════════ -// ClawFi Hyperliquid Skill Installer -// Cross-platform: macOS · Linux · Windows -// ═══════════════════════════════════════════════════════════════════ - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const { execSync, spawnSync } = require('child_process'); -const readline = require('readline'); - -// ─── Logging Helpers ──────────────────────────────────────────────────────── -function log(emoji, msg) { console.log(`${emoji} ${msg}`); } -function ok(msg) { console.log(`\u2705 ${msg}`); } -function warn(msg) { console.warn(`\u26a0\ufe0f ${msg}`); } -function fail(msg) { console.error(`\u274c ${msg}`); } -function section(title) { console.log(`\n${'─'.repeat(55)}\n ${title}\n${'─'.repeat(55)}`); } - -// ─── Global Constants ─────────────────────────────────────────────────────── -const homeDir = os.homedir(); -const platform = os.platform(); // 'darwin' | 'linux' | 'win32' -const isWindows = platform === 'win32'; -const isMac = platform === 'darwin'; -const isLinux = platform === 'linux'; -const SKILL_NAME = 'clawfi-hyperliquid'; - -// ─── CLI Args: --wallet=0x... --key=0x... ─────────────────────────────────── -const args = {}; -process.argv.slice(2).forEach(a => { - const m = a.match(/^--([\w]+)=(.+)$/); - if (m) args[m[1]] = m[2]; -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// SKILL CONTENT (English, embedded into SKILL.md at install time) -// ═══════════════════════════════════════════════════════════════════════════ -const SKILL_CONTENT = `--- -name: "clawfi-hyperliquid" -description: "Hyperliquid exchange integration toolkit for ClawFi Agents. Covers account management (Vault/Agent modes), trade execution (Market, Limit, TP/SL), position monitoring, and mandatory risk controls." ---- - -# Hyperliquid Trading Skill — ClawFi Edition - -## 1. Background & Trading Advice - -### 1.1 Background -This Agent is operating under **ClawFi** — *The On-Chain Wall Street for Agents*. -ClawFi provides each participating Agent with a risk-controlled allocation of real capital on the Hyperliquid perpetuals exchange. This Skill is the Agent's canonical reference for: -- Correct SDK usage patterns -- ClawFi-specific authentication architecture -- Mandatory risk control rules that must never be bypassed - -### 1.2 Trading Advice -> **To the Agent:** Risk control is your first priority — always. Protect your allocation. -> The market is full of uncertainty; learn from every drawdown and evolve into a mature, resilient investor. -> Your long-term track record matters more than any single trade. - ---- - -## 2. Environment Variables - -These two variables **must** be set before the Agent can trade. They are injected into your shell profile by the installer, or can be set manually. - -| Variable | Purpose | -|---|---| -| \`CLAWFI_WALLET_ADDRESS\` | Your **main account address** — the wallet that actually holds funds on Hyperliquid (your "bank account number"). | -| \`CLAWFI_PRIVATE_KEY\` | The **Agent Key's private key** — *not* your main wallet private key. ClawFi uses an "API Wallet" model: a dedicated sub-signing key that authorizes trade requests on behalf of your main wallet, without ever exposing the main private key to any automated system. Think of it as an authorized proxy card that can place trades but cannot withdraw or transfer funds. | - -### Simple analogy -- \`WALLET_ADDRESS\` = your bank account number -- \`PRIVATE_KEY\` = an authorized proxy card — can operate the account but **cannot withdraw or transfer** (Agent Keys have no \`withdraw\`/\`transfer\` permissions by design) - -> ⚠️ **Security Rules:** -> - NEVER hardcode either value in source code. -> - NEVER print, log, or expose \`CLAWFI_PRIVATE_KEY\` in any output. -> - Always load via \`os.getenv("CLAWFI_PRIVATE_KEY")\` at runtime. - ---- - -## 3. ClawFi Trading Standards & Risk Management - -### 3.1 Mandatory 10% Circuit Breaker -Monitor \`marginSummary.accountValue\` continuously. If total losses reach **10% of the initial allocated balance**: -1. Immediately stop opening new positions. -2. **Market-close** all open positions. -3. **Cancel** all resting orders. -4. Report the circuit breaker trigger and terminate gracefully. - -### 3.2 Asset Security -- **No transfers**: Never call \`withdraw\` or \`transfer\` endpoints. Agent Keys are structurally incapable of withdrawing — this is ClawFi's built-in safeguard. -- **Zero key leakage**: Never expose \`CLAWFI_PRIVATE_KEY\` in logs, chats, or front-ends. - -### 3.3 Compliant Trading -- **No wash trading**: Self-trades to inflate volume are strictly forbidden. -- **No spoofing**: Malicious quoting behaviors that manipulate the order book are forbidden. - ---- - -## 4. Key Concepts & Common Pitfalls - -### 4.1 ⚠️ Leverage MUST Be Set Separately (Critical) - -Hyperliquid does **NOT** support specifying leverage inside an order call. -Leverage is an **account-level global setting** per coin — it must be configured via a dedicated API call **before** opening any position. If you skip this step, the exchange silently uses the last configured value or its default. You cannot control leverage by passing a parameter to \`order()\` — that parameter does not exist. - -> ❌ **Wrong assumption:** "I can set leverage inside order() each time." — This is incorrect. -> ✅ **Correct pattern:** Call \`update_leverage()\` once at agent startup (or before switching coins), then trade. - -\`\`\`python -def set_leverage(exchange, coin: str, leverage: int, is_cross: bool = False) -> dict: - """ - Set leverage for a coin globally (affects all future positions for that coin). - Args: - coin: Coin symbol, e.g. "BTC", "ETH", "SOL" - leverage: Integer multiplier, e.g. 3 for 3x - is_cross: True = cross margin, False = isolated margin (default) - """ - return exchange.update_leverage(leverage, coin, is_cross) - -# ── Correct pattern ──────────────────────────────────────────────── -# Set leverage once at startup, then trade freely -set_leverage(exc, "BTC", leverage=3, is_cross=False) # 3x isolated -set_leverage(exc, "ETH", leverage=5, is_cross=False) # 5x isolated - -res = place_market_order(exc, "BTC", is_buy=True, size=0.01) -\`\`\` - -**ClawFi Leverage Rules:** -- Always call \`set_leverage()\` explicitly before the first trade on any coin. -- Re-call it before switching coins or changing strategy parameters. -- Do **not** rely on default or previously cached leverage values. -- ClawFi compliance guideline: keep leverage at **≤ 10x**. - -### 4.2 Authentication Architecture -ClawFi uses **Agent Key (API Proxy) mode** — the signer and the fund holder are *different* accounts: -- Always pass \`account_address=CLAWFI_WALLET_ADDRESS\` when initializing the SDK. -- Without it, the SDK defaults to the empty Agent Key wallet — no funds will be available. - -### 4.3 Withdrawable vs. Account Value -If \`withdrawable\` shows \`$0\`, **the account is NOT empty**. -Funds are likely locked as perpetual margin. This does NOT prevent order placement. -Always use \`marginSummary.accountValue\` for drawdown calculations. - -### 4.4 Tick Size (Price Precision) -Prices must conform to each asset's tick size or the exchange rejects the order with: -\`Price must be divisible by tick size\` - -\`\`\`python -def round_to_tick(price: float, tick: float = 1.0) -> float: - return round(round(price / tick) * tick, len(str(tick).rstrip('0').split('.')[-1])) - -def get_tick_size(info, coin: str) -> float: - for asset in info.meta()["universe"]: - if asset["name"] == coin: - return float(asset.get("tickSize", 1.0)) - return 1.0 -\`\`\` - -| Asset | asset index | szDecimals | tick size | -|-------|-------------|------------|-----------| -| BTC | 0 | 5 | 1.0 | -| ETH | 1 | 4 | 0.1 | -| SOL | 5 | 1 | 0.001 | - -### 4.5 API Response Format -\`\`\`python -# Resting limit order (waiting to fill) -{"status": "ok", "response": {"type": "order", "data": {"statuses": [{"resting": {"oid": 12345}}]}}} - -# Instantly filled (market order) -{"status": "ok", "response": {"type": "order", "data": {"statuses": [{"filled": {"totalSz": "1.0", "avgPx": "200.5", "oid": 12346}}]}}} - -def is_order_ok(res: dict) -> bool: - if res.get("status") != "ok": return False - statuses = res.get("response", {}).get("data", {}).get("statuses", []) - return all("error" not in s for s in statuses) -\`\`\` - ---- - -## 5. ⚠️ Pre-Trade Order Confirmation Protocol (MANDATORY) - -**This rule applies to EVERY trade. No exceptions.** - -Before submitting any order to the exchange, the Agent MUST present a complete order summary to the user and wait for explicit approval. Only execute the trade after the user confirms. - -**The only exception:** The user has explicitly said "execute immediately" / "no confirmation needed" / "auto-trade" for the current session or instruction. Even then, the Agent should acknowledge this mode is active. - -### 5.1 Required Confirmation Checklist - -Before placing any order, the Agent MUST surface all of the following to the user: - -\`\`\` -📋 ORDER CONFIRMATION REQUIRED -───────────────────────────────────────────── - Asset : BTC - Direction : LONG (Buy) - Order Type : Market - Size : 0.05 BTC (~$4,250 notional) - Leverage : 3x (Isolated) ← always confirm this - Take-Profit : $91,000 (+7.0%) - Stop-Loss : $82,000 (-3.5%) -───────────────────────────────────────────── - Account Value : $1,000.00 - Current Drawdown: 0.0% (Limit: 10%) - Est. Liquidation: ~$79,400 -───────────────────────────────────────────── -Type "confirm" to place this order, or "cancel" to abort. -\`\`\` - -### 5.2 Leverage Confirmation Is Non-Negotiable - -Leverage **must always be shown** in the confirmation, even if it has not changed since last trade. -- State whether it is **Isolated** or **Cross** margin. -- If leverage has not been explicitly set this session, warn the user: _"Leverage has not been set this session — will use last configured value. Please confirm or specify a new value."_ -- Never proceed without knowing the active leverage. - -### 5.3 Confirmation Modes - -| Mode | How the user activates it | Agent behavior | -|------|--------------------------|----------------| -| **Standard (default)** | — | Show confirmation panel, wait for "confirm" / "yes" / "go" | -| **Auto-execute** | "Execute immediately" / "no confirmation" / "auto-trade" | Skip panel, but log the order params before submitting | -| **Re-enable confirmation** | "Ask me before trading" / "confirmation mode" | Restore standard mode | - ---- - -## 6. Core Implementation Patterns - -### 6.1 Initialization -\`\`\`python -from eth_account import Account -from hyperliquid.info import Info -from hyperliquid.exchange import Exchange -from hyperliquid.utils import constants -import os - -def init_hyperliquid(private_key: str, target_address: str = None, is_vault: bool = False): - """ - Initialize the trading environment. - Args: - private_key: Agent Key private key (CLAWFI_PRIVATE_KEY) - target_address: Main wallet address (CLAWFI_WALLET_ADDRESS) - is_vault: True if target_address is a Vault address - Returns: - (account, exchange, info) tuple - """ - account = Account.from_key(private_key) - base_url = constants.MAINNET_API_URL - if target_address and target_address.lower() == account.address.lower(): - target_address = None # signer == target; no need to specify - info = Info(base_url, skip_ws=True) - if is_vault: - exchange = Exchange(account, base_url, vault_address=target_address) - else: - exchange = Exchange(account, base_url, account_address=target_address) - return account, exchange, info -\`\`\` - -### 6.2 Account State -\`\`\`python -def get_account_state(info: Info, address: str) -> dict: - """Returns net equity, withdrawable balance, and active positions.""" - user_state = info.user_state(address) - margin_summary = user_state.get("marginSummary", {}) - account_value = float(margin_summary.get("accountValue", 0)) - withdrawable = float(margin_summary.get("withdrawable", 0)) - positions = [] - for pos in user_state.get("assetPositions", []): - p = pos.get("position", {}) - size = float(p.get("szi", 0)) - if size == 0: continue # skip closed/flat positions - positions.append({ - "coin": p.get("coin"), - "size": size, # positive=long, negative=short - "entry_price": float(p.get("entryPx", 0)), - "pnl": float(p.get("unrealizedPnl", 0)), - "liquidation_price": p.get("liquidationPx"), - "leverage": p.get("leverage", {}).get("value"), - }) - return { - "value": account_value, # ⚠️ use this for drawdown calc, NOT withdrawable - "withdrawable": withdrawable, - "positions": positions, - } -\`\`\` - -### 6.3 Trade Execution -\`\`\`python -def place_market_order(exchange, coin, is_buy, size): - """Market open/add (SDK sends an IOC limit order internally).""" - return exchange.market_open(coin, is_buy, size) - -def place_limit_order(exchange, coin, is_buy, size, limit_price, tif="Gtc"): - """tif: 'Gtc' | 'Ioc' | 'Alo' (post-only)""" - return exchange.order(coin, is_buy, size, limit_price, {"limit": {"tif": tif}}) - -def place_tp_sl(exchange, coin, is_long, size, tp_price=None, sl_price=None): - """Attach Take-Profit and Stop-Loss to an existing position.""" - results, close_is_buy = [], not is_long - if tp_price: - results.append(exchange.order(coin, close_is_buy, size, tp_price, - {"limit": {"tif": "Gtc"}}, reduce_only=True)) - if sl_price: - exec_px = sl_price * 0.999 if close_is_buy else sl_price * 1.001 - results.append(exchange.order(coin, close_is_buy, size, exec_px, - {"trigger": {"triggerPx": sl_price, "isMarket": True, "tpsl": "sl"}}, - reduce_only=True)) - return results - -def cancel_order(exchange, coin, oid): - """Cancel a specific order by its oid.""" - return exchange.cancel(coin, oid) - -def cancel_all_orders(exchange): - """Cancel every resting order on the account.""" - return exchange.cancel_all_orders() - -def close_all_positions(exchange, positions): - """Market-close all open positions (circuit breaker).""" - for pos in positions: - if pos["size"] != 0: - exchange.market_close(pos["coin"]) -\`\`\` - -### 6.4 Risk Guardian (Circuit Breaker) -\`\`\`python -def check_risk_limits(current_value: float, initial_value: float, - drawdown_limit: float = 0.10) -> bool: - """ - Returns True if the circuit breaker threshold has been hit. - Call before every trade cycle. If True: cancel all orders, close all positions, halt. - """ - if initial_value <= 0: return False - drawdown = (initial_value - current_value) / initial_value - if drawdown >= drawdown_limit: - print(f"[RISK ALERT] Drawdown {drawdown:.2%} hit the circuit breaker! " - f"(Threshold: {drawdown_limit:.2%}, Initial: \${initial_value:.2f})") - return True - return False -\`\`\` - ---- - -## 6. Full Usage Example -\`\`\`python -import os, sys - -# Step 1 — Load credentials from environment (NEVER hardcode) -private_key = os.getenv("CLAWFI_PRIVATE_KEY") -wallet_address = os.getenv("CLAWFI_WALLET_ADDRESS") -initial_balance = float(os.getenv("CLAWFI_INITIAL_BALANCE", "1000.0")) - -if not private_key: - sys.exit("ERROR: CLAWFI_PRIVATE_KEY is not set. Run the installer or export it manually.") - -# Step 2 — Initialize -acc, exc, info = init_hyperliquid(private_key, target_address=wallet_address) -target_addr = wallet_address or acc.address -print(f"Agent signer : {acc.address}") -print(f"Target vault : {target_addr}") - -# Step 3 — Check risk before every cycle -state = get_account_state(info, target_addr) -print(f"Net value: \${state['value']:.2f} | Withdrawable: \${state['withdrawable']:.2f}") - -if check_risk_limits(state["value"], initial_balance): - cancel_all_orders(exc) - close_all_positions(exc, state["positions"]) - sys.exit("HALTED: circuit breaker triggered — max drawdown exceeded.") - -# Step 4 — Show positions -for p in state["positions"]: - side = "LONG " if p["size"] > 0 else "SHORT" - print(f" [{side}] {p['coin']}: qty={abs(p['size']):.4f}, " - f"entry=\${p['entry_price']}, PnL=\${p['pnl']:.2f}") - -# Step 5 — Place a trade (uncomment to use) -# tick = get_tick_size(info, "SOL") -# res = place_market_order(exc, "SOL", is_buy=True, size=1.0) -# if is_order_ok(res): -# place_tp_sl(exc, "SOL", is_long=True, size=1.0, -# tp_price=round_to_tick(state_price * 1.15, tick), -# sl_price=round_to_tick(state_price * 0.92, tick)) -\`\`\` -`; - -// ═══════════════════════════════════════════════════════════════════ -// STEP 0 — Banner -// ═══════════════════════════════════════════════════════════════════ -console.log('\n' + '═'.repeat(55)); -console.log(' 🦅 ClawFi Hyperliquid Skill Installer v1.0.1'); -console.log(` Platform: ${platform} | Node: ${process.version}`); -console.log('═'.repeat(55)); - -// Parse --wallet= --key= from CLI -const cliWallet = args['wallet'] || ''; -const cliKey = args['key'] || ''; - -// ═══════════════════════════════════════════════════════════════════ -// STEP 1 — Detect Python runtime -// ═══════════════════════════════════════════════════════════════════ -section('Step 1 · Detecting Python Runtime'); - -function detectPython() { - const candidates = isWindows ? ['python', 'python3', 'py'] : ['python3', 'python']; - for (const cmd of candidates) { - const r = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: isWindows }); - if (r.status === 0) return cmd; - } - return null; -} - -function detectPip(pythonCmd) { - // Prefer `python -m pip` — guaranteed to match the right interpreter - const r = spawnSync(pythonCmd, ['-m', 'pip', '--version'], - { encoding: 'utf8', shell: isWindows }); - if (r.status === 0) return [pythonCmd, '-m', 'pip']; - // Fallback to standalone binaries - const bins = isWindows ? ['pip', 'pip3'] : ['pip3', 'pip']; - for (const cmd of bins) { - const r2 = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: isWindows }); - if (r2.status === 0) return [cmd]; - } - return null; -} - -const pythonCmd = detectPython(); -if (!pythonCmd) { - fail('Python 3.8+ not found. Install from https://www.python.org/downloads/'); - process.exit(1); -} -const pipCmd = detectPip(pythonCmd); -if (!pipCmd) { - fail('pip not found. Ensure pip is bundled with your Python installation.'); - process.exit(1); -} - -const pyVer = spawnSync(pythonCmd, ['--version'], { encoding: 'utf8', shell: isWindows }).stdout.trim(); -ok(`Python check passed: ${pyVer} (pip: ${pipCmd.join(' ')})`); - -// ═══════════════════════════════════════════════════════════════════ -// STEP 2 — Locate skill directories (openclaw-specific + global) -// ═══════════════════════════════════════════════════════════════════ -section('Step 2 · Locating Agent Skill Directories'); - -function findOpenclawDir() { - let current = process.cwd(); - const root = path.parse(current).root; - while (current && current !== root) { - if (path.basename(current) === 'openclaw') return current; - const sub = path.join(current, 'openclaw'); - if (fs.existsSync(sub) && fs.statSync(sub).isDirectory()) return sub; - current = path.dirname(current); - } - const candidates = [ - path.join(homeDir, 'openclaw'), - path.join(homeDir, 'Documents', 'openclaw'), - path.join(homeDir, 'Projects', 'openclaw'), - path.join(homeDir, 'workspace', 'openclaw'), - path.join(homeDir, 'dev', 'openclaw'), - ]; - for (const p of candidates) { - if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p; - } - return null; -} - -function findGlobalSkillDir() { - for (const name of ['.agents', '.agent', '_agents', '_agent']) { - const d = path.join(homeDir, name, 'skills'); - if (fs.existsSync(d)) return d; - } - const def = path.join(homeDir, '.agents', 'skills'); - fs.mkdirSync(def, { recursive: true }); - return def; -} - -const installDirs = []; -const openclawRoot = findOpenclawDir(); - -if (openclawRoot) { - log('🎯', `openclaw project detected: ${openclawRoot}`); - const dir = path.join(openclawRoot, '.agents', 'skills'); - fs.mkdirSync(dir, { recursive: true }); - installDirs.push({ label: 'openclaw project', path: dir }); -} else { - log('ℹ️ ', 'No openclaw project found — skipping project-scoped install.'); -} - -const globalDir = findGlobalSkillDir(); -installDirs.push({ label: 'global (~/.agents/skills)', path: globalDir }); - -// ═══════════════════════════════════════════════════════════════════ -// STEP 3 — Write SKILL.md -// ═══════════════════════════════════════════════════════════════════ -section('Step 3 · Installing SKILL.md'); - -for (const dir of installDirs) { - const target = path.join(dir.path, SKILL_NAME); - fs.mkdirSync(target, { recursive: true }); - const dest = path.join(target, 'SKILL.md'); - fs.writeFileSync(dest, SKILL_CONTENT, 'utf8'); - ok(`[${dir.label}] → ${dest}`); -} - -// ═══════════════════════════════════════════════════════════════════ -// STEP 4 — Install Python dependencies -// ═══════════════════════════════════════════════════════════════════ -section('Step 4 · Installing Python Dependencies'); - -const packages = ['hyperliquid-python-sdk', 'eth-account']; -const sysBreak = (isMac || isLinux) ? ['--break-system-packages'] : []; -const pipBin = pipCmd[0]; -const pipBaseArgs = [...pipCmd.slice(1), 'install', ...packages]; - -log('📦', `Installing: ${packages.join(', ')}`); - -let pipOk = false; -// Primary attempt -try { - execSync([pipBin, ...pipBaseArgs, ...sysBreak].join(' '), - { stdio: 'inherit', shell: isWindows }); - pipOk = true; -} catch (_) { - warn('Primary install (with --break-system-packages) failed. Retrying without it...'); -} -// Fallback attempt -if (!pipOk) { - try { - execSync([pipBin, ...pipBaseArgs].join(' '), - { stdio: 'inherit', shell: isWindows }); - pipOk = true; - } catch (_) { - fail(`pip install failed. Please run manually:\n pip3 install ${packages.join(' ')}`); - } -} - -if (pipOk) ok('Python packages installed successfully.'); - -// ═══════════════════════════════════════════════════════════════════ -// STEP 5 — Verify SDK imports -// ═══════════════════════════════════════════════════════════════════ -section('Step 5 · Verifying SDK Imports'); - -const verifyPy = [ - 'import sys', - 'try:', - ' from hyperliquid.info import Info', - ' from hyperliquid.exchange import Exchange', - ' from eth_account import Account', - ' print("OK")', - 'except ImportError as e:', - ' print(f"FAIL:{e}")', - ' sys.exit(1)', -].join('\n'); - -const vr = spawnSync(pythonCmd, ['-c', verifyPy], { encoding: 'utf8', shell: isWindows }); -if (vr.status === 0 && vr.stdout.trim() === 'OK') { - ok('hyperliquid-python-sdk + eth-account import verification passed.'); -} else { - warn(`SDK import verification failed: ${vr.stdout.trim()} ${vr.stderr.trim()}`); - warn('SKILL.md was installed but Python SDK may need manual setup.'); -} - -// ═══════════════════════════════════════════════════════════════════ -// STEP 6 — Environment Variable Injection -// ═══════════════════════════════════════════════════════════════════ -section('Step 6 · Environment Variable Setup'); - -// Decide values: CLI args > existing env > empty (user will fill later) -const walletVal = cliWallet || process.env['CLAWFI_WALLET_ADDRESS'] || ''; -const keyVal = cliKey || process.env['CLAWFI_PRIVATE_KEY'] || ''; - -function writeEnvToShellProfile(wallet, key) { - const profileFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile']; - let written = false; - - for (const profileName of profileFiles) { - const profilePath = path.join(homeDir, profileName); - if (!fs.existsSync(profilePath)) continue; - - const content = fs.readFileSync(profilePath, 'utf8'); - const lines = []; - - // Wallet - if (wallet) { - const walletLine = `export CLAWFI_WALLET_ADDRESS="${wallet}"`; - if (!content.includes('CLAWFI_WALLET_ADDRESS')) { - lines.push(walletLine); - } else { - log('ℹ️ ', `CLAWFI_WALLET_ADDRESS already set in ${profileName} — skipping.`); - } - } - // Key - if (key) { - const keyLine = `export CLAWFI_PRIVATE_KEY="${key}"`; - if (!content.includes('CLAWFI_PRIVATE_KEY')) { - lines.push(keyLine); - } else { - log('ℹ️ ', `CLAWFI_PRIVATE_KEY already set in ${profileName} — skipping.`); - } - } - - if (lines.length > 0) { - const block = `\n# ClawFi Hyperliquid Agent — added by installer\n${lines.join('\n')}\n`; - fs.appendFileSync(profilePath, block, 'utf8'); - ok(`Environment variables written to ~/${profileName}`); - written = true; - } - break; // write to the first found profile only - } - return written; -} - -function writeEnvToWindowsRegistry(wallet, key) { - try { - if (wallet) { - execSync(`setx CLAWFI_WALLET_ADDRESS "${wallet}"`, { stdio: 'pipe' }); - ok('CLAWFI_WALLET_ADDRESS set in Windows user environment.'); - } - if (key) { - execSync(`setx CLAWFI_PRIVATE_KEY "${key}"`, { stdio: 'pipe' }); - ok('CLAWFI_PRIVATE_KEY set in Windows user environment.'); - } - return true; - } catch (e) { - warn(`setx failed: ${e.message}`); - return false; - } -} - -const hasValues = walletVal || keyVal; - -if (hasValues) { - if (walletVal) log('🔑', `CLAWFI_WALLET_ADDRESS = ${walletVal}`); - if (keyVal) log('🔐', `CLAWFI_PRIVATE_KEY = ${keyVal.slice(0, 6)}...${keyVal.slice(-4)}`); - - let envWritten = false; - if (isWindows) { - envWritten = writeEnvToWindowsRegistry(walletVal, keyVal); - } else { - envWritten = writeEnvToShellProfile(walletVal, keyVal); - if (!envWritten) { - // No profile file found — create .profile - const profilePath = path.join(homeDir, '.profile'); - const block = `\n# ClawFi Hyperliquid Agent — added by installer\n`; - let toAppend = block; - if (walletVal) toAppend += `export CLAWFI_WALLET_ADDRESS="${walletVal}"\n`; - if (keyVal) toAppend += `export CLAWFI_PRIVATE_KEY="${keyVal}"\n`; - fs.appendFileSync(profilePath, toAppend, 'utf8'); - ok(`Created ~/.profile with ClawFi environment variables.`); - } - } -} else { - warn('No environment variables provided via CLI flags.'); - log('ℹ️ ', 'To inject at install time, run with:'); - log(' ', ` npx clawfi-hyperliquid-skill --wallet=0xYourAddress --key=0xYourAgentKey`); - log('ℹ️ ', 'Or set manually in your shell profile:'); - log(' ', ' export CLAWFI_WALLET_ADDRESS="0xYourMainWallet"'); - log(' ', ' export CLAWFI_PRIVATE_KEY="0xYourAgentKey"'); -} - -// ═══════════════════════════════════════════════════════════════════ -// STEP 7 — Post-Install Summary -// ═══════════════════════════════════════════════════════════════════ -console.log('\n' + '═'.repeat(55)); -console.log(' 🚀 ClawFi Hyperliquid Skill — Installation Complete!'); -console.log('═'.repeat(55)); - -console.log('\n 📂 SKILL.md installed at:'); -for (const dir of installDirs) { - console.log(` [${dir.label}]`); - console.log(` → ${path.join(dir.path, SKILL_NAME, 'SKILL.md')}`); -} - -console.log('\n 🐍 Python SDK installed:'); -console.log(` • hyperliquid-python-sdk (exchange integration)`); -console.log(` • eth-account (wallet key management)`); - -console.log('\n 🔑 Environment Variables:'); -const walletStatus = walletVal ? `✅ Set → ${walletVal}` : '⚠️ Not set (required)'; -const keyStatus = keyVal ? `✅ Set (redacted for security)` : '⚠️ Not set (required)'; -console.log(` CLAWFI_WALLET_ADDRESS ${walletStatus}`); -console.log(` CLAWFI_PRIVATE_KEY ${keyStatus}`); - -console.log('\n ⚡ What the Agent can now do:'); -console.log(' ✦ Query account value, margin summary, open positions'); -console.log(' ✦ Place Market, Limit, Take-Profit and Stop-Loss orders'); -console.log(' ✦ Cancel individual or all resting orders'); -console.log(' ✦ Market-close all positions (circuit breaker)'); -console.log(' ✦ Verify 10% drawdown limits automatically'); - -console.log('\n 📋 Next steps:'); -if (!walletVal || !keyVal) { - console.log(' 1. Set missing environment variables and reload your shell:'); - if (!walletVal) console.log(' export CLAWFI_WALLET_ADDRESS="0xYourMainWallet"'); - if (!keyVal) console.log(' export CLAWFI_PRIVATE_KEY="0xYourAgentKey"'); - console.log(' source ~/.zshrc # or ~/.bashrc / ~/.profile'); -} else { - console.log(' 1. Reload your shell: source ~/.zshrc'); -} -console.log(' 2. Your Agent can now reference the SKILL.md for usage patterns.'); -console.log(' 3. Start with get_account_state() → check_risk_limits() → trade.'); -console.log('\n' + '─'.repeat(55)); -console.log(' ⚠️ Always run check_risk_limits() before every trade cycle.'); -console.log(' ⚠️ Never expose CLAWFI_PRIVATE_KEY in logs or source code.'); -console.log('─'.repeat(55) + '\n'); diff --git a/backend/openclawfi/package.json b/backend/openclawfi/package.json deleted file mode 100644 index 2427f5f..0000000 --- a/backend/openclawfi/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "clawfi-hyperliquid-skill", - "version": "1.0.6", - "description": "One-click installer for ClawFi x Hyperliquid Agent Skill. Automatically deploys trading skill documentation and installs Python dependencies.", - "main": "install.js", - "scripts": { - "start": "node install.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "clawfi", - "hyperliquid", - "agent", - "skill", - "trading" - ], - "author": "ClawFi", - "license": "ISC", - "type": "commonjs", - "bin": { - "install-clawfi": "./install.js" - } -} diff --git a/backend/requirements.txt b/backend/requirements.txt index fef603b..0ff8b40 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,7 +4,7 @@ langchain==0.1.0 langchain-community==0.0.20 zhipuai==2.0.1 openai>=1.0.0 -tushare>=1.4.0 # 升级到1.4+以支持更新的websocket-client(解决hyperliquid依赖冲突) +tushare>=1.4.0 sqlalchemy==2.0.25 pydantic==2.5.3 pydantic-settings==2.1.0 @@ -35,7 +35,4 @@ 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 deleted file mode 100644 index e769096..0000000 --- a/backend/test_hyperliquid.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/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) diff --git a/backend/test_hyperliquid_operations.py b/backend/test_hyperliquid_operations.py deleted file mode 100644 index e99fb85..0000000 --- a/backend/test_hyperliquid_operations.py +++ /dev/null @@ -1,355 +0,0 @@ -""" -Hyperliquid 小资金测试脚本 - -测试内容: -1. 获取账户状态 -2. 市价单测试(小金额) -3. 限价单测试(小金额) -4. 止盈止损设置 -5. 查询持仓和挂单 -6. 清理测试(平仓、取消挂单) - -运行方式: - python3 test_hyperliquid_operations.py - -⚠️ 请确保 .env 中已配置: - - CLAWFI_WALLET_ADDRESS - - CLAWFI_PRIVATE_KEY - - hyperliquid_trading_enabled=true -""" -import sys -import os -import time - -# 添加项目路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from app.services.hyperliquid_trading_service import HyperliquidTradingService -from app.utils.logger import logger - - -class HyperliquidTester: - """Hyperliquid 测试器""" - - def __init__(self): - """初始化测试器""" - try: - self.service = HyperliquidTradingService() - logger.info("✅ Hyperliquid 服务初始化成功") - except Exception as e: - logger.error(f"❌ 初始化失败: {e}") - raise - - def print_section(self, title: str): - """打印分隔线""" - print(f"\n{'='*60}") - print(f" {title}") - print(f"{'='*60}\n") - - def test_1_get_account_state(self): - """测试1:获取账户状态""" - self.print_section("测试1: 获取账户状态") - - state = self.service.get_account_state() - - print(f"💰 账户价值: ${state['account_value']:,.2f}") - print(f"📊 已用保证金: ${state['total_margin_used']:,.2f}") - print(f"💵 可用余额: ${state['available_balance']:,.2f}") - print(f"📈 持仓数量: {len(state['positions'])}") - - positions = self.service.get_open_positions() - if positions: - print(f"\n当前持仓:") - for pos in positions: - coin = pos['coin'] - size = pos['size'] - entry = pos['entry_price'] - pnl = pos['unrealized_pnl'] - print(f" - {coin}: {size:.4f} @ ${entry:,.2f} | PnL: ${pnl:,.2f}") - else: - print(f" 无持仓") - - return state - - def test_2_market_order(self, symbol: str = "BTC", is_buy: bool = True, size: float = 0.001): - """测试2:市价单(小金额测试)""" - self.print_section(f"测试2: 市价单 - {'买入' if is_buy else '卖出'} {symbol}") - - # 检查风险限制 - risk = self.service.check_risk_limits() - print(f"🔍 风险检查:") - print(f" 初始余额: ${risk['initial_balance']:,.2f}") - print(f" 当前价值: ${risk['current_value']:,.2f}") - print(f" 回撤: {risk['drawdown_percent']:.2f}%") - print(f" 安全交易: {'✅' if risk['safe_to_trade'] else '❌'}") - - if not risk['safe_to_trade']: - print("⚠️ 风险检查未通过,跳过测试") - return None - - # 更新杠杆 - print(f"\n⚙️ 设置杠杆为 2x...") - self.service.update_leverage(symbol, 2) - - # 下市价单 - print(f"\n📝 下市价单: {'买入' if is_buy else '卖出'} {size} {symbol}") - result = self.service.place_market_order( - symbol=symbol, - is_buy=is_buy, - size=size - ) - - if result.get('success'): - print(f"✅ 市价单成功") - print(f" 方向: {'买入' if is_buy else '卖出'}") - print(f" 数量: {size}") - print(f" 结果: {result.get('result')}") - - # 设置止盈止损 - print(f"\n🎯 设置止盈止损...") - current_positions = self.service.get_open_positions() - if current_positions: - pos = current_positions[0] - entry_price = pos['entry_price'] - is_long = pos['size'] > 0 - - # 计算止盈止损价格(小范围测试) - if is_long: - tp_price = entry_price * 1.01 # +1% - sl_price = entry_price * 0.995 # -0.5% - else: - tp_price = entry_price * 0.99 # -1% - sl_price = entry_price * 1.005 # +0.5% - - print(f" 入场价: ${entry_price:,.2f}") - print(f" 止盈价: ${tp_price:,.2f}") - print(f" 止损价: ${sl_price:,.2f}") - - tp_sl_result = self.service.set_tp_sl( - symbol=symbol, - is_long=is_long, - size=abs(size), - tp_price=tp_price, - sl_price=sl_price - ) - - if tp_sl_result.get('success'): - print(f"✅ 止盈止损设置成功") - else: - print(f"❌ 止盈止损设置失败: {tp_sl_result.get('error')}") - else: - print(f"❌ 市价单失败: {result.get('error')}") - - # 等待一下查看结果 - print(f"\n⏳ 等待2秒查看持仓状态...") - time.sleep(2) - - # 查询挂单 - orders = self.service.get_open_orders(symbol) - if orders: - print(f"\n📋 当前挂单:") - for order in orders: - print(f" - ID: {order['order_id']}") - print(f" 方向: {order['side']}") - print(f" 价格: ${order['price']:,.2f}") - print(f" 数量: {order['size']}") - else: - print(f"\n📋 无挂单") - - return result - - def test_3_limit_order(self, symbol: str = "BTC", is_buy: bool = True, size: float = 0.001): - """测试3:限价单(挂单测试)""" - self.print_section(f"测试3: 限价单 - {'买入' if is_buy else '卖出'} {symbol}") - - # 获取当前价格 - import requests - try: - response = requests.get("https://api.hyperliquid.xyz/info", json={ - "type": "meta", - "coin": symbol - }, timeout=5) - data = response.json() - # 从响应中获取当前价格(简化处理,使用一个估算值) - current_price = 95000 # 假设 BTC 当前价格 - except: - current_price = 95000 - - # 设置挂单价格(距离当前价格 1%) - if is_buy: - limit_price = current_price * 0.99 # 低于市价 1% - else: - limit_price = current_price * 1.01 # 高于市价 1% - - print(f"💡 当前价格估算: ${current_price:,.2f}") - print(f"📝 挂单价格: ${limit_price:,.2f} ({'低于' if is_buy else '高于'}市价1%)") - - # 下限价单 - result = self.service.place_limit_order( - symbol=symbol, - is_buy=is_buy, - size=size, - price=limit_price - ) - - if result.get('success'): - print(f"✅ 限价单成功") - print(f" 方向: {'买入' if is_buy else '卖出'}") - print(f" 数量: {size}") - print(f" 价格: ${limit_price:,.2f}") - - # 查询挂单 - orders = self.service.get_open_orders(symbol) - if orders: - print(f"\n📋 当前挂单:") - for order in orders: - print(f" - ID: {order['order_id']}") - print(f" 方向: {order['side']}") - print(f" 价格: ${order['price']:,.2f}") - print(f" 数量: {order['size']}") - - return result - else: - print(f"❌ 限价单失败: {result.get('error')}") - return None - - def test_4_check_tp_sl(self, symbol: str = "BTC"): - """测试4:查询止盈止损""" - self.print_section(f"测试4: 查询止盈止损 - {symbol}") - - tp_sl = self.service.get_tp_sl_prices(symbol) - - print(f"📊 止盈止损状态:") - if tp_sl['take_profit']: - print(f" ✅ 止盈: ${tp_sl['take_profit']:,.2f}") - else: - print(f" ➖ 止盈: 未设置") - - if tp_sl['stop_loss']: - print(f" ✅ 止损: ${tp_sl['stop_loss']:,.2f}") - else: - print(f" ➖ 止损: 未设置") - - return tp_sl - - def test_5_cleanup(self, symbol: str = "BTC"): - """测试5:清理(平仓、取消挂单)""" - self.print_section(f"测试5: 清理测试 - {symbol}") - - # 取消所有挂单 - print(f"🗑️ 取消所有挂单...") - cancel_result = self.service.cancel_all_orders(symbol) - if cancel_result.get('success'): - print(f"✅ 取消挂单成功") - else: - print(f"❌ 取消挂单失败: {cancel_result.get('error')}") - - # 平仓 - positions = self.service.get_open_positions() - symbol_positions = [p for p in positions if p['coin'] == symbol] - - if symbol_positions: - print(f"\n📤 平仓 {symbol}...") - for pos in symbol_positions: - size = abs(pos['size']) - is_long = pos['size'] > 0 - - result = self.service.place_market_order( - symbol=symbol, - is_buy=not is_long, # 平仓方向相反 - size=size, - reduce_only=True - ) - - if result.get('success'): - print(f"✅ 平仓成功: {size} {symbol}") - else: - print(f"❌ 平仓失败: {result.get('error')}") - else: - print(f"📊 无持仓需要平仓") - - # 最终状态 - print(f"\n📊 最终状态:") - final_positions = self.service.get_open_positions() - final_orders = self.service.get_open_orders(symbol) - - print(f" 持仓: {len(final_positions)} 个") - print(f" 挂单: {len(final_orders)} 个") - - if len(final_positions) == 0 and len(final_orders) == 0: - print(f"✅ 清理完成,无持仓和挂单") - else: - print(f"⚠️ 清理未完全,请检查") - - def run_all_tests(self, auto_mode: bool = True): - """运行所有测试 - - Args: - auto_mode: 自动模式,不等待用户确认(默认True) - """ - print("\n" + "="*60) - print(" Hyperliquid 小资金测试") - if auto_mode: - print(" 自动模式: 将直接执行所有测试") - else: - print(" 交互模式: 每步需要确认") - print("="*60) - - def wait_for_confirmation(msg: str): - """等待用户确认(仅在非自动模式下)""" - if not auto_mode: - try: - input(f"\n⚠️ {msg}\n按 Enter 继续,Ctrl+C 取消...") - except EOFError: - print("⚠️ 检测到非交互环境,切换到自动模式") - return True - return True - - try: - # 测试1:账户状态 - self.test_1_get_account_state() - - # 测试2:市价单 - wait_for_confirmation("即将测试市价单(小金额)") - self.test_2_market_order(symbol="BTC", is_buy=True, size=0.001) - - # 测试3:限价单 - wait_for_confirmation("即将测试限价单(小金额)") - self.test_3_limit_order(symbol="BTC", is_buy=False, size=0.001) - - # 测试4:查询止盈止损 - self.test_4_check_tp_sl(symbol="BTC") - - # 测试5:清理 - wait_for_confirmation("即将清理测试(平仓、取消挂单)") - self.test_5_cleanup(symbol="BTC") - - print("\n" + "="*60) - print(" ✅ 所有测试完成") - print("="*60) - - except KeyboardInterrupt: - print("\n\n⚠️ 测试被用户中断") - print("🧹 执行清理...") - - # 尝试清理 - self.test_5_cleanup(symbol="BTC") - - except Exception as e: - logger.error(f"❌ 测试出错: {e}") - import traceback - traceback.print_exc() - - # 尝试清理 - print("\n🧹 执行清理...") - self.test_5_cleanup(symbol="BTC") - - -def main(): - """主函数""" - tester = HyperliquidTester() - tester.run_all_tests() - - -if __name__ == "__main__": - main() diff --git a/backend/test_p0_fixes.py b/backend/test_p0_fixes.py deleted file mode 100644 index c35c92f..0000000 --- a/backend/test_p0_fixes.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env python3 -""" -P0问题修复验证测试 -测试3个严重问题的修复情况 -""" -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import asyncio -import json -from pathlib import Path -from datetime import datetime -from typing import Dict, Any - - -class TestP0Fixes: - """P0问题修复测试类""" - - def __init__(self): - self.test_results = [] - self.passed = 0 - self.failed = 0 - - def log(self, test_name: str, passed: bool, message: str): - """记录测试结果""" - status = "✅ PASS" if passed else "❌ FAIL" - result = f"{status} - {test_name}: {message}" - self.test_results.append(result) - - if passed: - self.passed += 1 - else: - self.failed += 1 - - print(result) - - def test_1_max_margin_pct(self): - """测试1: 配置不一致修复(max_margin_pct 应该是 25%)""" - print("\n" + "="*60) - print("测试1: max_margin_pct 配置(应该支持25%)") - print("="*60) - - try: - from app.crypto_agent.crypto_agent import CryptoAgent - - agent = CryptoAgent() - - # 检查 Bitget - bitget_max = agent.PLATFORM_RULES['Bitget']['max_margin_pct'] - self.log( - "Bitget max_margin_pct", - bitget_max == 0.25, - f"期望 0.25, 实际 {bitget_max}" - ) - - # 检查 PaperTrading - paper_max = agent.PLATFORM_RULES['PaperTrading']['max_margin_pct'] - self.log( - "PaperTrading max_margin_pct", - paper_max == 0.25, - f"期望 0.25, 实际 {paper_max}" - ) - - # 检查 Hyperliquid - hyper_max = agent.PLATFORM_RULES['Hyperliquid']['max_margin_pct'] - self.log( - "Hyperliquid max_margin_pct", - hyper_max == 0.25, - f"期望 0.25, 实际 {hyper_max}" - ) - - except Exception as e: - self.log("max_margin_pct 测试", False, f"异常: {e}") - - def test_2_position_sizing(self): - """测试2: A级信号应该能使用20%保证金(不被截断到10%)""" - print("\n" + "="*60) - print("测试2: A级信号仓位计算($1000账户应该能用$200保证金)") - print("="*60) - - try: - from app.crypto_agent.crypto_agent import CryptoAgent - - agent = CryptoAgent() - - # 模拟A级信号 - signal = { - 'symbol': 'BTCUSDT', - 'confidence': 92, # A级 - } - - # 模拟账户 - account = { - 'available': 1000.0, - 'current_balance': 1000.0, - 'current_total_leverage': 0, - 'max_total_leverage': 10, - } - - # 计算仓位 - margin, reason = agent._calculate_position_size(signal, account, 'PaperTrading') - - # A级信号期望 20% = $200 - expected_min = 190 # 考虑一些边界情况,至少应该接近$200 - expected_max = 210 - - is_correct = expected_min <= margin <= expected_max - - self.log( - "A级信号保证金计算", - is_correct, - f"期望 ${expected_min}-${expected_max}, 实际 ${margin:.2f} ({reason})" - ) - - except Exception as e: - self.log("仓位计算测试", False, f"异常: {e}") - - def test_3_initial_balance_persistence(self): - """测试3: 初始余额持久化机制""" - print("\n" + "="*60) - print("测试3: 初始余额持久化(应该保存到data/initial_balances.json)") - print("="*60) - - try: - from app.crypto_agent.crypto_agent import CryptoAgent - - # 清理测试文件 - test_file = Path("data/initial_balances.json") - if test_file.exists(): - test_file.unlink() - - # 创建新的agent实例 - agent = CryptoAgent() - - # 第一次获取(应该记录) - platform_name = "TestPlatform" - current_balance = 10000.0 - - initial_1 = agent._get_initial_balance(platform_name, current_balance) - - self.log( - "首次获取初始余额", - initial_1 == current_balance, - f"期望 {current_balance}, 实际 {initial_1}" - ) - - # 检查文件是否创建 - self.log( - "持久化文件创建", - test_file.exists(), - f"文件存在: {test_file.exists()}" - ) - - # 检查文件内容 - with open(test_file, 'r') as f: - saved_data = json.load(f) - - self.log( - "持久化内容正确", - saved_data.get(platform_name) == current_balance, - f"文件中 {platform_name}: {saved_data.get(platform_name)}" - ) - - # 模拟余额变化,再次获取(应该返回初始值) - current_balance_2 = 9000.0 # 亏损10% - initial_2 = agent._get_initial_balance(platform_name, current_balance_2) - - self.log( - "再次获取初始余额(余额变化后)", - initial_2 == current_balance, # 仍然是初始值 - f"期望 {current_balance}, 实际 {initial_2}" - ) - - # 计算回撤 - drawdown = (initial_2 - current_balance_2) / initial_2 - expected_drawdown = 0.10 # 10% - - self.log( - "回撤计算正确", - abs(drawdown - expected_drawdown) < 0.001, - f"期望 {expected_drawdown*100:.1f}%, 实际 {drawdown*100:.1f}%" - ) - - except Exception as e: - self.log("初始余额持久化测试", False, f"异常: {e}") - import traceback - traceback.print_exc() - - async def test_4_emergency_close_await(self): - """测试4: 紧急平仓async/await处理""" - print("\n" + "="*60) - print("测试4: 紧急平仓async/await检测") - print("="*60) - - try: - import asyncio - - # Mock同步方法 - def sync_close(symbol: str): - return {'success': True, 'symbol': symbol} - - # Mock异步方法 - async def async_close(symbol: str): - await asyncio.sleep(0.01) - return {'success': True, 'symbol': symbol} - - # 测试asyncio.iscoroutinefunction - is_sync = asyncio.iscoroutinefunction(sync_close) - is_async = asyncio.iscoroutinefunction(async_close) - - self.log( - "同步方法检测", - not is_sync, - f"sync_close 是协程: {is_sync} (应该是False)" - ) - - self.log( - "异步方法检测", - is_async, - f"async_close 是协程: {is_async} (应该是True)" - ) - - # 测试调用 - try: - # 同步调用 - result_sync = sync_close("BTC") - self.log( - "同步方法调用", - result_sync['success'], - f"结果: {result_sync}" - ) - - # 异步调用 - result_async = await async_close("ETH") - self.log( - "异步方法调用", - result_async['success'], - f"结果: {result_async}" - ) - - except Exception as e: - self.log("方法调用测试", False, f"异常: {e}") - - except Exception as e: - self.log("紧急平仓await测试", False, f"异常: {e}") - - def test_5_account_drawdown_calculation(self): - """测试5: 账户回撤计算""" - print("\n" + "="*60) - print("测试5: 账户回撤计算(模拟各种场景)") - print("="*60) - - try: - from app.crypto_agent.crypto_agent import CryptoAgent - - # 测试场景 - scenarios = [ - {"initial": 10000, "current": 10000, "expected_dd": 0.0, "desc": "无回撤"}, - {"initial": 10000, "current": 8500, "expected_dd": 0.15, "desc": "15%回撤(警告)"}, - {"initial": 10000, "current": 7500, "expected_dd": 0.25, "desc": "25%回撤(止损)"}, - {"initial": 10000, "current": 5000, "expected_dd": 0.50, "desc": "50%回撤(严重)"}, - ] - - agent = CryptoAgent() - - for scenario in scenarios: - initial = scenario['initial'] - current = scenario['current'] - expected = scenario['expected_dd'] - - # 模拟计算 - drawdown = (initial - current) / initial - - is_correct = abs(drawdown - expected) < 0.001 - - self.log( - f"回撤计算: {scenario['desc']}", - is_correct, - f"期望 {expected*100:.1f}%, 实际 {drawdown*100:.1f}%" - ) - - except Exception as e: - self.log("账户回撤计算测试", False, f"异常: {e}") - - def print_summary(self): - """打印测试总结""" - print("\n" + "="*60) - print("📊 测试总结") - print("="*60) - - total = self.passed + self.failed - pass_rate = (self.passed / total * 100) if total > 0 else 0 - - print(f"总测试数: {total}") - print(f"通过: {self.passed} ✅") - print(f"失败: {self.failed} ❌") - print(f"通过率: {pass_rate:.1f}%") - - if self.failed == 0: - print("\n🎉 所有测试通过!P0问题修复验证成功!") - else: - print(f"\n⚠️ 有 {self.failed} 个测试失败,请检查!") - - print("="*60 + "\n") - - async def run_all_tests(self): - """运行所有测试""" - print("\n" + "🚀 " * 30) - print("P0问题修复验证测试") - print("测试时间:", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - print("🚀 " * 30) - - # 运行测试 - self.test_1_max_margin_pct() - self.test_2_position_sizing() - self.test_3_initial_balance_persistence() - await self.test_4_emergency_close_await() - self.test_5_account_drawdown_calculation() - - # 打印总结 - self.print_summary() - - return self.failed == 0 - - -def main(): - """主函数""" - test = TestP0Fixes() - - # 运行异步测试 - success = asyncio.run(test.run_all_tests()) - - # 返回退出码 - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/backend/test_p0_fixes_simple.py b/backend/test_p0_fixes_simple.py deleted file mode 100644 index a9377f1..0000000 --- a/backend/test_p0_fixes_simple.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python3 -""" -P0问题修复验证测试 - 简化版(无需导入CryptoAgent) -直接测试核心逻辑 -""" -import asyncio -import json -from pathlib import Path -from datetime import datetime - - -def test_1_platform_rules(): - """测试1: 检查PLATFORM_RULES配置""" - print("\n" + "="*60) - print("测试1: PLATFORM_RULES 配置(max_margin_pct应该是25%)") - print("="*60) - - # 读取crypto_agent.py文件 - file_path = Path("/Users/aaron/source_code/Stock_Agent/backend/app/crypto_agent/crypto_agent.py") - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # 检查配置 - tests_passed = 0 - tests_total = 3 - - # 检查Bitget - if "'max_margin_pct': 0.25," in content and "'Bitget'" in content: - print("✅ PASS - Bitget max_margin_pct = 0.25") - tests_passed += 1 - else: - print("❌ FAIL - Bitget max_margin_pct != 0.25") - - # 检查PaperTrading - if "'PaperTrading'" in content: - # 找到PaperTrading部分 - start = content.find("'PaperTrading'") - section = content[start:start+500] - if "'max_margin_pct': 0.25," in section: - print("✅ PASS - PaperTrading max_margin_pct = 0.25") - tests_passed += 1 - else: - print("❌ FAIL - PaperTrading max_margin_pct != 0.25") - - # 检查Hyperliquid - if "'Hyperliquid'" in content: - start = content.find("'Hyperliquid'") - section = content[start:start+500] - if "'max_margin_pct': 0.25," in section: - print("✅ PASS - Hyperliquid max_margin_pct = 0.25") - tests_passed += 1 - else: - print("❌ FAIL - Hyperliquid max_margin_pct != 0.25") - - return tests_passed, tests_total - - -def test_2_position_sizing_logic(): - """测试2: 仓位计算逻辑""" - print("\n" + "="*60) - print("测试2: A级信号仓位计算逻辑") - print("="*60) - - tests_passed = 0 - tests_total = 3 - - # 测试场景:$1000账户,A级信号(20%) - account = { - 'available': 1000.0, - 'balance': 1000.0, - 'current_leverage': 0, - 'max_leverage': 10 - } - - # A级信号配置 - base_margin_pct = 0.20 # 20% - max_margin_pct = 0.25 # 25% (修复后) - - # 计算保证金 - margin = account['available'] * base_margin_pct # $200 - - # 应用最大限制 - max_margin = account['balance'] * max_margin_pct # $250 - - if margin <= max_margin: - final_margin = margin - print(f"✅ PASS - A级信号保证金: ${margin:.2f} (未被截断)") - tests_passed += 1 - else: - final_margin = max_margin - print(f"❌ FAIL - A级信号保证金被截断: ${margin:.2f} → ${max_margin:.2f}") - - # 验证仓位价值 - leverage = 10 - position_value = final_margin * leverage - - if position_value == 2000: # $200 * 10x = $2000 - print(f"✅ PASS - 持仓价值: ${position_value:.2f} (期望 $2000)") - tests_passed += 1 - else: - print(f"❌ FAIL - 持仓价值: ${position_value:.2f} (期望 $2000)") - - # 验证占账户比例 - position_pct = (position_value / account['balance']) * 100 - - if position_pct == 200: # 200% (20%保证金 * 10x杠杆) - print(f"✅ PASS - 账户比例: {position_pct:.0f}% (期望 200%)") - tests_passed += 1 - else: - print(f"❌ FAIL - 账户比例: {position_pct:.0f}% (期望 200%)") - - return tests_passed, tests_total - - -def test_3_emergency_close_code(): - """测试3: 检查紧急平仓代码""" - print("\n" + "="*60) - print("测试3: 紧急平仓async/await处理代码") - print("="*60) - - file_path = Path("/Users/aaron/source_code/Stock_Agent/backend/app/crypto_agent/crypto_agent.py") - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - tests_passed = 0 - tests_total = 2 - - # 检查是否有iscoroutinefunction - if "asyncio.iscoroutinefunction" in content: - print("✅ PASS - 包含 asyncio.iscoroutinefunction 检查") - tests_passed += 1 - else: - print("❌ FAIL - 缺少 asyncio.iscoroutinefunction 检查") - - # 检查是否有await调用 - if "await close_method" in content: - print("✅ PASS - 包含 await close_method 调用") - tests_passed += 1 - else: - print("❌ FAIL - 缺少 await close_method 调用") - - return tests_passed, tests_total - - -def test_4_initial_balance_methods(): - """测试4: 检查初始余额持久化方法""" - print("\n" + "="*60) - print("测试4: 初始余额持久化方法") - print("="*60) - - file_path = Path("/Users/aaron/source_code/Stock_Agent/backend/app/crypto_agent/crypto_agent.py") - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - tests_passed = 0 - tests_total = 4 - - # 检查方法定义 - methods = [ - '_load_initial_balances', - '_save_initial_balances', - '_get_initial_balance', - '_initial_balances' - ] - - for method in methods: - if method in content: - print(f"✅ PASS - 找到方法/属性: {method}") - tests_passed += 1 - else: - print(f"❌ FAIL - 未找到方法/属性: {method}") - - return tests_passed, tests_total - - -def test_5_drawdown_calculation(): - """测试5: 回撤计算逻辑""" - print("\n" + "="*60) - print("测试5: 回撤计算(各种场景)") - print("="*60) - - tests_passed = 0 - tests_total = 4 - - scenarios = [ - {"initial": 10000, "current": 10000, "expected_dd": 0.0, "desc": "无回撤"}, - {"initial": 10000, "current": 8500, "expected_dd": 0.15, "desc": "15%回撤(警告)"}, - {"initial": 10000, "current": 7500, "expected_dd": 0.25, "desc": "25%回撤(止损)"}, - {"initial": 10000, "current": 5000, "expected_dd": 0.50, "desc": "50%回撤(严重)"}, - ] - - for scenario in scenarios: - initial = scenario['initial'] - current = scenario['current'] - expected = scenario['expected_dd'] - - # 计算回撤 - drawdown = (initial - current) / initial - - if abs(drawdown - expected) < 0.001: - print(f"✅ PASS - {scenario['desc']}: {drawdown*100:.1f}%") - tests_passed += 1 - else: - print(f"❌ FAIL - {scenario['desc']}: 期望 {expected*100:.1f}%, 实际 {drawdown*100:.1f}%") - - return tests_passed, tests_total - - -async def test_6_async_await(): - """测试6: async/await机制""" - print("\n" + "="*60) - print("测试6: async/await机制验证") - print("="*60) - - tests_passed = 0 - tests_total = 4 - - # Mock同步方法 - def sync_close(symbol: str): - return {'success': True, 'symbol': symbol} - - # Mock异步方法 - async def async_close(symbol: str): - await asyncio.sleep(0.01) - return {'success': True, 'symbol': symbol} - - # 测试检测 - is_sync = asyncio.iscoroutinefunction(sync_close) - is_async = asyncio.iscoroutinefunction(async_close) - - if not is_sync: - print(f"✅ PASS - sync_close 不是协程: {is_sync}") - tests_passed += 1 - else: - print(f"❌ FAIL - sync_close 是协程: {is_sync}") - - if is_async: - print(f"✅ PASS - async_close 是协程: {is_async}") - tests_passed += 1 - else: - print(f"❌ FAIL - async_close 不是协程: {is_async}") - - # 测试调用 - result_sync = sync_close("BTC") - if result_sync['success']: - print(f"✅ PASS - 同步调用成功: {result_sync}") - tests_passed += 1 - else: - print(f"❌ FAIL - 同步调用失败") - - result_async = await async_close("ETH") - if result_async['success']: - print(f"✅ PASS - 异步调用成功: {result_async}") - tests_passed += 1 - else: - print(f"❌ FAIL - 异步调用失败") - - return tests_passed, tests_total - - -def test_7_base_executor_notifications(): - """测试7: 检查BaseExecutor通知功能""" - print("\n" + "="*60) - print("测试7: BaseExecutor飞书通知功能") - print("="*60) - - file_path = Path("/Users/aaron/source_code/Stock_Agent/backend/app/crypto_agent/executor/base_executor.py") - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - tests_passed = 0 - tests_total = 3 - - # 检查通知方法 - methods = [ - 'send_execution_notification', - '_send_open_notification', - '_send_close_notification', - ] - - for method in methods: - if method in content: - print(f"✅ PASS - 找到通知方法: {method}") - tests_passed += 1 - else: - print(f"❌ FAIL - 未找到通知方法: {method}") - - return tests_passed, tests_total - - -async def main(): - """主测试函数""" - print("\n" + "🚀 " * 30) - print("P0问题修复验证测试 - 简化版") - print("测试时间:", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - print("🚀 " * 30) - - total_passed = 0 - total_tests = 0 - - # 运行所有测试 - passed, total = test_1_platform_rules() - total_passed += passed - total_tests += total - - passed, total = test_2_position_sizing_logic() - total_passed += passed - total_tests += total - - passed, total = test_3_emergency_close_code() - total_passed += passed - total_tests += total - - passed, total = test_4_initial_balance_methods() - total_passed += passed - total_tests += total - - passed, total = test_5_drawdown_calculation() - total_passed += passed - total_tests += total - - passed, total = await test_6_async_await() - total_passed += passed - total_tests += total - - passed, total = test_7_base_executor_notifications() - total_passed += passed - total_tests += total - - # 打印总结 - print("\n" + "="*60) - print("📊 测试总结") - print("="*60) - - pass_rate = (total_passed / total_tests * 100) if total_tests > 0 else 0 - - print(f"总测试数: {total_tests}") - print(f"通过: {total_passed} ✅") - print(f"失败: {total_tests - total_passed} ❌") - print(f"通过率: {pass_rate:.1f}%") - - if total_passed == total_tests: - print("\n🎉 所有测试通过!P0问题修复验证成功!") - return True - else: - print(f"\n⚠️ 有 {total_tests - total_passed} 个测试失败,请检查!") - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - exit(0 if success else 1) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 621ce49..4f85da8 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -13,7 +13,6 @@ def _mock_settings(): s.bitget_max_total_leverage = 10.0 s.bitget_max_single_position = 1000.0 s.account_max_drawdown = 0.25 - s.hyperliquid_circuit_breaker_drawdown = 0.10 # 保留兼容性 s.bitget_trading_enabled = False return s diff --git a/backend/tests/test_crypto_agent_platform_halts.py b/backend/tests/test_crypto_agent_platform_halts.py index 9b6d5d4..17150a5 100644 --- a/backend/tests/test_crypto_agent_platform_halts.py +++ b/backend/tests/test_crypto_agent_platform_halts.py @@ -50,6 +50,7 @@ def load_crypto_agent_class(): feishu_module = types.ModuleType('app.services.feishu_service') feishu_module.get_feishu_service = MagicMock() feishu_module.get_feishu_paper_trading_service = MagicMock() + feishu_module.get_feishu_error_service = MagicMock() sys.modules['app.services.feishu_service'] = feishu_module telegram_module = types.ModuleType('app.services.telegram_service') @@ -95,15 +96,31 @@ def load_crypto_agent_class(): def make_agent(): CryptoAgent = load_crypto_agent_class() agent = CryptoAgent.__new__(CryptoAgent) - agent.settings = types.SimpleNamespace(account_max_drawdown=0.25, account_drawdown_alert=0.15) + agent.settings = types.SimpleNamespace( + account_max_drawdown=0.25, + account_drawdown_alert=0.15, + feishu_enabled=False, + crypto_intraday_llm_cooldown_minutes=15, + crypto_trend_llm_cooldown_minutes=60, + crypto_force_llm_surge_threshold=2.0, + crypto_force_llm_trade_zone_pct=0.4, + crypto_event_analysis_enabled=True, + crypto_event_analysis_window_minutes=5, + crypto_event_analysis_price_change_percent=1.0, + crypto_event_analysis_cooldown_minutes=10, + ) agent.paper_trading = None - agent.hyperliquid = None agent.bitget = None agent.symbols = ['BTCUSDT'] agent.executors = {} agent._platform_halts = {} from collections import deque agent._execution_events = deque(maxlen=120) + agent._analysis_events = deque(maxlen=240) + agent._analysis_monitor = {} + agent._analysis_notification_state = {} + agent._lane_analysis_state = {} + agent._event_analysis_state = {} agent._initial_balances = {} agent._save_platform_halts = MagicMock() agent._save_initial_balances = MagicMock() @@ -159,12 +176,12 @@ def test_execution_events_are_recorded_and_returned_in_reverse_time_order(): agent = make_agent() agent._record_execution_event('Bitget', 'open_failed', symbol='ETHUSDT', reason='余额不足', status='error') - agent._record_execution_event('Hyperliquid', 'hold', symbol='BTCUSDT', reason='已有盈利反向仓', status='hold') + agent._record_execution_event('PaperTrading', 'hold', symbol='BTCUSDT', reason='已有盈利反向仓', status='hold') events = agent.get_recent_execution_events(limit=10) assert len(events) == 2 - assert events[0]['platform'] == 'Hyperliquid' + assert events[0]['platform'] == 'PaperTrading' assert events[0]['event_type'] == 'hold' assert events[1]['platform'] == 'Bitget' assert events[1]['reason'] == '余额不足' @@ -182,7 +199,6 @@ def test_get_status_contains_last_execution_preview(): 'timestamp': '2026-04-22T12:00:00', 'current_price': 65000.0, 'paper': {'decision': 'OPEN', 'reason': '正常开仓'}, - 'hyperliquid': {'decision': 'HOLD', 'reason': '无适配信号'}, 'bitget': {'decision': 'CANCEL_PENDING', 'reason': '替换旧挂单'}, } } diff --git a/backend/tests/test_execution_safety_fixes.py b/backend/tests/test_execution_safety_fixes.py index 2f9b4d5..cd8fb26 100644 --- a/backend/tests/test_execution_safety_fixes.py +++ b/backend/tests/test_execution_safety_fixes.py @@ -29,13 +29,12 @@ def make_bitget_service(): 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.circuit_breaker_drawdown = 0.25 service.trading_api = mock_api service.initial_balance = 10000.0 @@ -77,41 +76,6 @@ def load_bitget_executor_class(): return sys.modules['app.crypto_agent.executor.bitget_executor'].BitgetExecutor -def load_hyperliquid_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.hyperliquid_executor' not in sys.modules: - executor_spec = importlib.util.spec_from_file_location( - 'app.crypto_agent.executor.hyperliquid_executor', - executor_dir / 'hyperliquid_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.hyperliquid_executor'].HyperliquidExecutor - - def test_bitget_market_close_position_only_closes_requested_symbol(): service, mock_api = make_bitget_service() mock_api.get_position.return_value = [ @@ -229,47 +193,3 @@ def test_bitget_executor_open_uses_actual_leverage_for_contracts(): executor.bitget.update_leverage.assert_called_once_with('ETH', 10) executor.bitget.place_market_order.assert_called_once_with('ETH', is_buy=True, size=1) - -def test_hyperliquid_executor_open_uses_decision_margin_not_account_value(): - HyperliquidExecutor = load_hyperliquid_executor_class() - - executor = HyperliquidExecutor.__new__(HyperliquidExecutor) - executor.hyperliquid = MagicMock() - executor.send_execution_notification = AsyncMock() - executor.decide_order_type = MagicMock(return_value=('market', 'test')) - executor.calculate_effective_margin = MagicMock(return_value=120.0) - executor.hyperliquid.get_account_state.return_value = { - 'available_balance': 5000.0, - 'account_value': 50000.0, - } - executor.hyperliquid.get_sz_decimals.return_value = 3 - executor.hyperliquid.place_market_order.return_value = { - 'success': True, - 'order_id': 'oid-hl-1', - 'order_status': 'filled', - } - executor.hyperliquid.set_tp_sl.return_value = {'tp_set': True, 'sl_set': True} - - result = asyncio.run( - executor.execute_open( - { - 'symbol': 'ETHUSDT', - 'action': 'buy', - 'margin': 120.0, - 'entry_price': 2000.0, - 'stop_loss': 1980.0, - 'take_profit': 2060.0, - 'leverage': 10, - }, - 2000.0, - ) - ) - - assert result['success'] is True - executor.hyperliquid.update_leverage.assert_called_once_with('ETH', 10) - executor.hyperliquid.place_market_order.assert_called_once_with( - symbol='ETH', - is_buy=True, - size=0.6, - reduce_only=False, - ) diff --git a/backend/tests/test_hyperliquid_live_integration.py b/backend/tests/test_hyperliquid_live_integration.py deleted file mode 100644 index 5df6074..0000000 --- a/backend/tests/test_hyperliquid_live_integration.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -Hyperliquid 真实 API 集成测试 - -⚠️ 警告:此测试会使用真实 API 调用和真实订单! -- 使用最小下单量(ETH,szDecimals=3) -- 市价单会立即成交,产生实际盈亏 -- 测试后自动清理所有订单和持仓 - -覆盖接口: - - 账户状态查询 - - 杠杆设置 - - 持仓查询 - - 市价开仓 - - 止盈止损设置(TP limit 单 + SL trigger 单) - - 止盈止损验证(读取挂单确认 TP 和 SL 都存在) - - 市价平仓 - -运行方式: - cd backend - python3 tests/test_hyperliquid_live_integration.py -""" -import os -import sys -import time -import traceback -from datetime import datetime - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from dotenv import load_dotenv -load_dotenv(os.path.join(os.path.dirname(__file__), '..', '..', '.env')) - - -# ==================== 测试配置 ==================== - -TEST_SYMBOL = 'ETH' # ETH 精度好(szDecimals=3),手续费低 -TEST_SIZE = 0.01 # 最小下单量 -TEST_LEVERAGE = 5 # 测试杠杆倍数 - - -class TestResult: - """测试结果收集器""" - - def __init__(self): - self.results = [] - - def record(self, name: str, passed: bool, detail: str = ""): - self.results.append((name, passed, detail)) - status = "✅ PASS" if passed else "❌ FAIL" - print(f" {status}: {name}") - if detail: - print(f" {detail}") - - def summary(self): - print(f"\n{'='*60}") - print("测试结果汇总") - print(f"{'='*60}") - passed = sum(1 for _, p, _ in self.results if p) - total = len(self.results) - for name, p, detail in self.results: - status = "✅" if p else "❌" - line = f" {status} {name}" - if not p and detail: - line += f" — {detail}" - print(line) - print(f"\n 总计: {passed}/{total} 通过") - print(f"{'='*60}") - return passed == total - - -# ==================== 测试函数 ==================== - -def test_account_state(service, r: TestResult): - """查询账户状态""" - try: - state = service.get_account_state() - av = state['account_value'] - ab = state['available_balance'] - r.record( - "查询账户状态", - av > 0, - f"权益=${av:,.2f}, 可用=${ab:,.2f}" - ) - except Exception as e: - r.record("查询账户状态", False, str(e)) - - -def test_update_leverage(service, r: TestResult): - """设置杠杆""" - try: - result = service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE) - r.record("设置杠杆", True, f"{TEST_SYMBOL} → {TEST_LEVERAGE}x") - except Exception as e: - r.record("设置杠杆", False, str(e)) - - -def test_get_positions(service, r: TestResult): - """查询持仓""" - try: - positions = service.get_open_positions() - count = len(positions) - r.record("查询持仓", True, f"当前活跃持仓: {count} 个") - except Exception as e: - r.record("查询持仓", False, str(e)) - - -def test_market_order_with_tp_sl(service, r: TestResult): - """市价开仓 → 设置止盈止损 → 验证 → 平仓""" - opened = False - try: - # 0. 先清理已有持仓和挂单 - try: - service.cancel_all_orders(TEST_SYMBOL) - except: - pass - try: - pos = service.get_position_for_symbol(TEST_SYMBOL) - if pos: - service.market_close_position(TEST_SYMBOL) - time.sleep(1) - except: - pass - - # 1. 获取当前价格 - all_mids = service.info.all_mids() - current_price = float(all_mids.get(TEST_SYMBOL, 0)) - if current_price <= 0: - r.record("获取当前价格", False, f"无法获取 {TEST_SYMBOL} 价格") - return - - print(f"\n 当前 {TEST_SYMBOL}: ${current_price:,.2f}") - - # 2. 计算最小下单量 - sz_decimals = service.get_sz_decimals(TEST_SYMBOL) - import math - size = max(math.floor(TEST_SIZE * (10 ** sz_decimals)) / (10 ** sz_decimals), 1 / (10 ** sz_decimals)) - print(f" 下单量: {size} ({sz_decimals} 位精度)") - - # 3. 设置杠杆 - service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE) - - # 4. 市价开多 - result = service.place_market_order( - symbol=TEST_SYMBOL, - is_buy=True, - size=size, - reduce_only=False - ) - if not result.get('success'): - r.record("市价开仓", False, result.get('error', '未知错误')) - return - - r.record("市价开仓", True, f"{TEST_SYMBOL} buy {size}") - opened = True - - time.sleep(2) - - # 5. 验证持仓 - position = service.get_position_for_symbol(TEST_SYMBOL) - r.record("验证持仓存在", position is not None, - f"size={position['size']}, entry=${position['entry_price']:,.2f}" if position else "未找到持仓") - - # 6. 设置止盈止损 - tp_price = round(current_price * 1.02, 2) # +2% - sl_price = round(current_price * 0.98, 2) # -2% - print(f" 设置 TP=${tp_price:,.2f}, SL=${sl_price:,.2f}") - - tp_sl_result = service.set_tp_sl( - symbol=TEST_SYMBOL, - is_long=True, - size=size, - tp_price=tp_price, - sl_price=sl_price - ) - - tp_set = tp_sl_result.get('tp_set', False) - sl_set = tp_sl_result.get('sl_set', False) - errors = tp_sl_result.get('errors', []) - - detail = f"TP=${tp_price:,.2f}({'✅' if tp_set else '❌'}), SL=${sl_price:,.2f}({'✅' if sl_set else '❌'})" - if errors: - detail += f" errors={errors}" - r.record("设置止盈止损", tp_set and sl_set, detail) - - # 7. 验证止盈止损挂单 - time.sleep(1) - tp_sl_prices = service.get_tp_sl_prices(TEST_SYMBOL) - has_tp = tp_sl_prices.get('take_profit') is not None - has_sl = tp_sl_prices.get('stop_loss') is not None - r.record("验证 TP/SL 挂单", has_tp and has_sl, - f"TP={tp_sl_prices.get('take_profit')}({'✅' if has_tp else '❌'}), " - f"SL={tp_sl_prices.get('stop_loss')}({'✅' if has_sl else '❌'})") - - # 8. 取消止盈止损 - try: - service.cancel_tp_sl_orders(TEST_SYMBOL) - except: - pass - time.sleep(1) - - # 9. 市价平仓 - close_result = service.market_close_position(TEST_SYMBOL) - r.record("市价平仓", close_result.get('success', False), - close_result.get('error', f"成功")) - if close_result.get('success'): - opened = False - - time.sleep(2) - - # 10. 验证已平仓 - position_after = service.get_position_for_symbol(TEST_SYMBOL) - r.record("验证已平仓", position_after is None) - - except Exception as e: - r.record("市价单流程异常", False, f"{e}\n{traceback.format_exc()}") - finally: - if opened: - try: - service.cancel_all_orders(TEST_SYMBOL) - time.sleep(0.5) - service.market_close_position(TEST_SYMBOL) - print(" 🧹 已自动清理残留持仓") - except Exception as cleanup_err: - print(f" ⚠️ 清理失败,请手动检查: {cleanup_err}") - - -def test_set_tp_sl_partial_failure(service, r: TestResult): - """测试 set_tp_sl: 第一个失败不影响第二个""" - # 这个测试验证我们的修复:独立的 try-except - # 如果 TP 失败(例如价格为 0),SL 应该仍然被设置 - opened = False - try: - # 先清理 - try: - service.cancel_all_orders(TEST_SYMBOL) - except: - pass - pos = service.get_position_for_symbol(TEST_SYMBOL) - if pos: - service.market_close_position(TEST_SYMBOL) - time.sleep(1) - - # 1. 市价开仓 - sz_decimals = service.get_sz_decimals(TEST_SYMBOL) - import math - size = max(math.floor(TEST_SIZE * (10 ** sz_decimals)) / (10 ** sz_decimals), 1 / (10 ** sz_decimals)) - service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE) - - result = service.place_market_order( - symbol=TEST_SYMBOL, - is_buy=True, - size=size, - reduce_only=False - ) - if not result.get('success'): - r.record("部分失败测试: 开仓", False, result.get('error', '未知')) - return - opened = True - time.sleep(2) - - # 2. 设置一个有效的 SL(但故意不设置 TP → tp_price=None) - all_mids = service.info.all_mids() - current_price = float(all_mids.get(TEST_SYMBOL, 0)) - sl_price = round(current_price * 0.98, 2) - - tp_sl_result = service.set_tp_sl( - symbol=TEST_SYMBOL, - is_long=True, - size=size, - tp_price=None, # 不设 TP - sl_price=sl_price # 只设 SL - ) - sl_set = tp_sl_result.get('sl_set', False) - r.record("部分设置测试 (仅 SL)", sl_set, f"sl_set={sl_set}, errors={tp_sl_result.get('errors', [])}") - - # 3. 验证 SL 挂单存在 - time.sleep(1) - tp_sl_prices = service.get_tp_sl_prices(TEST_SYMBOL) - has_sl = tp_sl_prices.get('stop_loss') is not None - r.record("验证 SL 挂单", has_sl, f"SL={tp_sl_prices.get('stop_loss')}") - - # 4. 清理 - service.cancel_all_orders(TEST_SYMBOL) - time.sleep(1) - service.market_close_position(TEST_SYMBOL) - opened = False - - except Exception as e: - r.record("部分失败测试异常", False, f"{e}\n{traceback.format_exc()}") - finally: - if opened: - try: - service.cancel_all_orders(TEST_SYMBOL) - time.sleep(0.5) - service.market_close_position(TEST_SYMBOL) - print(" 🧹 已自动清理残留持仓") - except: - pass - - -# ==================== 主入口 ==================== - -def main(): - print(f"\n{'='*60}") - print(f" Hyperliquid 实盘接口集成测试") - print(f"{'='*60}") - print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print(f" 交易对: {TEST_SYMBOL}") - print(f" 下单量: {TEST_SIZE}") - print(f" 杠杆: {TEST_LEVERAGE}x") - print(f"{'='*60}") - - r = TestResult() - - # 初始化 - try: - from app.services.hyperliquid_trading_service import HyperliquidTradingService - service = HyperliquidTradingService() - print(f" 钱包: {service.wallet_address[:10]}...") - except Exception as e: - print(f"\n❌ 初始化失败: {e}") - traceback.print_exc() - sys.exit(1) - - # ---- 基础测试 ---- - print(f"\n{'─'*40}") - print(" 基础接口") - print(f"{'─'*40}") - - test_account_state(service, r) - time.sleep(0.3) - - test_update_leverage(service, r) - time.sleep(0.3) - - test_get_positions(service, r) - time.sleep(0.3) - - # ---- 核心测试: 开仓 → TP/SL → 平仓 ---- - print(f"\n{'─'*40}") - print(" 开仓 → 止盈止损 → 验证 → 平仓") - print(f"{'─'*40}") - - test_market_order_with_tp_sl(service, r) - time.sleep(1) - - # ---- 边界测试: 部分设置 ---- - print(f"\n{'─'*40}") - print(" 部分设置测试") - print(f"{'─'*40}") - - test_set_tp_sl_partial_failure(service, r) - - # ---- 汇总 ---- - all_passed = r.summary() - sys.exit(0 if all_passed else 1) - - -if __name__ == '__main__': - print("\n⚠️ 此测试会产生真实订单和手续费!") - print(f" 使用 {TEST_SYMBOL} 最小量 {TEST_SIZE}") - - confirm = input("\n是否继续?(yes/no): ") - if confirm.strip().lower() != 'yes': - print("已取消") - sys.exit(0) - - main() diff --git a/frontend/console.html b/frontend/console.html index a77b735..a738be5 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -1832,7 +1832,7 @@ } function sumPlatformPositions(platforms) { - return ['paper', 'bitget', 'hyperliquid'] + return ['paper', 'bitget'] .map((key) => platforms?.[key]?.positions?.count || 0) .reduce((sum, count) => sum + count, 0); } @@ -1944,13 +1944,13 @@ const cryptoAgent = data.crypto_agent || {}; const monitor = cryptoAgent.analysis_monitor || {}; const halts = cryptoAgent.platform_halts || {}; - const enabledPlatforms = ['paper', 'bitget', 'hyperliquid'].filter((key) => platforms?.[key]?.enabled !== false); + const enabledPlatforms = ['paper', 'bitget'].filter((key) => platforms?.[key]?.enabled !== false); const haltedCount = countHalted(halts); const runtimeTone = toneClassForHealth(cryptoAgent.running ? monitor.last_cycle_status || monitor.last_analysis_status : 'stopped'); const riskTone = haltedCount > 0 ? 'danger' : ((data.management?.attention_items || []).some((item) => item.severity === 'danger' || item.severity === 'warning') ? 'warn' : 'good'); const platformTone = haltedCount > 0 ? 'warn' : 'good'; const platformHeadline = `${enabledPlatforms.length} 平台`; - const platformDetail = `${sumPlatformPositions(platforms)} 持仓 / ${['paper', 'bitget', 'hyperliquid'].map((key) => platforms?.[key]?.orders?.count || 0).reduce((a, b) => a + b, 0)} 挂单`; + const platformDetail = `${sumPlatformPositions(platforms)} 持仓 / ${['paper', 'bitget'].map((key) => platforms?.[key]?.orders?.count || 0).reduce((a, b) => a + b, 0)} 挂单`; const runtimeHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳'; const runtimeDetail = `状态 ${String(monitor.last_cycle_status || monitor.last_analysis_status || 'idle').toUpperCase()} / ${monitor.current_cycle_total ? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total}` : '待机'}`; const riskHeadline = haltedCount > 0 ? `${haltedCount} 停机` : '无停机'; @@ -1993,7 +1993,7 @@ const executionEvents = data.execution_events || []; const positions = data.management?.positions || []; const orders = data.management?.orders || []; - const platformCount = ['paper', 'bitget', 'hyperliquid'] + const platformCount = ['paper', 'bitget'] .filter((key) => data.platforms?.[key]?.enabled !== false) .length; const lastSignals = Object.keys(data.crypto_agent?.last_signals || {}).length; @@ -2114,7 +2114,7 @@
Hyperliquid 实盘交易未启用
-请在 .env 文件中设置 hyperliquid_trading_enabled=true
暂无持仓
-暂无挂单
-