Compare commits

...

2 Commits

Author SHA1 Message Date
aaron
f60d75e98a 1update 2026-03-22 11:42:35 +08:00
aaron
a22dfe459c 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
2026-03-22 11:42:25 +08:00
9 changed files with 2129 additions and 86 deletions

View 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)

View File

@ -207,6 +207,19 @@ class Settings(BaseSettings):
pullback_select_time: str = "09:00" # 选股时间24小时制 pullback_select_time: str = "09:00" # 选股时间24小时制
pullback_sectors_to_check: int = 5 # 检查板块数量 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 # 总杠杆上限≤10xClawFi 强制规则)
hyperliquid_circuit_breaker_drawdown: float = 0.10 # 10% 熔断阈值ClawFi 强制规则)
hyperliquid_max_single_position: float = 1000 # 单笔最大持仓金额 (USD)
class Config: class Config:
env_file = find_env_file() env_file = find_env_file()
case_sensitive = False case_sensitive = False

View File

@ -60,6 +60,15 @@ class CryptoAgent:
max_total_leverage=self.paper_trading.max_total_leverage 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.last_signals: Dict[str, Dict[str, Any]] = {}
self.signal_cooldown: Dict[str, datetime] = {} self.signal_cooldown: Dict[str, datetime] = {}
@ -81,6 +90,7 @@ class CryptoAgent:
monitor.update_config("crypto_agent", { monitor.update_config("crypto_agent", {
"symbols": self.symbols, "symbols": self.symbols,
"auto_trading_enabled": True, # 模拟交易始终启用 "auto_trading_enabled": True, # 模拟交易始终启用
"hyperliquid_enabled": self.hyperliquid is not None,
"analysis_interval": "每5分钟整点" "analysis_interval": "每5分钟整点"
}) })
@ -595,34 +605,46 @@ class CryptoAgent:
await self._send_market_signal_notification(market_signal, current_price) await self._send_market_signal_notification(market_signal, current_price)
# ============================================================ # ============================================================
# 第二阶段:交易决策(信号 + 仓位 + 账户状态 # 第二阶段:交易决策(双轨独立
# 模拟交易和实盘交易分别进行独立决策 # 模拟交易和 Hyperliquid 实盘分别进行独立决策
# ============================================================ # ============================================================
logger.info(f"\n🤖 【第二阶段:交易决策】") logger.info(f"\n🤖 【第二阶段:交易决策】")
# 获取配置 paper_decision = None
paper_trading_enabled = self.settings.paper_trading_enabled hyperliquid_decision = None
# 交易决策 # 2.1 模拟盘决策
if paper_trading_enabled: if self.settings.paper_trading_enabled:
logger.info(f"\n📊 【交易决策】") logger.info(f"\n📊 【模拟盘决策】")
positions, account, pending_orders = self._get_trading_state() paper_positions, paper_account, paper_pending = self._get_paper_trading_state()
# 过滤只传递当前symbol的挂单给决策器避免LLM搞混 paper_pending_for_symbol = [o for o in paper_pending if o.get('symbol') == symbol]
pending_orders_for_symbol = [o for o in pending_orders if o.get('symbol') == symbol]
decision = await self.decision_maker.make_decision( paper_decision = await self.decision_maker.make_decision(
market_signal, positions, account, current_price, pending_orders_for_symbol market_signal, paper_positions, paper_account, current_price, paper_pending_for_symbol
) )
self._log_trading_decision(decision) logger.info(f" 模拟盘决策: {paper_decision.get('action')} - {paper_decision.get('reasoning', '')}")
# 发送交易决策通知 await self._send_trading_decision_notification(paper_decision, market_signal, current_price, prefix="[模拟盘]")
await self._send_trading_decision_notification(decision, market_signal, current_price)
else: else:
logger.info(f"⏸️ 交易未启用") logger.info(f"⏸️ 模拟盘交易未启用")
decision = None
# 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: except Exception as e:
logger.error(f"❌ 分析 {symbol} 出错: {e}") logger.error(f"❌ 分析 {symbol} 出错: {e}")
@ -735,9 +757,9 @@ class CryptoAgent:
if risk: if risk:
logger.info(f" 风险: {risk}") logger.info(f" 风险: {risk}")
def _get_trading_state(self) -> tuple: def _get_paper_trading_state(self) -> tuple:
""" """
获取交易状态持仓和账户 获取模拟盘交易状态持仓和账户
Returns: Returns:
(positions, account, pending_orders) - 持仓列表账户状态挂单列表 (positions, account, pending_orders) - 持仓列表账户状态挂单列表
@ -774,9 +796,83 @@ class CryptoAgent:
return position_list, account, pending_orders 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): market_signal: Dict[str, Any], current_price: float):
"""执行交易决策""" """执行交易决策(双轨独立)"""
# 选择最佳信号用于保存 # 选择最佳信号用于保存
best_signal = self._get_best_signal_from_market(market_signal) best_signal = self._get_best_signal_from_market(market_signal)
@ -788,14 +884,23 @@ class CryptoAgent:
signal_to_save['current_price'] = current_price signal_to_save['current_price'] = current_price
self.signal_db.add_signal(signal_to_save) 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: if hyperliquid_decision and self.hyperliquid:
decision_type = decision.get('decision', 'HOLD') 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': if decision_type == 'HOLD':
reasoning = decision.get('reasoning', '观望') reasoning = decision.get('reasoning', '观望')
@ -847,8 +952,6 @@ class CryptoAgent:
await self._send_signal_notification(market_signal, decision, current_price) await self._send_signal_notification(market_signal, decision, current_price)
else: else:
logger.warning(f" ⚠️ 减仓未成功执行,跳过通知") logger.warning(f" ⚠️ 减仓未成功执行,跳过通知")
else:
logger.info(f"\n⏸️ 交易未启用或决策为空")
def _get_best_signal_from_market(self, market_signal: Dict[str, Any]) -> Dict[str, Any]: 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], async def _send_trading_decision_notification(self, decision: Dict[str, Any],
market_signal: Dict[str, Any], market_signal: Dict[str, Any],
current_price: float): current_price: float,
prefix: str = ""):
"""发送交易决策通知(第二阶段)""" """发送交易决策通知(第二阶段)"""
try: try:
decision_type = decision.get('decision', 'HOLD') decision_type = decision.get('decision', 'HOLD')
symbol = market_signal.get('symbol') symbol = market_signal.get('symbol')
# 账户类型标识 # 账户类型标识
account_type = "📊 交易" account_type = f"{prefix} 📊 交易" if prefix else "📊 交易"
# 决策类型映射 # 决策类型映射
decision_map = { decision_map = {
@ -1165,7 +1269,8 @@ class CryptoAgent:
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
async def _send_signal_notification(self, market_signal: Dict[str, Any], 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: try:
decision_type = decision.get('decision', 'HOLD') decision_type = decision.get('decision', 'HOLD')
@ -1176,6 +1281,9 @@ class CryptoAgent:
# 构建消息 - 使用旧格式风格 # 构建消息 - 使用旧格式风格
symbol = market_signal.get('symbol') symbol = market_signal.get('symbol')
# 添加前缀到标题
title_prefix = f"{prefix} " if prefix else ""
action = decision.get('action', '') action = decision.get('action', '')
reasoning = decision.get('reasoning', '') reasoning = decision.get('reasoning', '')
risk_analysis = decision.get('risk_analysis', '') risk_analysis = decision.get('risk_analysis', '')
@ -1222,22 +1330,22 @@ class CryptoAgent:
# 挂单时标题显示"挂单",现价单时显示"开仓"/"平仓"等 # 挂单时标题显示"挂单",现价单时显示"开仓"/"平仓"等
if decision_type == 'OPEN': if decision_type == 'OPEN':
decision_title = '挂单' if entry_type == 'limit' else '开仓' 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" color = "green"
elif decision_type == 'CLOSE': elif decision_type == 'CLOSE':
decision_title = '挂单' if entry_type == 'limit' else '平仓' 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" color = "orange"
elif decision_type == 'ADD': elif decision_type == 'ADD':
decision_title = '挂单' if entry_type == 'limit' else '加仓' 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" color = "green"
elif decision_type == 'REDUCE': elif decision_type == 'REDUCE':
decision_title = '挂单' if entry_type == 'limit' else '减仓' 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" color = "orange"
else: else:
title = f"[执行] {account_type} {symbol} 交易执行" title = f"{title_prefix}[执行] {account_type} {symbol} 交易执行"
color = "blue" color = "blue"
# 构建卡片内容 # 构建卡片内容
@ -1723,6 +1831,237 @@ class CryptoAgent:
logger.error(f"执行减仓失败: {e}") logger.error(f"执行减仓失败: {e}")
return False 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], def _convert_to_paper_signal(self, symbol: str, signal: Dict[str, Any],
current_price: float) -> Dict[str, Any]: current_price: float) -> Dict[str, Any]:
"""转换 LLM 信号格式为模拟交易格式""" """转换 LLM 信号格式为模拟交易格式"""

View 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"杠杆不能超过 10xClawFi 规则),当前: {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

View 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
View 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');

View 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"
}
}

View File

@ -35,3 +35,7 @@ lxml>=4.9.0
akshare>=1.12.0 akshare>=1.12.0
apscheduler>=3.10.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
View 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)