├── data ├── positions_record.json ├── .DS_Store ├── funding_rates_record.json ├── funding_diff_signs.json └── positions │ ├── HYPE_close_1744351571.json │ ├── BERA_close_1744352068.json │ ├── BNB_close_1744362010.json │ ├── HYPE_close_1744310582.json │ ├── TRUMP_close_1744351576.json │ ├── BERA_close_1744331760.json │ ├── JUP_close_1744361890.json │ ├── WIF_close_1744360547.json │ ├── BERA_close_1744351669.json │ ├── BERA_close_1744351733.json │ ├── IP_close_1744280078.json │ ├── JUP_close_1744327380.json │ ├── BERA_close_1744351998.json │ ├── HYPE_close_1744331765.json │ ├── IP_close_1744360466.json │ └── XRP_close_1744361896.json ├── funding_arbitrage_bot ├── core │ ├── __init__.py │ ├── .DS_Store │ └── data_manager.py ├── exchanges │ ├── __init__.py │ └── .DS_Store ├── .DS_Store ├── requirements.txt ├── utils │ ├── __init__.py │ ├── logger.py │ ├── webhook_alerter.py │ ├── manage_funding_signs.py │ ├── hyperliquid_sdk.py │ ├── diagnostics.py │ ├── log_utilities.py │ ├── helpers.py │ ├── display_manager.py.bak │ └── display_manager.py ├── run.py ├── 价格格式指南.md ├── docs │ ├── Hyperliquid 订单创建指南.md │ ├── 硬编码参数说明文档.md │ └── # Backpack和Hyperliquid交易所API对比文档.md ├── main.py ├── README.md ├── config.yaml └── strategies │ └── funding_arbitrage.py ├── logs └── .DS_Store ├── .gitattributes ├── requirements.txt ├── run_bot.py ├── run.py └── README.md /data/positions_record.json: -------------------------------------------------------------------------------- 1 | { 2 | "position_open_times": {} 3 | } -------------------------------------------------------------------------------- /data/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/411-2/HEAD/data/.DS_Store -------------------------------------------------------------------------------- /funding_arbitrage_bot/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心逻辑模块 3 | 4 | 包含套利机器人的核心组件 5 | """ -------------------------------------------------------------------------------- /logs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/411-2/HEAD/logs/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /funding_arbitrage_bot/exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 交易所API模块 3 | 4 | 包含与交易所交互的类和函数 5 | """ -------------------------------------------------------------------------------- /data/funding_rates_record.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry_funding_rates": {}, 3 | "entry_prices": {} 4 | } -------------------------------------------------------------------------------- /funding_arbitrage_bot/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/411-2/HEAD/funding_arbitrage_bot/.DS_Store -------------------------------------------------------------------------------- /funding_arbitrage_bot/core/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/411-2/HEAD/funding_arbitrage_bot/core/.DS_Store -------------------------------------------------------------------------------- /funding_arbitrage_bot/exchanges/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocj520/411-2/HEAD/funding_arbitrage_bot/exchanges/.DS_Store -------------------------------------------------------------------------------- /data/funding_diff_signs.json: -------------------------------------------------------------------------------- 1 | {"FARTCOIN": -1, "HYPE": -1, "IP": -1, "LINK": -1, "ONDO": -1, "XRP": 1, "JUP": -1, "BERA": 1, "BNB": -1, "AVAX": -1} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.1 2 | pyyaml==6.0.1 3 | websockets==12.0 4 | python-dotenv==1.0.0 5 | rich==13.7.0 6 | requests==2.31.0 7 | httpx==0.27.0 -------------------------------------------------------------------------------- /funding_arbitrage_bot/requirements.txt: -------------------------------------------------------------------------------- 1 | httpx>=0.24.0 2 | websockets>=11.0.0 3 | asyncio>=3.4.3 4 | pyyaml>=6.0 5 | hyperliquid-python-sdk>=0.0.4 6 | rich>=13.0.0 -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 工具函数模块 3 | 4 | 包含各种辅助函数和工具 5 | """ 6 | 7 | from .helpers import configure_logging 8 | from .webhook_alerter import WebhookAlerter -------------------------------------------------------------------------------- /data/positions/HYPE_close_1744351571.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "HYPE", 3 | "action": "close", 4 | "timestamp": 1744351571.17971, 5 | "formatted_time": "2025-04-11 14:06:11", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 5.0, 9 | "hl_size": 5.0, 10 | "bp_price": 15.107, 11 | "hl_price": 15.0785, 12 | "price_diff": 0.028499999999999304, 13 | "price_diff_percent": 0.18901084325363468, 14 | "bp_funding": 0.000203763, 15 | "hl_funding": 0.0001, 16 | "funding_diff": 0.000103763 17 | } -------------------------------------------------------------------------------- /data/positions/BERA_close_1744352068.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BERA", 3 | "action": "close", 4 | "timestamp": 1744352068.09896, 5 | "formatted_time": "2025-04-11 14:14:28", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 9.0, 9 | "hl_size": 9.0, 10 | "bp_price": 4.1445, 11 | "hl_price": 4.14325, 12 | "price_diff": 0.0012499999999997513, 13 | "price_diff_percent": 0.03016955288722021, 14 | "bp_funding": -0.000227962, 15 | "hl_funding": -0.00075932, 16 | "funding_diff": 0.000531358 17 | } -------------------------------------------------------------------------------- /data/positions/BNB_close_1744362010.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BNB", 3 | "action": "close", 4 | "timestamp": 1744362010.017546, 5 | "formatted_time": "2025-04-11 17:00:10", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 0.1, 9 | "hl_size": 0.1, 10 | "bp_price": 579.21, 11 | "hl_price": 579.725, 12 | "price_diff": -0.5149999999999864, 13 | "price_diff_percent": -0.08883522359739296, 14 | "bp_funding": -0.00053567, 15 | "hl_funding": -0.0001551048, 16 | "funding_diff": -0.0003805652 17 | } -------------------------------------------------------------------------------- /data/positions/HYPE_close_1744310582.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "HYPE", 3 | "action": "close", 4 | "timestamp": 1744310582.1601331, 5 | "formatted_time": "2025-04-11 02:43:02", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 10.0, 9 | "hl_size": 10.0, 10 | "bp_price": 13.889, 11 | "hl_price": 13.8795, 12 | "price_diff": 0.009499999999999176, 13 | "price_diff_percent": 0.06844626967829659, 14 | "bp_funding": 0.001830677, 15 | "hl_funding": 0.0001, 16 | "funding_diff": 0.001730677 17 | } -------------------------------------------------------------------------------- /data/positions/TRUMP_close_1744351576.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "TRUMP", 3 | "action": "close", 4 | "timestamp": 1744351576.8138728, 5 | "formatted_time": "2025-04-11 14:06:16", 6 | "bp_side": "SELL", 7 | "hl_side": "BUY", 8 | "bp_size": 6.0, 9 | "hl_size": 6.0, 10 | "bp_price": 8.04, 11 | "hl_price": 8.05395, 12 | "price_diff": -0.01395000000000124, 13 | "price_diff_percent": -0.17320693572720514, 14 | "bp_funding": -0.000141964, 15 | "hl_funding": 0.0001, 16 | "funding_diff": -0.000241964 17 | } -------------------------------------------------------------------------------- /data/positions/BERA_close_1744331760.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BERA", 3 | "action": "close", 4 | "timestamp": 1744331760.831684, 5 | "formatted_time": "2025-04-11 08:36:00", 6 | "bp_side": "SELL", 7 | "hl_side": "BUY", 8 | "bp_size": 9.0, 9 | "hl_size": 9.0, 10 | "bp_price": 3.8589, 11 | "hl_price": 3.86425, 12 | "price_diff": -0.005349999999999966, 13 | "price_diff_percent": -0.13844859934010392, 14 | "bp_funding": -0.000558813, 15 | "hl_funding": -0.0001071584, 16 | "funding_diff": -0.0004516546 17 | } -------------------------------------------------------------------------------- /data/positions/JUP_close_1744361890.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "JUP", 3 | "action": "close", 4 | "timestamp": 1744361890.697758, 5 | "formatted_time": "2025-04-11 16:58:10", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 130.0, 9 | "hl_size": 130.0, 10 | "bp_price": 0.3699, 11 | "hl_price": 0.369445, 12 | "price_diff": 0.0004549999999999832, 13 | "price_diff_percent": 0.12315770953727433, 14 | "bp_funding": -0.000347978, 15 | "hl_funding": 1.46136e-05, 16 | "funding_diff": -0.0003625916 17 | } -------------------------------------------------------------------------------- /data/positions/WIF_close_1744360547.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "WIF", 3 | "action": "close", 4 | "timestamp": 1744360547.619606, 5 | "formatted_time": "2025-04-11 16:35:47", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 130.0, 9 | "hl_size": 130.0, 10 | "bp_price": 0.3773, 11 | "hl_price": 0.377365, 12 | "price_diff": -6.499999999998174e-05, 13 | "price_diff_percent": -0.017224702873870585, 14 | "bp_funding": -0.000146388, 15 | "hl_funding": 0.0001, 16 | "funding_diff": -0.000246388 17 | } -------------------------------------------------------------------------------- /data/positions/BERA_close_1744351669.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BERA", 3 | "action": "close", 4 | "timestamp": 1744351669.646218, 5 | "formatted_time": "2025-04-11 14:07:49", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 9.0, 9 | "hl_size": 9.0, 10 | "bp_price": 4.1497, 11 | "hl_price": 4.139200000000001, 12 | "price_diff": 0.01049999999999951, 13 | "price_diff_percent": 0.2536722071897832, 14 | "bp_funding": -0.000236292, 15 | "hl_funding": -0.0008110992, 16 | "funding_diff": 0.0005748072 17 | } -------------------------------------------------------------------------------- /data/positions/BERA_close_1744351733.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BERA", 3 | "action": "close", 4 | "timestamp": 1744351733.191183, 5 | "formatted_time": "2025-04-11 14:08:53", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 9.0, 9 | "hl_size": 9.0, 10 | "bp_price": 4.1622, 11 | "hl_price": 4.1518, 12 | "price_diff": 0.010400000000000631, 13 | "price_diff_percent": 0.2504937617419103, 14 | "bp_funding": -0.000235198, 15 | "hl_funding": -0.000758532, 16 | "funding_diff": 0.0005233340000000001 17 | } -------------------------------------------------------------------------------- /data/positions/IP_close_1744280078.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "IP", 3 | "action": "close", 4 | "timestamp": 1744280078.1639948, 5 | "formatted_time": "2025-04-10 18:14:38", 6 | "bp_side": "SELL", 7 | "hl_side": "BUY", 8 | "bp_size": 13.0, 9 | "hl_size": 13.0, 10 | "bp_price": 4.2865, 11 | "hl_price": 4.298299999999999, 12 | "price_diff": -0.011799999999999145, 13 | "price_diff_percent": -0.27452713863618516, 14 | "bp_funding": -0.000780786, 15 | "hl_funding": 0.0001, 16 | "funding_diff": -0.000880786 17 | } -------------------------------------------------------------------------------- /data/positions/JUP_close_1744327380.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "JUP", 3 | "action": "close", 4 | "timestamp": 1744327380.321722, 5 | "formatted_time": "2025-04-11 07:23:00", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 130.0, 9 | "hl_size": 130.0, 10 | "bp_price": 0.3635, 11 | "hl_price": 0.36306499999999997, 12 | "price_diff": 0.0004350000000000187, 13 | "price_diff_percent": 0.11981325657940554, 14 | "bp_funding": 0.000694936, 15 | "hl_funding": 2.5936e-05, 16 | "funding_diff": 0.000669 17 | } -------------------------------------------------------------------------------- /data/positions/BERA_close_1744351998.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "BERA", 3 | "action": "close", 4 | "timestamp": 1744351998.939126, 5 | "formatted_time": "2025-04-11 14:13:18", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 18.0, 9 | "hl_size": 9.0, 10 | "bp_price": 4.1576, 11 | "hl_price": 4.15205, 12 | "price_diff": 0.005550000000000388, 13 | "price_diff_percent": 0.13366891053817723, 14 | "bp_funding": -0.000229221, 15 | "hl_funding": -0.0007298776, 16 | "funding_diff": 0.0005006565999999999 17 | } -------------------------------------------------------------------------------- /data/positions/HYPE_close_1744331765.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "HYPE", 3 | "action": "close", 4 | "timestamp": 1744331765.911217, 5 | "formatted_time": "2025-04-11 08:36:05", 6 | "bp_side": "SELL", 7 | "hl_side": "BUY", 8 | "bp_size": 5.0, 9 | "hl_size": 5.0, 10 | "bp_price": 13.983, 11 | "hl_price": 13.9815, 12 | "price_diff": 0.0015000000000000568, 13 | "price_diff_percent": 0.010728462611308207, 14 | "bp_funding": -0.000891498, 15 | "hl_funding": -0.0001780072, 16 | "funding_diff": -0.0007134908000000001 17 | } -------------------------------------------------------------------------------- /data/positions/IP_close_1744360466.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "IP", 3 | "action": "close", 4 | "timestamp": 1744360466.6462028, 5 | "formatted_time": "2025-04-11 16:34:26", 6 | "bp_side": "BUY", 7 | "hl_side": "SELL", 8 | "bp_size": 13.0, 9 | "hl_size": 13.0, 10 | "bp_price": 4.0734, 11 | "hl_price": 4.07455, 12 | "price_diff": -0.0011499999999999844, 13 | "price_diff_percent": -0.028223975653752786, 14 | "bp_funding": -0.000629181, 15 | "hl_funding": -6.79016e-05, 16 | "funding_diff": -0.0005612794000000001 17 | } -------------------------------------------------------------------------------- /data/positions/XRP_close_1744361896.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol": "XRP", 3 | "action": "close", 4 | "timestamp": 1744361896.425173, 5 | "formatted_time": "2025-04-11 16:58:16", 6 | "bp_side": "SELL", 7 | "hl_side": "BUY", 8 | "bp_size": 25.0, 9 | "hl_size": 25.0, 10 | "bp_price": 2.0037, 11 | "hl_price": 2.0025000000000004, 12 | "price_diff": 0.0011999999999994237, 13 | "price_diff_percent": 0.05992509363293002, 14 | "bp_funding": 0.000173908, 15 | "hl_funding": -0.0001996568, 16 | "funding_diff": 0.0003735648 17 | } -------------------------------------------------------------------------------- /funding_arbitrage_bot/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 直接运行脚本 5 | 6 | 在funding_arbitrage_bot目录中直接运行的入口脚本, 7 | 解决相对导入问题。 8 | """ 9 | 10 | import os 11 | import sys 12 | import asyncio 13 | import argparse 14 | 15 | # 将上级目录添加到路径,这样可以确保能找到funding_arbitrage_bot包 16 | parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | sys.path.insert(0, parent_dir) 18 | 19 | if __name__ == "__main__": 20 | # 解析命令行参数 21 | parser = argparse.ArgumentParser(description="资金费率套利机器人") 22 | parser.add_argument("--test", action="store_true", help="测试模式 - 只测试API连接和获取持仓") 23 | parser.add_argument("--config", default="funding_arbitrage_bot/config.yaml", help="配置文件路径") 24 | args = parser.parse_args() 25 | 26 | # 设置事件循环策略 27 | if sys.platform == "win32": 28 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 29 | 30 | # 使用绝对导入 31 | from funding_arbitrage_bot.main import main 32 | 33 | # 保存原始sys.argv 34 | original_argv = sys.argv.copy() 35 | 36 | # 重新构建sys.argv以传递正确的参数给main函数 37 | sys.argv = [sys.argv[0]] # 保留脚本名称 38 | 39 | # 添加测试模式参数 40 | if args.test: 41 | sys.argv.append("--test") 42 | 43 | # 添加配置文件参数 44 | sys.argv.extend(["--config", args.config]) 45 | 46 | # 运行主程序 47 | asyncio.run(main()) 48 | 49 | # 恢复原始sys.argv 50 | sys.argv = original_argv -------------------------------------------------------------------------------- /run_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 启动脚本 5 | 6 | 资金费率套利机器人的启动脚本,提供命令行接口 7 | """ 8 | 9 | import os 10 | import sys 11 | import asyncio 12 | import logging 13 | import traceback 14 | import argparse 15 | import yaml 16 | 17 | # 设置工作目录为项目根目录 18 | project_root = os.path.dirname(os.path.abspath(__file__)) 19 | os.chdir(project_root) 20 | 21 | # 添加项目根目录到Python路径 22 | if project_root not in sys.path: 23 | sys.path.insert(0, project_root) 24 | 25 | # 打印当前的Python路径,用于调试 26 | print(f"Python路径: {sys.path}") 27 | print(f"当前工作目录: {os.getcwd()}") 28 | 29 | try: 30 | # 导入主模块 31 | from funding_arbitrage_bot.main import main 32 | except ImportError as e: 33 | print(f"导入主模块时出错: {e}") 34 | print("尝试直接导入...") 35 | try: 36 | # 如果直接导入失败,可能是因为funding_arbitrage_bot不在Python路径中 37 | # 添加父目录到Python路径 38 | parent_dir = os.path.dirname(project_root) 39 | if parent_dir not in sys.path: 40 | sys.path.insert(0, parent_dir) 41 | print(f"添加父目录到Python路径: {parent_dir}") 42 | print(f"更新后的Python路径: {sys.path}") 43 | 44 | # 再次尝试导入 45 | from funding_arbitrage_bot.main import main 46 | except ImportError as e2: 47 | print(f"第二次尝试导入失败: {e2}") 48 | print("请确保已正确安装依赖或位于正确的工作目录") 49 | sys.exit(1) 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser(description="资金费率套利机器人") 53 | parser.add_argument("--test", action="store_true", help="测试模式 - 只测试API连接") 54 | parser.add_argument("--config", help="配置文件路径") 55 | args = parser.parse_args() 56 | 57 | try: 58 | # Windows系统需要设置事件循环策略 59 | if sys.platform == "win32": 60 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 61 | 62 | # 运行主函数 63 | asyncio.run(main(test=args.test, config_path=args.config)) 64 | 65 | except KeyboardInterrupt: 66 | print("\n程序已被用户中断") 67 | except FileNotFoundError as e: 68 | print(f"错误: {e}") 69 | print(f"当前工作目录: {os.getcwd()}") 70 | except yaml.YAMLError as e: 71 | print(f"YAML配置文件格式错误: {e}") 72 | except Exception as e: 73 | print(f"程序运行出错: {e}") 74 | traceback.print_exc() 75 | sys.exit(1) -------------------------------------------------------------------------------- /funding_arbitrage_bot/价格格式指南.md: -------------------------------------------------------------------------------- 1 | # Hyperliquid 价格格式指南 2 | 3 | ## 概述 4 | 5 | Hyperliquid交易所对订单价格有严格的格式要求,每个币种有特定的价格精度(price_precision)和最小价格变动单位(tick_size)。本文档提供了处理Hyperliquid价格格式的最佳实践。 6 | 7 | ## 价格精度和Tick Size 8 | 9 | ### 价格精度 (price_precision) 10 | 价格精度指价格的小数位数。例如: 11 | - BTC价格精度为1,意味着价格只能有1位小数,如30000.0 12 | - ETH价格精度为2,意味着价格可以有2位小数,如2500.50 13 | - SOL价格精度为3,意味着价格可以有3位小数,如100.500 14 | 15 | ### 最小价格变动单位 (tick_size) 16 | tick_size是价格的最小变动单位。例如: 17 | - BTC的tick_size为1.0,意味着价格只能按1.0的整数倍变动,如30000.0, 30001.0 18 | - ETH的tick_size为0.1,意味着价格只能按0.1的整数倍变动,如2500.0, 2500.1 19 | - SOL的tick_size为0.01,意味着价格只能按0.01的整数倍变动,如100.00, 100.01 20 | 21 | **注意**:tick_size必须大于等于price_precision对应的最小单位。 22 | 例如:如果price_precision=2,则tick_size最小为0.01。 23 | 24 | ## 常见问题 25 | 26 | ### "Order has invalid price"错误 27 | 当价格不符合交易所的要求时,会出现此错误。原因可能是: 28 | 1. 价格不符合tick_size要求,即不是tick_size的整数倍 29 | 2. 价格的小数位数超过了price_precision 30 | 3. 价格为负数或者为零 31 | 32 | ## 正确处理价格的步骤 33 | 34 | ### 1. 确保价格是浮点数 35 | 首先,确保价格是浮点数类型,而不是字符串。 36 | 37 | ```python 38 | price = float(price) # 如果price可能是字符串类型 39 | ``` 40 | 41 | ### 2. 按照tick_size调整价格 42 | 将价格调整为tick_size的整数倍: 43 | 44 | ```python 45 | adjusted_price = round(price / tick_size) * tick_size 46 | ``` 47 | 48 | ### 3. 控制小数位数 49 | 将价格控制在指定的小数位数: 50 | 51 | ```python 52 | adjusted_price = round(adjusted_price, price_precision) 53 | ``` 54 | 55 | ### 4. 完整示例 56 | 57 | ```python 58 | def adjust_price(price, tick_size, price_precision): 59 | """ 60 | 调整价格以符合交易所要求 61 | 62 | Args: 63 | price: 原始价格 64 | tick_size: 最小价格变动单位 65 | price_precision: 价格精度(小数位数) 66 | 67 | Returns: 68 | 调整后的价格 69 | """ 70 | # 确保价格为浮点数 71 | price = float(price) 72 | 73 | # 按照tick_size调整价格 74 | adjusted_price = round(price / tick_size) * tick_size 75 | 76 | # 控制小数位数 77 | adjusted_price = round(adjusted_price, price_precision) 78 | 79 | return adjusted_price 80 | ``` 81 | 82 | ## 币种配置 83 | 84 | 每个交易对的price_precision和tick_size在config.yaml文件中配置: 85 | 86 | ```yaml 87 | trading_pairs: 88 | - symbol: "BTC" 89 | price_precision: 1 90 | tick_size: 1.0 91 | 92 | - symbol: "ETH" 93 | price_precision: 2 94 | tick_size: 0.1 95 | 96 | - symbol: "SOL" 97 | price_precision: 3 98 | tick_size: 0.001 99 | ``` 100 | 101 | ## 特殊处理:模拟市价单 102 | 103 | 由于Hyperliquid不支持市价单,我们使用限价单模拟市价单效果。为确保订单能够快速成交,需要设置一个有利的价格: 104 | 105 | ### 买入订单 106 | 设置略高于市场价的限价(通常为市场价的105%),确保能迅速成交: 107 | ```python 108 | if is_buy: 109 | # 买入时设置价格略高于市场价 110 | limit_price = current_price * 1.05 111 | # 调整价格格式 112 | limit_price = adjust_price(limit_price, tick_size, price_precision) 113 | ``` 114 | 115 | ### 卖出订单 116 | 设置略低于市场价的限价(通常为市场价的95%),确保能迅速成交: 117 | ```python 118 | else: 119 | # 卖出时设置价格略低于市场价 120 | limit_price = current_price * 0.95 121 | # 调整价格格式 122 | limit_price = adjust_price(limit_price, tick_size, price_precision) 123 | ``` 124 | 125 | ## 日志记录最佳实践 126 | 127 | 为便于调试和故障排除,建议记录价格调整的每个步骤: 128 | 129 | ```python 130 | logger.info(f"原始价格: {original_price}") 131 | logger.info(f"按tick_size={tick_size}调整: {price_after_tick}") 132 | logger.info(f"按precision={price_precision}调整: {final_price}") 133 | ``` 134 | 135 | 这样可以在日志中清晰看到价格的变化过程,更容易定位问题。 136 | 137 | ## 结论 138 | 139 | 正确处理Hyperliquid的价格格式是确保订单成功的关键。通过遵循本文档的最佳实践,可以避免"Order has invalid price"错误,提高交易成功率。 -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 日志配置模块 5 | 6 | 配置应用程序的日志系统,支持控制台和文件输出 7 | 支持日志轮转功能、文件大小限制和备份 8 | """ 9 | 10 | import logging 11 | import os 12 | import sys 13 | import time 14 | from logging.handlers import RotatingFileHandler 15 | from datetime import datetime 16 | from pathlib import Path 17 | from typing import Dict, List, Any, Optional 18 | 19 | # 添加TRACE日志级别(比DEBUG更详细)- 暂时注释掉,因为需要重启应用才能生效 20 | # TRACE = 5 21 | # logging.addLevelName(TRACE, "TRACE") 22 | 23 | def setup_logger(config: Dict[str, Any], name: str = "funding_arbitrage") -> logging.Logger: 24 | """ 25 | 设置日志记录器,支持日志轮转 26 | 27 | Args: 28 | config: 日志配置字典,包含级别和文件路径 29 | name: 日志记录器名称 30 | 31 | Returns: 32 | 配置好的日志记录器 33 | """ 34 | # 导入系统输出进行调试 35 | import sys 36 | print(f"设置日志记录器: {name}", file=sys.__stdout__) 37 | 38 | # 创建日志目录 39 | log_file = config.get("file", "logs/arbitrage.log") 40 | log_dir = os.path.dirname(log_file) 41 | if log_dir and not os.path.exists(log_dir): 42 | os.makedirs(log_dir) 43 | 44 | # 获取日志级别 45 | log_level_str = config.get("level", "INFO").upper() 46 | log_level = getattr(logging, log_level_str, logging.INFO) 47 | 48 | # 获取日志轮转配置 49 | max_bytes = config.get("max_file_size", 10 * 1024 * 1024) # 默认10MB 50 | backup_count = config.get("backup_count", 5) # 默认保留5个备份 51 | 52 | # 检查是否完全禁用控制台日志 53 | disable_console = ( 54 | os.environ.get("DISABLE_CONSOLE_LOGGING", "0") == "1" or 55 | config.get("disable_console_logging", False) 56 | ) 57 | 58 | print(f"控制台日志输出: {'禁用' if disable_console else '启用'}", file=sys.__stdout__) 59 | print(f"日志轮转设置: 最大大小={max_bytes/1024/1024:.1f}MB, 备份数量={backup_count}", file=sys.__stdout__) 60 | 61 | # 移除所有已有的处理器 62 | # 获取根日志记录器并删除所有已有处理器 63 | for logger_name in logging.root.manager.loggerDict: 64 | logger_obj = logging.getLogger(logger_name) 65 | # 移除所有处理器 66 | for handler in logger_obj.handlers[:]: 67 | logger_obj.removeHandler(handler) 68 | 69 | # 移除根日志记录器的所有处理器 70 | root_logger = logging.getLogger() 71 | for handler in root_logger.handlers[:]: 72 | root_logger.removeHandler(handler) 73 | 74 | # 配置日志记录器 75 | logger = logging.getLogger(name) 76 | logger.setLevel(log_level) 77 | logger.handlers = [] # 清除所有处理器 78 | 79 | # 添加轮转文件处理器 80 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 81 | 82 | file_handler = RotatingFileHandler( 83 | log_file, 84 | maxBytes=max_bytes, 85 | backupCount=backup_count, 86 | encoding="utf-8" 87 | ) 88 | file_handler.setFormatter(formatter) 89 | logger.addHandler(file_handler) 90 | 91 | # 阻止日志传递到上层日志记录器 92 | logger.propagate = False 93 | 94 | # 创建空处理器添加到根日志记录器,防止其他地方添加控制台处理器 95 | class NullHandler(logging.Handler): 96 | def emit(self, record): 97 | pass 98 | 99 | root_handler = NullHandler() 100 | root_logger.addHandler(root_handler) 101 | 102 | # 确保root_logger设置了级别 103 | root_logger.setLevel(logging.WARNING) 104 | 105 | print(f"日志记录器设置完成,日志文件: {log_file}", file=sys.__stdout__) 106 | 107 | # 输出日志初始化信息到文件,但不到控制台 108 | logger.info(f"日志系统已初始化,级别: {log_level_str}, 文件: {log_file}, 轮转: {max_bytes/1024/1024:.1f}MB/{backup_count}个备份") 109 | 110 | return logger -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/webhook_alerter.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebhookAlerter - 用于通过Webhook发送资金费率套利机器人的交易通知 3 | """ 4 | 5 | import json 6 | import logging 7 | import requests 8 | from typing import Dict, Any, Optional 9 | 10 | class WebhookAlerter: 11 | """ 12 | 用于通过Webhook发送资金费率套利机器人的交易通知 13 | """ 14 | 15 | def __init__(self, webhook_url: Optional[str] = None): 16 | """ 17 | 初始化Webhook通知器 18 | 19 | Args: 20 | webhook_url: Webhook URL,如果为None则禁用通知 21 | """ 22 | self.webhook_url = webhook_url 23 | self.logger = logging.getLogger('funding_arbitrage') 24 | 25 | def send_notification(self, title: str, message: str, data: Dict[str, Any] = None) -> bool: 26 | """ 27 | 发送通知消息 28 | 29 | Args: 30 | title: 通知标题 31 | message: 通知内容 32 | data: 附加数据(可选) 33 | 34 | Returns: 35 | bool: 发送是否成功 36 | """ 37 | if not self.webhook_url: 38 | return False 39 | 40 | try: 41 | payload = { 42 | "title": title, 43 | "message": message 44 | } 45 | 46 | if data: 47 | payload["data"] = data 48 | 49 | response = requests.post( 50 | self.webhook_url, 51 | json=payload, 52 | headers={"Content-Type": "application/json"}, 53 | timeout=10 54 | ) 55 | 56 | if response.status_code >= 200 and response.status_code < 300: 57 | self.logger.debug(f"通知发送成功: {title}") 58 | return True 59 | else: 60 | self.logger.warning(f"通知发送失败,状态码: {response.status_code}, 响应: {response.text}") 61 | return False 62 | 63 | except Exception as e: 64 | self.logger.error(f"发送通知时出错: {str(e)}") 65 | return False 66 | 67 | def send_order_notification(self, symbol: str, action: str, quantity: float, 68 | price: float, side: str, exchange: str) -> bool: 69 | """ 70 | 发送订单通知 71 | 72 | Args: 73 | symbol: 交易对 74 | action: 动作 (开仓/平仓) 75 | quantity: 数量 76 | price: 价格 77 | side: 方向 (多/空) 78 | exchange: 交易所 79 | 80 | Returns: 81 | bool: 发送是否成功 82 | """ 83 | title = f"{action}通知 - {symbol}" 84 | message = f"{exchange}交易所{action}{side}单: {quantity} {symbol} @ {price}" 85 | 86 | data = { 87 | "symbol": symbol, 88 | "action": action, 89 | "quantity": quantity, 90 | "price": price, 91 | "side": side, 92 | "exchange": exchange 93 | } 94 | 95 | return self.send_notification(title, message, data) 96 | 97 | def send_funding_notification(self, symbol: str, funding_rate: float, 98 | funding_diff: float, exchanges: list) -> bool: 99 | """ 100 | 发送资金费率通知 101 | 102 | Args: 103 | symbol: 交易对 104 | funding_rate: 资金费率 105 | funding_diff: 资金费率差异 106 | exchanges: 交易所列表 107 | 108 | Returns: 109 | bool: 发送是否成功 110 | """ 111 | title = f"资金费率通知 - {symbol}" 112 | message = f"{symbol}资金费率差异: {funding_diff:.6f}% ({exchanges[0]}: {funding_rate:.6f}%)" 113 | 114 | data = { 115 | "symbol": symbol, 116 | "funding_rate": funding_rate, 117 | "funding_diff": funding_diff, 118 | "exchanges": exchanges 119 | } 120 | 121 | return self.send_notification(title, message, data) -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 资金费率套利机器人主运行文件 5 | """ 6 | 7 | import os 8 | import sys 9 | import asyncio 10 | import logging 11 | import yaml 12 | from logging.handlers import RotatingFileHandler 13 | from datetime import datetime 14 | 15 | from funding_arbitrage_bot.strategies.funding_arbitrage import FundingArbitrageStrategy 16 | 17 | 18 | def setup_logging(config): 19 | """设置日志系统""" 20 | # 获取日志配置 21 | log_config = config.get("logging", {}) 22 | log_level_str = log_config.get("level", "INFO") 23 | log_file = log_config.get("file", "arbitrage.log") 24 | max_size_mb = log_config.get("max_size_mb", 10) 25 | backup_count = log_config.get("backup_count", 5) 26 | 27 | # 转换日志级别字符串到日志级别 28 | log_level = getattr(logging, log_level_str.upper(), logging.INFO) 29 | 30 | # 创建日志格式 31 | log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 32 | formatter = logging.Formatter(log_format) 33 | 34 | # 创建根日志记录器 35 | root_logger = logging.getLogger() 36 | root_logger.setLevel(log_level) 37 | 38 | # 清除现有处理器 39 | for handler in root_logger.handlers[:]: 40 | root_logger.removeHandler(handler) 41 | 42 | # 创建控制台处理器 43 | console_handler = logging.StreamHandler(sys.stdout) 44 | console_handler.setFormatter(formatter) 45 | root_logger.addHandler(console_handler) 46 | 47 | # 创建文件处理器 (如果配置了日志文件) 48 | if log_file: 49 | # 确保日志目录存在 50 | log_dir = os.path.dirname(log_file) 51 | if log_dir and not os.path.exists(log_dir): 52 | os.makedirs(log_dir) 53 | 54 | # 使用RotatingFileHandler以便轮转日志 55 | file_handler = RotatingFileHandler( 56 | log_file, 57 | maxBytes=max_size_mb * 1024 * 1024, 58 | backupCount=backup_count 59 | ) 60 | file_handler.setFormatter(formatter) 61 | root_logger.addHandler(file_handler) 62 | 63 | # 记录日志配置信息 64 | root_logger.info(f"日志级别设置为: {log_level_str}") 65 | root_logger.info(f"日志文件: {log_file}") 66 | 67 | return root_logger 68 | 69 | 70 | async def main(): 71 | """主函数""" 72 | # 加载配置 73 | config_path = "funding_arbitrage_bot/config.yaml" 74 | with open(config_path, "r") as file: 75 | config = yaml.safe_load(file) 76 | 77 | # 设置日志系统 78 | logger = setup_logging(config) 79 | logger.info("启动资金费率套利程序...") 80 | 81 | # 记录启动详情 82 | current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 83 | logger.info(f"启动时间: {current_time}") 84 | 85 | # 获取并记录策略配置 86 | strategy_config = config.get("strategy", {}) 87 | execution_mode = strategy_config.get("execution_mode", "simulate") 88 | min_funding_diff = strategy_config.get("min_funding_diff", 0.01) 89 | coins = strategy_config.get("coins", ["BTC", "ETH", "SOL"]) 90 | 91 | logger.info(f"执行模式: {execution_mode}") 92 | logger.info(f"最小资金费率差异阈值: {min_funding_diff}") 93 | logger.info(f"监控币种: {', '.join(coins)}") 94 | 95 | # 获取并记录流动性分析参数 96 | max_slippage_pct = strategy_config.get("max_slippage_pct", 0.1) 97 | min_liquidity_ratio = strategy_config.get("min_liquidity_ratio", 3.0) 98 | logger.info(f"最大允许滑点: {max_slippage_pct}%") 99 | logger.info(f"最小流动性比率: {min_liquidity_ratio}倍") 100 | 101 | # 初始化显示管理器 102 | from funding_arbitrage_bot.utils.display_manager import DisplayManager 103 | display_manager = DisplayManager(logger=logger) 104 | display_manager.start() 105 | 106 | # 初始化策略 107 | strategy = FundingArbitrageStrategy(config=config, logger=logger, display_manager=display_manager) 108 | logger.info("策略初始化完成") 109 | 110 | # 开始连接交易所 111 | logger.info("正在连接交易所...") 112 | 113 | try: 114 | # 连接Hyperliquid WebSocket 115 | await strategy.hyperliquid_api.connect_websocket() 116 | logger.info("Hyperliquid连接成功") 117 | 118 | # 连接Backpack API (可能不需要WebSocket连接) 119 | logger.info("Backpack API就绪") 120 | 121 | # 运行策略 122 | await strategy.run() 123 | 124 | except KeyboardInterrupt: 125 | logger.info("接收到停止信号,正在关闭...") 126 | except Exception as e: 127 | logger.exception(f"运行出错: {e}") 128 | finally: 129 | # 关闭连接 130 | logger.info("正在关闭连接...") 131 | await strategy.hyperliquid_api.close() 132 | await strategy.backpack_api.close() 133 | 134 | # 停止显示管理器 135 | if display_manager: 136 | display_manager.stop() 137 | 138 | logger.info("程序已停止") 139 | 140 | 141 | if __name__ == "__main__": 142 | asyncio.run(main()) -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/manage_funding_signs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 资金费率符号记录管理工具 5 | 6 | 用于查看、添加、修改或删除资金费率符号记录 7 | """ 8 | 9 | import os 10 | import json 11 | import argparse 12 | from typing import Dict, Optional, List 13 | 14 | class FundingSignsManager: 15 | """资金费率符号记录管理器""" 16 | 17 | def __init__(self, file_path: Optional[str] = None): 18 | """ 19 | 初始化管理器 20 | 21 | Args: 22 | file_path: 资金费率符号记录文件路径,如果为None则使用默认路径 23 | """ 24 | if file_path is None: 25 | # 获取项目根目录 26 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 27 | self.file_path = os.path.join(root_dir, 'data', 'funding_diff_signs.json') 28 | else: 29 | self.file_path = file_path 30 | 31 | # 确保data目录存在 32 | os.makedirs(os.path.dirname(self.file_path), exist_ok=True) 33 | 34 | def load_signs(self) -> Dict[str, int]: 35 | """ 36 | 加载资金费率符号记录 37 | 38 | Returns: 39 | Dict[str, int]: 资金费率符号记录字典 40 | """ 41 | try: 42 | if os.path.exists(self.file_path): 43 | with open(self.file_path, 'r') as f: 44 | signs_data = json.load(f) 45 | # 确保符号值是整数类型 46 | return {symbol: int(sign) for symbol, sign in signs_data.items()} 47 | return {} 48 | except Exception as e: 49 | print(f"加载资金费率符号记录文件失败: {e}") 50 | return {} 51 | 52 | def save_signs(self, signs: Dict[str, int]) -> bool: 53 | """ 54 | 保存资金费率符号记录 55 | 56 | Args: 57 | signs: 资金费率符号记录字典 58 | 59 | Returns: 60 | bool: 保存是否成功 61 | """ 62 | try: 63 | with open(self.file_path, 'w') as f: 64 | json.dump(signs, f, indent=2) 65 | print(f"资金费率符号记录已保存到文件: {self.file_path}") 66 | return True 67 | except Exception as e: 68 | print(f"保存资金费率符号记录到文件失败: {e}") 69 | return False 70 | 71 | def list_signs(self) -> None: 72 | """列出所有资金费率符号记录""" 73 | signs = self.load_signs() 74 | if not signs: 75 | print("没有资金费率符号记录") 76 | return 77 | 78 | print("\n当前资金费率符号记录:") 79 | print("-" * 30) 80 | print(f"{'币种':<10} {'符号':<10} {'含义':<10}") 81 | print("-" * 30) 82 | 83 | for symbol, sign in signs.items(): 84 | meaning = "正差异(BP>HL)" if sign == 1 else "负差异(BP bool: 90 | """ 91 | 添加或修改资金费率符号记录 92 | 93 | Args: 94 | symbol: 币种符号,如 "BTC" 95 | sign: 符号值,1表示正差异,-1表示负差异 96 | 97 | Returns: 98 | bool: 操作是否成功 99 | """ 100 | if sign not in (1, -1): 101 | print("错误: 符号值必须为1或-1") 102 | return False 103 | 104 | signs = self.load_signs() 105 | action = "修改" if symbol in signs else "添加" 106 | signs[symbol] = sign 107 | 108 | if self.save_signs(signs): 109 | print(f"成功{action}{symbol}的资金费率符号为: {sign}") 110 | return True 111 | return False 112 | 113 | def delete_sign(self, symbol: str) -> bool: 114 | """ 115 | 删除资金费率符号记录 116 | 117 | Args: 118 | symbol: 币种符号,如 "BTC" 119 | 120 | Returns: 121 | bool: 操作是否成功 122 | """ 123 | signs = self.load_signs() 124 | if symbol not in signs: 125 | print(f"警告: {symbol}不存在于资金费率符号记录中") 126 | return False 127 | 128 | del signs[symbol] 129 | 130 | if self.save_signs(signs): 131 | print(f"成功删除{symbol}的资金费率符号记录") 132 | return True 133 | return False 134 | 135 | def clear_signs(self) -> bool: 136 | """ 137 | 清空所有资金费率符号记录 138 | 139 | Returns: 140 | bool: 操作是否成功 141 | """ 142 | return self.save_signs({}) 143 | 144 | def main(): 145 | """命令行工具主函数""" 146 | parser = argparse.ArgumentParser(description="资金费率符号记录管理工具") 147 | 148 | # 创建子命令 149 | subparsers = parser.add_subparsers(dest="command", help="可用命令") 150 | 151 | # 列出所有记录 152 | list_parser = subparsers.add_parser("list", help="列出所有资金费率符号记录") 153 | 154 | # 添加或修改记录 155 | add_parser = subparsers.add_parser("add", help="添加或修改资金费率符号记录") 156 | add_parser.add_argument("symbol", help="币种符号,如 BTC") 157 | add_parser.add_argument("sign", type=int, choices=[1, -1], help="符号值: 1表示正差异,-1表示负差异") 158 | 159 | # 删除记录 160 | delete_parser = subparsers.add_parser("delete", help="删除资金费率符号记录") 161 | delete_parser.add_argument("symbol", help="币种符号,如 BTC") 162 | 163 | # 清空所有记录 164 | clear_parser = subparsers.add_parser("clear", help="清空所有资金费率符号记录") 165 | 166 | # 文件路径参数(可选) 167 | parser.add_argument("--file", "-f", help="资金费率符号记录文件路径") 168 | 169 | args = parser.parse_args() 170 | 171 | # 创建管理器 172 | manager = FundingSignsManager(args.file) 173 | 174 | # 执行对应命令 175 | if args.command == "list": 176 | manager.list_signs() 177 | elif args.command == "add": 178 | manager.add_sign(args.symbol, args.sign) 179 | elif args.command == "delete": 180 | manager.delete_sign(args.symbol) 181 | elif args.command == "clear": 182 | confirm = input("确定要清空所有资金费率符号记录吗?(y/n): ") 183 | if confirm.lower() == 'y': 184 | manager.clear_signs() 185 | else: 186 | parser.print_help() 187 | 188 | if __name__ == "__main__": 189 | main() -------------------------------------------------------------------------------- /funding_arbitrage_bot/docs/ Hyperliquid 订单创建指南.md: -------------------------------------------------------------------------------- 1 | # Hyperliquid 订单创建指南 2 | 3 | ## 目录 4 | 1. [基本设置](#基本设置) 5 | 2. [订单类型](#订单类型) 6 | 3. [参数说明](#参数说明) 7 | 4. [注意事项](#注意事项) 8 | 5. [示例代码](#示例代码) 9 | 6. [常见错误](#常见错误) 10 | 7. [测试结果](#测试结果) 11 | 8. [主脚本修改](#主脚本修改) 12 | 13 | ## 基本设置 14 | 15 | ### 1. 环境准备 16 | ```python 17 | from hyperliquid.exchange import Exchange 18 | from eth_account import Account 19 | ``` 20 | 21 | ### 2. 初始化交易所 22 | ```python 23 | # 创建钱包对象 24 | wallet = Account.from_key(private_key) 25 | 26 | # 初始化交易所 27 | exchange = Exchange(wallet=wallet) 28 | ``` 29 | 30 | ## 订单类型 31 | 32 | 根据我们的测试结果,Hyperliquid 目前**只支持限价单(Limit Order)**,**不支持市价单(Market Order)**。 33 | 34 | ### 限价单格式 35 | ```python 36 | order_type = { 37 | "limit": { 38 | "tif": "Gtc" # Good Till Cancel 39 | } 40 | } 41 | ``` 42 | 43 | ### 市价单测试结果 44 | 我们尝试了多种市价单格式,包括: 45 | 1. 使用 IOC (Immediate or Cancel) 格式 `{"ioc": {}}` 46 | 2. 使用 FOK (Fill or Kill) 格式 `{"fok": {}}` 47 | 3. 使用 Post Only 格式 `{"post_only": {}}` 48 | 49 | 所有市价单格式都返回了 `Invalid order type` 错误,表明 Hyperliquid API 目前不支持市价单。 50 | 51 | ## 参数说明 52 | 53 | ### 1. 必需参数 54 | - `name`: 交易对名称(字符串),例如 "SOL" 55 | - `is_buy`: 交易方向(布尔值) 56 | - `True`: 买入/做多 57 | - `False`: 卖出/做空 58 | - `sz`: 交易数量(浮点数),例如 0.1 59 | - `limit_px`: 限价(浮点数),例如 127.0 60 | - `order_type`: 订单类型(字典) 61 | 62 | ### 2. 参数格式要求 63 | - 所有数值参数(`sz` 和 `limit_px`)必须是浮点数类型,不能是字符串 64 | - 订单类型必须是字典格式,包含 `limit` 和 `tif` 字段 65 | 66 | ## 注意事项 67 | 68 | 1. **数值类型** 69 | - 所有数值必须使用浮点数类型 70 | - 不要使用字符串类型的数值,例如 `"0.1"` 或 `"127.0"` 71 | 72 | 2. **订单类型** 73 | - 目前只支持限价单 74 | - 不支持市价单 75 | - 必须指定 `tif` 参数 76 | 77 | 3. **价格精度** 78 | - 确保价格符合交易所的精度要求 79 | - 建议使用合理的价格,避免价格过高或过低 80 | 81 | 4. **数量精度** 82 | - 确保数量符合交易所的最小交易单位 83 | - 建议使用合理的数量,避免数量过小 84 | 85 | ## 示例代码 86 | 87 | ### 限价单示例 88 | ```python 89 | # 创建限价单 90 | order_result = exchange.order( 91 | name="SOL", # 交易对 92 | is_buy=True, # 买入/做多 93 | sz=0.1, # 数量(浮点数) 94 | limit_px=127.0, # 限价(浮点数) 95 | order_type={ # 订单类型 96 | "limit": { 97 | "tif": "Gtc" 98 | } 99 | } 100 | ) 101 | ``` 102 | 103 | ## 常见错误 104 | 105 | 1. **Invalid order type** 106 | - 原因:订单类型格式不正确 107 | - 解决:使用正确的限价单格式 `{"limit": {"tif": "Gtc"}}` 108 | 109 | 2. **Unknown format code 'f' for object of type 'str'** 110 | - 原因:数值参数使用了字符串类型 111 | - 解决:将字符串类型的数值改为浮点数类型 112 | 113 | 3. **Order size too small** 114 | - 原因:订单数量小于最小交易单位 115 | - 解决:增加订单数量到最小交易单位以上 116 | 117 | 4. **Invalid price** 118 | - 原因:价格不符合交易所要求 119 | - 解决:使用合理的价格,确保符合交易所的精度要求 120 | 121 | ## 最佳实践 122 | 123 | 1. **错误处理** 124 | ```python 125 | try: 126 | order_result = exchange.order(...) 127 | logger.info(f"下单结果: {order_result}") 128 | except Exception as e: 129 | logger.error(f"下单失败: {e}", exc_info=True) 130 | ``` 131 | 132 | 2. **日志记录** 133 | ```python 134 | logger.info(f"正在下单: {symbol} {'买入' if is_buy else '卖出'} {size},限价: {limit_price}") 135 | ``` 136 | 137 | 3. **参数验证** 138 | - 在下单前验证所有参数 139 | - 确保数值类型正确 140 | - 确保价格和数量在合理范围内 141 | 142 | 4. **订单状态检查** 143 | - 下单后检查订单状态 144 | - 记录订单ID以便后续查询 145 | - 监控订单是否成交 146 | 147 | ## 测试结果 148 | 149 | ### 限价单测试 150 | 限价单测试成功,返回结果: 151 | ``` 152 | {'status': 'ok', 'response': {'type': 'order', 'data': {'statuses': [{'resting': {'oid': 83400267467}}]}}} 153 | ``` 154 | 155 | 这表明: 156 | 1. 订单状态为 `ok`,表示下单成功 157 | 2. 订单类型为 `order` 158 | 3. 订单状态为 `resting`,表示订单已经提交但尚未成交 159 | 4. 订单ID为 `83400267467` 160 | 161 | ### 市价单测试 162 | 我们尝试了以下市价单格式: 163 | 1. IOC (Immediate or Cancel): `{"ioc": {}}` 164 | 2. FOK (Fill or Kill): `{"fok": {}}` 165 | 3. Post Only: `{"post_only": {}}` 166 | 167 | 所有市价单格式都返回了 `Invalid order type` 错误,表明 Hyperliquid API 目前不支持市价单。 168 | 169 | ## 主脚本修改 170 | 171 | 根据测试结果,我们对主脚本进行了以下修改: 172 | 173 | ### 1. 修改 `hyperliquid_api.py` 中的 `place_order` 方法 174 | 175 | ```python 176 | # 修改前 177 | if order_type.upper() == "MARKET": 178 | # 市价单 179 | price_adjuster = 1.01 if is_buy else 0.99 180 | market_price = str(round(current_price * price_adjuster, price_precision)) 181 | 182 | order_result = self.exchange.order( 183 | name=name, 184 | is_buy=is_buy, 185 | sz=formatted_size, # 已经是字符串 186 | limit_px=market_price, # 市价单也需要提供参考价格 187 | order_type={'market': {'tif': 'Gtc'}} # 使用错误的订单类型格式 188 | ) 189 | else: 190 | # 限价单 191 | price_adjuster = 1.005 if is_buy else 0.995 192 | limit_price = str(round(current_price * price_adjuster, price_precision)) 193 | 194 | order_result = self.exchange.order( 195 | name=name, 196 | is_buy=is_buy, 197 | sz=formatted_size, # 已经是字符串 198 | limit_px=limit_price, # 已经是字符串 199 | order_type={'limit': {'tif': 'Gtc'}} # 使用正确的订单类型格式 200 | ) 201 | 202 | # 修改后 203 | # 根据方向调整价格,确保订单能够快速成交 204 | price_adjuster = 1.01 if is_buy else 0.99 # 买入时价格略高,卖出时价格略低 205 | limit_price = float(round(current_price * price_adjuster, price_precision)) # 使用浮点数类型 206 | 207 | # 使用限价单格式(Hyperliquid不支持市价单) 208 | order_result = self.exchange.order( 209 | name=name, 210 | is_buy=is_buy, 211 | sz=formatted_size, # 使用浮点数类型 212 | limit_px=limit_price, # 使用浮点数类型 213 | order_type={"limit": {"tif": "Gtc"}} # 使用正确的限价单格式 214 | ) 215 | ``` 216 | 217 | ### 2. 修改 `arbitrage_engine.py` 中的开仓和平仓逻辑 218 | 219 | ```python 220 | # 修改前 221 | bp_order, hl_order = await asyncio.gather( 222 | self.backpack_api.place_order( 223 | position_data["bp_symbol"], 224 | position_data["bp_side"], 225 | "MARKET", # 使用市价单 226 | bp_size, 227 | None # 市价单不需要价格 228 | ), 229 | self.hyperliquid_api.place_order( 230 | position_data["hl_symbol"], 231 | position_data["hl_side"], 232 | "MARKET", # 使用市价单 233 | hl_size, 234 | None # 市价单不需要价格 235 | ) 236 | ) 237 | 238 | # 修改后 239 | bp_order, hl_order = await asyncio.gather( 240 | self.backpack_api.place_order( 241 | position_data["bp_symbol"], 242 | position_data["bp_side"], 243 | "MARKET", # Backpack使用市价单 244 | bp_size, 245 | None # 市价单不需要价格 246 | ), 247 | self.hyperliquid_api.place_order( 248 | position_data["hl_symbol"], 249 | position_data["hl_side"], 250 | "LIMIT", # Hyperliquid使用限价单 251 | hl_size, 252 | None # 价格会在API内部计算 253 | ) 254 | ) 255 | ``` 256 | 257 | ### 3. 主要修改点 258 | 259 | 1. **订单类型**: 260 | - 将 Hyperliquid 的市价单改为限价单 261 | - 使用正确的限价单格式 `{"limit": {"tif": "Gtc"}}` 262 | 263 | 2. **参数类型**: 264 | - 将字符串类型的数值改为浮点数类型 265 | - 确保 `sz` 和 `limit_px` 参数使用浮点数类型 266 | 267 | 3. **价格调整**: 268 | - 买入时价格略高(+1%),卖出时价格略低(-1%) 269 | - 确保订单能够快速成交 270 | 271 | 这些修改应该能够解决 Hyperliquid 无法成功下单的问题。 -------------------------------------------------------------------------------- /funding_arbitrage_bot/docs/硬编码参数说明文档.md: -------------------------------------------------------------------------------- 1 | # 资金套利机器人硬编码参数说明文档 2 | 3 | ## 简介 4 | 5 | 本文档详细列出了`funding_arbitrage_bot/core/arbitrage_engine.py`中存在的硬编码参数。这些参数目前直接写在代码中,而非从配置文件中读取。为了提高系统的可配置性和灵活性,建议将这些参数移至配置文件中。 6 | 7 | ## 硬编码参数列表 8 | 9 | ### 1. 最小资金费率差异默认值 (0.00001) 10 | ```python 11 | self.arb_threshold = open_conditions.get("min_funding_diff", 0.00001) 12 | ``` 13 | **含义**:这是判断两个交易所之间资金费率差异是否足够大以开仓的最小阈值。当Backpack和Hyperliquid之间的资金费率差异小于此值时,系统认为没有足够的套利空间而不开仓。0.00001表示0.001%的差异。 14 | 15 | **推荐修改**:将此默认值调整为从配置文件中读取,例如在配置文件中添加`default_min_funding_diff`参数。 16 | 17 | ### 2. 最小价格差异百分比 (0.2) 18 | ```python 19 | self.min_price_diff_percent = open_conditions.get("min_price_diff_percent", 0.2) 20 | ``` 21 | **含义**:两个交易所间的价格差异必须超过此百分比才考虑开仓(价差套利)。0.2表示0.2%的价格差异,即如果两个交易所同一币种的价格差异小于0.2%,系统认为没有足够的套利机会。 22 | 23 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(0.05)而非硬编码的0.2。 24 | 25 | ### 3. 最大价格差异百分比 (1.0) 26 | ```python 27 | self.max_price_diff_percent = open_conditions.get("max_price_diff_percent", 1.0) 28 | ``` 29 | **含义**:两个交易所间的价格差异超过此值时,系统认为可能是异常数据而不开仓。1.0表示1%的价格差异,防止因极端行情或数据错误导致的错误开仓。 30 | 31 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(1.6)而非硬编码的1.0。 32 | 33 | ### 4. 最小获利百分比 (0.1) 34 | ```python 35 | self.min_profit_percent = close_conditions.get("min_profit_percent", 0.1) 36 | ``` 37 | **含义**:平仓时的最小预期获利比例。0.1表示0.1%的盈利,当持仓的盈利达到此值时,系统可能会考虑平仓获利。 38 | 39 | **推荐修改**:配置文件中已有此参数,默认值一致。为保持一致性,建议仍从全局默认配置中读取。 40 | 41 | ### 5. 最大亏损百分比 (0.3) 42 | ```python 43 | self.max_loss_percent = close_conditions.get("max_loss_percent", 0.3) 44 | ``` 45 | **含义**:平仓时的最大可接受亏损比例(止损点)。0.3表示0.3%的亏损,当持仓的亏损达到此值时,系统会考虑平仓止损。 46 | 47 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(11)而非硬编码的0.3。 48 | 49 | ### 6. 最大开仓滑点百分比 (0.3) 50 | ```python 51 | "open_conditions", {}).get("max_slippage_percent", 0.3) 52 | ``` 53 | **含义**:开仓时可接受的最大滑点百分比。0.3表示0.3%的滑点,如果预估开仓的滑点超过此值,系统会放弃开仓以避免成交价格过差。 54 | 55 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(0.5)而非硬编码的0.3。 56 | 57 | ### 7. 平仓最小资金费率差异 (0.000005) 58 | ```python 59 | min_funding_diff = close_conditions.get("min_funding_diff", 0.000005) 60 | ``` 61 | **含义**:平仓时判断资金费率差异是否足够小的阈值。0.000005表示0.0005%,当两交易所的资金费率差异小于此值时,认为套利空间消失,可以考虑平仓。 62 | 63 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(0.0004)而非硬编码的0.000005。 64 | 65 | ### 8. 平仓最大滑点百分比 (0.25) 66 | ```python 67 | "max_close_slippage_percent", 0.25) 68 | ``` 69 | **含义**:平仓时可接受的最大滑点百分比。0.25表示0.25%的滑点,如果预估平仓的滑点超过此值,系统会推迟平仓以避免成交价格过差。 70 | 71 | **推荐修改**:配置文件中已有此参数,但默认值不一致。建议使用配置文件中的默认值(0.3)而非硬编码的0.25。 72 | 73 | ### 9. 默认滑点值 (0.1和0.01) 74 | ```python 75 | return 0.1 # 默认较高滑点 76 | ``` 77 | ```python 78 | return 0.01 * price # 默认1%滑点 79 | ``` 80 | **含义**:当无法正常计算滑点时使用的默认值。0.1表示10%的固定滑点值,或者根据当前价格计算1%的滑点。这些值会在无法获取足够深度数据或计算出错时使用。 81 | 82 | **推荐修改**:建议将这些默认值添加到配置文件中,例如`default_slippage`和`default_slippage_percent`参数。 83 | 84 | ### 10. 价格调整系数 (1.005和0.995) 85 | ```python 86 | hl_price_adjuster = 1.005 if hl_side == "BUY" else 0.995 87 | ``` 88 | **含义**:在Hyperliquid交易所下限价单时,对市场价格的调整系数。买入时将价格上调0.5%,卖出时将价格下调0.5%,以提高订单成交概率。 89 | 90 | **推荐修改**:建议将这些调整系数添加到配置文件中,例如`hl_buy_price_adjuster`和`hl_sell_price_adjuster`参数。 91 | 92 | ### 11. 默认tick_size (0.001) 93 | ```python 94 | tick_size = trading_pair_config.get("tick_size", 0.001) 95 | ``` 96 | **含义**:默认的价格最小变动单位。0.001表示价格只能按0.001的整数倍变动,例如如果价格为100.000,下一个可能的价格是100.001而不是100.0005。 97 | 98 | **推荐修改**:建议在全局配置中添加默认的tick_size参数,而不是硬编码在代码中。 99 | 100 | ### 12. 最大持仓数量默认值 (5) 101 | ```python 102 | self.max_positions_count = strategy_config.get("max_positions_count", 5) 103 | ``` 104 | **含义**:系统同时可以持有的最大不同币种数量。5表示系统最多同时对5种不同的币种开仓,超过此限制后会拒绝开更多的币种。这是风险控制的一种方式。 105 | 106 | **推荐修改**:配置文件中已有此参数,默认值一致。为保持一致性,建议从全局默认配置中读取。 107 | 108 | ## 建议修改方案 109 | 110 | 建议在配置文件中增加一个`defaults`部分,专门用于存放这些默认参数值,例如: 111 | 112 | ```yaml 113 | # 全局默认值配置 114 | defaults: 115 | min_funding_diff: 0.00001 # 最小资金费率差异默认值 116 | min_price_diff_percent: 0.2 # 最小价格差异百分比 117 | max_price_diff_percent: 1.0 # 最大价格差异百分比 118 | min_profit_percent: 0.1 # 最小获利百分比 119 | max_loss_percent: 0.3 # 最大亏损百分比 120 | max_slippage_percent: 0.3 # 最大开仓滑点百分比 121 | close_min_funding_diff: 0.000005 # 平仓最小资金费率差异 122 | max_close_slippage_percent: 0.25 # 平仓最大滑点百分比 123 | default_slippage: 0.1 # 默认固定滑点值 124 | default_slippage_percent: 0.01 # 默认滑点百分比 125 | hl_buy_price_adjuster: 1.005 # Hyperliquid买入价格调整系数 126 | hl_sell_price_adjuster: 0.995 # Hyperliquid卖出价格调整系数 127 | default_tick_size: 0.001 # 默认tick_size 128 | max_positions_count: 5 # 最大持仓数量默认值 129 | ``` 130 | 131 | 然后在代码中从此部分读取默认值,例如: 132 | 133 | ```python 134 | defaults = config.get("defaults", {}) 135 | self.arb_threshold = open_conditions.get("min_funding_diff", defaults.get("min_funding_diff", 0.00001)) 136 | ``` 137 | 138 | 这样既可以保持代码的灵活性,又可以通过配置文件统一管理所有默认参数。 139 | 140 | ## 完全硬编码参数列表 141 | 142 | 以下是一些完全硬编码、没有尝试从配置文件读取的参数: 143 | 144 | ### 1. 重试延迟时间 (0.5秒) 145 | ```python 146 | await asyncio.sleep(0.5) # 延迟后重试 147 | ``` 148 | **作用**:在API请求失败后,等待0.5秒再重试。这个值直接硬编码在多个地方,没有任何配置参数。这个延迟时间会影响订单失败后的重试速度,太短可能导致频繁请求对API造成压力,太长则会延缓交易执行时间。 149 | 150 | ### 2. Hyperliquid价格调整系数 (1.005和0.995) 151 | ```python 152 | hl_price_adjuster = 1.005 if hl_side == "BUY" else 0.995 153 | ``` 154 | **作用**:在Hyperliquid交易所下限价单时对市场价格的调整系数。买入时价格上调0.5%,卖出时下调0.5%,以提高订单成交率。这个值是直接硬编码的,没有从任何配置中读取。这个调整系数直接影响订单的成交价格,对交易的实际执行价格有重要影响。 155 | 156 | ### 3. 默认滑点值 (0.1或10%) 157 | ```python 158 | return 0.1 # 默认较高滑点 159 | ``` 160 | **作用**:当无法计算实际滑点时使用的默认固定滑点值。这是一个相当高的滑点(10%),作为极端情况下的保守估计。这个值直接硬编码在多个错误处理分支中,没有从配置文件读取。高滑点估计会影响开仓决策,过高的默认值可能导致系统错误地拒绝一些本可以执行的交易。 161 | 162 | ### 4. 默认滑点百分比 (0.01或1%) 163 | ```python 164 | return 0.01 * price # 默认1%滑点 165 | ``` 166 | **作用**:另一种根据当前价格计算的默认滑点。相比固定值0.1,这是一个更温和的默认值,用于某些特定的错误处理情况。同样没有从配置中读取。这个百分比影响系统对交易成本的估计,从而影响交易决策。 167 | 168 | ### 5. 滑点最小/最大限制值 (0.01和0.5) 169 | ```python 170 | slippage = max(0.01, min(0.5, slippage)) 171 | ``` 172 | **作用**:对计算出的滑点值进行限制,使其不小于0.01%(最小值),也不大于0.5%(最大值)。这些限制值是直接硬编码的,没有配置选项。这些限制确保滑点估计在合理范围内,防止极端值影响交易决策。 173 | 174 | ### 6. API请求空响应的默认资金费率 (0.0001) 175 | 在`backpack_funding_api.py`文件中: 176 | ```python 177 | result[symbol] = 0.0001 # 默认值 178 | ``` 179 | **作用**:当API请求资金费率失败或返回空响应时,使用0.0001作为默认资金费率。这个值直接硬编码,没有配置选项。默认资金费率会在API失败时影响套利机会的判断,过高或过低都可能导致错误的交易决策。 180 | 181 | ### 7. 市场价格默认滑动比例 (1%) 182 | ```python 183 | slippage = 0.01 * price 184 | ``` 185 | **作用**:在无法获取足够的订单簿深度数据时,使用当前价格的1%作为默认滑点估计。这个百分比是直接硬编码的,没有从配置中读取。这个默认值会影响系统对交易成本的估计准确性。 186 | 187 | ### 8. 最小订单大小验证 (0) 188 | ```python 189 | if size <= 0: 190 | ``` 191 | **作用**:验证订单大小是否大于0,这是一个硬编码的最小值检查。虽然这是一个基本的有效性检查,但最小有效订单大小可能根据交易所和币种而不同。这个检查确保不会尝试下0或负数大小的订单。 192 | 193 | ### 9. 持仓时间检查中的小数比较精度 194 | ```python 195 | if position_duration < min_position_time: 196 | ``` 197 | **作用**:在比较持仓时间时,没有考虑浮点数比较的精度问题,可能在极端情况下导致精度误差。这里隐含了一个硬编码的精度假设。这影响系统判断持仓时间是否满足最小要求的准确性。 198 | 199 | ## 完全硬编码参数修改建议 200 | 201 | 上述完全硬编码的参数应该优先考虑移到配置文件中,因为它们没有任何后备机制来从配置中读取值,只能通过修改代码来改变其行为。建议在配置文件中添加以下部分: 202 | 203 | ```yaml 204 | # 系统参数配置 205 | system: 206 | api_retry_delay: 0.5 # API请求失败后的重试延迟(秒) 207 | order_price_adjusters: 208 | hyperliquid: 209 | buy_multiplier: 1.005 # 买入价格上调5% 210 | sell_multiplier: 0.995 # 卖出价格下调5% 211 | slippage_defaults: 212 | fallback_fixed: 0.1 # 固定滑点默认值(10%) 213 | fallback_percent: 0.01 # 百分比滑点默认值(1%) 214 | min_limit: 0.01 # 滑点最小限制(0.01%) 215 | max_limit: 0.5 # 滑点最大限制(0.5%) 216 | api_fallbacks: 217 | default_funding_rate: 0.0001 # API请求失败时的默认资金费率 218 | validation: 219 | float_comparison_epsilon: 0.000001 # 浮点数比较精度 220 | ``` 221 | 222 | 这样,即使是这些完全硬编码的参数也可以通过配置文件来调整,而不需要修改代码,大大提高了系统的灵活性和可维护性。 -------------------------------------------------------------------------------- /funding_arbitrage_bot/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 主模块 5 | 6 | 资金费率套利机器人的主入口点 7 | """ 8 | 9 | import os 10 | import sys 11 | import logging 12 | import argparse 13 | import asyncio 14 | from typing import Dict, Any 15 | import yaml 16 | 17 | # 添加导入异常处理 18 | try: 19 | # 首先尝试从包内导入 20 | from funding_arbitrage_bot.exchanges.backpack_api import BackpackAPI 21 | from funding_arbitrage_bot.exchanges.hyperliquid_api import HyperliquidAPI 22 | from funding_arbitrage_bot.core.arbitrage_engine import ArbitrageEngine 23 | from funding_arbitrage_bot.utils.helpers import load_config 24 | from funding_arbitrage_bot.utils.logger import setup_logger 25 | except ImportError: 26 | try: 27 | # 如果从包内导入失败,尝试相对导入 28 | from exchanges.backpack_api import BackpackAPI 29 | from exchanges.hyperliquid_api import HyperliquidAPI 30 | from core.arbitrage_engine import ArbitrageEngine 31 | from utils.helpers import load_config 32 | from utils.logger import setup_logger 33 | except ImportError: 34 | # 如果相对导入也失败,可能需要调整Python路径 35 | current_dir = os.path.dirname(os.path.abspath(__file__)) 36 | parent_dir = os.path.dirname(current_dir) 37 | if parent_dir not in sys.path: 38 | sys.path.insert(0, parent_dir) 39 | 40 | # 再次尝试相对导入 41 | from exchanges.backpack_api import BackpackAPI 42 | from exchanges.hyperliquid_api import HyperliquidAPI 43 | from core.arbitrage_engine import ArbitrageEngine 44 | from utils.helpers import load_config 45 | from utils.logger import setup_logger 46 | 47 | async def run_bot(config: Dict[str, Any], test_mode: bool = False): 48 | """ 49 | 运行套利机器人 50 | 51 | Args: 52 | config: 配置字典,已加载的配置内容 53 | test_mode: 如果为True,只测试API连接而不运行完整的套利策略 54 | """ 55 | # 设置日志 56 | log_config = config.get("logging", {}) 57 | 58 | # 使用新的日志配置函数 59 | logger = setup_logger(log_config, "funding_arbitrage") 60 | 61 | # 静音一些噪声太大的日志记录器 62 | for noisy_logger in ["websockets", "websockets.client", "websockets.server", "websockets.protocol"]: 63 | logging.getLogger(noisy_logger).setLevel(logging.WARNING) 64 | 65 | # 初始化交易所API 66 | exchange_config = config.get("exchanges", {}) 67 | 68 | # 初始化Backpack API 69 | print("正在初始化Backpack API...") 70 | bp_config = exchange_config.get("backpack", {}) 71 | backpack_api = BackpackAPI( 72 | api_key=bp_config.get("api_key", ""), 73 | api_secret=bp_config.get("api_secret", ""), 74 | logger=logger, 75 | config=config 76 | ) 77 | print("Backpack API初始化完成") 78 | 79 | # 初始化Hyperliquid API 80 | print("正在初始化Hyperliquid API...") 81 | hl_config = exchange_config.get("hyperliquid", {}) 82 | hyperliquid_api = HyperliquidAPI( 83 | api_key=hl_config.get("api_key", ""), 84 | api_secret=hl_config.get("api_secret", ""), 85 | logger=logger, 86 | config=config 87 | ) 88 | print("Hyperliquid API初始化完成") 89 | 90 | # 创建套利引擎实例 - 传递日志配置 91 | arbitrage_engine = ArbitrageEngine( 92 | config=config, 93 | backpack_api=backpack_api, 94 | hyperliquid_api=hyperliquid_api, 95 | logger=logger 96 | ) 97 | 98 | # 测试模式:仅测试API连接 99 | if test_mode: 100 | print("正在测试交易所API连接...") 101 | 102 | try: 103 | # 测试Backpack API 104 | print("测试Backpack API...") 105 | bp_positions = await backpack_api.get_positions() 106 | print(f"Backpack持仓信息: {bp_positions}") 107 | 108 | # 测试Hyperliquid API 109 | print("测试Hyperliquid API...") 110 | hl_positions = await hyperliquid_api.get_positions() 111 | print(f"Hyperliquid持仓: {hl_positions}") 112 | 113 | print("API测试成功!") 114 | return 115 | 116 | except Exception as e: 117 | print(f"API测试失败: {e}") 118 | logger.error(f"API测试失败: {e}") 119 | raise 120 | 121 | # 创建套利引擎 122 | print("正在创建套利引擎...") 123 | 124 | # 确保配置包含所有必要的键 125 | print(f"配置类型: {type(config)}") 126 | 127 | # 检查配置中是否包含策略配置 128 | if "strategy" not in config: 129 | raise ValueError("配置中缺少'strategy'部分") 130 | 131 | # 打印策略配置以便于调试 132 | strategy_config = config.get("strategy", {}) 133 | print(f"策略配置: {strategy_config}") 134 | 135 | # 检查open_conditions是否存在 136 | if "open_conditions" not in strategy_config: 137 | # 向下兼容:创建默认的open_conditions结构 138 | strategy_config["open_conditions"] = { 139 | "condition_type": "funding_only", 140 | "min_funding_diff": strategy_config.get("min_funding_diff", 0.00001), 141 | "min_price_diff_percent": strategy_config.get("min_price_diff_percent", 0.2), 142 | "max_price_diff_percent": 1.0 143 | } 144 | logger.warning("配置文件使用旧格式,已自动转换为新格式") 145 | 146 | # 检查close_conditions是否存在 147 | if "close_conditions" not in strategy_config: 148 | # 创建默认的close_conditions结构 149 | strategy_config["close_conditions"] = { 150 | "condition_type": "any", 151 | "funding_diff_sign_change": True, 152 | "min_funding_diff": strategy_config["open_conditions"]["min_funding_diff"] / 2, 153 | "min_profit_percent": 0.1, 154 | "max_loss_percent": 0.3, 155 | "max_position_time": 28800 # 默认8小时 156 | } 157 | logger.warning("配置文件缺少close_conditions部分,已使用默认值") 158 | 159 | # 启动套利引擎 160 | print("正在启动套利引擎...") 161 | await arbitrage_engine.start() 162 | 163 | # 清理资源 164 | print("正在清理资源...") 165 | await backpack_api.close() 166 | await hyperliquid_api.close() 167 | 168 | print("套利机器人已停止") 169 | 170 | async def main(test: bool = False, config_path: str = None): 171 | """ 172 | 兼容旧版本的主函数入口点 173 | 174 | Args: 175 | test: 是否为测试模式 176 | config_path: 配置文件路径 177 | """ 178 | if config_path: 179 | config = load_config(config_path) 180 | await run_bot(config, test_mode=test) 181 | else: 182 | parser = argparse.ArgumentParser(description="运行资金费率套利机器人") 183 | parser.add_argument("--test", action="store_true", help="仅测试API连接") 184 | parser.add_argument("--config", type=str, help="配置文件路径") 185 | args = parser.parse_args() 186 | 187 | config_path = args.config or "funding_arbitrage_bot/config.yaml" 188 | config = load_config(config_path) 189 | await run_bot(config, test_mode=args.test) 190 | 191 | def load_config(config_path: str = None) -> Dict[str, Any]: 192 | """ 193 | 加载配置文件 194 | 195 | Args: 196 | config_path: 配置文件路径 197 | 198 | Returns: 199 | 配置字典 200 | """ 201 | try: 202 | # 如果没有指定配置路径,使用默认路径 203 | if not config_path: 204 | # 检查用户主目录 205 | home_config = os.path.expanduser("~/config.yaml") 206 | if os.path.exists(home_config): 207 | config_path = home_config 208 | else: 209 | # 使用默认配置路径 210 | config_path = os.path.join(os.path.dirname(__file__), "config.yaml") 211 | 212 | print(f"加载配置文件: {config_path}") 213 | 214 | with open(config_path, "r", encoding="utf-8") as f: 215 | config = yaml.safe_load(f) 216 | 217 | # 检查TRACE日志级别设置 218 | if config.get("logging", {}).get("level", "").upper() == "TRACE": 219 | print("警告: 当前版本不支持TRACE日志级别,将使用DEBUG级别替代") 220 | config["logging"]["level"] = "DEBUG" 221 | 222 | return config 223 | except Exception as e: 224 | print(f"加载配置文件失败: {e}") 225 | return None 226 | 227 | if __name__ == "__main__": 228 | # Windows系统需要设置事件循环策略 229 | if sys.platform == 'win32': 230 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 231 | 232 | # 运行主函数 233 | asyncio.run(main()) -------------------------------------------------------------------------------- /funding_arbitrage_bot/README.md: -------------------------------------------------------------------------------- 1 | # 资金费率套利机器人 2 | 3 | 自动监控Backpack和Hyperliquid交易所的资金费率差异,执行跨交易所资金费率套利策略。 4 | 5 | ## 功能特点 6 | 7 | - 实时监控多个交易对的资金费率和价格 8 | - 自动执行资金费率套利策略:在资金费率高的交易所做多,在资金费率低的交易所做空 9 | - 当资金费率差异符号反转时自动平仓获利 10 | - 支持自定义开仓阈值和仓位大小 11 | - 完整的日志记录和仓位管理 12 | - 异常处理和自动重连机制 13 | 14 | ## 安装 15 | 16 | 1. 克隆仓库 17 | ```bash 18 | git clone 19 | cd funding-arbitrage-bot 20 | ``` 21 | 22 | 2. 安装依赖 23 | ```bash 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | 3. 配置交易所API密钥 28 | 编辑`config.yaml`文件,设置你的API密钥: 29 | ```yaml 30 | exchanges: 31 | backpack: 32 | api_key: "YOUR_BACKPACK_API_KEY" 33 | api_secret: "YOUR_BACKPACK_API_SECRET" 34 | hyperliquid: 35 | api_key: "YOUR_HYPERLIQUID_API_KEY_OR_ADDRESS" 36 | api_secret: "YOUR_HYPERLIQUID_API_SECRET_OR_PRIVATE_KEY" 37 | ``` 38 | 39 | ## 使用方法 40 | 41 | 1. 启动机器人 42 | ```bash 43 | python main.py 44 | ``` 45 | 46 | 2. 使用自定义配置文件 47 | ```bash 48 | python main.py --config your_config.yaml 49 | ``` 50 | 51 | 3. 模拟运行模式(不实际下单) 52 | ```bash 53 | python main.py --dry-run 54 | ``` 55 | 56 | ## 配置选项 57 | 58 | 在`config.yaml`中配置策略参数: 59 | 60 | ```yaml 61 | strategy: 62 | symbols: ["BTC", "ETH", "SOL"] # 监控的基础币种列表 63 | max_positions_count: 5 # 全局持仓数量限制,最多同时持有的不同币种数量 64 | min_funding_diff: 0.000001 # 最小资金费率差阈值 65 | min_price_diff_percent: 0.000001 # 最小价格差阈值(百分比) 66 | position_sizes: # 每个币种的单次开仓数量 67 | BTC: 0.001 # BTC开仓数量 68 | ETH: 0.01 # ETH开仓数量 69 | SOL: 0.1 # SOL开仓数量 70 | max_total_position_usd: 150 # 所有币种的总开仓金额上限(美元) 71 | check_interval: 5 # 检查套利机会的时间间隔 (秒) 72 | funding_update_interval: 60 # 更新资金费率的时间间隔 (秒) 73 | 74 | # 交易对配置 75 | trading_pairs: 76 | - symbol: "BTC" 77 | min_volume: 0.001 78 | price_precision: 1 # BTC价格精度:1位小数 79 | size_precision: 3 80 | max_position_size: 0.001 # BTC最大持仓数量 81 | - symbol: "ETH" 82 | min_volume: 0.01 83 | price_precision: 2 84 | size_precision: 2 # ETH数量精度:2位小数 85 | max_position_size: 0.01 # ETH最大持仓数量 86 | - symbol: "SOL" 87 | min_volume: 0.1 88 | price_precision: 3 89 | size_precision: 2 # SOL数量精度:2位小数 90 | max_position_size: 0.1 # SOL最大持仓数量 91 | ``` 92 | 93 | ## 注意事项 94 | 95 | - 请使用小额资金进行测试 96 | - 确保网络稳定,因为套利策略需要同时在两个交易所下单 97 | - 请了解资金费率套利的风险,包括但不限于价格波动风险、流动性风险等 98 | 99 | ## 风险控制 100 | 101 | 机器人内置了多项风险控制机制,帮助您限制潜在的风险: 102 | 103 | 1. **全局持仓数量限制**:通过`strategy.max_positions_count`配置项限制同时最多持有的不同币种数量。例如,设置为5时,即使更多币种满足开仓条件,也最多只持有5个不同币种的仓位。这有助于: 104 | - 防止资金过度分散 105 | - 控制整体风险敞口 106 | - 在市场波动时限制潜在的亏损 107 | 108 | 2. **单币种持仓限制**:每个币种都有`max_position_size`设置,限制单一币种的最大持仓量。 109 | 110 | 3. **价格差异过滤**:通过`min_price_diff_percent`和`max_price_diff_percent`过滤异常的价格数据,避免在错误数据基础上开仓。 111 | 112 | 合理设置这些参数可以有效控制套利策略的风险。 113 | 114 | ## 项目结构 115 | 116 | ``` 117 | funding_arbitrage_bot/ 118 | ├── main.py # 主程序入口 119 | ├── config.yaml # 配置文件 120 | ├── requirements.txt # Python依赖 121 | ├── README.md # 项目说明文档 122 | ├── exchanges/ # 交易所API封装 123 | │ ├── backpack_api.py # Backpack交易所API 124 | │ └── hyperliquid_api.py # Hyperliquid交易所API 125 | ├── core/ # 核心逻辑 126 | │ ├── arbitrage_engine.py # 套利引擎 127 | │ ├── data_manager.py # 数据管理 128 | │ └── position_manager.py # 仓位管理 129 | └── utils/ # 工具函数 130 | ├── logger.py # 日志工具 131 | └── helpers.py # 辅助函数 132 | ``` 133 | 134 | # Hyperliquid API 修复说明 135 | 136 | ## 问题 137 | 138 | 在使用自建的Hyperliquid SDK时,发现以下问题: 139 | 140 | 1. API请求格式不正确 141 | 2. 身份验证签名方式不正确 142 | 3. 参数类型问题(浮点数vs字符串) 143 | 4. 资金费率获取失败,显示为0 144 | 145 | ## 解决方案 146 | 147 | 我们采用了以下解决方案: 148 | 149 | 1. 使用官方SDK代替自己实现的SDK 150 | 2. 修改了API代码,使其正确调用官方SDK 151 | 3. 确保所有参数使用正确的格式和类型 152 | 4. 直接使用REST API获取资金费率数据 153 | 154 | ## 具体修改 155 | 156 | 1. 添加了对官方SDK的支持: 157 | ```python 158 | from hyperliquid.exchange import Exchange 159 | from eth_account import Account 160 | 161 | # 创建钱包对象 162 | wallet = Account.from_key(private_key) 163 | 164 | # 初始化Exchange 165 | exchange = Exchange(wallet=wallet) 166 | 167 | # 下单 168 | order_response = exchange.order( 169 | coin=symbol, 170 | is_buy=is_buy, 171 | sz=size, 172 | limit_px=price, 173 | order_type="Limit" # 限价单 174 | ) 175 | ``` 176 | 177 | 2. 修复了REST API的使用方式: 178 | ```python 179 | # 获取持仓信息 180 | url = f"{self.base_url}/info" 181 | payload = { 182 | "type": "clearinghouseState", 183 | "user": self.hyperliquid_address 184 | } 185 | response = await self.http_client.post(url, json=payload) 186 | ``` 187 | 188 | 3. 修复资金费率获取: 189 | ```python 190 | # 获取资金费率 191 | url = f"{self.base_url}/info" 192 | payload = {"type": "metaAndAssetCtxs"} 193 | 194 | response = await self.http_client.post(url, json=payload) 195 | data = response.json() 196 | 197 | # 解析资金费率 198 | if isinstance(data, list) and len(data) >= 2: 199 | universe_data = data[0] 200 | asset_ctxs = data[1] 201 | 202 | # 查找特定币种 203 | for i, coin_data in enumerate(universe_data.get("universe", [])): 204 | if coin_data.get("name") == symbol: 205 | coin_ctx = asset_ctxs[i] 206 | if "funding" in coin_ctx: 207 | funding_rate = float(coin_ctx["funding"]) 208 | return funding_rate 209 | ``` 210 | 211 | 4. 创建了测试脚本验证功能: 212 | - `test_order.py`: 使用官方SDK直接下单测试 213 | - `test_hl_api.py`: 通过自己的API接口下单测试 214 | - `test_funding_rate.py`: 验证资金费率获取功能 215 | 216 | ## 注意事项 217 | 218 | 1. Hyperliquid只支持限价单,不支持市价单 219 | 2. 下单时需要注意价格精度和数量精度 220 | 3. 钱包地址必须与私钥匹配 221 | 4. 为了避免意外成交,测试时将价格设置为市场价格的95%(买入)或105%(卖出) 222 | 5. 资金费率通过直接调用REST API获取,避免SDK中可能存在的问题 223 | 224 | ## API使用方式 225 | 226 | ```python 227 | # 初始化API 228 | api = HyperliquidAPI( 229 | api_key=wallet_address, # 钱包地址 230 | api_secret=private_key, # 钱包私钥 231 | logger=logger, 232 | config=config 233 | ) 234 | 235 | # 下限价单 236 | order_result = await api.place_order( 237 | symbol="SOL", 238 | side="BUY", 239 | size=0.01, 240 | price=current_price*0.95, # 买入价格比市场价低5% 241 | order_type="LIMIT" 242 | ) 243 | 244 | # 获取持仓 245 | positions = await api.get_positions() 246 | 247 | # 获取资金费率 248 | funding_rate = await api.get_funding_rate("BTC") 249 | 250 | # 平仓 251 | await api.close_position(symbol="SOL") 252 | ``` 253 | 254 | # 资金费率获取修复总结 255 | 256 | 我们已经成功修复了Hyperliquid资金费率获取功能,现在可以正确获取各个交易对的资金费率数据。 257 | 258 | ## 问题原因 259 | 260 | 1. 原代码尝试使用官方SDK的`meta_and_asset_ctx`方法获取资金费率,但该方法在当前版本的SDK中不存在 261 | 2. REST API请求格式不正确,导致无法获取资金费率数据 262 | 3. 对资金费率数据的解析逻辑存在问题 263 | 264 | ## 解决方案 265 | 266 | 1. 移除对不存在的SDK方法的调用 267 | 2. 采用直接的REST API请求获取资金费率数据 268 | 3. 正确解析API响应,提取资金费率信息 269 | 4. 增加详细的错误处理和日志记录 270 | 271 | ## 修复验证 272 | 273 | 我们创建了专门的测试脚本(`test_funding_rate.py`和`test_detailed_funding.py`)来验证资金费率获取功能: 274 | 275 | 1. 直接使用REST API获取资金费率 276 | 2. 通过修改后的`HyperliquidAPI`类获取资金费率 277 | 3. 对比两种方法获取的结果,确保一致性 278 | 279 | 测试结果显示,修复后的代码可以正确获取Hyperliquid的资金费率数据,不再出现获取失败或返回0的情况。现在资金费率可以用于套利策略的计算。 280 | 281 | ## 示例资金费率数据 282 | 283 | 以下是部分交易对当前的资金费率数据(测试时间:2025-04-06): 284 | 285 | | 币种 | 每小时费率 | 8小时费率 | 24小时费率 | 286 | |------|-----------|-----------|-----------| 287 | | BTC | 0.001250% | 0.010000% | 0.030000% | 288 | | ETH | -0.000611% | -0.004888% | -0.014664% | 289 | | SOL | -0.002420% | -0.019357% | -0.058072% | 290 | | AVAX | -0.004687% | -0.037495% | -0.112484% | 291 | | LINK | -0.001299% | -0.010394% | -0.031181% | 292 | 293 | 请注意,资金费率为正值表示做多需要支付资金费用,做空可以获得资金费用;为负值表示做空需要支付费用,做多可以获得资金费用。套利策略应利用这些差异进行交易。 294 | 295 | # 盈亏状态计算修复 (2025-04-11) 296 | 297 | ## 问题描述 298 | 299 | 终端界面上持仓的盈亏状态始终显示为"亏损",即使资金费率数据显示应该是"盈利"状态。日志中显示盈亏计算时使用的资金费率数值均为0,导致计算结果总是为0,被判断为"亏损"状态。 300 | 301 | ## 问题原因 302 | 303 | 1. 在向 `check_position_profit_status` 函数传递参数时,使用了可能已经过时的资金费率数据 304 | 2. 资金费率数据没有从最新的 market_data 中获取,而是使用了函数调用时的参数值 305 | 3. 部分环节中存在空值处理不当,导致 '<' 比较操作时出现 NoneType 错误 306 | 307 | ## 修复方案 308 | 309 | 1. 在 `_update_position_direction_info` 方法中,直接从 market_data 中获取最新的资金费率数据进行盈亏计算 310 | 2. 添加防御性编程代码,对可能为 None 的值进行检查和处理,避免 NoneType 错误 311 | 3. 当资金费率接近0时,将盈亏状态显示为"持平"而不是"亏损",更准确反映实际情况 312 | 313 | ## 修复效果 314 | 315 | 1. 终端界面现在能够正确显示持仓的盈亏状态("盈利"、"亏损"或"持平") 316 | 2. 日志中记录的资金费率盈亏计算使用了准确的最新资金费率数据 317 | 3. 程序运行更加稳定,不再出现NoneType比较错误 318 | 319 | 这一修复确保了盈亏状态的计算与显示能够准确反映实际的资金费率情况,帮助用户做出更准确的交易决策。 -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/hyperliquid_sdk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hyperliquid SDK包装器 3 | 4 | 提供与Hyperliquid API交互的简化方法 5 | """ 6 | 7 | import json 8 | import httpx 9 | import time 10 | from typing import Dict, List, Tuple, Any, Optional 11 | from eth_account import Account 12 | from eth_account.messages import encode_defunct 13 | 14 | class HyperliquidBase: 15 | """Hyperliquid基础连接类""" 16 | 17 | def __init__(self): 18 | self.base_url = "https://api.hyperliquid.xyz" 19 | self.http_client = httpx.AsyncClient(timeout=10.0) 20 | 21 | async def close(self): 22 | """关闭HTTP连接""" 23 | await self.http_client.aclose() 24 | 25 | async def request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict: 26 | """ 27 | 发送API请求 28 | 29 | Args: 30 | endpoint: API端点 31 | method: HTTP方法 32 | data: 请求数据 33 | 34 | Returns: 35 | 响应数据 36 | """ 37 | url = f"{self.base_url}/{endpoint}" 38 | print(f"HTTP Request: {method} {url} with data: {json.dumps(data, default=str)}") 39 | 40 | if method.upper() == "GET": 41 | response = await self.http_client.get(url, json=data) 42 | else: 43 | response = await self.http_client.post(url, json=data) 44 | 45 | print(f"HTTP Response: {response.status_code} - {response.text[:200]}...") 46 | 47 | if response.status_code != 200: 48 | raise Exception(f"API请求失败,状态码: {response.status_code}, 响应: {response.text}") 49 | 50 | return response.json() 51 | 52 | class HyperliquidInfo: 53 | """Hyperliquid市场信息类""" 54 | 55 | def __init__(self, connection: HyperliquidBase): 56 | self.connection = connection 57 | 58 | async def meta_and_asset_ctxs(self) -> Tuple[Dict, List]: 59 | """ 60 | 获取市场元数据和资产上下文 61 | 62 | Returns: 63 | 元数据和资产上下文的元组 64 | """ 65 | response = await self.connection.request("info", method="POST", data={"type": "metaAndAssetCtxs"}) 66 | 67 | # 处理不同格式的响应 68 | universe = [] 69 | asset_contexts = [] 70 | 71 | # 检查数据是否为字典类型,有universe和assetCtxs键 72 | if isinstance(response, dict) and "universe" in response and "assetCtxs" in response: 73 | universe = response["universe"] 74 | asset_contexts = response["assetCtxs"] 75 | # 检查数据是否为列表类型,且有两个元素 76 | elif isinstance(response, list) and len(response) >= 2: 77 | # 假设第一个元素是universe,第二个元素是assetCtxs 78 | if isinstance(response[0], list): 79 | universe = response[0] 80 | if len(response) > 1 and isinstance(response[1], list): 81 | asset_contexts = response[1] 82 | 83 | meta_data = { 84 | "universe": universe 85 | } 86 | 87 | return meta_data, asset_contexts 88 | 89 | class HyperliquidMarketData: 90 | """Hyperliquid市场数据类""" 91 | 92 | def __init__(self, connection: HyperliquidBase): 93 | self.connection = connection 94 | 95 | async def get_funding_rate(self, symbol: str) -> Optional[float]: 96 | """ 97 | 获取资金费率 98 | 99 | Args: 100 | symbol: 币种 101 | 102 | Returns: 103 | 资金费率,如果无法获取则返回None 104 | """ 105 | try: 106 | response = await self.connection.request( 107 | "info", method="POST", data={"type": "metaAndAssetCtxs"} 108 | ) 109 | 110 | # 处理不同格式的响应 111 | universe = [] 112 | asset_contexts = [] 113 | 114 | # 检查数据是否为字典类型,有universe和assetCtxs键 115 | if isinstance(response, dict) and "universe" in response and "assetCtxs" in response: 116 | universe = response["universe"] 117 | asset_contexts = response["assetCtxs"] 118 | # 检查数据是否为列表类型,且有两个元素 119 | elif isinstance(response, list) and len(response) >= 2: 120 | # 假设第一个元素是universe,第二个元素是assetCtxs 121 | if isinstance(response[0], list): 122 | universe = response[0] 123 | if len(response) > 1 and isinstance(response[1], list): 124 | asset_contexts = response[1] 125 | 126 | # 查找指定币种的索引 127 | idx = None 128 | for i, coin in enumerate(universe): 129 | coin_name = None 130 | if isinstance(coin, dict) and "name" in coin: 131 | coin_name = coin.get("name") 132 | elif isinstance(coin, str): 133 | coin_name = coin 134 | 135 | if coin_name == symbol: 136 | idx = i 137 | break 138 | 139 | if idx is not None and idx < len(asset_contexts): 140 | asset_ctx = asset_contexts[idx] 141 | if isinstance(asset_ctx, dict) and "funding" in asset_ctx: 142 | return float(asset_ctx["funding"]) 143 | 144 | return None 145 | except Exception: 146 | return None 147 | 148 | class HyperliquidUser: 149 | """Hyperliquid用户类""" 150 | 151 | def __init__(self, connection: HyperliquidBase, wallet_address: str, wallet_secret: str): 152 | self.connection = connection 153 | self.wallet_address = wallet_address 154 | self.wallet_secret = wallet_secret 155 | # 创建钱包对象 156 | self.wallet = Account.from_key(wallet_secret) 157 | 158 | def sign_request(self, action_type: str, data: Dict) -> Dict: 159 | """ 160 | 签名请求 161 | 162 | Args: 163 | action_type: 操作类型 164 | data: 请求数据 165 | 166 | Returns: 167 | 完整的签名请求 168 | """ 169 | # 创建时间戳 170 | ts = int(time.time()) 171 | 172 | # 创建待签名的消息(根据Hyperliquid文档格式) 173 | message = f"hyperliquid\n{action_type}\n{self.wallet_address.lower()}\n{ts}" 174 | 175 | # 编码消息 176 | message_hash = encode_defunct(text=message) 177 | 178 | # 签名 179 | signed_message = self.wallet.sign_message(message_hash) 180 | 181 | # 构建完整请求 182 | payload = { 183 | "action": { 184 | "type": action_type, 185 | "data": data 186 | }, 187 | "signature": { 188 | "r": signed_message.r, 189 | "s": signed_message.s, 190 | "v": signed_message.v 191 | }, 192 | "nonce": ts, 193 | "agent": "sdk", 194 | "wallet": self.wallet_address.lower() 195 | } 196 | 197 | return payload 198 | 199 | class HyperliquidExchange: 200 | """Hyperliquid交易类""" 201 | 202 | def __init__(self, connection: HyperliquidBase, user: HyperliquidUser): 203 | self.connection = connection 204 | self.user = user 205 | 206 | async def order(self, name: str, is_buy: bool, sz: float, limit_px: float, order_type): 207 | """ 208 | 下单方法 209 | 210 | 参数: 211 | name: 币种名称,如"BTC" 212 | is_buy: 是否买入 213 | sz: 数量 214 | limit_px: 限价 215 | order_type: 订单类型参数,可以是字符串("Limit")或字典格式({"limit": {"tif": "Gtc"}})或OrderType类 216 | 217 | 返回: 218 | 订单结果 219 | """ 220 | # 处理order_type参数,支持多种格式 221 | processed_order_type = order_type 222 | 223 | # 如果是字符串类型,转换为适当的格式 224 | if isinstance(order_type, str): 225 | if order_type.lower() == "limit": 226 | processed_order_type = {"limit": {"tif": "Gtc"}} 227 | 228 | # 创建订单数据 229 | order_data = { 230 | "coin": name, 231 | "is_buy": is_buy, 232 | "sz": sz, 233 | "limit_px": limit_px, 234 | "order_type": processed_order_type, 235 | "reduce_only": False, 236 | "cloid": int(time.time() * 1000) # 客户端订单ID 237 | } 238 | 239 | # 签名请求 240 | signed_request = self.user.sign_request("order", order_data) 241 | 242 | # 发送请求 243 | return await self.connection.request("exchange", method="POST", data=signed_request) -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/diagnostics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Hyperliquid API诊断工具 5 | 6 | 用于测试Hyperliquid API连接和资金费率获取,帮助用户诊断显示"na"的问题 7 | """ 8 | 9 | import sys 10 | import asyncio 11 | import argparse 12 | import logging 13 | import yaml 14 | import json 15 | import traceback 16 | from pathlib import Path 17 | 18 | # 设置日志格式 19 | logging.basicConfig( 20 | level=logging.DEBUG, 21 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 22 | handlers=[ 23 | logging.StreamHandler(sys.stdout) 24 | ] 25 | ) 26 | 27 | logger = logging.getLogger('hl_diagnostics') 28 | 29 | async def test_hyperliquid_api(config_path: str, symbol: str = "BTC"): 30 | """ 31 | 测试Hyperliquid API连接和资金费率获取 32 | 33 | Args: 34 | config_path: 配置文件路径 35 | symbol: 要测试的币种 36 | """ 37 | # 打印诊断头部 38 | logger.info("=" * 50) 39 | logger.info("Hyperliquid API连接和资金费率获取诊断工具") 40 | logger.info("=" * 50) 41 | 42 | # 加载配置 43 | try: 44 | with open(config_path, 'r') as f: 45 | config = yaml.safe_load(f) 46 | logger.info(f"已成功加载配置文件: {config_path}") 47 | except Exception as e: 48 | logger.error(f"加载配置文件失败: {e}") 49 | return 50 | 51 | # 提取Hyperliquid API凭证 52 | hl_api_key = config.get('exchanges', {}).get('hyperliquid', {}).get('api_key') 53 | hl_api_secret = config.get('exchanges', {}).get('hyperliquid', {}).get('api_secret') 54 | 55 | if not hl_api_key or not hl_api_secret: 56 | logger.error("配置文件中未找到Hyperliquid API凭证") 57 | return 58 | 59 | logger.info(f"已找到Hyperliquid API凭证,钱包地址前缀: {hl_api_key[:10]}...") 60 | 61 | # 导入Hyperliquid API 62 | try: 63 | from funding_arbitrage_bot.exchanges.hyperliquid_api import HyperliquidAPI 64 | logger.info("已成功导入HyperliquidAPI类") 65 | except ImportError: 66 | logger.error("导入HyperliquidAPI类失败,请确保您在正确的目录中运行此脚本") 67 | import traceback 68 | logger.debug(f"导入错误详情: {traceback.format_exc()}") 69 | return 70 | 71 | # 创建API实例 72 | try: 73 | api = HyperliquidAPI( 74 | api_key=hl_api_key, 75 | api_secret=hl_api_secret, 76 | logger=logger, 77 | config=config 78 | ) 79 | logger.info("已成功创建HyperliquidAPI实例") 80 | except Exception as e: 81 | logger.error(f"创建HyperliquidAPI实例失败: {e}") 82 | return 83 | 84 | # 测试直接REST API调用 85 | try: 86 | import httpx 87 | import traceback 88 | client = httpx.AsyncClient(timeout=10.0) 89 | 90 | logger.info("尝试直接调用Hyperliquid REST API获取资金费率...") 91 | url = "https://api.hyperliquid.xyz/info" 92 | payload = {"type": "metaAndAssetCtxs"} 93 | 94 | response = await client.post(url, json=payload) 95 | 96 | if response.status_code == 200: 97 | logger.info(f"REST API请求成功,状态码: {response.status_code}") 98 | 99 | # 解析响应并查找特定币种 100 | data = response.json() 101 | 102 | # 记录响应格式 103 | logger.debug(f"响应数据类型: {type(data)}") 104 | if isinstance(data, dict): 105 | logger.debug(f"响应数据键: {data.keys()}") 106 | elif isinstance(data, list): 107 | logger.debug(f"响应数据是列表,长度: {len(data)}") 108 | if len(data) > 0: 109 | logger.debug(f"第一项类型: {type(data[0])}") 110 | if isinstance(data[0], dict): 111 | logger.debug(f"第一项键: {data[0].keys()}") 112 | 113 | # 尝试以不同格式解析 114 | universe = [] 115 | asset_ctxs = [] 116 | 117 | # 检查数据是否为字典类型,有universe和assetCtxs键 118 | if isinstance(data, dict) and "universe" in data and "assetCtxs" in data: 119 | universe = data["universe"] 120 | asset_ctxs = data["assetCtxs"] 121 | logger.info("使用字典格式解析API响应") 122 | # 检查数据是否为列表类型,且有两个元素 123 | elif isinstance(data, list) and len(data) >= 2: 124 | # 假设第一个元素是universe,第二个元素是assetCtxs 125 | if isinstance(data[0], list): 126 | universe = data[0] 127 | if len(data) > 1 and isinstance(data[1], list): 128 | asset_ctxs = data[1] 129 | logger.info("使用列表格式解析API响应") 130 | 131 | logger.info(f"获取到{len(universe)}个币种信息") 132 | 133 | # 查找特定币种 134 | symbol_idx = None 135 | for i, coin in enumerate(universe): 136 | coin_name = None 137 | if isinstance(coin, dict) and "name" in coin: 138 | coin_name = coin.get("name") 139 | elif isinstance(coin, str): 140 | coin_name = coin 141 | 142 | if coin_name == symbol: 143 | symbol_idx = i 144 | break 145 | 146 | if symbol_idx is not None and symbol_idx < len(asset_ctxs): 147 | asset_ctx = asset_ctxs[symbol_idx] 148 | logger.info(f"找到{symbol}的资产上下文: {json.dumps(asset_ctx, indent=2)}") 149 | 150 | # 检查是否存在资金费率字段 151 | if isinstance(asset_ctx, dict) and "funding" in asset_ctx: 152 | logger.info(f"{symbol}的资金费率: {asset_ctx['funding']}") 153 | else: 154 | logger.warning(f"{symbol}的资产上下文中不存在资金费率字段") 155 | else: 156 | logger.warning(f"在API响应中未找到{symbol}的资产上下文") 157 | 158 | # 打印所有币种名称以供参考 159 | all_coins = [] 160 | for coin in universe: 161 | if isinstance(coin, dict) and "name" in coin: 162 | all_coins.append(coin.get("name")) 163 | elif isinstance(coin, str): 164 | all_coins.append(coin) 165 | 166 | logger.info(f"所有可用币种: {', '.join(all_coins)}") 167 | else: 168 | logger.error(f"REST API请求失败,状态码: {response.status_code}") 169 | 170 | await client.aclose() 171 | except Exception as e: 172 | logger.error(f"直接调用REST API失败: {e}") 173 | import traceback 174 | logger.debug(f"错误详情: {traceback.format_exc()}") 175 | 176 | # 测试SDK方法 177 | try: 178 | logger.info(f"尝试通过SDK获取{symbol}的资金费率...") 179 | 180 | funding_rate = await api.get_funding_rate(symbol) 181 | 182 | if funding_rate is not None: 183 | logger.info(f"成功获取到{symbol}的资金费率: {funding_rate}") 184 | else: 185 | logger.warning(f"获取{symbol}的资金费率失败,返回None") 186 | except Exception as e: 187 | logger.error(f"通过SDK获取资金费率失败: {e}") 188 | logger.debug(f"错误详情: {traceback.format_exc()}") 189 | 190 | # 测试获取价格 191 | try: 192 | logger.info(f"尝试获取{symbol}的价格...") 193 | 194 | # 先启动WebSocket连接 195 | logger.info("启动WebSocket价格连接...") 196 | await api.start_ws_price_stream() 197 | 198 | # 等待一段时间让WebSocket连接建立并接收数据 199 | logger.info("等待5秒以接收价格数据...") 200 | await asyncio.sleep(5) 201 | 202 | # 获取价格 203 | price = await api.get_price(symbol) 204 | 205 | if price is not None: 206 | logger.info(f"成功获取到{symbol}的价格: {price}") 207 | else: 208 | logger.warning(f"获取{symbol}的价格失败,返回None") 209 | except Exception as e: 210 | logger.error(f"获取价格失败: {e}") 211 | logger.debug(f"错误详情: {traceback.format_exc()}") 212 | 213 | # 清理资源 214 | try: 215 | await api.close() 216 | except Exception as e: 217 | logger.error(f"关闭API连接失败: {e}") 218 | 219 | logger.info("=" * 50) 220 | logger.info("诊断完成") 221 | logger.info("=" * 50) 222 | 223 | def main(): 224 | """命令行入口函数""" 225 | parser = argparse.ArgumentParser(description='Hyperliquid API诊断工具') 226 | parser.add_argument('--config', type=str, default='funding_arbitrage_bot/config.yaml', 227 | help='配置文件路径') 228 | parser.add_argument('--symbol', type=str, default='BTC', 229 | help='要测试的币种') 230 | 231 | args = parser.parse_args() 232 | 233 | # 检查配置文件是否存在 234 | config_path = Path(args.config) 235 | if not config_path.exists(): 236 | print(f"错误: 配置文件 {args.config} 不存在") 237 | sys.exit(1) 238 | 239 | # 运行异步测试函数 240 | asyncio.run(test_hyperliquid_api(args.config, args.symbol)) 241 | 242 | if __name__ == '__main__': 243 | main() -------------------------------------------------------------------------------- /funding_arbitrage_bot/config.yaml: -------------------------------------------------------------------------------- 1 | # 资金费率套利机器人配置文件 2 | # 本配置文件包含程序运行所需的所有参数设置 3 | 4 | # 交易所API配置 5 | # 用于连接Backpack和Hyperliquid交易所的API密钥 6 | exchanges: 7 | backpack: 8 | api_key: "交易所API配置" # Backpack交易所的API密钥 9 | api_secret: "交易所API配置" # Backpack交易所的API密钥对应的密钥 10 | hyperliquid: 11 | # 注意:Hyperliquid使用以太坊钱包地址和私钥进行认证 12 | api_key: "交易所API配置" # 以太坊钱包地址(API钱包),用于签名认证 13 | api_secret: "交易所API配置" # 以太坊钱包私钥 14 | public_address: "交易所API配置" # 公共ETH钱包地址,用于查询账户信息和持仓 15 | 16 | # 套利策略配置 17 | # 控制套利策略的运行方式、触发条件和交易参数 18 | strategy: 19 | symbols: ["BTC", "ETH", "SOL", "AVAX", "AAVE", "ADA", "ARB", "BERA", "BNB", "DOGE", "DOT", "ENA", "FARTCOIN", "HYPE", "IP", "JUP", "KAITO", "LINK", "LTC", "ONDO", "SUI", "S", "TRUMP", "WIF", "XRP"] # 监控的基础币种列表,只有这些币种会被检查套利机会 20 | 21 | # 全局持仓数量限制 22 | max_positions_count: 5 # 最大持仓币种数量,同时最多只允许持有这么多个不同币种的仓位 23 | 24 | # 开仓条件配置------------------------------------------------------------------------------ 25 | open_conditions: 26 | # 预期收益要求参数 27 | check_expected_profit: true # 是否检查预期收益 28 | min_expected_profit: 0.0008 # 最小预期收益要求 29 | 30 | # 滑点控制参数 31 | max_slippage_percent: 0.6 # 开仓时允许的最大滑点(百分比) 32 | ignore_high_slippage: false # 是否忽略滑点限制,设为true时无论滑点多大都尝试开仓 33 | 34 | # 平仓条件配置------------------------------------------------------------------------------ 35 | close_conditions: 36 | # 预期收益要求参数 37 | check_expected_profit: true # 是否检查预期收益 38 | min_expected_profit: -0.0003 # 最小预期收益要求,低于此值时平仓 39 | 40 | # 基础时间条件 41 | min_position_time: 30 # 最短持仓时间(秒),持仓时间低于此值不会平仓 42 | 43 | # 滑点控制参数 44 | max_close_slippage_percent: 0.8 # 平仓时允许的最大滑点(百分比) 45 | ignore_close_slippage: false # 是否忽略平仓滑点限制,设为true时无论滑点多大都尝试平仓 46 | 47 | position_sizes: # 每个币种的单次开仓数量,决定了下单的规模------------------------------------------------------------------------------ 48 | BTC: 0.001 # BTC开仓数量为0.001个BTC 49 | ETH: 0.01 # ETH开仓数量为0.01个ETH 50 | SOL: 0.1 # SOL开仓数量为0.1个SOL 51 | AVAX: 2 # 添加新币种的开仓数量 52 | AAVE: 0.4 # AAVE开仓数量 53 | ADA: 80.0 # ADA开仓数量 54 | ARB: 160.0 # ARB开仓数量 55 | BERA: 9.0 # BERA开仓数量 56 | BNB: 0.1 # BNB开仓数量 57 | DOGE: 300.0 # DOGE开仓数量 58 | DOT: 13.0 # DOT开仓数量 59 | ENA: 160.0 # ENA开仓数量 60 | FARTCOIN: 110.0 # FARTCOIN开仓数量 61 | HYPE: 5.0 # HYPE开仓数量 62 | IP: 13.0 # IP开仓数量 63 | JUP: 130.0 # JUP开仓数量 64 | KAITO: 55.0 # KAITO开仓数量 65 | LINK: 4.0 # LINK开仓数量 66 | LTC: 0.6 # LTC开仓数量 67 | ONDO: 65.0 # ONDO开仓数量 68 | SUI: 22.0 # SUI开仓数量 69 | S: 100.0 # S开仓数量 70 | TRUMP: 6.0 # TRUMP开仓数量 71 | WIF: 130.0 # WIF开仓数量 72 | XRP: 25.0 # XRP开仓数量 73 | 74 | max_total_position_usd: 150 # 所有币种的总开仓金额上限(美元),暂未使用 75 | check_interval: 5 # 检查套利机会的时间间隔(秒),每隔这么多秒检查一次是否有套利机会 76 | funding_update_interval: 60 # 更新资金费率的时间间隔(秒),每隔这么多秒从交易所获取一次最新的资金费率 77 | position_sync_interval: 30 # 与交易所同步持仓的时间间隔(秒),每隔这么多秒检查一次交易所实际持仓并同步本地记录 78 | 79 | # 日志配置 80 | # 控制日志输出的详细程度和保存位置 81 | logging: 82 | level: "INFO" # 日志级别,可选值:DEBUG, INFO, WARNING, ERROR, CRITICAL 83 | # 注意:DEBUG级别会记录详细调试信息,适合开发和问题排查 84 | file: "logs/arbitrage.log" # 日志文件路径 85 | 86 | # 日志文件轮转设置 87 | max_file_size: 10485760 # 单个日志文件最大字节数(10MB) 88 | backup_count: 5 # 保留最近5个日志文件 89 | 90 | # 日志频率控制设置 91 | throttling: 92 | enabled: true # 启用日志频率限制 93 | # 各类型日志的最小间隔时间(秒) 94 | price_update: 300 # 价格更新日志间隔: 5分钟 95 | connection: 60 # 连接相关日志间隔: 1分钟 96 | websocket: 120 # WebSocket消息日志间隔: 2分钟 97 | heartbeat: 300 # 心跳消息日志间隔: 5分钟 98 | funding_wait: 300 # 资金费率等待日志间隔: 5分钟 99 | default: 60 # 默认其他类型日志间隔: 1分钟 100 | 101 | # 日志摘要设置 102 | summary_interval: 900 # 生成日志摘要的间隔(秒): 15分钟 103 | 104 | # 交易对配置 105 | # 控制每个交易对的精度和交易参数 106 | trading_pairs: 107 | # 价格精度说明: 108 | # price_precision: 控制价格的小数位数 109 | # 例如:BTC价格精度为1,表示价格只能有1位小数,如30000.0 110 | # ETH价格精度为2,表示价格可以有2位小数,如2500.50 111 | # SOL价格精度为3,表示价格可以有3位小数,如100.500 112 | 113 | # 数量精度说明: 114 | # size_precision: 控制交易数量的小数位数 115 | # 例如:BTC数量精度为3,表示最小交易单位为0.001 116 | # ETH数量精度为2,表示最小交易单位为0.01 117 | # SOL数量精度为2,表示最小交易单位为0.01 118 | 119 | # tick_size说明: 120 | # tick_size: 控制价格的最小变动单位 121 | # 例如:BTC的tick_size为1.0,表示价格只能按1.0的整数倍变动,如30000.0, 30001.0 122 | # ETH的tick_size为0.1,表示价格只能按0.1的整数倍变动,如2500.0, 2500.1 123 | # SOL的tick_size为0.01,表示价格只能按0.01的整数倍变动,如100.00, 100.01 124 | # 注意:tick_size必须大于等于price_precision对应的最小单位 125 | # 例如:如果price_precision=2,则tick_size最小为0.01 126 | 127 | # 以下是各交易对的具体配置 128 | - symbol: "BTC" # 币种符号 129 | min_volume: 0.001 # 最小交易量,小于此值的订单会被交易所拒绝 130 | price_precision: 1 # 价格精度,BTC价格保留1位小数 131 | size_precision: 3 # 数量精度,BTC数量保留3位小数 132 | max_position_size: 0.001 # 最大持仓数量,防止持仓过大 133 | tick_size: 1.0 # 价格最小变动单位,BTC价格必须是1.0的整数倍 134 | 135 | - symbol: "ETH" 136 | min_volume: 0.01 137 | price_precision: 2 138 | size_precision: 2 139 | max_position_size: 0.01 140 | tick_size: 0.1 141 | 142 | - symbol: "SOL" 143 | min_volume: 0.1 144 | price_precision: 2 145 | size_precision: 2 146 | max_position_size: 0.1 147 | tick_size: 0.01 148 | 149 | - symbol: "AVAX" 150 | min_volume: 2 151 | price_precision: 3 152 | size_precision: 2 153 | max_position_size: 2 154 | tick_size: 0.001 155 | 156 | - symbol: "AAVE" 157 | min_volume: 0.4 158 | price_precision: 2 159 | size_precision: 1 160 | max_position_size: 0.4 161 | tick_size: 0.01 162 | 163 | - symbol: "ADA" 164 | min_volume: 80 165 | price_precision: 4 166 | size_precision: 1 167 | max_position_size: 80 168 | tick_size: 0.0001 169 | 170 | - symbol: "ARB" 171 | min_volume: 160 172 | price_precision: 4 173 | size_precision: 1 174 | max_position_size: 160 175 | tick_size: 0.0001 176 | 177 | - symbol: "BERA" 178 | min_volume: 9.0 179 | price_precision: 4 180 | size_precision: 1 181 | max_position_size: 9 182 | tick_size: 0.0001 183 | 184 | - symbol: "BNB" 185 | min_volume: 0.1 186 | price_precision: 1 187 | size_precision: 1 188 | max_position_size: 0.1 189 | tick_size: 0.1 190 | 191 | - symbol: "DOGE" 192 | min_volume: 300 193 | price_precision: 5 194 | size_precision: 1 195 | max_position_size: 300 196 | tick_size: 0.00001 197 | 198 | - symbol: "DOT" 199 | min_volume: 13 200 | price_precision: 3 201 | size_precision: 1 202 | max_position_size: 13 203 | tick_size: 0.001 204 | 205 | - symbol: "ENA" 206 | min_volume: 160 207 | price_precision: 4 208 | size_precision: 1 209 | max_position_size: 160 210 | tick_size: 0.0001 211 | 212 | - symbol: "FARTCOIN" 213 | min_volume: 110 214 | price_precision: 3 215 | size_precision: 1 216 | max_position_size: 110 217 | tick_size: 0.001 218 | 219 | - symbol: "HYPE" 220 | min_volume: 5 221 | price_precision: 3 222 | size_precision: 1 223 | max_position_size: 5 224 | tick_size: 0.001 225 | 226 | - symbol: "IP" 227 | min_volume: 13 228 | price_precision: 4 229 | size_precision: 1 230 | max_position_size: 13 231 | tick_size: 0.0001 232 | 233 | - symbol: "JUP" 234 | min_volume: 130 235 | price_precision: 4 236 | size_precision: 1 237 | max_position_size: 130 238 | tick_size: 0.0001 239 | 240 | - symbol: "KAITO" 241 | min_volume: 55 242 | price_precision: 4 243 | size_precision: 1 244 | max_position_size: 55 245 | tick_size: 0.0001 246 | 247 | - symbol: "LINK" 248 | min_volume: 4 249 | price_precision: 2 250 | size_precision: 1 251 | max_position_size: 4 252 | tick_size: 0.01 253 | 254 | - symbol: "LTC" 255 | min_volume: 0.6 256 | price_precision: 2 257 | size_precision: 1 258 | max_position_size: 0.6 259 | tick_size: 0.01 260 | 261 | - symbol: "ONDO" 262 | min_volume: 65 263 | price_precision: 3 264 | size_precision: 1 265 | max_position_size: 65 266 | tick_size: 0.001 267 | 268 | - symbol: "SUI" 269 | min_volume: 22 270 | price_precision: 2 271 | size_precision: 1 272 | max_position_size: 22 273 | tick_size: 0.01 274 | 275 | - symbol: "S" 276 | min_volume: 100 277 | price_precision: 3 278 | size_precision: 1 279 | max_position_size: 100 280 | tick_size: 0.001 281 | 282 | - symbol: "TRUMP" 283 | min_volume: 6 284 | price_precision: 3 285 | size_precision: 1 286 | max_position_size: 6 287 | tick_size: 0.001 288 | 289 | - symbol: "WIF" 290 | min_volume: 130 291 | price_precision: 4 292 | size_precision: 1 293 | max_position_size: 130 294 | tick_size: 0.0001 295 | 296 | - symbol: "XRP" 297 | min_volume: 25 298 | price_precision: 4 299 | size_precision: 1 300 | max_position_size: 25 301 | tick_size: 0.0001 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 加密货币资金费率套利机器人 2 | 3 | ## 项目概述 4 | 5 | 这是一个专业的加密货币资金费率套利机器人,专注于利用不同交易所之间的资金费率差异进行套利交易。目前支持Hyperliquid和Backpack两个交易所,通过实时监控资金费率和价格差异,自动执行套利策略。 6 | 7 | ### 核心特点 8 | 9 | - **多交易所支持**:同时连接Hyperliquid和Backpack交易所 10 | - **实时监控**:持续监控资金费率和价格差异 11 | - **智能套利**:基于预设条件自动执行套利交易 12 | - **风险控制**:完善的仓位管理和风险控制机制 13 | - **滑点控制**:智能分析订单深度,控制交易滑点 14 | - **详细日志**:完整的交易记录和操作日志 15 | 16 | ## 系统架构 17 | 18 | ### 目录结构 19 | ``` 20 | funding_arbitrage_bot/ 21 | ├── core/ # 核心功能模块 22 | │ ├── arbitrage_engine.py # 套利引擎 23 | │ ├── exchange.py # 交易所接口 24 | │ └── risk_manager.py # 风险管理 25 | ├── utils/ # 工具函数 26 | │ ├── logger.py # 日志工具 27 | │ └── helpers.py # 辅助函数 28 | ├── config.yaml # 配置文件 29 | └── main.py # 主程序入口 30 | ``` 31 | 32 | ### 核心模块说明 33 | 34 | 1. **套利引擎 (arbitrage_engine.py)** 35 | - 实现套利策略逻辑 36 | - 管理交易执行 37 | - 处理开平仓条件 38 | - 计算套利机会 39 | 40 | 2. **交易所接口 (exchange.py)** 41 | - 封装交易所API 42 | - 处理订单管理 43 | - 获取市场数据 44 | - 执行交易操作 45 | 46 | 3. **风险管理 (risk_manager.py)** 47 | - 仓位控制 48 | - 风险限制 49 | - 资金管理 50 | - 止损策略 51 | 52 | ## 安装指南 53 | 54 | ### 环境要求 55 | - Python 3.8+ 56 | - 操作系统:Windows/Linux/MacOS 57 | 58 | ### 安装步骤 59 | 60 | 1. **克隆仓库** 61 | ```bash 62 | git clone https://github.com/yourusername/funding-arbitrage-bot.git 63 | cd funding-arbitrage-bot 64 | ``` 65 | 66 | 2. **安装依赖** 67 | ```bash 68 | pip install -r requirements.txt 69 | ``` 70 | 71 | 3. **配置API密钥** 72 | 在`config.yaml`中配置交易所API密钥: 73 | ```yaml 74 | exchanges: 75 | hyperliquid: 76 | api_key: "你的Hyperliquid钱包地址" 77 | api_secret: "你的Hyperliquid私钥" 78 | backpack: 79 | api_key: "你的Backpack API密钥" 80 | api_secret: "你的Backpack API密钥" 81 | ``` 82 | 83 | ## 配置说明 84 | 85 | ### 基础配置 86 | ```yaml 87 | strategy: 88 | # 基本参数 89 | min_funding_diff: 0.01 # 最小资金费率差异(1%) 90 | min_price_diff_pct: 0.001 # 最小价格差异(0.1%) 91 | 92 | # 交易参数 93 | trade_size_usd: 100 # 单笔交易金额(美元) 94 | max_position_usd: 500 # 最大仓位(美元) 95 | trade_cooldown: 3600 # 交易冷却时间(秒) 96 | ``` 97 | 98 | ### 开仓条件 99 | ```yaml 100 | open_conditions: 101 | condition_type: "funding_only" # 条件类型 102 | min_funding_diff: 0.01 # 最小资金费率差异 103 | min_price_diff_percent: 0.2 # 最小价格差异 104 | max_slippage_percent: 0.15 # 最大允许滑点 105 | ignore_high_slippage: false # 是否忽略高滑点 106 | check_expected_profit: true # 是否检查预期收益 107 | min_expected_profit: 0.00001 # 最小预期收益要求 108 | ``` 109 | 110 | ### 平仓条件 111 | ```yaml 112 | close_conditions: 113 | condition_type: "any" # 条件类型 114 | funding_diff_sign_change: true # 资金费率符号变化时平仓 115 | min_funding_diff: 0.005 # 最小资金费率差异 116 | min_profit_percent: 0.1 # 最小利润百分比 117 | max_loss_percent: 0.3 # 最大损失百分比 118 | check_expected_profit: true # 是否检查预期收益 119 | min_expected_profit: 0.00001 # 最小预期收益要求 120 | ``` 121 | 122 | ### 预期收益检查功能 123 | ```yaml 124 | # 新增:预期收益检查功能 125 | open_conditions: 126 | check_expected_profit: true # 是否检查预期收益 127 | min_expected_profit: 0.00001 # 最小预期收益要求 128 | 129 | close_conditions: 130 | check_expected_profit: true # 是否检查预期收益 131 | min_expected_profit: 0.00001 # 最小预期收益要求 132 | ``` 133 | 预期收益检查是一项强大的功能,可以帮助系统做出更明智的交易决策: 134 | - 在开仓时,系统计算基于当前资金费率的预期收益 135 | - 在平仓时,系统计算维持当前持仓的预期收益 136 | - 只有当预期收益超过设定阈值时才开仓 137 | - 当预期收益低于设定阈值时触发平仓 138 | - 这可以有效防止在双方资金费率均为负或均为正的情况下产生的亏损交易 139 | 140 | ## 运行指南 141 | 142 | ### 启动程序 143 | ```bash 144 | python run_bot.py 145 | ``` 146 | 147 | ### 测试模式 148 | ```bash 149 | python run_bot.py --test 150 | ``` 151 | 152 | ### 指定配置文件 153 | ```bash 154 | python run_bot.py --config path/to/config.yaml 155 | ``` 156 | 157 | ## 风险控制 158 | 159 | ### 仓位管理 160 | - 单个币种最大仓位限制 161 | - 总体仓位限制 162 | - 动态仓位调整 163 | 164 | ### 滑点控制 165 | - 订单深度分析 166 | - 滑点预估 167 | - 动态滑点限制 168 | 169 | ### 止损机制 170 | - 固定止损 171 | - 追踪止损 172 | - 时间止损 173 | 174 | ## 日志系统 175 | 176 | ### 日志级别 177 | - DEBUG: 调试信息 178 | - INFO: 一般信息 179 | - WARNING: 警告信息 180 | - ERROR: 错误信息 181 | 182 | ### 日志内容 183 | - 交易执行记录 184 | - 资金费率变化 185 | - 套利机会分析 186 | - 错误和异常信息 187 | 188 | ## 常见问题 189 | 190 | 1. **如何处理API连接失败?** 191 | - 检查网络连接 192 | - 验证API密钥 193 | - 查看错误日志 194 | 195 | 2. **如何优化套利策略?** 196 | - 调整资金费率差异阈值 197 | - 优化开平仓条件 198 | - 调整交易金额 199 | 200 | 3. **如何处理高滑点情况?** 201 | - 增加最小流动性要求 202 | - 调整最大滑点限制 203 | - 使用限价单替代市价单 204 | 205 | ## 更新日志 206 | 207 | ### 2025-04-17 208 | - 添加持仓盈亏状态实时计算和显示功能 209 | - 移除旧的方向一致性检查功能,用盈亏状态显示替代 210 | - 优化持仓信息展示,包括预期收益率和盈亏状态 211 | - 在界面中直观显示持仓时长和开仓时间 212 | 213 | ### 2025-04-16 214 | - 大幅简化开仓和平仓条件,只保留两个核心条件:预期收益和滑点控制 215 | - 移除了方向一致性检查、价格差异、资金费率差异等复杂条件 216 | - 提高了滑点容忍度,开仓滑点从0.25%提高到0.6%,平仓滑点从0.3%提高到0.8% 217 | - 简化了逻辑,使系统更加稳定和可预测 218 | 219 | ### 2025-04-15 220 | - 添加预期收益检查功能,防止亏损开仓 221 | - 改进资金费率套利逻辑,增加盈利判断 222 | - 优化开平仓条件检查,考虑实际预期收益 223 | - 在同为正费率或负费率的情况下增加智能方向选择 224 | 225 | ## 持仓盈亏状态计算算法 226 | 227 | 系统实时计算已持有仓位的盈亏状态,帮助用户直观了解当前持仓在最新资金费率下的收益情况。 228 | 229 | ### 盈亏计算核心逻辑 230 | 231 | 盈亏状态计算采用了简单统一的模型: 232 | 1. 计算理论最大收益(费率差的绝对值) 233 | 2. 判断当前持仓方向是否是最优方向 234 | 3. 如果方向正确,则盈利=最大理论收益;如果方向错误,则亏损=最大理论收益 235 | 236 | ### 不同资金费率组合的计算方式 237 | 238 | #### 1. 两个交易所都是负费率时 239 | - **理论最大收益**:`|abs(bp_funding) - abs(hl_funding)|` (两个负费率绝对值之差) 240 | - **最优交易方向**:绝对值大的交易所做多,绝对值小的交易所做空 241 | - **示例**: 242 | - BP费率 = -0.005(-0.5%),HL费率 = -0.002(-0.2%) 243 | - 最优方向:BP做多,HL做空 244 | - 理论最大收益 = |0.005 - 0.002| = 0.003(0.3%) 245 | - 如持仓方向正确,预期收益 = +0.003(0.3%);方向错误,预期收益 = -0.003(-0.3%) 246 | 247 | #### 2. 两个交易所都是正费率时 248 | - **理论最大收益**:`|bp_funding - hl_funding|` (两个正费率之差) 249 | - **最优交易方向**:费率大的交易所做空,费率小的交易所做多 250 | - **示例**: 251 | - BP费率 = 0.008(0.8%),HL费率 = 0.003(0.3%) 252 | - 最优方向:BP做空,HL做多 253 | - 理论最大收益 = |0.008 - 0.003| = 0.005(0.5%) 254 | - 如持仓方向正确,预期收益 = +0.005(0.5%);方向错误,预期收益 = -0.005(-0.5%) 255 | 256 | #### 3. 一个交易所正费率,一个交易所负费率(经典资金费率套利) 257 | - **理论最大收益**:`abs(bp_funding) + abs(hl_funding)` (正费率绝对值 + 负费率绝对值) 258 | - **最优交易方向**:正费率交易所做空,负费率交易所做多 259 | - **示例**: 260 | - BP费率 = 0.006(0.6%),HL费率 = -0.004(-0.4%) 261 | - 最优方向:BP做空,HL做多 262 | - 理论最大收益 = 0.006 + 0.004 = 0.01(1.0%) 263 | - 如持仓方向正确,预期收益 = +0.01(1.0%);方向错误,预期收益 = -0.01(-1.0%) 264 | 265 | ### 关键特点 266 | 267 | 1. **统一的逻辑**:不论哪种资金费率组合,都使用同一套判断逻辑,方向正确则盈利,方向错误则亏损。 268 | 2. **绝对值的运用**:通过使用费率绝对值,使计算更加直观。 269 | 3. **方向的重要性**:持仓方向是决定盈亏的关键因素,正确的方向使盈利最大化。 270 | 4. **自动调整**:系统会根据最新资金费率自动计算,实时反映持仓盈亏状态。 271 | 272 | ### 时间周期调整 273 | 274 | 由于两个交易所的资金费率结算周期不同: 275 | - Backpack为8小时结算一次 276 | - Hyperliquid为1小时结算一次 277 | 278 | 系统在计算时会将Hyperliquid的资金费率乘以8进行调整,使两个交易所的资金费率在时间周期上可比较。 279 | 280 | ### 界面显示 281 | 282 | 系统在交易界面展示每个持仓的盈亏状态: 283 | - **盈利/亏损状态**:直观显示当前持仓是否盈利 284 | - **预期收益率**:显示具体的预期收益百分比 285 | - **持仓时长**:显示从开仓至今的时间 286 | - **最优方向**:显示当前资金费率下的最优持仓方向 287 | 288 | 当预期收益低于配置的最小阈值(默认0.03%)时,系统会自动触发平仓操作,实现资金的有效利用。 289 | 290 | ## 简化后的开平仓策略 291 | 292 | 当前策略采用极简的开平仓逻辑,只考虑两个核心因素: 293 | 294 | ### 开仓条件 295 | - **预期收益要求**:预计的资金费率收益必须≥0.05% 296 | - **滑点控制**:执行开仓时,滑点必须≤0.6% 297 | 298 | ### 平仓条件 299 | - **预期收益要求**:当预计的资金费率收益<0.02%时平仓 300 | - **滑点控制**:执行平仓时,滑点必须≤0.8% 301 | - **最短持仓时间**:30秒(防止频繁交易) 302 | 303 | 这种简化的策略有以下优势: 304 | 1. 逻辑清晰,易于理解和维护 305 | 2. 聚焦于资金费率套利的核心盈利点 306 | 3. 减少过度交易,提高每笔交易的盈利性 307 | 4. 更稳健的滑点控制,适应不同市场流动性条件 308 | 309 | 通过这次更新,移除了复杂的方向检查、价格差异和资金费率差异的阈值判断,集中关注预期收益,让策略回归资金费率套利的本质。 310 | 311 | ## 问题修复日志 312 | 313 | ### 2025-04-10: 修复Hyperliquid市价单下单问题 314 | - **问题**:Hyperliquid平仓失败,日志显示错误"'Exchange' object has no attribute 'market_order'" 315 | - **原因**:代码中使用了不存在的`market_order`和`limit_order`方法 316 | - **解决方案**: 317 | - 修改了`place_order_sync`方法,将`market_order`和`limit_order`调用替换为正确的`order`方法 318 | - 使用正确的参数格式:`order_type="market"`和`order_type="limit"` 319 | - 这种修改适应了Hyperliquid SDK的最新版本 320 | - **文件修改**:`funding_arbitrage_bot/exchanges/hyperliquid_api.py` 321 | 322 | ### 2025-04-11: 修复Hyperliquid无效订单类型问题 323 | - **问题**:下单时出现"Invalid order type, market"错误 324 | - **原因**:经测试发现Hyperliquid API不支持市价单(Market Order),只支持限价单(Limit Order) 325 | - **解决方案**: 326 | - 修改了`place_order_sync`方法中的市价单处理逻辑 327 | - 当用户请求市价单时,使用限价单模拟市价单效果 328 | - 使用当前市场价格加上一定的滑点(买入+5%,卖出-5%)来确保订单能够快速成交 329 | - 使用正确的限价单格式:`order_type={"limit": {"tif": "Gtc"}}` 330 | - **文件修改**:`funding_arbitrage_bot/exchanges/hyperliquid_api.py` 331 | - **注意事项**: 332 | - 所有对Hyperliquid的交易,包括开仓和平仓,现在都使用限价单 333 | - 系统会自动计算合适的价格,确保类似市价单的快速成交体验 334 | - 详细的技术文档已添加在`funding_arbitrage_bot/Hyperliquid 订单创建指南.md`中 335 | 336 | ### 2025-04-12: 修复Hyperliquid价格格式错误问题 337 | - **问题**:下单时出现"Order has invalid price"错误 338 | - **原因**:Hyperliquid对价格格式有严格要求,必须按照币种配置的tick_size和price_precision进行调整 339 | - **解决方案**: 340 | - 修改了`place_order_sync`方法中的价格处理逻辑 341 | - 从配置文件中读取每个币种的tick_size和price_precision 342 | - 按照tick_size调整价格以符合交易所的最小价格变动单位 343 | - 控制价格的小数位数保持在配置的精度范围内 344 | - 完善了日志记录,显示价格调整前后的变化 345 | - **文件修改**:`funding_arbitrage_bot/exchanges/hyperliquid_api.py` 346 | - **相关配置**: 347 | - 交易对的价格精度和tick_size在`config.yaml`的`trading_pairs`部分配置 348 | - 例如:FARTCOIN的price_precision=3, tick_size=0.001 349 | - 例如:HYPE的price_precision=3, tick_size=0.001 350 | 351 | ## 贡献指南 352 | 353 | 欢迎提交Issue和Pull Request来帮助改进项目。在提交代码前,请确保: 354 | 1. 代码符合PEP 8规范 355 | 2. 添加适当的测试 356 | 3. 更新相关文档 357 | 358 | ## 许可证 359 | 360 | 本项目采用MIT许可证。详见LICENSE文件。 361 | 362 | ## 联系方式 363 | 364 | 如有问题或建议,请通过以下方式联系: 365 | - 提交Issue 366 | - 发送邮件至:your.email@example.com -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/log_utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 日志工具模块 5 | 6 | 提供日志频率限制和批量日志处理工具,用于减少冗余日志输出 7 | """ 8 | 9 | import time 10 | import logging 11 | from typing import Dict, List, Any, Optional, Tuple, Union 12 | from datetime import datetime 13 | 14 | 15 | class RateLimitedLogger: 16 | """频率限制日志记录器,避免相同类型的日志短时间内重复输出""" 17 | 18 | def __init__(self, min_interval_seconds: Dict[str, int] = None): 19 | """ 20 | 初始化频率限制日志记录器 21 | 22 | Args: 23 | min_interval_seconds: 各种日志类型的最小记录间隔(秒),格式为{类型: 间隔} 24 | """ 25 | self.last_log_times = {} # 存储每种类型的上次记录时间 26 | self.min_intervals = min_interval_seconds or { 27 | "default": 60, # 默认限制:60秒 28 | "price_update": 300, # 价格更新:5分钟 29 | "connection": 60, # 连接日志:1分钟 30 | "api_call": 60, # API调用:1分钟 31 | "heartbeat": 300, # 心跳消息:5分钟 32 | "websocket": 120, # WebSocket消息:2分钟 33 | } 34 | 35 | def should_log(self, log_type: str) -> bool: 36 | """ 37 | 检查是否应该记录指定类型的日志 38 | 39 | Args: 40 | log_type: 日志类型标识 41 | 42 | Returns: 43 | 如果应该记录返回True,否则返回False 44 | """ 45 | now = time.time() 46 | last_time = self.last_log_times.get(log_type, 0) 47 | interval = self.min_intervals.get(log_type, self.min_intervals["default"]) 48 | 49 | if now - last_time >= interval: 50 | self.last_log_times[log_type] = now 51 | return True 52 | return False 53 | 54 | def log(self, logger: logging.Logger, level: str, log_type: str, message: str, *args, **kwargs) -> None: 55 | """ 56 | 按照频率限制记录日志 57 | 58 | Args: 59 | logger: 日志记录器 60 | level: 日志级别('debug', 'info', 'warning', 'error', 'critical') 61 | log_type: 日志类型标识 62 | message: 日志消息 63 | *args, **kwargs: 传递给日志方法的参数 64 | """ 65 | if self.should_log(log_type): 66 | log_method = getattr(logger, level.lower()) 67 | log_method(message, *args, **kwargs) 68 | 69 | 70 | class LogSummarizer: 71 | """日志摘要生成器,收集一段时间内的日志并生成摘要""" 72 | 73 | def __init__(self, logger: logging.Logger, interval_seconds: int = 300): 74 | """ 75 | 初始化日志摘要生成器 76 | 77 | Args: 78 | logger: 日志记录器 79 | interval_seconds: 摘要生成间隔(秒) 80 | """ 81 | self.logger = logger 82 | self.interval = interval_seconds 83 | self.last_summary_time = time.time() 84 | 85 | # 收集各种事件的临时存储 86 | self.price_updates = {} # {symbol: [(old_price, new_price, timestamp), ...]} 87 | self.funding_updates = {} # {symbol: (rate, timestamp)} 88 | self.api_calls = {"success": 0, "failed": 0} 89 | self.errors = {} # {error_type: count} 90 | self.connection_events = {"connect": 0, "disconnect": 0} 91 | 92 | def record_price_update(self, symbol: str, exchange: str, old_price: float, new_price: float) -> None: 93 | """ 94 | 记录价格更新事件 95 | 96 | Args: 97 | symbol: 币种符号 98 | exchange: 交易所名称 99 | old_price: 旧价格 100 | new_price: 新价格 101 | """ 102 | key = f"{exchange}_{symbol}" 103 | if key not in self.price_updates: 104 | self.price_updates[key] = [] 105 | 106 | self.price_updates[key].append((old_price, new_price, time.time())) 107 | self._check_summary() 108 | 109 | def record_funding_update(self, symbol: str, exchange: str, rate: float) -> None: 110 | """ 111 | 记录资金费率更新事件 112 | 113 | Args: 114 | symbol: 币种符号 115 | exchange: 交易所名称 116 | rate: 资金费率 117 | """ 118 | key = f"{exchange}_{symbol}" 119 | self.funding_updates[key] = (rate, time.time()) 120 | self._check_summary() 121 | 122 | def record_api_call(self, success: bool = True) -> None: 123 | """ 124 | 记录API调用结果 125 | 126 | Args: 127 | success: 调用是否成功 128 | """ 129 | if success: 130 | self.api_calls["success"] += 1 131 | else: 132 | self.api_calls["failed"] += 1 133 | self._check_summary() 134 | 135 | def record_error(self, error_type: str) -> None: 136 | """ 137 | 记录错误事件 138 | 139 | Args: 140 | error_type: 错误类型 141 | """ 142 | if error_type not in self.errors: 143 | self.errors[error_type] = 0 144 | 145 | self.errors[error_type] += 1 146 | self._check_summary() 147 | 148 | def record_connection_event(self, event_type: str) -> None: 149 | """ 150 | 记录连接事件 151 | 152 | Args: 153 | event_type: 事件类型('connect'或'disconnect') 154 | """ 155 | if event_type in self.connection_events: 156 | self.connection_events[event_type] += 1 157 | self._check_summary() 158 | 159 | def _check_summary(self) -> None: 160 | """检查是否应该生成摘要""" 161 | now = time.time() 162 | if now - self.last_summary_time >= self.interval: 163 | self._generate_summary() 164 | self.last_summary_time = now 165 | 166 | def _generate_summary(self) -> None: 167 | """生成并记录摘要日志""" 168 | # 处理价格更新摘要 169 | if self.price_updates: 170 | significant_updates = [] 171 | for key, updates in self.price_updates.items(): 172 | if not updates: 173 | continue 174 | 175 | exchange, symbol = key.split("_") 176 | first_update = updates[0] 177 | last_update = updates[-1] 178 | first_price = first_update[0] or 0 # 处理None值 179 | last_price = last_update[1] 180 | update_count = len(updates) 181 | 182 | # 计算价格变化百分比 183 | if first_price > 0: 184 | change_pct = ((last_price - first_price) / first_price) * 100 185 | # 只记录有显著变化的价格 186 | if abs(change_pct) > 0.5 or update_count > 10: # 变化超过0.5%或者更新次数多 187 | significant_updates.append((key, first_price, last_price, change_pct, update_count)) 188 | 189 | # 按变化幅度排序,显示最显著的变化 190 | significant_updates.sort(key=lambda x: abs(x[3]), reverse=True) 191 | top_updates = significant_updates[:5] # 最多显示5个显著变化 192 | 193 | if top_updates: 194 | updates_text = [] 195 | for key, first, last, change, count in top_updates: 196 | exchange, symbol = key.split("_") 197 | updates_text.append(f"{exchange}/{symbol}: {first:.2f}→{last:.2f} ({change:+.2f}%, {count}次)") 198 | 199 | more_count = len(significant_updates) - len(top_updates) 200 | more_text = f" 及其他{more_count}个交易对有变化" if more_count > 0 else "" 201 | 202 | self.logger.info(f"价格显著变化: {', '.join(updates_text)}{more_text}") 203 | 204 | # 处理资金费率摘要 205 | if self.funding_updates: 206 | updates_text = [] 207 | sorted_items = sorted(self.funding_updates.items(), 208 | key=lambda x: abs(x[1][0]), 209 | reverse=True)[:5] # 按费率绝对值排序 210 | 211 | for key, (rate, _) in sorted_items: 212 | exchange, symbol = key.split("_") 213 | updates_text.append(f"{exchange}/{symbol}: {rate:+.6f}") 214 | 215 | more_count = len(self.funding_updates) - len(sorted_items) 216 | more_text = f" 及其他{more_count}个更新" if more_count > 0 else "" 217 | 218 | self.logger.info(f"资金费率更新: {', '.join(updates_text)}{more_text}") 219 | 220 | # 处理API调用摘要 221 | if self.api_calls["success"] > 0 or self.api_calls["failed"] > 0: 222 | self.logger.info(f"API调用统计: 成功 {self.api_calls['success']}次, 失败 {self.api_calls['failed']}次") 223 | self.api_calls = {"success": 0, "failed": 0} 224 | 225 | # 处理错误摘要 226 | if self.errors: 227 | error_texts = [] 228 | for error_type, count in self.errors.items(): 229 | error_texts.append(f"{error_type}: {count}次") 230 | 231 | self.logger.warning(f"错误统计: {', '.join(error_texts)}") 232 | self.errors = {} 233 | 234 | # 处理连接事件摘要 235 | if self.connection_events["connect"] > 0 or self.connection_events["disconnect"] > 0: 236 | self.logger.info(f"连接事件统计: 连接 {self.connection_events['connect']}次, 断开 {self.connection_events['disconnect']}次") 237 | self.connection_events = {"connect": 0, "disconnect": 0} 238 | 239 | # 清空收集的数据 240 | self.price_updates = {} 241 | self.funding_updates = {} 242 | 243 | def force_summary(self) -> None: 244 | """强制生成摘要,不考虑时间间隔""" 245 | self._generate_summary() 246 | self.last_summary_time = time.time() -------------------------------------------------------------------------------- /funding_arbitrage_bot/docs/# Backpack和Hyperliquid交易所API对比文档.md: -------------------------------------------------------------------------------- 1 | # Backpack和Hyperliquid交易所API对比文档 2 | 3 | 本文档详细对比了Backpack和Hyperliquid两个交易所在资金费率套利机器人中的API实现方式,包括价格获取、资金费率查询、持仓管理和订单创建等关键功能。 4 | 5 | ## 价格获取方式 6 | 7 | ### Backpack 8 | 1. **主要方式**:WebSocket订阅实时价格 9 | ```python 10 | # 如果已经通过WebSocket获取了价格,直接返回 11 | if symbol in self.prices: 12 | return self.prices.get(symbol) 13 | ``` 14 | 15 | 2. **备用方式**:REST API请求 16 | ```python 17 | # 如果没有WebSocket缓存,通过REST API获取 18 | response = await self.http_client.get(f"{self.base_url}/api/v1/ticker/24hr?symbol={symbol}") 19 | if response.status_code == 200: 20 | data = response.json() 21 | if "lastPrice" in data: 22 | price = float(data["lastPrice"]) 23 | self.prices[symbol] = price 24 | return price 25 | ``` 26 | 27 | 3. **API端点**:`/api/v1/ticker/24hr?symbol={symbol}` 28 | 29 | ### Hyperliquid 30 | 1. **主要方式**:WebSocket订阅实时价格 31 | ```python 32 | # 首先检查WebSocket价格缓存 33 | if symbol in self.prices: 34 | return self.prices[symbol] 35 | ``` 36 | 37 | 2. **备用方式**:REST API请求 38 | ```python 39 | # 如果WebSocket中没有,尝试通过REST API获取 40 | url = f"{self.base_url}/info" 41 | response = await self.http_client.get(url) 42 | 43 | if response.status_code == 200: 44 | data = response.json() 45 | 46 | # 解析响应中的价格信息 47 | for meta in data[0].get("universe", []): 48 | if meta.get("name") == symbol: 49 | return float(meta.get("midPrice", 0)) 50 | ``` 51 | 52 | 3. **API端点**:`/info` 53 | 54 | ## 资金费率获取方式 55 | 56 | ### Backpack 57 | 1. **方式**:REST API请求 58 | ```python 59 | url = f"{self.base_url}/api/v1/fundingRates?symbol={symbol}" 60 | response = await self.http_client.get(url) 61 | 62 | # 解析响应 63 | data = response.json() 64 | latest_funding = data[0] 65 | funding_rate = float(latest_funding["fundingRate"]) 66 | ``` 67 | 68 | 2. **API端点**:`/api/v1/fundingRates?symbol={symbol}` 69 | 70 | ### Hyperliquid 71 | 1. **方式**:REST API请求 72 | ```python 73 | url = f"{self.base_url}/info" 74 | payload = {"type": "metaAndAssetCtxs"} 75 | 76 | response = await self.http_client.post(url, json=payload) 77 | data = response.json() 78 | 79 | # 查找特定币种 80 | coin_idx = -1 81 | for i, coin_data in enumerate(universe): 82 | if isinstance(coin_data, dict) and coin_data.get("name") == symbol: 83 | coin_idx = i 84 | break 85 | 86 | if coin_idx >= 0 and coin_idx < len(asset_ctxs): 87 | coin_ctx = asset_ctxs[coin_idx] 88 | 89 | if "funding" in coin_ctx: 90 | funding_rate = float(coin_ctx["funding"]) 91 | return funding_rate 92 | ``` 93 | 94 | 2. **API端点**:`/info` 使用POST方法和`{"type": "metaAndAssetCtxs"}`载荷 95 | 96 | 3. **响应示例**: 97 | ``` 98 | 2025-04-06 00:35:24,200 - INFO - BTC的资产上下文: { 99 | 'funding': '0.0000125', 100 | 'openInterest': '10719.24896', 101 | 'prevDayPx': '83114.0', 102 | 'dayNtlVlm': '2190927944.0337491035', 103 | 'premium': '-0.0002900547', 104 | 'oraclePx': '82743.0', 105 | 'markPx': '82713.0', 106 | 'midPx': '82718.5', 107 | 'impactPxs': ['82718.0', '82719.0'], 108 | 'dayBaseVlm': '26185.4109' 109 | } 110 | 2025-04-06 00:35:24,200 - INFO - BTC 资金费率(小时): 1.25e-05, 调整后(8小时): 0.0001 111 | ``` 112 | 113 | ## 持仓查询方式 114 | 115 | ### Backpack 116 | 1. **方式**:签名的REST API请求 117 | ```python 118 | response = await self._make_signed_request("GET", "/api/v1/positions") 119 | 120 | # 解析响应找到特定交易对的持仓 121 | for position in response: 122 | if position.get("symbol") == symbol: 123 | # 处理持仓数据 124 | size = float(position.get("size", 0)) 125 | side = "BUY" if size > 0 else "SELL" 126 | abs_size = abs(size) 127 | ``` 128 | 129 | 2. **API端点**:`/api/v1/positions` 130 | 3. **认证方式**:ED25519签名算法 131 | 132 | ### Hyperliquid 133 | 1. **方式**:REST API请求 134 | ```python 135 | url = f"{self.base_url}/info" 136 | payload = { 137 | "type": "clearinghouseState", 138 | "user": self.hyperliquid_address 139 | } 140 | 141 | response = await self.http_client.post(url, json=payload) 142 | user_data = response.json() 143 | 144 | # 解析持仓数据 145 | positions = {} 146 | if "assetPositions" in user_data: 147 | asset_positions = user_data["assetPositions"] 148 | 149 | for pos_item in asset_positions: 150 | # 处理每个持仓数据 151 | pos = pos_item["position"] 152 | coin = pos.get("coin") 153 | size = pos.get("szi") 154 | 155 | side = "BUY" if size_value > 0 else "SELL" 156 | abs_size = abs(size_value) 157 | ``` 158 | 159 | 2. **API端点**:`/info` 使用POST方法和`{"type": "clearinghouseState", "user": wallet_address}`载荷 160 | 3. **认证方式**:通过钱包地址查询 161 | 162 | ## 订单创建方式 163 | 164 | ### Backpack 165 | 1. **方式**:签名的REST API请求 166 | ```python 167 | # 准备订单数据 168 | order_data = { 169 | "symbol": symbol, 170 | "side": "Bid" if side == "BUY" else "Ask", 171 | "orderType": "Market" if order_type == "MARKET" else "Limit", 172 | "quantity": formatted_size, 173 | "timeInForce": "GTC", 174 | "clientId": client_id 175 | } 176 | 177 | # 如果是限价单,添加价格 178 | if order_type == "LIMIT": 179 | order_data["price"] = formatted_price 180 | 181 | # 生成签名 182 | timestamp = int(time.time() * 1000) 183 | signature = self._generate_ed25519_signature(order_data, "orderExecute", timestamp) 184 | 185 | # 发送请求 186 | response = await client.post( 187 | f"{self.base_url}/api/v1/order", 188 | headers=headers, 189 | json=order_data 190 | ) 191 | ``` 192 | 193 | 2. **API端点**:`/api/v1/order` 194 | 3. **订单类型**:支持限价单(LIMIT)和市价单(MARKET) 195 | 4. **认证方式**:ED25519签名算法 196 | 197 | ### Hyperliquid 198 | 1. **方式**:使用官方SDK或REST API 199 | ```python 200 | # 使用SDK下单 201 | if order_type.upper() == "LIMIT": 202 | # 限价单 203 | response = self.hl_exchange.order( 204 | coin=coin, 205 | is_buy=is_buy, 206 | sz=sz, 207 | limit_px=limit_px, 208 | order_type="Limit" 209 | ) 210 | ``` 211 | 212 | 2. **SDK方法**:`hl_exchange.order()` 213 | 3. **订单类型**:只支持限价单(Limit),市价单通过调整价格的限价单模拟 214 | 4. **认证方式**:通过钱包私钥创建的wallet对象进行签名 215 | 216 | ## 总结对比 217 | 218 | | 功能 | Backpack | Hyperliquid | 219 | |--------------|-----------------------------------------------------|-------------------------------------------------------| 220 | | 价格获取 | 1. WebSocket实时订阅
2. REST API `/api/v1/ticker/24hr` | 1. WebSocket实时订阅
2. REST API `/info` | 221 | | 资金费率获取 | REST API `/api/v1/fundingRates` | REST API `/info` POST `{"type": "metaAndAssetCtxs"}` | 222 | | 持仓查询 | 签名的REST API `/api/v1/positions` | REST API `/info` POST `{"type": "clearinghouseState"}` | 223 | | 订单创建 | 签名的REST API `/api/v1/order` | 官方SDK `hl_exchange.order()` | 224 | | 认证方式 | ED25519签名算法 | 基于以太坊钱包私钥的签名 | 225 | | 订单类型 | 支持限价单和市价单 | 仅支持限价单(通过调整价格模拟市价单) | 226 | | API格式 | REST API,参数通过URL或JSON传递 | REST API + SDK,大部分操作使用JSON载荷 | 227 | | 币种格式 | 交易对格式如 `BTC_USDC_PERP` | 基础币种格式如 `BTC` | 228 | 229 | ## 资金费率数据示例 230 | 231 | 下面是Hyperliquid资金费率的测试输出示例: 232 | 233 | ``` 234 | 2025-04-06 00:35:23,554 - INFO - 直接使用REST API获取资金费率 235 | 2025-04-06 00:35:24,199 - INFO - API响应类型: 236 | 2025-04-06 00:35:24,200 - INFO - 找到187个币种 237 | 2025-04-06 00:35:24,200 - INFO - 币种0: BTC 238 | 2025-04-06 00:35:24,200 - INFO - BTC的资产上下文: {'funding': '0.0000125', 'openInterest': '10719.24896', 'prevDayPx': '83114.0', 'dayNtlVlm': '2190927944.0337491035', 'premium': '-0.0002900547', 'oraclePx': '82743.0', 'markPx': '82713.0', 'midPx': '82718.5', 'impactPxs': ['82718.0', '82719.0'], 'dayBaseVlm': '26185.4109'} 239 | 2025-04-06 00:35:24,200 - INFO - BTC 资金费率(小时): 1.25e-05, 调整后(8小时): 0.0001 240 | 2025-04-06 00:35:24,200 - INFO - 币种1: ETH 241 | 2025-04-06 00:35:24,200 - INFO - ETH的资产上下文: {'funding': '-0.0000066154', 'openInterest': '353244.807', 'prevDayPx': '1789.2', 'dayNtlVlm': '439389975.3573400974', 'premium': '-0.0005595971', 'oraclePx': '1787.0', 'markPx': '1785.9', 'midPx': '1785.95', 'impactPxs': ['1785.9', '1786.0'], 'dayBaseVlm': '243368.3274'} 242 | 2025-04-06 00:35:24,200 - INFO - ETH 资金费率(小时): -6.6154e-06, 调整后(8小时): -5.29232e-05 243 | 2025-04-06 00:35:24,200 - INFO - 币种2: ATOM 244 | 2025-04-06 00:35:24,200 - INFO - ATOM的资产上下文: {'funding': '-0.0000135665', 'openInterest': '340191.82', 'prevDayPx': '5.0226', 'dayNtlVlm': '2219603.6365310005', 'premium': '-0.0005309915', 'oraclePx': '4.8965', 'markPx': '4.8922', 'midPx': '4.8932', 'impactPxs': ['4.891', '4.8939'], 'dayBaseVlm': '445576.73'} 245 | 2025-04-06 00:35:24,200 - INFO - ATOM 资金费率(小时): -1.35665e-05, 调整后(8小时): -0.000108532 246 | 2025-04-06 00:35:24,200 - INFO - 单独查询 - BTC 资金费率: 1.25e-05 247 | 2025-04-06 00:35:24,200 - INFO - 单独查询 - ETH 资金费率: -6.6154e-06 248 | 2025-04-06 00:35:24,200 - INFO - 单独查询 - SOL 资金费率: -2.39842e-05 249 | ``` 250 | 251 | ## 注意事项与实现提示 252 | 253 | 1. **币种格式转换**:在两个交易所之间进行套利时,需要注意币种格式转换: 254 | - Backpack: `BTC_USDC_PERP` 255 | - Hyperliquid: `BTC` 256 | 257 | 2. **资金费率比较**: 258 | - Hyperliquid的资金费率为每小时费率 259 | - 比较时可能需要转换为8小时费率以保持一致性 260 | 261 | 3. **订单下单限制**: 262 | - Hyperliquid只支持限价单,市价单需要通过调整价格实现 263 | - Backpack支持多种订单类型 264 | 265 | 4. **认证方式**: 266 | - Backpack: 需要API Key和Secret,使用ED25519签名 267 | - Hyperliquid: 需要以太坊钱包私钥和地址,使用区块链钱包认证 268 | 269 | 5. **API设计差异**: 270 | - Backpack: 每个功能有专门的API端点 271 | - Hyperliquid: 多个功能通过同一个端点(`/info`)实现,通过不同的payload区分 272 | 273 | 以上对比可帮助开发者理解两个交易所API的实现方式,为套利策略开发提供参考。 -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 辅助函数模块 5 | 6 | 包含各种工具函数,用于支持套利机器人的操作 7 | """ 8 | 9 | import os 10 | import yaml 11 | import decimal 12 | from decimal import Decimal 13 | from typing import Dict, List, Any, Optional, Union, Tuple 14 | from datetime import datetime 15 | import logging 16 | 17 | 18 | def get_backpack_symbol(symbol): 19 | """获取Backpack格式的交易对""" 20 | return f"{symbol}_USDC_PERP" 21 | 22 | 23 | def get_hyperliquid_symbol(symbol): 24 | """获取Hyperliquid格式的交易对""" 25 | return symbol 26 | 27 | 28 | def decimal_adjust(value: float, precision: int, rounding_mode: str = 'ROUND_DOWN') -> float: 29 | """ 30 | 根据指定精度调整数值 31 | 32 | Args: 33 | value: 需要调整的浮点数 34 | precision: 小数位精度 35 | rounding_mode: 舍入模式,默认为 'ROUND_DOWN' 36 | 37 | Returns: 38 | 调整后的浮点数 39 | """ 40 | if not hasattr(decimal, rounding_mode): 41 | raise ValueError(f"无效的舍入模式: {rounding_mode}") 42 | 43 | rounding = getattr(decimal, rounding_mode) 44 | context = decimal.getcontext().copy() 45 | context.rounding = rounding 46 | 47 | quantize_exp = Decimal('0.' + '0' * precision) 48 | result = Decimal(str(value)).quantize(quantize_exp, context=context) 49 | 50 | return float(result) 51 | 52 | 53 | def safe_get(data: Dict, keys: List[str], default: Any = None) -> Any: 54 | """ 55 | 安全地从嵌套字典中获取值 56 | 57 | Args: 58 | data: 嵌套字典 59 | keys: 键的路径列表 60 | default: 如果键不存在时返回的默认值 61 | 62 | Returns: 63 | 获取到的值,或默认值 64 | """ 65 | result = data 66 | for key in keys: 67 | if isinstance(result, dict) and key in result: 68 | result = result[key] 69 | else: 70 | return default 71 | return result 72 | 73 | 74 | def load_config(config_path: str) -> Dict[str, Any]: 75 | """ 76 | 加载YAML配置文件 77 | 78 | Args: 79 | config_path: 配置文件路径 80 | 81 | Returns: 82 | 配置字典 83 | 84 | Raises: 85 | FileNotFoundError: 配置文件不存在 86 | yaml.YAMLError: YAML格式错误 87 | """ 88 | if not os.path.exists(config_path): 89 | raise FileNotFoundError(f"配置文件不存在: {config_path}") 90 | 91 | with open(config_path, 'r', encoding='utf-8') as file: 92 | config = yaml.safe_load(file) 93 | 94 | return config 95 | 96 | 97 | def calculate_funding_diff(bp_funding: float, hl_funding: float) -> Tuple[float, int]: 98 | """ 99 | 计算资金费率差异和差异符号 100 | 101 | 由于Hyperliquid的资金费率是每1小时结算一次,而Backpack是每8小时结算一次, 102 | 需要将Hyperliquid的资金费率乘以8进行标准化比较。 103 | 104 | Args: 105 | bp_funding: Backpack资金费率(8小时结算) 106 | hl_funding: Hyperliquid资金费率(1小时结算) 107 | 108 | Returns: 109 | (资金费率差值, 差值符号(1,-1或0)) 110 | """ 111 | # 将Hyperliquid的资金费率乘以8,以匹配Backpack的8小时周期 112 | adjusted_hl_funding = hl_funding * 8 113 | 114 | # 计算调整后的差异 115 | diff = bp_funding - adjusted_hl_funding 116 | 117 | # 获取差值符号 118 | if diff > 0: 119 | sign = 1 120 | elif diff < 0: 121 | sign = -1 122 | else: 123 | sign = 0 124 | 125 | return abs(diff), sign 126 | 127 | 128 | def get_symbol_from_exchange_symbol(exchange_symbol: str, exchange_type: str) -> Optional[str]: 129 | """ 130 | 从交易所交易对格式获取基础币种 131 | 132 | Args: 133 | exchange_symbol: 交易所格式的交易对,如"BTC_USDC_PERP"或"BTC" 134 | exchange_type: 交易所类型,"backpack"或"hyperliquid" 135 | 136 | Returns: 137 | 基础币种,如"BTC",如果无法转换则返回None 138 | """ 139 | if not exchange_symbol: 140 | return None 141 | 142 | if exchange_type.lower() == "backpack": 143 | # 处理Backpack交易对格式,如"BTC_USDC_PERP" 144 | if "_" in exchange_symbol: 145 | # 打印日志以便调试 146 | print(f"转换Backpack交易对: {exchange_symbol}") 147 | parts = exchange_symbol.split("_") 148 | if len(parts) > 0: 149 | return parts[0] # 返回第一部分,即基础币种 150 | return None 151 | elif exchange_type.lower() == "hyperliquid": 152 | # Hyperliquid直接使用币种作为交易对 153 | return exchange_symbol 154 | 155 | # 默认情况下尝试直接返回 156 | return exchange_symbol 157 | 158 | 159 | def get_hyperliquid_symbol(base_symbol: str) -> str: 160 | """ 161 | 将基础币种名称转换为Hyperliquid交易对格式 162 | 163 | Args: 164 | base_symbol: 基础币种名称,如 "BTC" 165 | 166 | Returns: 167 | Hyperliquid格式的交易对名称,如 "BTC" 168 | """ 169 | return base_symbol 170 | 171 | 172 | def get_backpack_symbol(base_symbol: str) -> str: 173 | """ 174 | 将基础币种名称转换为Backpack交易对格式 175 | 176 | Args: 177 | base_symbol: 基础币种名称,如 "BTC" 178 | 179 | Returns: 180 | Backpack格式的交易对名称,如 "BTC_USDC_PERP" 181 | """ 182 | return f"{base_symbol}_USDC_PERP" 183 | 184 | 185 | def convert_exchange_positions_to_local( 186 | bp_positions: Dict[str, Dict[str, Any]], 187 | hl_positions: Dict[str, Dict[str, Any]] 188 | ) -> Dict[str, Dict[str, Any]]: 189 | """ 190 | 将交易所持仓格式转换为本地持仓格式 191 | 192 | Args: 193 | bp_positions: Backpack持仓字典,格式为{"BTC_USDC_PERP": {"size": 0.001, "side": "BUY"}} 194 | hl_positions: Hyperliquid持仓字典,格式为{"BTC": {"size": 0.001, "side": "BUY"}} 195 | 196 | Returns: 197 | 本地持仓格式字典,格式为{"BTC": {"bp_symbol": "BTC_USDC_PERP", "bp_side": "BUY", ...}} 198 | """ 199 | local_positions = {} 200 | 201 | # 处理Backpack持仓 202 | for bp_symbol, bp_pos in bp_positions.items(): 203 | base_symbol = get_symbol_from_exchange_symbol(bp_symbol, "backpack") 204 | if not base_symbol: 205 | continue 206 | 207 | if base_symbol not in local_positions: 208 | local_positions[base_symbol] = { 209 | "bp_symbol": bp_symbol, 210 | "bp_side": bp_pos["side"], 211 | "bp_size": bp_pos["size"], 212 | "entry_time": datetime.now().isoformat() 213 | } 214 | 215 | # 处理Hyperliquid持仓 216 | for hl_symbol, hl_pos in hl_positions.items(): 217 | base_symbol = get_symbol_from_exchange_symbol(hl_symbol, "hyperliquid") 218 | if not base_symbol: 219 | continue 220 | 221 | if base_symbol in local_positions: 222 | # 如果已经有Backpack持仓,添加Hyperliquid信息 223 | local_positions[base_symbol].update({ 224 | "hl_symbol": hl_symbol, 225 | "hl_side": hl_pos["side"], 226 | "hl_size": hl_pos["size"] 227 | }) 228 | else: 229 | # 如果只有Hyperliquid持仓 230 | local_positions[base_symbol] = { 231 | "hl_symbol": hl_symbol, 232 | "hl_side": hl_pos["side"], 233 | "hl_size": hl_pos["size"], 234 | "entry_time": datetime.now().isoformat() 235 | } 236 | 237 | # 填充缺失的资金费率信息 238 | for symbol, pos in local_positions.items(): 239 | if "entry_bp_funding" not in pos: 240 | pos["entry_bp_funding"] = 0.0 241 | if "entry_hl_funding" not in pos: 242 | pos["entry_hl_funding"] = 0.0 243 | if "entry_funding_diff_sign" not in pos: 244 | diff, sign = calculate_funding_diff(pos["entry_bp_funding"], pos["entry_hl_funding"]) 245 | pos["entry_funding_diff_sign"] = sign 246 | 247 | return local_positions 248 | 249 | 250 | def configure_logging( 251 | logger_name: str, 252 | log_level: str = "INFO", 253 | log_file: Optional[str] = None, 254 | quiet_loggers: List[str] = None 255 | ) -> logging.Logger: 256 | """ 257 | 配置日志记录器 258 | 259 | Args: 260 | logger_name: 日志记录器名称 261 | log_level: 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL) 262 | log_file: 日志文件路径,如果为None则只输出到控制台 263 | quiet_loggers: 需要静音的日志记录器名称列表(将设置为ERROR级别) 264 | 265 | Returns: 266 | 配置好的日志记录器 267 | """ 268 | # 获取日志级别 269 | level = getattr(logging, log_level.upper()) 270 | 271 | # 创建日志记录器 272 | logger = logging.getLogger(logger_name) 273 | logger.setLevel(level) 274 | 275 | # 清除可能已存在的处理器 276 | for handler in logger.handlers[:]: 277 | logger.removeHandler(handler) 278 | 279 | # 配置日志格式 280 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 281 | formatter = logging.Formatter(log_format) 282 | 283 | # 添加控制台处理器 284 | console_handler = logging.StreamHandler() 285 | console_handler.setLevel(level) 286 | console_handler.setFormatter(formatter) 287 | logger.addHandler(console_handler) 288 | 289 | # 如果指定了日志文件,添加文件处理器 290 | if log_file: 291 | # 确保日志目录存在 292 | log_dir = os.path.dirname(log_file) 293 | if log_dir and not os.path.exists(log_dir): 294 | os.makedirs(log_dir) 295 | 296 | file_handler = logging.FileHandler(log_file) 297 | file_handler.setLevel(level) 298 | file_handler.setFormatter(formatter) 299 | logger.addHandler(file_handler) 300 | 301 | # 静音指定的日志记录器(只记录错误) 302 | if quiet_loggers: 303 | for logger_name in quiet_loggers: 304 | logging.getLogger(logger_name).setLevel(logging.ERROR) 305 | 306 | return logger 307 | 308 | 309 | def round_to_tick(value: float, tick_size: float) -> float: 310 | """ 311 | 将值舍入到指定的刻度大小 312 | 313 | Args: 314 | value: 要舍入的值 315 | tick_size: 刻度大小 316 | 317 | Returns: 318 | 舍入后的值 319 | """ 320 | return round(value / tick_size) * tick_size 321 | 322 | 323 | def format_number(value: float, precision: int) -> str: 324 | """ 325 | 将数字格式化为指定精度的字符串 326 | 327 | Args: 328 | value: 要格式化的值 329 | precision: 小数位数 330 | 331 | Returns: 332 | 格式化后的字符串 333 | """ 334 | format_str = f"{{:.{precision}f}}" 335 | return format_str.format(value) -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/display_manager.py.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 显示管理模块 5 | 6 | 管理终端显示,包括价格和资金费率表格,日志信息输出到日志文件而不在终端显示 7 | """ 8 | 9 | import os 10 | import sys 11 | import time 12 | import logging 13 | from typing import Dict, List, Optional 14 | from datetime import datetime 15 | from rich.console import Console 16 | from rich.table import Table 17 | from rich.live import Live 18 | from rich.panel import Panel 19 | from rich.text import Text 20 | from rich import box 21 | 22 | class DisplayManager: 23 | """显示管理类,负责管理终端显示""" 24 | 25 | def __init__(self, logger: Optional[logging.Logger] = None): 26 | """ 27 | 初始化显示管理器 28 | 29 | Args: 30 | logger: 日志记录器 31 | """ 32 | self.logger = logger or logging.getLogger(__name__) 33 | # 使用系统输出文件 34 | self.console = Console(file=sys.__stdout__) 35 | self.current_table = None # 保存当前表格的引用 36 | self.last_update_time = time.time() 37 | self.order_stats = { 38 | "total_orders": 0, 39 | "successful_orders": 0, 40 | "failed_orders": 0, 41 | "last_order_time": None, 42 | "last_order_message": None 43 | } 44 | 45 | # 测试直接输出 46 | print("初始化DisplayManager", file=sys.__stdout__) 47 | 48 | # 创建Live显示上下文 49 | self.live = Live( 50 | console=self.console, 51 | refresh_per_second=4, # 增加刷新率以使表格更新更流畅 52 | auto_refresh=True, 53 | transient=False # 确保表格保持在屏幕上 54 | ) 55 | 56 | def start(self): 57 | """启动显示""" 58 | # 显示开始信息直接使用系统输出 59 | print("资金费率套利机器人已启动,日志信息输出到日志文件", file=sys.__stdout__) 60 | print("按 Ctrl+C 退出", file=sys.__stdout__) 61 | 62 | # 创建初始表格 63 | initial_table = Table(title="正在加载市场数据...", box=box.ROUNDED) 64 | initial_table.add_column("状态", style="bold") 65 | initial_table.add_row("等待数据更新...") 66 | self.current_table = initial_table 67 | 68 | # 启动Live显示 69 | try: 70 | print("开始启动Live显示...", file=sys.__stdout__) 71 | self.live.start(self.current_table) 72 | print("Live显示已启动", file=sys.__stdout__) 73 | except Exception as e: 74 | print(f"启动Live显示出错: {e}", file=sys.__stdout__) 75 | raise 76 | 77 | def stop(self): 78 | """停止显示""" 79 | try: 80 | print("正在停止Live显示...", file=sys.__stdout__) 81 | self.live.stop() 82 | print("Live显示已停止", file=sys.__stdout__) 83 | except Exception as e: 84 | print(f"停止Live显示出错: {e}", file=sys.__stdout__) 85 | 86 | def update_market_data(self, data: Dict[str, Dict]): 87 | """ 88 | 更新市场数据显示 89 | 90 | Args: 91 | data: 市场数据字典 92 | """ 93 | # 创建市场数据表格 94 | table = Table( 95 | title="市场数据", 96 | box=box.ROUNDED, 97 | show_header=True, 98 | header_style="bold white", 99 | title_style="bold white" 100 | ) 101 | 102 | # 添加列 103 | table.add_column("币种", style="cyan", justify="center") 104 | table.add_column("BP价格", style="green", justify="right") 105 | table.add_column("HL价格", style="green", justify="right") 106 | table.add_column("价格差%", style="yellow", justify="right") 107 | table.add_column("BP费率(8h)", style="blue", justify="right") 108 | table.add_column("HL原始(1h)", style="blue", justify="right") 109 | table.add_column("HL调整(8h)", style="blue", justify="right") 110 | table.add_column("费率差%", style="magenta", justify="right") 111 | table.add_column("总滑点%", style="red", justify="right") 112 | table.add_column("BP方向", style="red", justify="center") 113 | table.add_column("HL方向", style="red", justify="center") 114 | 115 | try: 116 | # 计数有效数据 117 | valid_data_count = 0 118 | 119 | # 记录市场数据处理 120 | self.logger.debug(f"市场数据字典键: {list(data.keys())}") 121 | 122 | # 创建数据行列表,稍后按资金费率差的绝对值排序 123 | rows_data = [] 124 | 125 | # 填充数据行 126 | for symbol, symbol_data in data.items(): 127 | bp_data = symbol_data.get("backpack", {}) 128 | hl_data = symbol_data.get("hyperliquid", {}) 129 | 130 | if not isinstance(bp_data, dict): 131 | self.logger.warning(f"BP数据格式错误: {bp_data}") 132 | bp_data = {"price": None, "funding_rate": None} 133 | 134 | if not isinstance(hl_data, dict): 135 | self.logger.warning(f"HL数据格式错误: {hl_data}") 136 | hl_data = {"price": None, "funding_rate": None} 137 | 138 | # 获取价格,确保数据有效 139 | bp_price = bp_data.get("price") 140 | hl_price = hl_data.get("price") 141 | 142 | if bp_price is not None or hl_price is not None: 143 | valid_data_count += 1 144 | 145 | # 计算价格差 146 | if bp_price and hl_price: 147 | price_diff = (bp_price - hl_price) / hl_price * 100 148 | else: 149 | price_diff = 0 150 | 151 | # 计算资金费率差 152 | bp_funding = bp_data.get("funding_rate") 153 | hl_funding = hl_data.get("funding_rate") 154 | adjusted_hl_funding = hl_data.get("adjusted_funding_rate") # 直接使用存储的调整后资金费率 155 | 156 | # 计算调整后的资金费率差 157 | if bp_funding is not None and adjusted_hl_funding is not None: 158 | funding_diff = (bp_funding - adjusted_hl_funding) * 100 159 | else: 160 | funding_diff = 0 161 | 162 | # 计算资金费率差的绝对值用于排序 163 | funding_diff_abs = abs(funding_diff) 164 | 165 | # 获取滑点信息,并记录当前符号所有可用键 166 | self.logger.debug(f"{symbol}的市场数据键: {list(symbol_data.keys())}") 167 | total_slippage = symbol_data.get("total_slippage") 168 | 169 | # 记录滑点获取情况 170 | self.logger.debug(f"获取{symbol}的总滑点: {total_slippage}") 171 | 172 | if total_slippage is None: 173 | # 尝试从流动性分析中获取滑点信息 174 | liquidity_analysis = symbol_data.get("liquidity_analysis", {}) 175 | self.logger.debug(f"{symbol}的流动性分析数据键: {list(liquidity_analysis.keys()) if liquidity_analysis else 'None'}") 176 | 177 | if liquidity_analysis: 178 | # 确定做多和做空的交易所 179 | if bp_funding and adjusted_hl_funding: 180 | long_exchange = "hyperliquid" if bp_funding > adjusted_hl_funding else "backpack" 181 | short_exchange = "backpack" if long_exchange == "hyperliquid" else "hyperliquid" 182 | else: 183 | # 默认设置 184 | long_exchange = "hyperliquid" 185 | short_exchange = "backpack" 186 | 187 | # 提取交易所的流动性分析数据 188 | long_exchange_data = liquidity_analysis.get(long_exchange, {}) 189 | short_exchange_data = liquidity_analysis.get(short_exchange, {}) 190 | 191 | self.logger.debug(f"{symbol}的{long_exchange}流动性分析键: {list(long_exchange_data.keys()) if long_exchange_data else 'None'}") 192 | self.logger.debug(f"{symbol}的{short_exchange}流动性分析键: {list(short_exchange_data.keys()) if short_exchange_data else 'None'}") 193 | 194 | # 提取滑点信息 195 | long_slippage = long_exchange_data.get("bid_slippage_pct", 0) 196 | short_slippage = short_exchange_data.get("ask_slippage_pct", 0) 197 | 198 | # 计算总滑点 199 | if long_slippage is not None and short_slippage is not None: 200 | total_slippage = long_slippage + short_slippage 201 | self.logger.debug(f"{symbol}的总滑点计算: {long_slippage} + {short_slippage} = {total_slippage}") 202 | 203 | # 获取持仓方向信息(如果存在) 204 | bp_position_side = symbol_data.get("bp_position_side", None) 205 | hl_position_side = symbol_data.get("hl_position_side", None) 206 | 207 | # 存储行数据和排序值 208 | row_data = { 209 | "symbol": symbol, 210 | "bp_price": bp_price, 211 | "hl_price": hl_price, 212 | "price_diff": price_diff, 213 | "bp_funding": bp_funding, 214 | "hl_funding": hl_funding, 215 | "adjusted_hl_funding": adjusted_hl_funding, 216 | "funding_diff": funding_diff, 217 | "funding_diff_abs": funding_diff_abs, # 用于排序的绝对值 218 | "total_slippage": total_slippage, 219 | "has_position": symbol_data.get("position"), 220 | "bp_position_side": bp_position_side, 221 | "hl_position_side": hl_position_side 222 | } 223 | rows_data.append(row_data) 224 | 225 | # 按资金费率差的绝对值排序(降序) 226 | sorted_rows = sorted(rows_data, key=lambda x: x["funding_diff_abs"], reverse=True) 227 | 228 | # 将排序后的数据添加到表格 229 | for row in sorted_rows: 230 | table.add_row( 231 | row["symbol"], 232 | f"{row['bp_price']:.2f}" if row['bp_price'] is not None else "N/A", 233 | f"{row['hl_price']:.2f}" if row['hl_price'] is not None else "N/A", 234 | f"{row['price_diff']:+.4f}" if row['bp_price'] and row['hl_price'] else "N/A", 235 | f"{row['bp_funding']:.6f}" if row['bp_funding'] is not None else "0.000000", 236 | f"{row['hl_funding']:.6f}" if row['hl_funding'] is not None else "0.000000", 237 | f"{row['adjusted_hl_funding']:.6f}" if row['adjusted_hl_funding'] is not None else "0.000000", 238 | f"{row['funding_diff']:+.6f}" if row['bp_funding'] is not None and row['adjusted_hl_funding'] is not None else "0.000000", 239 | f"{row['total_slippage']:.4f}" if row['total_slippage'] is not None else "N/A", 240 | "多" if row['bp_position_side'] == "BUY" else "空" if row['bp_position_side'] == "SELL" else "-", 241 | "多" if row['hl_position_side'] == "BUY" else "空" if row['hl_position_side'] == "SELL" else "-" 242 | ) 243 | 244 | # 创建订单统计信息表格 245 | stats_table = Table( 246 | title="订单统计信息", 247 | box=box.ROUNDED, 248 | show_header=False, 249 | title_style="bold white" 250 | ) 251 | 252 | stats_table.add_column("项目", style="cyan") 253 | stats_table.add_column("数值", style="yellow") 254 | 255 | # 添加统计信息 256 | stats_table.add_row("总订单数", str(self.order_stats["total_orders"])) 257 | stats_table.add_row("成功订单", str(self.order_stats["successful_orders"])) 258 | stats_table.add_row("失败订单", str(self.order_stats["failed_orders"])) 259 | 260 | last_time = "无" if not self.order_stats["last_order_time"] else self.order_stats["last_order_time"].strftime("%H:%M:%S") 261 | stats_table.add_row("最近订单时间", last_time) 262 | 263 | last_msg = "无" if not self.order_stats["last_order_message"] else self.order_stats["last_order_message"] 264 | stats_table.add_row("最近订单消息", last_msg[:50] + "..." if last_msg and len(last_msg) > 50 else last_msg) 265 | 266 | # 创建组合布局 267 | main_table = Table.grid(padding=1) 268 | main_table.add_row(table) 269 | main_table.add_row(Panel(stats_table, border_style="blue")) 270 | 271 | # 记录调试信息 272 | now = time.time() 273 | self.last_update_time = now 274 | 275 | # 保存并直接更新表格 276 | self.current_table = main_table 277 | 278 | self.logger.debug(f"更新表格中,包含{valid_data_count}个有效数据") 279 | 280 | # 尝试使用直接的控制台渲染 281 | try: 282 | self.live.update(self.current_table) 283 | self.logger.debug("表格已更新") 284 | except Exception as e: 285 | self.logger.error(f"表格更新出错: {e}") 286 | 287 | # 如果live更新失败,尝试直接渲染 288 | try: 289 | print("\n" + "-" * 80, file=sys.__stdout__) 290 | self.console.print(self.current_table) 291 | print("-" * 80, file=sys.__stdout__) 292 | except Exception as direct_e: 293 | self.logger.error(f"直接渲染表格出错: {direct_e}") 294 | 295 | except Exception as e: 296 | self.logger.error(f"更新表格显示出错: {e}") 297 | # 出错也不中断程序 298 | 299 | def add_order_message(self, message: str): 300 | """ 301 | 添加订单信息 - 只输出到日志而不显示在终端 302 | 303 | Args: 304 | message: 订单信息 305 | """ 306 | try: 307 | # 更新订单统计 308 | self.order_stats["total_orders"] += 1 309 | if "成功" in message or "已完成" in message: 310 | self.order_stats["successful_orders"] += 1 311 | elif "失败" in message or "错误" in message: 312 | self.order_stats["failed_orders"] += 1 313 | 314 | # 更新最近订单信息 315 | self.order_stats["last_order_time"] = datetime.now() 316 | self.order_stats["last_order_message"] = message 317 | 318 | # 记录到日志 319 | self.logger.info(f"订单消息: {message}") 320 | except Exception as e: 321 | self.logger.error(f"处理订单消息时出错: {e}") 322 | # 出错也不中断程序 323 | 324 | def update_order_stats(self, action: str, success: bool): 325 | """ 326 | 根据持仓变化验证结果更新订单统计信息 327 | 328 | Args: 329 | action: 操作类型,"open"表示开仓,"close"表示平仓 330 | success: 是否成功,基于持仓变化验证的结果 331 | """ 332 | try: 333 | # 更新订单统计 334 | self.order_stats["total_orders"] += 1 335 | 336 | if success: 337 | self.order_stats["successful_orders"] += 1 338 | action_desc = "开仓" if action == "open" else "平仓" 339 | self.order_stats["last_order_message"] = f"{action_desc}成功 (持仓变化验证)" 340 | else: 341 | self.order_stats["failed_orders"] += 1 342 | action_desc = "开仓" if action == "open" else "平仓" 343 | self.order_stats["last_order_message"] = f"{action_desc}失败 (持仓变化验证)" 344 | 345 | # 更新最近订单时间 346 | self.order_stats["last_order_time"] = datetime.now() 347 | 348 | # 记录到日志 349 | msg = f"{action_desc}{'成功' if success else '失败'} (持仓变化验证)" 350 | self.logger.info(f"订单统计更新: {msg}") 351 | 352 | except Exception as e: 353 | self.logger.error(f"更新订单统计时出错: {e}") 354 | # 出错也不中断程序 -------------------------------------------------------------------------------- /funding_arbitrage_bot/utils/display_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 显示管理模块 5 | 6 | 管理终端显示,包括价格和资金费率表格,日志信息输出到日志文件而不在终端显示 7 | """ 8 | 9 | import os 10 | import sys 11 | import time 12 | import logging 13 | from typing import Dict, List, Optional 14 | from datetime import datetime 15 | from rich.console import Console 16 | from rich.table import Table 17 | from rich.live import Live 18 | from rich.panel import Panel 19 | from rich.text import Text 20 | from rich import box 21 | 22 | class DisplayManager: 23 | """显示管理类,负责管理终端显示""" 24 | 25 | def __init__(self, logger: Optional[logging.Logger] = None): 26 | """ 27 | 初始化显示管理器 28 | 29 | Args: 30 | logger: 日志记录器 31 | """ 32 | self.logger = logger or logging.getLogger(__name__) 33 | # 使用系统输出文件 34 | self.console = Console(file=sys.__stdout__) 35 | self.current_table = None # 保存当前表格的引用 36 | self.last_update_time = time.time() 37 | self.order_stats = { 38 | "total_orders": 0, 39 | "successful_orders": 0, 40 | "failed_orders": 0, 41 | "last_order_time": None, 42 | "last_order_message": None 43 | } 44 | 45 | # 测试直接输出 46 | print("初始化DisplayManager", file=sys.__stdout__) 47 | 48 | # 创建Live显示上下文 49 | self.live = Live( 50 | console=self.console, 51 | refresh_per_second=4, # 增加刷新率以使表格更新更流畅 52 | auto_refresh=True, 53 | transient=False # 确保表格保持在屏幕上 54 | ) 55 | 56 | def start(self): 57 | """启动显示""" 58 | # 显示开始信息直接使用系统输出 59 | print("资金费率套利机器人已启动,日志信息输出到日志文件", file=sys.__stdout__) 60 | print("按 Ctrl+C 退出", file=sys.__stdout__) 61 | 62 | # 创建初始表格 63 | initial_table = Table(title="正在加载市场数据...", box=box.ROUNDED) 64 | initial_table.add_column("状态", style="bold") 65 | initial_table.add_row("等待数据更新...") 66 | self.current_table = initial_table 67 | 68 | # 启动Live显示 69 | try: 70 | print("开始启动Live显示...", file=sys.__stdout__) 71 | self.live.start(self.current_table) 72 | print("Live显示已启动", file=sys.__stdout__) 73 | except Exception as e: 74 | print(f"启动Live显示出错: {e}", file=sys.__stdout__) 75 | raise 76 | 77 | def stop(self): 78 | """停止显示""" 79 | try: 80 | print("正在停止Live显示...", file=sys.__stdout__) 81 | self.live.stop() 82 | print("Live显示已停止", file=sys.__stdout__) 83 | except Exception as e: 84 | print(f"停止Live显示出错: {e}", file=sys.__stdout__) 85 | 86 | def update_market_data(self, data: Dict[str, Dict]): 87 | """ 88 | 更新市场数据显示 89 | 90 | Args: 91 | data: 市场数据字典 92 | """ 93 | # 创建市场数据表格 94 | table = Table( 95 | title="市场数据", 96 | box=box.ROUNDED, 97 | show_header=True, 98 | header_style="bold white", 99 | title_style="bold white" 100 | ) 101 | 102 | # 添加列 103 | table.add_column("币种", style="cyan", justify="center") 104 | table.add_column("BP价格", style="green", justify="right") 105 | table.add_column("HL价格", style="green", justify="right") 106 | table.add_column("价格差%", style="yellow", justify="right") 107 | table.add_column("BP费率(8h)", style="blue", justify="right") 108 | table.add_column("HL原始(1h)", style="blue", justify="right") 109 | table.add_column("HL调整(8h)", style="blue", justify="right") 110 | table.add_column("费率差%", style="magenta", justify="right") 111 | table.add_column("总滑点%", style="red", justify="right") 112 | table.add_column("BP方向", style="red", justify="center") 113 | table.add_column("HL方向", style="red", justify="center") 114 | table.add_column("盈亏状态", style="bold green", justify="center") # 新增盈亏状态列 115 | table.add_column("开仓时间", style="cyan", justify="center") # 开仓时间列 116 | table.add_column("持仓时长", style="cyan", justify="center") # 新增持仓时长列 117 | 118 | try: 119 | # 计数有效数据 120 | valid_data_count = 0 121 | 122 | # 记录市场数据处理 123 | self.logger.debug(f"市场数据字典键: {list(data.keys())}") 124 | 125 | # 创建数据行列表,稍后按资金费率差的绝对值排序 126 | rows_data = [] 127 | 128 | # 填充数据行 129 | for symbol, symbol_data in data.items(): 130 | bp_data = symbol_data.get("backpack", {}) 131 | hl_data = symbol_data.get("hyperliquid", {}) 132 | 133 | if not isinstance(bp_data, dict): 134 | self.logger.warning(f"BP数据格式错误: {bp_data}") 135 | bp_data = {"price": None, "funding_rate": None} 136 | 137 | if not isinstance(hl_data, dict): 138 | self.logger.warning(f"HL数据格式错误: {hl_data}") 139 | hl_data = {"price": None, "funding_rate": None} 140 | 141 | # 获取价格,确保数据有效 142 | bp_price = bp_data.get("price") 143 | hl_price = hl_data.get("price") 144 | 145 | if bp_price is not None or hl_price is not None: 146 | valid_data_count += 1 147 | 148 | # 计算价格差 149 | if bp_price and hl_price: 150 | price_diff = (bp_price - hl_price) / hl_price * 100 151 | else: 152 | price_diff = 0 153 | 154 | # 计算资金费率差 155 | bp_funding = bp_data.get("funding_rate") 156 | hl_funding = hl_data.get("funding_rate") 157 | adjusted_hl_funding = hl_data.get("adjusted_funding_rate") # 直接使用存储的调整后资金费率 158 | 159 | # 计算调整后的资金费率差 160 | if bp_funding is not None and adjusted_hl_funding is not None: 161 | funding_diff = (bp_funding - adjusted_hl_funding) * 100 162 | else: 163 | funding_diff = 0 164 | 165 | # 计算资金费率差的绝对值用于排序 166 | funding_diff_abs = abs(funding_diff) 167 | 168 | # 获取滑点信息,并记录当前符号所有可用键 169 | self.logger.debug(f"{symbol}的市场数据键: {list(symbol_data.keys())}") 170 | total_slippage = symbol_data.get("total_slippage") 171 | 172 | # 记录滑点获取情况 173 | self.logger.debug(f"获取{symbol}的总滑点: {total_slippage}") 174 | 175 | if total_slippage is None: 176 | # 尝试从流动性分析中获取滑点信息 177 | liquidity_analysis = symbol_data.get("liquidity_analysis", {}) 178 | self.logger.debug(f"{symbol}的流动性分析数据键: {list(liquidity_analysis.keys()) if liquidity_analysis else 'None'}") 179 | 180 | if liquidity_analysis: 181 | # 确定做多和做空的交易所 182 | if bp_funding and adjusted_hl_funding: 183 | long_exchange = "hyperliquid" if bp_funding > adjusted_hl_funding else "backpack" 184 | short_exchange = "backpack" if long_exchange == "hyperliquid" else "hyperliquid" 185 | else: 186 | # 默认设置 187 | long_exchange = "hyperliquid" 188 | short_exchange = "backpack" 189 | 190 | # 提取交易所的流动性分析数据 191 | long_exchange_data = liquidity_analysis.get(long_exchange, {}) 192 | short_exchange_data = liquidity_analysis.get(short_exchange, {}) 193 | 194 | self.logger.debug(f"{symbol}的{long_exchange}流动性分析键: {list(long_exchange_data.keys()) if long_exchange_data else 'None'}") 195 | self.logger.debug(f"{symbol}的{short_exchange}流动性分析键: {list(short_exchange_data.keys()) if short_exchange_data else 'None'}") 196 | 197 | # 提取滑点信息 198 | long_slippage = long_exchange_data.get("bid_slippage_pct", 0) 199 | short_slippage = short_exchange_data.get("ask_slippage_pct", 0) 200 | 201 | # 计算总滑点 202 | if long_slippage is not None and short_slippage is not None: 203 | total_slippage = long_slippage + short_slippage 204 | self.logger.debug(f"{symbol}的总滑点计算: {long_slippage} + {short_slippage} = {total_slippage}") 205 | 206 | # 获取持仓信息(如果存在) 207 | bp_position_side = symbol_data.get("bp_position_side", None) 208 | hl_position_side = symbol_data.get("hl_position_side", None) 209 | 210 | # 获取开仓时间和持仓时长信息(如果存在) 211 | open_time = symbol_data.get("open_time", None) 212 | if open_time: 213 | # 将时间戳转换为可读的时间格式 - 改为日/小时/分钟格式 214 | formatted_open_time = datetime.fromtimestamp(open_time).strftime("%d/%H:%M") 215 | else: 216 | formatted_open_time = "-" 217 | 218 | # 获取持仓时长 219 | position_duration = symbol_data.get("position_duration", "-") 220 | 221 | # 获取盈亏状态(如果存在) 222 | profit_status = symbol_data.get("profit_status", "-") 223 | 224 | # 存储行数据和排序值 225 | row_data = { 226 | "symbol": symbol, 227 | "bp_price": bp_price, 228 | "hl_price": hl_price, 229 | "price_diff": price_diff, 230 | "bp_funding": bp_funding, 231 | "hl_funding": hl_funding, 232 | "adjusted_hl_funding": adjusted_hl_funding, 233 | "funding_diff": funding_diff, 234 | "funding_diff_abs": funding_diff_abs, # 用于排序的绝对值 235 | "total_slippage": total_slippage, 236 | "has_position": symbol_data.get("position"), 237 | "bp_position_side": bp_position_side, 238 | "hl_position_side": hl_position_side, 239 | "direction_consistent": symbol_data.get("direction_consistent", "-"), 240 | "profit_status": profit_status, # 盈亏状态 241 | "open_time": formatted_open_time, # 开仓时间 242 | "position_duration": position_duration # 持仓时长 243 | } 244 | rows_data.append(row_data) 245 | 246 | # 按资金费率差的绝对值排序(降序) 247 | sorted_rows = sorted(rows_data, key=lambda x: x["funding_diff_abs"], reverse=True) 248 | 249 | # 将排序后的数据添加到表格 250 | for row in sorted_rows: 251 | # 设置盈亏状态颜色 252 | if row["profit_status"] == "盈利": 253 | profit_status_style = "green" 254 | elif row["profit_status"] == "亏损": 255 | profit_status_style = "red" 256 | elif row["profit_status"] == "持平": 257 | profit_status_style = "yellow" # 持平状态使用黄色 258 | else: 259 | profit_status_style = "white" 260 | 261 | table.add_row( 262 | row["symbol"], 263 | f"{row['bp_price']:.2f}" if row['bp_price'] is not None else "N/A", 264 | f"{row['hl_price']:.2f}" if row['hl_price'] is not None else "N/A", 265 | f"{row['price_diff']:+.4f}" if row['bp_price'] and row['hl_price'] else "N/A", 266 | f"{row['bp_funding']:.6f}" if row['bp_funding'] is not None else "0.000000", 267 | f"{row['hl_funding']:.6f}" if row['hl_funding'] is not None else "0.000000", 268 | f"{row['adjusted_hl_funding']:.6f}" if row['adjusted_hl_funding'] is not None else "0.000000", 269 | f"{row['funding_diff']:+.6f}" if row['bp_funding'] is not None and row['adjusted_hl_funding'] is not None else "0.000000", 270 | f"{row['total_slippage']:.4f}" if row['total_slippage'] is not None else "N/A", 271 | "多" if row['bp_position_side'] == "BUY" or row['bp_position_side'] == "LONG" else "空" if row['bp_position_side'] == "SELL" or row['bp_position_side'] == "SHORT" else "-", 272 | "多" if row['hl_position_side'] == "LONG" or row['hl_position_side'] == "BUY" else "空" if row['hl_position_side'] == "SHORT" or row['hl_position_side'] == "SELL" else "-", 273 | Text(row["profit_status"], style=profit_status_style), # 盈亏状态带颜色 274 | row["open_time"], # 开仓时间 275 | row["position_duration"] # 持仓时长 276 | ) 277 | 278 | # 创建订单统计信息表格 279 | stats_table = Table( 280 | title="订单统计信息", 281 | box=box.ROUNDED, 282 | show_header=False, 283 | title_style="bold white" 284 | ) 285 | 286 | stats_table.add_column("项目", style="cyan") 287 | stats_table.add_column("数值", style="yellow") 288 | 289 | # 添加统计信息 290 | stats_table.add_row("总订单数", str(self.order_stats["total_orders"])) 291 | stats_table.add_row("成功订单", str(self.order_stats["successful_orders"])) 292 | stats_table.add_row("失败订单", str(self.order_stats["failed_orders"])) 293 | 294 | last_time = "无" if not self.order_stats["last_order_time"] else self.order_stats["last_order_time"].strftime("%H:%M:%S") 295 | stats_table.add_row("最近订单时间", last_time) 296 | 297 | last_msg = "无" if not self.order_stats["last_order_message"] else self.order_stats["last_order_message"] 298 | stats_table.add_row("最近订单消息", last_msg[:50] + "..." if last_msg and len(last_msg) > 50 else last_msg) 299 | 300 | # 创建组合布局 301 | main_table = Table.grid(padding=1) 302 | main_table.add_row(table) 303 | main_table.add_row(Panel(stats_table, border_style="blue")) 304 | 305 | # 记录调试信息 306 | now = time.time() 307 | self.last_update_time = now 308 | 309 | # 保存并直接更新表格 310 | self.current_table = main_table 311 | 312 | self.logger.debug(f"更新表格中,包含{valid_data_count}个有效数据") 313 | 314 | # 尝试使用直接的控制台渲染 315 | try: 316 | self.live.update(self.current_table) 317 | self.logger.debug("表格已更新") 318 | except Exception as e: 319 | self.logger.error(f"表格更新出错: {e}") 320 | 321 | # 如果live更新失败,尝试直接渲染 322 | try: 323 | print("\n" + "-" * 80, file=sys.__stdout__) 324 | self.console.print(self.current_table) 325 | print("-" * 80, file=sys.__stdout__) 326 | except Exception as direct_e: 327 | self.logger.error(f"直接渲染表格出错: {direct_e}") 328 | 329 | except Exception as e: 330 | self.logger.error(f"更新表格显示出错: {e}") 331 | # 出错也不中断程序 332 | 333 | def add_order_message(self, message: str = ""): 334 | """ 335 | 添加订单信息 - 只输出到日志而不显示在终端 336 | 337 | Args: 338 | message: 订单信息, 默认为空字符串 339 | """ 340 | try: 341 | # 确保message是字符串类型 342 | if message is None: 343 | message = "" 344 | 345 | # 确保转换为字符串(防止传入非字符串类型) 346 | message_str = str(message) 347 | 348 | # 更新订单统计 349 | self.order_stats["total_orders"] += 1 350 | if "成功" in message_str or "已完成" in message_str: 351 | self.order_stats["successful_orders"] += 1 352 | elif "失败" in message_str or "错误" in message_str: 353 | self.order_stats["failed_orders"] += 1 354 | 355 | # 更新最近订单信息 356 | self.order_stats["last_order_time"] = datetime.now() 357 | self.order_stats["last_order_message"] = message_str 358 | 359 | # 记录到日志 360 | self.logger.info(f"订单消息: {message_str}") 361 | except Exception as e: 362 | # 记录详细的错误信息 363 | self.logger.error(f"处理订单消息时出错: {e}") 364 | self.logger.error(f"异常类型: {type(e).__name__}, 尝试记录的消息: {message if message is not None else 'None'}") 365 | 366 | # 尝试另一种方式记录 367 | try: 368 | self.logger.info("订单消息: [消息处理出错]") 369 | except: 370 | pass 371 | # 出错也不中断程序 372 | 373 | def update_order_stats(self, action: str, success: bool): 374 | """ 375 | 根据持仓变化验证结果更新订单统计信息 376 | 377 | Args: 378 | action: 操作类型,"open"表示开仓,"close"表示平仓 379 | success: 是否成功,基于持仓变化验证的结果 380 | """ 381 | try: 382 | # 更新订单统计 383 | self.order_stats["total_orders"] += 1 384 | 385 | if success: 386 | self.order_stats["successful_orders"] += 1 387 | action_desc = "开仓" if action == "open" else "平仓" 388 | self.order_stats["last_order_message"] = f"{action_desc}成功 (持仓变化验证)" 389 | else: 390 | self.order_stats["failed_orders"] += 1 391 | action_desc = "开仓" if action == "open" else "平仓" 392 | self.order_stats["last_order_message"] = f"{action_desc}失败 (持仓变化验证)" 393 | 394 | # 更新最近订单时间 395 | self.order_stats["last_order_time"] = datetime.now() 396 | 397 | # 记录到日志 398 | msg = f"{action_desc}{'成功' if success else '失败'} (持仓变化验证)" 399 | self.logger.info(f"订单统计更新: {msg}") 400 | 401 | except Exception as e: 402 | self.logger.error(f"更新订单统计时出错: {e}") 403 | # 出错也不中断程序 404 | 405 | def add_condition_message(self, message: str = ""): 406 | """ 407 | 添加条件检查消息 - 只输出到日志而不显示在终端 408 | 409 | Args: 410 | message: 条件检查消息, 默认为空字符串 411 | """ 412 | try: 413 | # 确保message是字符串类型 414 | if message is None: 415 | message = "" 416 | 417 | # 确保转换为字符串(防止传入非字符串类型) 418 | message_str = str(message) 419 | 420 | # 记录到日志 421 | self.logger.info(f"条件检查: {message_str}") 422 | except Exception as e: 423 | # 记录详细的错误信息 424 | self.logger.error(f"处理条件检查消息时出错: {e}") 425 | self.logger.error(f"异常类型: {type(e).__name__}, 尝试记录的消息: {message if message is not None else 'None'}") 426 | 427 | # 尝试另一种方式记录 428 | try: 429 | self.logger.info("条件检查: [消息处理出错]") 430 | except: 431 | pass 432 | # 出错也不中断程序 433 | 434 | def add_closing_process_message(self, message: str = ""): 435 | """ 436 | 添加平仓过程消息 - 只输出到日志而不显示在终端 437 | 438 | Args: 439 | message: 平仓过程消息, 默认为空字符串 440 | """ 441 | try: 442 | # 确保message是字符串类型 443 | if message is None: 444 | message = "" 445 | 446 | # 确保转换为字符串(防止传入非字符串类型) 447 | message_str = str(message) 448 | 449 | # 记录到日志 450 | self.logger.info(f"平仓过程: {message_str}") 451 | except Exception as e: 452 | # 记录详细的错误信息 453 | self.logger.error(f"处理平仓过程消息时出错: {e}") 454 | self.logger.error(f"异常类型: {type(e).__name__}, 尝试记录的消息: {message if message is not None else 'None'}") 455 | 456 | # 尝试另一种方式记录 457 | try: 458 | self.logger.info("平仓过程: [消息处理出错]") 459 | except: 460 | pass 461 | # 出错也不中断程序 -------------------------------------------------------------------------------- /funding_arbitrage_bot/core/data_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 数据管理模块 5 | 6 | 管理从两个交易所获取的价格和资金费率数据 7 | 集成了日志频率限制和批量摘要处理 8 | """ 9 | 10 | import asyncio 11 | import time 12 | import logging 13 | from typing import Dict, List, Optional, Tuple 14 | from datetime import datetime 15 | 16 | from funding_arbitrage_bot.exchanges.backpack_api import BackpackAPI 17 | from funding_arbitrage_bot.exchanges.hyperliquid_api import HyperliquidAPI 18 | from funding_arbitrage_bot.utils.helpers import get_backpack_symbol, get_hyperliquid_symbol 19 | from funding_arbitrage_bot.utils.log_utilities import RateLimitedLogger, LogSummarizer 20 | 21 | 22 | class DataManager: 23 | """数据管理类,负责获取和管理交易所数据""" 24 | 25 | def __init__( 26 | self, 27 | backpack_api: BackpackAPI, 28 | hyperliquid_api: HyperliquidAPI, 29 | symbols: List[str], 30 | funding_update_interval: int = 60, 31 | logger: Optional[logging.Logger] = None, 32 | log_config: Optional[Dict] = None 33 | ): 34 | """ 35 | 初始化数据管理器 36 | 37 | Args: 38 | backpack_api: Backpack API实例 39 | hyperliquid_api: Hyperliquid API实例 40 | symbols: 需要监控的基础币种列表,如 ["BTC", "ETH", "SOL"] 41 | funding_update_interval: 资金费率更新间隔(秒) 42 | logger: 日志记录器 43 | log_config: 日志配置选项 44 | """ 45 | self.backpack_api = backpack_api 46 | self.hyperliquid_api = hyperliquid_api 47 | self.symbols = symbols 48 | self.funding_update_interval = funding_update_interval 49 | self.logger = logger or logging.getLogger(__name__) 50 | 51 | # 设置日志工具 52 | log_config = log_config or {} 53 | self.rate_logger = RateLimitedLogger(min_interval_seconds=log_config.get("throttling", {})) 54 | self.log_summarizer = LogSummarizer( 55 | logger=self.logger, 56 | interval_seconds=log_config.get("throttling", {}).get("summary_interval", 900) 57 | ) 58 | 59 | # 检查API实例是否有效 60 | self.backpack_available = self.backpack_api is not None 61 | self.hyperliquid_available = self.hyperliquid_api is not None 62 | 63 | if not self.backpack_available: 64 | self.logger.warning("Backpack API实例无效,将无法获取Backpack数据") 65 | 66 | if not self.hyperliquid_available: 67 | self.logger.warning("Hyperliquid API实例无效,将无法获取Hyperliquid数据") 68 | 69 | # 设置Hyperliquid需要获取价格的币种列表 70 | if self.hyperliquid_available: 71 | self.hyperliquid_api.set_price_coins(symbols) 72 | 73 | # 初始化数据存储 74 | self.latest_data = {} 75 | self._init_data_structure() 76 | 77 | # 资金费率更新任务 78 | self.funding_update_task = None 79 | 80 | def _init_data_structure(self): 81 | """初始化数据结构""" 82 | # 初始化币种映射关系 83 | self.backpack_symbols_map = {} 84 | for symbol in self.symbols: 85 | # 对应Backpack的交易对格式 86 | bp_symbol = get_backpack_symbol(symbol) 87 | if bp_symbol not in self.backpack_symbols_map: 88 | self.backpack_symbols_map[symbol] = [bp_symbol] 89 | else: 90 | self.backpack_symbols_map[symbol].append(bp_symbol) 91 | 92 | # 初始化数据存储结构 93 | for symbol in self.symbols: 94 | self.latest_data[symbol] = { 95 | "backpack": { 96 | "symbol": get_backpack_symbol(symbol), 97 | "price": None, 98 | "funding_rate": None, 99 | "price_timestamp": None, 100 | "funding_timestamp": None 101 | }, 102 | "hyperliquid": { 103 | "symbol": get_hyperliquid_symbol(symbol), 104 | "price": None, 105 | "funding_rate": None, 106 | "adjusted_funding_rate": None, 107 | "price_timestamp": None, 108 | "funding_timestamp": None 109 | } 110 | } 111 | 112 | async def start_price_feeds(self): 113 | """启动价格数据流""" 114 | # 启动WebSocket价格流 115 | if self.backpack_available: 116 | await self.backpack_api.start_ws_price_stream() 117 | 118 | if self.hyperliquid_available: 119 | await self.hyperliquid_api.start_ws_price_stream() 120 | 121 | # 启动资金费率定期更新 122 | if not self.funding_update_task: 123 | self.funding_update_task = asyncio.create_task(self._update_funding_rates_loop()) 124 | 125 | async def _update_funding_rates_loop(self): 126 | """资金费率定期更新循环""" 127 | while True: 128 | try: 129 | start_time = time.time() 130 | await self.update_funding_rates() 131 | except Exception as e: 132 | self.logger.error(f"更新资金费率失败: {e}") 133 | self.log_summarizer.record_error("funding_rate_update") 134 | finally: 135 | # 计算已经花费的时间 136 | elapsed = time.time() - start_time 137 | # 确保更新间隔至少为配置的秒数 138 | sleep_time = max(1, self.funding_update_interval - elapsed) 139 | # 记录等待信息(使用频率限制) 140 | self.rate_logger.log( 141 | self.logger, "debug", "funding_wait", 142 | f"等待{sleep_time:.1f}秒后进行下一次资金费率更新" 143 | ) 144 | # 等待下一次更新 145 | await asyncio.sleep(sleep_time) 146 | 147 | async def update_funding_rates(self): 148 | """更新所有交易对的资金费率""" 149 | try: 150 | # 尝试使用批量方式获取Hyperliquid资金费率 151 | if self.hyperliquid_api: 152 | try: 153 | # 检查是否有批量获取方法 154 | if hasattr(self.hyperliquid_api, 'get_all_funding_rates'): 155 | self.logger.info("尝试批量获取Hyperliquid资金费率...") 156 | hl_rates = await self.hyperliquid_api.get_all_funding_rates() 157 | 158 | if hl_rates and len(hl_rates) > 0: # 如果成功获取数据 159 | self.logger.info(f"批量获取Hyperliquid资金费率成功,共{len(hl_rates)}个") 160 | 161 | # 更新数据存储 162 | updated_count = 0 163 | for symbol in self.symbols: 164 | if symbol in hl_rates: 165 | rate = hl_rates[symbol] 166 | self.latest_data[symbol]["hyperliquid"]["funding_rate"] = rate 167 | # 计算调整后的资金费率(乘以8) 168 | self.latest_data[symbol]["hyperliquid"]["adjusted_funding_rate"] = rate * 8 169 | self.latest_data[symbol]["hyperliquid"]["funding_timestamp"] = datetime.now() 170 | updated_count += 1 171 | 172 | # 使用摘要记录器收集资金费率信息 173 | self.log_summarizer.record_funding_update( 174 | symbol, "Hyperliquid", rate * 8 175 | ) 176 | 177 | # 只有当资金费率相对较高时才单独记录 178 | if abs(rate) > 0.0001: 179 | self.rate_logger.log( 180 | self.logger, "debug", f"hl_funding_{symbol}", 181 | f"较高Hyperliquid {symbol}资金费率: {rate}(1h), 调整后: {rate * 8}(8h)" 182 | ) 183 | 184 | self.logger.info(f"成功批量更新 {updated_count}/{len(self.symbols)} 个Hyperliquid币种的资金费率") 185 | else: 186 | self.logger.warning("批量获取Hyperliquid资金费率结果为空,将使用单个请求模式") 187 | else: 188 | self.logger.debug("Hyperliquid API不支持批量获取资金费率,将使用单个请求模式") 189 | 190 | except Exception as e: 191 | self.logger.error(f"批量获取Hyperliquid资金费率失败: {e}") 192 | self.logger.info("将使用单个请求模式获取Hyperliquid资金费率") 193 | 194 | # 尝试使用批量方式获取Backpack资金费率 195 | if self.backpack_api: 196 | try: 197 | # 检查是否有批量获取方法 198 | if hasattr(self.backpack_api, 'get_all_funding_rates'): 199 | self.logger.info("尝试批量获取Backpack资金费率...") 200 | bp_rates = await self.backpack_api.get_all_funding_rates() 201 | 202 | if bp_rates and len(bp_rates) > 0: # 如果成功获取数据 203 | self.logger.info(f"批量获取Backpack资金费率成功,共{len(bp_rates)}个") 204 | 205 | # 更新数据存储 206 | updated_count = 0 207 | for symbol in self.symbols: 208 | # 在Backpack中查找匹配的交易对 209 | bp_symbol = get_backpack_symbol(symbol) 210 | if bp_symbol in bp_rates: 211 | rate = bp_rates[bp_symbol] 212 | self.latest_data[symbol]["backpack"]["funding_rate"] = rate 213 | self.latest_data[symbol]["backpack"]["funding_timestamp"] = datetime.now() 214 | updated_count += 1 215 | 216 | # 使用摘要记录器收集资金费率信息 217 | self.log_summarizer.record_funding_update( 218 | symbol, "Backpack", rate 219 | ) 220 | 221 | # 只有当资金费率相对较高时才单独记录 222 | if abs(rate) > 0.0001: 223 | self.rate_logger.log( 224 | self.logger, "debug", f"bp_funding_{symbol}", 225 | f"较高Backpack {bp_symbol}资金费率: {rate}" 226 | ) 227 | 228 | self.logger.info(f"成功批量更新 {updated_count}/{len(self.symbols)} 个Backpack币种的资金费率") 229 | else: 230 | self.logger.warning("批量获取Backpack资金费率结果为空,将使用单个请求模式") 231 | else: 232 | self.logger.debug("Backpack API不支持批量获取资金费率,将使用单个请求模式") 233 | 234 | except Exception as e: 235 | self.logger.error(f"批量获取Backpack资金费率失败: {e}") 236 | self.logger.info("将使用单个请求模式获取Backpack资金费率") 237 | 238 | # 如果当前没有任何资金费率数据,则使用传统的单个请求获取 239 | need_traditional_update = False 240 | 241 | for symbol in self.symbols: 242 | hl_data = self.latest_data[symbol]["hyperliquid"] 243 | bp_data = self.latest_data[symbol]["backpack"] 244 | 245 | # 检查是否有任何交易所缺少数据 246 | if hl_data["funding_rate"] is None or bp_data["funding_rate"] is None: 247 | need_traditional_update = True 248 | break 249 | 250 | # 如果需要,使用传统的单个请求获取方式(保留原有逻辑) 251 | if need_traditional_update: 252 | self.logger.info("检测到部分币种缺失资金费率数据,将使用单个请求模式补充") 253 | for symbol in self.symbols: 254 | # 尝试从Hyperliquid获取资金费率(针对缺失的数据) 255 | if self.hyperliquid_api and self.latest_data[symbol]["hyperliquid"]["funding_rate"] is None: 256 | try: 257 | funding_rate = await self.hyperliquid_api.get_funding_rate(symbol) 258 | # 更新资金费率 259 | if funding_rate is not None: 260 | self.latest_data[symbol]["hyperliquid"]["funding_rate"] = funding_rate 261 | # 计算调整后的资金费率(乘以8) 262 | self.latest_data[symbol]["hyperliquid"]["adjusted_funding_rate"] = funding_rate * 8 263 | self.latest_data[symbol]["hyperliquid"]["funding_timestamp"] = datetime.now() 264 | 265 | # 使用摘要记录器收集资金费率信息 266 | self.log_summarizer.record_funding_update( 267 | symbol, "Hyperliquid", funding_rate * 8 268 | ) 269 | 270 | # 只有当资金费率相对较高时才单独记录 271 | if abs(funding_rate) > 0.0001: 272 | self.rate_logger.log( 273 | self.logger, "debug", f"hl_funding_{symbol}", 274 | f"较高Hyperliquid {symbol}资金费率: {funding_rate}(1h), 调整后: {funding_rate * 8}(8h)" 275 | ) 276 | except Exception as e: 277 | self.logger.error(f"获取Hyperliquid {symbol}资金费率失败: {e}") 278 | self.log_summarizer.record_error("hl_funding_error") 279 | 280 | # 尝试从Backpack获取资金费率(针对缺失的数据) 281 | if self.backpack_api and self.latest_data[symbol]["backpack"]["funding_rate"] is None: 282 | try: 283 | # 在Backpack中查找匹配的交易对 284 | for backpack_symbol in self.backpack_symbols_map.get(symbol, []): 285 | funding_rate = await self.backpack_api.get_funding_rate(backpack_symbol) 286 | # 更新资金费率 287 | if funding_rate is not None: 288 | self.latest_data[symbol]["backpack"]["funding_rate"] = funding_rate 289 | self.latest_data[symbol]["backpack"]["funding_timestamp"] = datetime.now() 290 | 291 | # 使用摘要记录器收集资金费率信息 292 | self.log_summarizer.record_funding_update( 293 | symbol, "Backpack", funding_rate 294 | ) 295 | 296 | # 只有当资金费率相对较高时才单独记录 297 | if abs(funding_rate) > 0.0001: 298 | self.rate_logger.log( 299 | self.logger, "debug", f"bp_funding_{symbol}", 300 | f"较高Backpack {backpack_symbol}资金费率: {funding_rate}" 301 | ) 302 | except Exception as e: 303 | self.logger.error(f"获取Backpack {symbol}资金费率失败: {e}") 304 | self.log_summarizer.record_error("bp_funding_error") 305 | 306 | # 使用频率限制记录更新完成信息 307 | self.rate_logger.log(self.logger, "info", "funding_complete", "资金费率更新完成") 308 | except Exception as e: 309 | self.logger.error(f"更新资金费率过程中发生错误: {e}") 310 | self.log_summarizer.record_error("funding_update_error") 311 | 312 | async def update_prices(self): 313 | """更新所有交易对的价格数据""" 314 | try: 315 | for symbol in self.symbols: 316 | # 尝试从Hyperliquid获取价格 317 | if self.hyperliquid_api: 318 | try: 319 | price = await self.hyperliquid_api.get_price(symbol) 320 | # 更新价格 321 | if price is not None: 322 | # 检查是否有显著变化 323 | old_price = self.latest_data[symbol]["hyperliquid"]["price"] 324 | self.latest_data[symbol]["hyperliquid"]["price"] = price 325 | self.latest_data[symbol]["hyperliquid"]["price_timestamp"] = datetime.now() 326 | 327 | # 记录到价格摘要收集器 328 | self.log_summarizer.record_price_update( 329 | symbol, "Hyperliquid", old_price, price 330 | ) 331 | 332 | # 只有当价格变化超过1%时才单独记录日志 333 | if old_price is not None and abs((price - old_price) / old_price) > 0.01: 334 | self.rate_logger.log( 335 | self.logger, "debug", f"hl_price_{symbol}", 336 | f"Hyperliquid {symbol}价格显著变化: {old_price:.4f} → {price:.4f} " + 337 | f"({((price-old_price)/old_price*100):.2f}%)" 338 | ) 339 | except Exception as e: 340 | self.logger.error(f"获取Hyperliquid {symbol}价格失败: {e}") 341 | self.log_summarizer.record_error("hl_price_error") 342 | 343 | # 尝试从Backpack获取价格 344 | if self.backpack_api: 345 | try: 346 | # 在Backpack中查找匹配的交易对 347 | for backpack_symbol in self.backpack_symbols_map.get(symbol, []): 348 | price = await self.backpack_api.get_price(backpack_symbol) 349 | # 更新价格 350 | if price is not None: 351 | # 检查是否有显著变化 352 | old_price = self.latest_data[symbol]["backpack"]["price"] 353 | self.latest_data[symbol]["backpack"]["price"] = price 354 | self.latest_data[symbol]["backpack"]["price_timestamp"] = datetime.now() 355 | 356 | # 记录到价格摘要收集器 357 | self.log_summarizer.record_price_update( 358 | symbol, "Backpack", old_price, price 359 | ) 360 | 361 | # 只有当价格变化超过1%时才单独记录日志 362 | if old_price is not None and abs((price - old_price) / old_price) > 0.01: 363 | self.rate_logger.log( 364 | self.logger, "debug", f"bp_price_{symbol}", 365 | f"Backpack {backpack_symbol}价格显著变化: {old_price:.4f} → {price:.4f} " + 366 | f"({((price-old_price)/old_price*100):.2f}%)" 367 | ) 368 | except Exception as e: 369 | self.logger.error(f"获取Backpack {symbol}价格失败: {e}") 370 | self.log_summarizer.record_error("bp_price_error") 371 | except Exception as e: 372 | self.logger.error(f"更新价格过程中发生错误: {e}") 373 | self.log_summarizer.record_error("price_update_error") 374 | 375 | async def get_data(self, symbol: str) -> Dict: 376 | """ 377 | 获取指定币种的最新数据 378 | 379 | Args: 380 | symbol: 基础币种,如 "BTC" 381 | 382 | Returns: 383 | 包含价格和资金费率的数据字典 384 | """ 385 | # 确保价格是最新的 386 | await self.update_prices() 387 | 388 | return self.latest_data.get(symbol, {}) 389 | 390 | async def get_latest_data(self, symbol: str) -> Dict: 391 | """ 392 | 获取指定币种的最新数据(get_data的别名,为了兼容性) 393 | 394 | Args: 395 | symbol: 基础币种,如 "BTC" 396 | 397 | Returns: 398 | 包含价格和资金费率的数据字典 399 | """ 400 | return await self.get_data(symbol) 401 | 402 | def get_all_data(self) -> Dict: 403 | """ 404 | 获取所有币种的最新数据 405 | 406 | Returns: 407 | 所有币种的数据字典 408 | """ 409 | return self.latest_data 410 | 411 | def is_data_valid(self, symbol: str, max_age_seconds: int = 300) -> bool: 412 | """ 413 | 检查数据是否有效(不太旧) 414 | 415 | Args: 416 | symbol: 基础币种,如 "BTC" 417 | max_age_seconds: 数据最大有效期(秒) 418 | 419 | Returns: 420 | 如果数据有效返回True,否则返回False 421 | """ 422 | if symbol not in self.latest_data: 423 | return False 424 | 425 | data = self.latest_data[symbol] 426 | now = datetime.now() 427 | 428 | # 如果某个交易所不可用,则忽略其数据检查 429 | backpack_check = True 430 | hyperliquid_check = True 431 | 432 | # 检查Backpack数据 433 | if self.backpack_available: 434 | bp_data = data["backpack"] 435 | if (bp_data["price"] is None or 436 | bp_data["funding_rate"] is None or 437 | bp_data["price_timestamp"] is None or 438 | bp_data["funding_timestamp"] is None): 439 | backpack_check = False 440 | else: 441 | bp_price_age = (now - bp_data["price_timestamp"]).total_seconds() 442 | bp_funding_age = (now - bp_data["funding_timestamp"]).total_seconds() 443 | 444 | if bp_price_age > max_age_seconds or bp_funding_age > max_age_seconds: 445 | backpack_check = False 446 | 447 | # 检查Hyperliquid数据 448 | if self.hyperliquid_available: 449 | hl_data = data["hyperliquid"] 450 | if (hl_data["price"] is None or 451 | hl_data["funding_rate"] is None or 452 | hl_data["price_timestamp"] is None or 453 | hl_data["funding_timestamp"] is None): 454 | hyperliquid_check = False 455 | else: 456 | hl_price_age = (now - hl_data["price_timestamp"]).total_seconds() 457 | hl_funding_age = (now - hl_data["funding_timestamp"]).total_seconds() 458 | 459 | if hl_price_age > max_age_seconds or hl_funding_age > max_age_seconds: 460 | hyperliquid_check = False 461 | 462 | # 根据可用交易所的数量返回结果 463 | if self.backpack_available and self.hyperliquid_available: 464 | # 两个交易所都可用时,需要两个都有效 465 | return backpack_check and hyperliquid_check 466 | elif self.backpack_available: 467 | # 只有Backpack可用时,只检查Backpack 468 | return backpack_check 469 | elif self.hyperliquid_available: 470 | # 只有Hyperliquid可用时,只检查Hyperliquid 471 | return hyperliquid_check 472 | else: 473 | # 都不可用时返回False 474 | return False 475 | 476 | async def close(self): 477 | """关闭数据管理器""" 478 | # 强制生成最终摘要 479 | self.log_summarizer.force_summary() 480 | 481 | # 取消资金费率更新任务 482 | if self.funding_update_task: 483 | self.funding_update_task.cancel() 484 | try: 485 | await self.funding_update_task 486 | except asyncio.CancelledError: 487 | pass -------------------------------------------------------------------------------- /funding_arbitrage_bot/strategies/funding_arbitrage.py: -------------------------------------------------------------------------------- 1 | import time 2 | import asyncio 3 | import logging 4 | from typing import Dict, List, Any, Optional, Tuple 5 | import pandas as pd 6 | from datetime import datetime, timezone 7 | import numpy as np 8 | import sys 9 | 10 | from funding_arbitrage_bot.exchanges.hyperliquid_api import HyperliquidApi 11 | from funding_arbitrage_bot.exchanges.backpack_api import BackpackApi 12 | 13 | class FundingArbitrageStrategy: 14 | def __init__(self, config: Dict[str, Any], logger: Optional[logging.Logger] = None, display_manager = None): 15 | """ 16 | 资金费率套利策略初始化 17 | 18 | Args: 19 | config: 配置字典 20 | logger: 日志记录器 21 | display_manager: 显示管理器 22 | """ 23 | self.logger = logger or logging.getLogger(__name__) 24 | self.config = config 25 | self.display_manager = display_manager 26 | 27 | # 创建交易所API实例 28 | self.hyperliquid_api = HyperliquidApi( 29 | config=config, 30 | logger=self.logger 31 | ) 32 | 33 | self.backpack_api = BackpackApi( 34 | config=config, 35 | logger=self.logger 36 | ) 37 | 38 | # 提取配置 39 | strategy_config = config.get("strategy", {}) 40 | 41 | # 交易配置参数 42 | self.min_funding_diff = strategy_config.get("min_funding_diff", 0.01) # 最小资金费率差异 43 | self.min_price_diff_pct = strategy_config.get("min_price_diff_pct", 0.001) # 最小价格差异百分比 44 | self.coins_to_monitor = strategy_config.get("coins", ["BTC", "ETH", "SOL"]) # 监控的币种 45 | 46 | # 资金费率套利参数 47 | self.trade_size_usd = strategy_config.get("trade_size_usd", 100) # 每笔交易的美元价值 48 | self.max_position_usd = strategy_config.get("max_position_usd", 500) # 每个币种的最大仓位 49 | self.trade_timeout = strategy_config.get("trade_timeout", 30) # 交易超时时间(秒) 50 | 51 | # 订单深度和滑点设置 52 | self.max_slippage_pct = strategy_config.get("max_slippage_pct", 0.1) # 最大允许滑点百分比 53 | self.min_liquidity_ratio = strategy_config.get("min_liquidity_ratio", 3.0) # 最小流动性比率(交易金额的倍数) 54 | 55 | # 价格更新和检查设置 56 | self.price_check_interval = strategy_config.get("price_check_interval", 5) # 价格检查间隔(秒) 57 | self.arbitrage_check_interval = strategy_config.get("arbitrage_check_interval", 60) # 套利检查间隔(秒) 58 | 59 | # 运行控制 60 | self.is_running = False 61 | self.last_check_time = 0 62 | self.last_trade_time = {} # 币种 -> 上次交易时间 63 | self.trade_cooldown = strategy_config.get("trade_cooldown", 3600) # 交易冷却时间(秒) 64 | 65 | # 执行模式 66 | self.execution_mode = strategy_config.get("execution_mode", "simulate") # simulate, live 67 | 68 | # 记录器 69 | self.trade_history = [] 70 | 71 | # 设置价格监控的币种列表 72 | self.hyperliquid_api.price_coins = self.coins_to_monitor 73 | 74 | # 统计信息 75 | self.stats = { 76 | "checks": 0, 77 | "opportunities_found": 0, 78 | "trades_executed": 0, 79 | "total_profit_usd": 0, 80 | "start_time": time.time() 81 | } 82 | 83 | # 初始化市场数据字典 84 | self.market_data = {} 85 | 86 | async def analyze_liquidity(self, coin: str) -> Dict[str, Any]: 87 | """ 88 | 分析指定币种在两个交易所的流动性情况和可能的滑点 89 | 90 | Args: 91 | coin: 币种名称 92 | 93 | Returns: 94 | 包含两个交易所流动性分析结果的字典 95 | """ 96 | try: 97 | results = {} 98 | 99 | # 获取Hyperliquid订单深度数据并分析 100 | hl_orderbook = await self.hyperliquid_api.get_orderbook(coin) 101 | hl_price = self.hyperliquid_api.prices.get(coin) 102 | hl_analysis = await self._analyze_single_exchange_liquidity( 103 | "hyperliquid", coin, hl_orderbook, hl_price 104 | ) 105 | results["hyperliquid"] = hl_analysis 106 | 107 | # 获取Backpack订单深度数据并分析 108 | bp_orderbook = await self.backpack_api.get_orderbook(coin) 109 | bp_price = await self.backpack_api.get_price(coin) 110 | bp_analysis = await self._analyze_single_exchange_liquidity( 111 | "backpack", coin, bp_orderbook, bp_price 112 | ) 113 | results["backpack"] = bp_analysis 114 | 115 | # 综合评估两个交易所的流动性情况 116 | has_sufficient_liquidity = ( 117 | hl_analysis.get("has_sufficient_liquidity", False) and 118 | bp_analysis.get("has_sufficient_liquidity", False) 119 | ) 120 | 121 | results["combined"] = { 122 | "has_sufficient_liquidity": has_sufficient_liquidity, 123 | "issues": [] 124 | } 125 | 126 | # 记录任何流动性问题 127 | if not hl_analysis.get("has_sufficient_liquidity", False): 128 | results["combined"]["issues"].append( 129 | f"Hyperliquid流动性不足: {hl_analysis.get('error', '未知原因')}" 130 | ) 131 | 132 | if not bp_analysis.get("has_sufficient_liquidity", False): 133 | results["combined"]["issues"].append( 134 | f"Backpack流动性不足: {bp_analysis.get('error', '未知原因')}" 135 | ) 136 | 137 | # 确定做多和做空的交易所 138 | hl_funding = self.funding_rates.get(f"HL_{coin}", 0) 139 | bp_funding = self.funding_rates.get(f"BP_{coin}", 0) 140 | funding_diff = hl_funding - bp_funding if hl_funding is not None and bp_funding is not None else 0 141 | 142 | long_exchange = "hyperliquid" if funding_diff < 0 else "backpack" 143 | short_exchange = "backpack" if funding_diff < 0 else "hyperliquid" 144 | 145 | # 获取滑点信息 146 | long_analysis = results.get(long_exchange, {}) 147 | short_analysis = results.get(short_exchange, {}) 148 | 149 | long_slippage = long_analysis.get("bid_slippage_pct", 0) 150 | short_slippage = short_analysis.get("ask_slippage_pct", 0) 151 | total_slippage = long_slippage + short_slippage 152 | 153 | # 将滑点信息添加到results中 154 | results["long_exchange"] = long_exchange 155 | results["short_exchange"] = short_exchange 156 | results["long_slippage"] = long_slippage 157 | results["short_slippage"] = short_slippage 158 | results["total_slippage"] = total_slippage 159 | 160 | # 将滑点信息添加到市场数据中 161 | if hasattr(self, "market_data") and coin in self.market_data: 162 | self.market_data[coin]["total_slippage"] = total_slippage 163 | self.market_data[coin]["long_slippage"] = long_slippage 164 | self.market_data[coin]["short_slippage"] = short_slippage 165 | self.market_data[coin]["liquidity_analysis"] = results 166 | self.logger.debug(f"在流动性分析中添加{coin}的滑点信息: total_slippage={total_slippage}") 167 | 168 | # 如果有display_manager,立即更新显示 169 | if hasattr(self, "display_manager") and self.display_manager: 170 | self.display_manager.update_market_data(self.market_data) 171 | 172 | return results 173 | 174 | except Exception as e: 175 | self.logger.error(f"分析{coin}流动性出错: {e}") 176 | return { 177 | "combined": { 178 | "has_sufficient_liquidity": False, 179 | "error": f"流动性分析错误: {e}" 180 | } 181 | } 182 | 183 | async def _analyze_single_exchange_liquidity( 184 | self, exchange: str, coin: str, orderbook: Dict, current_price: float 185 | ) -> Dict[str, Any]: 186 | """ 187 | 分析单个交易所的流动性情况 188 | 189 | Args: 190 | exchange: 交易所名称 191 | coin: 币种名称 192 | orderbook: 订单深度数据 193 | current_price: 当前价格 194 | 195 | Returns: 196 | 包含流动性分析结果的字典 197 | """ 198 | if not orderbook or not current_price: 199 | return { 200 | "has_sufficient_liquidity": False, 201 | "error": f"无法获取{exchange}的{coin}订单深度数据或价格" 202 | } 203 | 204 | try: 205 | # 计算买入/卖出所需的金额 206 | trade_size_usd = self.trade_size_usd 207 | trade_size_coin = trade_size_usd / current_price 208 | 209 | # 分析买入深度 210 | bid_liquidity = 0 211 | bid_executed_price = 0 212 | bid_slippage = 0 213 | bid_remaining = 0 214 | 215 | if orderbook["bids"]: 216 | # 计算买入执行价格和滑点 217 | remaining_size = trade_size_coin 218 | weighted_price_sum = 0 219 | 220 | for bid in orderbook["bids"]: 221 | price = float(bid["px"]) 222 | size = float(bid["sz"]) 223 | 224 | if remaining_size <= 0: 225 | break 226 | 227 | executed_size = min(size, remaining_size) 228 | weighted_price_sum += price * executed_size 229 | bid_liquidity += price * size 230 | remaining_size -= executed_size 231 | 232 | if remaining_size <= 0: 233 | bid_executed_price = weighted_price_sum / trade_size_coin 234 | bid_slippage = (current_price - bid_executed_price) / current_price * 100 235 | else: 236 | bid_remaining = remaining_size 237 | 238 | # 分析卖出深度 239 | ask_liquidity = 0 240 | ask_executed_price = 0 241 | ask_slippage = 0 242 | ask_remaining = 0 243 | 244 | if orderbook["asks"]: 245 | # 计算卖出执行价格和滑点 246 | remaining_size = trade_size_coin 247 | weighted_price_sum = 0 248 | 249 | for ask in orderbook["asks"]: 250 | price = float(ask["px"]) 251 | size = float(ask["sz"]) 252 | 253 | if remaining_size <= 0: 254 | break 255 | 256 | executed_size = min(size, remaining_size) 257 | weighted_price_sum += price * executed_size 258 | ask_liquidity += price * size 259 | remaining_size -= executed_size 260 | 261 | if remaining_size <= 0: 262 | ask_executed_price = weighted_price_sum / trade_size_coin 263 | ask_slippage = (ask_executed_price - current_price) / current_price * 100 264 | else: 265 | ask_remaining = remaining_size 266 | 267 | # 判断流动性是否充足 268 | has_sufficient_liquidity = ( 269 | bid_liquidity >= trade_size_usd * self.min_liquidity_ratio and 270 | ask_liquidity >= trade_size_usd * self.min_liquidity_ratio and 271 | bid_slippage <= self.max_slippage_pct and 272 | ask_slippage <= self.max_slippage_pct and 273 | bid_remaining <= 0 and 274 | ask_remaining <= 0 275 | ) 276 | 277 | # 如果流动性不足,确定具体原因 278 | issues = [] 279 | if bid_liquidity < trade_size_usd * self.min_liquidity_ratio: 280 | issues.append(f"买单深度不足 (${bid_liquidity:.2f} < ${trade_size_usd * self.min_liquidity_ratio:.2f})") 281 | 282 | if ask_liquidity < trade_size_usd * self.min_liquidity_ratio: 283 | issues.append(f"卖单深度不足 (${ask_liquidity:.2f} < ${trade_size_usd * self.min_liquidity_ratio:.2f})") 284 | 285 | if bid_slippage > self.max_slippage_pct: 286 | issues.append(f"买入滑点过高 ({bid_slippage:.4f}% > {self.max_slippage_pct:.4f}%)") 287 | 288 | if ask_slippage > self.max_slippage_pct: 289 | issues.append(f"卖出滑点过高 ({ask_slippage:.4f}% > {self.max_slippage_pct:.4f}%)") 290 | 291 | if bid_remaining > 0: 292 | issues.append(f"买单深度不足以完成交易 (剩余{bid_remaining:.6f}{coin})") 293 | 294 | if ask_remaining > 0: 295 | issues.append(f"卖单深度不足以完成交易 (剩余{ask_remaining:.6f}{coin})") 296 | 297 | error = "; ".join(issues) if issues else None 298 | 299 | return { 300 | "has_sufficient_liquidity": has_sufficient_liquidity, 301 | "current_price": current_price, 302 | "trade_size_usd": trade_size_usd, 303 | "trade_size_coin": trade_size_coin, 304 | "bid_liquidity_usd": bid_liquidity, 305 | "ask_liquidity_usd": ask_liquidity, 306 | "bid_executed_price": bid_executed_price, 307 | "ask_executed_price": ask_executed_price, 308 | "bid_slippage_pct": bid_slippage, 309 | "ask_slippage_pct": ask_slippage, 310 | "min_liquidity_ratio": self.min_liquidity_ratio, 311 | "required_liquidity_usd": trade_size_usd * self.min_liquidity_ratio, 312 | "error": error 313 | } 314 | except Exception as e: 315 | return { 316 | "has_sufficient_liquidity": False, 317 | "error": f"分析{exchange}的{coin}流动性时出错: {e}" 318 | } 319 | 320 | async def run(self): 321 | """ 322 | 运行套利策略 323 | """ 324 | self.is_running = True 325 | self.logger.info("套利策略已启动") 326 | 327 | try: 328 | # 初始化市场数据字典 329 | self.market_data = {} 330 | 331 | while self.is_running: 332 | # 检查套利机会 333 | await self.check_for_opportunities() 334 | 335 | # 等待指定时间 336 | await asyncio.sleep(self.price_check_interval) 337 | 338 | except asyncio.CancelledError: 339 | self.logger.info("套利策略已取消") 340 | self.is_running = False 341 | except Exception as e: 342 | self.logger.error(f"套利策略运行出错: {e}") 343 | self.is_running = False 344 | raise 345 | 346 | self.logger.info("套利策略已停止") 347 | 348 | async def update_market_data(self): 349 | """ 350 | 更新市场数据字典,用于显示和记录 351 | """ 352 | try: 353 | # 如果市场数据字典尚未初始化 354 | if not hasattr(self, "market_data"): 355 | self.market_data = {} 356 | 357 | # 更新每个币种的市场数据 358 | for coin in self.coins_to_monitor: 359 | if coin not in self.market_data: 360 | self.market_data[coin] = {} 361 | 362 | # 获取Hyperliquid数据 363 | hl_price = self.hyperliquid_api.prices.get(coin) 364 | hl_funding_rate = self.funding_rates.get(f"HL_{coin}") 365 | 366 | if hl_price: 367 | if "hyperliquid" not in self.market_data[coin]: 368 | self.market_data[coin]["hyperliquid"] = {} 369 | 370 | self.market_data[coin]["hyperliquid"]["price"] = hl_price 371 | 372 | if hl_funding_rate is not None: 373 | if "hyperliquid" not in self.market_data[coin]: 374 | self.market_data[coin]["hyperliquid"] = {} 375 | 376 | self.market_data[coin]["hyperliquid"]["funding_rate"] = hl_funding_rate 377 | # 调整为8小时资金费率,方便与BP比较 378 | self.market_data[coin]["hyperliquid"]["adjusted_funding_rate"] = hl_funding_rate * 8 379 | 380 | # 获取Backpack数据 381 | bp_price = await self.backpack_api.get_price(coin) 382 | bp_funding_rate = self.funding_rates.get(f"BP_{coin}") 383 | 384 | if bp_price: 385 | if "backpack" not in self.market_data[coin]: 386 | self.market_data[coin]["backpack"] = {} 387 | 388 | self.market_data[coin]["backpack"]["price"] = bp_price 389 | 390 | if bp_funding_rate is not None: 391 | if "backpack" not in self.market_data[coin]: 392 | self.market_data[coin]["backpack"] = {} 393 | 394 | self.market_data[coin]["backpack"]["funding_rate"] = bp_funding_rate 395 | 396 | # 更新DisplayManager显示 397 | if self.display_manager: 398 | self.display_manager.update_market_data(self.market_data) 399 | 400 | return self.market_data 401 | 402 | except Exception as e: 403 | self.logger.error(f"更新市场数据出错: {e}") 404 | return {} 405 | 406 | async def check_for_opportunities(self): 407 | """ 408 | 检查所有交易对的套利机会 409 | 扩展版本: 检查资金费率、价格差异、流动性情况和滑点控制 410 | """ 411 | current_time = time.time() 412 | 413 | # 避免频繁检查 414 | if current_time - self.last_check_time < self.arbitrage_check_interval: 415 | return 416 | 417 | self.last_check_time = current_time 418 | self.stats["checks"] += 1 419 | 420 | # 获取资金费率信息 421 | try: 422 | await self.update_funding_rates() 423 | 424 | # 更新市场数据 425 | await self.update_market_data() 426 | 427 | # 检查每个币种的套利机会 428 | for coin in self.coins_to_monitor: 429 | # 检查是否在冷却期 430 | if coin in self.last_trade_time: 431 | time_since_last_trade = current_time - self.last_trade_time[coin] 432 | if time_since_last_trade < self.trade_cooldown: 433 | cooldown_left = self.trade_cooldown - time_since_last_trade 434 | self.logger.debug(f"{coin}仍在交易冷却期 (剩余{cooldown_left:.1f}秒)") 435 | continue 436 | 437 | # 获取资金费率 438 | hl_funding_rate = self.funding_rates.get(f"HL_{coin}", 0) 439 | bp_funding_rate = self.funding_rates.get(f"BP_{coin}", 0) 440 | 441 | if hl_funding_rate is None or bp_funding_rate is None: 442 | self.logger.warning(f"无法获取{coin}的完整资金费率") 443 | continue 444 | 445 | # 计算资金费率差异 446 | funding_diff = hl_funding_rate - bp_funding_rate 447 | abs_funding_diff = abs(funding_diff) 448 | 449 | # 获取两个交易所的价格 450 | hl_price = self.hyperliquid_api.prices.get(coin) 451 | bp_price = await self.backpack_api.get_price(coin) 452 | 453 | if not hl_price or not bp_price: 454 | self.logger.warning(f"无法获取{coin}的完整价格信息") 455 | continue 456 | 457 | # 计算价格差异 458 | price_diff_pct = abs(hl_price - bp_price) / min(hl_price, bp_price) 459 | 460 | # 分析两个交易所的流动性情况(提前获取滑点信息用于日志记录) 461 | liquidity_analysis = await self.analyze_liquidity(coin) 462 | combined_analysis = liquidity_analysis.get("combined", {}) 463 | 464 | # 确定做多和做空的交易所 465 | long_exchange = "hyperliquid" if funding_diff < 0 else "backpack" 466 | short_exchange = "backpack" if funding_diff < 0 else "hyperliquid" 467 | 468 | # 获取相应的滑点信息 469 | long_analysis = liquidity_analysis.get(long_exchange, {}) 470 | short_analysis = liquidity_analysis.get(short_exchange, {}) 471 | 472 | long_slippage = long_analysis.get("bid_slippage_pct", 0) 473 | short_slippage = short_analysis.get("ask_slippage_pct", 0) 474 | total_slippage = long_slippage + short_slippage 475 | 476 | # 判断滑点是否在允许范围内 477 | slippage_ok = total_slippage <= self.max_slippage_pct * 2 478 | 479 | # 添加INFO级别的滑点信息日志,确保在INFO级别也可以看到 480 | self.logger.info( 481 | f"{coin} - 滑点分析: 总滑点={total_slippage:.4f}%, " 482 | f"做多交易所({long_exchange})滑点={long_slippage:.4f}%, " 483 | f"做空交易所({short_exchange})滑点={short_slippage:.4f}%, " 484 | f"是否符合条件: {'' if slippage_ok else '不'}符合" 485 | ) 486 | 487 | # 将滑点信息添加到市场数据中,用于显示 488 | if coin in self.market_data: 489 | self.market_data[coin]["total_slippage"] = total_slippage 490 | self.market_data[coin]["long_slippage"] = long_slippage 491 | self.market_data[coin]["short_slippage"] = short_slippage 492 | self.market_data[coin]["liquidity_analysis"] = liquidity_analysis 493 | self.logger.debug(f"已将{coin}的滑点信息添加到市场数据: total_slippage={total_slippage}") 494 | 495 | # 记录基本信息(添加滑点信息) 496 | self.logger.debug( 497 | f"{coin} - 资金费率差: {funding_diff:.6f} " 498 | f"(HL: {hl_funding_rate:.6f}, BP: {bp_funding_rate:.6f}), " 499 | f"价格差: {price_diff_pct:.4%} " 500 | f"(HL: {hl_price:.2f}, BP: {bp_price:.2f}), " 501 | f"滑点: {long_exchange}买入{long_slippage:.4f}%, " 502 | f"{short_exchange}卖出{short_slippage:.4f}%, " 503 | f"总滑点: {total_slippage:.4f}% " 504 | f"[{'' if slippage_ok else '不'}符合条件: {self.max_slippage_pct * 2:.4f}%]" 505 | ) 506 | 507 | # 资金费率和价格差异条件检查 508 | funding_ok = abs_funding_diff >= self.min_funding_diff 509 | price_ok = price_diff_pct <= self.min_price_diff_pct 510 | 511 | self.logger.debug( 512 | f"{coin} - 条件检查: " 513 | f"资金费率差: {abs_funding_diff:.6f}% >= {self.min_funding_diff:.6f}% [{'' if funding_ok else '不'}符合], " 514 | f"价格差: {price_diff_pct:.4%} <= {self.min_price_diff_pct:.4%} [{'' if price_ok else '不'}符合], " 515 | f"滑点: {total_slippage:.4f}% <= {self.max_slippage_pct * 2:.4f}% [{'' if slippage_ok else '不'}符合]" 516 | ) 517 | 518 | # 检查是否满足套利条件 519 | if funding_ok and price_ok: 520 | # 检查流动性是否充足 521 | if not combined_analysis.get("has_sufficient_liquidity", False): 522 | issues = combined_analysis.get("issues", []) 523 | issues_text = "; ".join(issues) if issues else "未知流动性问题" 524 | self.logger.info(f"{coin}套利机会 - 但{issues_text}") 525 | 526 | # 记录详细流动性分析结果 527 | if self.logger.level <= logging.DEBUG: 528 | self.logger.debug(f"Hyperliquid流动性分析: {liquidity_analysis.get('hyperliquid', {})}") 529 | self.logger.debug(f"Backpack流动性分析: {liquidity_analysis.get('backpack', {})}") 530 | continue 531 | 532 | # 检查滑点是否在可接受范围内 533 | if not slippage_ok: 534 | self.logger.info( 535 | f"{coin}套利机会 - 但总滑点过高: {total_slippage:.4f}% > {self.max_slippage_pct * 2:.4f}%, " 536 | f"({long_exchange}买入{long_slippage:.4f}%, {short_exchange}卖出{short_slippage:.4f}%)" 537 | ) 538 | continue 539 | 540 | # 检查预期收益是否能覆盖滑点成本 541 | expected_daily_return = abs(funding_diff) * self.trade_size_usd 542 | slippage_cost = (total_slippage / 100) * self.trade_size_usd 543 | profit_cover_slippage = slippage_cost <= expected_daily_return * 0.5 544 | 545 | self.logger.debug( 546 | f"{coin} - 收益分析: " 547 | f"预期日收益: ${expected_daily_return:.2f}, " 548 | f"滑点成本: ${slippage_cost:.2f}, " 549 | f"成本占比: {slippage_cost/expected_daily_return*100:.2f}% <= 50% [{'' if profit_cover_slippage else '不'}符合]" 550 | ) 551 | 552 | if not profit_cover_slippage: 553 | self.logger.info( 554 | f"{coin}套利机会 - 但滑点成本过高: ${slippage_cost:.2f} > ${expected_daily_return * 0.5:.2f} " 555 | f"(预期日收益的50%)" 556 | ) 557 | continue 558 | 559 | self.stats["opportunities_found"] += 1 560 | 561 | # 记录套利机会详情 562 | self.logger.info( 563 | f"发现{coin}套利机会! 资金费率差: {funding_diff:.6f}, " 564 | f"价格差: {price_diff_pct:.4%}, " 565 | f"预计滑点: {long_exchange}买入{long_slippage:.4f}%, " 566 | f"{short_exchange}卖出{short_slippage:.4f}%, " 567 | f"总滑点: {total_slippage:.4f}% [符合条件: <={self.max_slippage_pct * 2:.4f}%], " 568 | f"预期收益: ${expected_daily_return:.2f}/天, 滑点成本: ${slippage_cost:.2f} " 569 | f"[符合条件: <={expected_daily_return * 0.5:.2f}]" 570 | ) 571 | 572 | # 执行套利 573 | if self.execution_mode == "live": 574 | await self.execute_arbitrage(coin, funding_diff, liquidity_analysis) 575 | 576 | # 在所有币种检查完成后,再次更新市场数据以确保滑点信息显示在终端表格中 577 | if self.display_manager: 578 | self.display_manager.update_market_data(self.market_data) 579 | 580 | except Exception as e: 581 | self.logger.error(f"检查套利机会时出错: {e}") 582 | # 即使出错也尝试更新显示 583 | if self.display_manager: 584 | try: 585 | self.display_manager.update_market_data(self.market_data) 586 | except Exception as display_e: 587 | self.logger.error(f"更新显示时出错: {display_e}") 588 | 589 | async def execute_arbitrage(self, coin: str, funding_diff: float, liquidity_analysis: Dict[str, Any]): 590 | """ 591 | 执行套利交易 592 | 593 | Args: 594 | coin: 币种 595 | funding_diff: 资金费率差异 596 | liquidity_analysis: 流动性分析结果 597 | """ 598 | try: 599 | # 记录交易时间 600 | self.last_trade_time[coin] = time.time() 601 | 602 | # 确定交易方向 603 | long_exchange = "hyperliquid" if funding_diff < 0 else "backpack" 604 | short_exchange = "backpack" if funding_diff < 0 else "hyperliquid" 605 | 606 | # 获取相应交易所的分析结果 607 | long_analysis = liquidity_analysis.get(long_exchange, {}) 608 | short_analysis = liquidity_analysis.get(short_exchange, {}) 609 | 610 | trade_size_usd = self.trade_size_usd 611 | long_price = long_analysis.get("current_price", 0) 612 | short_price = short_analysis.get("current_price", 0) 613 | 614 | # 获取滑点信息 615 | long_slippage = long_analysis.get("bid_slippage_pct", 0) 616 | short_slippage = short_analysis.get("ask_slippage_pct", 0) 617 | 618 | self.logger.info( 619 | f"执行{coin}套利: 在{long_exchange}做多, 在{short_exchange}做空, " 620 | f"交易金额: ${trade_size_usd:.2f}, " 621 | f"价格: {long_exchange}${long_price:.2f}, {short_exchange}${short_price:.2f}" 622 | ) 623 | 624 | # 计算预期收益 625 | expected_daily_return = abs(funding_diff) * trade_size_usd 626 | expected_return_per_hour = expected_daily_return / 24 627 | 628 | # 考虑滑点的净收益 629 | slippage_cost = (long_slippage + short_slippage) / 100 * trade_size_usd 630 | net_expected_daily_return = expected_daily_return - slippage_cost 631 | 632 | self.logger.info( 633 | f"预期收益: 每天${expected_daily_return:.2f}(扣除滑点后${net_expected_daily_return:.2f}), " 634 | f"每小时${expected_return_per_hour:.2f}" 635 | ) 636 | 637 | # 在这里添加实际交易代码 638 | # ... 639 | 640 | # 记录交易 641 | trade_record = { 642 | "timestamp": time.time(), 643 | "coin": coin, 644 | "funding_diff": funding_diff, 645 | "long_exchange": long_exchange, 646 | "short_exchange": short_exchange, 647 | "trade_size_usd": trade_size_usd, 648 | "long_price": long_price, 649 | "short_price": short_price, 650 | "expected_daily_return": expected_daily_return, 651 | "slippage_cost": slippage_cost, 652 | "net_expected_daily_return": net_expected_daily_return, 653 | "slippage_long": long_slippage, 654 | "slippage_short": short_slippage 655 | } 656 | 657 | self.trade_history.append(trade_record) 658 | self.stats["trades_executed"] += 1 659 | self.stats["total_profit_usd"] += net_expected_daily_return 660 | 661 | self.logger.info(f"{coin}套利交易已执行") 662 | 663 | except Exception as e: 664 | self.logger.error(f"执行{coin}套利时出错: {e}") 665 | 666 | async def update_funding_rates(self): 667 | """ 668 | 更新所有监控币种的资金费率 669 | """ 670 | try: 671 | # 初始化资金费率字典 672 | if not hasattr(self, "funding_rates"): 673 | self.funding_rates = {} 674 | 675 | # 获取Hyperliquid资金费率 676 | hl_funding_rates = await self.hyperliquid_api.get_funding_rates() 677 | if hl_funding_rates: 678 | for coin, rate in hl_funding_rates.items(): 679 | self.funding_rates[f"HL_{coin}"] = rate 680 | 681 | # 获取Backpack资金费率 682 | bp_funding_rates = await self.backpack_api.get_funding_rates() 683 | if bp_funding_rates: 684 | for coin, rate in bp_funding_rates.items(): 685 | self.funding_rates[f"BP_{coin}"] = rate 686 | 687 | # 记录资金费率更新 688 | funding_info = [] 689 | for coin in self.coins_to_monitor: 690 | hl_rate = self.funding_rates.get(f"HL_{coin}") 691 | bp_rate = self.funding_rates.get(f"BP_{coin}") 692 | 693 | if hl_rate is not None and bp_rate is not None: 694 | diff = hl_rate - bp_rate 695 | funding_info.append(f"{coin}: HL={hl_rate:.6f}, BP={bp_rate:.6f}, 差={diff:.6f}") 696 | 697 | if funding_info: 698 | self.logger.debug(f"资金费率更新: {'; '.join(funding_info)}") 699 | 700 | except Exception as e: 701 | self.logger.error(f"更新资金费率时出错: {e}") 702 | 703 | def stop(self): 704 | """停止策略运行""" 705 | self.is_running = False 706 | self.logger.info("正在停止套利策略...") 707 | 708 | async def get_statistics(self): 709 | """获取策略运行统计信息""" 710 | stats = self.stats.copy() 711 | 712 | # 计算运行时间 713 | stats["run_time"] = time.time() - stats["start_time"] 714 | 715 | # 格式化数字 716 | stats["run_time_formatted"] = f"{stats['run_time'] / 60:.1f}分钟" 717 | stats["profit_per_check"] = stats["total_profit_usd"] / max(stats["checks"], 1) 718 | 719 | # 计算机会转化率 720 | if stats["opportunities_found"] > 0: 721 | stats["conversion_rate"] = stats["trades_executed"] / stats["opportunities_found"] 722 | else: 723 | stats["conversion_rate"] = 0 724 | 725 | # 记录资金费率信息 726 | stats["funding_rates"] = {} 727 | for coin in self.coins_to_monitor: 728 | hl_rate = self.funding_rates.get(f"HL_{coin}") 729 | bp_rate = self.funding_rates.get(f"BP_{coin}") 730 | 731 | if hl_rate is not None and bp_rate is not None: 732 | stats["funding_rates"][coin] = { 733 | "hyperliquid": hl_rate, 734 | "backpack": bp_rate, 735 | "diff": hl_rate - bp_rate 736 | } 737 | 738 | return stats --------------------------------------------------------------------------------