├── .gitignore ├── .env.example ├── cli ├── __init__.py └── commands.py ├── api ├── __init__.py ├── auth.py └── client.py ├── utils ├── __init__.py ├── helpers.py └── grid_helper.py ├── .gitattributes ├── database ├── __init__.py └── db.py ├── requirements.txt ├── ws_client ├── __init__.py └── client.py ├── strategies ├── __init__.py └── grid_trader.py ├── config.py ├── logger.py ├── main.py ├── README.md └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.log 3 | *.pyc 4 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_KEY= 2 | SECRET_KEY= 3 | PROXY_WEBSOCKET= -------------------------------------------------------------------------------- /cli/__init__.py: -------------------------------------------------------------------------------- 1 | # cli/__init__.py 2 | """ 3 | CLI 模塊,提供命令行界面 4 | """ -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | # api/__init__.py 2 | """ 3 | API 模塊,負責與交易所API的通訊 4 | """ -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # utils/__init__.py 2 | """ 3 | Utils 模塊,提供各種輔助工具函數 4 | """ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | # database/__init__.py 2 | """ 3 | Database 模塊,負責數據存儲與檢索 4 | """ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyNaCl 2 | requests 3 | websocket-client 4 | numpy 5 | python-dotenv -------------------------------------------------------------------------------- /ws_client/__init__.py: -------------------------------------------------------------------------------- 1 | # ws_client/__init__.py 2 | """ 3 | WebSocket 模塊,負責實時數據處理 4 | """ -------------------------------------------------------------------------------- /strategies/__init__.py: -------------------------------------------------------------------------------- 1 | # strategies/__init__.py 2 | """ 3 | Strategies 模塊,包含各種交易策略 4 | """ -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置文件 3 | """ 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | # 載入環境變數 8 | load_dotenv() 9 | 10 | # API配置 11 | API_KEY = os.getenv('API_KEY') 12 | SECRET_KEY = os.getenv('SECRET_KEY') 13 | WS_PROXY = os.getenv('PROXY_WEBSOCKET') 14 | API_URL = "https://api.backpack.exchange" 15 | WS_URL = "wss://ws.backpack.exchange" 16 | API_VERSION = "v1" 17 | DEFAULT_WINDOW = "5000" 18 | 19 | # 數據庫配置 20 | DB_PATH = 'orders.db' 21 | 22 | # 日誌配置 23 | LOG_FILE = "grid_trader.log" -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 日誌配置模塊 3 | """ 4 | import logging 5 | import sys 6 | from config import LOG_FILE 7 | 8 | def setup_logger(name="grid_trader"): 9 | """ 10 | 設置並返回一個配置好的logger實例 11 | """ 12 | logger = logging.getLogger(name) 13 | 14 | # 防止重複配置 15 | if logger.handlers: 16 | return logger 17 | 18 | logger.setLevel(logging.INFO) 19 | 20 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 21 | 22 | # 文件處理器 23 | file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8') 24 | file_handler.setFormatter(formatter) 25 | 26 | # 控制台處理器 27 | console_handler = logging.StreamHandler(sys.stdout) 28 | console_handler.setFormatter(formatter) 29 | 30 | # 添加處理器 31 | logger.addHandler(file_handler) 32 | logger.addHandler(console_handler) 33 | 34 | return logger -------------------------------------------------------------------------------- /api/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | API認證和簽名相關模塊 3 | """ 4 | import base64 5 | import nacl.signing 6 | import sys 7 | from typing import Optional 8 | from logger import setup_logger 9 | 10 | logger = setup_logger("api.auth") 11 | 12 | def create_signature(secret_key: str, message: str) -> Optional[str]: 13 | """ 14 | 創建API簽名 15 | 16 | Args: 17 | secret_key: API密鑰 18 | message: 要簽名的消息 19 | 20 | Returns: 21 | 簽名字符串或None(如果簽名失敗) 22 | """ 23 | try: 24 | # 嘗試對密鑰進行解碼和簽名 25 | decoded_key = base64.b64decode(secret_key) 26 | signing_key = nacl.signing.SigningKey(decoded_key) 27 | signature = signing_key.sign(message.encode('utf-8')).signature 28 | return base64.b64encode(signature).decode('utf-8') 29 | except Exception as e: 30 | logger.error(f"簽名創建失敗: {e}") 31 | logger.error("無法創建API簽名,程序將終止") 32 | # 強制終止程序 33 | sys.exit(1) -------------------------------------------------------------------------------- /utils/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | 輔助函數模塊 3 | """ 4 | import math 5 | import numpy as np 6 | from typing import List, Union, Optional 7 | 8 | def round_to_precision(value: float, precision: int) -> float: 9 | """ 10 | 根據精度四捨五入數字 11 | 12 | Args: 13 | value: 要四捨五入的數值 14 | precision: 小數點精度 15 | 16 | Returns: 17 | 四捨五入後的數值 18 | """ 19 | factor = 10 ** precision 20 | return math.floor(value * factor) / factor 21 | 22 | def round_to_tick_size(price: float, tick_size: float) -> float: 23 | """ 24 | 根據tick_size四捨五入價格 25 | 26 | Args: 27 | price: 原始價格 28 | tick_size: 價格步長 29 | 30 | Returns: 31 | 調整後的價格 32 | """ 33 | tick_size_float = float(tick_size) 34 | tick_size_str = format(tick_size_float, 'f').rstrip('0') 35 | precision = len(tick_size_str.split('.')[-1]) if '.' in tick_size_str else 0 36 | rounded_price = round(price / tick_size_float) * tick_size_float 37 | return round(rounded_price, precision) 38 | 39 | def calculate_volatility(prices: List[float], window: int = 20) -> float: 40 | """ 41 | 計算波動率 42 | 43 | Args: 44 | prices: 價格列表 45 | window: 計算窗口大小 46 | 47 | Returns: 48 | 波動率百分比 49 | """ 50 | if len(prices) < window: 51 | return 0 52 | 53 | # 使用最近N個價格計算標準差 54 | recent_prices = prices[-window:] 55 | returns = np.diff(recent_prices) / recent_prices[:-1] 56 | return np.std(returns) * 100 # 轉換為百分比 -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Backpack Exchange 網格交易程序主執行文件 4 | """ 5 | import argparse 6 | import sys 7 | import os 8 | 9 | from logger import setup_logger 10 | from config import API_KEY, SECRET_KEY, WS_PROXY 11 | from cli.commands import main_cli 12 | from strategies.grid_trader import GridTrader 13 | 14 | logger = setup_logger("main") 15 | 16 | def parse_arguments(): 17 | """解析命令行參數""" 18 | parser = argparse.ArgumentParser(description='Backpack Exchange 網格交易程序') 19 | 20 | # 基本參數 21 | parser.add_argument('--api-key', type=str, help='API Key (可選,默認使用環境變數或配置文件)') 22 | parser.add_argument('--secret-key', type=str, help='Secret Key (可選,默認使用環境變數或配置文件)') 23 | parser.add_argument('--cli', action='store_true', help='啟動命令行界面') 24 | parser.add_argument('--ws-proxy', type=str, help='WebSocket Proxy (可選,默認使用環境變數或配置文件)') 25 | 26 | # 網格交易參數 27 | parser.add_argument('--symbol', type=str, help='交易對 (例如: SOL_USDC)') 28 | parser.add_argument('--grid-upper', type=float, help='網格上限價格') 29 | parser.add_argument('--grid-lower', type=float, help='網格下限價格') 30 | parser.add_argument('--grid-num', type=int, default=10, help='網格數量 (默認: 10)') 31 | parser.add_argument('--quantity', type=float, help='訂單數量 (可選)') 32 | parser.add_argument('--auto-price', action='store_true', help='自動設置價格範圍') 33 | parser.add_argument('--price-range', type=float, default=5.0, help='自動模式下的價格範圍百分比 (默認: 5.0)') 34 | parser.add_argument('--duration', type=int, default=3600, help='運行時間(秒)(默認: 3600)') 35 | parser.add_argument('--interval', type=int, default=60, help='更新間隔(秒)(默認: 60)') 36 | 37 | return parser.parse_args() 38 | 39 | def run_grid_trader(args, api_key, secret_key, ws_proxy=None): 40 | """運行網格交易策略""" 41 | # 檢查必要參數 42 | if not args.symbol: 43 | logger.error("缺少交易對參數 (--symbol)") 44 | return 45 | 46 | # 檢查網格參數 47 | if not args.auto_price and (args.grid_upper is None or args.grid_lower is None): 48 | logger.error("使用手動網格模式時需要指定網格上下限價格 (--grid-upper 和 --grid-lower)") 49 | return 50 | 51 | try: 52 | # 初始化網格交易 53 | grid_trader = GridTrader( 54 | api_key=api_key, 55 | secret_key=secret_key, 56 | symbol=args.symbol, 57 | grid_upper_price=args.grid_upper, 58 | grid_lower_price=args.grid_lower, 59 | grid_num=args.grid_num, 60 | order_quantity=args.quantity, 61 | auto_price_range=args.auto_price, 62 | price_range_percent=args.price_range, 63 | ws_proxy=ws_proxy 64 | ) 65 | 66 | # 執行網格交易策略 67 | grid_trader.run(duration_seconds=args.duration, interval_seconds=args.interval) 68 | 69 | except KeyboardInterrupt: 70 | logger.info("收到中斷信號,正在退出...") 71 | except Exception as e: 72 | logger.error(f"網格交易過程中發生錯誤: {e}") 73 | import traceback 74 | traceback.print_exc() 75 | 76 | def main(): 77 | """主函數""" 78 | args = parse_arguments() 79 | 80 | # 優先使用命令行參數中的API密鑰 81 | api_key = args.api_key or API_KEY 82 | secret_key = args.secret_key or SECRET_KEY 83 | # 读取wss代理 84 | ws_proxy = args.ws_proxy or WS_PROXY 85 | 86 | # 檢查API密鑰 87 | if not api_key or not secret_key: 88 | logger.error("缺少API密鑰,請通過命令行參數或環境變量提供") 89 | sys.exit(1) 90 | 91 | # 決定執行模式 92 | if args.cli: 93 | # 啟動命令行界面 94 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 95 | elif args.symbol: 96 | # 如果指定了交易對,直接運行網格交易策略 97 | run_grid_trader(args, api_key, secret_key, ws_proxy=ws_proxy) 98 | else: 99 | # 默認啟動命令行界面 100 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 101 | 102 | if __name__ == "__main__": 103 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 注意:此代碼庫已不再更新 2 | > 此庫已整合到 [Backpack-MM-Simple](https://github.com/yanowo/Backpack-MM-Simple/tree/main) ,請前往該倉庫查看最新代碼與後續更新。 3 | 4 | > This repository has been merged into [Backpack-MM-Simple](https://github.com/yanowo/Backpack-MM-Simple/tree/main) Please visit that repository for the latest code and future updates. 5 | # Backpack Exchange 網格交易程序 6 | 7 | 這是一個針對 Backpack Exchange 的加密貨幣網格交易程序。該程序提供自動化網格交易功能,通過在預設價格範圍內設置多個買賣網格點,實現低買高賣賺取利潤。 8 | 9 | Backpack 註冊連結:[https://backpack.exchange/refer/yan](https://backpack.exchange/refer/yan) 10 | 11 | Twitter:[Yan Practice ⭕散修](https://x.com/practice_y11) 12 | 13 | ## 功能特點 14 | 15 | - 自動化網格交易策略 16 | - 自動或手動設置價格範圍 17 | - 可調節網格數量 18 | - 買單成交後自動補充賣單 19 | - 賣單成交後自動補充買單 20 | - 詳細的交易統計 21 | - WebSocket 實時數據連接 22 | - 命令行界面 23 | 24 | ## 項目結構 25 | 26 | ``` 27 | Backpack-Grid-Trading/ 28 | │ 29 | ├── api/ # API相關模塊 30 | │ ├── __init__.py 31 | │ ├── auth.py # API認證和簽名相關 32 | │ └── client.py # API請求客戶端 33 | │ 34 | ├── websocket/ # WebSocket模塊 35 | │ ├── __init__.py 36 | │ └── client.py # WebSocket客戶端 37 | │ 38 | ├── database/ # 數據庫模塊 39 | │ ├── __init__.py 40 | │ └── db.py # 數據庫操作 41 | │ 42 | ├── strategies/ # 策略模塊 43 | │ ├── __init__.py 44 | │ └── grid_trader.py # 網格交易策略 45 | │ 46 | ├── utils/ # 工具模塊 47 | │ ├── __init__.py 48 | │ └── helpers.py # 輔助函數 49 | │ └── grid_helper.py # 輔助設定 50 | │ 51 | ├── cli/ # 命令行界面 52 | │ ├── __init__.py 53 | │ └── commands.py # 命令行命令 54 | │ 55 | ├── config.py # 配置文件 56 | ├── logger.py # 日誌配置 57 | ├── main.py # 主執行文件 58 | ├── run.py # 統一入口文件 59 | └── requirements.txt # 依賴元件 60 | └── README.md # 說明文檔 61 | ``` 62 | 63 | ## 環境要求 64 | 65 | - Python 3.8 或更高版本 66 | - 所需第三方庫: 67 | - PyNaCl (用於API簽名) 68 | - requests 69 | - websocket-client 70 | - numpy 71 | - python-dotenv 72 | 73 | ## 安裝 74 | 75 | 1. 克隆或下載此代碼庫: 76 | 77 | ```bash 78 | git clone https://github.com/yanowo/Backpack-MM-Simple.git 79 | cd Backpack-MM-Simple 80 | ``` 81 | 82 | 2. 安裝依賴: 83 | 84 | ```bash 85 | pip install -r requirements.txt 86 | ``` 87 | 88 | 3. 設置環境變數: 89 | 90 | 創建 `.env` 文件並添加: 91 | 92 | ``` 93 | API_KEY=your_api_key 94 | SECRET_KEY=your_secret_key 95 | PROXY_WEBSOCKET=http://user:pass@host:port/ 或者 http://host:port (若不需要則留空或移除) 96 | ``` 97 | 98 | ## 使用方法 99 | 100 | ### 統一入口 101 | 102 | ```bash 103 | # 啟動命令行界面 (默認) 104 | python run.py 105 | ``` 106 | 107 | ### 命令行界面 108 | 109 | 啟動命令行界面: 110 | 111 | ```bash 112 | python main.py --cli 113 | ``` 114 | 115 | ### 直接執行網格交易策略 116 | 117 | ```bash 118 | python main.py --symbol SOL_USDC --grid-upper 30.5 --grid-lower 29.5 --grid-num 10 --duration 3600 --interval 60 119 | ``` 120 | 121 | ### 命令行參數 122 | 123 | - `--api-key`: API 密鑰 (可選,默認使用環境變數) 124 | - `--secret-key`: API 密鑰 (可選,默認使用環境變數) 125 | - `--ws-proxy`: Websocket 代理 (可選,默認使用環境變數) 126 | 127 | ### 網格交易參數 128 | 129 | - `--symbol`: 交易對 (例如: SOL_USDC) 130 | - `--grid-upper`: 網格上限價格 131 | - `--grid-lower`: 網格下限價格 132 | - `--grid-num`: 網格數量 (默認: 10) 133 | - `--quantity`: 每個網格的訂單數量 (可選) 134 | - `--auto-price`: 自動設置價格範圍 (基於當前市場價格) 135 | - `--price-range`: 自動模式下的價格範圍百分比 (默認: 5.0) 136 | - `--duration`: 運行時間(秒)(默認: 3600) 137 | - `--interval`: 更新間隔(秒)(默認: 60) 138 | 139 | ## 運行示例 140 | 141 | ### 自動設置價格範圍 142 | 143 | ```bash 144 | python run.py --symbol SOL_USDC --auto-price --price-range 5.0 --grid-num 10 145 | ``` 146 | 147 | ### 手動設置價格範圍 148 | 149 | ```bash 150 | python run.py --symbol SOL_USDC --grid-upper 30.5 --grid-lower 29.5 --grid-num 15 151 | ``` 152 | 153 | ### 指定訂單數量 154 | 155 | ```bash 156 | python run.py --symbol SOL_USDC --auto-price --price-range 3.0 --grid-num 8 --quantity 0.5 157 | ``` 158 | 159 | ### 長時間運行示例 160 | 161 | ```bash 162 | python run.py --symbol SOL_USDC --auto-price --price-range 4.0 --duration 86400 --interval 120 163 | ``` 164 | 165 | ### 完整參數示例 166 | 167 | ```bash 168 | python run.py --symbol SOL_USDC --grid-upper 30.5 --grid-lower 29.5 --grid-num 12 --quantity 0.2 --duration 7200 --interval 60 169 | ``` 170 | 171 | ## 策略說明 172 | 173 | 網格交易是一種常見的量化交易策略,特別適合在震盪行情中使用。策略邏輯如下: 174 | 175 | 1. 在設定的價格區間內(上限和下限之間)均勻劃分多個價格點 176 | 2. 在每個價格點上放置買單或賣單 177 | 3. 當買單成交後,自動在更高的價格點放置賣單 178 | 4. 當賣單成交後,自動在更低的價格點放置買單 179 | 5. 通過低買高賣的差價獲取利潤 180 | 181 | 該策略在震盪市場中效果較好,但在單邊行情中可能面臨風險。建議根據市場情況調整網格參數。 182 | 183 | ## 注意事項 184 | 185 | - 交易涉及風險,請謹慎使用 186 | - 建議先在小資金上測試策略效果 187 | - 定期檢查交易統計以評估策略表現 188 | - 網格交易需要足夠的資金以覆蓋整個網格區間 189 | - 強烈建議在相對穩定的市場使用網格交易 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Backpack Exchange 網格交易程序統一入口 4 | 支持命令行模式和智能參數設置 5 | """ 6 | import argparse 7 | import sys 8 | import os 9 | 10 | # 嘗試導入需要的模塊 11 | try: 12 | from logger import setup_logger 13 | from config import API_KEY, SECRET_KEY, WS_PROXY 14 | from utils.grid_helper import interactive_setup, calculate_optimal_grid_params, print_grid_params 15 | except ImportError: 16 | API_KEY = os.getenv('API_KEY') 17 | SECRET_KEY = os.getenv('SECRET_KEY') 18 | WS_PROXY = os.getenv('PROXY_WEBSOCKET') 19 | 20 | def setup_logger(name): 21 | import logging 22 | logger = logging.getLogger(name) 23 | logger.setLevel(logging.INFO) 24 | handler = logging.StreamHandler() 25 | handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 26 | logger.addHandler(handler) 27 | return logger 28 | 29 | print("無法導入網格助手模塊,將使用基本參數設置") 30 | 31 | # 創建記錄器 32 | logger = setup_logger("main") 33 | 34 | def parse_arguments(): 35 | """解析命令行參數""" 36 | parser = argparse.ArgumentParser(description='Backpack Exchange 網格交易程序') 37 | 38 | # 模式選擇 39 | parser.add_argument('--cli', action='store_true', help='啟動命令行界面 (默認模式)') 40 | parser.add_argument('--setup', action='store_true', help='啟動交互式參數設置助手') 41 | parser.add_argument('--smart', action='store_true', help='使用智能參數計算') 42 | 43 | # 基本參數 44 | parser.add_argument('--api-key', type=str, help='API Key (可選,默認使用環境變數或配置文件)') 45 | parser.add_argument('--secret-key', type=str, help='Secret Key (可選,默認使用環境變數或配置文件)') 46 | parser.add_argument('--ws-proxy', type=str, help='WebSocket Proxy (可選,默認使用環境變數或配置文件)') 47 | 48 | # 網格交易參數 49 | parser.add_argument('--symbol', type=str, help='交易對 (例如: SOL_USDC)') 50 | parser.add_argument('--grid-upper', type=float, help='網格上限價格') 51 | parser.add_argument('--grid-lower', type=float, help='網格下限價格') 52 | parser.add_argument('--grid-num', type=int, default=10, help='網格數量 (默認: 10)') 53 | parser.add_argument('--quantity', type=float, help='每格訂單數量 (可選)') 54 | parser.add_argument('--auto-price', action='store_true', help='自動設置價格範圍') 55 | parser.add_argument('--price-range', type=float, default=5.0, help='自動模式下的價格範圍百分比 (默認: 5.0)') 56 | parser.add_argument('--duration', type=int, default=3600, help='運行時間(秒)(默認: 3600)') 57 | parser.add_argument('--interval', type=int, default=60, help='更新間隔(秒)(默認: 60)') 58 | 59 | # 額外參數 60 | parser.add_argument('--fee-rate', type=float, default=0.1, help='Maker手續費率百分比 (默認: 0.1)') 61 | parser.add_argument('--risk', type=str, choices=['low', 'medium', 'high'], default='medium', 62 | help='風險等級 (low/medium/high, 默認: medium)') 63 | 64 | return parser.parse_args() 65 | 66 | def main(): 67 | """主函數""" 68 | args = parse_arguments() 69 | 70 | # 優先使用命令行參數中的API密鑰 71 | api_key = args.api_key or API_KEY 72 | secret_key = args.secret_key or SECRET_KEY 73 | 74 | # 读取wss代理 75 | ws_proxy = args.ws_proxy or WS_PROXY 76 | 77 | # 檢查API密鑰 78 | if not api_key or not secret_key: 79 | logger.error("缺少API密鑰,請通過命令行參數或環境變量提供") 80 | sys.exit(1) 81 | 82 | # 決定執行模式 83 | if args.setup: 84 | # 啟動交互式參數設置助手 85 | try: 86 | params = interactive_setup() 87 | print("\n設置完成!可以使用以下命令運行網格交易:") 88 | cmd = f"python run.py --symbol {params['symbol']} --grid-upper {params['grid_upper_price']} --grid-lower {params['grid_lower_price']} --grid-num {params['grid_num']}" 89 | if params.get('order_quantity'): 90 | cmd += f" --quantity {params['order_quantity']}" 91 | if 'duration' in params: 92 | cmd += f" --duration {params['duration']}" 93 | if 'interval' in params: 94 | cmd += f" --interval {params['interval']}" 95 | print(cmd) 96 | 97 | # 詢問是否立即運行 98 | run_now = input("\n是否立即運行?(y/n): ").strip().lower() 99 | if run_now != 'y': 100 | return 101 | 102 | # 更新參數 103 | args.symbol = params['symbol'] 104 | args.grid_upper = params['grid_upper_price'] 105 | args.grid_lower = params['grid_lower_price'] 106 | args.grid_num = params['grid_num'] 107 | if params.get('order_quantity'): 108 | args.quantity = params['order_quantity'] 109 | if 'duration' in params: 110 | args.duration = params['duration'] 111 | if 'interval' in params: 112 | args.interval = params['interval'] 113 | 114 | except ImportError: 115 | logger.error("無法找到參數設置助手模塊,請安裝必要的依賴") 116 | return 117 | except Exception as e: 118 | logger.error(f"參數設置過程中發生錯誤: {e}") 119 | return 120 | 121 | if args.smart and args.symbol: 122 | # 使用智能參數計算 123 | try: 124 | maker_fee_rate = args.fee_rate / 100 125 | params = calculate_optimal_grid_params( 126 | args.symbol, 127 | maker_fee_rate=maker_fee_rate, 128 | risk_level=args.risk 129 | ) 130 | print_grid_params(params) 131 | 132 | # 詢問是否使用這些參數 133 | confirm = input("\n是否使用這些參數?(y/n): ").strip().lower() 134 | if confirm != 'y': 135 | return 136 | 137 | # 更新參數 138 | args.grid_upper = params['grid_upper_price'] 139 | args.grid_lower = params['grid_lower_price'] 140 | args.grid_num = params['grid_num'] 141 | if params.get('order_quantity'): 142 | args.quantity = params['order_quantity'] 143 | 144 | except ImportError: 145 | logger.error("無法找到智能參數計算模塊,請安裝必要的依賴") 146 | return 147 | except Exception as e: 148 | logger.error(f"智能參數計算過程中發生錯誤: {e}") 149 | return 150 | 151 | if args.symbol: 152 | # 如果指定了交易對,直接運行網格交易策略 153 | try: 154 | from strategies.grid_trader import GridTrader 155 | 156 | # 檢查網格參數 157 | if not args.auto_price and (args.grid_upper is None or args.grid_lower is None): 158 | logger.error("使用手動網格模式時需要指定網格上下限價格 (--grid-upper 和 --grid-lower)") 159 | return 160 | 161 | # 確保數量參數被正確傳遞 162 | if args.quantity is None: 163 | logger.warning("未指定訂單數量,將使用交易所最小訂單大小") 164 | else: 165 | logger.info(f"使用訂單數量: {args.quantity}") 166 | 167 | # 初始化網格交易 168 | grid_trader = GridTrader( 169 | api_key=api_key, 170 | secret_key=secret_key, 171 | symbol=args.symbol, 172 | grid_upper_price=args.grid_upper, 173 | grid_lower_price=args.grid_lower, 174 | grid_num=args.grid_num, 175 | order_quantity=args.quantity, 176 | auto_price_range=args.auto_price, 177 | price_range_percent=args.price_range, 178 | ws_proxy=ws_proxy 179 | ) 180 | 181 | # 執行網格交易策略 182 | grid_trader.run(duration_seconds=args.duration, interval_seconds=args.interval) 183 | 184 | except KeyboardInterrupt: 185 | logger.info("收到中斷信號,正在退出...") 186 | except Exception as e: 187 | logger.error(f"網格交易過程中發生錯誤: {e}") 188 | import traceback 189 | traceback.print_exc() 190 | else: 191 | # 默認啟動命令行界面 192 | try: 193 | from cli.commands import main_cli 194 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 195 | except ImportError as e: 196 | logger.error(f"啟動命令行界面時出錯: {str(e)}") 197 | sys.exit(1) 198 | 199 | if __name__ == "__main__": 200 | main() -------------------------------------------------------------------------------- /api/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | API請求客戶端模塊 3 | """ 4 | import json 5 | import time 6 | import requests 7 | from typing import Dict, Any, Optional, List, Union 8 | from .auth import create_signature 9 | from config import API_URL, API_VERSION, DEFAULT_WINDOW 10 | from logger import setup_logger 11 | 12 | logger = setup_logger("api.client") 13 | 14 | def make_request(method: str, endpoint: str, api_key=None, secret_key=None, instruction=None, 15 | params=None, data=None, retry_count=3) -> Dict: 16 | """ 17 | 執行API請求,支持重試機制 18 | 19 | Args: 20 | method: HTTP方法 (GET, POST, DELETE) 21 | endpoint: API端點 22 | api_key: API密鑰 23 | secret_key: API密鑰 24 | instruction: API指令 25 | params: 查詢參數 26 | data: 請求體數據 27 | retry_count: 重試次數 28 | 29 | Returns: 30 | API響應數據 31 | """ 32 | url = f"{API_URL}{endpoint}" 33 | headers = {'Content-Type': 'application/json'} 34 | 35 | # 構建簽名信息(如需要) 36 | if api_key and secret_key and instruction: 37 | timestamp = str(int(time.time() * 1000)) 38 | window = DEFAULT_WINDOW 39 | 40 | # 構建簽名消息 41 | query_string = "" 42 | if params: 43 | sorted_params = sorted(params.items()) 44 | query_string = "&".join([f"{k}={v}" for k, v in sorted_params]) 45 | 46 | sign_message = f"instruction={instruction}" 47 | if query_string: 48 | sign_message += f"&{query_string}" 49 | sign_message += f"×tamp={timestamp}&window={window}" 50 | 51 | signature = create_signature(secret_key, sign_message) 52 | if not signature: 53 | return {"error": "簽名創建失敗"} 54 | 55 | headers.update({ 56 | 'X-API-KEY': api_key, 57 | 'X-SIGNATURE': signature, 58 | 'X-TIMESTAMP': timestamp, 59 | 'X-WINDOW': window, 60 | 'X-Broker-Id': '1500' 61 | }) 62 | 63 | # 添加查詢參數到URL 64 | if params and method.upper() in ['GET', 'DELETE']: 65 | query_string = "&".join([f"{k}={v}" for k, v in params.items()]) 66 | url += f"?{query_string}" 67 | 68 | # 實施重試機制 69 | for attempt in range(retry_count): 70 | try: 71 | if method.upper() == 'GET': 72 | response = requests.get(url, headers=headers, timeout=10) 73 | elif method.upper() == 'POST': 74 | response = requests.post(url, headers=headers, data=json.dumps(data) if data else None, timeout=10) 75 | elif method.upper() == 'DELETE': 76 | response = requests.delete(url, headers=headers, data=json.dumps(data) if data else None, timeout=10) 77 | else: 78 | return {"error": f"不支持的請求方法: {method}"} 79 | 80 | # 處理響應 81 | if response.status_code in [200, 201]: 82 | return response.json() if response.text.strip() else {} 83 | elif response.status_code == 429: # 速率限制 84 | wait_time = 1 * (2 ** attempt) # 指數退避 85 | logger.warning(f"遇到速率限制,等待 {wait_time} 秒後重試") 86 | time.sleep(wait_time) 87 | continue 88 | else: 89 | error_msg = f"狀態碼: {response.status_code}, 消息: {response.text}" 90 | if attempt < retry_count - 1: 91 | logger.warning(f"請求失敗 ({attempt+1}/{retry_count}): {error_msg}") 92 | time.sleep(1) # 簡單重試延遲 93 | continue 94 | return {"error": error_msg} 95 | 96 | except requests.exceptions.Timeout: 97 | if attempt < retry_count - 1: 98 | logger.warning(f"請求超時 ({attempt+1}/{retry_count}),重試中...") 99 | continue 100 | return {"error": "請求超時"} 101 | except requests.exceptions.ConnectionError: 102 | if attempt < retry_count - 1: 103 | logger.warning(f"連接錯誤 ({attempt+1}/{retry_count}),重試中...") 104 | time.sleep(2) # 連接錯誤通常需要更長等待 105 | continue 106 | return {"error": "連接錯誤"} 107 | except Exception as e: 108 | if attempt < retry_count - 1: 109 | logger.warning(f"請求異常 ({attempt+1}/{retry_count}): {str(e)},重試中...") 110 | continue 111 | return {"error": f"請求失敗: {str(e)}"} 112 | 113 | return {"error": "達到最大重試次數"} 114 | 115 | # 各API端點函數 116 | def get_deposit_address(api_key, secret_key, blockchain): 117 | """獲取存款地址""" 118 | endpoint = f"/wapi/{API_VERSION}/capital/deposit/address" 119 | instruction = "depositAddressQuery" 120 | params = {"blockchain": blockchain} 121 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 122 | 123 | def get_balance(api_key, secret_key): 124 | """獲取賬戶餘額""" 125 | endpoint = f"/api/{API_VERSION}/capital" 126 | instruction = "balanceQuery" 127 | return make_request("GET", endpoint, api_key, secret_key, instruction) 128 | 129 | def execute_order(api_key, secret_key, order_details): 130 | """執行訂單""" 131 | endpoint = f"/api/{API_VERSION}/order" 132 | instruction = "orderExecute" 133 | 134 | # 提取所有參數用於簽名 135 | params = { 136 | "orderType": order_details["orderType"], 137 | "price": order_details.get("price", "0"), 138 | "quantity": order_details["quantity"], 139 | "side": order_details["side"], 140 | "symbol": order_details["symbol"], 141 | "timeInForce": order_details.get("timeInForce", "GTC") 142 | } 143 | 144 | # 添加可選參數 145 | for key in ["postOnly", "reduceOnly", "clientId", "quoteQuantity", 146 | "autoBorrow", "autoLendRedeem", "autoBorrowRepay", "autoLend"]: 147 | if key in order_details: 148 | params[key] = str(order_details[key]).lower() if isinstance(order_details[key], bool) else str(order_details[key]) 149 | 150 | return make_request("POST", endpoint, api_key, secret_key, instruction, params, order_details) 151 | 152 | def get_open_orders(api_key, secret_key, symbol=None): 153 | """獲取未成交訂單""" 154 | endpoint = f"/api/{API_VERSION}/orders" 155 | instruction = "orderQueryAll" 156 | params = {} 157 | if symbol: 158 | params["symbol"] = symbol 159 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 160 | 161 | def cancel_all_orders(api_key, secret_key, symbol): 162 | """取消所有訂單""" 163 | endpoint = f"/api/{API_VERSION}/orders" 164 | instruction = "orderCancelAll" 165 | params = {"symbol": symbol} 166 | data = {"symbol": symbol} 167 | return make_request("DELETE", endpoint, api_key, secret_key, instruction, params, data) 168 | 169 | def cancel_order(api_key, secret_key, order_id, symbol): 170 | """取消指定訂單""" 171 | endpoint = f"/api/{API_VERSION}/order" 172 | instruction = "orderCancel" 173 | params = {"orderId": order_id, "symbol": symbol} 174 | data = {"orderId": order_id, "symbol": symbol} 175 | return make_request("DELETE", endpoint, api_key, secret_key, instruction, params, data) 176 | 177 | def get_ticker(symbol): 178 | """獲取市場價格""" 179 | endpoint = f"/api/{API_VERSION}/ticker" 180 | params = {"symbol": symbol} 181 | return make_request("GET", endpoint, params=params) 182 | 183 | def get_markets(): 184 | """獲取所有交易對信息""" 185 | endpoint = f"/api/{API_VERSION}/markets" 186 | return make_request("GET", endpoint) 187 | 188 | def get_order_book(symbol, limit=20): 189 | """獲取市場深度""" 190 | endpoint = f"/api/{API_VERSION}/depth" 191 | params = {"symbol": symbol, "limit": str(limit)} 192 | return make_request("GET", endpoint, params=params) 193 | 194 | def get_fill_history(api_key, secret_key, symbol=None, limit=100): 195 | """獲取歷史成交記錄""" 196 | endpoint = f"/wapi/{API_VERSION}/history/fills" 197 | instruction = "fillHistoryQueryAll" 198 | params = {"limit": str(limit)} 199 | if symbol: 200 | params["symbol"] = symbol 201 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 202 | 203 | def get_klines(symbol, interval="1h", limit=100): 204 | """獲取K線數據""" 205 | endpoint = f"/api/{API_VERSION}/klines" 206 | 207 | # 計算起始時間 (秒) 208 | current_time = int(time.time()) 209 | 210 | # 各間隔對應的秒數 211 | interval_seconds = { 212 | "1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800, 213 | "1h": 3600, "2h": 7200, "4h": 14400, "6h": 21600, "8h": 28800, 214 | "12h": 43200, "1d": 86400, "3d": 259200, "1w": 604800, "1month": 2592000 215 | } 216 | 217 | # 計算合適的起始時間 218 | duration = interval_seconds.get(interval, 3600) 219 | start_time = current_time - (duration * limit) 220 | 221 | params = { 222 | "symbol": symbol, 223 | "interval": interval, 224 | "startTime": str(start_time) 225 | } 226 | 227 | return make_request("GET", endpoint, params=params) 228 | 229 | def get_market_limits(symbol): 230 | """獲取交易對的最低訂單量和價格精度""" 231 | markets_info = get_markets() 232 | 233 | if not isinstance(markets_info, dict) and isinstance(markets_info, list): 234 | for market_info in markets_info: 235 | if market_info.get('symbol') == symbol: 236 | base_asset = market_info.get('baseSymbol') 237 | quote_asset = market_info.get('quoteSymbol') 238 | 239 | # 從filters中獲取精度和最小訂單量信息 240 | filters = market_info.get('filters', {}) 241 | base_precision = 8 # 默認值 242 | quote_precision = 8 # 默認值 243 | min_order_size = "0" # 默認值 244 | tick_size = "0.00000001" # 默認值 245 | 246 | if 'price' in filters: 247 | tick_size = filters['price'].get('tickSize', '0.00000001') 248 | quote_precision = len(tick_size.split('.')[-1]) if '.' in tick_size else 0 249 | 250 | if 'quantity' in filters: 251 | min_order_size = filters['quantity'].get('minQuantity', '0') 252 | min_value = filters['quantity'].get('minQuantity', '0.00000001') 253 | base_precision = len(min_value.split('.')[-1]) if '.' in min_value else 0 254 | 255 | return { 256 | 'base_asset': base_asset, 257 | 'quote_asset': quote_asset, 258 | 'base_precision': base_precision, 259 | 'quote_precision': quote_precision, 260 | 'min_order_size': min_order_size, 261 | 'tick_size': tick_size 262 | } 263 | 264 | logger.error(f"找不到交易對 {symbol} 的信息") 265 | return None 266 | else: 267 | logger.error(f"無法獲取交易對信息: {markets_info}") 268 | return None -------------------------------------------------------------------------------- /utils/grid_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | 網格交易參數助手模塊 3 | 提供自動計算和優化網格參數的功能 4 | """ 5 | import math 6 | from typing import Dict, Tuple, List, Optional 7 | from api.client import get_ticker, get_market_limits 8 | from utils.helpers import round_to_precision, round_to_tick_size 9 | 10 | def calculate_optimal_grid_params( 11 | symbol: str, 12 | current_price: float = None, 13 | volatility: float = None, 14 | maker_fee_rate: float = 0.001, 15 | tick_size: float = None, 16 | risk_level: str = "medium", 17 | order_quantity: float = None, 18 | min_order_size: float = None 19 | ) -> Dict: 20 | """ 21 | 計算最優網格參數 22 | 23 | Args: 24 | symbol: 交易對 25 | current_price: 當前價格,如果為None則會自動獲取 26 | volatility: 波動率,如果為None則使用預設值 27 | maker_fee_rate: Maker手續費率,默認0.1% 28 | tick_size: 價格步長,如果為None則會自動獲取 29 | risk_level: 風險等級 ('low', 'medium', 'high') 30 | order_quantity: 每格訂單數量 31 | min_order_size: 最小訂單大小 32 | 33 | Returns: 34 | 包含網格參數的字典 35 | """ 36 | # 獲取市場限制和價格步長 37 | market_limits = None 38 | base_asset = None 39 | quote_asset = None 40 | base_precision = None 41 | 42 | if tick_size is None or min_order_size is None: 43 | try: 44 | market_limits = get_market_limits(symbol) 45 | if market_limits: 46 | if 'tick_size' in market_limits and tick_size is None: 47 | tick_size = float(market_limits['tick_size']) 48 | print(f"自動獲取價格步長: {tick_size}") 49 | 50 | if 'min_order_size' in market_limits and min_order_size is None: 51 | min_order_size = float(market_limits['min_order_size']) 52 | print(f"自動獲取最小訂單大小: {min_order_size}") 53 | 54 | if 'base_asset' in market_limits: 55 | base_asset = market_limits['base_asset'] 56 | 57 | if 'quote_asset' in market_limits: 58 | quote_asset = market_limits['quote_asset'] 59 | 60 | if 'base_precision' in market_limits: 61 | base_precision = int(market_limits['base_precision']) 62 | except Exception as e: 63 | print(f"獲取市場限制出錯: {e}") 64 | 65 | # 默認值處理 66 | if tick_size is None: 67 | print("使用默認價格步長: 0.01") 68 | tick_size = 0.01 69 | 70 | if min_order_size is None: 71 | print("使用默認最小訂單大小: 0.01") 72 | min_order_size = 0.01 73 | 74 | # 嘗試從交易對解析資產 75 | if base_asset is None or quote_asset is None: 76 | parts = symbol.split('_') 77 | if len(parts) == 2: 78 | if base_asset is None: 79 | base_asset = parts[0] 80 | if quote_asset is None: 81 | quote_asset = parts[1] 82 | 83 | # 獲取當前價格(如果未提供) 84 | if current_price is None: 85 | ticker = get_ticker(symbol) 86 | if isinstance(ticker, dict) and "lastPrice" in ticker: 87 | current_price = float(ticker['lastPrice']) 88 | else: 89 | raise ValueError(f"無法獲取 {symbol} 的價格") 90 | 91 | # 根據風險等級設置參數 92 | risk_params = { 93 | "low": {"price_range_pct": 2.0, "grid_num": 6, "profit_factor": 3}, 94 | "medium": {"price_range_pct": 4.0, "grid_num": 10, "profit_factor": 4}, 95 | "high": {"price_range_pct": 8.0, "grid_num": 16, "profit_factor": 5} 96 | } 97 | 98 | params = risk_params.get(risk_level, risk_params["medium"]) 99 | 100 | # 如果提供了波動率,則使用波動率調整價格範圍 101 | if volatility: 102 | # 使用波動率的2-4倍作為價格範圍 103 | volatility_factor = 2.0 if risk_level == "low" else 3.0 if risk_level == "medium" else 4.0 104 | params["price_range_pct"] = min(max(volatility * volatility_factor, 1.0), 15.0) 105 | 106 | # 計算網格間距,確保能覆蓋手續費 107 | total_fee_pct = maker_fee_rate * 200 # 買賣循環總手續費百分比 (0.1% * 2 * 100%) 108 | min_grid_gap_pct = total_fee_pct * params["profit_factor"] # 最小網格間距百分比 109 | 110 | # 調整網格數量,確保網格間距足夠 111 | price_range_pct = params["price_range_pct"] 112 | adjusted_grid_num = int(price_range_pct / min_grid_gap_pct) 113 | grid_num = min(params["grid_num"], max(4, adjusted_grid_num)) 114 | 115 | # 計算網格上下限 116 | upper_price = round_to_tick_size(current_price * (1 + price_range_pct/100), tick_size) 117 | lower_price = round_to_tick_size(current_price * (1 - price_range_pct/100), tick_size) 118 | 119 | # 計算實際網格間距 120 | actual_gap_pct = (upper_price - lower_price) / (grid_num * current_price) * 100 121 | profit_factor = actual_gap_pct / total_fee_pct 122 | 123 | # 檢查訂單數量是否符合最小訂單大小 124 | if order_quantity is not None: 125 | if order_quantity < min_order_size: 126 | print(f"警告: 設置的訂單數量 {order_quantity} 小於最小訂單大小 {min_order_size}") 127 | print(f"已自動調整為最小訂單大小: {min_order_size}") 128 | order_quantity = min_order_size 129 | 130 | # 計算所需資金 131 | required_base = 0 # 基礎貨幣需求 132 | required_quote = 0 # 報價貨幣需求 133 | 134 | if order_quantity: 135 | # 計算賣單所需的基礎貨幣(上半部分網格) 136 | sell_grids = (grid_num + 1) // 2 # 上半部分網格數(含中間網格) 137 | required_base = sell_grids * order_quantity 138 | 139 | # 計算買單所需的報價貨幣(下半部分網格) 140 | buy_grids = (grid_num + 1) - sell_grids # 下半部分網格數 141 | avg_buy_price = (current_price + lower_price) / 2 # 平均買入價格 142 | required_quote = buy_grids * order_quantity * avg_buy_price 143 | 144 | return { 145 | "symbol": symbol, # 確保包含交易對 146 | "grid_upper_price": upper_price, 147 | "grid_lower_price": lower_price, 148 | "grid_num": grid_num, 149 | "price_range_percent": price_range_pct, 150 | "grid_gap_percent": actual_gap_pct, 151 | "profit_factor": profit_factor, 152 | "order_quantity": order_quantity, 153 | "current_price": current_price, 154 | "tick_size": tick_size, 155 | "min_order_size": min_order_size, 156 | "base_asset": base_asset, 157 | "quote_asset": quote_asset, 158 | "base_precision": base_precision, 159 | "required_base": required_base, 160 | "required_quote": required_quote, 161 | "estimated_profit_per_grid": f"{(actual_gap_pct - total_fee_pct):.4f}%" 162 | } 163 | 164 | def print_grid_params(params: Dict): 165 | """ 166 | 打印網格參數詳細信息 167 | 168 | Args: 169 | params: 網格參數字典 170 | """ 171 | print("\n=== 網格交易參數 ===") 172 | print(f"交易對: {params.get('symbol', 'Unknown')}") 173 | 174 | base_asset = params.get('base_asset', '') 175 | quote_asset = params.get('quote_asset', '') 176 | 177 | print(f"當前價格: {params['current_price']:.6f} {quote_asset}") 178 | print(f"網格上限: {params['grid_upper_price']:.6f} {quote_asset}") 179 | print(f"網格下限: {params['grid_lower_price']:.6f} {quote_asset}") 180 | print(f"網格數量: {params['grid_num']}") 181 | print(f"價格範圍: ±{params['price_range_percent']/2:.2f}%") 182 | print(f"網格間距: {params['grid_gap_percent']:.4f}%") 183 | print(f"利潤因子: {params['profit_factor']:.2f}x") 184 | 185 | if params.get('tick_size'): 186 | print(f"價格步長: {params['tick_size']}") 187 | 188 | if params.get('min_order_size'): 189 | print(f"最小訂單大小: {params['min_order_size']} {base_asset}") 190 | 191 | if params.get('order_quantity'): 192 | print(f"每格訂單數量: {params['order_quantity']} {base_asset}") 193 | 194 | print(f"預估每格利潤: {params['estimated_profit_per_grid']}") 195 | 196 | # 顯示所需資金 197 | if params.get('required_base') and params.get('required_quote'): 198 | print(f"\n=== 所需資金 ===") 199 | print(f"基礎貨幣({base_asset}): {params['required_base']:.6f}") 200 | print(f"報價貨幣({quote_asset}): {params['required_quote']:.6f}") 201 | 202 | # 打印網格點位 203 | if params['grid_num'] <= 20: # 只在網格數量較少時打印所有點位 204 | price_step = (params['grid_upper_price'] - params['grid_lower_price']) / params['grid_num'] 205 | print("\n網格價格點位:") 206 | for i in range(params['grid_num'] + 1): 207 | grid_price = params['grid_lower_price'] + i * price_step 208 | distance = ((grid_price / params['current_price']) - 1) * 100 209 | print(f"網格 {i}: {grid_price:.6f} ({distance:+.2f}% 距當前價)") 210 | 211 | def get_risk_profile(symbol: str) -> str: 212 | """ 213 | 根據交易對獲取建議的風險級別 214 | 215 | Args: 216 | symbol: 交易對符號 217 | 218 | Returns: 219 | 風險級別 ('low', 'medium', 'high') 220 | """ 221 | # 常見穩定幣交易對 222 | stable_pairs = ['USDT_USDC', 'USDC_USDT', 'USDT_DAI', 'DAI_USDC', 'BUSD_USDT', 'BUSD_USDC'] 223 | 224 | # 中等波動性資產 225 | medium_volatility = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'ADA', 'DOT'] 226 | 227 | # 穩定幣交易對使用低風險設置 228 | if symbol in stable_pairs: 229 | return "low" 230 | 231 | # 檢查交易對中的資產 232 | for asset in medium_volatility: 233 | if asset in symbol: 234 | return "medium" 235 | 236 | # 默認使用高風險設置 237 | return "high" 238 | 239 | def interactive_setup(): 240 | """ 241 | 交互式設置網格參數 242 | 243 | Returns: 244 | 網格參數字典 245 | """ 246 | print("\n=== 網格交易參數助手 ===") 247 | symbol = input("請輸入交易對 (例如: SOL_USDC): ").strip() 248 | 249 | # 獲取市場限制 250 | tick_size = None 251 | min_order_size = None 252 | base_asset = None 253 | quote_asset = None 254 | base_precision = None 255 | 256 | try: 257 | market_limits = get_market_limits(symbol) 258 | if market_limits: 259 | if 'tick_size' in market_limits: 260 | tick_size = float(market_limits['tick_size']) 261 | print(f"自動獲取價格步長: {tick_size}") 262 | 263 | if 'min_order_size' in market_limits: 264 | min_order_size = float(market_limits['min_order_size']) 265 | print(f"自動獲取最小訂單大小: {min_order_size}") 266 | 267 | if 'base_asset' in market_limits: 268 | base_asset = market_limits['base_asset'] 269 | print(f"基礎貨幣: {base_asset}") 270 | 271 | if 'quote_asset' in market_limits: 272 | quote_asset = market_limits['quote_asset'] 273 | print(f"報價貨幣: {quote_asset}") 274 | 275 | if 'base_precision' in market_limits: 276 | base_precision = int(market_limits['base_precision']) 277 | except Exception as e: 278 | print(f"獲取市場限制出錯: {e}") 279 | # 嘗試從交易對名稱解析資產 280 | if '_' in symbol: 281 | base_asset = symbol.split('_')[0] 282 | quote_asset = symbol.split('_')[1] 283 | 284 | # 設置默認值 285 | if tick_size is None: 286 | tick_size_input = input("自動獲取價格步長失敗,請手動輸入 (默認0.01): ").strip() 287 | tick_size = float(tick_size_input) if tick_size_input else 0.01 288 | 289 | if min_order_size is None: 290 | min_order_size_input = input("自動獲取最小訂單大小失敗,請手動輸入 (默認0.01): ").strip() 291 | min_order_size = float(min_order_size_input) if min_order_size_input else 0.01 292 | 293 | # 獲取當前價格 294 | try: 295 | ticker = get_ticker(symbol) 296 | if isinstance(ticker, dict) and "lastPrice" in ticker: 297 | current_price = float(ticker['lastPrice']) 298 | print(f"當前價格: {current_price} {quote_asset or ''}") 299 | else: 300 | current_price = float(input("無法自動獲取價格,請手動輸入當前價格: ")) 301 | except: 302 | current_price = float(input("無法自動獲取價格,請手動輸入當前價格: ")) 303 | 304 | # 指定每格訂單數量 305 | quantity_prompt = f"請輸入每格訂單數量 (最小 {min_order_size} {base_asset or ''}): " 306 | quantity_input = input(quantity_prompt).strip() 307 | 308 | if quantity_input: 309 | order_quantity = float(quantity_input) 310 | # 檢查是否滿足最小訂單大小 311 | if order_quantity < min_order_size: 312 | print(f"警告: 訂單數量 {order_quantity} 小於最小限制 {min_order_size},已自動調整") 313 | order_quantity = min_order_size 314 | else: 315 | order_quantity = None 316 | 317 | # 獲取手續費率 318 | fee_input = input("請輸入Maker手續費率 (%, 默認0.1): ").strip() 319 | maker_fee_rate = float(fee_input) / 100 if fee_input else 0.001 320 | 321 | # 獲取風險級別 322 | default_risk = get_risk_profile(symbol) 323 | risk_input = input(f"選擇風險級別 (low/medium/high, 默認 {default_risk}): ").strip().lower() 324 | risk_level = risk_input if risk_input in ["low", "medium", "high"] else default_risk 325 | 326 | # 獲取策略執行時間與間隔 327 | duration_input = input("請輸入策略執行時間(秒, 默認3600): ").strip() 328 | duration = int(duration_input) if duration_input else 3600 329 | 330 | interval_input = input("請輸入執行間隔(秒, 默認60): ").strip() 331 | interval = int(interval_input) if interval_input else 60 332 | 333 | # 計算最優參數 334 | params = calculate_optimal_grid_params( 335 | symbol, 336 | current_price=current_price, 337 | maker_fee_rate=maker_fee_rate, 338 | tick_size=tick_size, 339 | risk_level=risk_level, 340 | order_quantity=order_quantity, 341 | min_order_size=min_order_size 342 | ) 343 | 344 | # 打印參數 345 | print_grid_params(params) 346 | 347 | # 詢問是否確認 348 | confirm = input("\n是否使用這些參數? (y/n): ").strip().lower() 349 | 350 | if confirm == 'y': 351 | params['duration'] = duration 352 | params['interval'] = interval 353 | return params 354 | else: 355 | # 允許手動調整參數 356 | print("\n--- 手動調整參數 ---") 357 | range_input = input(f"價格範圍百分比 (默認 {params['price_range_percent']}): ").strip() 358 | price_range = float(range_input) if range_input else params['price_range_percent'] 359 | 360 | grid_num_input = input(f"網格數量 (默認 {params['grid_num']}): ").strip() 361 | grid_num = int(grid_num_input) if grid_num_input else params['grid_num'] 362 | 363 | quantity_input = input(f"每格訂單數量 (默認 {params.get('order_quantity') or '自動'}): ").strip() 364 | if quantity_input: 365 | order_quantity = float(quantity_input) 366 | # 檢查是否滿足最小訂單大小 367 | if order_quantity < min_order_size: 368 | print(f"警告: 訂單數量 {order_quantity} 小於最小限制 {min_order_size},已自動調整") 369 | order_quantity = min_order_size 370 | else: 371 | order_quantity = params.get('order_quantity') 372 | 373 | # 重新計算 374 | upper_price = round_to_tick_size(current_price * (1 + price_range/100), tick_size) 375 | lower_price = round_to_tick_size(current_price * (1 - price_range/100), tick_size) 376 | actual_gap_pct = (upper_price - lower_price) / (grid_num * current_price) * 100 377 | total_fee_pct = maker_fee_rate * 200 378 | profit_factor = actual_gap_pct / total_fee_pct 379 | 380 | # 計算所需資金 381 | required_base = 0 382 | required_quote = 0 383 | 384 | if order_quantity: 385 | # 計算賣單所需的基礎貨幣(上半部分網格) 386 | sell_grids = (grid_num + 1) // 2 # 上半部分網格數(含中間網格) 387 | required_base = sell_grids * order_quantity 388 | 389 | # 計算買單所需的報價貨幣(下半部分網格) 390 | buy_grids = (grid_num + 1) - sell_grids # 下半部分網格數 391 | avg_buy_price = (current_price + lower_price) / 2 # 平均買入價格 392 | required_quote = buy_grids * order_quantity * avg_buy_price 393 | 394 | adjusted_params = { 395 | "symbol": symbol, 396 | "grid_upper_price": upper_price, 397 | "grid_lower_price": lower_price, 398 | "grid_num": grid_num, 399 | "price_range_percent": price_range, 400 | "grid_gap_percent": actual_gap_pct, 401 | "profit_factor": profit_factor, 402 | "order_quantity": order_quantity, 403 | "current_price": current_price, 404 | "tick_size": tick_size, 405 | "min_order_size": min_order_size, 406 | "base_asset": base_asset, 407 | "quote_asset": quote_asset, 408 | "base_precision": base_precision, 409 | "required_base": required_base, 410 | "required_quote": required_quote, 411 | "estimated_profit_per_grid": f"{(actual_gap_pct - total_fee_pct):.4f}%", 412 | "duration": duration, 413 | "interval": interval 414 | 415 | } 416 | 417 | print("\n--- 調整後參數 ---") 418 | print_grid_params(adjusted_params) 419 | 420 | return adjusted_params 421 | 422 | if __name__ == "__main__": 423 | # 獨立運行時啟動交互式設置 424 | params = interactive_setup() 425 | 426 | # 打印可直接使用的命令行 427 | cmd = f"python run.py --symbol {params['symbol']} --grid-upper {params['grid_upper_price']} --grid-lower {params['grid_lower_price']} --grid-num {params['grid_num']}" 428 | if params.get('order_quantity'): 429 | cmd += f" --quantity {params['order_quantity']}" 430 | if 'duration' in params: 431 | cmd += f" --duration {params['duration']}" 432 | if 'interval' in params: 433 | cmd += f" --interval {params['interval']}" 434 | 435 | print("\n=== 可直接使用的命令 ===") 436 | print(cmd) -------------------------------------------------------------------------------- /cli/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI命令模塊,提供命令行交互功能 3 | """ 4 | import time 5 | from datetime import datetime 6 | 7 | from api.client import ( 8 | get_deposit_address, get_balance, get_markets, get_order_book, 9 | get_ticker, get_fill_history, get_klines 10 | ) 11 | from ws_client.client import BackpackWebSocket 12 | from strategies.grid_trader import GridTrader 13 | from utils.helpers import calculate_volatility 14 | from database.db import Database 15 | from config import API_KEY, SECRET_KEY 16 | from logger import setup_logger 17 | 18 | logger = setup_logger("cli") 19 | 20 | def get_address_command(api_key, secret_key): 21 | """獲取存款地址命令""" 22 | blockchain = input("請輸入區塊鏈名稱(Solana, Ethereum, Bitcoin等): ") 23 | result = get_deposit_address(api_key, secret_key, blockchain) 24 | print(result) 25 | 26 | def get_balance_command(api_key, secret_key): 27 | """獲取餘額命令""" 28 | balances = get_balance(api_key, secret_key) 29 | if isinstance(balances, dict) and "error" in balances and balances["error"]: 30 | print(f"獲取餘額失敗: {balances['error']}") 31 | else: 32 | print("\n當前餘額:") 33 | if isinstance(balances, dict): 34 | for coin, details in balances.items(): 35 | if float(details.get('available', 0)) > 0 or float(details.get('locked', 0)) > 0: 36 | print(f"{coin}: 可用 {details.get('available', 0)}, 凍結 {details.get('locked', 0)}") 37 | else: 38 | print(f"獲取餘額失敗: 無法識別返回格式 {type(balances)}") 39 | 40 | def get_markets_command(): 41 | """獲取市場信息命令""" 42 | print("\n獲取市場信息...") 43 | markets_info = get_markets() 44 | 45 | if isinstance(markets_info, dict) and "error" in markets_info: 46 | print(f"獲取市場信息失敗: {markets_info['error']}") 47 | return 48 | 49 | spot_markets = [m for m in markets_info if m.get('marketType') == 'SPOT'] 50 | print(f"\n找到 {len(spot_markets)} 個現貨市場:") 51 | for i, market in enumerate(spot_markets): 52 | symbol = market.get('symbol') 53 | base = market.get('baseSymbol') 54 | quote = market.get('quoteSymbol') 55 | market_type = market.get('marketType') 56 | print(f"{i+1}. {symbol} ({base}/{quote}) - {market_type}") 57 | 58 | def get_orderbook_command(api_key, secret_key, ws_proxy=None): 59 | """獲取市場深度命令""" 60 | symbol = input("請輸入交易對 (例如: SOL_USDC): ") 61 | try: 62 | print("連接WebSocket獲取實時訂單簿...") 63 | ws = BackpackWebSocket(api_key, secret_key, symbol, auto_reconnect=True, proxy=ws_proxy) 64 | ws.connect() 65 | 66 | # 等待連接建立 67 | wait_time = 0 68 | max_wait_time = 5 69 | while not ws.connected and wait_time < max_wait_time: 70 | time.sleep(0.5) 71 | wait_time += 0.5 72 | 73 | if not ws.connected: 74 | print("WebSocket連接超時,使用REST API獲取訂單簿") 75 | depth = get_order_book(symbol) 76 | else: 77 | # 初始化訂單簿並訂閲深度流 78 | ws.initialize_orderbook() 79 | ws.subscribe_depth() 80 | 81 | # 等待數據更新 82 | time.sleep(2) 83 | depth = ws.get_orderbook() 84 | 85 | print("\n訂單簿:") 86 | print("\n賣單 (從低到高):") 87 | if 'asks' in depth and depth['asks']: 88 | asks = sorted(depth['asks'], key=lambda x: x[0])[:10] # 多展示幾個深度 89 | for i, (price, quantity) in enumerate(asks): 90 | print(f"{i+1}. 價格: {price}, 數量: {quantity}") 91 | else: 92 | print("無賣單數據") 93 | 94 | print("\n買單 (從高到低):") 95 | if 'bids' in depth and depth['bids']: 96 | bids = sorted(depth['bids'], key=lambda x: x[0], reverse=True)[:10] # 多展示幾個深度 97 | for i, (price, quantity) in enumerate(bids): 98 | print(f"{i+1}. 價格: {price}, 數量: {quantity}") 99 | else: 100 | print("無買單數據") 101 | 102 | # 分析市場情緒 103 | if ws.connected: 104 | liquidity_profile = ws.get_liquidity_profile() 105 | if liquidity_profile: 106 | buy_volume = liquidity_profile['bid_volume'] 107 | sell_volume = liquidity_profile['ask_volume'] 108 | imbalance = liquidity_profile['imbalance'] 109 | 110 | print("\n市場流動性分析:") 111 | print(f"買單量: {buy_volume:.4f}") 112 | print(f"賣單量: {sell_volume:.4f}") 113 | print(f"買賣比例: {(buy_volume/sell_volume):.2f}") if sell_volume > 0 else print("買賣比例: 無限") 114 | 115 | # 判斷市場情緒 116 | sentiment = "買方壓力較大" if imbalance > 0.2 else "賣方壓力較大" if imbalance < -0.2 else "買賣壓力平衡" 117 | print(f"市場情緒: {sentiment} ({imbalance:.2f})") 118 | 119 | # 關閉WebSocket連接 120 | ws.close() 121 | 122 | except Exception as e: 123 | print(f"獲取訂單簿失敗: {str(e)}") 124 | # 嘗試使用REST API 125 | try: 126 | depth = get_order_book(symbol) 127 | if isinstance(depth, dict) and "error" in depth: 128 | print(f"獲取訂單簿失敗: {depth['error']}") 129 | return 130 | 131 | print("\n訂單簿 (REST API):") 132 | print("\n賣單 (從低到高):") 133 | if 'asks' in depth and depth['asks']: 134 | asks = sorted([ 135 | [float(price), float(quantity)] for price, quantity in depth['asks'] 136 | ], key=lambda x: x[0])[:10] 137 | for i, (price, quantity) in enumerate(asks): 138 | print(f"{i+1}. 價格: {price}, 數量: {quantity}") 139 | else: 140 | print("無賣單數據") 141 | 142 | print("\n買單 (從高到低):") 143 | if 'bids' in depth and depth['bids']: 144 | bids = sorted([ 145 | [float(price), float(quantity)] for price, quantity in depth['bids'] 146 | ], key=lambda x: x[0], reverse=True)[:10] 147 | for i, (price, quantity) in enumerate(bids): 148 | print(f"{i+1}. 價格: {price}, 數量: {quantity}") 149 | else: 150 | print("無買單數據") 151 | except Exception as e: 152 | print(f"使用REST API獲取訂單簿也失敗: {str(e)}") 153 | 154 | def run_grid_trading_command(api_key, secret_key, ws_proxy=None): 155 | """執行網格交易策略命令""" 156 | symbol = input("請輸入要交易的交易對 (例如: SOL_USDC): ") 157 | markets_info = get_markets() 158 | valid_symbol = False 159 | if isinstance(markets_info, list): 160 | for market in markets_info: 161 | if market.get('symbol') == symbol: 162 | valid_symbol = True 163 | break 164 | 165 | if not valid_symbol: 166 | print(f"交易對 {symbol} 不存在或不可交易") 167 | return 168 | 169 | # 獲取當前市場價格作為參考 170 | current_price = None 171 | try: 172 | ticker = get_ticker(symbol) 173 | if "lastPrice" in ticker: 174 | current_price = float(ticker['lastPrice']) 175 | print(f"當前市場價格: {current_price}") 176 | except Exception as e: 177 | print(f"獲取當前價格失敗: {e}") 178 | 179 | # 網格設置方式 180 | auto_price = input("是否自動設置價格範圍?(y/n): ").lower() == 'y' 181 | 182 | grid_upper_price = None 183 | grid_lower_price = None 184 | price_range_percent = 5.0 185 | 186 | if auto_price: 187 | price_range_percent = float(input("請輸入價格範圍百分比 (例如: 5.0 表示當前價格上下5%): ")) 188 | if current_price: 189 | grid_upper_price = current_price * (1 + price_range_percent/100) 190 | grid_lower_price = current_price * (1 - price_range_percent/100) 191 | print(f"自動設置網格範圍: {grid_lower_price:.6f} - {grid_upper_price:.6f}") 192 | else: 193 | grid_upper_price = float(input("請輸入網格上限價格: ")) 194 | grid_lower_price = float(input("請輸入網格下限價格: ")) 195 | if grid_lower_price >= grid_upper_price: 196 | print("網格下限價格必須小於上限價格") 197 | return 198 | 199 | grid_num = int(input("請輸入網格數量 (例如: 10): ")) 200 | quantity_input = input("請輸入每個網格訂單的數量 (留空則自動根據餘額計算): ") 201 | quantity = float(quantity_input) if quantity_input.strip() else None 202 | 203 | duration = int(input("請輸入運行時間(秒) (例如: 3600 表示1小時): ")) 204 | interval = int(input("請輸入更新間隔(秒) (例如: 60 表示1分鐘): ")) 205 | 206 | try: 207 | # 初始化數據庫 208 | db = Database() 209 | 210 | # 初始化網格交易 211 | grid_trader = GridTrader( 212 | api_key=api_key, 213 | secret_key=secret_key, 214 | symbol=symbol, 215 | db_instance=db, 216 | grid_upper_price=grid_upper_price, 217 | grid_lower_price=grid_lower_price, 218 | grid_num=grid_num, 219 | order_quantity=quantity, 220 | auto_price_range=auto_price, 221 | price_range_percent=price_range_percent, 222 | ws_proxy=ws_proxy 223 | ) 224 | 225 | # 執行網格交易策略 226 | grid_trader.run(duration_seconds=duration, interval_seconds=interval) 227 | 228 | except Exception as e: 229 | print(f"網格交易過程中發生錯誤: {str(e)}") 230 | import traceback 231 | traceback.print_exc() 232 | 233 | def trading_stats_command(api_key, secret_key): 234 | """查看交易統計命令""" 235 | symbol = input("請輸入要查看統計的交易對 (例如: SOL_USDC): ") 236 | 237 | try: 238 | # 初始化數據庫 239 | db = Database() 240 | 241 | # 獲取今日統計 242 | today = datetime.now().strftime('%Y-%m-%d') 243 | today_stats = db.get_trading_stats(symbol, today) 244 | 245 | print("\n=== 網格交易統計 ===") 246 | print(f"交易對: {symbol}") 247 | 248 | if today_stats and len(today_stats) > 0: 249 | stat = today_stats[0] 250 | maker_buy = stat['maker_buy_volume'] 251 | maker_sell = stat['maker_sell_volume'] 252 | taker_buy = stat['taker_buy_volume'] 253 | taker_sell = stat['taker_sell_volume'] 254 | profit = stat['realized_profit'] 255 | fees = stat['total_fees'] 256 | net = stat['net_profit'] 257 | avg_spread = stat.get('avg_spread', 0) 258 | volatility = stat.get('volatility', 0) 259 | 260 | total_volume = maker_buy + maker_sell + taker_buy + taker_sell 261 | maker_percentage = ((maker_buy + maker_sell) / total_volume * 100) if total_volume > 0 else 0 262 | 263 | print(f"\n今日統計 ({today}):") 264 | print(f"總成交量: {total_volume}") 265 | print(f"買入量: {maker_buy + taker_buy}") 266 | print(f"賣出量: {maker_sell + taker_sell}") 267 | print(f"Maker佔比: {maker_percentage:.2f}%") 268 | print(f"波動率: {volatility:.4f}%") 269 | print(f"毛利潤: {profit:.8f}") 270 | print(f"總手續費: {fees:.8f}") 271 | print(f"凈利潤: {net:.8f}") 272 | else: 273 | print(f"今日沒有 {symbol} 的交易記錄") 274 | 275 | # 獲取所有時間的統計 276 | all_time_stats = db.get_all_time_stats(symbol) 277 | 278 | if all_time_stats: 279 | maker_buy = all_time_stats['total_maker_buy'] 280 | maker_sell = all_time_stats['total_maker_sell'] 281 | taker_buy = all_time_stats['total_taker_buy'] 282 | taker_sell = all_time_stats['total_taker_sell'] 283 | profit = all_time_stats['total_profit'] 284 | fees = all_time_stats['total_fees'] 285 | net = all_time_stats['total_net_profit'] 286 | avg_spread = all_time_stats.get('avg_spread_all_time', 0) 287 | 288 | total_volume = maker_buy + maker_sell + taker_buy + taker_sell 289 | maker_percentage = ((maker_buy + maker_sell) / total_volume * 100) if total_volume > 0 else 0 290 | 291 | print(f"\n累計統計:") 292 | print(f"總成交量: {total_volume}") 293 | print(f"買入量: {maker_buy + taker_buy}") 294 | print(f"賣出量: {maker_sell + taker_sell}") 295 | print(f"Maker佔比: {maker_percentage:.2f}%") 296 | print(f"毛利潤: {profit:.8f}") 297 | print(f"總手續費: {fees:.8f}") 298 | print(f"凈利潤: {net:.8f}") 299 | else: 300 | print(f"沒有 {symbol} 的歷史交易記錄") 301 | 302 | # 獲取最近交易 303 | recent_trades = db.get_recent_trades(symbol, 10) 304 | 305 | if recent_trades and len(recent_trades) > 0: 306 | print("\n最近10筆成交:") 307 | for i, trade in enumerate(recent_trades): 308 | maker_str = "Maker" if trade['maker'] else "Taker" 309 | print(f"{i+1}. {trade['timestamp']} - {trade['side']} {trade['quantity']} @ {trade['price']} ({maker_str}) 手續費: {trade['fee']:.8f}") 310 | else: 311 | print(f"沒有 {symbol} 的最近成交記錄") 312 | 313 | # 關閉數據庫連接 314 | db.close() 315 | 316 | except Exception as e: 317 | print(f"查看交易統計時發生錯誤: {str(e)}") 318 | import traceback 319 | traceback.print_exc() 320 | 321 | def market_analysis_command(api_key, secret_key, ws_proxy=None): 322 | """市場分析命令""" 323 | symbol = input("請輸入要分析的交易對 (例如: SOL_USDC): ") 324 | try: 325 | print("\n執行市場分析...") 326 | 327 | # 創建臨時WebSocket連接 328 | ws = BackpackWebSocket(api_key, secret_key, symbol, auto_reconnect=True, proxy=ws_proxy) 329 | ws.connect() 330 | 331 | # 等待連接建立 332 | wait_time = 0 333 | max_wait_time = 5 334 | while not ws.connected and wait_time < max_wait_time: 335 | time.sleep(0.5) 336 | wait_time += 0.5 337 | 338 | if not ws.connected: 339 | print("WebSocket連接超時,無法進行完整分析") 340 | else: 341 | # 初始化訂單簿 342 | ws.initialize_orderbook() 343 | 344 | # 訂閲必要數據流 345 | ws.subscribe_depth() 346 | ws.subscribe_bookTicker() 347 | 348 | # 等待數據更新 349 | print("等待數據更新...") 350 | time.sleep(3) 351 | 352 | # 獲取K線數據分析趨勢 353 | print("獲取歷史數據分析趨勢...") 354 | klines = get_klines(symbol, "15m") 355 | 356 | # 添加調試信息查看數據結構 357 | print("K線數據結構: ") 358 | if isinstance(klines, dict) and "error" in klines: 359 | print(f"獲取K線數據出錯: {klines['error']}") 360 | else: 361 | print(f"收到 {len(klines) if isinstance(klines, list) else type(klines)} 條K線數據") 362 | 363 | # 檢查第一條記錄以確定結構 364 | if isinstance(klines, list) and len(klines) > 0: 365 | print(f"第一條K線數據: {klines[0]}") 366 | 367 | # 根據實際結構提取收盤價 368 | try: 369 | if isinstance(klines[0], dict): 370 | if 'close' in klines[0]: 371 | # 如果是包含'close'字段的字典 372 | prices = [float(kline['close']) for kline in klines] 373 | elif 'c' in klines[0]: 374 | # 另一種常見格式 375 | prices = [float(kline['c']) for kline in klines] 376 | else: 377 | print(f"無法識別的字典K線格式,可用字段: {list(klines[0].keys())}") 378 | raise ValueError("無法識別的K線數據格式") 379 | elif isinstance(klines[0], list): 380 | # 如果是列表格式,打印元素數量和數據樣例 381 | print(f"K線列表格式,每條記錄有 {len(klines[0])} 個元素") 382 | if len(klines[0]) >= 5: 383 | # 通常第4或第5個元素是收盤價 384 | try: 385 | # 嘗試第4個元素 (索引3) 386 | prices = [float(kline[3]) for kline in klines] 387 | print("使用索引3作為收盤價") 388 | except (ValueError, IndexError): 389 | # 如果失敗,嘗試第5個元素 (索引4) 390 | prices = [float(kline[4]) for kline in klines] 391 | print("使用索引4作為收盤價") 392 | else: 393 | print("K線記錄元素數量不足") 394 | raise ValueError("K線數據格式不兼容") 395 | else: 396 | print(f"未知的K線數據類型: {type(klines[0])}") 397 | raise ValueError("未知的K線數據類型") 398 | 399 | # 計算移動平均 400 | short_ma = sum(prices[-5:]) / 5 if len(prices) >= 5 else sum(prices) / len(prices) 401 | medium_ma = sum(prices[-20:]) / 20 if len(prices) >= 20 else short_ma 402 | long_ma = sum(prices[-50:]) / 50 if len(prices) >= 50 else medium_ma 403 | 404 | # 判斷趨勢 405 | trend = "上漲" if short_ma > medium_ma > long_ma else "下跌" if short_ma < medium_ma < long_ma else "盤整" 406 | 407 | # 計算波動率 408 | volatility = calculate_volatility(prices) 409 | 410 | print("\n市場趨勢分析:") 411 | print(f"短期均價 (5週期): {short_ma:.6f}") 412 | print(f"中期均價 (20週期): {medium_ma:.6f}") 413 | print(f"長期均價 (50週期): {long_ma:.6f}") 414 | print(f"當前趨勢: {trend}") 415 | print(f"波動率: {volatility:.2f}%") 416 | 417 | # 獲取最新價格和波動性指標 418 | current_price = ws.get_current_price() 419 | liquidity_profile = ws.get_liquidity_profile() 420 | 421 | if current_price and liquidity_profile: 422 | print(f"\n當前價格: {current_price}") 423 | print(f"相對長期均價: {(current_price / long_ma - 1) * 100:.2f}%") 424 | 425 | # 流動性分析 426 | buy_volume = liquidity_profile['bid_volume'] 427 | sell_volume = liquidity_profile['ask_volume'] 428 | imbalance = liquidity_profile['imbalance'] 429 | 430 | print("\n市場流動性分析:") 431 | print(f"買單量: {buy_volume:.4f}") 432 | print(f"賣單量: {sell_volume:.4f}") 433 | print(f"買賣比例: {(buy_volume/sell_volume):.2f}" if sell_volume > 0 else "買賣比例: 無限") 434 | 435 | # 判斷市場情緒 436 | sentiment = "買方壓力較大" if imbalance > 0.2 else "賣方壓力較大" if imbalance < -0.2 else "買賣壓力平衡" 437 | print(f"市場情緒: {sentiment} ({imbalance:.2f})") 438 | 439 | # 給出建議的網格參數 440 | print("\n建議網格參數:") 441 | 442 | # 根據波動率調整網格範圍 443 | suggested_range_percent = max(2.0, min(10.0, volatility * 0.8)) 444 | suggested_upper = current_price * (1 + suggested_range_percent/100) 445 | suggested_lower = current_price * (1 - suggested_range_percent/100) 446 | 447 | print(f"建議網格範圍: {suggested_lower:.6f} - {suggested_upper:.6f} ({suggested_range_percent:.2f}%)") 448 | 449 | # 根據波動性和流動性建議網格數量 450 | suggested_grid_num = 10 451 | if volatility > 5: 452 | suggested_grid_num = 15 # 高波動性,更多網格 453 | elif volatility < 2: 454 | suggested_grid_num = 8 # 低波動性,較少網格 455 | 456 | print(f"建議網格數量: {suggested_grid_num}") 457 | 458 | # 根據趨勢建議執行模式 459 | if trend == "上漲": 460 | print("建議執行模式: 網格上移策略 (上限設高,下限適中)") 461 | elif trend == "下跌": 462 | print("建議執行模式: 網格下移策略 (下限設低,上限適中)") 463 | else: 464 | print("建議執行模式: 標準網格策略") 465 | except Exception as e: 466 | print(f"處理K線數據時出錯: {e}") 467 | import traceback 468 | traceback.print_exc() 469 | else: 470 | print("未收到有效的K線數據") 471 | 472 | # 關閉WebSocket連接 473 | if ws: 474 | ws.close() 475 | 476 | except Exception as e: 477 | print(f"市場分析時發生錯誤: {str(e)}") 478 | import traceback 479 | traceback.print_exc() 480 | 481 | def main_cli(api_key=API_KEY, secret_key=SECRET_KEY, ws_proxy=None): 482 | """主CLI函數""" 483 | while True: 484 | print("\n===== Backpack Exchange 交易程序 =====") 485 | print("1 - 查詢存款地址") 486 | print("2 - 查詢餘額") 487 | print("3 - 獲取市場信息") 488 | print("4 - 獲取訂單簿") 489 | print("5 - 執行網格交易策略") 490 | print("6 - 交易統計報表") 491 | print("7 - 市場分析") 492 | print("8 - 退出") 493 | 494 | operation = input("請輸入操作類型: ") 495 | 496 | if operation == '1': 497 | get_address_command(api_key, secret_key) 498 | elif operation == '2': 499 | get_balance_command(api_key, secret_key) 500 | elif operation == '3': 501 | get_markets_command() 502 | elif operation == '4': 503 | get_orderbook_command(api_key, secret_key, ws_proxy=ws_proxy) 504 | elif operation == '5': 505 | run_grid_trading_command(api_key, secret_key, ws_proxy=ws_proxy) 506 | elif operation == '6': 507 | trading_stats_command(api_key, secret_key) 508 | elif operation == '7': 509 | market_analysis_command(api_key, secret_key, ws_proxy=ws_proxy) 510 | elif operation == '8': 511 | print("退出程序。") 512 | break 513 | else: 514 | print("輸入錯誤,請重新輸入。") -------------------------------------------------------------------------------- /database/db.py: -------------------------------------------------------------------------------- 1 | """ 2 | 數據庫操作模塊 3 | """ 4 | import sqlite3 5 | from datetime import datetime 6 | from typing import Dict, List, Tuple, Any, Optional 7 | from config import DB_PATH 8 | from logger import setup_logger 9 | 10 | logger = setup_logger("database") 11 | 12 | class Database: 13 | def __init__(self, db_path=DB_PATH): 14 | """ 15 | 初始化數據庫連接 16 | 17 | Args: 18 | db_path: 數據庫文件路徑 19 | """ 20 | self.db_path = db_path 21 | self.conn = None 22 | self.cursor = None 23 | self._connect() 24 | self._init_tables() 25 | 26 | def _connect(self): 27 | """建立數據庫連接""" 28 | try: 29 | self.conn = sqlite3.connect(self.db_path, check_same_thread=False) 30 | # 主游標只用於初始化 31 | self.cursor = self.conn.cursor() 32 | logger.info(f"數據庫連接成功: {self.db_path}") 33 | except Exception as e: 34 | logger.error(f"數據庫連接失敗: {e}") 35 | raise 36 | 37 | def _init_tables(self): 38 | """初始化資料庫表結構""" 39 | try: 40 | # 改進的交易記錄表 41 | self.cursor.execute( 42 | """ 43 | CREATE TABLE IF NOT EXISTS completed_orders ( 44 | id INTEGER PRIMARY KEY AUTOINCREMENT, 45 | order_id TEXT, 46 | symbol TEXT, 47 | side TEXT, 48 | quantity REAL, 49 | price REAL, 50 | maker BOOLEAN, 51 | fee REAL, 52 | fee_asset TEXT, 53 | trade_type TEXT, 54 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 55 | ) 56 | """ 57 | ) 58 | 59 | # 創建索引提高查詢效率 60 | self.cursor.execute( 61 | """ 62 | CREATE INDEX IF NOT EXISTS idx_completed_orders_symbol 63 | ON completed_orders(symbol) 64 | """ 65 | ) 66 | 67 | # 統計表來跟蹤每日/每週成交量和利潤 68 | self.cursor.execute( 69 | """ 70 | CREATE TABLE IF NOT EXISTS trading_stats ( 71 | id INTEGER PRIMARY KEY AUTOINCREMENT, 72 | date TEXT, 73 | symbol TEXT, 74 | maker_buy_volume REAL DEFAULT 0, 75 | maker_sell_volume REAL DEFAULT 0, 76 | taker_buy_volume REAL DEFAULT 0, 77 | taker_sell_volume REAL DEFAULT 0, 78 | realized_profit REAL DEFAULT 0, 79 | total_fees REAL DEFAULT 0, 80 | net_profit REAL DEFAULT 0, 81 | avg_spread REAL DEFAULT 0, 82 | trade_count INTEGER DEFAULT 0, 83 | volatility REAL DEFAULT 0 84 | ) 85 | """ 86 | ) 87 | 88 | # 重平衡訂單記錄表 89 | self.cursor.execute( 90 | """ 91 | CREATE TABLE IF NOT EXISTS rebalance_orders ( 92 | id INTEGER PRIMARY KEY AUTOINCREMENT, 93 | order_id TEXT, 94 | symbol TEXT, 95 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 96 | ) 97 | """ 98 | ) 99 | 100 | # 市場數據表 101 | self.cursor.execute( 102 | """ 103 | CREATE TABLE IF NOT EXISTS market_data ( 104 | id INTEGER PRIMARY KEY AUTOINCREMENT, 105 | symbol TEXT, 106 | price REAL, 107 | volume REAL, 108 | bid_ask_spread REAL, 109 | liquidity_score REAL, 110 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 111 | ) 112 | """ 113 | ) 114 | 115 | try: 116 | self.conn.commit() 117 | logger.info("數據庫表初始化成功") 118 | except sqlite3.OperationalError as e: 119 | logger.debug(f"提交表初始化時出錯: {e}") 120 | except Exception as e: 121 | logger.error(f"初始化資料庫表時出錯: {e}") 122 | try: 123 | self.conn.rollback() 124 | except sqlite3.OperationalError: 125 | # 忽略"no transaction is active"錯誤 126 | pass 127 | raise 128 | 129 | def execute(self, query, params=None): 130 | """ 131 | 執行SQL查詢 - 創建新游標避免遞歸問題 132 | 133 | Args: 134 | query: SQL查詢字符串 135 | params: 查詢參數 136 | 137 | Returns: 138 | 游標對象 139 | """ 140 | try: 141 | # 每次操作創建新游標 142 | cursor = self.conn.cursor() 143 | if params: 144 | return cursor.execute(query, params) 145 | else: 146 | return cursor.execute(query) 147 | except sqlite3.OperationalError as e: 148 | error_str = str(e) 149 | if "within a transaction" in error_str: 150 | # 如果是事務錯誤,嘗試提交或回滾後重試 151 | logger.warning(f"事務錯誤: {e}, 嘗試重設事務後重試") 152 | try: 153 | self.conn.commit() 154 | except: 155 | try: 156 | self.conn.rollback() 157 | except: 158 | pass 159 | 160 | # 重新嘗試 161 | cursor = self.conn.cursor() 162 | if params: 163 | return cursor.execute(query, params) 164 | else: 165 | return cursor.execute(query) 166 | else: 167 | logger.error(f"SQL執行錯誤: {e}, 查詢: {query}") 168 | raise 169 | except Exception as e: 170 | logger.error(f"SQL執行錯誤: {e}, 查詢: {query}") 171 | raise 172 | 173 | def executemany(self, query, params_list): 174 | """ 175 | 批量執行SQL查詢 - 創建新游標避免遞歸問題 176 | 177 | Args: 178 | query: SQL查詢字符串 179 | params_list: 參數列表,每個元素對應一次執行 180 | 181 | Returns: 182 | 游標對象 183 | """ 184 | try: 185 | # 每次操作創建新游標 186 | cursor = self.conn.cursor() 187 | return cursor.executemany(query, params_list) 188 | except sqlite3.OperationalError as e: 189 | error_str = str(e) 190 | if "within a transaction" in error_str: 191 | # 如果是事務錯誤,嘗試提交或回滾後重試 192 | logger.warning(f"批量事務錯誤: {e}, 嘗試重設事務後重試") 193 | try: 194 | self.conn.commit() 195 | except: 196 | try: 197 | self.conn.rollback() 198 | except: 199 | pass 200 | 201 | # 重新嘗試 202 | cursor = self.conn.cursor() 203 | return cursor.executemany(query, params_list) 204 | else: 205 | logger.error(f"批量SQL執行錯誤: {e}, 查詢: {query}") 206 | raise 207 | except Exception as e: 208 | logger.error(f"批量SQL執行錯誤: {e}, 查詢: {query}") 209 | raise 210 | 211 | def commit(self): 212 | """提交事務""" 213 | try: 214 | self.conn.commit() 215 | except sqlite3.OperationalError as e: 216 | # 忽略"no transaction is active"錯誤 217 | logger.debug(f"提交事務時發生操作錯誤: {e}") 218 | pass 219 | 220 | def rollback(self): 221 | """回滾事務""" 222 | try: 223 | self.conn.rollback() 224 | except sqlite3.OperationalError as e: 225 | # 忽略"no transaction is active"錯誤 226 | logger.debug(f"回滾事務時發生操作錯誤: {e}") 227 | pass 228 | 229 | def close(self): 230 | """關閉數據庫連接""" 231 | if self.conn: 232 | self.conn.close() 233 | logger.info("數據庫連接已關閉") 234 | 235 | def insert_order(self, order_data): 236 | """ 237 | 插入訂單記錄 238 | 239 | Args: 240 | order_data: 訂單數據字典 241 | 242 | Returns: 243 | 插入的行ID 244 | """ 245 | try: 246 | # 檢查是否有活動交易 247 | try: 248 | # 先嘗試提交任何可能存在的交易 249 | self.conn.commit() 250 | except sqlite3.OperationalError: 251 | # 忽略"no transaction is active"錯誤 252 | pass 253 | 254 | query = """ 255 | INSERT INTO completed_orders 256 | (order_id, symbol, side, quantity, price, maker, fee, fee_asset, trade_type) 257 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 258 | """ 259 | params = ( 260 | order_data['order_id'], 261 | order_data['symbol'], 262 | order_data['side'], 263 | order_data['quantity'], 264 | order_data['price'], 265 | 1 if order_data['maker'] else 0, 266 | order_data['fee'], 267 | order_data['fee_asset'], 268 | order_data['trade_type'] 269 | ) 270 | 271 | cursor = self.execute(query, params) 272 | self.commit() 273 | return cursor.lastrowid 274 | except Exception as e: 275 | logger.error(f"插入訂單記錄時出錯: {e}") 276 | # 嘗試回滾可能存在的事務 277 | try: 278 | self.conn.rollback() 279 | except sqlite3.OperationalError: 280 | # 忽略"no transaction is active"錯誤 281 | pass 282 | return None 283 | 284 | def record_rebalance_order(self, order_id, symbol): 285 | """ 286 | 記錄重平衡訂單 287 | 288 | Args: 289 | order_id: 訂單ID 290 | symbol: 交易對符號 291 | 292 | Returns: 293 | 插入的行ID 294 | """ 295 | try: 296 | # 檢查是否有活動交易 297 | try: 298 | # 先嘗試提交任何可能存在的交易 299 | self.conn.commit() 300 | except sqlite3.OperationalError: 301 | # 忽略"no transaction is active"錯誤 302 | pass 303 | 304 | query = """ 305 | INSERT INTO rebalance_orders (order_id, symbol) 306 | VALUES (?, ?) 307 | """ 308 | cursor = self.execute(query, (order_id, symbol)) 309 | self.commit() 310 | return cursor.lastrowid 311 | except Exception as e: 312 | logger.error(f"記錄重平衡訂單時出錯: {e}") 313 | # 嘗試回滾可能存在的事務 314 | try: 315 | self.conn.rollback() 316 | except sqlite3.OperationalError: 317 | # 忽略"no transaction is active"錯誤 318 | pass 319 | return None 320 | 321 | def is_rebalance_order(self, order_id, symbol): 322 | """ 323 | 檢查訂單是否為重平衡訂單 324 | 325 | Args: 326 | order_id: 訂單ID 327 | symbol: 交易對符號 328 | 329 | Returns: 330 | 布爾值,表示是否為重平衡訂單 331 | """ 332 | try: 333 | # 檢查是否有活動交易 334 | try: 335 | # 先嘗試提交任何可能存在的交易 336 | self.conn.commit() 337 | except sqlite3.OperationalError: 338 | # 忽略"no transaction is active"錯誤 339 | pass 340 | 341 | query = """ 342 | SELECT id FROM rebalance_orders 343 | WHERE order_id = ? AND symbol = ? 344 | """ 345 | cursor = self.execute(query, (order_id, symbol)) 346 | result = cursor.fetchone() 347 | cursor.close() # 關閉游標 348 | return result is not None 349 | except Exception as e: 350 | logger.error(f"檢查重平衡訂單時出錯: {e}") 351 | return False 352 | 353 | def update_market_data(self, market_data): 354 | """ 355 | 更新市場數據 356 | 357 | Args: 358 | market_data: 市場數據字典 359 | 360 | Returns: 361 | 插入的行ID 362 | """ 363 | cursor = None 364 | try: 365 | # 檢查是否有活動交易並嘗試提交 366 | try: 367 | self.conn.commit() 368 | except sqlite3.OperationalError: 369 | # 忽略"no transaction is active"錯誤 370 | pass 371 | 372 | # 創建新的游標 373 | cursor = self.conn.cursor() 374 | 375 | query = """ 376 | INSERT INTO market_data 377 | (symbol, price, volume, bid_ask_spread, liquidity_score) 378 | VALUES (?, ?, ?, ?, ?) 379 | """ 380 | params = ( 381 | market_data['symbol'], 382 | market_data['price'], 383 | market_data['volume'], 384 | market_data['bid_ask_spread'], 385 | market_data['liquidity_score'] 386 | ) 387 | 388 | cursor.execute(query, params) 389 | lastrowid = cursor.lastrowid 390 | 391 | # 提交事務 392 | self.conn.commit() 393 | return lastrowid 394 | except Exception as e: 395 | logger.error(f"更新市場數據時出錯: {e}") 396 | # 嘗試回滾可能存在的事務 397 | try: 398 | self.conn.rollback() 399 | except sqlite3.OperationalError: 400 | # 忽略"no transaction is active"錯誤 401 | pass 402 | return None 403 | finally: 404 | # 確保無論如何都關閉游標 405 | if cursor: 406 | cursor.close() 407 | 408 | def update_trading_stats(self, stats_data): 409 | """ 410 | 更新交易統計數據 411 | 412 | Args: 413 | stats_data: 統計數據字典 414 | 415 | Returns: 416 | 布爾值,表示更新是否成功 417 | """ 418 | cursor = None 419 | try: 420 | # 嘗試提交任何可能存在的交易 421 | try: 422 | self.conn.commit() 423 | except sqlite3.OperationalError: 424 | # 忽略"no transaction is active"錯誤 425 | pass 426 | 427 | # 創建獨立的游標進行全部操作 428 | cursor = self.conn.cursor() 429 | 430 | # 檢查今天的記錄是否存在 431 | check_query = """ 432 | SELECT id FROM trading_stats 433 | WHERE date = ? AND symbol = ? 434 | """ 435 | cursor.execute(check_query, (stats_data['date'], stats_data['symbol'])) 436 | record = cursor.fetchone() 437 | 438 | if record: 439 | # 更新現有記錄 440 | update_query = """ 441 | UPDATE trading_stats 442 | SET maker_buy_volume = ?, 443 | maker_sell_volume = ?, 444 | taker_buy_volume = ?, 445 | taker_sell_volume = ?, 446 | realized_profit = ?, 447 | total_fees = ?, 448 | net_profit = ?, 449 | avg_spread = ?, 450 | trade_count = ?, 451 | volatility = ? 452 | WHERE date = ? AND symbol = ? 453 | """ 454 | params = ( 455 | stats_data['maker_buy_volume'], 456 | stats_data['maker_sell_volume'], 457 | stats_data['taker_buy_volume'], 458 | stats_data['taker_sell_volume'], 459 | stats_data['realized_profit'], 460 | stats_data['total_fees'], 461 | stats_data['net_profit'], 462 | stats_data['avg_spread'], 463 | stats_data['trade_count'], 464 | stats_data['volatility'], 465 | stats_data['date'], 466 | stats_data['symbol'] 467 | ) 468 | cursor.execute(update_query, params) 469 | else: 470 | # 創建新記錄 471 | insert_query = """ 472 | INSERT INTO trading_stats 473 | (date, symbol, maker_buy_volume, maker_sell_volume, taker_buy_volume, taker_sell_volume, 474 | realized_profit, total_fees, net_profit, avg_spread, trade_count, volatility) 475 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 476 | """ 477 | params = ( 478 | stats_data['date'], 479 | stats_data['symbol'], 480 | stats_data['maker_buy_volume'], 481 | stats_data['maker_sell_volume'], 482 | stats_data['taker_buy_volume'], 483 | stats_data['taker_sell_volume'], 484 | stats_data['realized_profit'], 485 | stats_data['total_fees'], 486 | stats_data['net_profit'], 487 | stats_data['avg_spread'], 488 | stats_data['trade_count'], 489 | stats_data['volatility'] 490 | ) 491 | cursor.execute(insert_query, params) 492 | 493 | # 提交事務 494 | self.conn.commit() 495 | return True 496 | except Exception as e: 497 | logger.error(f"更新交易統計時出錯: {e}") 498 | # 嘗試回滾可能存在的事務 499 | try: 500 | self.conn.rollback() 501 | except sqlite3.OperationalError: 502 | # 忽略"no transaction is active"錯誤 503 | pass 504 | return False 505 | finally: 506 | # 確保無論如何都關閉游標 507 | if cursor: 508 | cursor.close() 509 | 510 | def get_trading_stats(self, symbol, date=None): 511 | """ 512 | 獲取交易統計數據 513 | 514 | Args: 515 | symbol: 交易對符號 516 | date: 日期字符串,如果為None則獲取所有日期的統計 517 | 518 | Returns: 519 | 統計數據列表 520 | """ 521 | try: 522 | # 檢查是否有活動交易 523 | try: 524 | # 先嘗試提交任何可能存在的交易 525 | self.conn.commit() 526 | except sqlite3.OperationalError: 527 | # 忽略"no transaction is active"錯誤 528 | pass 529 | 530 | cursor = self.conn.cursor() # 創建新游標 531 | 532 | if date: 533 | query = """ 534 | SELECT * FROM trading_stats 535 | WHERE symbol = ? AND date = ? 536 | """ 537 | cursor.execute(query, (symbol, date)) 538 | else: 539 | query = """ 540 | SELECT * FROM trading_stats 541 | WHERE symbol = ? 542 | ORDER BY date DESC 543 | """ 544 | cursor.execute(query, (symbol,)) 545 | 546 | columns = [description[0] for description in cursor.description] 547 | result = [] 548 | for row in cursor.fetchall(): 549 | result.append(dict(zip(columns, row))) 550 | 551 | cursor.close() # 關閉游標 552 | return result 553 | except Exception as e: 554 | logger.error(f"獲取交易統計時出錯: {e}") 555 | return [] 556 | 557 | def get_all_time_stats(self, symbol): 558 | """ 559 | 獲取所有時間的總計統計數據 560 | 561 | Args: 562 | symbol: 交易對符號 563 | 564 | Returns: 565 | 總計統計數據字典 566 | """ 567 | cursor = self.conn.cursor() # 創建新游標 568 | 569 | query = """ 570 | SELECT 571 | SUM(maker_buy_volume) as total_maker_buy, 572 | SUM(maker_sell_volume) as total_maker_sell, 573 | SUM(taker_buy_volume) as total_taker_buy, 574 | SUM(taker_sell_volume) as total_taker_sell, 575 | SUM(realized_profit) as total_profit, 576 | SUM(total_fees) as total_fees, 577 | SUM(net_profit) as total_net_profit, 578 | AVG(avg_spread) as avg_spread_all_time 579 | FROM trading_stats 580 | WHERE symbol = ? 581 | """ 582 | cursor.execute(query, (symbol,)) 583 | result = cursor.fetchone() 584 | 585 | if result and result[0] is not None: 586 | columns = ['total_maker_buy', 'total_maker_sell', 'total_taker_buy', 587 | 'total_taker_sell', 'total_profit', 'total_fees', 588 | 'total_net_profit', 'avg_spread_all_time'] 589 | stat_dict = dict(zip(columns, result)) 590 | cursor.close() # 關閉游標 591 | return stat_dict 592 | 593 | cursor.close() # 關閉游標 594 | return None 595 | 596 | def get_recent_trades(self, symbol, limit=10): 597 | """ 598 | 獲取最近的成交記錄 599 | 600 | Args: 601 | symbol: 交易對符號 602 | limit: 返回記錄數量限制 603 | 604 | Returns: 605 | 成交記錄列表 606 | """ 607 | cursor = self.conn.cursor() # 創建新游標 608 | 609 | query = """ 610 | SELECT side, quantity, price, maker, fee, timestamp 611 | FROM completed_orders 612 | WHERE symbol = ? 613 | ORDER BY timestamp DESC 614 | LIMIT ? 615 | """ 616 | cursor.execute(query, (symbol, limit)) 617 | 618 | columns = ['side', 'quantity', 'price', 'maker', 'fee', 'timestamp'] 619 | result = [] 620 | for row in cursor.fetchall(): 621 | result.append(dict(zip(columns, row))) 622 | 623 | cursor.close() # 關閉游標 624 | return result 625 | 626 | def get_order_history(self, symbol, limit=1000): 627 | """ 628 | 獲取訂單歷史 629 | 630 | Args: 631 | symbol: 交易對符號 632 | limit: 返回記錄數量限制 633 | 634 | Returns: 635 | 訂單記錄列表 636 | """ 637 | cursor = self.conn.cursor() # 創建新游標 638 | 639 | query = """ 640 | SELECT side, quantity, price, maker, fee 641 | FROM completed_orders 642 | WHERE symbol = ? 643 | ORDER BY timestamp DESC 644 | LIMIT ? 645 | """ 646 | cursor.execute(query, (symbol, limit)) 647 | 648 | result = cursor.fetchall() 649 | cursor.close() # 關閉游標 650 | return result -------------------------------------------------------------------------------- /ws_client/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebSocket客户端模塊 3 | """ 4 | import json 5 | import time 6 | import threading 7 | import websocket as ws 8 | from typing import Dict, List, Tuple, Any, Optional, Callable 9 | from config import WS_URL, DEFAULT_WINDOW 10 | from api.auth import create_signature 11 | from api.client import get_order_book 12 | from utils.helpers import calculate_volatility 13 | from logger import setup_logger 14 | 15 | logger = setup_logger("backpack_ws") 16 | 17 | class BackpackWebSocket: 18 | def __init__(self, api_key, secret_key, symbol, on_message_callback=None, auto_reconnect=True, proxy=None): 19 | """ 20 | 初始化WebSocket客户端 21 | 22 | Args: 23 | api_key: API密鑰 24 | secret_key: API密鑰 25 | symbol: 交易對符號 26 | on_message_callback: 消息回調函數 27 | auto_reconnect: 是否自動重連 28 | proxy: wss代理 支持格式为 http://user:pass@host:port/ 或者 http://host:port 29 | 30 | """ 31 | self.api_key = api_key 32 | self.secret_key = secret_key 33 | self.symbol = symbol 34 | self.ws = None 35 | self.on_message_callback = on_message_callback 36 | self.connected = False 37 | self.last_price = None 38 | self.bid_price = None 39 | self.ask_price = None 40 | self.orderbook = {"bids": [], "asks": []} 41 | self.order_updates = [] 42 | self.historical_prices = [] # 儲存歷史價格用於計算波動率 43 | self.max_price_history = 100 # 最多儲存的價格數量 44 | 45 | # 重連相關參數 46 | self.auto_reconnect = auto_reconnect 47 | self.reconnect_delay = 1 48 | self.max_reconnect_delay = 30 49 | self.reconnect_attempts = 0 50 | self.max_reconnect_attempts = 10 51 | self.running = False 52 | self.ws_thread = None 53 | 54 | # 記錄已訂閲的頻道 55 | self.subscriptions = [] 56 | 57 | # 添加WebSocket執行緒鎖 58 | self.ws_lock = threading.Lock() 59 | 60 | # 添加心跳檢測 61 | self.last_heartbeat = time.time() 62 | self.heartbeat_interval = 30 63 | self.heartbeat_thread = None 64 | 65 | # 添加代理参数 66 | self.proxy = proxy 67 | 68 | def initialize_orderbook(self): 69 | """通過REST API獲取訂單簿初始快照""" 70 | try: 71 | # 使用REST API獲取完整訂單簿 72 | order_book = get_order_book(self.symbol, 100) # 增加深度 73 | if isinstance(order_book, dict) and "error" in order_book: 74 | logger.error(f"初始化訂單簿失敗: {order_book['error']}") 75 | return False 76 | 77 | # 重置並填充orderbook數據結構 78 | self.orderbook = { 79 | "bids": [[float(price), float(quantity)] for price, quantity in order_book.get('bids', [])], 80 | "asks": [[float(price), float(quantity)] for price, quantity in order_book.get('asks', [])] 81 | } 82 | 83 | # 按價格排序 84 | self.orderbook["bids"] = sorted(self.orderbook["bids"], key=lambda x: x[0], reverse=True) 85 | self.orderbook["asks"] = sorted(self.orderbook["asks"], key=lambda x: x[0]) 86 | 87 | logger.info(f"訂單簿初始化成功: {len(self.orderbook['bids'])} 個買單, {len(self.orderbook['asks'])} 個賣單") 88 | 89 | # 初始化最高買價和最低賣價 90 | if self.orderbook["bids"]: 91 | self.bid_price = self.orderbook["bids"][0][0] 92 | if self.orderbook["asks"]: 93 | self.ask_price = self.orderbook["asks"][0][0] 94 | if self.bid_price and self.ask_price: 95 | self.last_price = (self.bid_price + self.ask_price) / 2 96 | self.add_price_to_history(self.last_price) 97 | 98 | return True 99 | except Exception as e: 100 | logger.error(f"初始化訂單簿時出錯: {e}") 101 | return False 102 | 103 | def add_price_to_history(self, price): 104 | """添加價格到歷史記錄用於計算波動率""" 105 | if price: 106 | self.historical_prices.append(price) 107 | # 保持歷史記錄在設定長度內 108 | if len(self.historical_prices) > self.max_price_history: 109 | self.historical_prices = self.historical_prices[-self.max_price_history:] 110 | 111 | def get_volatility(self, window=20): 112 | """獲取當前波動率""" 113 | return calculate_volatility(self.historical_prices, window) 114 | 115 | def start_heartbeat(self): 116 | """開始心跳檢測線程""" 117 | if self.heartbeat_thread is None or not self.heartbeat_thread.is_alive(): 118 | self.heartbeat_thread = threading.Thread(target=self._heartbeat_check, daemon=True) 119 | self.heartbeat_thread.start() 120 | 121 | def _heartbeat_check(self): 122 | """定期檢查WebSocket連接狀態並在需要時重連""" 123 | while self.running: 124 | current_time = time.time() 125 | time_since_last_heartbeat = current_time - self.last_heartbeat 126 | 127 | if time_since_last_heartbeat > self.heartbeat_interval * 2: 128 | logger.warning(f"心跳檢測超時 ({time_since_last_heartbeat:.1f}秒),嘗試重新連接") 129 | self.reconnect() 130 | 131 | time.sleep(5) # 每5秒檢查一次 132 | 133 | def connect(self): 134 | """建立WebSocket連接""" 135 | with self.ws_lock: 136 | self.running = True 137 | self.reconnect_attempts = 0 138 | ws.enableTrace(False) # 使用 ws.enableTrace 而不是 websocket.enableTrace 139 | self.ws = ws.WebSocketApp( # 同樣使用 ws.WebSocketApp 140 | WS_URL, 141 | on_open=self.on_open, 142 | on_message=self.on_message, 143 | on_error=self.on_error, 144 | on_close=self.on_close, 145 | on_ping=self.on_ping, 146 | on_pong=self.on_pong 147 | ) 148 | self.ws_thread = threading.Thread(target=self.ws_run_forever) 149 | self.ws_thread.daemon = True 150 | self.ws_thread.start() 151 | 152 | # 啟動心跳檢測 153 | self.start_heartbeat() 154 | 155 | def ws_run_forever(self): 156 | try: 157 | # 確保在運行前檢查socket狀態 158 | if hasattr(self.ws, 'sock') and self.ws.sock and self.ws.sock.connected: 159 | logger.debug("發現socket已經打開,跳過run_forever") 160 | return 161 | 162 | proxy_type=None 163 | http_proxy_auth=None 164 | http_proxy_host=None 165 | http_proxy_port=None 166 | if self.proxy and 3<=len(self.proxy.split(":"))<=4: 167 | arrs=self.proxy.split(":") 168 | proxy_type = arrs[0] 169 | arrs[1]=arrs[1][2:] #去掉 // 170 | if len(arrs)==3: 171 | http_proxy_host = arrs[1] 172 | else: 173 | password,http_proxy_host = arrs[2].split("@") 174 | http_proxy_auth=(arrs[1],password) 175 | http_proxy_port = arrs[-1] 176 | 177 | # 添加ping_interval和ping_timeout參數 178 | self.ws.run_forever(ping_interval=self.heartbeat_interval, ping_timeout=10, http_proxy_auth=http_proxy_auth, http_proxy_host=http_proxy_host, http_proxy_port=http_proxy_port, proxy_type=proxy_type) 179 | 180 | except Exception as e: 181 | logger.error(f"WebSocket運行時出錯: {e}") 182 | finally: 183 | with self.ws_lock: 184 | if self.running and self.auto_reconnect and not self.connected: 185 | self.reconnect() 186 | 187 | def on_pong(self, ws, message): 188 | """處理pong響應""" 189 | self.last_heartbeat = time.time() 190 | 191 | def reconnect(self): 192 | """完全斷開並重新建立WebSocket連接""" 193 | with self.ws_lock: 194 | if not self.running or self.reconnect_attempts >= self.max_reconnect_attempts: 195 | logger.warning(f"重連次數超過上限 ({self.max_reconnect_attempts}),停止重連") 196 | return False 197 | 198 | self.reconnect_attempts += 1 199 | delay = min(self.reconnect_delay * (2 ** (self.reconnect_attempts - 1)), self.max_reconnect_delay) 200 | 201 | logger.info(f"嘗試第 {self.reconnect_attempts} 次重連,等待 {delay} 秒...") 202 | time.sleep(delay) 203 | 204 | # 確保完全斷開連接前先標記連接狀態 205 | self.connected = False 206 | 207 | # 完全斷開並清理之前的WebSocket連接 208 | if self.ws: 209 | try: 210 | # 顯式設置內部標記表明這是用户主動關閉 211 | if hasattr(self.ws, '_closed_by_me'): 212 | self.ws._closed_by_me = True 213 | 214 | # 關閉WebSocket 215 | self.ws.close() 216 | self.ws.keep_running = False 217 | 218 | # 強制關閉socket 219 | if hasattr(self.ws, 'sock') and self.ws.sock: 220 | self.ws.sock.close() 221 | self.ws.sock = None 222 | except Exception as e: 223 | logger.error(f"關閉之前的WebSocket連接時出錯: {e}") 224 | 225 | # 給系統更多時間完全關閉連接 226 | time.sleep(1.0) # 增加等待時間 227 | self.ws = None 228 | 229 | # 確保舊的線程已終止 230 | if self.ws_thread and self.ws_thread.is_alive(): 231 | try: 232 | # 更長的超時等待線程終止 233 | self.ws_thread.join(timeout=2) 234 | except Exception as e: 235 | logger.error(f"等待舊線程終止時出錯: {e}") 236 | 237 | # 重置所有相關狀態 238 | self.ws_thread = None 239 | self.subscriptions = [] # 清空訂閲列表,以便重新訂閲 240 | 241 | # 創建全新的WebSocket連接 242 | ws.enableTrace(False) 243 | self.ws = ws.WebSocketApp( 244 | WS_URL, 245 | on_open=self.on_open, 246 | on_message=self.on_message, 247 | on_error=self.on_error, 248 | on_close=self.on_close, 249 | on_ping=self.on_ping, 250 | on_pong=self.on_pong 251 | ) 252 | 253 | # 創建新線程 254 | self.ws_thread = threading.Thread(target=self.ws_run_forever) 255 | self.ws_thread.daemon = True 256 | self.ws_thread.start() 257 | 258 | # 更新最後心跳時間,避免重連後立即觸發心跳檢測 259 | self.last_heartbeat = time.time() 260 | 261 | return True 262 | 263 | def on_ping(self, ws, message): 264 | """處理ping消息""" 265 | try: 266 | self.last_heartbeat = time.time() 267 | if ws and hasattr(ws, 'sock') and ws.sock: 268 | ws.sock.pong(message) 269 | else: 270 | logger.debug("無法迴應ping:WebSocket或sock為None") 271 | except Exception as e: 272 | logger.debug(f"迴應ping失敗: {e}") 273 | 274 | def on_open(self, ws): 275 | """WebSocket打開時的處理""" 276 | logger.info("WebSocket連接已建立") 277 | self.connected = True 278 | self.reconnect_attempts = 0 279 | self.last_heartbeat = time.time() 280 | 281 | # 添加短暫延遲確保連接穩定 282 | time.sleep(0.5) 283 | 284 | # 初始化訂單簿 285 | orderbook_initialized = self.initialize_orderbook() 286 | 287 | # 如果初始化成功,訂閲深度和行情數據 288 | if orderbook_initialized: 289 | if "bookTicker" in self.subscriptions or not self.subscriptions: 290 | self.subscribe_bookTicker() 291 | 292 | if "depth" in self.subscriptions or not self.subscriptions: 293 | self.subscribe_depth() 294 | 295 | # 重新訂閲私有訂單更新流 296 | for sub in self.subscriptions: 297 | if sub.startswith("account."): 298 | self.private_subscribe(sub) 299 | 300 | def subscribe_bookTicker(self): 301 | """訂閲最優價格""" 302 | logger.info(f"訂閲 {self.symbol} 的bookTicker...") 303 | if not self.connected or not self.ws: 304 | logger.warning("WebSocket未連接,無法訂閲bookTicker") 305 | return False 306 | 307 | try: 308 | message = { 309 | "method": "SUBSCRIBE", 310 | "params": [f"bookTicker.{self.symbol}"] 311 | } 312 | self.ws.send(json.dumps(message)) 313 | if "bookTicker" not in self.subscriptions: 314 | self.subscriptions.append("bookTicker") 315 | return True 316 | except Exception as e: 317 | logger.error(f"訂閲bookTicker失敗: {e}") 318 | return False 319 | 320 | def subscribe_depth(self): 321 | """訂閲深度信息""" 322 | logger.info(f"訂閲 {self.symbol} 的深度信息...") 323 | if not self.connected or not self.ws: 324 | logger.warning("WebSocket未連接,無法訂閲深度信息") 325 | return False 326 | 327 | try: 328 | message = { 329 | "method": "SUBSCRIBE", 330 | "params": [f"depth.{self.symbol}"] 331 | } 332 | self.ws.send(json.dumps(message)) 333 | if "depth" not in self.subscriptions: 334 | self.subscriptions.append("depth") 335 | return True 336 | except Exception as e: 337 | logger.error(f"訂閲深度信息失敗: {e}") 338 | return False 339 | 340 | def private_subscribe(self, stream): 341 | """訂閲私有數據流""" 342 | if not self.connected or not self.ws: 343 | logger.warning("WebSocket未連接,無法訂閲私有數據流") 344 | return False 345 | 346 | try: 347 | timestamp = str(int(time.time() * 1000)) 348 | window = DEFAULT_WINDOW 349 | sign_message = f"instruction=subscribe×tamp={timestamp}&window={window}" 350 | signature = create_signature(self.secret_key, sign_message) 351 | 352 | if not signature: 353 | logger.error("簽名創建失敗,無法訂閲私有數據流") 354 | return False 355 | 356 | message = { 357 | "method": "SUBSCRIBE", 358 | "params": [stream], 359 | "signature": [self.api_key, signature, timestamp, window] 360 | } 361 | 362 | self.ws.send(json.dumps(message)) 363 | logger.info(f"已訂閲私有數據流: {stream}") 364 | if stream not in self.subscriptions: 365 | self.subscriptions.append(stream) 366 | return True 367 | except Exception as e: 368 | logger.error(f"訂閲私有數據流失敗: {e}") 369 | return False 370 | 371 | def on_message(self, ws, message): 372 | """處理WebSocket消息""" 373 | try: 374 | data = json.loads(message) 375 | 376 | # 處理ping pong消息 377 | if isinstance(data, dict) and data.get("ping"): 378 | pong_message = {"pong": data.get("ping")} 379 | if self.ws and self.connected: 380 | self.ws.send(json.dumps(pong_message)) 381 | self.last_heartbeat = time.time() 382 | return 383 | 384 | if "stream" in data and "data" in data: 385 | stream = data["stream"] 386 | event_data = data["data"] 387 | 388 | # 處理bookTicker 389 | if stream.startswith("bookTicker."): 390 | if 'b' in event_data and 'a' in event_data: 391 | self.bid_price = float(event_data['b']) 392 | self.ask_price = float(event_data['a']) 393 | self.last_price = (self.bid_price + self.ask_price) / 2 394 | # 記錄歷史價格用於計算波動率 395 | self.add_price_to_history(self.last_price) 396 | 397 | # 處理depth 398 | elif stream.startswith("depth."): 399 | if 'b' in event_data and 'a' in event_data: 400 | self._update_orderbook(event_data) 401 | 402 | # 訂單更新數據流 403 | elif stream.startswith("account.orderUpdate."): 404 | self.order_updates.append(event_data) 405 | 406 | if self.on_message_callback: 407 | self.on_message_callback(stream, event_data) 408 | 409 | except Exception as e: 410 | logger.error(f"處理WebSocket消息時出錯: {e}") 411 | 412 | def _update_orderbook(self, data): 413 | """更新訂單簿(優化處理速度)""" 414 | # 處理買單更新 415 | if 'b' in data: 416 | for bid in data['b']: 417 | price = float(bid[0]) 418 | quantity = float(bid[1]) 419 | 420 | # 使用二分查找來優化插入位置查找 421 | if quantity == 0: 422 | # 移除價位 423 | self.orderbook["bids"] = [b for b in self.orderbook["bids"] if b[0] != price] 424 | else: 425 | # 先查找是否存在相同價位 426 | found = False 427 | for i, b in enumerate(self.orderbook["bids"]): 428 | if b[0] == price: 429 | self.orderbook["bids"][i] = [price, quantity] 430 | found = True 431 | break 432 | 433 | # 如果不存在,插入並保持排序 434 | if not found: 435 | self.orderbook["bids"].append([price, quantity]) 436 | # 按價格降序排序 437 | self.orderbook["bids"] = sorted(self.orderbook["bids"], key=lambda x: x[0], reverse=True) 438 | 439 | # 處理賣單更新 440 | if 'a' in data: 441 | for ask in data['a']: 442 | price = float(ask[0]) 443 | quantity = float(ask[1]) 444 | 445 | if quantity == 0: 446 | # 移除價位 447 | self.orderbook["asks"] = [a for a in self.orderbook["asks"] if a[0] != price] 448 | else: 449 | # 先查找是否存在相同價位 450 | found = False 451 | for i, a in enumerate(self.orderbook["asks"]): 452 | if a[0] == price: 453 | self.orderbook["asks"][i] = [price, quantity] 454 | found = True 455 | break 456 | 457 | # 如果不存在,插入並保持排序 458 | if not found: 459 | self.orderbook["asks"].append([price, quantity]) 460 | # 按價格升序排序 461 | self.orderbook["asks"] = sorted(self.orderbook["asks"], key=lambda x: x[0]) 462 | 463 | def on_error(self, ws, error): 464 | """處理WebSocket錯誤""" 465 | logger.error(f"WebSocket發生錯誤: {error}") 466 | self.last_heartbeat = 0 # 強制觸發重連 467 | 468 | def on_close(self, ws, close_status_code, close_msg): 469 | """處理WebSocket關閉""" 470 | previous_connected = self.connected 471 | self.connected = False 472 | logger.info(f"WebSocket連接已關閉: {close_msg if close_msg else 'No message'} (狀態碼: {close_status_code if close_status_code else 'None'})") 473 | 474 | # 清理當前socket資源 475 | if hasattr(ws, 'sock') and ws.sock: 476 | try: 477 | ws.sock.close() 478 | ws.sock = None 479 | except Exception as e: 480 | logger.debug(f"關閉socket時出錯: {e}") 481 | 482 | if close_status_code == 1000 or getattr(ws, '_closed_by_me', False): 483 | logger.info("WebSocket正常關閉,不進行重連") 484 | elif previous_connected and self.running and self.auto_reconnect: 485 | logger.info("WebSocket非正常關閉,將自動重連") 486 | # 使用線程觸發重連,避免在回調中直接重連 487 | threading.Thread(target=self.reconnect, daemon=True).start() 488 | 489 | def close(self): 490 | """完全關閉WebSocket連接""" 491 | with self.ws_lock: 492 | logger.info("主動關閉WebSocket連接...") 493 | self.running = False 494 | self.connected = False 495 | 496 | # 停止心跳檢測線程 497 | if self.heartbeat_thread and self.heartbeat_thread.is_alive(): 498 | try: 499 | self.heartbeat_thread.join(timeout=1) 500 | except Exception: 501 | pass 502 | self.heartbeat_thread = None 503 | 504 | if self.ws: 505 | # 標記為主動關閉 506 | if not hasattr(self.ws, '_closed_by_me'): 507 | self.ws._closed_by_me = True 508 | else: 509 | self.ws._closed_by_me = True 510 | 511 | try: 512 | # 關閉WebSocket 513 | self.ws.close() 514 | self.ws.keep_running = False 515 | 516 | # 強制關閉socket 517 | if hasattr(self.ws, 'sock') and self.ws.sock: 518 | self.ws.sock.close() 519 | except Exception as e: 520 | logger.error(f"關閉WebSocket時出錯: {e}") 521 | 522 | # 等待完全關閉 523 | time.sleep(0.5) 524 | self.ws = None 525 | 526 | # 清理線程 527 | if self.ws_thread and self.ws_thread.is_alive(): 528 | try: 529 | self.ws_thread.join(timeout=1) 530 | except Exception: 531 | pass 532 | self.ws_thread = None 533 | 534 | # 重置訂閲狀態 535 | self.subscriptions = [] 536 | 537 | logger.info("WebSocket連接已完全關閉") 538 | 539 | def get_current_price(self): 540 | """獲取當前價格""" 541 | return self.last_price 542 | 543 | def get_bid_ask(self): 544 | """獲取買賣價""" 545 | return self.bid_price, self.ask_price 546 | 547 | def get_orderbook(self): 548 | """獲取訂單簿""" 549 | return self.orderbook 550 | 551 | def is_connected(self): 552 | """檢查連接狀態""" 553 | if not self.connected: 554 | return False 555 | if not self.ws: 556 | return False 557 | if not hasattr(self.ws, 'sock') or not self.ws.sock: 558 | return False 559 | 560 | # 檢查socket是否連接 561 | try: 562 | return self.ws.sock.connected 563 | except: 564 | return False 565 | 566 | def get_liquidity_profile(self, depth_percentage=0.01): 567 | """分析市場流動性特徵""" 568 | if not self.orderbook["bids"] or not self.orderbook["asks"]: 569 | return None 570 | 571 | mid_price = (self.bid_price + self.ask_price) / 2 if self.bid_price and self.ask_price else None 572 | if not mid_price: 573 | return None 574 | 575 | # 計算價格範圍 576 | min_price = mid_price * (1 - depth_percentage) 577 | max_price = mid_price * (1 + depth_percentage) 578 | 579 | # 分析買賣單流動性 580 | bid_volume = sum(qty for price, qty in self.orderbook["bids"] if price >= min_price) 581 | ask_volume = sum(qty for price, qty in self.orderbook["asks"] if price <= max_price) 582 | 583 | # 計算買賣比例 584 | ratio = bid_volume / ask_volume if ask_volume > 0 else float('inf') 585 | 586 | # 買賣壓力差異 587 | imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume) if (bid_volume + ask_volume) > 0 else 0 588 | 589 | return { 590 | 'bid_volume': bid_volume, 591 | 'ask_volume': ask_volume, 592 | 'volume_ratio': ratio, 593 | 'imbalance': imbalance, 594 | 'mid_price': mid_price 595 | } -------------------------------------------------------------------------------- /strategies/grid_trader.py: -------------------------------------------------------------------------------- 1 | """ 2 | 網格交易策略模塊 3 | """ 4 | import time 5 | import threading 6 | from datetime import datetime, timedelta 7 | from typing import Dict, List, Tuple, Optional, Union, Any 8 | from concurrent.futures import ThreadPoolExecutor 9 | 10 | from api.client import ( 11 | get_balance, execute_order, get_open_orders, cancel_all_orders, 12 | cancel_order, get_market_limits, get_klines, get_ticker, get_order_book 13 | ) 14 | from ws_client.client import BackpackWebSocket 15 | from database.db import Database 16 | from utils.helpers import round_to_precision, round_to_tick_size, calculate_volatility 17 | from logger import setup_logger 18 | 19 | logger = setup_logger("grid_trader") 20 | 21 | class GridTrader: 22 | def __init__( 23 | self, 24 | api_key, 25 | secret_key, 26 | symbol, 27 | db_instance=None, 28 | grid_upper_price=None, # 網格上限價格 29 | grid_lower_price=None, # 網格下限價格 30 | grid_num=10, # 網格數量 31 | order_quantity=None, # 每格訂單數量 32 | auto_price_range=True, # 自動設置價格範圍 33 | price_range_percent=5.0, # 自動模式下的價格範圍百分比 34 | ws_proxy=None, 35 | max_position=0.5 # 最大持倉量限制 36 | ): 37 | self.api_key = api_key 38 | self.secret_key = secret_key 39 | self.symbol = symbol 40 | self.grid_num = grid_num 41 | self.order_quantity = order_quantity 42 | self.auto_price_range = auto_price_range 43 | self.price_range_percent = price_range_percent 44 | self.grid_upper_price = grid_upper_price 45 | self.grid_lower_price = grid_lower_price 46 | self.max_position = max_position 47 | 48 | # 網格交易狀態 49 | self.grid_initialized = False 50 | 51 | # 舊的網格訂單跟蹤結構(保留向後兼容) 52 | self.grid_orders = {} # 保存網格訂單 {網格價格: 訂單信息} 53 | self.grid_buy_orders = {} # 買入網格訂單 54 | self.grid_sell_orders = {} # 賣出網格訂單 55 | 56 | # 新的網格訂單跟蹤結構 57 | self.grid_orders_by_price = {} # {價格: [訂單信息1, 訂單信息2, ...]} 58 | self.grid_orders_by_id = {} # {訂單ID: 訂單信息} 59 | self.grid_buy_orders_by_price = {} # {價格: [買單信息1, 買單信息2, ...]} 60 | self.grid_sell_orders_by_price = {} # {價格: [賣單信息1, 賣單信息2, ...]} 61 | 62 | # 添加網格點位狀態跟蹤 63 | self.grid_status = {} # {價格: 狀態} 狀態可以是 'buy_placed', 'buy_filled', 'sell_placed', 'sell_filled' 64 | self.grid_dependencies = {} # {價格: 依賴價格} 記錄點位之間的依賴關係 65 | 66 | self.grid_levels = [] # 保存所有網格價格點位 67 | 68 | # 初始化數據庫 69 | self.db = db_instance if db_instance else Database() 70 | 71 | # 統計屬性 72 | self.session_start_time = datetime.now() 73 | self.session_buy_trades = [] 74 | self.session_sell_trades = [] 75 | self.session_fees = 0.0 76 | self.session_maker_buy_volume = 0.0 77 | self.session_maker_sell_volume = 0.0 78 | self.session_taker_buy_volume = 0.0 79 | self.session_taker_sell_volume = 0.0 80 | 81 | # 初始化市場限制 82 | self.market_limits = get_market_limits(symbol) 83 | if not self.market_limits: 84 | raise ValueError(f"無法獲取 {symbol} 的市場限制") 85 | 86 | self.base_asset = self.market_limits['base_asset'] 87 | self.quote_asset = self.market_limits['quote_asset'] 88 | self.base_precision = self.market_limits['base_precision'] 89 | self.quote_precision = self.market_limits['quote_precision'] 90 | self.min_order_size = float(self.market_limits['min_order_size']) 91 | self.tick_size = float(self.market_limits['tick_size']) 92 | 93 | # 交易量統計 94 | self.maker_buy_volume = 0 95 | self.maker_sell_volume = 0 96 | self.taker_buy_volume = 0 97 | self.taker_sell_volume = 0 98 | self.total_fees = 0 99 | 100 | # 添加代理參數 101 | self.ws_proxy = ws_proxy 102 | # 建立WebSocket連接 103 | self.ws = BackpackWebSocket(api_key, secret_key, symbol, self.on_ws_message, auto_reconnect=True, proxy=self.ws_proxy) 104 | self.ws.connect() 105 | 106 | # 記錄買賣數量以便跟蹤 107 | self.total_bought = 0 108 | self.total_sold = 0 109 | 110 | # 交易記錄 - 用於計算利潤 111 | self.buy_trades = [] 112 | self.sell_trades = [] 113 | 114 | # 利潤統計 115 | self.total_profit = 0 116 | self.trades_executed = 0 117 | self.orders_placed = 0 118 | self.orders_cancelled = 0 119 | 120 | # 執行緒池用於後台任務 121 | self.executor = ThreadPoolExecutor(max_workers=3) 122 | 123 | # 等待WebSocket連接建立並進行初始化訂閲 124 | self._initialize_websocket() 125 | 126 | # 載入交易統計和歷史交易 127 | self._load_trading_stats() 128 | self._load_recent_trades() 129 | 130 | logger.info(f"初始化網格交易: {symbol}") 131 | logger.info(f"基礎資產: {self.base_asset}, 報價資產: {self.quote_asset}") 132 | logger.info(f"基礎精度: {self.base_precision}, 報價精度: {self.quote_precision}") 133 | logger.info(f"最小訂單大小: {self.min_order_size}, 價格步長: {self.tick_size}") 134 | logger.info(f"網格數量: {self.grid_num}") 135 | logger.info(f"最大持倉限制: {self.max_position} {self.base_asset}") 136 | 137 | def _initialize_websocket(self): 138 | """等待WebSocket連接建立並進行初始化訂閲""" 139 | wait_time = 0 140 | max_wait_time = 10 141 | while not self.ws.connected and wait_time < max_wait_time: 142 | time.sleep(0.5) 143 | wait_time += 0.5 144 | 145 | if self.ws.connected: 146 | logger.info("WebSocket連接已建立,初始化數據流...") 147 | 148 | # 初始化訂單簿 149 | orderbook_initialized = self.ws.initialize_orderbook() 150 | 151 | # 訂閲深度流和行情數據 152 | if orderbook_initialized: 153 | depth_subscribed = self.ws.subscribe_depth() 154 | ticker_subscribed = self.ws.subscribe_bookTicker() 155 | 156 | if depth_subscribed and ticker_subscribed: 157 | logger.info("數據流訂閲成功!") 158 | 159 | # 訂閲私有訂單更新流 160 | self.subscribe_order_updates() 161 | else: 162 | logger.warning(f"WebSocket連接建立超時,將在運行過程中繼續嘗試連接") 163 | 164 | def _load_trading_stats(self): 165 | """從數據庫加載交易統計數據""" 166 | try: 167 | today = datetime.now().strftime('%Y-%m-%d') 168 | 169 | # 查詢今天的統計數據 170 | stats = self.db.get_trading_stats(self.symbol, today) 171 | 172 | if stats and len(stats) > 0: 173 | stat = stats[0] 174 | self.maker_buy_volume = stat['maker_buy_volume'] 175 | self.maker_sell_volume = stat['maker_sell_volume'] 176 | self.taker_buy_volume = stat['taker_buy_volume'] 177 | self.taker_sell_volume = stat['taker_sell_volume'] 178 | self.total_profit = stat['realized_profit'] 179 | self.total_fees = stat['total_fees'] 180 | 181 | logger.info(f"已從數據庫加載今日交易統計") 182 | logger.info(f"Maker買入量: {self.maker_buy_volume}, Maker賣出量: {self.maker_sell_volume}") 183 | logger.info(f"Taker買入量: {self.taker_buy_volume}, Taker賣出量: {self.taker_sell_volume}") 184 | logger.info(f"已實現利潤: {self.total_profit}, 總手續費: {self.total_fees}") 185 | else: 186 | logger.info("今日無交易統計記錄,將創建新記錄") 187 | except Exception as e: 188 | logger.error(f"加載交易統計時出錯: {e}") 189 | 190 | def _load_recent_trades(self): 191 | """從數據庫加載歷史成交記錄""" 192 | try: 193 | # 獲取訂單歷史 194 | trades = self.db.get_order_history(self.symbol, 1000) 195 | trades_count = len(trades) if trades else 0 196 | 197 | if trades_count > 0: 198 | for side, quantity, price, maker, fee in trades: 199 | quantity = float(quantity) 200 | price = float(price) 201 | fee = float(fee) 202 | 203 | if side == 'Bid': # 買入 204 | self.buy_trades.append((price, quantity)) 205 | self.total_bought += quantity 206 | if maker: 207 | self.maker_buy_volume += quantity 208 | else: 209 | self.taker_buy_volume += quantity 210 | elif side == 'Ask': # 賣出 211 | self.sell_trades.append((price, quantity)) 212 | self.total_sold += quantity 213 | if maker: 214 | self.maker_sell_volume += quantity 215 | else: 216 | self.taker_sell_volume += quantity 217 | 218 | self.total_fees += fee 219 | 220 | logger.info(f"已從數據庫載入 {trades_count} 條歷史成交記錄") 221 | logger.info(f"總買入: {self.total_bought} {self.base_asset}, 總賣出: {self.total_sold} {self.base_asset}") 222 | logger.info(f"Maker買入: {self.maker_buy_volume} {self.base_asset}, Maker賣出: {self.maker_sell_volume} {self.base_asset}") 223 | logger.info(f"Taker買入: {self.taker_buy_volume} {self.base_asset}, Taker賣出: {self.taker_sell_volume} {self.base_asset}") 224 | 225 | # 計算精確利潤 226 | self.total_profit = self._calculate_db_profit() 227 | logger.info(f"計算得出已實現利潤: {self.total_profit:.8f} {self.quote_asset}") 228 | logger.info(f"總手續費: {self.total_fees:.8f} {self.quote_asset}") 229 | else: 230 | logger.info("數據庫中沒有歷史成交記錄,嘗試從API獲取") 231 | self._load_trades_from_api() 232 | 233 | except Exception as e: 234 | logger.error(f"載入歷史成交記錄時出錯: {e}") 235 | import traceback 236 | traceback.print_exc() 237 | 238 | def _initialize_dependencies_from_history(self): 239 | """根據歷史交易初始化網格依賴關係""" 240 | logger.info("根據歷史交易初始化網格依賴關係...") 241 | 242 | # 獲取所有歷史交易,不僅僅是10筆 243 | recent_trades = self.db.get_recent_trades(self.symbol, 100) # 獲取更多歷史交易 244 | 245 | # 按時間排序 246 | if recent_trades: 247 | recent_trades.sort(key=lambda x: x['timestamp']) 248 | 249 | # 跟蹤每個網格點位的最新狀態 250 | for trade in recent_trades: 251 | price = float(trade['price']) 252 | side = trade['side'] 253 | 254 | # 找到最接近的網格點位 255 | grid_price = min(self.grid_levels, key=lambda x: abs(x - price)) 256 | 257 | if side == 'Bid': # 買入成交 258 | # 找到下一個更高的網格點位 259 | next_price = None 260 | for p in sorted(self.grid_levels): 261 | if p > grid_price: 262 | next_price = p 263 | break 264 | 265 | if next_price: 266 | # 設置狀態和依賴關係 267 | self.grid_status[grid_price] = 'buy_filled' 268 | self.grid_status[next_price] = 'sell_placed' 269 | self.grid_dependencies[grid_price] = next_price 270 | 271 | elif side == 'Ask': # 賣出成交 272 | # 解除依賴關係 273 | for price, dependent_price in list(self.grid_dependencies.items()): 274 | if dependent_price == grid_price: 275 | del self.grid_dependencies[price] 276 | 277 | # 記錄當前依賴關係 278 | dependency_count = len(self.grid_dependencies) 279 | if dependency_count > 0: 280 | logger.info(f"已從歷史交易建立 {dependency_count} 個網格依賴關係") 281 | for price, dependent_price in self.grid_dependencies.items(): 282 | logger.info(f" 價格點位 {price} 依賴於 {dependent_price} 的賣單成交") 283 | 284 | def _load_trades_from_api(self): 285 | """從API加載歷史成交記錄""" 286 | from api.client import get_fill_history 287 | 288 | fill_history = get_fill_history(self.api_key, self.secret_key, self.symbol, 100) 289 | 290 | if isinstance(fill_history, dict) and "error" in fill_history: 291 | logger.error(f"載入成交記錄失敗: {fill_history['error']}") 292 | return 293 | 294 | if not fill_history: 295 | logger.info("沒有找到歷史成交記錄") 296 | return 297 | 298 | # 批量插入準備 299 | for fill in fill_history: 300 | price = float(fill.get('price', 0)) 301 | quantity = float(fill.get('quantity', 0)) 302 | side = fill.get('side') 303 | maker = fill.get('maker', False) 304 | fee = float(fill.get('fee', 0)) 305 | fee_asset = fill.get('feeAsset', '') 306 | order_id = fill.get('orderId', '') 307 | 308 | # 準備訂單數據 309 | order_data = { 310 | 'order_id': order_id, 311 | 'symbol': self.symbol, 312 | 'side': side, 313 | 'quantity': quantity, 314 | 'price': price, 315 | 'maker': maker, 316 | 'fee': fee, 317 | 'fee_asset': fee_asset, 318 | 'trade_type': 'manual' 319 | } 320 | 321 | # 插入數據庫 322 | self.db.insert_order(order_data) 323 | 324 | if side == 'Bid': # 買入 325 | self.buy_trades.append((price, quantity)) 326 | self.total_bought += quantity 327 | if maker: 328 | self.maker_buy_volume += quantity 329 | else: 330 | self.taker_buy_volume += quantity 331 | elif side == 'Ask': # 賣出 332 | self.sell_trades.append((price, quantity)) 333 | self.total_sold += quantity 334 | if maker: 335 | self.maker_sell_volume += quantity 336 | else: 337 | self.taker_sell_volume += quantity 338 | 339 | self.total_fees += fee 340 | 341 | if fill_history: 342 | logger.info(f"已從API載入並存儲 {len(fill_history)} 條歷史成交記錄") 343 | 344 | # 更新總計 345 | logger.info(f"總買入: {self.total_bought} {self.base_asset}, 總賣出: {self.total_sold} {self.base_asset}") 346 | logger.info(f"Maker買入: {self.maker_buy_volume} {self.base_asset}, Maker賣出: {self.maker_sell_volume} {self.base_asset}") 347 | logger.info(f"Taker買入: {self.taker_buy_volume} {self.base_asset}, Taker賣出: {self.taker_sell_volume} {self.base_asset}") 348 | 349 | # 計算精確利潤 350 | self.total_profit = self._calculate_db_profit() 351 | logger.info(f"計算得出已實現利潤: {self.total_profit:.8f} {self.quote_asset}") 352 | logger.info(f"總手續費: {self.total_fees:.8f} {self.quote_asset}") 353 | 354 | def check_ws_connection(self): 355 | """檢查並恢復WebSocket連接""" 356 | ws_connected = self.ws and self.ws.is_connected() 357 | 358 | if not ws_connected: 359 | logger.warning("WebSocket連接已斷開或不可用,嘗試重新連接...") 360 | 361 | # 嘗試關閉現有連接 362 | if self.ws: 363 | try: 364 | if hasattr(self.ws, 'running') and self.ws.running: 365 | self.ws.running = False 366 | if hasattr(self.ws, 'ws') and self.ws.ws: 367 | try: 368 | self.ws.ws.close() 369 | except: 370 | pass 371 | self.ws.close() 372 | time.sleep(0.5) 373 | except Exception as e: 374 | logger.error(f"關閉現有WebSocket時出錯: {e}") 375 | 376 | # 創建新的連接 377 | try: 378 | logger.info("創建新的WebSocket連接...") 379 | self.ws = BackpackWebSocket( 380 | self.api_key, 381 | self.secret_key, 382 | self.symbol, 383 | self.on_ws_message, 384 | auto_reconnect=True, 385 | proxy=self.ws_proxy 386 | ) 387 | self.ws.connect() 388 | 389 | # 等待連接建立 390 | wait_time = 0 391 | max_wait_time = 5 392 | while not self.ws.is_connected() and wait_time < max_wait_time: 393 | time.sleep(0.5) 394 | wait_time += 0.5 395 | 396 | if self.ws.is_connected(): 397 | logger.info("WebSocket重新連接成功") 398 | 399 | # 重新初始化 400 | self.ws.initialize_orderbook() 401 | self.ws.subscribe_depth() 402 | self.ws.subscribe_bookTicker() 403 | self.subscribe_order_updates() 404 | else: 405 | logger.warning("WebSocket重新連接嘗試中,將在下次迭代再次檢查") 406 | 407 | except Exception as e: 408 | logger.error(f"創建新WebSocket連接時出錯: {e}") 409 | return False 410 | 411 | return self.ws and self.ws.is_connected() 412 | 413 | def on_ws_message(self, stream, data): 414 | """處理WebSocket消息回調""" 415 | if stream.startswith("account.orderUpdate."): 416 | event_type = data.get('e') 417 | 418 | # 「訂單成交」事件 419 | if event_type == 'orderFill': 420 | try: 421 | side = data.get('S') 422 | quantity = float(data.get('l', '0')) # 此次成交數量 423 | price = float(data.get('L', '0')) # 此次成交價格 424 | order_id = data.get('i') # 訂單 ID 425 | maker = data.get('m', False) # 是否是 Maker 426 | fee = float(data.get('n', '0')) # 手續費 427 | fee_asset = data.get('N', '') # 手續費資產 428 | 429 | logger.info(f"訂單成交: ID={order_id}, 方向={side}, 數量={quantity}, 價格={price}, Maker={maker}, 手續費={fee:.8f}") 430 | 431 | # 查找是哪個網格點位的訂單 432 | # 優先使用新結構 433 | order_info = self.grid_orders_by_id.get(order_id) 434 | 435 | if order_info: 436 | # 使用新的訂單結構 437 | grid_price = order_info['price'] 438 | logger.info(f"網格交易: 價格 {grid_price} 的訂單已成交") 439 | 440 | # 從訂單跟蹤結構中移除此訂單 441 | self._remove_order(order_id, grid_price, side) 442 | 443 | if side == 'Bid': # 買單成交 444 | # 在下一個更高的網格點位創建賣單 445 | self._place_sell_order_after_buy(price, grid_price, quantity, fee if fee_asset == self.base_asset else None) 446 | elif side == 'Ask': # 賣單成交 447 | # 在下一個更低的網格點位創建買單 448 | self._place_buy_order_after_sell(price, grid_price, quantity, fee if fee_asset == self.quote_asset else None) 449 | else: 450 | # 嘗試使用舊的訂單結構 451 | found = False 452 | for grid_price, old_order_info in list(self.grid_orders.items()): 453 | if old_order_info.get('order_id') == order_id: 454 | logger.info(f"網格交易(舊結構): 價格 {grid_price} 的訂單已成交") 455 | # 從當前網格訂單中移除 456 | self.grid_orders.pop(grid_price, None) 457 | if side == 'Bid': # 買單成交 458 | self.grid_buy_orders.pop(grid_price, None) 459 | # 在下一個更高的網格點位創建賣單 460 | self._place_sell_order_after_buy(price, grid_price, quantity, fee if fee_asset == self.base_asset else None) 461 | elif side == 'Ask': # 賣單成交 462 | self.grid_sell_orders.pop(grid_price, None) 463 | # 在下一個更低的網格點位創建買單 464 | self._place_buy_order_after_sell(price, grid_price, quantity, fee if fee_asset == self.quote_asset else None) 465 | found = True 466 | break 467 | 468 | if not found: 469 | logger.warning(f"找不到與訂單ID={order_id}匹配的網格訂單") 470 | 471 | # 判斷交易類型 472 | trade_type = 'grid_trading' # 默認為網格交易 473 | 474 | # 準備訂單數據 475 | order_data = { 476 | 'order_id': order_id, 477 | 'symbol': self.symbol, 478 | 'side': side, 479 | 'quantity': quantity, 480 | 'price': price, 481 | 'maker': maker, 482 | 'fee': fee, 483 | 'fee_asset': fee_asset, 484 | 'trade_type': trade_type 485 | } 486 | 487 | # 安全地插入數據庫 488 | def safe_insert_order(): 489 | try: 490 | self.db.insert_order(order_data) 491 | except Exception as db_err: 492 | logger.error(f"插入訂單數據時出錯: {db_err}") 493 | 494 | # 直接在當前線程中插入訂單數據,確保先寫入基本數據 495 | safe_insert_order() 496 | 497 | # 更新買賣量和做市商成交量統計 498 | if side == 'Bid': # 買入 499 | self.total_bought += quantity 500 | self.buy_trades.append((price, quantity)) 501 | logger.info(f"買入成交: {quantity} {self.base_asset} @ {price} {self.quote_asset}") 502 | 503 | # 更新做市商成交量 504 | if maker: 505 | self.maker_buy_volume += quantity 506 | self.session_maker_buy_volume += quantity 507 | else: 508 | self.taker_buy_volume += quantity 509 | self.session_taker_buy_volume += quantity 510 | 511 | self.session_buy_trades.append((price, quantity)) 512 | 513 | elif side == 'Ask': # 賣出 514 | self.total_sold += quantity 515 | self.sell_trades.append((price, quantity)) 516 | logger.info(f"賣出成交: {quantity} {self.base_asset} @ {price} {self.quote_asset}") 517 | 518 | # 更新做市商成交量 519 | if maker: 520 | self.maker_sell_volume += quantity 521 | self.session_maker_sell_volume += quantity 522 | else: 523 | self.taker_sell_volume += quantity 524 | self.session_taker_sell_volume += quantity 525 | 526 | self.session_sell_trades.append((price, quantity)) 527 | 528 | # 更新累計手續費 529 | self.total_fees += fee 530 | self.session_fees += fee 531 | 532 | # 在單獨的線程中更新統計數據,避免阻塞主回調 533 | def safe_update_stats_wrapper(): 534 | try: 535 | self._update_trading_stats() 536 | except Exception as e: 537 | logger.error(f"更新交易統計時出錯: {e}") 538 | 539 | self.executor.submit(safe_update_stats_wrapper) 540 | 541 | # 重新計算利潤(基於數據庫記錄) 542 | # 也在單獨的線程中進行計算,避免阻塞 543 | def update_profit(): 544 | try: 545 | profit = self._calculate_db_profit() 546 | self.total_profit = profit 547 | except Exception as e: 548 | logger.error(f"更新利潤計算時出錯: {e}") 549 | 550 | self.executor.submit(update_profit) 551 | 552 | # 計算本次執行的簡單利潤(不涉及數據庫查詢) 553 | session_profit = self._calculate_session_profit() 554 | 555 | # 執行簡要統計 556 | logger.info(f"累計利潤: {self.total_profit:.8f} {self.quote_asset}") 557 | logger.info(f"本次執行利潤: {session_profit:.8f} {self.quote_asset}") 558 | logger.info(f"本次執行手續費: {self.session_fees:.8f} {self.quote_asset}") 559 | logger.info(f"本次執行淨利潤: {(session_profit - self.session_fees):.8f} {self.quote_asset}") 560 | 561 | self.trades_executed += 1 562 | logger.info(f"總買入: {self.total_bought} {self.base_asset}, 總賣出: {self.total_sold} {self.base_asset}") 563 | logger.info(f"Maker買入: {self.maker_buy_volume} {self.base_asset}, Maker賣出: {self.maker_sell_volume} {self.base_asset}") 564 | logger.info(f"Taker買入: {self.taker_buy_volume} {self.base_asset}, Taker賣出: {self.taker_sell_volume} {self.base_asset}") 565 | 566 | except Exception as e: 567 | logger.error(f"處理訂單成交消息時出錯: {e}") 568 | import traceback 569 | traceback.print_exc() 570 | 571 | def _remove_order(self, order_id, grid_price, side): 572 | """從訂單跟蹤結構中移除訂單""" 573 | # 從ID字典中移除 574 | if order_id in self.grid_orders_by_id: 575 | del self.grid_orders_by_id[order_id] 576 | 577 | # 從價格字典中移除 578 | if grid_price in self.grid_orders_by_price: 579 | self.grid_orders_by_price[grid_price] = [order for order in self.grid_orders_by_price[grid_price] 580 | if order.get('order_id') != order_id] 581 | # 如果此價格沒有訂單了,清除此項 582 | if not self.grid_orders_by_price[grid_price]: 583 | del self.grid_orders_by_price[grid_price] 584 | 585 | # 從相應的買賣單字典中移除 586 | if side == 'Bid' and grid_price in self.grid_buy_orders_by_price: 587 | self.grid_buy_orders_by_price[grid_price] = [order for order in self.grid_buy_orders_by_price[grid_price] 588 | if order.get('order_id') != order_id] 589 | if not self.grid_buy_orders_by_price[grid_price]: 590 | del self.grid_buy_orders_by_price[grid_price] 591 | 592 | elif side == 'Ask' and grid_price in self.grid_sell_orders_by_price: 593 | self.grid_sell_orders_by_price[grid_price] = [order for order in self.grid_sell_orders_by_price[grid_price] 594 | if order.get('order_id') != order_id] 595 | if not self.grid_sell_orders_by_price[grid_price]: 596 | del self.grid_sell_orders_by_price[grid_price] 597 | 598 | def _place_sell_order_after_buy(self, executed_price, grid_price, quantity, actual_fee=None): 599 | """買單成交後在上一個網格點位放置賣單""" 600 | # 找到買入價格的下一個更高網格點位 601 | next_price = None 602 | for price in sorted(self.grid_levels): 603 | if price > grid_price: 604 | next_price = price 605 | break 606 | 607 | if next_price: 608 | logger.info(f"在網格點位 {next_price} 放置賣單 (買入價格: {executed_price})") 609 | 610 | # 調整數量,考慮到實際手續費 611 | if actual_fee is not None and isinstance(actual_fee, (int, float)): 612 | # 實際手續費(確認單位是否為SOL) 613 | if actual_fee > 0 and actual_fee < quantity: # 合理的手續費範圍 614 | adjusted_quantity = round_to_precision(quantity - actual_fee, self.base_precision) 615 | else: 616 | adjusted_quantity = round_to_precision(quantity, self.base_precision) 617 | else: 618 | # 如果沒有傳入有效的實際手續費,使用估計值 619 | adjusted_quantity = round_to_precision(quantity * 0.999, self.base_precision) 620 | 621 | # 檢查最小訂單大小 622 | if adjusted_quantity < self.min_order_size: 623 | logger.info(f"調整後數量 {adjusted_quantity} 低於最小訂單大小,使用最小值 {self.min_order_size}") 624 | adjusted_quantity = self.min_order_size 625 | 626 | # 放置賣單 627 | order_details = { 628 | "orderType": "Limit", 629 | "price": str(next_price), 630 | "quantity": str(adjusted_quantity), 631 | "side": "Ask", 632 | "symbol": self.symbol, 633 | "timeInForce": "GTC", 634 | "postOnly": True 635 | } 636 | 637 | result = execute_order(self.api_key, self.secret_key, order_details) 638 | 639 | if isinstance(result, dict) and "error" in result: 640 | logger.error(f"放置賣單失敗: {result['error']}") 641 | else: 642 | order_id = result.get('id') 643 | logger.info(f"成功放置賣單: 價格={next_price}, 數量={adjusted_quantity}, 訂單ID={order_id}") 644 | 645 | if order_id: 646 | # 創建訂單信息 647 | order_info = { 648 | 'order_id': order_id, 649 | 'side': 'Ask', 650 | 'quantity': adjusted_quantity, 651 | 'price': next_price, 652 | 'created_time': datetime.now(), 653 | 'created_from': f"BUY@{grid_price}" 654 | } 655 | 656 | # 添加到按訂單ID索引的字典 657 | self.grid_orders_by_id[order_id] = order_info 658 | 659 | # 添加到按價格索引的字典 660 | if next_price not in self.grid_orders_by_price: 661 | self.grid_orders_by_price[next_price] = [] 662 | self.grid_orders_by_price[next_price].append(order_info) 663 | 664 | # 添加到賣單字典 665 | if next_price not in self.grid_sell_orders_by_price: 666 | self.grid_sell_orders_by_price[next_price] = [] 667 | self.grid_sell_orders_by_price[next_price].append(order_info) 668 | 669 | # 更新舊的結構(向後兼容) 670 | self.grid_orders[next_price] = { 671 | 'order_id': order_id, 672 | 'side': 'Ask', 673 | 'quantity': adjusted_quantity, 674 | 'price': next_price 675 | } 676 | self.grid_sell_orders[next_price] = self.grid_orders[next_price] 677 | 678 | # 更新訂單計數 679 | self.orders_placed += 1 680 | 681 | # 記錄此價格點位的訂單數量 682 | sell_count = len(self.grid_sell_orders_by_price.get(next_price, [])) 683 | logger.info(f"網格點位 {next_price} 現有賣單數: {sell_count}") 684 | 685 | # 更新網格狀態 686 | self.grid_status[grid_price] = 'buy_filled' 687 | self.grid_status[next_price] = 'sell_placed' 688 | 689 | # 建立依賴關係:當next_price的賣單成交後,才能在grid_price補充買單 690 | self.grid_dependencies[grid_price] = next_price 691 | logger.info(f"建立依賴關係: 價格點位 {grid_price} 依賴於 {next_price} 的賣單成交") 692 | 693 | def _place_buy_order_after_sell(self, executed_price, grid_price, quantity, actual_fee=None): 694 | """賣單成交後在下一個網格點位放置買單""" 695 | # 找到賣出價格的下一個更低網格點位 696 | next_price = None 697 | for price in sorted(self.grid_levels, reverse=True): 698 | if price < grid_price: 699 | next_price = price 700 | break 701 | 702 | if next_price: 703 | logger.info(f"在網格點位 {next_price} 放置買單 (賣出價格: {executed_price})") 704 | 705 | # 計算賣出實際獲得的資金 706 | if actual_fee is not None and isinstance(actual_fee, (int, float)): 707 | # 使用實際手續費(USDC為單位) 708 | if actual_fee > 0 and actual_fee < (executed_price * quantity): # 合理的手續費範圍 709 | sell_value = executed_price * quantity - actual_fee 710 | else: 711 | sell_value = executed_price * quantity 712 | else: 713 | # 使用估計手續費 714 | sell_value = executed_price * quantity * 0.999 715 | 716 | # 計算可買入的數量 717 | buy_quantity = round_to_precision(sell_value / next_price, self.base_precision) 718 | 719 | # 檢查最小訂單大小 720 | if buy_quantity < self.min_order_size: 721 | logger.info(f"計算得出的買入數量 {buy_quantity} 低於最小訂單大小,使用最小值 {self.min_order_size}") 722 | buy_quantity = self.min_order_size 723 | 724 | # 檢查是否超過最大持倉限制 725 | net_position = self.total_bought - self.total_sold 726 | 727 | if net_position + buy_quantity > self.max_position: 728 | logger.warning(f"跳過買入:當前淨持倉 {net_position},新增 {buy_quantity} 將超過最大限制 {self.max_position}") 729 | return 730 | 731 | # 放置買單 732 | order_details = { 733 | "orderType": "Limit", 734 | "price": str(next_price), 735 | "quantity": str(buy_quantity), 736 | "side": "Bid", 737 | "symbol": self.symbol, 738 | "timeInForce": "GTC", 739 | "postOnly": True 740 | } 741 | 742 | result = execute_order(self.api_key, self.secret_key, order_details) 743 | 744 | if isinstance(result, dict) and "error" in result: 745 | logger.error(f"放置買單失敗: {result['error']}") 746 | else: 747 | order_id = result.get('id') 748 | logger.info(f"成功放置買單: 價格={next_price}, 數量={buy_quantity}, 訂單ID={order_id}") 749 | 750 | if order_id: 751 | # 創建訂單信息 752 | order_info = { 753 | 'order_id': order_id, 754 | 'side': 'Bid', 755 | 'quantity': buy_quantity, 756 | 'price': next_price, 757 | 'created_time': datetime.now(), 758 | 'created_from': f"SELL@{grid_price}" 759 | } 760 | 761 | # 添加到按訂單ID索引的字典 762 | self.grid_orders_by_id[order_id] = order_info 763 | 764 | # 添加到按價格索引的字典 765 | if next_price not in self.grid_orders_by_price: 766 | self.grid_orders_by_price[next_price] = [] 767 | self.grid_orders_by_price[next_price].append(order_info) 768 | 769 | # 添加到買單字典 770 | if next_price not in self.grid_buy_orders_by_price: 771 | self.grid_buy_orders_by_price[next_price] = [] 772 | self.grid_buy_orders_by_price[next_price].append(order_info) 773 | 774 | # 更新舊的結構(向後兼容) 775 | self.grid_orders[next_price] = { 776 | 'order_id': order_id, 777 | 'side': 'Bid', 778 | 'quantity': buy_quantity, 779 | 'price': next_price 780 | } 781 | self.grid_buy_orders[next_price] = self.grid_orders[next_price] 782 | 783 | # 更新訂單計數 784 | self.orders_placed += 1 785 | 786 | # 記錄此價格點位的訂單數量 787 | buy_count = len(self.grid_buy_orders_by_price.get(next_price, [])) 788 | logger.info(f"網格點位 {next_price} 現有買單數: {buy_count}") 789 | 790 | # 更新網格狀態 791 | self.grid_status[grid_price] = 'sell_filled' 792 | self.grid_status[next_price] = 'buy_placed' 793 | 794 | # 解除依賴關係:釋放依賴於此銷售點位的買點位 795 | dependencies_resolved = [] 796 | for price, dependent_price in list(self.grid_dependencies.items()): 797 | if dependent_price == grid_price: 798 | # 移除依賴,表示可以在price處放置新的買單 799 | del self.grid_dependencies[price] 800 | dependencies_resolved.append(price) 801 | 802 | if dependencies_resolved: 803 | logger.info(f"解除依賴關係: 價格點位 {dependencies_resolved} 已釋放") 804 | 805 | def _calculate_db_profit(self): 806 | """基於數據庫記錄計算已實現利潤(FIFO方法)""" 807 | try: 808 | # 獲取訂單歷史,注意這裡將返回一個列表 809 | order_history = self.db.get_order_history(self.symbol) 810 | if not order_history: 811 | return 0 812 | 813 | buy_trades = [] 814 | sell_trades = [] 815 | for side, quantity, price, maker, fee in order_history: 816 | if side == 'Bid': 817 | buy_trades.append((float(price), float(quantity), float(fee))) 818 | elif side == 'Ask': 819 | sell_trades.append((float(price), float(quantity), float(fee))) 820 | 821 | if not buy_trades or not sell_trades: 822 | return 0 823 | 824 | buy_queue = buy_trades.copy() 825 | total_profit = 0 826 | total_fees = 0 827 | 828 | for sell_price, sell_quantity, sell_fee in sell_trades: 829 | remaining_sell = sell_quantity 830 | total_fees += sell_fee 831 | 832 | while remaining_sell > 0 and buy_queue: 833 | buy_price, buy_quantity, buy_fee = buy_queue[0] 834 | matched_quantity = min(remaining_sell, buy_quantity) 835 | 836 | trade_profit = (sell_price - buy_price) * matched_quantity 837 | allocated_buy_fee = buy_fee * (matched_quantity / buy_quantity) 838 | total_fees += allocated_buy_fee 839 | 840 | net_trade_profit = trade_profit 841 | total_profit += net_trade_profit 842 | 843 | remaining_sell -= matched_quantity 844 | if matched_quantity >= buy_quantity: 845 | buy_queue.pop(0) 846 | else: 847 | remaining_fee = buy_fee * (1 - matched_quantity / buy_quantity) 848 | buy_queue[0] = (buy_price, buy_quantity - matched_quantity, remaining_fee) 849 | 850 | self.total_fees = total_fees 851 | return total_profit 852 | 853 | except Exception as e: 854 | logger.error(f"計算數據庫利潤時出錯: {e}") 855 | import traceback 856 | traceback.print_exc() 857 | return 0 858 | 859 | def _update_trading_stats(self): 860 | """更新每日交易統計數據""" 861 | try: 862 | today = datetime.now().strftime('%Y-%m-%d') 863 | 864 | # 計算額外指標 865 | volatility = 0 866 | if self.ws and hasattr(self.ws, 'historical_prices'): 867 | volatility = calculate_volatility(self.ws.historical_prices) 868 | 869 | # 計算平均價差 870 | avg_spread = 0 871 | if self.ws and self.ws.bid_price and self.ws.ask_price: 872 | avg_spread = (self.ws.ask_price - self.ws.bid_price) / ((self.ws.ask_price + self.ws.bid_price) / 2) * 100 873 | 874 | # 準備統計數據 875 | stats_data = { 876 | 'date': today, 877 | 'symbol': self.symbol, 878 | 'maker_buy_volume': self.maker_buy_volume, 879 | 'maker_sell_volume': self.maker_sell_volume, 880 | 'taker_buy_volume': self.taker_buy_volume, 881 | 'taker_sell_volume': self.taker_sell_volume, 882 | 'realized_profit': self.total_profit, 883 | 'total_fees': self.total_fees, 884 | 'net_profit': self.total_profit - self.total_fees, 885 | 'avg_spread': avg_spread, 886 | 'trade_count': self.trades_executed, 887 | 'volatility': volatility 888 | } 889 | 890 | # 使用專門的函數來處理數據庫操作 891 | def safe_update_stats(): 892 | try: 893 | success = self.db.update_trading_stats(stats_data) 894 | if not success: 895 | logger.warning("更新交易統計失敗,下次再試") 896 | except Exception as db_err: 897 | logger.error(f"更新交易統計時出錯: {db_err}") 898 | 899 | # 直接在當前線程執行,避免過多的並發操作 900 | safe_update_stats() 901 | 902 | except Exception as e: 903 | logger.error(f"更新交易統計數據時出錯: {e}") 904 | import traceback 905 | traceback.print_exc() 906 | 907 | def _calculate_average_buy_cost(self): 908 | """計算平均買入成本""" 909 | if not self.buy_trades: 910 | return 0 911 | 912 | total_buy_cost = sum(price * quantity for price, quantity in self.buy_trades) 913 | total_buy_quantity = sum(quantity for _, quantity in self.buy_trades) 914 | 915 | if not self.sell_trades or total_buy_quantity <= 0: 916 | return total_buy_cost / total_buy_quantity if total_buy_quantity > 0 else 0 917 | 918 | buy_queue = self.buy_trades.copy() 919 | consumed_cost = 0 920 | consumed_quantity = 0 921 | 922 | for _, sell_quantity in self.sell_trades: 923 | remaining_sell = sell_quantity 924 | 925 | while remaining_sell > 0 and buy_queue: 926 | buy_price, buy_quantity = buy_queue[0] 927 | matched_quantity = min(remaining_sell, buy_quantity) 928 | consumed_cost += buy_price * matched_quantity 929 | consumed_quantity += matched_quantity 930 | remaining_sell -= matched_quantity 931 | 932 | if matched_quantity >= buy_quantity: 933 | buy_queue.pop(0) 934 | else: 935 | buy_queue[0] = (buy_price, buy_quantity - matched_quantity) 936 | 937 | remaining_buy_quantity = total_buy_quantity - consumed_quantity 938 | remaining_buy_cost = total_buy_cost - consumed_cost 939 | 940 | if remaining_buy_quantity <= 0: 941 | if self.ws and self.ws.connected and self.ws.bid_price: 942 | return self.ws.bid_price 943 | return 0 944 | 945 | return remaining_buy_cost / remaining_buy_quantity 946 | 947 | def _calculate_session_profit(self): 948 | """計算本次執行的已實現利潤""" 949 | if not self.session_buy_trades or not self.session_sell_trades: 950 | return 0 951 | 952 | buy_queue = self.session_buy_trades.copy() 953 | total_profit = 0 954 | 955 | for sell_price, sell_quantity in self.session_sell_trades: 956 | remaining_sell = sell_quantity 957 | 958 | while remaining_sell > 0 and buy_queue: 959 | buy_price, buy_quantity = buy_queue[0] 960 | matched_quantity = min(remaining_sell, buy_quantity) 961 | 962 | # 計算這筆交易的利潤 963 | trade_profit = (sell_price - buy_price) * matched_quantity 964 | total_profit += trade_profit 965 | 966 | remaining_sell -= matched_quantity 967 | if matched_quantity >= buy_quantity: 968 | buy_queue.pop(0) 969 | else: 970 | buy_queue[0] = (buy_price, buy_quantity - matched_quantity) 971 | 972 | return total_profit 973 | 974 | def calculate_pnl(self): 975 | """計算已實現和未實現PnL""" 976 | # 總的已實現利潤 977 | realized_pnl = self._calculate_db_profit() 978 | 979 | # 本次執行的已實現利潤 980 | session_realized_pnl = self._calculate_session_profit() 981 | 982 | # 計算未實現利潤 983 | unrealized_pnl = 0 984 | net_position = self.total_bought - self.total_sold 985 | 986 | if net_position > 0: 987 | current_price = self.get_current_price() 988 | if current_price: 989 | avg_buy_cost = self._calculate_average_buy_cost() 990 | unrealized_pnl = (current_price - avg_buy_cost) * net_position 991 | 992 | # 返回總的PnL和本次執行的PnL 993 | return realized_pnl, unrealized_pnl, self.total_fees, realized_pnl - self.total_fees, session_realized_pnl, self.session_fees, session_realized_pnl - self.session_fees 994 | 995 | def get_current_price(self): 996 | """獲取當前價格(優先使用WebSocket數據)""" 997 | self.check_ws_connection() 998 | price = None 999 | if self.ws and self.ws.connected: 1000 | price = self.ws.get_current_price() 1001 | 1002 | if price is None: 1003 | ticker = get_ticker(self.symbol) 1004 | if isinstance(ticker, dict) and "error" in ticker: 1005 | logger.error(f"獲取價格失敗: {ticker['error']}") 1006 | return None 1007 | 1008 | if "lastPrice" not in ticker: 1009 | logger.error(f"獲取到的價格數據不完整: {ticker}") 1010 | return None 1011 | return float(ticker['lastPrice']) 1012 | return price 1013 | 1014 | def get_market_depth(self): 1015 | """獲取市場深度(優先使用WebSocket數據)""" 1016 | self.check_ws_connection() 1017 | bid_price, ask_price = None, None 1018 | if self.ws and self.ws.connected: 1019 | bid_price, ask_price = self.ws.get_bid_ask() 1020 | 1021 | if bid_price is None or ask_price is None: 1022 | order_book = get_order_book(self.symbol) 1023 | if isinstance(order_book, dict) and "error" in order_book: 1024 | logger.error(f"獲取訂單簿失敗: {order_book['error']}") 1025 | return None, None 1026 | 1027 | bids = order_book.get('bids', []) 1028 | asks = order_book.get('asks', []) 1029 | if not bids or not asks: 1030 | return None, None 1031 | 1032 | highest_bid = float(bids[-1][0]) if bids else None 1033 | lowest_ask = float(asks[0][0]) if asks else None 1034 | 1035 | return highest_bid, lowest_ask 1036 | 1037 | return bid_price, ask_price 1038 | 1039 | def calculate_grid_levels(self): 1040 | """計算網格價格點位""" 1041 | current_price = self.get_current_price() 1042 | if current_price is None: 1043 | logger.error("無法獲取當前價格,無法計算網格") 1044 | return [] 1045 | 1046 | # 如果未設置網格上下限,則基於當前價格和價格範圍百分比自動計算 1047 | if self.auto_price_range or (self.grid_upper_price is None or self.grid_lower_price is None): 1048 | price_range_ratio = self.price_range_percent / 100 1049 | self.grid_upper_price = round_to_tick_size(current_price * (1 + price_range_ratio), self.tick_size) 1050 | self.grid_lower_price = round_to_tick_size(current_price * (1 - price_range_ratio), self.tick_size) 1051 | logger.info(f"自動設置網格價格範圍: {self.grid_lower_price} - {self.grid_upper_price} (當前價格: {current_price})") 1052 | 1053 | # 計算網格間隔 1054 | grid_interval = (self.grid_upper_price - self.grid_lower_price) / self.grid_num 1055 | 1056 | # 生成網格價格點位 1057 | grid_levels = [] 1058 | for i in range(self.grid_num + 1): 1059 | price = round_to_tick_size(self.grid_lower_price + i * grid_interval, self.tick_size) 1060 | grid_levels.append(price) 1061 | 1062 | logger.info(f"計算得出 {len(grid_levels)} 個網格點位: {grid_levels[0]} - {grid_levels[-1]}") 1063 | return grid_levels 1064 | 1065 | def subscribe_order_updates(self): 1066 | """訂閲訂單更新流""" 1067 | if not self.ws or not self.ws.is_connected(): 1068 | logger.warning("無法訂閲訂單更新:WebSocket連接不可用") 1069 | return False 1070 | 1071 | # 嘗試訂閲訂單更新流 1072 | stream = f"account.orderUpdate.{self.symbol}" 1073 | if stream not in self.ws.subscriptions: 1074 | retry_count = 0 1075 | max_retries = 3 1076 | success = False 1077 | 1078 | while retry_count < max_retries and not success: 1079 | try: 1080 | success = self.ws.private_subscribe(stream) 1081 | if success: 1082 | logger.info(f"成功訂閲訂單更新: {stream}") 1083 | return True 1084 | else: 1085 | logger.warning(f"訂閲訂單更新失敗,嘗試重試... ({retry_count+1}/{max_retries})") 1086 | except Exception as e: 1087 | logger.error(f"訂閲訂單更新時發生異常: {e}") 1088 | 1089 | retry_count += 1 1090 | if retry_count < max_retries: 1091 | time.sleep(1) # 重試前等待 1092 | 1093 | if not success: 1094 | logger.error(f"在 {max_retries} 次嘗試後仍無法訂閲訂單更新") 1095 | return False 1096 | else: 1097 | logger.info(f"已經訂閲了訂單更新: {stream}") 1098 | return True 1099 | 1100 | def initialize_grid(self): 1101 | """初始化網格交易""" 1102 | logger.info("開始初始化網格交易...") 1103 | 1104 | # 計算網格價格點位 1105 | self.grid_levels = self.calculate_grid_levels() 1106 | if not self.grid_levels: 1107 | logger.error("無法計算網格點位,初始化失敗") 1108 | return False 1109 | 1110 | # 獲取當前價格 1111 | current_price = self.get_current_price() 1112 | if current_price is None: 1113 | logger.error("無法獲取當前價格,初始化失敗") 1114 | return False 1115 | 1116 | logger.info(f"當前價格: {current_price}") 1117 | 1118 | # 取消所有現有訂單 1119 | self.cancel_existing_orders() 1120 | 1121 | # 獲取賬戶餘額 1122 | balances = get_balance(self.api_key, self.secret_key) 1123 | if isinstance(balances, dict) and "error" in balances: 1124 | logger.error(f"獲取餘額失敗: {balances['error']}") 1125 | return False 1126 | 1127 | base_balance = 0 1128 | quote_balance = 0 1129 | for asset, balance in balances.items(): 1130 | if asset == self.base_asset: 1131 | base_balance = float(balance.get('available', 0)) 1132 | elif asset == self.quote_asset: 1133 | quote_balance = float(balance.get('available', 0)) 1134 | 1135 | logger.info(f"當前餘額: {base_balance} {self.base_asset}, {quote_balance} {self.quote_asset}") 1136 | 1137 | # 如果未設置訂單數量,根據當前餘額計算 1138 | if self.order_quantity is None: 1139 | # 估算合適的訂單數量 1140 | total_quote_value = quote_balance + (base_balance * current_price) 1141 | average_quantity = (total_quote_value / current_price) / (self.grid_num * 2) # 平均分配到每個網格 1142 | self.order_quantity = max(self.min_order_size, round_to_precision(average_quantity, self.base_precision)) 1143 | logger.info(f"自動計算訂單數量: {self.order_quantity} {self.base_asset}") 1144 | 1145 | # 初始化網格狀態和依賴關係 1146 | self.grid_status = {} 1147 | self.grid_dependencies = {} 1148 | 1149 | # 在網格點位下單 1150 | placed_orders = 0 1151 | 1152 | # 清空訂單跟蹤結構 1153 | self.grid_orders = {} 1154 | self.grid_buy_orders = {} 1155 | self.grid_sell_orders = {} 1156 | self.grid_orders_by_price = {} 1157 | self.grid_orders_by_id = {} 1158 | self.grid_buy_orders_by_price = {} 1159 | self.grid_sell_orders_by_price = {} 1160 | 1161 | # 設置買單和賣單 1162 | for i, price in enumerate(self.grid_levels): 1163 | # 根據價格相對於當前價格的位置決定買單或賣單 1164 | if price < current_price: 1165 | # 在當前價格下方設置買單 1166 | order_details = { 1167 | "orderType": "Limit", 1168 | "price": str(price), 1169 | "quantity": str(self.order_quantity), 1170 | "side": "Bid", 1171 | "symbol": self.symbol, 1172 | "timeInForce": "GTC", 1173 | "postOnly": True 1174 | } 1175 | 1176 | result = execute_order(self.api_key, self.secret_key, order_details) 1177 | 1178 | if isinstance(result, dict) and "error" in result: 1179 | logger.error(f"設置買單失敗 (價格 {price}): {result['error']}") 1180 | else: 1181 | order_id = result.get('id') 1182 | logger.info(f"成功設置買單: 價格={price}, 數量={self.order_quantity}, 訂單ID={order_id}") 1183 | 1184 | # 更新舊訂單結構 1185 | self.grid_orders[price] = { 1186 | 'order_id': order_id, 1187 | 'side': 'Bid', 1188 | 'quantity': self.order_quantity, 1189 | 'price': price 1190 | } 1191 | self.grid_buy_orders[price] = self.grid_orders[price] 1192 | 1193 | # 更新新訂單結構 1194 | order_info = { 1195 | 'order_id': order_id, 1196 | 'side': 'Bid', 1197 | 'quantity': self.order_quantity, 1198 | 'price': price, 1199 | 'created_time': datetime.now(), 1200 | 'created_from': 'INIT' 1201 | } 1202 | 1203 | self.grid_orders_by_id[order_id] = order_info 1204 | 1205 | if price not in self.grid_orders_by_price: 1206 | self.grid_orders_by_price[price] = [] 1207 | self.grid_orders_by_price[price].append(order_info) 1208 | 1209 | if price not in self.grid_buy_orders_by_price: 1210 | self.grid_buy_orders_by_price[price] = [] 1211 | self.grid_buy_orders_by_price[price].append(order_info) 1212 | 1213 | # 設置網格狀態 1214 | self.grid_status[price] = 'buy_placed' 1215 | 1216 | placed_orders += 1 1217 | 1218 | elif price > current_price: 1219 | # 在當前價格上方設置賣單 1220 | # 確保有足夠的基礎資產 1221 | if base_balance >= self.order_quantity: 1222 | order_details = { 1223 | "orderType": "Limit", 1224 | "price": str(price), 1225 | "quantity": str(self.order_quantity), 1226 | "side": "Ask", 1227 | "symbol": self.symbol, 1228 | "timeInForce": "GTC", 1229 | "postOnly": True 1230 | } 1231 | 1232 | result = execute_order(self.api_key, self.secret_key, order_details) 1233 | 1234 | if isinstance(result, dict) and "error" in result: 1235 | logger.error(f"設置賣單失敗 (價格 {price}): {result['error']}") 1236 | else: 1237 | order_id = result.get('id') 1238 | logger.info(f"成功設置賣單: 價格={price}, 數量={self.order_quantity}, 訂單ID={order_id}") 1239 | 1240 | # 更新舊訂單結構 1241 | self.grid_orders[price] = { 1242 | 'order_id': order_id, 1243 | 'side': 'Ask', 1244 | 'quantity': self.order_quantity, 1245 | 'price': price 1246 | } 1247 | self.grid_sell_orders[price] = self.grid_orders[price] 1248 | 1249 | # 更新新訂單結構 1250 | order_info = { 1251 | 'order_id': order_id, 1252 | 'side': 'Ask', 1253 | 'quantity': self.order_quantity, 1254 | 'price': price, 1255 | 'created_time': datetime.now(), 1256 | 'created_from': 'INIT' 1257 | } 1258 | 1259 | self.grid_orders_by_id[order_id] = order_info 1260 | 1261 | if price not in self.grid_orders_by_price: 1262 | self.grid_orders_by_price[price] = [] 1263 | self.grid_orders_by_price[price].append(order_info) 1264 | 1265 | if price not in self.grid_sell_orders_by_price: 1266 | self.grid_sell_orders_by_price[price] = [] 1267 | self.grid_sell_orders_by_price[price].append(order_info) 1268 | 1269 | # 設置網格狀態 1270 | self.grid_status[price] = 'sell_placed' 1271 | 1272 | placed_orders += 1 1273 | base_balance -= self.order_quantity # 更新可用基礎資產餘額 1274 | else: 1275 | logger.warning(f"基礎資產餘額不足,無法設置賣單 (價格 {price})") 1276 | 1277 | logger.info(f"網格初始化完成: 共放置 {placed_orders} 個訂單") 1278 | self.grid_initialized = True 1279 | self.orders_placed += placed_orders 1280 | 1281 | # 根據歷史交易初始化依賴關係 1282 | self._initialize_dependencies_from_history() 1283 | 1284 | return True 1285 | 1286 | def place_limit_orders(self): 1287 | """放置網格訂單""" 1288 | # 如果網格尚未初始化,則先初始化 1289 | if not self.grid_initialized: 1290 | success = self.initialize_grid() 1291 | if not success: 1292 | logger.error("網格初始化失敗,無法放置訂單") 1293 | return 1294 | # 已在initialize_grid中放置訂單,無需繼續 1295 | return 1296 | 1297 | # 網格已初始化,檢查並補充缺失的網格訂單 1298 | current_price = self.get_current_price() 1299 | if current_price is None: 1300 | logger.error("無法獲取當前價格,無法檢查網格訂單") 1301 | return 1302 | 1303 | # 獲取賬戶餘額 1304 | balances = get_balance(self.api_key, self.secret_key) 1305 | if isinstance(balances, dict) and "error" in balances: 1306 | logger.error(f"獲取餘額失敗: {balances['error']}") 1307 | return 1308 | 1309 | base_balance = 0 1310 | quote_balance = 0 1311 | for asset, balance in balances.items(): 1312 | if asset == self.base_asset: 1313 | base_balance = float(balance.get('available', 0)) 1314 | elif asset == self.quote_asset: 1315 | quote_balance = float(balance.get('available', 0)) 1316 | 1317 | # 檢查網格點位 1318 | buy_orders_per_level = {} 1319 | sell_orders_per_level = {} 1320 | 1321 | # 統計每個價格點位的訂單數量 1322 | for price in self.grid_levels: 1323 | # 使用新的數據結構 1324 | buy_orders_per_level[price] = len(self.grid_buy_orders_by_price.get(price, [])) 1325 | sell_orders_per_level[price] = len(self.grid_sell_orders_by_price.get(price, [])) 1326 | 1327 | # 補充缺失的訂單 1328 | orders_to_place = [] 1329 | 1330 | for price in self.grid_levels: 1331 | if price < current_price and buy_orders_per_level.get(price, 0) == 0: 1332 | # 檢查此價格點位是否存在依賴關係 - 如果有依賴且依賴未解除,則不補單 1333 | if price in self.grid_dependencies: 1334 | dependent_price = self.grid_dependencies[price] 1335 | logger.info(f"價格點位 {price} 的買單依賴於價位 {dependent_price} 的賣單成交,暫不補充") 1336 | continue 1337 | 1338 | # 此網格點位沒有買單,需要補充 1339 | orders_to_place.append({ 1340 | 'price': price, 1341 | 'side': 'Bid', 1342 | 'quantity': self.order_quantity 1343 | }) 1344 | elif price > current_price and sell_orders_per_level.get(price, 0) == 0: 1345 | # 此網格點位沒有賣單,需要補充 1346 | if base_balance >= self.order_quantity: 1347 | orders_to_place.append({ 1348 | 'price': price, 1349 | 'side': 'Ask', 1350 | 'quantity': self.order_quantity 1351 | }) 1352 | base_balance -= self.order_quantity 1353 | else: 1354 | logger.warning(f"基礎資產餘額不足,無法在價格 {price} 處補充賣單") 1355 | 1356 | # 放置新訂單 1357 | orders_placed = 0 1358 | for order_info in orders_to_place: 1359 | order_details = { 1360 | "orderType": "Limit", 1361 | "price": str(order_info['price']), 1362 | "quantity": str(order_info['quantity']), 1363 | "side": order_info['side'], 1364 | "symbol": self.symbol, 1365 | "timeInForce": "GTC", 1366 | "postOnly": True 1367 | } 1368 | 1369 | result = execute_order(self.api_key, self.secret_key, order_details) 1370 | 1371 | if isinstance(result, dict) and "error" in result: 1372 | logger.error(f"補充訂單失敗 (價格 {order_info['price']}, 方向 {order_info['side']}): {result['error']}") 1373 | else: 1374 | order_id = result.get('id') 1375 | logger.info(f"成功補充訂單: 價格={order_info['price']}, 數量={order_info['quantity']}, 方向={order_info['side']}") 1376 | 1377 | price = order_info['price'] 1378 | side = order_info['side'] 1379 | quantity = order_info['quantity'] 1380 | 1381 | # 更新舊訂單結構 1382 | self.grid_orders[price] = { 1383 | 'order_id': order_id, 1384 | 'side': side, 1385 | 'quantity': quantity, 1386 | 'price': price 1387 | } 1388 | 1389 | if side == 'Bid': 1390 | self.grid_buy_orders[price] = self.grid_orders[price] 1391 | else: 1392 | self.grid_sell_orders[price] = self.grid_orders[price] 1393 | 1394 | # 更新新訂單結構 1395 | new_order_info = { 1396 | 'order_id': order_id, 1397 | 'side': side, 1398 | 'quantity': quantity, 1399 | 'price': price, 1400 | 'created_time': datetime.now(), 1401 | 'created_from': 'REFILL' 1402 | } 1403 | 1404 | self.grid_orders_by_id[order_id] = new_order_info 1405 | 1406 | if price not in self.grid_orders_by_price: 1407 | self.grid_orders_by_price[price] = [] 1408 | self.grid_orders_by_price[price].append(new_order_info) 1409 | 1410 | if side == 'Bid': 1411 | if price not in self.grid_buy_orders_by_price: 1412 | self.grid_buy_orders_by_price[price] = [] 1413 | self.grid_buy_orders_by_price[price].append(new_order_info) 1414 | # 更新網格狀態 1415 | self.grid_status[price] = 'buy_placed' 1416 | else: 1417 | if price not in self.grid_sell_orders_by_price: 1418 | self.grid_sell_orders_by_price[price] = [] 1419 | self.grid_sell_orders_by_price[price].append(new_order_info) 1420 | # 更新網格狀態 1421 | self.grid_status[price] = 'sell_placed' 1422 | 1423 | orders_placed += 1 1424 | self.orders_placed += 1 1425 | 1426 | if orders_placed > 0: 1427 | logger.info(f"共補充了 {orders_placed} 個網格訂單") 1428 | 1429 | def cancel_existing_orders(self): 1430 | """取消所有現有訂單""" 1431 | open_orders = get_open_orders(self.api_key, self.secret_key, self.symbol) 1432 | 1433 | if isinstance(open_orders, dict) and "error" in open_orders: 1434 | logger.error(f"獲取訂單失敗: {open_orders['error']}") 1435 | return 1436 | 1437 | if not open_orders: 1438 | logger.info("沒有需要取消的現有訂單") 1439 | # 清空訂單跟蹤結構 1440 | self.grid_orders = {} 1441 | self.grid_buy_orders = {} 1442 | self.grid_sell_orders = {} 1443 | self.grid_orders_by_price = {} 1444 | self.grid_orders_by_id = {} 1445 | self.grid_buy_orders_by_price = {} 1446 | self.grid_sell_orders_by_price = {} 1447 | return 1448 | 1449 | logger.info(f"正在取消 {len(open_orders)} 個現有訂單") 1450 | 1451 | try: 1452 | # 嘗試批量取消 1453 | result = cancel_all_orders(self.api_key, self.secret_key, self.symbol) 1454 | 1455 | if isinstance(result, dict) and "error" in result: 1456 | logger.error(f"批量取消訂單失敗: {result['error']}") 1457 | logger.info("嘗試逐個取消...") 1458 | 1459 | # 初始化線程池 1460 | with ThreadPoolExecutor(max_workers=5) as executor: 1461 | cancel_futures = [] 1462 | 1463 | # 提交取消訂單任務 1464 | for order in open_orders: 1465 | order_id = order.get('id') 1466 | if not order_id: 1467 | continue 1468 | 1469 | future = executor.submit( 1470 | cancel_order, 1471 | self.api_key, 1472 | self.secret_key, 1473 | order_id, 1474 | self.symbol 1475 | ) 1476 | cancel_futures.append((order_id, future)) 1477 | 1478 | # 處理結果 1479 | for order_id, future in cancel_futures: 1480 | try: 1481 | res = future.result() 1482 | if isinstance(res, dict) and "error" in res: 1483 | logger.error(f"取消訂單 {order_id} 失敗: {res['error']}") 1484 | else: 1485 | logger.info(f"取消訂單 {order_id} 成功") 1486 | self.orders_cancelled += 1 1487 | except Exception as e: 1488 | logger.error(f"取消訂單 {order_id} 時出錯: {e}") 1489 | else: 1490 | logger.info("批量取消訂單成功") 1491 | self.orders_cancelled += len(open_orders) 1492 | except Exception as e: 1493 | logger.error(f"取消訂單過程中發生錯誤: {str(e)}") 1494 | 1495 | # 等待一下確保訂單已取消 1496 | time.sleep(1) 1497 | 1498 | # 檢查是否還有未取消的訂單 1499 | remaining_orders = get_open_orders(self.api_key, self.secret_key, self.symbol) 1500 | if remaining_orders and len(remaining_orders) > 0: 1501 | logger.warning(f"警告: 仍有 {len(remaining_orders)} 個未取消的訂單") 1502 | else: 1503 | logger.info("所有訂單已成功取消") 1504 | 1505 | # 重置訂單跟蹤 1506 | self.grid_orders = {} 1507 | self.grid_buy_orders = {} 1508 | self.grid_sell_orders = {} 1509 | self.grid_orders_by_price = {} 1510 | self.grid_orders_by_id = {} 1511 | self.grid_buy_orders_by_price = {} 1512 | self.grid_sell_orders_by_price = {} 1513 | 1514 | # 如果有網格初始化過,需要重新初始化網格 1515 | self.grid_initialized = False 1516 | 1517 | def check_order_fills(self): 1518 | """檢查訂單成交情況""" 1519 | open_orders = get_open_orders(self.api_key, self.secret_key, self.symbol) 1520 | 1521 | if isinstance(open_orders, dict) and "error" in open_orders: 1522 | logger.error(f"獲取訂單失敗: {open_orders['error']}") 1523 | return 1524 | 1525 | # 獲取當前所有訂單ID 1526 | current_order_ids = set() 1527 | if open_orders: 1528 | for order in open_orders: 1529 | order_id = order.get('id') 1530 | if order_id: 1531 | current_order_ids.add(order_id) 1532 | 1533 | # 檢查網格訂單成交情況 1534 | filled_orders = [] 1535 | 1536 | # 使用新訂單結構進行檢查 1537 | for order_id, order_info in list(self.grid_orders_by_id.items()): 1538 | if order_id not in current_order_ids: 1539 | # 訂單不在當前訂單列表中,可能已成交 1540 | grid_price = order_info.get('price') 1541 | side = order_info.get('side') 1542 | logger.info(f"網格點位 {grid_price} 的訂單可能已成交: {order_info}") 1543 | filled_orders.append((order_id, order_info)) 1544 | 1545 | # 從網格訂單中移除已成交的訂單 1546 | for order_id, order_info in filled_orders: 1547 | side = order_info.get('side') 1548 | grid_price = order_info.get('price') 1549 | self._remove_order(order_id, grid_price, side) 1550 | 1551 | # 記錄活躍訂單數量 1552 | total_buy_orders = sum(len(orders) for orders in self.grid_buy_orders_by_price.values()) 1553 | total_sell_orders = sum(len(orders) for orders in self.grid_sell_orders_by_price.values()) 1554 | 1555 | logger.info(f"當前活躍網格訂單: 買單 {total_buy_orders} 個, 賣單 {total_sell_orders} 個") 1556 | 1557 | def estimate_profit(self): 1558 | """估算潛在利潤""" 1559 | if not self.grid_levels or len(self.grid_levels) <= 1: 1560 | logger.warning("未設置網格或網格點位不足,無法估算利潤") 1561 | return 1562 | 1563 | # 計算網格間隔 1564 | avg_grid_interval = (self.grid_levels[-1] - self.grid_levels[0]) / (len(self.grid_levels) - 1) 1565 | 1566 | # 計算每次成交的潛在利潤 1567 | potential_profit_per_trade = avg_grid_interval * self.order_quantity 1568 | 1569 | # 估算平均每日成交次數(假設)- 這裡可以基於歷史數據優化 1570 | estimated_daily_trades = 4 # 假設每天有4次網格成交 1571 | 1572 | # 計算總的PnL和本次執行的PnL 1573 | realized_pnl, unrealized_pnl, total_fees, net_pnl, session_realized_pnl, session_fees, session_net_pnl = self.calculate_pnl() 1574 | 1575 | # 計算預估日利潤 1576 | estimated_daily_profit = potential_profit_per_trade * estimated_daily_trades 1577 | estimated_daily_fees = (self.total_fees / max(1, self.trades_executed)) * estimated_daily_trades 1578 | estimated_net_daily_profit = estimated_daily_profit - estimated_daily_fees 1579 | 1580 | logger.info(f"\n--- 網格交易潛在利潤估算 ---") 1581 | logger.info(f"網格數量: {len(self.grid_levels)}") 1582 | logger.info(f"平均網格間隔: {avg_grid_interval:.8f} {self.quote_asset}") 1583 | logger.info(f"每網格訂單數量: {self.order_quantity} {self.base_asset}") 1584 | logger.info(f"每次成交潛在利潤: {potential_profit_per_trade:.8f} {self.quote_asset}") 1585 | logger.info(f"估計每日成交次數: {estimated_daily_trades} 次") 1586 | logger.info(f"估計日利潤: {estimated_daily_profit:.8f} {self.quote_asset}") 1587 | logger.info(f"估計日手續費: {estimated_daily_fees:.8f} {self.quote_asset}") 1588 | logger.info(f"估計日淨利潤: {estimated_net_daily_profit:.8f} {self.quote_asset}") 1589 | 1590 | logger.info(f"\n--- 當前交易統計 ---") 1591 | logger.info(f"已實現利潤(總): {realized_pnl:.8f} {self.quote_asset}") 1592 | logger.info(f"總手續費(總): {total_fees:.8f} {self.quote_asset}") 1593 | logger.info(f"凈利潤(總): {net_pnl:.8f} {self.quote_asset}") 1594 | logger.info(f"未實現利潤: {unrealized_pnl:.8f} {self.quote_asset}") 1595 | 1596 | # 打印本次執行的統計信息 1597 | logger.info(f"\n---本次執行統計---") 1598 | logger.info(f"本次執行已實現利潤: {session_realized_pnl:.8f} {self.quote_asset}") 1599 | logger.info(f"本次執行手續費: {session_fees:.8f} {self.quote_asset}") 1600 | logger.info(f"本次執行凈利潤: {session_net_pnl:.8f} {self.quote_asset}") 1601 | 1602 | session_buy_volume = sum(qty for _, qty in self.session_buy_trades) 1603 | session_sell_volume = sum(qty for _, qty in self.session_sell_trades) 1604 | 1605 | logger.info(f"本次執行買入量: {session_buy_volume} {self.base_asset}, 賣出量: {session_sell_volume} {self.base_asset}") 1606 | logger.info(f"本次執行Maker買入: {self.session_maker_buy_volume} {self.base_asset}, Maker賣出: {self.session_maker_sell_volume} {self.base_asset}") 1607 | logger.info(f"本次執行Taker買入: {self.session_taker_buy_volume} {self.base_asset}, Taker賣出: {self.session_taker_sell_volume} {self.base_asset}") 1608 | 1609 | def print_trading_stats(self): 1610 | """打印交易統計報表""" 1611 | try: 1612 | logger.info("\n=== 網格交易統計 ===") 1613 | logger.info(f"交易對: {self.symbol}") 1614 | 1615 | today = datetime.now().strftime('%Y-%m-%d') 1616 | 1617 | # 獲取今天的統計數據 1618 | today_stats = self.db.get_trading_stats(self.symbol, today) 1619 | 1620 | if today_stats and len(today_stats) > 0: 1621 | stat = today_stats[0] 1622 | maker_buy = stat['maker_buy_volume'] 1623 | maker_sell = stat['maker_sell_volume'] 1624 | taker_buy = stat['taker_buy_volume'] 1625 | taker_sell = stat['taker_sell_volume'] 1626 | profit = stat['realized_profit'] 1627 | fees = stat['total_fees'] 1628 | net = stat['net_profit'] 1629 | avg_spread = stat['avg_spread'] 1630 | volatility = stat['volatility'] 1631 | 1632 | total_volume = maker_buy + maker_sell + taker_buy + taker_sell 1633 | maker_percentage = ((maker_buy + maker_sell) / total_volume * 100) if total_volume > 0 else 0 1634 | 1635 | logger.info(f"\n今日統計 ({today}):") 1636 | logger.info(f"買入量: {maker_buy + taker_buy} {self.base_asset}") 1637 | logger.info(f"賣出量: {maker_sell + taker_sell} {self.base_asset}") 1638 | logger.info(f"總成交量: {total_volume} {self.base_asset}") 1639 | logger.info(f"Maker佔比: {maker_percentage:.2f}%") 1640 | logger.info(f"波動率: {volatility:.4f}%") 1641 | logger.info(f"毛利潤: {profit:.8f} {self.quote_asset}") 1642 | logger.info(f"總手續費: {fees:.8f} {self.quote_asset}") 1643 | logger.info(f"凈利潤: {net:.8f} {self.quote_asset}") 1644 | 1645 | # 網格狀態 1646 | logger.info(f"\n網格狀態:") 1647 | logger.info(f"網格數量: {len(self.grid_levels)}") 1648 | logger.info(f"價格範圍: {self.grid_levels[0]} - {self.grid_levels[-1]} {self.quote_asset}") 1649 | logger.info(f"每格訂單數量: {self.order_quantity} {self.base_asset}") 1650 | 1651 | # 使用新結構計算活躍訂單數 1652 | total_buy_orders = sum(len(orders) for orders in self.grid_buy_orders_by_price.values()) 1653 | total_sell_orders = sum(len(orders) for orders in self.grid_sell_orders_by_price.values()) 1654 | 1655 | logger.info(f"當前活躍網格訂單: 買單 {total_buy_orders} 個, 賣單 {total_sell_orders} 個") 1656 | 1657 | # 持倉統計 1658 | net_position = self.total_bought - self.total_sold 1659 | position_percentage = (net_position / self.max_position * 100) if self.max_position > 0 else 0 1660 | logger.info(f"當前淨持倉: {net_position:.8f} {self.base_asset} ({position_percentage:.2f}% 占用)") 1661 | 1662 | # 獲取所有時間的總計 1663 | all_time_stats = self.db.get_all_time_stats(self.symbol) 1664 | 1665 | if all_time_stats: 1666 | total_maker_buy = all_time_stats['total_maker_buy'] 1667 | total_maker_sell = all_time_stats['total_maker_sell'] 1668 | total_taker_buy = all_time_stats['total_taker_buy'] 1669 | total_taker_sell = all_time_stats['total_taker_sell'] 1670 | total_profit = all_time_stats['total_profit'] 1671 | total_fees = all_time_stats['total_fees'] 1672 | total_net = all_time_stats['total_net_profit'] 1673 | 1674 | total_volume = total_maker_buy + total_maker_sell + total_taker_buy + total_taker_sell 1675 | maker_percentage = ((total_maker_buy + total_maker_sell) / total_volume * 100) if total_volume > 0 else 0 1676 | 1677 | logger.info(f"\n累計統計:") 1678 | logger.info(f"買入量: {total_maker_buy + total_taker_buy} {self.base_asset}") 1679 | logger.info(f"賣出量: {total_maker_sell + total_taker_sell} {self.base_asset}") 1680 | logger.info(f"總成交量: {total_volume} {self.base_asset}") 1681 | logger.info(f"Maker佔比: {maker_percentage:.2f}%") 1682 | logger.info(f"毛利潤: {total_profit:.8f} {self.quote_asset}") 1683 | logger.info(f"總手續費: {total_fees:.8f} {self.quote_asset}") 1684 | logger.info(f"凈利潤: {total_net:.8f} {self.quote_asset}") 1685 | 1686 | # 查詢前10筆最新成交 1687 | recent_trades = self.db.get_recent_trades(self.symbol, 10) 1688 | 1689 | if recent_trades and len(recent_trades) > 0: 1690 | logger.info("\n最近10筆成交:") 1691 | for i, trade in enumerate(recent_trades): 1692 | maker_str = "Maker" if trade['maker'] else "Taker" 1693 | logger.info(f"{i+1}. {trade['timestamp']} - {trade['side']} {trade['quantity']} @ {trade['price']} ({maker_str}) 手續費: {trade['fee']:.8f}") 1694 | 1695 | # 打印當前依賴關係 1696 | if self.grid_dependencies: 1697 | logger.info("\n當前網格依賴關係:") 1698 | for price, dependent_price in self.grid_dependencies.items(): 1699 | logger.info(f"價格點位 {price} 依賴於 {dependent_price} 的賣單成交") 1700 | 1701 | except Exception as e: 1702 | logger.error(f"打印交易統計時出錯: {e}") 1703 | 1704 | def _ensure_data_streams(self): 1705 | """確保所有必要的數據流訂閲都是活躍的""" 1706 | # 檢查深度流訂閲 1707 | if "depth" not in self.ws.subscriptions: 1708 | logger.info("重新訂閲深度數據流...") 1709 | self.ws.initialize_orderbook() # 重新初始化訂單簿 1710 | self.ws.subscribe_depth() 1711 | 1712 | # 檢查行情數據訂閲 1713 | if "bookTicker" not in self.ws.subscriptions: 1714 | logger.info("重新訂閲行情數據...") 1715 | self.ws.subscribe_bookTicker() 1716 | 1717 | # 檢查私有訂單更新流 1718 | if f"account.orderUpdate.{self.symbol}" not in self.ws.subscriptions: 1719 | logger.info("重新訂閲私有訂單更新流...") 1720 | self.subscribe_order_updates() 1721 | 1722 | def run(self, duration_seconds=3600, interval_seconds=60): 1723 | """執行網格交易策略""" 1724 | logger.info(f"開始運行網格交易策略: {self.symbol}") 1725 | logger.info(f"運行時間: {duration_seconds} 秒, 間隔: {interval_seconds} 秒") 1726 | logger.info(f"最大持倉限制: {self.max_position} {self.base_asset}") 1727 | 1728 | # 重置本次執行的統計數據 1729 | self.session_start_time = datetime.now() 1730 | self.session_buy_trades = [] 1731 | self.session_sell_trades = [] 1732 | self.session_fees = 0.0 1733 | self.session_maker_buy_volume = 0.0 1734 | self.session_maker_sell_volume = 0.0 1735 | self.session_taker_buy_volume = 0.0 1736 | self.session_taker_sell_volume = 0.0 1737 | 1738 | start_time = time.time() 1739 | iteration = 0 1740 | last_report_time = start_time 1741 | report_interval = 300 # 5分鐘打印一次報表 1742 | 1743 | try: 1744 | # 先確保 WebSocket 連接可用 1745 | connection_status = self.check_ws_connection() 1746 | if connection_status: 1747 | # 初始化訂單簿和數據流 1748 | if not self.ws.orderbook["bids"] and not self.ws.orderbook["asks"]: 1749 | self.ws.initialize_orderbook() 1750 | 1751 | # 檢查並確保所有數據流訂閲 1752 | if "depth" not in self.ws.subscriptions: 1753 | self.ws.subscribe_depth() 1754 | if "bookTicker" not in self.ws.subscriptions: 1755 | self.ws.subscribe_bookTicker() 1756 | if f"account.orderUpdate.{self.symbol}" not in self.ws.subscriptions: 1757 | self.subscribe_order_updates() 1758 | 1759 | # 初始化網格交易 1760 | if not self.grid_initialized: 1761 | success = self.initialize_grid() 1762 | if not success: 1763 | logger.error("網格初始化失敗,終止運行") 1764 | return 1765 | 1766 | while time.time() - start_time < duration_seconds: 1767 | iteration += 1 1768 | current_time = time.time() 1769 | logger.info(f"\n=== 第 {iteration} 次迭代 ===") 1770 | logger.info(f"時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 1771 | 1772 | # 檢查連接並在必要時重連 1773 | connection_status = self.check_ws_connection() 1774 | 1775 | # 如果連接成功,檢查並確保所有流訂閲 1776 | if connection_status: 1777 | # 重新訂閲必要的數據流 1778 | self._ensure_data_streams() 1779 | 1780 | # 檢查訂單成交情況 1781 | self.check_order_fills() 1782 | 1783 | # 補充網格訂單 1784 | self.place_limit_orders() 1785 | 1786 | # 估算利潤 1787 | self.estimate_profit() 1788 | 1789 | # 定期打印交易統計報表 1790 | if current_time - last_report_time >= report_interval: 1791 | self.print_trading_stats() 1792 | last_report_time = current_time 1793 | 1794 | # 計算總的PnL和本次執行的PnL 1795 | realized_pnl, unrealized_pnl, total_fees, net_pnl, session_realized_pnl, session_fees, session_net_pnl = self.calculate_pnl() 1796 | 1797 | # 計算持倉信息 1798 | net_position = self.total_bought - self.total_sold 1799 | position_percentage = (net_position / self.max_position * 100) if self.max_position > 0 else 0 1800 | 1801 | logger.info(f"\n統計信息:") 1802 | logger.info(f"總交易次數: {self.trades_executed}") 1803 | logger.info(f"總下單次數: {self.orders_placed}") 1804 | logger.info(f"總取消訂單次數: {self.orders_cancelled}") 1805 | logger.info(f"買入總量: {self.total_bought} {self.base_asset}") 1806 | logger.info(f"賣出總量: {self.total_sold} {self.base_asset}") 1807 | logger.info(f"淨持倉: {net_position:.8f} {self.base_asset} ({position_percentage:.2f}% 占用)") 1808 | logger.info(f"總手續費: {total_fees:.8f} {self.quote_asset}") 1809 | logger.info(f"已實現利潤: {realized_pnl:.8f} {self.quote_asset}") 1810 | logger.info(f"凈利潤: {net_pnl:.8f} {self.quote_asset}") 1811 | logger.info(f"未實現利潤: {unrealized_pnl:.8f} {self.quote_asset}") 1812 | logger.info(f"WebSocket連接狀態: {'已連接' if self.ws and self.ws.is_connected() else '未連接'}") 1813 | 1814 | # 打印本次執行的統計數據 1815 | logger.info(f"\n---本次執行統計---") 1816 | session_buy_volume = sum(qty for _, qty in self.session_buy_trades) 1817 | session_sell_volume = sum(qty for _, qty in self.session_sell_trades) 1818 | logger.info(f"買入量: {session_buy_volume} {self.base_asset}, 賣出量: {session_sell_volume} {self.base_asset}") 1819 | logger.info(f"Maker買入: {self.session_maker_buy_volume} {self.base_asset}, Maker賣出: {self.session_maker_sell_volume} {self.base_asset}") 1820 | logger.info(f"Taker買入: {self.session_taker_buy_volume} {self.base_asset}, Taker賣出: {self.session_taker_sell_volume} {self.base_asset}") 1821 | logger.info(f"本次執行已實現利潤: {session_realized_pnl:.8f} {self.quote_asset}") 1822 | logger.info(f"本次執行手續費: {session_fees:.8f} {self.quote_asset}") 1823 | logger.info(f"本次執行凈利潤: {session_net_pnl:.8f} {self.quote_asset}") 1824 | 1825 | # 使用新結構打印當前網格狀態 1826 | total_buy_orders = sum(len(orders) for orders in self.grid_buy_orders_by_price.values()) 1827 | total_sell_orders = sum(len(orders) for orders in self.grid_sell_orders_by_price.values()) 1828 | 1829 | # 打印當前網格狀態 1830 | logger.info(f"\n---當前網格狀態---") 1831 | logger.info(f"網格點位: {self.grid_levels[0]} - {self.grid_levels[-1]} ({len(self.grid_levels)} 個點位)") 1832 | logger.info(f"活躍買單數量: {total_buy_orders}") 1833 | logger.info(f"活躍賣單數量: {total_sell_orders}") 1834 | logger.info(f"依賴關係數量: {len(self.grid_dependencies)}") 1835 | current_price = self.get_current_price() 1836 | if current_price: 1837 | logger.info(f"當前價格: {current_price} {self.quote_asset}") 1838 | 1839 | # 打印當前存在的依賴關係 1840 | if self.grid_dependencies: 1841 | logger.info("\n當前網格依賴關係:") 1842 | for price, dependent_price in self.grid_dependencies.items(): 1843 | logger.info(f" 價格點位 {price} 依賴於 {dependent_price} 的賣單成交") 1844 | 1845 | wait_time = interval_seconds 1846 | logger.info(f"等待 {wait_time} 秒後進行下一次迭代...") 1847 | time.sleep(wait_time) 1848 | 1849 | # 結束運行時打印最終報表 1850 | logger.info("\n=== 網格交易策略運行結束 ===") 1851 | self.print_trading_stats() 1852 | 1853 | except KeyboardInterrupt: 1854 | logger.info("\n用户中斷,停止網格交易") 1855 | 1856 | finally: 1857 | logger.info("取消所有網格訂單...") 1858 | self.cancel_existing_orders() 1859 | 1860 | # 關閉 WebSocket 1861 | if self.ws: 1862 | self.ws.close() 1863 | 1864 | # 關閉數據庫連接 1865 | if self.db: 1866 | self.db.close() 1867 | logger.info("數據庫連接已關閉") --------------------------------------------------------------------------------