Compare commits
2 Commits
ed6fcb50cd
...
f60d75e98a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f60d75e98a | ||
|
|
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,14 +884,23 @@ 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)
|
||||
|
||||
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 == 'HOLD':
|
||||
reasoning = decision.get('reasoning', '观望')
|
||||
@ -847,8 +952,6 @@ class CryptoAgent:
|
||||
await self._send_signal_notification(market_signal, decision, current_price)
|
||||
else:
|
||||
logger.warning(f" ⚠️ 减仓未成功执行,跳过通知")
|
||||
else:
|
||||
logger.info(f"\n⏸️ 交易未启用或决策为空")
|
||||
|
||||
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
|
||||
48
backend/openclawfi/README.md
Normal file
48
backend/openclawfi/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
711
backend/openclawfi/install.js
Executable file
711
backend/openclawfi/install.js
Executable file
@ -0,0 +1,711 @@
|
||||
#!/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');
|
||||
23
backend/openclawfi/package.json
Normal file
23
backend/openclawfi/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@ -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