stock-ai-agent/backend/tests/test_hyperliquid_live_integration.py
2026-04-22 10:38:25 +08:00

370 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Hyperliquid 真实 API 集成测试
⚠️ 警告:此测试会使用真实 API 调用和真实订单!
- 使用最小下单量ETHszDecimals=3
- 市价单会立即成交,产生实际盈亏
- 测试后自动清理所有订单和持仓
覆盖接口:
- 账户状态查询
- 杠杆设置
- 持仓查询
- 市价开仓
- 止盈止损设置TP limit 单 + SL trigger 单)
- 止盈止损验证(读取挂单确认 TP 和 SL 都存在)
- 市价平仓
运行方式:
cd backend
python3 tests/test_hyperliquid_live_integration.py
"""
import os
import sys
import time
import traceback
from datetime import datetime
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
# ==================== 测试配置 ====================
TEST_SYMBOL = 'ETH' # ETH 精度好szDecimals=3手续费低
TEST_SIZE = 0.01 # 最小下单量
TEST_LEVERAGE = 5 # 测试杠杆倍数
class TestResult:
"""测试结果收集器"""
def __init__(self):
self.results = []
def record(self, name: str, passed: bool, detail: str = ""):
self.results.append((name, passed, detail))
status = "✅ PASS" if passed else "❌ FAIL"
print(f" {status}: {name}")
if detail:
print(f" {detail}")
def summary(self):
print(f"\n{'='*60}")
print("测试结果汇总")
print(f"{'='*60}")
passed = sum(1 for _, p, _ in self.results if p)
total = len(self.results)
for name, p, detail in self.results:
status = "" if p else ""
line = f" {status} {name}"
if not p and detail:
line += f"{detail}"
print(line)
print(f"\n 总计: {passed}/{total} 通过")
print(f"{'='*60}")
return passed == total
# ==================== 测试函数 ====================
def test_account_state(service, r: TestResult):
"""查询账户状态"""
try:
state = service.get_account_state()
av = state['account_value']
ab = state['available_balance']
r.record(
"查询账户状态",
av > 0,
f"权益=${av:,.2f}, 可用=${ab:,.2f}"
)
except Exception as e:
r.record("查询账户状态", False, str(e))
def test_update_leverage(service, r: TestResult):
"""设置杠杆"""
try:
result = service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE)
r.record("设置杠杆", True, f"{TEST_SYMBOL}{TEST_LEVERAGE}x")
except Exception as e:
r.record("设置杠杆", False, str(e))
def test_get_positions(service, r: TestResult):
"""查询持仓"""
try:
positions = service.get_open_positions()
count = len(positions)
r.record("查询持仓", True, f"当前活跃持仓: {count}")
except Exception as e:
r.record("查询持仓", False, str(e))
def test_market_order_with_tp_sl(service, r: TestResult):
"""市价开仓 → 设置止盈止损 → 验证 → 平仓"""
opened = False
try:
# 0. 先清理已有持仓和挂单
try:
service.cancel_all_orders(TEST_SYMBOL)
except:
pass
try:
pos = service.get_position_for_symbol(TEST_SYMBOL)
if pos:
service.market_close_position(TEST_SYMBOL)
time.sleep(1)
except:
pass
# 1. 获取当前价格
all_mids = service.info.all_mids()
current_price = float(all_mids.get(TEST_SYMBOL, 0))
if current_price <= 0:
r.record("获取当前价格", False, f"无法获取 {TEST_SYMBOL} 价格")
return
print(f"\n 当前 {TEST_SYMBOL}: ${current_price:,.2f}")
# 2. 计算最小下单量
sz_decimals = service.get_sz_decimals(TEST_SYMBOL)
import math
size = max(math.floor(TEST_SIZE * (10 ** sz_decimals)) / (10 ** sz_decimals), 1 / (10 ** sz_decimals))
print(f" 下单量: {size} ({sz_decimals} 位精度)")
# 3. 设置杠杆
service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE)
# 4. 市价开多
result = service.place_market_order(
symbol=TEST_SYMBOL,
is_buy=True,
size=size,
reduce_only=False
)
if not result.get('success'):
r.record("市价开仓", False, result.get('error', '未知错误'))
return
r.record("市价开仓", True, f"{TEST_SYMBOL} buy {size}")
opened = True
time.sleep(2)
# 5. 验证持仓
position = service.get_position_for_symbol(TEST_SYMBOL)
r.record("验证持仓存在", position is not None,
f"size={position['size']}, entry=${position['entry_price']:,.2f}" if position else "未找到持仓")
# 6. 设置止盈止损
tp_price = round(current_price * 1.02, 2) # +2%
sl_price = round(current_price * 0.98, 2) # -2%
print(f" 设置 TP=${tp_price:,.2f}, SL=${sl_price:,.2f}")
tp_sl_result = service.set_tp_sl(
symbol=TEST_SYMBOL,
is_long=True,
size=size,
tp_price=tp_price,
sl_price=sl_price
)
tp_set = tp_sl_result.get('tp_set', False)
sl_set = tp_sl_result.get('sl_set', False)
errors = tp_sl_result.get('errors', [])
detail = f"TP=${tp_price:,.2f}({'' if tp_set else ''}), SL=${sl_price:,.2f}({'' if sl_set else ''})"
if errors:
detail += f" errors={errors}"
r.record("设置止盈止损", tp_set and sl_set, detail)
# 7. 验证止盈止损挂单
time.sleep(1)
tp_sl_prices = service.get_tp_sl_prices(TEST_SYMBOL)
has_tp = tp_sl_prices.get('take_profit') is not None
has_sl = tp_sl_prices.get('stop_loss') is not None
r.record("验证 TP/SL 挂单", has_tp and has_sl,
f"TP={tp_sl_prices.get('take_profit')}({'' if has_tp else ''}), "
f"SL={tp_sl_prices.get('stop_loss')}({'' if has_sl else ''})")
# 8. 取消止盈止损
try:
service.cancel_tp_sl_orders(TEST_SYMBOL)
except:
pass
time.sleep(1)
# 9. 市价平仓
close_result = service.market_close_position(TEST_SYMBOL)
r.record("市价平仓", close_result.get('success', False),
close_result.get('error', f"成功"))
if close_result.get('success'):
opened = False
time.sleep(2)
# 10. 验证已平仓
position_after = service.get_position_for_symbol(TEST_SYMBOL)
r.record("验证已平仓", position_after is None)
except Exception as e:
r.record("市价单流程异常", False, f"{e}\n{traceback.format_exc()}")
finally:
if opened:
try:
service.cancel_all_orders(TEST_SYMBOL)
time.sleep(0.5)
service.market_close_position(TEST_SYMBOL)
print(" 🧹 已自动清理残留持仓")
except Exception as cleanup_err:
print(f" ⚠️ 清理失败,请手动检查: {cleanup_err}")
def test_set_tp_sl_partial_failure(service, r: TestResult):
"""测试 set_tp_sl: 第一个失败不影响第二个"""
# 这个测试验证我们的修复:独立的 try-except
# 如果 TP 失败(例如价格为 0SL 应该仍然被设置
opened = False
try:
# 先清理
try:
service.cancel_all_orders(TEST_SYMBOL)
except:
pass
pos = service.get_position_for_symbol(TEST_SYMBOL)
if pos:
service.market_close_position(TEST_SYMBOL)
time.sleep(1)
# 1. 市价开仓
sz_decimals = service.get_sz_decimals(TEST_SYMBOL)
import math
size = max(math.floor(TEST_SIZE * (10 ** sz_decimals)) / (10 ** sz_decimals), 1 / (10 ** sz_decimals))
service.update_leverage(TEST_SYMBOL, TEST_LEVERAGE)
result = service.place_market_order(
symbol=TEST_SYMBOL,
is_buy=True,
size=size,
reduce_only=False
)
if not result.get('success'):
r.record("部分失败测试: 开仓", False, result.get('error', '未知'))
return
opened = True
time.sleep(2)
# 2. 设置一个有效的 SL但故意不设置 TP → tp_price=None
all_mids = service.info.all_mids()
current_price = float(all_mids.get(TEST_SYMBOL, 0))
sl_price = round(current_price * 0.98, 2)
tp_sl_result = service.set_tp_sl(
symbol=TEST_SYMBOL,
is_long=True,
size=size,
tp_price=None, # 不设 TP
sl_price=sl_price # 只设 SL
)
sl_set = tp_sl_result.get('sl_set', False)
r.record("部分设置测试 (仅 SL)", sl_set, f"sl_set={sl_set}, errors={tp_sl_result.get('errors', [])}")
# 3. 验证 SL 挂单存在
time.sleep(1)
tp_sl_prices = service.get_tp_sl_prices(TEST_SYMBOL)
has_sl = tp_sl_prices.get('stop_loss') is not None
r.record("验证 SL 挂单", has_sl, f"SL={tp_sl_prices.get('stop_loss')}")
# 4. 清理
service.cancel_all_orders(TEST_SYMBOL)
time.sleep(1)
service.market_close_position(TEST_SYMBOL)
opened = False
except Exception as e:
r.record("部分失败测试异常", False, f"{e}\n{traceback.format_exc()}")
finally:
if opened:
try:
service.cancel_all_orders(TEST_SYMBOL)
time.sleep(0.5)
service.market_close_position(TEST_SYMBOL)
print(" 🧹 已自动清理残留持仓")
except:
pass
# ==================== 主入口 ====================
def main():
print(f"\n{'='*60}")
print(f" Hyperliquid 实盘接口集成测试")
print(f"{'='*60}")
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f" 交易对: {TEST_SYMBOL}")
print(f" 下单量: {TEST_SIZE}")
print(f" 杠杆: {TEST_LEVERAGE}x")
print(f"{'='*60}")
r = TestResult()
# 初始化
try:
from app.services.hyperliquid_trading_service import HyperliquidTradingService
service = HyperliquidTradingService()
print(f" 钱包: {service.wallet_address[:10]}...")
except Exception as e:
print(f"\n❌ 初始化失败: {e}")
traceback.print_exc()
sys.exit(1)
# ---- 基础测试 ----
print(f"\n{''*40}")
print(" 基础接口")
print(f"{''*40}")
test_account_state(service, r)
time.sleep(0.3)
test_update_leverage(service, r)
time.sleep(0.3)
test_get_positions(service, r)
time.sleep(0.3)
# ---- 核心测试: 开仓 → TP/SL → 平仓 ----
print(f"\n{''*40}")
print(" 开仓 → 止盈止损 → 验证 → 平仓")
print(f"{''*40}")
test_market_order_with_tp_sl(service, r)
time.sleep(1)
# ---- 边界测试: 部分设置 ----
print(f"\n{''*40}")
print(" 部分设置测试")
print(f"{''*40}")
test_set_tp_sl_partial_failure(service, r)
# ---- 汇总 ----
all_passed = r.summary()
sys.exit(0 if all_passed else 1)
if __name__ == '__main__':
print("\n⚠️ 此测试会产生真实订单和手续费!")
print(f" 使用 {TEST_SYMBOL} 最小量 {TEST_SIZE}")
confirm = input("\n是否继续?(yes/no): ")
if confirm.strip().lower() != 'yes':
print("已取消")
sys.exit(0)
main()