├── .env ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── README.md ├── __pycache__ ├── command_panel.cpython-312.pyc ├── config.cpython-312.pyc ├── interactive_panel.cpython-312.pyc ├── key_handler.cpython-312.pyc └── logger.cpython-312.pyc ├── api ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ ├── auth.cpython-312.pyc │ └── client.cpython-312.pyc ├── auth.py └── client.py ├── cli ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ └── commands.cpython-312.pyc └── commands.py ├── config.py ├── database ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ └── db.cpython-312.pyc └── db.py ├── logger.py ├── main.py ├── panel ├── README.md ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ ├── interactive_panel.cpython-312.pyc │ ├── key_handler.cpython-312.pyc │ ├── panel_main.cpython-312.pyc │ ├── settings.cpython-312.pyc │ └── simple_panel.cpython-312.pyc ├── interactive_panel.py ├── key_handler.py ├── panel_main.py └── settings.py ├── requirements.txt ├── run.py ├── settings └── panel_settings.json ├── strategies ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ └── market_maker.cpython-312.pyc └── market_maker.py ├── utils ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ ├── helpers.cpython-312.pyc │ └── settings_manager.cpython-312.pyc └── helpers.py └── ws_client ├── __init__.py ├── __pycache__ ├── __init__.cpython-312.pyc └── client.cpython-312.pyc └── client.py /.env: -------------------------------------------------------------------------------- 1 | API_KEY=輸入你的API_KEY 2 | SECRET_KEY=輸入你的SECRET_KEY 3 | PROXY_WEBSOCKET= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | .env -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.2] - 2025-04-10 4 | 5 | ### 新增 6 | 7 | - 添加 WebSocket 代理連接支持,允許通過代理服務器連接 8 | 9 | ### 優化 10 | 11 | - 自動終止程序功能:當 API 簽名創建失敗時立即停止運行,防止無效操作 12 | 13 | 14 | ## [1.1.1] - 2025-04-09 15 | 16 | ### 新增 17 | 18 | - 徹底改進重平衡機制風險評估方法,基於總資產價值而非累計交易量 19 | - 添加動態風險暴露度計算,對實際風險提供更精確的評估 20 | - 為重平衡決策添加詳細日誌,包括淨部位價值和風險暴露比例 21 | 22 | ### 優化 23 | 24 | - 完善重平衡系統的錯誤處理,確保網絡或API問題時仍能正常運作 25 | - 為風險判斷增加備用機制,在資產數據無法獲取時回退至舊方法 26 | 27 | ### 安全 28 | 29 | - 改進風險評估算法,防止在大量交易累積後風險被系統性低估 30 | 31 | ## [1.1.0] - 2025-04-09 32 | 33 | ### 新增 34 | 35 | - 為 WebSocket 客戶端添加專門的日誌標識,使日誌更易識別 36 | - 在心跳檢測失敗時添加更詳細的日誌記錄 37 | 38 | ### 修改 39 | 40 | - 將 WebSocket 相關的日誌改名為 `backpack_ws`,使日誌更具描述性 41 | 42 | ### 修復 43 | 44 | - 徹底重構 WebSocket 斷線重連機制,解決連接無法完全重置的問題 45 | - 修復 WebSocket 斷線後資源未完全釋放的問題 46 | - 改進 `on_close` 處理邏輯,使用獨立線程進行重連以避免阻塞回調 47 | - 優化連接關閉邏輯,確保所有相關資源和線程都被正確清理 48 | - 增強 WebSocket 心跳檢測機制,更快偵測並恢復中斷的連接 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backpack Exchange 做市交易程序 2 | 3 | 這是一個針對 Backpack Exchange 的加密貨幣做市交易程序。該程序提供自動化做市功能,通過維持買賣價差賺取利潤。 4 | 5 | Backpack 註冊連結:[https://backpack.exchange/refer/yan](https://backpack.exchange/refer/yan) 6 | 7 | Twitter:[Yan Practice ⭕散修](https://x.com/practice_y11) 8 | 9 | ## 功能特點 10 | 11 | - 自動化做市策略 12 | - 基礎價差設置 13 | - 自動重平衡倉位 14 | - 詳細的交易統計 15 | - WebSocket 實時數據連接 16 | - 命令行界面 17 | 18 | ## 項目結構 19 | 20 | ``` 21 | lemon_trader/ 22 | │ 23 | ├── api/ # API相關模塊 24 | │ ├── __init__.py 25 | │ ├── auth.py # API認證和簽名相關 26 | │ └── client.py # API請求客戶端 27 | │ 28 | ├── websocket/ # WebSocket模塊 29 | │ ├── __init__.py 30 | │ └── client.py # WebSocket客戶端 31 | │ 32 | ├── database/ # 數據庫模塊 33 | │ ├── __init__.py 34 | │ └── db.py # 數據庫操作 35 | │ 36 | ├── strategies/ # 策略模塊 37 | │ ├── __init__.py 38 | │ └── market_maker.py # 做市策略 39 | │ 40 | ├── utils/ # 工具模塊 41 | │ ├── __init__.py 42 | │ └── helpers.py # 輔助函數 43 | │ 44 | ├── cli/ # 命令行界面 45 | │ ├── __init__.py 46 | │ └── commands.py # 命令行命令 47 | │ 48 | ├── panel/ # 交互式面板 49 | │ ├── __init__.py 50 | │ └── interactive_panel.py # 交互式面板實現 51 | │ 52 | ├── config.py # 配置文件 53 | ├── logger.py # 日誌配置 54 | ├── main.py # 主執行文件 55 | ├── run.py # 統一入口文件 56 | └── README.md # 說明文檔 57 | ``` 58 | 59 | ## 環境要求 60 | 61 | - Python 3.8 或更高版本 62 | - 所需第三方庫: 63 | - nacl (用於API簽名) 64 | - requests 65 | - websocket-client 66 | - numpy 67 | - python-dotenv 68 | 69 | ## 安裝 70 | 71 | 1. 克隆或下載此代碼庫: 72 | 73 | ```bash 74 | git clone https://github.com/yanowo/Backpack-MM-Simple.git 75 | cd Backpack-MM-Simple 76 | ``` 77 | 78 | 2. 安裝依賴: 79 | 80 | ```bash 81 | pip install -r requirements.txt 82 | ``` 83 | 84 | 3. 設置環境變數: 85 | 86 | 創建 `.env` 文件並添加: 87 | 88 | ``` 89 | API_KEY=your_api_key 90 | SECRET_KEY=your_secret_key 91 | PROXY_WEBSOCKET=http://user:pass@host:port/ 或者 http://host:port (若不需要則留空或移除) 92 | ``` 93 | 94 | ## 使用方法 95 | 96 | ### 統一入口 (推薦) 97 | 98 | ```bash 99 | # 啟動交互式面板 100 | python run.py --panel 101 | 102 | # 啟動命令行界面 103 | python run.py --cli 104 | 105 | # 直接運行做市策略 106 | python run.py --symbol SOL_USDC --spread 0.1 107 | ``` 108 | 109 | ### 命令行界面 110 | 111 | 啟動命令行界面: 112 | 113 | ```bash 114 | python main.py --cli 115 | ``` 116 | 117 | ### 直接執行做市策略 118 | 119 | ```bash 120 | python main.py --symbol SOL_USDC --spread 0.5 --max-orders 3 --duration 3600 --interval 60 121 | ``` 122 | 123 | ### 命令行參數 124 | 125 | - `--api-key`: API 密鑰 (可選,默認使用環境變數) 126 | - `--secret-key`: API 密鑰 (可選,默認使用環境變數) 127 | - `--ws-proxy`: Websocket 代理 (可選,默認使用環境變數) 128 | - `--cli`: 啟動命令行界面 129 | - `--panel`: 啟動交互式面板 130 | 131 | ### 做市參數 132 | 133 | - `--symbol`: 交易對 (例如: SOL_USDC) 134 | - `--spread`: 價差百分比 (例如: 0.5) 135 | - `--quantity`: 訂單數量 (可選) 136 | - `--max-orders`: 每側最大訂單數量 (默認: 3) 137 | - `--duration`: 運行時間(秒)(默認: 3600) 138 | - `--interval`: 更新間隔(秒)(默認: 60) 139 | 140 | ## 設定保存 141 | 142 | 通過面板模式修改的設定會自動保存到 `settings/panel_settings.json` 文件中,下次啟動時會自動加載。 143 | 144 | ## 運行示例 145 | 146 | ### 基本做市示例 147 | 148 | ```bash 149 | python run.py --symbol SOL_USDC --spread 0.2 --max-orders 5 150 | ``` 151 | 152 | ### 長時間運行示例 153 | 154 | ```bash 155 | python run.py --symbol SOL_USDC --spread 0.1 --duration 86400 --interval 120 156 | ``` 157 | 158 | ### 完整參數示例 159 | 160 | ```bash 161 | python run.py --symbol SOL_USDC --spread 0.3 --quantity 0.5 --max-orders 3 --duration 7200 --interval 60 162 | ``` 163 | 164 | ## 注意事項 165 | 166 | - 交易涉及風險,請謹慎使用 167 | - 建議先在小資金上測試策略效果 168 | - 定期檢查交易統計以評估策略表現 169 | -------------------------------------------------------------------------------- /__pycache__/command_panel.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/__pycache__/command_panel.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/config.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/__pycache__/config.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/interactive_panel.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/__pycache__/interactive_panel.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/key_handler.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/__pycache__/key_handler.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/logger.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/__pycache__/logger.cpython-312.pyc -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | # api/__init__.py 2 | """ 3 | API 模塊,負責與交易所API的通訊 4 | """ -------------------------------------------------------------------------------- /api/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/api/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /api/__pycache__/auth.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/api/__pycache__/auth.cpython-312.pyc -------------------------------------------------------------------------------- /api/__pycache__/client.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/api/__pycache__/client.cpython-312.pyc -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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 | }) 61 | 62 | # 添加查詢參數到URL 63 | if params and method.upper() in ['GET', 'DELETE']: 64 | query_string = "&".join([f"{k}={v}" for k, v in params.items()]) 65 | url += f"?{query_string}" 66 | 67 | # 實施重試機制 68 | for attempt in range(retry_count): 69 | try: 70 | if method.upper() == 'GET': 71 | response = requests.get(url, headers=headers, timeout=10) 72 | elif method.upper() == 'POST': 73 | response = requests.post(url, headers=headers, data=json.dumps(data) if data else None, timeout=10) 74 | elif method.upper() == 'DELETE': 75 | response = requests.delete(url, headers=headers, data=json.dumps(data) if data else None, timeout=10) 76 | else: 77 | return {"error": f"不支持的請求方法: {method}"} 78 | 79 | # 處理響應 80 | if response.status_code in [200, 201]: 81 | return response.json() if response.text.strip() else {} 82 | elif response.status_code == 429: # 速率限制 83 | wait_time = 1 * (2 ** attempt) # 指數退避 84 | logger.warning(f"遇到速率限制,等待 {wait_time} 秒後重試") 85 | time.sleep(wait_time) 86 | continue 87 | else: 88 | error_msg = f"狀態碼: {response.status_code}, 消息: {response.text}" 89 | if attempt < retry_count - 1: 90 | logger.warning(f"請求失敗 ({attempt+1}/{retry_count}): {error_msg}") 91 | time.sleep(1) # 簡單重試延遲 92 | continue 93 | return {"error": error_msg} 94 | 95 | except requests.exceptions.Timeout: 96 | if attempt < retry_count - 1: 97 | logger.warning(f"請求超時 ({attempt+1}/{retry_count}),重試中...") 98 | continue 99 | return {"error": "請求超時"} 100 | except requests.exceptions.ConnectionError: 101 | if attempt < retry_count - 1: 102 | logger.warning(f"連接錯誤 ({attempt+1}/{retry_count}),重試中...") 103 | time.sleep(2) # 連接錯誤通常需要更長等待 104 | continue 105 | return {"error": "連接錯誤"} 106 | except Exception as e: 107 | if attempt < retry_count - 1: 108 | logger.warning(f"請求異常 ({attempt+1}/{retry_count}): {str(e)},重試中...") 109 | continue 110 | return {"error": f"請求失敗: {str(e)}"} 111 | 112 | return {"error": "達到最大重試次數"} 113 | 114 | # 各API端點函數 115 | def get_deposit_address(api_key, secret_key, blockchain): 116 | """獲取存款地址""" 117 | endpoint = f"/wapi/{API_VERSION}/capital/deposit/address" 118 | instruction = "depositAddressQuery" 119 | params = {"blockchain": blockchain} 120 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 121 | 122 | def get_balance(api_key, secret_key): 123 | """獲取賬戶餘額""" 124 | endpoint = f"/api/{API_VERSION}/capital" 125 | instruction = "balanceQuery" 126 | return make_request("GET", endpoint, api_key, secret_key, instruction) 127 | 128 | def execute_order(api_key, secret_key, order_details): 129 | """執行訂單""" 130 | endpoint = f"/api/{API_VERSION}/order" 131 | instruction = "orderExecute" 132 | 133 | # 提取所有參數用於簽名 134 | params = { 135 | "orderType": order_details["orderType"], 136 | "price": order_details.get("price", "0"), 137 | "quantity": order_details["quantity"], 138 | "side": order_details["side"], 139 | "symbol": order_details["symbol"], 140 | "timeInForce": order_details.get("timeInForce", "GTC") 141 | } 142 | 143 | # 添加可選參數 144 | for key in ["postOnly", "reduceOnly", "clientId", "quoteQuantity", 145 | "autoBorrow", "autoLendRedeem", "autoBorrowRepay", "autoLend"]: 146 | if key in order_details: 147 | params[key] = str(order_details[key]).lower() if isinstance(order_details[key], bool) else str(order_details[key]) 148 | 149 | return make_request("POST", endpoint, api_key, secret_key, instruction, params, order_details) 150 | 151 | def get_open_orders(api_key, secret_key, symbol=None): 152 | """獲取未成交訂單""" 153 | endpoint = f"/api/{API_VERSION}/orders" 154 | instruction = "orderQueryAll" 155 | params = {} 156 | if symbol: 157 | params["symbol"] = symbol 158 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 159 | 160 | def cancel_all_orders(api_key, secret_key, symbol): 161 | """取消所有訂單""" 162 | endpoint = f"/api/{API_VERSION}/orders" 163 | instruction = "orderCancelAll" 164 | params = {"symbol": symbol} 165 | data = {"symbol": symbol} 166 | return make_request("DELETE", endpoint, api_key, secret_key, instruction, params, data) 167 | 168 | def cancel_order(api_key, secret_key, order_id, symbol): 169 | """取消指定訂單""" 170 | endpoint = f"/api/{API_VERSION}/order" 171 | instruction = "orderCancel" 172 | params = {"orderId": order_id, "symbol": symbol} 173 | data = {"orderId": order_id, "symbol": symbol} 174 | return make_request("DELETE", endpoint, api_key, secret_key, instruction, params, data) 175 | 176 | def get_ticker(symbol): 177 | """獲取市場價格""" 178 | endpoint = f"/api/{API_VERSION}/ticker" 179 | params = {"symbol": symbol} 180 | return make_request("GET", endpoint, params=params) 181 | 182 | def get_markets(): 183 | """獲取所有交易對信息""" 184 | endpoint = f"/api/{API_VERSION}/markets" 185 | return make_request("GET", endpoint) 186 | 187 | def get_order_book(symbol, limit=20): 188 | """獲取市場深度""" 189 | endpoint = f"/api/{API_VERSION}/depth" 190 | params = {"symbol": symbol, "limit": str(limit)} 191 | return make_request("GET", endpoint, params=params) 192 | 193 | def get_fill_history(api_key, secret_key, symbol=None, limit=100): 194 | """獲取歷史成交記錄""" 195 | endpoint = f"/wapi/{API_VERSION}/history/fills" 196 | instruction = "fillHistoryQueryAll" 197 | params = {"limit": str(limit)} 198 | if symbol: 199 | params["symbol"] = symbol 200 | return make_request("GET", endpoint, api_key, secret_key, instruction, params) 201 | 202 | def get_klines(symbol, interval="1h", limit=100): 203 | """獲取K線數據""" 204 | endpoint = f"/api/{API_VERSION}/klines" 205 | 206 | # 計算起始時間 (秒) 207 | current_time = int(time.time()) 208 | 209 | # 各間隔對應的秒數 210 | interval_seconds = { 211 | "1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800, 212 | "1h": 3600, "2h": 7200, "4h": 14400, "6h": 21600, "8h": 28800, 213 | "12h": 43200, "1d": 86400, "3d": 259200, "1w": 604800, "1month": 2592000 214 | } 215 | 216 | # 計算合適的起始時間 217 | duration = interval_seconds.get(interval, 3600) 218 | start_time = current_time - (duration * limit) 219 | 220 | params = { 221 | "symbol": symbol, 222 | "interval": interval, 223 | "startTime": str(start_time) 224 | } 225 | 226 | return make_request("GET", endpoint, params=params) 227 | 228 | def get_market_limits(symbol): 229 | """獲取交易對的最低訂單量和價格精度""" 230 | markets_info = get_markets() 231 | 232 | if not isinstance(markets_info, dict) and isinstance(markets_info, list): 233 | for market_info in markets_info: 234 | if market_info.get('symbol') == symbol: 235 | base_asset = market_info.get('baseSymbol') 236 | quote_asset = market_info.get('quoteSymbol') 237 | 238 | # 從filters中獲取精度和最小訂單量信息 239 | filters = market_info.get('filters', {}) 240 | base_precision = 8 # 默認值 241 | quote_precision = 8 # 默認值 242 | min_order_size = "0" # 默認值 243 | tick_size = "0.00000001" # 默認值 244 | 245 | if 'price' in filters: 246 | tick_size = filters['price'].get('tickSize', '0.00000001') 247 | quote_precision = len(tick_size.split('.')[-1]) if '.' in tick_size else 0 248 | 249 | if 'quantity' in filters: 250 | min_order_size = filters['quantity'].get('minQuantity', '0') 251 | min_value = filters['quantity'].get('minQuantity', '0.00000001') 252 | base_precision = len(min_value.split('.')[-1]) if '.' in min_value else 0 253 | 254 | return { 255 | 'base_asset': base_asset, 256 | 'quote_asset': quote_asset, 257 | 'base_precision': base_precision, 258 | 'quote_precision': quote_precision, 259 | 'min_order_size': min_order_size, 260 | 'tick_size': tick_size 261 | } 262 | 263 | logger.error(f"找不到交易對 {symbol} 的信息") 264 | return None 265 | else: 266 | logger.error(f"無法獲取交易對信息: {markets_info}") 267 | return None -------------------------------------------------------------------------------- /cli/__init__.py: -------------------------------------------------------------------------------- 1 | # cli/__init__.py 2 | """ 3 | CLI 模塊,提供命令行界面 4 | """ -------------------------------------------------------------------------------- /cli/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/cli/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /cli/__pycache__/commands.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/cli/__pycache__/commands.cpython-312.pyc -------------------------------------------------------------------------------- /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.market_maker import MarketMaker 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_market_maker_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 | spread_percentage = float(input("請輸入價差百分比 (例如: 0.5 表示0.5%): ")) 170 | quantity_input = input("請輸入每個訂單的數量 (留空則自動根據餘額計算): ") 171 | quantity = float(quantity_input) if quantity_input.strip() else None 172 | max_orders = int(input("請輸入每側(買/賣)最大訂單數 (例如: 3): ")) 173 | 174 | duration = int(input("請輸入運行時間(秒) (例如: 3600 表示1小時): ")) 175 | interval = int(input("請輸入更新間隔(秒) (例如: 60 表示1分鐘): ")) 176 | 177 | try: 178 | # 初始化數據庫 179 | db = Database() 180 | 181 | # 初始化做市商 182 | market_maker = MarketMaker( 183 | api_key=api_key, 184 | secret_key=secret_key, 185 | symbol=symbol, 186 | db_instance=db, 187 | base_spread_percentage=spread_percentage, 188 | order_quantity=quantity, 189 | max_orders=max_orders, 190 | ws_proxy=ws_proxy 191 | ) 192 | 193 | # 執行做市策略 194 | market_maker.run(duration_seconds=duration, interval_seconds=interval) 195 | 196 | except Exception as e: 197 | print(f"做市過程中發生錯誤: {str(e)}") 198 | import traceback 199 | traceback.print_exc() 200 | 201 | def trading_stats_command(api_key, secret_key): 202 | """查看交易統計命令""" 203 | symbol = input("請輸入要查看統計的交易對 (例如: SOL_USDC): ") 204 | 205 | try: 206 | # 初始化數據庫 207 | db = Database() 208 | 209 | # 獲取今日統計 210 | today = datetime.now().strftime('%Y-%m-%d') 211 | today_stats = db.get_trading_stats(symbol, today) 212 | 213 | print("\n=== 做市商交易統計 ===") 214 | print(f"交易對: {symbol}") 215 | 216 | if today_stats and len(today_stats) > 0: 217 | stat = today_stats[0] 218 | maker_buy = stat['maker_buy_volume'] 219 | maker_sell = stat['maker_sell_volume'] 220 | taker_buy = stat['taker_buy_volume'] 221 | taker_sell = stat['taker_sell_volume'] 222 | profit = stat['realized_profit'] 223 | fees = stat['total_fees'] 224 | net = stat['net_profit'] 225 | avg_spread = stat.get('avg_spread', 0) 226 | volatility = stat.get('volatility', 0) 227 | 228 | total_volume = maker_buy + maker_sell + taker_buy + taker_sell 229 | maker_percentage = ((maker_buy + maker_sell) / total_volume * 100) if total_volume > 0 else 0 230 | 231 | print(f"\n今日統計 ({today}):") 232 | print(f"總成交量: {total_volume}") 233 | print(f"Maker買入量: {maker_buy}") 234 | print(f"Maker賣出量: {maker_sell}") 235 | print(f"Taker買入量: {taker_buy}") 236 | print(f"Taker賣出量: {taker_sell}") 237 | print(f"Maker佔比: {maker_percentage:.2f}%") 238 | print(f"平均價差: {avg_spread:.4f}%") 239 | print(f"波動率: {volatility:.4f}%") 240 | print(f"毛利潤: {profit:.8f}") 241 | print(f"總手續費: {fees:.8f}") 242 | print(f"凈利潤: {net:.8f}") 243 | else: 244 | print(f"今日沒有 {symbol} 的交易記錄") 245 | 246 | # 獲取所有時間的統計 247 | all_time_stats = db.get_all_time_stats(symbol) 248 | 249 | if all_time_stats: 250 | maker_buy = all_time_stats['total_maker_buy'] 251 | maker_sell = all_time_stats['total_maker_sell'] 252 | taker_buy = all_time_stats['total_taker_buy'] 253 | taker_sell = all_time_stats['total_taker_sell'] 254 | profit = all_time_stats['total_profit'] 255 | fees = all_time_stats['total_fees'] 256 | net = all_time_stats['total_net_profit'] 257 | avg_spread = all_time_stats.get('avg_spread_all_time', 0) 258 | 259 | total_volume = maker_buy + maker_sell + taker_buy + taker_sell 260 | maker_percentage = ((maker_buy + maker_sell) / total_volume * 100) if total_volume > 0 else 0 261 | 262 | print(f"\n累計統計:") 263 | print(f"總成交量: {total_volume}") 264 | print(f"Maker買入量: {maker_buy}") 265 | print(f"Maker賣出量: {maker_sell}") 266 | print(f"Taker買入量: {taker_buy}") 267 | print(f"Taker賣出量: {taker_sell}") 268 | print(f"Maker佔比: {maker_percentage:.2f}%") 269 | print(f"平均價差: {avg_spread:.4f}%") 270 | print(f"毛利潤: {profit:.8f}") 271 | print(f"總手續費: {fees:.8f}") 272 | print(f"凈利潤: {net:.8f}") 273 | else: 274 | print(f"沒有 {symbol} 的歷史交易記錄") 275 | 276 | # 獲取最近交易 277 | recent_trades = db.get_recent_trades(symbol, 10) 278 | 279 | if recent_trades and len(recent_trades) > 0: 280 | print("\n最近10筆成交:") 281 | for i, trade in enumerate(recent_trades): 282 | maker_str = "Maker" if trade['maker'] else "Taker" 283 | print(f"{i+1}. {trade['timestamp']} - {trade['side']} {trade['quantity']} @ {trade['price']} ({maker_str}) 手續費: {trade['fee']:.8f}") 284 | else: 285 | print(f"沒有 {symbol} 的最近成交記錄") 286 | 287 | # 關閉數據庫連接 288 | db.close() 289 | 290 | except Exception as e: 291 | print(f"查看交易統計時發生錯誤: {str(e)}") 292 | import traceback 293 | traceback.print_exc() 294 | 295 | def market_analysis_command(api_key, secret_key, ws_proxy=None): 296 | """市場分析命令""" 297 | symbol = input("請輸入要分析的交易對 (例如: SOL_USDC): ") 298 | try: 299 | print("\n執行市場分析...") 300 | 301 | # 創建臨時WebSocket連接 302 | ws = BackpackWebSocket(api_key, secret_key, symbol, auto_reconnect=True, proxy=ws_proxy) 303 | ws.connect() 304 | 305 | # 等待連接建立 306 | wait_time = 0 307 | max_wait_time = 5 308 | while not ws.connected and wait_time < max_wait_time: 309 | time.sleep(0.5) 310 | wait_time += 0.5 311 | 312 | if not ws.connected: 313 | print("WebSocket連接超時,無法進行完整分析") 314 | else: 315 | # 初始化訂單簿 316 | ws.initialize_orderbook() 317 | 318 | # 訂閲必要數據流 319 | ws.subscribe_depth() 320 | ws.subscribe_bookTicker() 321 | 322 | # 等待數據更新 323 | print("等待數據更新...") 324 | time.sleep(3) 325 | 326 | # 獲取K線數據分析趨勢 327 | print("獲取歷史數據分析趨勢...") 328 | klines = get_klines(symbol, "15m") 329 | 330 | # 添加調試信息查看數據結構 331 | print("K線數據結構: ") 332 | if isinstance(klines, dict) and "error" in klines: 333 | print(f"獲取K線數據出錯: {klines['error']}") 334 | else: 335 | print(f"收到 {len(klines) if isinstance(klines, list) else type(klines)} 條K線數據") 336 | 337 | # 檢查第一條記錄以確定結構 338 | if isinstance(klines, list) and len(klines) > 0: 339 | print(f"第一條K線數據: {klines[0]}") 340 | 341 | # 根據實際結構提取收盤價 342 | try: 343 | if isinstance(klines[0], dict): 344 | if 'close' in klines[0]: 345 | # 如果是包含'close'字段的字典 346 | prices = [float(kline['close']) for kline in klines] 347 | elif 'c' in klines[0]: 348 | # 另一種常見格式 349 | prices = [float(kline['c']) for kline in klines] 350 | else: 351 | print(f"無法識別的字典K線格式,可用字段: {list(klines[0].keys())}") 352 | raise ValueError("無法識別的K線數據格式") 353 | elif isinstance(klines[0], list): 354 | # 如果是列表格式,打印元素數量和數據樣例 355 | print(f"K線列表格式,每條記錄有 {len(klines[0])} 個元素") 356 | if len(klines[0]) >= 5: 357 | # 通常第4或第5個元素是收盤價 358 | try: 359 | # 嘗試第4個元素 (索引3) 360 | prices = [float(kline[3]) for kline in klines] 361 | print("使用索引3作為收盤價") 362 | except (ValueError, IndexError): 363 | # 如果失敗,嘗試第5個元素 (索引4) 364 | prices = [float(kline[4]) for kline in klines] 365 | print("使用索引4作為收盤價") 366 | else: 367 | print("K線記錄元素數量不足") 368 | raise ValueError("K線數據格式不兼容") 369 | else: 370 | print(f"未知的K線數據類型: {type(klines[0])}") 371 | raise ValueError("未知的K線數據類型") 372 | 373 | # 計算移動平均 374 | short_ma = sum(prices[-5:]) / 5 if len(prices) >= 5 else sum(prices) / len(prices) 375 | medium_ma = sum(prices[-20:]) / 20 if len(prices) >= 20 else short_ma 376 | long_ma = sum(prices[-50:]) / 50 if len(prices) >= 50 else medium_ma 377 | 378 | # 判斷趨勢 379 | trend = "上漲" if short_ma > medium_ma > long_ma else "下跌" if short_ma < medium_ma < long_ma else "盤整" 380 | 381 | # 計算波動率 382 | volatility = calculate_volatility(prices) 383 | 384 | print("\n市場趨勢分析:") 385 | print(f"短期均價 (5週期): {short_ma:.6f}") 386 | print(f"中期均價 (20週期): {medium_ma:.6f}") 387 | print(f"長期均價 (50週期): {long_ma:.6f}") 388 | print(f"當前趨勢: {trend}") 389 | print(f"波動率: {volatility:.2f}%") 390 | 391 | # 獲取最新價格和波動性指標 392 | current_price = ws.get_current_price() 393 | liquidity_profile = ws.get_liquidity_profile() 394 | 395 | if current_price and liquidity_profile: 396 | print(f"\n當前價格: {current_price}") 397 | print(f"相對長期均價: {(current_price / long_ma - 1) * 100:.2f}%") 398 | 399 | # 流動性分析 400 | buy_volume = liquidity_profile['bid_volume'] 401 | sell_volume = liquidity_profile['ask_volume'] 402 | imbalance = liquidity_profile['imbalance'] 403 | 404 | print("\n市場流動性分析:") 405 | print(f"買單量: {buy_volume:.4f}") 406 | print(f"賣單量: {sell_volume:.4f}") 407 | print(f"買賣比例: {(buy_volume/sell_volume):.2f}" if sell_volume > 0 else "買賣比例: 無限") 408 | 409 | # 判斷市場情緒 410 | sentiment = "買方壓力較大" if imbalance > 0.2 else "賣方壓力較大" if imbalance < -0.2 else "買賣壓力平衡" 411 | print(f"市場情緒: {sentiment} ({imbalance:.2f})") 412 | 413 | # 給出建議的做市參數 414 | print("\n建議做市參數:") 415 | 416 | # 根據波動率調整價差 417 | suggested_spread = max(0.2, min(2.0, volatility * 0.2)) 418 | print(f"建議價差: {suggested_spread:.2f}%") 419 | 420 | # 根據流動性調整訂單數量 421 | liquidity_score = (buy_volume + sell_volume) / 2 422 | orders_suggestion = 3 423 | if liquidity_score > 10: 424 | orders_suggestion = 5 425 | elif liquidity_score < 1: 426 | orders_suggestion = 2 427 | print(f"建議訂單數: {orders_suggestion}") 428 | 429 | # 根據趨勢和情緒建議執行模式 430 | if trend == "上漲" and imbalance > 0: 431 | mode = "adaptive" 432 | print("建議執行模式: 自適應模式 (跟隨上漲趨勢)") 433 | elif trend == "下跌" and imbalance < 0: 434 | mode = "passive" 435 | print("建議執行模式: 被動模式 (降低下跌風險)") 436 | else: 437 | mode = "standard" 438 | print("建議執行模式: 標準模式") 439 | except Exception as e: 440 | print(f"處理K線數據時出錯: {e}") 441 | import traceback 442 | traceback.print_exc() 443 | else: 444 | print("未收到有效的K線數據") 445 | 446 | # 關閉WebSocket連接 447 | if ws: 448 | ws.close() 449 | 450 | except Exception as e: 451 | print(f"市場分析時發生錯誤: {str(e)}") 452 | import traceback 453 | traceback.print_exc() 454 | 455 | def main_cli(api_key=API_KEY, secret_key=SECRET_KEY, ws_proxy=None): 456 | """主CLI函數""" 457 | while True: 458 | print("\n===== Backpack Exchange 交易程序 =====") 459 | print("1 - 查詢存款地址") 460 | print("2 - 查詢餘額") 461 | print("3 - 獲取市場信息") 462 | print("4 - 獲取訂單簿") 463 | print("5 - 執行做市策略") 464 | print("6 - 交易統計報表") 465 | print("7 - 市場分析") 466 | print("8 - 退出") 467 | 468 | operation = input("請輸入操作類型: ") 469 | 470 | if operation == '1': 471 | get_address_command(api_key, secret_key) 472 | elif operation == '2': 473 | get_balance_command(api_key, secret_key) 474 | elif operation == '3': 475 | get_markets_command() 476 | elif operation == '4': 477 | get_orderbook_command(api_key, secret_key, ws_proxy=ws_proxy) 478 | elif operation == '5': 479 | run_market_maker_command(api_key, secret_key, ws_proxy=ws_proxy) 480 | elif operation == '6': 481 | trading_stats_command(api_key, secret_key) 482 | elif operation == '7': 483 | market_analysis_command(api_key, secret_key, ws_proxy=ws_proxy) 484 | elif operation == '8': 485 | print("退出程序。") 486 | break 487 | else: 488 | print("輸入錯誤,請重新輸入。") -------------------------------------------------------------------------------- /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 = "market_maker.log" -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | # database/__init__.py 2 | """ 3 | Database 模塊,負責數據存儲與檢索 4 | """ -------------------------------------------------------------------------------- /database/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/database/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /database/__pycache__/db.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/database/__pycache__/db.cpython-312.pyc -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 日誌配置模塊 3 | """ 4 | import logging 5 | import sys 6 | from config import LOG_FILE 7 | 8 | def setup_logger(name="market_maker"): 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 -------------------------------------------------------------------------------- /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.market_maker import MarketMaker 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('--spread', type=float, help='價差百分比 (例如: 0.5)') 29 | parser.add_argument('--quantity', type=float, help='訂單數量 (可選)') 30 | parser.add_argument('--max-orders', type=int, default=3, help='每側最大訂單數量 (默認: 3)') 31 | parser.add_argument('--duration', type=int, default=3600, help='運行時間(秒)(默認: 3600)') 32 | parser.add_argument('--interval', type=int, default=60, help='更新間隔(秒)(默認: 60)') 33 | 34 | return parser.parse_args() 35 | 36 | def run_market_maker(args, api_key, secret_key, ws_proxy=None): 37 | """運行做市策略""" 38 | # 檢查必要參數 39 | if not args.symbol: 40 | logger.error("缺少交易對參數 (--symbol)") 41 | return 42 | 43 | if not args.spread and args.spread != 0: 44 | logger.error("缺少價差參數 (--spread)") 45 | return 46 | 47 | try: 48 | # 初始化做市商 49 | market_maker = MarketMaker( 50 | api_key=api_key, 51 | secret_key=secret_key, 52 | symbol=args.symbol, 53 | base_spread_percentage=args.spread, 54 | order_quantity=args.quantity, 55 | max_orders=args.max_orders, 56 | ws_proxy=ws_proxy 57 | ) 58 | 59 | # 執行做市策略 60 | market_maker.run(duration_seconds=args.duration, interval_seconds=args.interval) 61 | 62 | except KeyboardInterrupt: 63 | logger.info("收到中斷信號,正在退出...") 64 | except Exception as e: 65 | logger.error(f"做市過程中發生錯誤: {e}") 66 | import traceback 67 | traceback.print_exc() 68 | 69 | def main(): 70 | """主函數""" 71 | args = parse_arguments() 72 | 73 | # 優先使用命令行參數中的API密鑰 74 | api_key = args.api_key or API_KEY 75 | secret_key = args.secret_key or SECRET_KEY 76 | # 读取wss代理 77 | ws_proxy = args.ws_proxy or WS_PROXY 78 | 79 | # 檢查API密鑰 80 | if not api_key or not secret_key: 81 | logger.error("缺少API密鑰,請通過命令行參數或環境變量提供") 82 | sys.exit(1) 83 | 84 | # 決定執行模式 85 | if args.cli: 86 | # 啟動命令行界面 87 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 88 | elif args.symbol: 89 | # 如果指定了交易對,直接運行做市策略 90 | run_market_maker(args, api_key, secret_key, ws_proxy=ws_proxy) 91 | else: 92 | # 默認啟動命令行界面 93 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 94 | 95 | if __name__ == "__main__": 96 | main() -------------------------------------------------------------------------------- /panel/README.md: -------------------------------------------------------------------------------- 1 | # 交互式命令面板模組 2 | 3 | 這個模組提供了一個交互式的命令面板,用於控制和監控做市策略。 4 | 5 | ## 功能特點 6 | 7 | - 直觀的圖形界面顯示市場資料 8 | - 命令行操作控制策略 9 | - 實時數據更新 10 | - 設定保存和加載 11 | - 跨平台鍵盤輸入處理 12 | 13 | ## 文件說明 14 | 15 | - `interactive_panel.py`: 核心面板界面和功能 16 | - `key_handler.py`: 跨平台鍵盤輸入處理 17 | - `panel_main.py`: 面板獨立運行入口 18 | - `settings.py`: 設定的保存和加載 19 | 20 | ## 使用方法 21 | 22 | ### 直接運行 23 | 24 | ```bash 25 | python panel/panel_main.py 26 | ``` 27 | 28 | ### 透過統一入口運行 29 | 30 | ```bash 31 | python run.py --panel 32 | ``` 33 | 34 | ### 命令行參數 35 | 36 | ```bash 37 | python panel/panel_main.py --api-key YOUR_API_KEY --secret-key YOUR_SECRET_KEY --symbol SOL_USDC 38 | ``` 39 | 40 | ## 設定文件 41 | 42 | 設定自動保存在 `settings/panel_settings.json` 文件中,修改設定後會自動保存。 43 | 44 | ## 鍵盤操作 45 | 46 | - 按 `:` 或 `/` 進入命令模式 47 | - 命令模式下按 `Enter` 執行命令,按 `ESC` 取消 48 | - 按 `q` 退出程序 49 | 50 | ## 可用命令 51 | 52 | - `help`: 顯示幫助信息 53 | - `symbols`: 列出可用交易對 54 | - `start `: 啟動指定交易對的做市策略 55 | - `stop`: 停止當前策略 56 | - `params`: 顯示當前策略參數 57 | - `set spread <值>`: 設置價差百分比 58 | - `set max_orders <值>`: 設置每側最大訂單數 59 | - `set quantity <值>`: 設置訂單數量 60 | - `set interval <值>`: 設置更新間隔(秒) 61 | - `status`: 顯示當前狀態 62 | - `balance`: 查詢餘額 63 | - `orders`: 顯示活躍訂單 64 | - `cancel`: 取消所有訂單 65 | - `clear`: 清除日誌 66 | - `exit`/`quit`: 退出程序 -------------------------------------------------------------------------------- /panel/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 交互式面板包 - 提供圖形化操作界面 3 | """ 4 | 5 | # 版本信息 6 | __version__ = "1.0.0" 7 | 8 | # 導出主要的類和函數 9 | from panel.interactive_panel import InteractivePanel 10 | from panel.key_handler import KeyboardHandler 11 | 12 | # 導出設定相關的函數 13 | from panel.settings import ( 14 | get_setting, 15 | set_setting, 16 | update_settings, 17 | load_settings, 18 | reset_defaults 19 | ) -------------------------------------------------------------------------------- /panel/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /panel/__pycache__/interactive_panel.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/interactive_panel.cpython-312.pyc -------------------------------------------------------------------------------- /panel/__pycache__/key_handler.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/key_handler.cpython-312.pyc -------------------------------------------------------------------------------- /panel/__pycache__/panel_main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/panel_main.cpython-312.pyc -------------------------------------------------------------------------------- /panel/__pycache__/settings.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/settings.cpython-312.pyc -------------------------------------------------------------------------------- /panel/__pycache__/simple_panel.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/panel/__pycache__/simple_panel.cpython-312.pyc -------------------------------------------------------------------------------- /panel/interactive_panel.py: -------------------------------------------------------------------------------- 1 | """ 2 | 交互式做市策略命令面板 3 | """ 4 | import time 5 | import threading 6 | import os 7 | import sys 8 | from datetime import datetime 9 | from typing import Dict, List, Tuple, Any, Optional, Callable 10 | 11 | from rich.console import Console 12 | from rich.table import Table 13 | from rich.panel import Panel 14 | from rich.layout import Layout 15 | from rich.live import Live 16 | from rich.text import Text 17 | from rich.align import Align 18 | from rich.prompt import Prompt, Confirm 19 | from rich import box 20 | from rich.console import Group 21 | 22 | # 導入配置模塊 23 | try: 24 | from config import API_KEY, SECRET_KEY 25 | except ImportError: 26 | API_KEY = os.getenv('API_KEY') 27 | SECRET_KEY = os.getenv('SECRET_KEY') 28 | 29 | # 導入設定模塊 30 | try: 31 | from panel.settings import get_setting, set_setting, update_settings, load_settings 32 | except ImportError: 33 | # 在直接運行面板文件時,可能會遇到導入問題,嘗試直接導入 34 | try: 35 | from settings import get_setting, set_setting, update_settings, load_settings 36 | except ImportError: 37 | # 如果無法導入,創建空的設定函數 38 | def get_setting(key, default=None): return default 39 | def set_setting(key, value): pass 40 | def update_settings(settings_dict): pass 41 | def load_settings(): return {} 42 | 43 | class InteractivePanel: 44 | def __init__(self): 45 | """初始化交互面板""" 46 | # 初始化默認設定 47 | self.settings = load_settings() 48 | 49 | # 策略參數 50 | self.strategy_params = { 51 | 'base_spread_percentage': self.settings.get('base_spread_percentage', 0.1), 52 | 'order_quantity': self.settings.get('order_quantity', None), 53 | 'max_orders': self.settings.get('max_orders', 3), 54 | 'duration': self.settings.get('duration', 24*3600), 55 | 'interval': self.settings.get('interval', 60), 56 | } 57 | 58 | # 策略狀態 59 | self.strategy_running = False 60 | self._initializing_strategy = False 61 | self.current_symbol = None 62 | self.market_maker = None 63 | self.strategy_thread = None 64 | self.last_market_update = datetime.now() 65 | 66 | # 市場數據 67 | self.market_data = { 68 | 'bp_prices': {}, # 基準價格 69 | 'bid_prices': {}, # 買價 70 | 'ask_prices': {}, # 賣價 71 | 'spread_pct': {}, # 價差百分比 72 | 'buy_orders': {}, # 買單數量 73 | 'sell_orders': {}, # 賣單數量 74 | 'positions': {}, # 持倉狀態 (多/空/平) 75 | } 76 | 77 | # 策略數據 78 | self.strategy_data = { 79 | 'base_spread': 0.0, # 基礎價差 80 | 'total_bought': 0.0, # 總購買量 81 | 'total_sold': 0.0, # 總賣出量 82 | 'maker_buy_volume': 0.0, # Maker買入量 83 | 'maker_sell_volume': 0.0,# Maker賣出量 84 | 'taker_buy_volume': 0.0, # Taker買入量 85 | 'taker_sell_volume': 0.0,# Taker賣出量 86 | 'session_profit': 0.0, # 本次利潤 87 | 'total_profit': 0.0, # 總利潤 88 | 'orders_placed': 0, # 訂單數量 89 | 'trades_executed': 0, # 成交數量 90 | } 91 | 92 | # API 密鑰 (從環境變數或設定讀取) 93 | self.api_key = os.environ.get('API_KEY', '') 94 | self.secret_key = os.environ.get('SECRET_KEY', '') 95 | 96 | self.console = Console() 97 | self.layout = self.create_layout() 98 | self.live = None 99 | self.running = False 100 | self.update_thread = None 101 | 102 | # 命令和狀態 103 | self.command_handlers = {} 104 | self.command_mode = False # 切換命令模式 105 | self.current_command = "" 106 | self.command_history = [] 107 | self.max_command_history = 20 108 | 109 | # 系統日誌 110 | self.logs = [] 111 | self.max_logs = 15 # 最多顯示日誌條數 112 | 113 | # 註冊命令處理函數 114 | self.register_commands() 115 | 116 | def add_log(self, message, level="INFO"): 117 | """添加日誌""" 118 | timestamp = datetime.now().strftime("%H:%M:%S") 119 | self.logs.append((timestamp, level, message)) 120 | if len(self.logs) > self.max_logs: 121 | self.logs = self.logs[-self.max_logs:] 122 | 123 | def register_commands(self): 124 | """註冊所有命令和處理函數""" 125 | self.command_handlers = { 126 | 'help': self.cmd_help, 127 | 'clear': self.cmd_clear, 128 | 'exit': self.cmd_exit, 129 | 'quit': self.cmd_exit, 130 | 'symbols': self.cmd_list_symbols, 131 | 'start': self.cmd_start_strategy, 132 | 'stop': self.cmd_stop_strategy, 133 | 'params': self.cmd_show_params, 134 | 'set': self.cmd_set_param, 135 | 'status': self.cmd_show_status, 136 | 'balance': self.cmd_show_balance, 137 | 'orders': self.cmd_show_orders, 138 | 'cancel': self.cmd_cancel_orders, 139 | 'diagnose': self.cmd_diagnose, 140 | } 141 | 142 | def create_layout(self): 143 | """創建UI布局""" 144 | layout = Layout() 145 | 146 | # 分成上中下三部分 147 | layout.split( 148 | Layout(name="header", size=3), 149 | Layout(name="main"), 150 | Layout(name="command", size=3) 151 | ) 152 | 153 | # 主區域分成左右兩部分 154 | layout["main"].split_row( 155 | Layout(name="market_data", ratio=3), 156 | Layout(name="logs", ratio=2) 157 | ) 158 | 159 | return layout 160 | 161 | def generate_header(self): 162 | """生成頭部面板""" 163 | status = "閒置中" 164 | if self.strategy_running: 165 | status = f"運行中 - {self.current_symbol}" if self.current_symbol else "運行中" 166 | 167 | title = f"做市交易機器人 - [{status}]" 168 | return Panel( 169 | Align.center(title, vertical="middle"), 170 | style="bold white on blue" 171 | ) 172 | 173 | def generate_market_table(self): 174 | """生成市場數據表格""" 175 | # 創建表格 176 | last_update_str = self.last_market_update.strftime("%H:%M:%S") 177 | table = Table(title=f"市場數據 (更新: {last_update_str})", show_header=True, header_style="bold white on dark_blue", box=box.SIMPLE) 178 | 179 | 180 | # 添加列 181 | table.add_column("幣種", style="cyan") 182 | table.add_column("BP價格", justify="right", style="green") 183 | table.add_column("買價", justify="right", style="green") 184 | table.add_column("賣價", justify="right", style="green") 185 | table.add_column("價差%", justify="right", style="magenta") 186 | table.add_column("買單數", justify="right", style="blue") 187 | table.add_column("賣單數", justify="right", style="red") 188 | table.add_column("持倉", justify="center", style="yellow") 189 | 190 | # 添加活躍交易對的數據 191 | if self.current_symbol: 192 | symbol = self.current_symbol 193 | bp_price = self.market_data['bp_prices'].get(symbol, "-") 194 | bid_price = self.market_data['bid_prices'].get(symbol, "-") 195 | ask_price = self.market_data['ask_prices'].get(symbol, "-") 196 | spread_pct = self.market_data['spread_pct'].get(symbol, "-") 197 | buy_orders = self.market_data['buy_orders'].get(symbol, 0) 198 | sell_orders = self.market_data['sell_orders'].get(symbol, 0) 199 | position = self.market_data['positions'].get(symbol, "-") 200 | 201 | table.add_row( 202 | symbol, 203 | f"{bp_price}" if bp_price != "-" else "-", 204 | f"{bid_price}" if bid_price != "-" else "-", 205 | f"{ask_price}" if ask_price != "-" else "-", 206 | f"{spread_pct}" if spread_pct != "-" else "-", 207 | str(buy_orders), 208 | str(sell_orders), 209 | position 210 | ) 211 | 212 | # 添加策略數據 213 | strategy_table = Table(title="策略數據", show_header=True, header_style="bold white on dark_blue", box=box.SIMPLE) 214 | strategy_table.add_column("參數", style="yellow") 215 | strategy_table.add_column("數值", style="cyan", justify="right") 216 | 217 | # 添加重要的策略參數 218 | strategy_table.add_row("基礎價差", f"{self.strategy_data['base_spread']:.4f}%") 219 | 220 | # 顯示訂單數量 221 | order_quantity = self.strategy_params.get('order_quantity') 222 | if order_quantity is not None: 223 | strategy_table.add_row("訂單數量", f"{order_quantity}") 224 | else: 225 | strategy_table.add_row("訂單數量", "自動") 226 | 227 | # 顯示利潤表 228 | profit_table = Table(title="利潤統計", show_header=True, header_style="bold white on dark_blue", box=box.SIMPLE) 229 | profit_table.add_column("指標", style="yellow") 230 | profit_table.add_column("數值", style="cyan", justify="right") 231 | 232 | total_profit = self.strategy_data['total_profit'] 233 | session_profit = self.strategy_data['session_profit'] 234 | 235 | profit_style = "green" if total_profit >= 0 else "red" 236 | session_style = "green" if session_profit >= 0 else "red" 237 | 238 | profit_table.add_row("總利潤", f"{total_profit:.6f}") 239 | profit_table.add_row("本次利潤", Text(f"{session_profit:.6f}", style=session_style)) 240 | 241 | # 添加倉位表 242 | position_table = Table(title="倉位統計", show_header=True, header_style="bold white on dark_blue", box=box.SIMPLE) 243 | position_table.add_column("指標", style="yellow") 244 | position_table.add_column("數值", style="cyan", justify="right") 245 | 246 | total_bought = self.strategy_data['total_bought'] 247 | total_sold = self.strategy_data['total_sold'] 248 | imbalance = total_bought - total_sold 249 | imbalance_pct = abs(imbalance) / max(total_bought, total_sold) * 100 if max(total_bought, total_sold) > 0 else 0 250 | 251 | position_table.add_row("買入總量", f"{total_bought:.6f}") 252 | position_table.add_row("賣出總量", f"{total_sold:.6f}") 253 | position_table.add_row("淨倉位", f"{imbalance:.6f}") 254 | position_table.add_row("不平衡%", f"{imbalance_pct:.2f}%") 255 | position_table.add_row("Maker買入", f"{self.strategy_data['maker_buy_volume']:.6f}") 256 | position_table.add_row("Maker賣出", f"{self.strategy_data['maker_sell_volume']:.6f}") 257 | 258 | return Group(table, strategy_table, profit_table, position_table) 259 | 260 | def generate_log_panel(self): 261 | """生成日誌面板""" 262 | log_text = "" 263 | 264 | for timestamp, level, message in self.logs: 265 | if level == "ERROR": 266 | log_text += f"[bold red][{timestamp}] {message}[/bold red]\n" 267 | elif level == "WARNING": 268 | log_text += f"[yellow][{timestamp}] {message}[/yellow]\n" 269 | elif level == "COMMAND": 270 | log_text += f"[bold cyan][{timestamp}] {message}[/bold cyan]\n" 271 | elif level == "SYSTEM": 272 | log_text += f"[bold magenta][{timestamp}] {message}[/bold magenta]\n" 273 | else: 274 | log_text += f"[{timestamp}] {message}\n" 275 | 276 | return Panel( 277 | log_text, 278 | title="系統日誌", 279 | border_style="bright_blue" 280 | ) 281 | 282 | def generate_command_panel(self): 283 | """生成命令面板""" 284 | if self.command_mode: 285 | command_text = f"> {self.current_command}" 286 | else: 287 | command_text = "按 : 或 / 進入命令模式 | 幫助命令: help" 288 | 289 | return Panel( 290 | Text(command_text, style="bold cyan"), 291 | title="命令", 292 | border_style="green" 293 | ) 294 | 295 | def update_display(self): 296 | """更新顯示內容""" 297 | self.layout["header"].update(self.generate_header()) 298 | self.layout["market_data"].update(self.generate_market_table()) 299 | self.layout["logs"].update(self.generate_log_panel()) 300 | self.layout["command"].update(self.generate_command_panel()) 301 | 302 | def update_thread_function(self): 303 | """更新線程函數""" 304 | while self.running: 305 | self.update_display() 306 | time.sleep(0.5) # 每0.5秒更新一次,避免過高的CPU使用率 307 | 308 | def handle_input(self, key): 309 | """處理鍵盤輸入""" 310 | if self.command_mode: 311 | # 命令模式下的按鍵處理 312 | if key == "enter": 313 | self.execute_command(self.current_command) 314 | self.command_mode = False 315 | self.current_command = "" 316 | elif key == "escape": 317 | self.command_mode = False 318 | self.current_command = "" 319 | elif key == "backspace": 320 | self.current_command = self.current_command[:-1] 321 | else: 322 | # 添加普通字符到命令 323 | self.current_command += key 324 | else: 325 | # 非命令模式下的按鍵處理 326 | if key == ":" or key == "/": 327 | self.command_mode = True 328 | self.current_command = "" 329 | elif key == "q": 330 | self.running = False 331 | 332 | def execute_command(self, command): 333 | """執行命令""" 334 | # 添加到命令歷史 335 | if command.strip(): 336 | self.command_history.append(command) 337 | if len(self.command_history) > self.max_command_history: 338 | self.command_history = self.command_history[-self.max_command_history:] 339 | 340 | # 解析命令 341 | parts = command.strip().split() 342 | if not parts: 343 | return 344 | 345 | cmd = parts[0].lower() 346 | args = parts[1:] if len(parts) > 1 else [] 347 | 348 | # 執行對應的命令處理函數 349 | if cmd in self.command_handlers: 350 | self.add_log(f"執行命令: {command}", "COMMAND") 351 | try: 352 | self.command_handlers[cmd](args) 353 | except Exception as e: 354 | self.add_log(f"執行命令出錯: {str(e)}", "ERROR") 355 | else: 356 | self.add_log(f"未知命令: {cmd}", "ERROR") 357 | 358 | def cmd_help(self, args): 359 | """顯示幫助信息""" 360 | self.add_log("可用命令:", "SYSTEM") 361 | self.add_log("help - 顯示幫助", "SYSTEM") 362 | self.add_log("symbols - 列出可用交易對", "SYSTEM") 363 | self.add_log("start - 啟動指定交易對的做市策略", "SYSTEM") 364 | self.add_log("stop - 停止當前做市策略", "SYSTEM") 365 | self.add_log("params - 顯示當前策略參數", "SYSTEM") 366 | self.add_log("set <參數> <值> - 設置策略參數", "SYSTEM") 367 | self.add_log("status - 顯示當前狀態", "SYSTEM") 368 | self.add_log("balance - 查詢餘額", "SYSTEM") 369 | self.add_log("orders - 顯示活躍訂單", "SYSTEM") 370 | self.add_log("cancel - 取消所有訂單", "SYSTEM") 371 | self.add_log("clear - 清除日誌", "SYSTEM") 372 | self.add_log("diagnose - 執行系統診斷檢查", "SYSTEM") 373 | self.add_log("exit/quit - 退出程序", "SYSTEM") 374 | 375 | def cmd_clear(self, args): 376 | """清除日誌""" 377 | self.logs = [] 378 | self.add_log("日誌已清除", "SYSTEM") 379 | 380 | def cmd_exit(self, args): 381 | """退出程序""" 382 | self.running = False 383 | 384 | def cmd_list_symbols(self, args): 385 | """列出可用交易對""" 386 | self.add_log("正在獲取可用交易對...", "SYSTEM") 387 | 388 | try: 389 | # 導入需要的模塊 390 | from api.client import get_markets 391 | 392 | markets_info = get_markets() 393 | if isinstance(markets_info, dict) and "error" in markets_info: 394 | self.add_log(f"獲取市場信息失敗: {markets_info['error']}", "ERROR") 395 | return 396 | 397 | spot_markets = [m for m in markets_info if m.get('marketType') == 'SPOT'] 398 | self.market_data['symbols'] = [m.get('symbol') for m in spot_markets] 399 | 400 | self.add_log(f"找到 {len(spot_markets)} 個現貨市場:", "SYSTEM") 401 | 402 | # 分組顯示,每行最多5個 403 | symbols_per_line = 5 404 | for i in range(0, len(spot_markets), symbols_per_line): 405 | group = spot_markets[i:i+symbols_per_line] 406 | symbols_line = ", ".join([m.get('symbol') for m in group]) 407 | self.add_log(symbols_line, "SYSTEM") 408 | 409 | except Exception as e: 410 | self.add_log(f"獲取交易對時出錯: {str(e)}", "ERROR") 411 | 412 | def cmd_start_strategy(self, args): 413 | """啟動做市策略""" 414 | if not args: 415 | self.add_log("請指定交易對,例如: start SOL_USDC", "ERROR") 416 | return 417 | 418 | symbol = args[0] 419 | 420 | if self.strategy_running: 421 | self.add_log("已有策略運行中,請先停止當前策略", "ERROR") 422 | return 423 | 424 | self.add_log(f"正在啟動 {symbol} 的做市策略...", "SYSTEM") 425 | 426 | try: 427 | # 導入必要的類 428 | from database.db import Database 429 | from strategies.market_maker import MarketMaker 430 | 431 | # 導入或獲取API密鑰 432 | try: 433 | from config import API_KEY as CONFIG_API_KEY, SECRET_KEY as CONFIG_SECRET_KEY, WS_PROXY as CONFIG_WS_PROXY 434 | except ImportError: 435 | CONFIG_API_KEY = os.getenv('API_KEY') 436 | CONFIG_SECRET_KEY = os.getenv('SECRET_KEY') 437 | CONFIG_WS_PROXY = os.getenv('PROXY_WEBSOCKET') 438 | 439 | api_key = CONFIG_API_KEY 440 | secret_key = CONFIG_SECRET_KEY 441 | ws_proxy = CONFIG_WS_PROXY 442 | 443 | if not api_key or not secret_key: 444 | self.add_log("缺少API密鑰,請檢查config.py或環境變量", "ERROR") 445 | return 446 | 447 | # 默認策略參數 448 | params = { 449 | 'base_spread_percentage': 0.1, # 默認價差0.1% 450 | 'order_quantity': None, # 添加訂單數量 451 | 'max_orders': 3, # 每側3個訂單 452 | 'execution_mode': 'standard', # 標準執行模式 453 | 'risk_factor': 0.5, # 默認風險因子 454 | 'duration': 24*3600, # 運行24小時 455 | 'interval': 60 # 每分鐘更新一次 456 | } 457 | 458 | # 合併用戶設置的參數 459 | for key, value in self.strategy_params.items(): 460 | if key in params: 461 | params[key] = value 462 | 463 | # 初始化數據庫 464 | db = Database() 465 | 466 | # 設置當前交易對和標記策略為運行狀態 467 | self.current_symbol = symbol 468 | 469 | # 記錄當前正在初始化 470 | self._initializing_strategy = True 471 | 472 | # 更新策略數據 473 | self.strategy_data['base_spread'] = params['base_spread_percentage'] 474 | 475 | # 初始化做市商 476 | self.market_maker = MarketMaker( 477 | api_key=api_key, 478 | secret_key=secret_key, 479 | symbol=symbol, 480 | db_instance=db, 481 | base_spread_percentage=params['base_spread_percentage'], 482 | order_quantity=params['order_quantity'], # 使用設定的訂單數量 483 | max_orders=params['max_orders'], 484 | ws_proxy=ws_proxy 485 | ) 486 | 487 | # 標記策略為運行狀態 488 | self.strategy_running = True 489 | self._initializing_strategy = False 490 | 491 | # 啟動策略在單獨的線程中 492 | self.strategy_thread = threading.Thread( 493 | target=self._run_strategy_thread, 494 | args=(params['duration'], params['interval']), 495 | daemon=True 496 | ) 497 | self.strategy_thread.start() 498 | 499 | self.add_log(f"{symbol} 做市策略已啟動", "SYSTEM") 500 | 501 | except Exception as e: 502 | self._initializing_strategy = False 503 | self.strategy_running = False # 確保在出錯時重置狀態 504 | self.current_symbol = None 505 | self.add_log(f"啟動策略時出錯: {str(e)}", "ERROR") 506 | import traceback 507 | self.add_log(f"詳細錯誤: {traceback.format_exc()}", "ERROR") 508 | 509 | def _run_strategy_thread(self, duration_seconds, interval_seconds): 510 | """在單獨線程中運行策略""" 511 | if not self.market_maker: 512 | self.add_log("做市商未初始化", "ERROR") 513 | self.strategy_running = False # 確保重置狀態 514 | return 515 | 516 | try: 517 | start_time = time.time() 518 | iteration = 0 519 | 520 | # 記錄開始信息 521 | self.add_log(f"開始運行做市策略: {self.market_maker.symbol}") 522 | 523 | # 確保WebSocket連接 524 | try: 525 | # 首先檢查WebSocket連接 526 | self.add_log("檢查WebSocket連接...") 527 | connection_status = self.market_maker.check_ws_connection() 528 | if not connection_status: 529 | self.add_log("WebSocket未連接,嘗試建立連接...") 530 | # 在MarketMaker中應該有初始化WebSocket的方法 531 | if hasattr(self.market_maker, 'initialize_websocket'): 532 | self.market_maker.initialize_websocket() 533 | elif hasattr(self.market_maker, 'reconnect_websocket'): 534 | self.market_maker.reconnect_websocket() 535 | 536 | # 再次檢查連接狀態 537 | connection_status = self.market_maker.check_ws_connection() 538 | if connection_status: 539 | self.add_log("WebSocket連接成功") 540 | 541 | # 等待WebSocket就緒 542 | self.add_log("等待WebSocket就緒...") 543 | time.sleep(2) 544 | 545 | # 初始化訂單簿 546 | if hasattr(self.market_maker, 'ws') and self.market_maker.ws: 547 | if not self.market_maker.ws.orderbook.get("bids") and not self.market_maker.ws.orderbook.get("asks"): 548 | self.add_log("初始化訂單簿...") 549 | if hasattr(self.market_maker.ws, 'initialize_orderbook'): 550 | self.market_maker.ws.initialize_orderbook() 551 | # 等待訂單簿填充 552 | time.sleep(1) 553 | 554 | # 確保所有數據流訂閱 555 | self.add_log("確保數據流訂閱...") 556 | if hasattr(self.market_maker, '_ensure_data_streams'): 557 | self.market_maker._ensure_data_streams() 558 | 559 | # 增加小延遲確保訂閱成功 560 | time.sleep(2) 561 | self.add_log("數據流訂閱完成,進入主循環...") 562 | else: 563 | self.add_log("WebSocket連接失敗,請檢查網絡或API配置", "ERROR") 564 | self.strategy_running = False 565 | return 566 | except Exception as ws_error: 567 | self.add_log(f"WebSocket設置出錯: {str(ws_error)}", "ERROR") 568 | import traceback 569 | self.add_log(f"WebSocket錯誤詳情: {traceback.format_exc()}", "ERROR") 570 | self.strategy_running = False 571 | return 572 | 573 | # 主循環前檢查策略運行狀態 574 | if not self.strategy_running: 575 | self.add_log("策略在初始化後被停止", "WARNING") 576 | return 577 | 578 | # 檢查訂單簿是否已填充 579 | if hasattr(self.market_maker, 'ws') and self.market_maker.ws and hasattr(self.market_maker.ws, 'orderbook'): 580 | if not self.market_maker.ws.orderbook.get("bids") or not self.market_maker.ws.orderbook.get("asks"): 581 | self.add_log("警告: 訂單簿可能未完全初始化", "WARNING") 582 | 583 | # 主循環 584 | self.add_log("開始執行策略主循環...") 585 | while time.time() - start_time < duration_seconds and self.strategy_running: 586 | iteration += 1 587 | 588 | self.add_log(f"第 {iteration} 次迭代") 589 | 590 | try: 591 | # 檢查連接 592 | connection_status = self.market_maker.check_ws_connection() 593 | if not connection_status: 594 | self.add_log("WebSocket連接已斷開,嘗試重新連接...", "WARNING") 595 | reconnected = self.market_maker.reconnect_websocket() 596 | if not reconnected: 597 | self.add_log("重新連接失敗,停止策略", "ERROR") 598 | break 599 | # 給連接一些時間重新建立 600 | time.sleep(2) 601 | continue # 跳過這次迭代 602 | 603 | # 更新面板數據 604 | self._update_strategy_data() 605 | 606 | # 檢查訂單成交情況 607 | self.add_log("檢查訂單成交情況...") 608 | self.market_maker.check_order_fills() 609 | 610 | # 檢查是否需要重平衡倉位 611 | needs_rebalance = self.market_maker.need_rebalance() 612 | if needs_rebalance: 613 | self.add_log("執行倉位重平衡") 614 | self.market_maker.rebalance_position() 615 | 616 | # 下限價單 617 | self.add_log("下限價單...") 618 | self.market_maker.place_limit_orders() 619 | 620 | # 估算利潤 621 | self.market_maker.estimate_profit() 622 | 623 | except Exception as loop_error: 624 | self.add_log(f"策略迭代中出錯: {str(loop_error)}", "ERROR") 625 | import traceback 626 | self.add_log(f"迭代錯誤詳情: {traceback.format_exc()}", "ERROR") 627 | # 不因為單次循環錯誤停止整個策略,繼續下一次循環 628 | time.sleep(5) # 出錯時等待更長時間 629 | continue 630 | 631 | # 等待下一次迭代 632 | time.sleep(interval_seconds) 633 | 634 | # 結束時記錄信息 635 | if not self.strategy_running: 636 | self.add_log("策略已手動停止") 637 | else: 638 | self.add_log("策略運行完成") 639 | 640 | # 清理資源 641 | self._cleanup_strategy() 642 | 643 | except Exception as e: 644 | self.add_log(f"策略運行出錯: {str(e)}", "ERROR") 645 | import traceback 646 | self.add_log(f"錯誤詳情: {traceback.format_exc()}", "ERROR") 647 | 648 | # 確保清理資源 649 | self._cleanup_strategy() 650 | 651 | def _update_strategy_data(self): 652 | """從市場做市商更新數據到面板""" 653 | if not self.market_maker: 654 | return 655 | 656 | try: 657 | # 更新最後一次市場數據更新時間 658 | self.last_market_update = datetime.now() 659 | 660 | # 檢查 WebSocket 連接 661 | if not hasattr(self.market_maker, 'ws') or not self.market_maker.ws: 662 | self.add_log("WebSocket 連接不可用", "WARNING") 663 | return 664 | 665 | # 更新市場數據 666 | symbol = self.current_symbol 667 | if symbol: 668 | try: 669 | # 更新價格數據 670 | bid_price = getattr(self.market_maker.ws, 'bid_price', None) 671 | ask_price = getattr(self.market_maker.ws, 'ask_price', None) 672 | 673 | if bid_price and ask_price: 674 | bp_price = (bid_price + ask_price) / 2 675 | self.market_data['bp_prices'][symbol] = bp_price 676 | self.market_data['bid_prices'][symbol] = bid_price 677 | self.market_data['ask_prices'][symbol] = ask_price 678 | 679 | # 計算價差 680 | spread_pct = (ask_price - bid_price) / bp_price * 100 681 | self.market_data['spread_pct'][symbol] = f"{spread_pct:.6f}%" 682 | except Exception as price_err: 683 | self.add_log(f"更新價格數據時出錯: {str(price_err)}", "WARNING") 684 | 685 | try: 686 | # 更新訂單數量 687 | buy_orders = getattr(self.market_maker, 'active_buy_orders', []) 688 | sell_orders = getattr(self.market_maker, 'active_sell_orders', []) 689 | self.market_data['buy_orders'][symbol] = len(buy_orders) 690 | self.market_data['sell_orders'][symbol] = len(sell_orders) 691 | except Exception as order_err: 692 | self.add_log(f"更新訂單數量時出錯: {str(order_err)}", "WARNING") 693 | 694 | try: 695 | # 更新持倉狀態 696 | total_bought = getattr(self.market_maker, 'total_bought', 0) 697 | total_sold = getattr(self.market_maker, 'total_sold', 0) 698 | 699 | if total_bought > total_sold: 700 | self.market_data['positions'][symbol] = "多" 701 | elif total_bought < total_sold: 702 | self.market_data['positions'][symbol] = "空" 703 | else: 704 | self.market_data['positions'][symbol] = "平" 705 | except Exception as pos_err: 706 | self.add_log(f"更新持倉狀態時出錯: {str(pos_err)}", "WARNING") 707 | 708 | # 更新交易量數據 709 | try: 710 | self.strategy_data['total_bought'] = getattr(self.market_maker, 'total_bought', 0) 711 | self.strategy_data['total_sold'] = getattr(self.market_maker, 'total_sold', 0) 712 | self.strategy_data['maker_buy_volume'] = getattr(self.market_maker, 'maker_buy_volume', 0) 713 | self.strategy_data['maker_sell_volume'] = getattr(self.market_maker, 'maker_sell_volume', 0) 714 | self.strategy_data['taker_buy_volume'] = getattr(self.market_maker, 'taker_buy_volume', 0) 715 | self.strategy_data['taker_sell_volume'] = getattr(self.market_maker, 'taker_sell_volume', 0) 716 | self.strategy_data['orders_placed'] = getattr(self.market_maker, 'orders_placed', 0) 717 | self.strategy_data['trades_executed'] = getattr(self.market_maker, 'trades_executed', 0) 718 | 719 | # 利潤統計 720 | self.strategy_data['session_profit'] = getattr(self.market_maker, 'session_profit', 0.0) 721 | self.strategy_data['total_profit'] = getattr(self.market_maker, 'total_profit', 0.0) 722 | except Exception as vol_err: 723 | self.add_log(f"更新交易量數據時出錯: {str(vol_err)}", "WARNING") 724 | 725 | except Exception as e: 726 | self.add_log(f"更新面板數據時出錯: {str(e)}", "ERROR") 727 | 728 | def _cleanup_strategy(self): 729 | """清理策略資源""" 730 | if not self.market_maker: 731 | return 732 | 733 | # 標記清理開始 734 | was_running = self.strategy_running 735 | # 標記策略為停止狀態,以防止任何進一步的操作 736 | self.strategy_running = False 737 | 738 | try: 739 | # 記錄清理消息 740 | if was_running: 741 | self.add_log("正在清理策略資源...", "SYSTEM") 742 | 743 | # 取消所有活躍訂單 744 | self.add_log("取消所有未成交訂單...") 745 | try: 746 | if hasattr(self.market_maker, 'cancel_existing_orders'): 747 | self.market_maker.cancel_existing_orders() 748 | self.add_log("所有訂單已取消") 749 | else: 750 | self.add_log("無法取消訂單: 方法不可用", "WARNING") 751 | except Exception as cancel_err: 752 | self.add_log(f"取消訂單時出錯: {str(cancel_err)}", "ERROR") 753 | 754 | # 關閉WebSocket連接 755 | try: 756 | if hasattr(self.market_maker, 'ws') and self.market_maker.ws: 757 | self.add_log("關閉WebSocket連接...") 758 | self.market_maker.ws.close() 759 | self.add_log("WebSocket連接已關閉") 760 | except Exception as ws_err: 761 | self.add_log(f"關閉WebSocket時出錯: {str(ws_err)}", "ERROR") 762 | 763 | # 關閉數據庫連接 764 | try: 765 | if hasattr(self.market_maker, 'db') and self.market_maker.db: 766 | self.add_log("關閉數據庫連接...") 767 | self.market_maker.db.close() 768 | self.add_log("數據庫連接已關閉") 769 | except Exception as db_err: 770 | self.add_log(f"關閉數據庫時出錯: {str(db_err)}", "ERROR") 771 | 772 | # 確認清理完成 773 | if was_running: 774 | self.add_log("策略資源清理完成", "SYSTEM") 775 | 776 | except Exception as e: 777 | self.add_log(f"清理資源時遇到未知錯誤: {str(e)}", "ERROR") 778 | import traceback 779 | self.add_log(f"錯誤詳情: {traceback.format_exc()}", "ERROR") 780 | finally: 781 | # 清空策略實例 782 | self.current_symbol = None 783 | self.market_maker = None 784 | 785 | def cmd_stop_strategy(self, args): 786 | """停止當前運行的策略""" 787 | if not self.strategy_running and not self._initializing_strategy: 788 | self.add_log("沒有正在運行的策略", "ERROR") 789 | return 790 | 791 | if self._initializing_strategy: 792 | self.add_log("策略正在初始化中,請稍後再試", "WARNING") 793 | return 794 | 795 | self.add_log("正在停止策略...") 796 | self.strategy_running = False 797 | 798 | # 等待策略線程結束 799 | if hasattr(self, 'strategy_thread') and self.strategy_thread and self.strategy_thread.is_alive(): 800 | try: 801 | self.strategy_thread.join(timeout=3) 802 | if self.strategy_thread.is_alive(): 803 | self.add_log("策略線程未能在3秒內結束,可能需要手動重啟程序", "WARNING") 804 | except Exception as join_err: 805 | self.add_log(f"等待策略線程時出錯: {str(join_err)}", "ERROR") 806 | 807 | self.add_log("策略已停止") 808 | 809 | def cmd_show_params(self, args): 810 | """顯示當前策略參數""" 811 | self.add_log("當前策略參數:", "SYSTEM") 812 | 813 | if not self.strategy_params: 814 | self.add_log("尚未設置任何參數,使用默認值", "SYSTEM") 815 | self.add_log("可用參數:", "SYSTEM") 816 | self.add_log("base_spread - 基礎價差百分比", "SYSTEM") 817 | self.add_log("order_quantity - 訂單數量 (例如: 0.5 SOL)", "SYSTEM") 818 | self.add_log("max_orders - 每側最大訂單數", "SYSTEM") 819 | self.add_log("duration - 運行時間(秒)", "SYSTEM") 820 | self.add_log("interval - 更新間隔(秒)", "SYSTEM") 821 | return 822 | 823 | for param, value in self.strategy_params.items(): 824 | # 訂單數量可能為空,特殊處理 825 | if param == 'order_quantity' and value is None: 826 | self.add_log(f"{param} = 自動 (根據餘額決定)", "SYSTEM") 827 | else: 828 | self.add_log(f"{param} = {value}", "SYSTEM") 829 | 830 | # 添加使用說明 831 | self.add_log("\n設置參數示例:", "SYSTEM") 832 | self.add_log("set base_spread 0.2 - 設置價差為0.2%", "SYSTEM") 833 | self.add_log("set order_quantity 0.5 - 設置訂單數量為0.5", "SYSTEM") 834 | self.add_log("set max_orders 5 - 設置每側最大訂單數為5", "SYSTEM") 835 | 836 | def cmd_set_param(self, args): 837 | """設置策略參數""" 838 | if len(args) < 2: 839 | self.add_log("用法: set <參數名> <參數值>", "ERROR") 840 | return 841 | 842 | param = args[0] 843 | value = args[1] 844 | 845 | valid_params = { 846 | 'base_spread_percentage': float, 847 | 'order_quantity': float, 848 | 'max_orders': int, 849 | 'duration': int, 850 | 'interval': int 851 | } 852 | 853 | if param not in valid_params: 854 | self.add_log(f"無效的參數名: {param}", "ERROR") 855 | self.add_log("有效參數: " + ", ".join(valid_params.keys()), "SYSTEM") 856 | return 857 | 858 | # 轉換參數值 859 | try: 860 | # 處理特殊值:auto, none, null 861 | if value.lower() in ('auto', 'none', 'null', 'auto'): 862 | typed_value = None 863 | self.add_log(f"訂單數量將設為自動 (由程序根據餘額決定)", "SYSTEM") 864 | else: 865 | # 數值處理 866 | typed_value = float(value) 867 | if typed_value <= 0: 868 | raise ValueError("訂單數量必須大於0") 869 | 870 | # 存儲參數 871 | self.strategy_params[param] = typed_value 872 | 873 | # 保存到設定文件 874 | try: 875 | set_setting(param, typed_value) 876 | self.add_log(f"參數已設置並保存: {param} = {typed_value}", "SYSTEM") 877 | except Exception as e: 878 | self.add_log(f"參數已設置但保存失敗: {str(e)}", "WARNING") 879 | 880 | except ValueError as e: 881 | self.add_log(f"參數值轉換錯誤: {str(e)}", "ERROR") 882 | self.add_log(f"參數 {param} 需要 {valid_params[param].__name__} 類型", "SYSTEM") 883 | 884 | def cmd_show_status(self, args): 885 | """顯示當前狀態""" 886 | if not self.strategy_running: 887 | self.add_log("沒有正在運行的策略", "SYSTEM") 888 | return 889 | 890 | self.add_log(f"正在運行 {self.current_symbol} 的做市策略", "SYSTEM") 891 | 892 | # 顯示策略參數 893 | self.add_log("策略參數:", "SYSTEM") 894 | self.add_log(f"基礎價差: {self.strategy_data['base_spread']:.4f}%", "SYSTEM") 895 | 896 | # 顯示訂單數量 897 | order_quantity = self.strategy_params.get('order_quantity') 898 | if order_quantity is not None: 899 | self.add_log(f"訂單數量: {order_quantity}", "SYSTEM") 900 | else: 901 | self.add_log("訂單數量: 自動", "SYSTEM") 902 | 903 | self.add_log(f"最大訂單數: {self.strategy_params.get('max_orders', 3)}", "SYSTEM") 904 | 905 | # 顯示重要狀態指標 906 | self.add_log("\n倉位統計:", "SYSTEM") 907 | total_bought = self.strategy_data['total_bought'] 908 | total_sold = self.strategy_data['total_sold'] 909 | imbalance = total_bought - total_sold 910 | imbalance_pct = abs(imbalance) / max(total_bought, total_sold) * 100 if max(total_bought, total_sold) > 0 else 0 911 | 912 | self.add_log(f"總買入: {total_bought} - 總賣出: {total_sold}", "SYSTEM") 913 | self.add_log(f"倉位不平衡度: {imbalance_pct:.2f}%", "SYSTEM") 914 | 915 | # 顯示利潤信息 916 | self.add_log("\n利潤統計:", "SYSTEM") 917 | total_profit = self.strategy_data['total_profit'] 918 | 919 | self.add_log(f"總利潤: {total_profit:.6f}", "SYSTEM") 920 | 921 | def cmd_show_balance(self, args): 922 | """顯示當前餘額""" 923 | self.add_log("正在查詢餘額...", "SYSTEM") 924 | 925 | try: 926 | # 導入API客戶端 927 | from api.client import get_balance 928 | 929 | balances = get_balance(API_KEY, SECRET_KEY) 930 | if isinstance(balances, dict) and "error" in balances and balances["error"]: 931 | self.add_log(f"獲取餘額失敗: {balances['error']}", "ERROR") 932 | return 933 | 934 | self.add_log("當前餘額:", "SYSTEM") 935 | if isinstance(balances, dict): 936 | for coin, details in balances.items(): 937 | available = float(details.get('available', 0)) 938 | locked = float(details.get('locked', 0)) 939 | if available > 0 or locked > 0: 940 | self.add_log(f"{coin}: 可用 {available}, 凍結 {locked}", "SYSTEM") 941 | else: 942 | self.add_log(f"獲取餘額失敗: 無法識別返回格式", "ERROR") 943 | 944 | except Exception as e: 945 | self.add_log(f"查詢餘額時出錯: {str(e)}", "ERROR") 946 | 947 | def cmd_show_orders(self, args): 948 | """顯示活躍訂單""" 949 | if not self.strategy_running: 950 | self.add_log("沒有正在運行的策略", "ERROR") 951 | return 952 | 953 | # 顯示活躍買單 954 | self.add_log(f"活躍買單 ({len(self.market_maker.active_buy_orders)}):", "SYSTEM") 955 | for i, order in enumerate(self.market_maker.active_buy_orders[:5]): # 只顯示前5個 956 | price = float(order.get('price', 0)) 957 | quantity = float(order.get('quantity', 0)) 958 | self.add_log(f"{i+1}. 買入 {quantity} @ {price}", "SYSTEM") 959 | 960 | if len(self.market_maker.active_buy_orders) > 5: 961 | self.add_log(f"... 還有 {len(self.market_maker.active_buy_orders) - 5} 個買單", "SYSTEM") 962 | 963 | # 顯示活躍賣單 964 | self.add_log(f"活躍賣單 ({len(self.market_maker.active_sell_orders)}):", "SYSTEM") 965 | for i, order in enumerate(self.market_maker.active_sell_orders[:5]): # 只顯示前5個 966 | price = float(order.get('price', 0)) 967 | quantity = float(order.get('quantity', 0)) 968 | self.add_log(f"{i+1}. 賣出 {quantity} @ {price}", "SYSTEM") 969 | 970 | if len(self.market_maker.active_sell_orders) > 5: 971 | self.add_log(f"... 還有 {len(self.market_maker.active_sell_orders) - 5} 個賣單", "SYSTEM") 972 | 973 | def cmd_cancel_orders(self, args): 974 | """取消所有訂單""" 975 | if not self.strategy_running: 976 | self.add_log("沒有正在運行的策略", "ERROR") 977 | return 978 | 979 | self.add_log("正在取消所有訂單...", "SYSTEM") 980 | 981 | try: 982 | self.market_maker.cancel_existing_orders() 983 | self.add_log("所有訂單已取消", "SYSTEM") 984 | except Exception as e: 985 | self.add_log(f"取消訂單時出錯: {str(e)}", "ERROR") 986 | 987 | def cmd_diagnose(self, args): 988 | """執行系統診斷以檢查問題""" 989 | self.add_log("開始系統診斷...", "SYSTEM") 990 | 991 | # 檢查API密鑰 992 | try: 993 | from config import API_KEY, SECRET_KEY 994 | if not API_KEY or not SECRET_KEY: 995 | self.add_log("診斷問題: API密鑰未設置在config.py中", "ERROR") 996 | else: 997 | self.add_log("API密鑰已在config.py中設置", "SYSTEM") 998 | except ImportError: 999 | api_key = os.getenv('API_KEY') 1000 | secret_key = os.getenv('SECRET_KEY') 1001 | if not api_key or not secret_key: 1002 | self.add_log("診斷問題: API密鑰未在環境變量中設置", "ERROR") 1003 | else: 1004 | self.add_log("API密鑰已在環境變量中設置", "SYSTEM") 1005 | 1006 | # 檢查網絡連接 1007 | self.add_log("檢查網絡連接...", "SYSTEM") 1008 | try: 1009 | import socket 1010 | try: 1011 | # 嘗試連接到Backpack Exchange API域名 1012 | socket.create_connection(("api.backpack.exchange", 443), timeout=10) 1013 | self.add_log("網絡連接正常,可訪問Backpack Exchange API", "SYSTEM") 1014 | except (socket.timeout, socket.error): 1015 | self.add_log("診斷問題: 無法連接到Backpack Exchange API,請檢查網絡連接", "ERROR") 1016 | except ImportError: 1017 | self.add_log("無法檢查網絡連接:缺少socket模塊", "WARNING") 1018 | 1019 | # 檢查必要模塊 1020 | self.add_log("檢查必要模塊...", "SYSTEM") 1021 | modules_to_check = [ 1022 | ('WebSocket庫', 'websocket'), 1023 | ('API客戶端', 'api.client'), 1024 | ('數據庫模塊', 'database.db'), 1025 | ('策略模塊', 'strategies.market_maker') 1026 | ] 1027 | 1028 | for module_name, module_path in modules_to_check: 1029 | try: 1030 | __import__(module_path) 1031 | self.add_log(f"{module_name}可用", "SYSTEM") 1032 | except ImportError as e: 1033 | self.add_log(f"診斷問題: {module_name}導入失敗: {str(e)}", "ERROR") 1034 | 1035 | # 檢查設定目錄 1036 | settings_dir = 'settings' 1037 | if not os.path.exists(settings_dir): 1038 | self.add_log(f"診斷問題: 設定目錄不存在: {settings_dir}", "ERROR") 1039 | try: 1040 | os.makedirs(settings_dir, exist_ok=True) 1041 | self.add_log("已創建設定目錄", "SYSTEM") 1042 | except Exception as e: 1043 | self.add_log(f"無法創建設定目錄: {str(e)}", "ERROR") 1044 | else: 1045 | self.add_log("設定目錄已存在", "SYSTEM") 1046 | 1047 | # 如果當前正在運行策略,檢查策略狀態 1048 | if self.strategy_running and self.market_maker: 1049 | self.add_log("檢查策略狀態...", "SYSTEM") 1050 | 1051 | # 檢查WebSocket連接 1052 | if not hasattr(self.market_maker, 'ws') or not self.market_maker.ws: 1053 | self.add_log("診斷問題: WebSocket連接不可用", "ERROR") 1054 | elif not getattr(self.market_maker.ws, '_thread', None) or not self.market_maker.ws._thread.is_alive(): 1055 | self.add_log("診斷問題: WebSocket線程未運行", "ERROR") 1056 | else: 1057 | self.add_log("WebSocket連接正常", "SYSTEM") 1058 | 1059 | # 檢查訂單簿數據 1060 | if hasattr(self.market_maker, 'ws') and self.market_maker.ws: 1061 | if not self.market_maker.ws.orderbook or (not self.market_maker.ws.orderbook.get('bids') and not self.market_maker.ws.orderbook.get('asks')): 1062 | self.add_log("診斷問題: 訂單簿數據為空", "ERROR") 1063 | else: 1064 | self.add_log("訂單簿數據正常", "SYSTEM") 1065 | 1066 | self.add_log("診斷完成", "SYSTEM") 1067 | self.add_log("如遇問題,請檢查API密鑰是否正確,網絡連接是否正常", "SYSTEM") 1068 | self.add_log("或者嘗試重新啟動程序", "SYSTEM") 1069 | 1070 | def start(self): 1071 | """啟動交互式面板""" 1072 | # 設置初始日誌 1073 | self.add_log("做市交易面板已啟動", "SYSTEM") 1074 | self.add_log("按 : 或 / 進入命令模式", "SYSTEM") 1075 | self.add_log("輸入 help 查看可用命令", "SYSTEM") 1076 | 1077 | self.running = True 1078 | 1079 | # 創建並啟動更新線程 1080 | self.update_thread = threading.Thread(target=self.update_thread_function, daemon=True) 1081 | self.update_thread.start() 1082 | 1083 | try: 1084 | # 創建實時顯示並處理按鍵 1085 | with Live(self.layout, refresh_per_second=2, screen=True) as live: 1086 | while self.running: 1087 | # 更新顯示 1088 | self.update_display() 1089 | 1090 | # 等待按鍵輸入 1091 | # 注意:這裡需要在真實環境中實現鍵盤輸入捕獲 1092 | # 但在這個示例中,我們只是簡單地休眠 1093 | time.sleep(0.1) 1094 | 1095 | except KeyboardInterrupt: 1096 | self.running = False 1097 | finally: 1098 | self.cleanup() 1099 | 1100 | def cleanup(self): 1101 | """清理資源""" 1102 | # 停止策略(如果正在運行) 1103 | if self.strategy_running and hasattr(self, '_cleanup_strategy'): 1104 | self._cleanup_strategy() 1105 | 1106 | # 主函數 1107 | def main(): 1108 | panel = InteractivePanel() 1109 | panel.start() 1110 | 1111 | if __name__ == "__main__": 1112 | main() -------------------------------------------------------------------------------- /panel/key_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | 鍵盤處理模塊 - 提供跨平台鍵盤輸入處理 3 | """ 4 | import os 5 | import sys 6 | import threading 7 | import time 8 | from typing import Callable, Any 9 | 10 | # 嘗試導入適合的鍵盤處理庫 11 | try: 12 | # Windows平台 13 | import msvcrt 14 | 15 | def get_key(): 16 | """ 17 | Windows下獲取按鍵 18 | """ 19 | if msvcrt.kbhit(): 20 | key = msvcrt.getch() 21 | # 將bytes轉換為字符串 22 | key_decoded = key.decode('utf-8', errors='ignore') 23 | 24 | # 處理特殊按鍵 25 | if key == b'\xe0': # 擴展按鍵 26 | key = msvcrt.getch() 27 | if key == b'H': # 上箭頭 28 | return "up" 29 | elif key == b'P': # 下箭頭 30 | return "down" 31 | elif key == b'K': # 左箭頭 32 | return "left" 33 | elif key == b'M': # 右箭頭 34 | return "right" 35 | else: 36 | return None 37 | elif key == b'\r': # 回車鍵 38 | return "enter" 39 | elif key == b'\x08': # 退格鍵 40 | return "backspace" 41 | elif key == b'\x1b': # ESC鍵 42 | return "escape" 43 | elif key == b'\t': # Tab鍵 44 | return "tab" 45 | else: 46 | return key_decoded 47 | return None 48 | 49 | WINDOWS = True 50 | 51 | except ImportError: 52 | # Unix平台 53 | try: 54 | import termios 55 | import tty 56 | import select 57 | 58 | def get_key(): 59 | """ 60 | Unix下獲取按鍵 61 | """ 62 | # 設置為非阻塞模式 63 | fd = sys.stdin.fileno() 64 | old_settings = termios.tcgetattr(fd) 65 | try: 66 | tty.setraw(fd) 67 | # 檢查是否有輸入 68 | if select.select([sys.stdin], [], [], 0)[0]: 69 | key = sys.stdin.read(1) 70 | 71 | # 處理特殊按鍵 72 | if key == '\x1b': # ESC鍵可能是特殊按鍵的開始 73 | next_key = sys.stdin.read(1) if select.select([sys.stdin], [], [], 0.1)[0] else None 74 | if next_key == '[': # 箭頭按鍵的序列 75 | key = sys.stdin.read(1) if select.select([sys.stdin], [], [], 0.1)[0] else None 76 | if key == 'A': 77 | return "up" 78 | elif key == 'B': 79 | return "down" 80 | elif key == 'C': 81 | return "right" 82 | elif key == 'D': 83 | return "left" 84 | return "escape" 85 | elif key == '\r': # 回車鍵 86 | return "enter" 87 | elif key == '\x7f': # 退格鍵 88 | return "backspace" 89 | elif key == '\t': # Tab鍵 90 | return "tab" 91 | else: 92 | return key 93 | return None 94 | finally: 95 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 96 | 97 | WINDOWS = False 98 | 99 | except (ImportError, AttributeError): 100 | # 無法使用特定平台的庫,創建簡單替代方案 101 | def get_key(): 102 | """ 103 | 簡單的輸入捕獲(非實時) 104 | """ 105 | if sys.stdin.isatty(): 106 | if select.select([sys.stdin], [], [], 0)[0]: 107 | key = sys.stdin.read(1) 108 | return key 109 | return None 110 | 111 | WINDOWS = False 112 | 113 | class KeyboardHandler: 114 | """ 115 | 跨平台的鍵盤處理類 116 | """ 117 | def __init__(self, callback: Callable[[str], Any]): 118 | """ 119 | 初始化鍵盤處理器 120 | 121 | Args: 122 | callback: 按鍵處理回調函數 123 | """ 124 | self.callback = callback 125 | self.running = False 126 | self.thread = None 127 | 128 | def start(self): 129 | """ 130 | 啟動鍵盤監聽 131 | """ 132 | self.running = True 133 | self.thread = threading.Thread(target=self._listen_keyboard, daemon=True) 134 | self.thread.start() 135 | 136 | def stop(self): 137 | """ 138 | 停止鍵盤監聽 139 | """ 140 | self.running = False 141 | if self.thread and self.thread.is_alive(): 142 | self.thread.join(timeout=1) 143 | 144 | def _listen_keyboard(self): 145 | """ 146 | 監聽鍵盤輸入 147 | """ 148 | while self.running: 149 | key = get_key() 150 | if key: 151 | self.callback(key) 152 | time.sleep(0.01) # 降低CPU使用率 153 | 154 | # 直接測試 155 | if __name__ == "__main__": 156 | def key_callback(key): 157 | print(f"按下鍵: {key}") 158 | if key == 'q': 159 | print("退出...") 160 | return False 161 | return True 162 | 163 | print("按鍵測試 (按 'q' 退出)...") 164 | 165 | handler = KeyboardHandler(key_callback) 166 | handler.start() 167 | 168 | try: 169 | while True: 170 | time.sleep(0.1) 171 | except KeyboardInterrupt: 172 | pass 173 | finally: 174 | handler.stop() -------------------------------------------------------------------------------- /panel/panel_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 交互式命令面板主程序 4 | """ 5 | import sys 6 | import os 7 | import argparse 8 | 9 | # 檢查所需庫 10 | try: 11 | import rich 12 | except ImportError: 13 | print("錯誤: 未安裝rich庫。請執行 pip install rich 安裝。") 14 | sys.exit(1) 15 | 16 | # 導入面板模塊 17 | from panel.interactive_panel import InteractivePanel 18 | from panel.key_handler import KeyboardHandler 19 | from panel.settings import load_settings, update_settings 20 | 21 | def parse_arguments(): 22 | """解析命令行參數""" 23 | parser = argparse.ArgumentParser(description='Backpack Exchange 做市交易面板') 24 | 25 | # 基本參數 26 | parser.add_argument('--api-key', type=str, help='API Key (可選,默認使用環境變數或配置文件)') 27 | parser.add_argument('--secret-key', type=str, help='Secret Key (可選,默認使用環境變數或配置文件)') 28 | parser.add_argument('--symbol', type=str, help='默認交易對 (例如: SOL_USDC)') 29 | parser.add_argument('--settings-dir', type=str, default='settings', help='設定目錄路徑') 30 | 31 | return parser.parse_args() 32 | 33 | def run_panel(api_key=None, secret_key=None, default_symbol=None): 34 | """ 35 | 啟動交互式面板 36 | 37 | Args: 38 | api_key: API密鑰 39 | secret_key: API密鑰 40 | default_symbol: 默認交易對 41 | """ 42 | # 檢查API密鑰 43 | try: 44 | from config import API_KEY as CONFIG_API_KEY, SECRET_KEY as CONFIG_SECRET_KEY 45 | except ImportError: 46 | CONFIG_API_KEY = os.getenv('API_KEY') 47 | CONFIG_SECRET_KEY = os.getenv('SECRET_KEY') 48 | 49 | # 優先使用傳入的參數,其次使用配置文件或環境變量 50 | api_key = api_key or CONFIG_API_KEY 51 | secret_key = secret_key or CONFIG_SECRET_KEY 52 | 53 | if not api_key or not secret_key: 54 | print("錯誤: 未設置API密鑰。請在config.py中設置、使用環境變量或通過命令行參數提供。") 55 | sys.exit(1) 56 | 57 | # 加載設定並更新默認交易對 58 | settings = load_settings() 59 | if default_symbol: 60 | update_settings({'default_symbol': default_symbol}) 61 | 62 | # 建立交互式面板 63 | panel = InteractivePanel() 64 | 65 | # 設置鍵盤處理器 66 | handler = KeyboardHandler(panel.handle_input) 67 | handler.start() 68 | 69 | try: 70 | # 啟動面板 71 | panel.start() 72 | except KeyboardInterrupt: 73 | print("\n程序已被中斷") 74 | except Exception as e: 75 | print(f"運行錯誤: {str(e)}") 76 | import traceback 77 | traceback.print_exc() 78 | finally: 79 | # 釋放資源 80 | handler.stop() 81 | if hasattr(panel, 'cleanup'): 82 | panel.cleanup() 83 | 84 | def main(): 85 | """主函數""" 86 | args = parse_arguments() 87 | run_panel( 88 | api_key=args.api_key, 89 | secret_key=args.secret_key, 90 | default_symbol=args.symbol 91 | ) 92 | 93 | if __name__ == "__main__": 94 | main() -------------------------------------------------------------------------------- /panel/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | 面板設定模塊 - 處理設定的保存和加載 3 | """ 4 | import os 5 | import json 6 | import logging 7 | from typing import Dict, Any, Optional 8 | 9 | # 設定文件的默認路徑 10 | DEFAULT_SETTINGS_DIR = 'settings' 11 | DEFAULT_SETTINGS_FILE = 'panel_settings.json' 12 | 13 | # 默認設定 14 | DEFAULT_SETTINGS = { 15 | 'base_spread_percentage': 0.1, # 默認價差0.1% 16 | 'order_quantity': None, # 訂單數量,默認為自動 17 | 'max_orders': 3, # 每側3個訂單 18 | 'duration': 24*3600, # 運行24小時 19 | 'interval': 60, # 每分鐘更新一次 20 | 'default_symbol': None # 默認交易對 21 | } 22 | 23 | class SettingsManager: 24 | """設定管理器""" 25 | 26 | def __init__(self, settings_dir: str = DEFAULT_SETTINGS_DIR, settings_file: str = DEFAULT_SETTINGS_FILE): 27 | """ 28 | 初始化設定管理器 29 | 30 | Args: 31 | settings_dir: 設定目錄 32 | settings_file: 設定文件名 33 | """ 34 | self.settings_dir = settings_dir 35 | self.settings_file = settings_file 36 | self.settings_path = os.path.join(settings_dir, settings_file) 37 | self.settings = DEFAULT_SETTINGS.copy() 38 | 39 | # 確保設定目錄存在 40 | os.makedirs(settings_dir, exist_ok=True) 41 | 42 | # 嘗試加載設定 43 | self.load_settings() 44 | 45 | def load_settings(self) -> Dict[str, Any]: 46 | """ 47 | 從文件加載設定 48 | 49 | Returns: 50 | 加載的設定字典 51 | """ 52 | try: 53 | if os.path.exists(self.settings_path): 54 | with open(self.settings_path, 'r', encoding='utf-8') as f: 55 | loaded_settings = json.load(f) 56 | 57 | # 更新設定,保留默認值作為備用 58 | for key, value in loaded_settings.items(): 59 | if key in self.settings: 60 | self.settings[key] = value 61 | 62 | return self.settings 63 | else: 64 | # 文件不存在時創建默認設定文件 65 | self.save_settings() 66 | return self.settings 67 | except Exception as e: 68 | logging.error(f"加載設定時出錯: {str(e)}") 69 | return self.settings 70 | 71 | def save_settings(self) -> bool: 72 | """ 73 | 保存設定到文件 74 | 75 | Returns: 76 | 保存是否成功 77 | """ 78 | try: 79 | with open(self.settings_path, 'w', encoding='utf-8') as f: 80 | json.dump(self.settings, f, indent=4, ensure_ascii=False) 81 | return True 82 | except Exception as e: 83 | logging.error(f"保存設定時出錯: {str(e)}") 84 | return False 85 | 86 | def get_setting(self, key: str, default: Any = None) -> Any: 87 | """ 88 | 獲取設定值 89 | 90 | Args: 91 | key: 設定鍵 92 | default: 默認值(如果設定不存在) 93 | 94 | Returns: 95 | 設定值 96 | """ 97 | return self.settings.get(key, default) 98 | 99 | def set_setting(self, key: str, value: Any) -> None: 100 | """ 101 | 設置設定值 102 | 103 | Args: 104 | key: 設定鍵 105 | value: 設定值 106 | """ 107 | self.settings[key] = value 108 | 109 | def update_settings(self, settings_dict: Dict[str, Any]) -> None: 110 | """ 111 | 批量更新設定 112 | 113 | Args: 114 | settings_dict: 設定字典 115 | """ 116 | for key, value in settings_dict.items(): 117 | self.settings[key] = value 118 | 119 | # 自動保存更新後的設定 120 | self.save_settings() 121 | 122 | def reset_to_defaults(self) -> None: 123 | """重置為默認設定""" 124 | self.settings = DEFAULT_SETTINGS.copy() 125 | self.save_settings() 126 | 127 | # 創建單例實例 128 | settings_manager = SettingsManager() 129 | 130 | # 導出便捷函數 131 | def get_setting(key: str, default: Any = None) -> Any: 132 | """獲取設定值""" 133 | return settings_manager.get_setting(key, default) 134 | 135 | def set_setting(key: str, value: Any) -> None: 136 | """設置設定值並保存""" 137 | settings_manager.set_setting(key, value) 138 | settings_manager.save_settings() 139 | 140 | def update_settings(settings_dict: Dict[str, Any]) -> None: 141 | """批量更新設定並保存""" 142 | settings_manager.update_settings(settings_dict) 143 | 144 | def load_settings() -> Dict[str, Any]: 145 | """加載設定""" 146 | return settings_manager.load_settings() 147 | 148 | def reset_defaults() -> None: 149 | """重置為默認設定""" 150 | settings_manager.reset_to_defaults() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyNaCl 2 | requests 3 | websocket-client 4 | numpy 5 | python-dotenv 6 | rich -------------------------------------------------------------------------------- /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 | except ImportError: 15 | API_KEY = os.getenv('API_KEY') 16 | SECRET_KEY = os.getenv('SECRET_KEY') 17 | WS_PROXY = os.getenv('PROXY_WEBSOCKET') 18 | 19 | def setup_logger(name): 20 | import logging 21 | logger = logging.getLogger(name) 22 | logger.setLevel(logging.INFO) 23 | handler = logging.StreamHandler() 24 | handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 25 | logger.addHandler(handler) 26 | return logger 27 | 28 | # 創建記錄器 29 | logger = setup_logger("main") 30 | 31 | def parse_arguments(): 32 | """解析命令行參數""" 33 | parser = argparse.ArgumentParser(description='Backpack Exchange 做市交易程序') 34 | 35 | # 模式選擇 36 | parser.add_argument('--panel', action='store_true', help='啟動圖形界面面板') 37 | parser.add_argument('--cli', action='store_true', help='啟動命令行界面') 38 | 39 | # 基本參數 40 | parser.add_argument('--api-key', type=str, help='API Key (可選,默認使用環境變數或配置文件)') 41 | parser.add_argument('--secret-key', type=str, help='Secret Key (可選,默認使用環境變數或配置文件)') 42 | parser.add_argument('--ws-proxy', type=str, help='WebSocket Proxy (可選,默認使用環境變數或配置文件)') 43 | 44 | # 做市參數 45 | parser.add_argument('--symbol', type=str, help='交易對 (例如: SOL_USDC)') 46 | parser.add_argument('--spread', type=float, help='價差百分比 (例如: 0.5)') 47 | parser.add_argument('--quantity', type=float, help='訂單數量 (可選)') 48 | parser.add_argument('--max-orders', type=int, default=3, help='每側最大訂單數量 (默認: 3)') 49 | parser.add_argument('--duration', type=int, default=3600, help='運行時間(秒)(默認: 3600)') 50 | parser.add_argument('--interval', type=int, default=60, help='更新間隔(秒)(默認: 60)') 51 | 52 | return parser.parse_args() 53 | 54 | def main(): 55 | """主函數""" 56 | args = parse_arguments() 57 | 58 | # 優先使用命令行參數中的API密鑰 59 | api_key = args.api_key or API_KEY 60 | secret_key = args.secret_key or SECRET_KEY 61 | 62 | # 读取wss代理 63 | ws_proxy = args.ws_proxy or WS_PROXY 64 | 65 | # 檢查API密鑰 66 | if not api_key or not secret_key: 67 | logger.error("缺少API密鑰,請通過命令行參數或環境變量提供") 68 | sys.exit(1) 69 | 70 | # 決定執行模式 71 | if args.panel: 72 | # 啟動圖形界面面板 73 | try: 74 | from panel.panel_main import run_panel 75 | run_panel(api_key=api_key, secret_key=secret_key, default_symbol=args.symbol) 76 | except ImportError as e: 77 | logger.error(f"啟動面板時出錯,缺少必要的庫: {str(e)}") 78 | logger.error("請執行 pip install rich 安裝所需庫") 79 | sys.exit(1) 80 | elif args.cli: 81 | # 啟動命令行界面 82 | try: 83 | from cli.commands import main_cli 84 | main_cli(api_key, secret_key, ws_proxy=ws_proxy) 85 | except ImportError as e: 86 | logger.error(f"啟動命令行界面時出錯: {str(e)}") 87 | sys.exit(1) 88 | elif args.symbol and args.spread is not None: 89 | # 如果指定了交易對和價差,直接運行做市策略 90 | try: 91 | from strategies.market_maker import MarketMaker 92 | 93 | # 初始化做市商 94 | market_maker = MarketMaker( 95 | api_key=api_key, 96 | secret_key=secret_key, 97 | symbol=args.symbol, 98 | base_spread_percentage=args.spread, 99 | order_quantity=args.quantity, 100 | max_orders=args.max_orders, 101 | ws_proxy=ws_proxy 102 | ) 103 | 104 | # 執行做市策略 105 | market_maker.run(duration_seconds=args.duration, interval_seconds=args.interval) 106 | 107 | except KeyboardInterrupt: 108 | logger.info("收到中斷信號,正在退出...") 109 | except Exception as e: 110 | logger.error(f"做市過程中發生錯誤: {e}") 111 | import traceback 112 | traceback.print_exc() 113 | else: 114 | # 沒有指定執行模式時顯示幫助 115 | print("請指定執行模式:") 116 | print(" --panel 啟動圖形界面面板") 117 | print(" --cli 啟動命令行界面") 118 | print(" 直接指定 --symbol 和 --spread 參數運行做市策略") 119 | print("\n使用 --help 查看完整幫助") 120 | 121 | if __name__ == "__main__": 122 | main() -------------------------------------------------------------------------------- /settings/panel_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_spread_percentage": 0.1, 3 | "order_quantity": 0.01, 4 | "max_orders": 5, 5 | "duration": 9999999999999999999999999999999999, 6 | "interval": 60, 7 | "default_symbol": null 8 | } -------------------------------------------------------------------------------- /strategies/__init__.py: -------------------------------------------------------------------------------- 1 | # strategies/__init__.py 2 | """ 3 | Strategies 模塊,包含各種交易策略 4 | """ -------------------------------------------------------------------------------- /strategies/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/strategies/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /strategies/__pycache__/market_maker.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/strategies/__pycache__/market_maker.cpython-312.pyc -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # utils/__init__.py 2 | """ 3 | Utils 模塊,提供各種輔助工具函數 4 | """ -------------------------------------------------------------------------------- /utils/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/utils/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /utils/__pycache__/helpers.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/utils/__pycache__/helpers.cpython-312.pyc -------------------------------------------------------------------------------- /utils/__pycache__/settings_manager.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/utils/__pycache__/settings_manager.cpython-312.pyc -------------------------------------------------------------------------------- /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 | rounded_price = round(price / tick_size_float) * tick_size_float 35 | precision = len(str(tick_size_float).split('.')[-1]) if '.' in str(tick_size_float) else 0 36 | return round(rounded_price, precision) 37 | 38 | def calculate_volatility(prices: List[float], window: int = 20) -> float: 39 | """ 40 | 計算波動率 41 | 42 | Args: 43 | prices: 價格列表 44 | window: 計算窗口大小 45 | 46 | Returns: 47 | 波動率百分比 48 | """ 49 | if len(prices) < window: 50 | return 0 51 | 52 | # 使用最近N個價格計算標準差 53 | recent_prices = prices[-window:] 54 | returns = np.diff(recent_prices) / recent_prices[:-1] 55 | return np.std(returns) * 100 # 轉換為百分比 -------------------------------------------------------------------------------- /ws_client/__init__.py: -------------------------------------------------------------------------------- 1 | # ws_client/__init__.py 2 | """ 3 | WebSocket 模塊,負責實時數據處理 4 | """ -------------------------------------------------------------------------------- /ws_client/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/ws_client/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /ws_client/__pycache__/client.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanowo/Backpack-MM-Simple/9cdbbef1ffcaa2db977e3d222503d53a3183d145/ws_client/__pycache__/client.cpython-312.pyc -------------------------------------------------------------------------------- /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 | # 添加重連中標誌,避免多次重連 69 | self.reconnecting = False 70 | 71 | def initialize_orderbook(self): 72 | """通過REST API獲取訂單簿初始快照""" 73 | try: 74 | # 使用REST API獲取完整訂單簿 75 | order_book = get_order_book(self.symbol, 100) # 增加深度 76 | if isinstance(order_book, dict) and "error" in order_book: 77 | logger.error(f"初始化訂單簿失敗: {order_book['error']}") 78 | return False 79 | 80 | # 重置並填充orderbook數據結構 81 | self.orderbook = { 82 | "bids": [[float(price), float(quantity)] for price, quantity in order_book.get('bids', [])], 83 | "asks": [[float(price), float(quantity)] for price, quantity in order_book.get('asks', [])] 84 | } 85 | 86 | # 按價格排序 87 | self.orderbook["bids"] = sorted(self.orderbook["bids"], key=lambda x: x[0], reverse=True) 88 | self.orderbook["asks"] = sorted(self.orderbook["asks"], key=lambda x: x[0]) 89 | 90 | logger.info(f"訂單簿初始化成功: {len(self.orderbook['bids'])} 個買單, {len(self.orderbook['asks'])} 個賣單") 91 | 92 | # 初始化最高買價和最低賣價 93 | if self.orderbook["bids"]: 94 | self.bid_price = self.orderbook["bids"][0][0] 95 | if self.orderbook["asks"]: 96 | self.ask_price = self.orderbook["asks"][0][0] 97 | if self.bid_price and self.ask_price: 98 | self.last_price = (self.bid_price + self.ask_price) / 2 99 | self.add_price_to_history(self.last_price) 100 | 101 | return True 102 | except Exception as e: 103 | logger.error(f"初始化訂單簿時出錯: {e}") 104 | return False 105 | 106 | def add_price_to_history(self, price): 107 | """添加價格到歷史記錄用於計算波動率""" 108 | if price: 109 | self.historical_prices.append(price) 110 | # 保持歷史記錄在設定長度內 111 | if len(self.historical_prices) > self.max_price_history: 112 | self.historical_prices = self.historical_prices[-self.max_price_history:] 113 | 114 | def get_volatility(self, window=20): 115 | """獲取當前波動率""" 116 | return calculate_volatility(self.historical_prices, window) 117 | 118 | def start_heartbeat(self): 119 | """開始心跳檢測線程""" 120 | if self.heartbeat_thread is None or not self.heartbeat_thread.is_alive(): 121 | self.heartbeat_thread = threading.Thread(target=self._heartbeat_check, daemon=True) 122 | self.heartbeat_thread.start() 123 | 124 | def _heartbeat_check(self): 125 | """定期檢查WebSocket連接狀態並在需要時重連""" 126 | while self.running: 127 | current_time = time.time() 128 | time_since_last_heartbeat = current_time - self.last_heartbeat 129 | 130 | if time_since_last_heartbeat > self.heartbeat_interval * 2: 131 | logger.warning(f"心跳檢測超時 ({time_since_last_heartbeat:.1f}秒),嘗試重新連接") 132 | # 使用非阻塞方式觸發重連 133 | threading.Thread(target=self._trigger_reconnect, daemon=True).start() 134 | 135 | time.sleep(5) # 每5秒檢查一次 136 | 137 | def _trigger_reconnect(self): 138 | """非阻塞觸發重連""" 139 | if not self.reconnecting: 140 | self.reconnect() 141 | 142 | def connect(self): 143 | """建立WebSocket連接""" 144 | try: 145 | self.running = True 146 | self.reconnect_attempts = 0 147 | self.reconnecting = False 148 | ws.enableTrace(False) 149 | self.ws = ws.WebSocketApp( 150 | WS_URL, 151 | on_open=self.on_open, 152 | on_message=self.on_message, 153 | on_error=self.on_error, 154 | on_close=self.on_close, 155 | on_ping=self.on_ping, 156 | on_pong=self.on_pong 157 | ) 158 | 159 | self.ws_thread = threading.Thread(target=self.ws_run_forever) 160 | self.ws_thread.daemon = True 161 | self.ws_thread.start() 162 | 163 | # 啟動心跳檢測 164 | self.start_heartbeat() 165 | except Exception as e: 166 | logger.error(f"初始化WebSocket連接時出錯: {e}") 167 | 168 | def ws_run_forever(self): 169 | """WebSocket運行循環 - 修復版本""" 170 | try: 171 | # 確保在運行前檢查socket狀態 172 | if hasattr(self.ws, 'sock') and self.ws.sock and self.ws.sock.connected: 173 | logger.debug("發現socket已經打開,跳過run_forever") 174 | return 175 | 176 | proxy_type = None 177 | http_proxy_auth = None 178 | http_proxy_host = None 179 | http_proxy_port = None 180 | 181 | if self.proxy and 3 <= len(self.proxy.split(":")) <= 4: 182 | arrs = self.proxy.split(":") 183 | proxy_type = arrs[0] 184 | arrs[1] = arrs[1][2:] # 去掉 // 185 | if len(arrs) == 3: 186 | http_proxy_host = arrs[1] 187 | else: 188 | password, http_proxy_host = arrs[2].split("@") 189 | http_proxy_auth = (arrs[1], password) 190 | http_proxy_port = arrs[-1] 191 | 192 | # 添加ping_interval和ping_timeout參數 193 | self.ws.run_forever( 194 | ping_interval=self.heartbeat_interval, 195 | ping_timeout=10, 196 | http_proxy_auth=http_proxy_auth, 197 | http_proxy_host=http_proxy_host, 198 | http_proxy_port=http_proxy_port, 199 | proxy_type=proxy_type 200 | ) 201 | 202 | except Exception as e: 203 | logger.error(f"WebSocket運行時出錯: {e}") 204 | finally: 205 | # 移除可能導致遞歸調用的重連邏輯 206 | logger.debug("WebSocket run_forever 執行結束") 207 | 208 | def on_pong(self, ws, message): 209 | """處理pong響應""" 210 | self.last_heartbeat = time.time() 211 | 212 | def reconnect(self): 213 | """完全斷開並重新建立WebSocket連接 - 修復版本""" 214 | # 防止多次重連 215 | if self.reconnecting: 216 | logger.debug("重連已在進行中,跳過此次重連請求") 217 | return False 218 | 219 | with self.ws_lock: 220 | if not self.running or self.reconnect_attempts >= self.max_reconnect_attempts: 221 | logger.warning(f"重連次數超過上限 ({self.max_reconnect_attempts}),停止重連") 222 | return False 223 | 224 | self.reconnecting = True 225 | self.reconnect_attempts += 1 226 | delay = min(self.reconnect_delay * (2 ** (self.reconnect_attempts - 1)), self.max_reconnect_delay) 227 | 228 | logger.info(f"嘗試第 {self.reconnect_attempts} 次重連,等待 {delay} 秒...") 229 | time.sleep(delay) 230 | 231 | # 確保完全斷開連接前先標記連接狀態 232 | self.connected = False 233 | 234 | # 優雅關閉現有連接 235 | self._force_close_connection() 236 | 237 | # 重置所有相關狀態 238 | self.ws_thread = None 239 | self.subscriptions = [] # 清空訂閲列表,以便重新訂閴 240 | 241 | try: 242 | # 創建全新的WebSocket連接 243 | ws.enableTrace(False) 244 | self.ws = ws.WebSocketApp( 245 | WS_URL, 246 | on_open=self.on_open, 247 | on_message=self.on_message, 248 | on_error=self.on_error, 249 | on_close=self.on_close, 250 | on_ping=self.on_ping, 251 | on_pong=self.on_pong 252 | ) 253 | 254 | # 創建新線程 255 | self.ws_thread = threading.Thread(target=self.ws_run_forever) 256 | self.ws_thread.daemon = True 257 | self.ws_thread.start() 258 | 259 | # 更新最後心跳時間,避免重連後立即觸發心跳檢測 260 | self.last_heartbeat = time.time() 261 | 262 | logger.info(f"第 {self.reconnect_attempts} 次重連已啟動") 263 | 264 | self.reconnecting = False 265 | return True 266 | 267 | except Exception as e: 268 | logger.error(f"重連過程中發生錯誤: {e}") 269 | self.reconnecting = False 270 | return False 271 | 272 | def _force_close_connection(self): 273 | """強制關閉現有連接""" 274 | try: 275 | # 完全斷開並清理之前的WebSocket連接 276 | if self.ws: 277 | try: 278 | # 顯式設置內部標記表明這是用户主動關閉 279 | if hasattr(self.ws, '_closed_by_me'): 280 | self.ws._closed_by_me = True 281 | 282 | # 關閉WebSocket 283 | self.ws.close() 284 | if hasattr(self.ws, 'keep_running'): 285 | self.ws.keep_running = False 286 | 287 | # 強制關閉socket 288 | if hasattr(self.ws, 'sock') and self.ws.sock: 289 | try: 290 | self.ws.sock.close() 291 | self.ws.sock = None 292 | except: 293 | pass 294 | except Exception as e: 295 | logger.debug(f"關閉WebSocket時的預期錯誤: {e}") 296 | 297 | self.ws = None 298 | 299 | # 處理舊線程 - 使用較短的超時時間 300 | if self.ws_thread and self.ws_thread.is_alive(): 301 | try: 302 | # 不要無限等待線程結束 303 | self.ws_thread.join(timeout=1.0) 304 | if self.ws_thread.is_alive(): 305 | logger.warning("舊線程未能在超時時間內結束,但繼續重連過程") 306 | except Exception as e: 307 | logger.debug(f"等待舊線程終止時出錯: {e}") 308 | 309 | # 給系統少量時間清理資源 310 | time.sleep(0.5) 311 | 312 | except Exception as e: 313 | logger.error(f"強制關閉連接時出錯: {e}") 314 | 315 | def on_ping(self, ws, message): 316 | """處理ping消息""" 317 | try: 318 | self.last_heartbeat = time.time() 319 | if ws and hasattr(ws, 'sock') and ws.sock: 320 | ws.sock.pong(message) 321 | else: 322 | logger.debug("無法迴應ping:WebSocket或sock為None") 323 | except Exception as e: 324 | logger.debug(f"迴應ping失敗: {e}") 325 | 326 | def on_open(self, ws): 327 | """WebSocket打開時的處理""" 328 | logger.info("WebSocket連接已建立") 329 | self.connected = True 330 | self.reconnect_attempts = 0 331 | self.reconnecting = False 332 | self.last_heartbeat = time.time() 333 | 334 | # 添加短暫延遲確保連接穩定 335 | time.sleep(0.5) 336 | 337 | # 初始化訂單簿 338 | orderbook_initialized = self.initialize_orderbook() 339 | 340 | # 如果初始化成功,訂閲深度和行情數據 341 | if orderbook_initialized: 342 | if "bookTicker" in self.subscriptions or not self.subscriptions: 343 | self.subscribe_bookTicker() 344 | 345 | if "depth" in self.subscriptions or not self.subscriptions: 346 | self.subscribe_depth() 347 | 348 | # 重新訂閲私有訂單更新流 349 | for sub in self.subscriptions: 350 | if sub.startswith("account."): 351 | self.private_subscribe(sub) 352 | 353 | def subscribe_bookTicker(self): 354 | """訂閲最優價格""" 355 | logger.info(f"訂閲 {self.symbol} 的bookTicker...") 356 | if not self.connected or not self.ws: 357 | logger.warning("WebSocket未連接,無法訂閲bookTicker") 358 | return False 359 | 360 | try: 361 | message = { 362 | "method": "SUBSCRIBE", 363 | "params": [f"bookTicker.{self.symbol}"] 364 | } 365 | self.ws.send(json.dumps(message)) 366 | if "bookTicker" not in self.subscriptions: 367 | self.subscriptions.append("bookTicker") 368 | return True 369 | except Exception as e: 370 | logger.error(f"訂閴bookTicker失敗: {e}") 371 | return False 372 | 373 | def subscribe_depth(self): 374 | """訂閴深度信息""" 375 | logger.info(f"訂閴 {self.symbol} 的深度信息...") 376 | if not self.connected or not self.ws: 377 | logger.warning("WebSocket未連接,無法訂閴深度信息") 378 | return False 379 | 380 | try: 381 | message = { 382 | "method": "SUBSCRIBE", 383 | "params": [f"depth.{self.symbol}"] 384 | } 385 | self.ws.send(json.dumps(message)) 386 | if "depth" not in self.subscriptions: 387 | self.subscriptions.append("depth") 388 | return True 389 | except Exception as e: 390 | logger.error(f"訂閴深度信息失敗: {e}") 391 | return False 392 | 393 | def private_subscribe(self, stream): 394 | """訂閴私有數據流""" 395 | if not self.connected or not self.ws: 396 | logger.warning("WebSocket未連接,無法訂閴私有數據流") 397 | return False 398 | 399 | try: 400 | timestamp = str(int(time.time() * 1000)) 401 | window = DEFAULT_WINDOW 402 | sign_message = f"instruction=subscribe×tamp={timestamp}&window={window}" 403 | signature = create_signature(self.secret_key, sign_message) 404 | 405 | if not signature: 406 | logger.error("簽名創建失敗,無法訂閴私有數據流") 407 | return False 408 | 409 | message = { 410 | "method": "SUBSCRIBE", 411 | "params": [stream], 412 | "signature": [self.api_key, signature, timestamp, window] 413 | } 414 | 415 | self.ws.send(json.dumps(message)) 416 | logger.info(f"已訂閴私有數據流: {stream}") 417 | if stream not in self.subscriptions: 418 | self.subscriptions.append(stream) 419 | return True 420 | except Exception as e: 421 | logger.error(f"訂閴私有數據流失敗: {e}") 422 | return False 423 | 424 | def on_message(self, ws, message): 425 | """處理WebSocket消息""" 426 | try: 427 | data = json.loads(message) 428 | 429 | # 處理ping pong消息 430 | if isinstance(data, dict) and data.get("ping"): 431 | pong_message = {"pong": data.get("ping")} 432 | if self.ws and self.connected: 433 | self.ws.send(json.dumps(pong_message)) 434 | self.last_heartbeat = time.time() 435 | return 436 | 437 | if "stream" in data and "data" in data: 438 | stream = data["stream"] 439 | event_data = data["data"] 440 | 441 | # 處理bookTicker 442 | if stream.startswith("bookTicker."): 443 | if 'b' in event_data and 'a' in event_data: 444 | self.bid_price = float(event_data['b']) 445 | self.ask_price = float(event_data['a']) 446 | self.last_price = (self.bid_price + self.ask_price) / 2 447 | # 記錄歷史價格用於計算波動率 448 | self.add_price_to_history(self.last_price) 449 | 450 | # 處理depth 451 | elif stream.startswith("depth."): 452 | if 'b' in event_data and 'a' in event_data: 453 | self._update_orderbook(event_data) 454 | 455 | # 訂單更新數據流 456 | elif stream.startswith("account.orderUpdate."): 457 | self.order_updates.append(event_data) 458 | 459 | if self.on_message_callback: 460 | self.on_message_callback(stream, event_data) 461 | 462 | except Exception as e: 463 | logger.error(f"處理WebSocket消息時出錯: {e}") 464 | 465 | def _update_orderbook(self, data): 466 | """更新訂單簿(優化處理速度)""" 467 | # 處理買單更新 468 | if 'b' in data: 469 | for bid in data['b']: 470 | price = float(bid[0]) 471 | quantity = float(bid[1]) 472 | 473 | # 使用二分查找來優化插入位置查找 474 | if quantity == 0: 475 | # 移除價位 476 | self.orderbook["bids"] = [b for b in self.orderbook["bids"] if b[0] != price] 477 | else: 478 | # 先查找是否存在相同價位 479 | found = False 480 | for i, b in enumerate(self.orderbook["bids"]): 481 | if b[0] == price: 482 | self.orderbook["bids"][i] = [price, quantity] 483 | found = True 484 | break 485 | 486 | # 如果不存在,插入並保持排序 487 | if not found: 488 | self.orderbook["bids"].append([price, quantity]) 489 | # 按價格降序排序 490 | self.orderbook["bids"] = sorted(self.orderbook["bids"], key=lambda x: x[0], reverse=True) 491 | 492 | # 處理賣單更新 493 | if 'a' in data: 494 | for ask in data['a']: 495 | price = float(ask[0]) 496 | quantity = float(ask[1]) 497 | 498 | if quantity == 0: 499 | # 移除價位 500 | self.orderbook["asks"] = [a for a in self.orderbook["asks"] if a[0] != price] 501 | else: 502 | # 先查找是否存在相同價位 503 | found = False 504 | for i, a in enumerate(self.orderbook["asks"]): 505 | if a[0] == price: 506 | self.orderbook["asks"][i] = [price, quantity] 507 | found = True 508 | break 509 | 510 | # 如果不存在,插入並保持排序 511 | if not found: 512 | self.orderbook["asks"].append([price, quantity]) 513 | # 按價格升序排序 514 | self.orderbook["asks"] = sorted(self.orderbook["asks"], key=lambda x: x[0]) 515 | 516 | def on_error(self, ws, error): 517 | """處理WebSocket錯誤""" 518 | logger.error(f"WebSocket發生錯誤: {error}") 519 | self.last_heartbeat = 0 # 強制觸發重連 520 | 521 | def on_close(self, ws, close_status_code, close_msg): 522 | """處理WebSocket關閉""" 523 | previous_connected = self.connected 524 | self.connected = False 525 | logger.info(f"WebSocket連接已關閉: {close_msg if close_msg else 'No message'} (狀態碼: {close_status_code if close_status_code else 'None'})") 526 | 527 | # 清理當前socket資源 528 | if hasattr(ws, 'sock') and ws.sock: 529 | try: 530 | ws.sock.close() 531 | ws.sock = None 532 | except Exception as e: 533 | logger.debug(f"關閉socket時出錯: {e}") 534 | 535 | if close_status_code == 1000 or getattr(ws, '_closed_by_me', False): 536 | logger.info("WebSocket正常關閉,不進行重連") 537 | elif previous_connected and self.running and self.auto_reconnect and not self.reconnecting: 538 | logger.info("WebSocket非正常關閉,將自動重連") 539 | # 使用線程觸發重連,避免在回調中直接重連 540 | threading.Thread(target=self._trigger_reconnect, daemon=True).start() 541 | 542 | def close(self): 543 | """完全關閉WebSocket連接""" 544 | logger.info("主動關閉WebSocket連接...") 545 | self.running = False 546 | self.connected = False 547 | self.reconnecting = False 548 | 549 | # 停止心跳檢測線程 550 | if self.heartbeat_thread and self.heartbeat_thread.is_alive(): 551 | try: 552 | self.heartbeat_thread.join(timeout=1) 553 | except Exception: 554 | pass 555 | self.heartbeat_thread = None 556 | 557 | # 強制關閉連接 558 | self._force_close_connection() 559 | 560 | # 重置訂閴狀態 561 | self.subscriptions = [] 562 | 563 | logger.info("WebSocket連接已完全關閉") 564 | 565 | def get_current_price(self): 566 | """獲取當前價格""" 567 | return self.last_price 568 | 569 | def get_bid_ask(self): 570 | """獲取買賣價""" 571 | return self.bid_price, self.ask_price 572 | 573 | def get_orderbook(self): 574 | """獲取訂單簿""" 575 | return self.orderbook 576 | 577 | def is_connected(self): 578 | """檢查連接狀態""" 579 | if not self.connected: 580 | return False 581 | if not self.ws: 582 | return False 583 | if not hasattr(self.ws, 'sock') or not self.ws.sock: 584 | return False 585 | 586 | # 檢查socket是否連接 587 | try: 588 | return self.ws.sock.connected 589 | except: 590 | return False 591 | 592 | def get_liquidity_profile(self, depth_percentage=0.01): 593 | """分析市場流動性特徵""" 594 | if not self.orderbook["bids"] or not self.orderbook["asks"]: 595 | return None 596 | 597 | mid_price = (self.bid_price + self.ask_price) / 2 if self.bid_price and self.ask_price else None 598 | if not mid_price: 599 | return None 600 | 601 | # 計算價格範圍 602 | min_price = mid_price * (1 - depth_percentage) 603 | max_price = mid_price * (1 + depth_percentage) 604 | 605 | # 分析買賣單流動性 606 | bid_volume = sum(qty for price, qty in self.orderbook["bids"] if price >= min_price) 607 | ask_volume = sum(qty for price, qty in self.orderbook["asks"] if price <= max_price) 608 | 609 | # 計算買賣比例 610 | ratio = bid_volume / ask_volume if ask_volume > 0 else float('inf') 611 | 612 | # 買賣壓力差異 613 | imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume) if (bid_volume + ask_volume) > 0 else 0 614 | 615 | return { 616 | 'bid_volume': bid_volume, 617 | 'ask_volume': ask_volume, 618 | 'volume_ratio': ratio, 619 | 'imbalance': imbalance, 620 | 'mid_price': mid_price 621 | } 622 | 623 | def check_and_reconnect_if_needed(self): 624 | """檢查連接狀態並在需要時重連 - 供外部調用""" 625 | if not self.is_connected() and not self.reconnecting: 626 | logger.info("外部檢查發現連接斷開,觸發重連...") 627 | threading.Thread(target=self._trigger_reconnect, daemon=True).start() 628 | return self.is_connected() --------------------------------------------------------------------------------