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