From 30f4a22fb42ec4b39728b386694fda13cadea5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 16 Feb 2026 17:11:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(binance=5Fclient,=20position=5Fmanager):?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E4=BB=B7=E6=A0=BC=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=B8=8E=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `binance_client` 中引入 K线和最优挂单的 WebSocket 流,优先从缓存中获取价格数据,减少对 REST API 的依赖。同时,更新了价格获取逻辑,确保在未能获取价格时提供详细的错误信息。增强了异常处理,确保在请求超时或失败时记录相关日志,提升系统的稳定性和可追溯性。 --- trading_system/binance_client.py | 97 +++++--- trading_system/book_ticker_stream.py | 181 ++++++++++++++ trading_system/config.py | 2 + trading_system/kline_stream.py | 256 ++++++++++++++++++++ trading_system/main.py | 42 ++++ trading_system/position_manager.py | 67 ++++-- ws交易接口.txt | 338 +++++++++++++++++++++++++++ ws行情推送.txt | 318 +++++++++++++++++++++++++ 行情ws接口.txt | 206 ++++++++++++++++ 行情接口REST.txt | 243 +++++++++++++++++++ 10 files changed, 1699 insertions(+), 51 deletions(-) create mode 100644 trading_system/book_ticker_stream.py create mode 100644 trading_system/kline_stream.py create mode 100644 ws交易接口.txt create mode 100644 ws行情推送.txt create mode 100644 行情ws接口.txt create mode 100644 行情接口REST.txt diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 29d1028..5220bdc 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -554,7 +554,7 @@ class BinanceClient: async def get_klines(self, symbol: str, interval: str = '5m', limit: int = 2) -> List[List]: """ 获取K线数据(合约市场) - 优先从 Redis 缓存读取,如果缓存不可用或过期则使用 REST API + 优先级:WS 缓存 > Redis 缓存 > REST API Args: symbol: 交易对 @@ -564,15 +564,30 @@ class BinanceClient: Returns: K线数据列表 """ - # 先查 Redis 缓存 + # 1. 优先从 WS 缓存读取(实时更新,无 REST 请求) + try: + from .kline_stream import get_klines_from_cache, get_kline_stream_instance, is_kline_cache_fresh + stream = get_kline_stream_instance() + if stream: + # 确保订阅该流(首次请求时自动订阅) + await stream.subscribe(symbol, interval, limit=max(limit, 50)) + if is_kline_cache_fresh(symbol, interval, max_age_sec=300.0): + ws_cached = get_klines_from_cache(symbol, interval, limit) + if ws_cached and len(ws_cached) >= limit: + logger.debug(f"从 WS 缓存获取 {symbol} K线数据: {interval} x{limit}") + return ws_cached + except Exception as e: + logger.debug(f"读取 K线 WS 缓存失败: {e}") + + # 2. 查 Redis 缓存 cache_key = f"klines:{symbol}:{interval}:{limit}" cached = await self.redis_cache.get(cache_key) if cached: - logger.debug(f"从缓存获取 {symbol} K线数据: {interval} x{limit}") + logger.debug(f"从 Redis 缓存获取 {symbol} K线数据: {interval} x{limit}") return cached + # 3. REST API(兜底) try: - # 缓存未命中,调用 API klines = await self._rate_limited_request( f'klines_{symbol}_{interval}', self.client.futures_klines(symbol=symbol, interval=interval, limit=limit) @@ -580,22 +595,11 @@ class BinanceClient: # 写入 Redis 缓存(根据 interval 动态设置 TTL) if klines: - # TTL 设置:1m=10秒, 5m=30秒, 15m=1分钟, 1h=5分钟, 4h=15分钟, 1d=1小时 ttl_map = { - '1m': 10, - '3m': 20, - '5m': 30, - '15m': 60, - '30m': 120, - '1h': 300, - '2h': 600, - '4h': 900, - '6h': 1200, - '8h': 1800, - '12h': 2400, - '1d': 3600 + '1m': 10, '3m': 20, '5m': 30, '15m': 60, '30m': 120, + '1h': 300, '2h': 600, '4h': 900, '6h': 1200, '8h': 1800, '12h': 2400, '1d': 3600 } - ttl = ttl_map.get(interval, 300) # 默认 5 分钟 + ttl = ttl_map.get(interval, 300) await self.redis_cache.set(cache_key, klines, ttl=ttl) logger.debug(f"已缓存 {symbol} K线数据: {interval} x{limit} (TTL: {ttl}秒)") @@ -1352,13 +1356,39 @@ class BinanceClient: # 获取交易对精度信息 symbol_info = await self.get_symbol_info(symbol) - # 获取当前价格以计算名义价值 + # 获取当前价格以计算名义价值(优先用 bookTicker 估算执行价,提升准确性) if price is None: - ticker = await self.get_ticker_24h(symbol) - if not ticker: - logger.error(f"无法获取 {symbol} 的价格信息") - return None - current_price = ticker['price'] + # 优先用最优挂单估算(买单用 askPrice,卖单用 bidPrice) + try: + from .book_ticker_stream import get_book_ticker + book = get_book_ticker(symbol) + if book: + if side == "BUY": + estimated_price = float(book.get("askPrice", 0)) + else: + estimated_price = float(book.get("bidPrice", 0)) + if estimated_price > 0: + current_price = estimated_price + logger.debug(f"{symbol} 使用 bookTicker 估算价格: {current_price:.4f} ({side})") + else: + ticker = await self.get_ticker_24h(symbol) + if not ticker: + logger.error(f"无法获取 {symbol} 的价格信息") + return None + current_price = ticker['price'] + else: + ticker = await self.get_ticker_24h(symbol) + if not ticker: + logger.error(f"无法获取 {symbol} 的价格信息") + return None + current_price = ticker['price'] + except Exception as e: + logger.debug(f"使用 bookTicker 估算价格失败,回退到 ticker: {e}") + ticker = await self.get_ticker_24h(symbol) + if not ticker: + logger.error(f"无法获取 {symbol} 的价格信息") + return None + current_price = ticker['price'] else: current_price = price @@ -1716,10 +1746,19 @@ class BinanceClient: # ========================= async def futures_create_algo_order(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]: + symbol = params.get('symbol', 'UNKNOWN') + algo_timeout = getattr(config, 'ALGO_ORDER_TIMEOUT_SEC', 30) # 条件单接口易超时,单独加长 try: - # python-binance 内部会自动补 timestamp / signature - res = await self.client._request_futures_api("post", "algoOrder", True, data=params) + # python-binance 内部会自动补 timestamp / signature;用较长超时避免 TimeoutError 空消息 + res = await asyncio.wait_for( + self.client._request_futures_api("post", "algoOrder", True, data=params), + timeout=algo_timeout, + ) return res if isinstance(res, dict) else None + except asyncio.TimeoutError: + logger.error(f"{symbol} ❌ 创建 Algo 条件单失败: 请求超时({algo_timeout}秒)") + logger.error(f" 参数: {params}") + return None except BinanceAPIException as e: error_code = e.code if hasattr(e, 'code') else None error_msg = str(e) @@ -1749,8 +1788,8 @@ class BinanceClient: return None except Exception as e: - symbol = params.get('symbol', 'UNKNOWN') - logger.error(f"{symbol} ❌ 创建 Algo 条件单失败: {type(e).__name__}: {e}") + err_msg = getattr(e, "message", str(e)) or repr(e) + logger.error(f"{symbol} ❌ 创建 Algo 条件单失败: {type(e).__name__}: {err_msg}") logger.error(f" 参数: {params}") import traceback logger.debug(f" 堆栈跟踪: {traceback.format_exc()}") @@ -2007,7 +2046,7 @@ class BinanceClient: logger.error(f"{symbol} ❌ 挂保护单失败({trigger_type}): {error_msg}") logger.error(f" 错误代码: {error_code}") logger.error(f" 触发价格: {stop_price:.8f} (格式化后: {stop_price_str})") - logger.error(f" 当前价格: {cp if cp else 'N/A'}") + logger.error(f" 当前价格: {cp if cp else '无(已尝试 WS/REST)'}") logger.error(f" 持仓方向: {pd}") logger.error(f" 平仓方向: {close_side}") logger.error(f" 工作类型: {working_type}") diff --git a/trading_system/book_ticker_stream.py b/trading_system/book_ticker_stream.py new file mode 100644 index 0000000..3831180 --- /dev/null +++ b/trading_system/book_ticker_stream.py @@ -0,0 +1,181 @@ +""" +最优挂单 WebSocket 流:订阅 !bookTicker,维护全市场最优买/卖价缓存。 +用于滑点估算、入场价格优化,提升交易执行效果。 +文档:更新速度 5s,推送所有交易对的最优挂单(最高买单、最低卖单)。 +""" +import asyncio +import json +import logging +import time +from typing import Dict, Optional, Any + +logger = logging.getLogger(__name__) + +# 最优挂单缓存:symbol -> { bidPrice, bidQty, askPrice, askQty, time } +_book_ticker_cache: Dict[str, Dict[str, Any]] = {} +_book_ticker_updated_at: float = 0.0 + + +def get_book_ticker_cache() -> Dict[str, Dict[str, Any]]: + """返回当前最优挂单缓存。""" + return dict(_book_ticker_cache) + + +def get_book_ticker(symbol: str) -> Optional[Dict[str, Any]]: + """获取指定交易对的最优挂单;无缓存时返回 None。""" + return _book_ticker_cache.get(symbol.upper()) + + +def is_book_ticker_cache_fresh(max_age_sec: float = 10.0) -> bool: + """缓存是否在 max_age_sec 秒内更新过且非空。""" + if not _book_ticker_cache: + return False + return (time.monotonic() - _book_ticker_updated_at) <= max_age_sec + + +def estimate_slippage(symbol: str, side: str, quantity: float) -> Optional[float]: + """ + 估算滑点(USDT):基于最优挂单估算执行价格与标记价格的偏差。 + + Args: + symbol: 交易对 + side: BUY/SELL + quantity: 下单数量 + + Returns: + 估算滑点(USDT),None 表示无法估算 + """ + ticker = get_book_ticker(symbol) + if not ticker: + return None + try: + bid_price = float(ticker.get("bidPrice", 0)) + ask_price = float(ticker.get("askPrice", 0)) + bid_qty = float(ticker.get("bidQty", 0)) + ask_qty = float(ticker.get("askQty", 0)) + if bid_price <= 0 or ask_price <= 0: + return None + mid_price = (bid_price + ask_price) / 2 + if side == "BUY": + # 买单:用 askPrice(卖一)估算,滑点 = (askPrice - midPrice) * quantity + if ask_qty >= quantity: + slippage = (ask_price - mid_price) * quantity + else: + # 数量超过卖一,需要吃多档,粗略估算 + slippage = (ask_price - mid_price) * quantity * 1.2 + else: # SELL + # 卖单:用 bidPrice(买一)估算,滑点 = (midPrice - bidPrice) * quantity + if bid_qty >= quantity: + slippage = (mid_price - bid_price) * quantity + else: + slippage = (mid_price - bid_price) * quantity * 1.2 + return slippage + except Exception: + return None + + +class BookTickerStream: + """订阅合约 !bookTicker,持续更新 _book_ticker_cache。无需 listenKey,公开行情。""" + + def __init__(self, testnet: bool = False): + self.testnet = testnet + self._ws = None + self._task: Optional[asyncio.Task] = None + self._running = False + + def _ws_url(self) -> str: + if self.testnet: + return "wss://stream.binancefuture.com/ws/!bookTicker" + return "wss://fstream.binance.com/ws/!bookTicker" + + async def start(self) -> bool: + if self._running: + return True + self._running = True + self._task = asyncio.create_task(self._run_ws()) + logger.info("BookTickerStream: 已启动(!bookTicker),用于滑点估算") + return True + + async def stop(self): + self._running = False + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + self._task = None + if self._ws: + try: + await self._ws.close() + except Exception: + pass + self._ws = None + logger.info("BookTickerStream: 已停止") + + async def _run_ws(self): + import aiohttp + while self._running: + url = self._ws_url() + try: + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + url, heartbeat=50, timeout=aiohttp.ClientTimeout(total=15) + ) as ws: + self._ws = ws + logger.info("BookTickerStream: WS 已连接") + async for msg in ws: + if not self._running: + break + if msg.type == aiohttp.WSMsgType.TEXT: + self._handle_message(msg.data) + elif msg.type in (aiohttp.WSMsgType.ERROR, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE): + break + except asyncio.CancelledError: + break + except Exception as e: + err_msg = getattr(e, "message", str(e)) or repr(e) + err_type = type(e).__name__ + logger.warning( + "BookTickerStream: WS 异常 %s: %s,10s 后重连", + err_type, err_msg, + exc_info=logger.isEnabledFor(logging.DEBUG), + ) + await asyncio.sleep(10) + self._ws = None + if not self._running: + break + + def _handle_message(self, raw: str): + global _book_ticker_cache, _book_ticker_updated_at + try: + data = json.loads(raw) + except Exception: + return + # 可能是单条对象或组合流格式 + if isinstance(data, dict) and "stream" in data: + ticker_data = data.get("data", {}) + else: + ticker_data = data + + if not isinstance(ticker_data, dict) or ticker_data.get("e") != "bookTicker": + return + + s = (ticker_data.get("s") or "").strip().upper() + if not s or not s.endswith("USDT"): + return + + try: + _book_ticker_cache[s] = { + "symbol": s, + "bidPrice": float(ticker_data.get("b", 0)), + "bidQty": float(ticker_data.get("B", 0)), + "askPrice": float(ticker_data.get("a", 0)), + "askQty": float(ticker_data.get("A", 0)), + "time": int(ticker_data.get("T", 0)), + } + except (TypeError, ValueError): + return + + _book_ticker_updated_at = time.monotonic() + logger.debug(f"BookTickerStream: 已更新 {s} bid={_book_ticker_cache[s]['bidPrice']:.4f} ask={_book_ticker_cache[s]['askPrice']:.4f}") diff --git a/trading_system/config.py b/trading_system/config.py index 9c02ab8..76f4ec7 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -405,6 +405,8 @@ CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试 # 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败 # 调大此值会延长单次请求最大等待时间,在同步/查询持仓时可能阻塞事件循环,影响实时性;保持 60 秒,通过增加重试+退避应对偶发超时 READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '60')) +# 创建 Algo 条件单(止损/止盈)单次请求超时(秒),网络不稳时可适当调大 +ALGO_ORDER_TIMEOUT_SEC = int(os.getenv('ALGO_ORDER_TIMEOUT_SEC', '30')) # 获取持仓时过滤掉名义价值低于此值的仓位(USDT),与币安仪表板不一致时可调低或设为 0 POSITION_MIN_NOTIONAL_USDT = float(os.getenv('POSITION_MIN_NOTIONAL_USDT', '1.0')) diff --git a/trading_system/kline_stream.py b/trading_system/kline_stream.py new file mode 100644 index 0000000..d625bf5 --- /dev/null +++ b/trading_system/kline_stream.py @@ -0,0 +1,256 @@ +""" +K线 WebSocket 流:订阅 @kline_,维护K线缓存。 +供 get_klines 优先使用,替代 REST 拉取,减少超时、实时更新技术指标。 +文档:推送间隔 250ms,仅推送最新一根K线的更新;x=false 表示K线未完结,x=true 表示已完结。 +""" +import asyncio +import json +import logging +import time +from typing import Dict, List, Optional, Any, Tuple + +logger = logging.getLogger(__name__) + +# K线缓存:{ (symbol, interval): [kline1, kline2, ...] },最多保留 limit 根 +_kline_cache: Dict[Tuple[str, str], List[List]] = {} +_kline_cache_updated_at: Dict[Tuple[str, str], float] = {} +_kline_cache_limit: Dict[Tuple[str, str], int] = {} # 每个 (symbol, interval) 的 limit + + +def get_klines_from_cache(symbol: str, interval: str, limit: int = 50) -> Optional[List[List]]: + """从缓存返回K线数据(与 REST get_klines 格式兼容)。未订阅或数据不足时返回 None。""" + key = (symbol.upper(), interval.lower()) + cached = _kline_cache.get(key) + if not cached or len(cached) < limit: + return None + # 返回最后 limit 根 + return cached[-limit:] + + +def is_kline_cache_fresh(symbol: str, interval: str, max_age_sec: float = 300.0) -> bool: + """缓存是否在 max_age_sec 秒内更新过。""" + key = (symbol.upper(), interval.lower()) + updated = _kline_cache_updated_at.get(key, 0) + if updated == 0: + return False + return (time.monotonic() - updated) <= max_age_sec + + +class KlineStream: + """订阅合约 K线流,持续更新 _kline_cache。支持动态订阅/取消订阅。""" + + def __init__(self, testnet: bool = False): + self.testnet = testnet + self._ws = None + self._task: Optional[asyncio.Task] = None + self._running = False + self._subscribed: Dict[Tuple[str, str], bool] = {} # (symbol, interval) -> 是否已订阅 + self._subscription_lock = asyncio.Lock() + + def _ws_base_url(self) -> str: + if self.testnet: + return "wss://stream.binancefuture.com" + return "wss://fstream.binance.com" + + async def start(self) -> bool: + global _kline_stream_instance + if self._running: + return True + self._running = True + _kline_stream_instance = self + self._task = asyncio.create_task(self._run_ws()) + logger.info("KlineStream: 已启动(支持动态订阅)") + return True + + async def stop(self): + global _kline_stream_instance + self._running = False + _kline_stream_instance = None + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + self._task = None + if self._ws: + try: + await self._ws.close() + except Exception: + pass + self._ws = None + logger.info("KlineStream: 已停止") + + async def subscribe(self, symbol: str, interval: str, limit: int = 50) -> bool: + """订阅指定 symbol 和 interval 的K线流(若 WS 未连接则等待连接后订阅)。""" + symbol = symbol.upper() + interval = interval.lower() + key = (symbol, interval) + async with self._subscription_lock: + if self._subscribed.get(key): + return True + if not self._running: + return False + # 等待 WS 连接(最多等待 5 秒) + for _ in range(50): + if self._ws: + break + await asyncio.sleep(0.1) + if not self._ws: + return False + stream_name = f"{symbol.lower()}@kline_{interval}" + try: + await self._ws.send_json({ + "method": "SUBSCRIBE", + "params": [stream_name], + "id": int(time.time() * 1000) % 1000000, + }) + self._subscribed[key] = True + _kline_cache_limit[key] = limit + logger.debug(f"KlineStream: 已订阅 {symbol} {interval}") + return True + except Exception as e: + logger.warning(f"KlineStream: 订阅 {symbol} {interval} 失败: {e}") + return False + + async def _run_ws(self): + import aiohttp + # 使用组合流,支持动态订阅 + url = f"{self._ws_base_url()}/stream" + while self._running: + try: + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + url, heartbeat=50, timeout=aiohttp.ClientTimeout(total=15) + ) as ws: + self._ws = ws + logger.info("KlineStream: WS 已连接(组合流,支持动态订阅)") + async for msg in ws: + if not self._running: + break + if msg.type == aiohttp.WSMsgType.TEXT: + raw = msg.data + # 处理订阅响应({"result": null, "id": ...})或K线数据 + try: + data = json.loads(raw) + if isinstance(data, dict) and "result" in data: + # 订阅响应,忽略 + continue + except Exception: + pass + self._handle_message(raw) + elif msg.type in (aiohttp.WSMsgType.ERROR, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE): + break + except asyncio.CancelledError: + break + except Exception as e: + err_msg = getattr(e, "message", str(e)) or repr(e) + err_type = type(e).__name__ + logger.warning( + "KlineStream: WS 异常 %s: %s,10s 后重连", + err_type, err_msg, + exc_info=logger.isEnabledFor(logging.DEBUG), + ) + await asyncio.sleep(10) + self._ws = None + # 重连时清空订阅状态,需要重新订阅 + async with self._subscription_lock: + self._subscribed.clear() + if not self._running: + break + + def _handle_message(self, raw: str): + global _kline_cache, _kline_cache_updated_at + try: + data = json.loads(raw) + except Exception: + return + # 组合流格式:{ "stream": "btcusdt@kline_1h", "data": {...} } + if isinstance(data, dict) and "stream" in data: + stream = data.get("stream", "") + kline_data = data.get("data", {}) + else: + kline_data = data + stream = "" + + if not isinstance(kline_data, dict) or kline_data.get("e") != "kline": + return + + k = kline_data.get("k") + if not isinstance(k, dict): + return + + s = (k.get("s") or "").strip().upper() + i = (k.get("i") or "").strip().lower() + if not s or not i: + return + + key = (s, i) + if key not in self._subscribed: + return + + # 转换为 REST 格式:[open_time, open, high, low, close, volume, close_time, quote_volume, trades, ...] + try: + t = int(k.get("t", 0)) + o = float(k.get("o", 0)) + h = float(k.get("h", 0)) + l = float(k.get("l", 0)) + c = float(k.get("c", 0)) + v = float(k.get("v", 0)) + T = int(k.get("T", 0)) + q = float(k.get("q", 0)) + n = int(k.get("n", 0)) + x = k.get("x", False) # 是否完结 + except (TypeError, ValueError): + return + + kline_rest_format = [ + t, # open_time + str(o), # open + str(h), # high + str(l), # low + str(c), # close + str(v), # volume + T, # close_time + str(q), # quote_volume + n, # trades + "0", # taker_buy_base_volume + "0", # taker_buy_quote_volume + "0", # ignore + ] + + # 更新缓存:若 x=true(完结),追加新K线;若 x=false(未完结),更新最后一根 + if key not in _kline_cache: + _kline_cache[key] = [] + cache_list = _kline_cache[key] + + if x: + # K线完结:追加新K线 + cache_list.append(kline_rest_format) + limit = _kline_cache_limit.get(key, 50) + if len(cache_list) > limit * 2: + cache_list[:] = cache_list[-limit:] + else: + # K线未完结:更新最后一根(或追加第一根) + if cache_list: + cache_list[-1] = kline_rest_format + else: + cache_list.append(kline_rest_format) + + _kline_cache_updated_at[key] = time.monotonic() + logger.debug(f"KlineStream: 已更新 {s} {i} (完结={x}),缓存 {len(cache_list)} 根") + + +# 全局 KlineStream 实例 +_kline_stream_instance: Optional["KlineStream"] = None + + +def get_kline_stream_instance() -> Optional["KlineStream"]: + """返回当前运行的 KlineStream 实例(未启动时为 None)。""" + return _kline_stream_instance + + +def set_kline_stream_instance(instance: Optional["KlineStream"]): + """设置全局 KlineStream 实例(由 main 调用)。""" + global _kline_stream_instance + _kline_stream_instance = instance diff --git a/trading_system/main.py b/trading_system/main.py index 88784e3..5d3a548 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -237,6 +237,8 @@ async def main(): client = None user_data_stream = None ticker_24h_stream = None + kline_stream = None + book_ticker_stream = None try: # 1. 初始化币安客户端 logger.info("初始化币安客户端...") @@ -338,6 +340,34 @@ async def main(): logger.debug(f"启动 24h ticker WS 失败(将使用 REST): {e}") ticker_24h_stream = None + # 3.2 启动 K线 WS 流(技术指标实时更新,减少 REST 超时) + kline_stream = None + try: + from .kline_stream import KlineStream + use_testnet = getattr(config, "USE_TESTNET", False) + kline_stream = KlineStream(testnet=use_testnet) + if await kline_stream.start(): + logger.info("✓ K线 WS 已启动(技术指标将优先使用 WS 缓存,减少超时)") + else: + kline_stream = None + except Exception as e: + logger.debug(f"启动 K线 WS 失败(将使用 REST): {e}") + kline_stream = None + + # 3.3 启动最优挂单 WS 流(滑点估算,优化入场价格) + book_ticker_stream = None + try: + from .book_ticker_stream import BookTickerStream + use_testnet = getattr(config, "USE_TESTNET", False) + book_ticker_stream = BookTickerStream(testnet=use_testnet) + if await book_ticker_stream.start(): + logger.info("✓ 最优挂单 WS 已启动(用于滑点估算与价格优化)") + else: + book_ticker_stream = None + except Exception as e: + logger.debug(f"启动最优挂单 WS 失败: {e}") + book_ticker_stream = None + # 4. 初始化各个模块 logger.info("初始化交易模块...") scanner = MarketScanner(client) @@ -431,6 +461,18 @@ async def main(): logger.info("Ticker24h Stream 已停止") except Exception as e: logger.debug(f"停止 Ticker24h Stream 时异常: {e}") + try: + if kline_stream is not None: + await kline_stream.stop() + logger.info("Kline Stream 已停止") + except Exception as e: + logger.debug(f"停止 Kline Stream 时异常: {e}") + try: + if book_ticker_stream is not None: + await book_ticker_stream.stop() + logger.info("BookTicker Stream 已停止") + except Exception as e: + logger.debug(f"停止 BookTicker Stream 时异常: {e}") if client: await client.disconnect() logger.info("程序已退出") diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 461a13e..ef57f17 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1568,35 +1568,58 @@ class PositionManager: except Exception as e: logger.debug(f"{symbol} 取消旧保护单时出错(可忽略): {e}") - # 获取当前价格(如果未提供,优先使用标记价格 MARK_PRICE,因为止损单使用 MARK_PRICE) + # 获取当前价格(如果未提供):优先 WS 缓存(bookTicker/ticker24h)→ 持仓 markPrice → REST ticker if current_price is None: try: - # 优先获取标记价格(MARK_PRICE),因为止损单使用 MARK_PRICE 作为触发基准 - positions = await self._get_open_positions() - position = next((p for p in positions if p['symbol'] == symbol), None) - if position: - mark_price = position.get('markPrice') - if mark_price and float(mark_price) > 0: - current_price = float(mark_price) - logger.debug(f"{symbol} 从持仓获取标记价格: {current_price}") - else: - # 如果没有标记价格,使用 ticker 价格 + # 1) 优先 WS:最优挂单中点价或 24h ticker 缓存(避免 REST 超时) + try: + try: + from .book_ticker_stream import get_book_ticker + except ImportError: + from book_ticker_stream import get_book_ticker + book = get_book_ticker(symbol) + if book and float(book.get("bidPrice", 0)) > 0 and float(book.get("askPrice", 0)) > 0: + mid = (float(book["bidPrice"]) + float(book["askPrice"])) / 2 + current_price = mid + logger.debug(f"{symbol} 从 bookTicker WS 获取当前价格: {current_price}") + except Exception: + pass + if current_price is None: + try: + try: + from .ticker_24h_stream import get_tickers_24h_cache, is_ticker_24h_cache_fresh + except ImportError: + from ticker_24h_stream import get_tickers_24h_cache, is_ticker_24h_cache_fresh + if is_ticker_24h_cache_fresh(max_age_sec=120): + tickers = get_tickers_24h_cache() + t = tickers.get(symbol) + if t and t.get("price"): + current_price = float(t["price"]) + logger.debug(f"{symbol} 从 ticker24h WS 获取当前价格: {current_price}") + except Exception: + pass + # 2) 持仓标记价格(MARK_PRICE,与止损单触发基准一致) + if current_price is None: + positions = await self._get_open_positions() + position = next((p for p in positions if p['symbol'] == symbol), None) + if position: + mark_price = position.get('markPrice') + if mark_price and float(mark_price) > 0: + current_price = float(mark_price) + logger.debug(f"{symbol} 从持仓获取标记价格: {current_price}") + if current_price is None: ticker = await self.client.get_ticker_24h(symbol) if ticker: - current_price = ticker.get('price') - logger.debug(f"{symbol} 从ticker获取当前价格: {current_price}") - else: - # 如果没有持仓,使用 ticker 价格 - ticker = await self.client.get_ticker_24h(symbol) - if ticker: - current_price = ticker.get('price') - logger.debug(f"{symbol} 从ticker获取当前价格: {current_price}") + current_price = ticker.get('price') and float(ticker['price']) or None + if current_price: + logger.debug(f"{symbol} 从 ticker REST 获取当前价格: {current_price}") except Exception as e: - logger.warning(f"{symbol} 获取当前价格失败: {e}") + err_msg = getattr(e, "message", str(e)) or repr(e) + logger.warning(f"{symbol} 获取当前价格失败: {type(e).__name__}: {err_msg}") # 如果仍然没有当前价格,记录警告 if current_price is None: - logger.warning(f"{symbol} ⚠️ 无法获取当前价格,止损单可能无法正确验证触发条件") + logger.warning(f"{symbol} ⚠️ 无法获取当前价格(已尝试 WS bookTicker/ticker24h、持仓、REST),止损单可能无法正确验证触发条件") # 在挂止损单前,检查当前价格是否已经触发止损(避免 -2021 错误) if current_price and stop_loss: @@ -1705,7 +1728,7 @@ class PositionManager: else: logger.error(f"{symbol} ❌ 止损单挂单失败!将依赖WebSocket监控,但可能无法及时止损") logger.error(f" 止损价格: {stop_loss:.8f}") - logger.error(f" 当前价格: {current_price if current_price else 'N/A'}") + logger.error(f" 当前价格: {current_price if current_price else '无(已尝试 WS bookTicker/ticker24h、持仓、REST)'}") logger.error(f" 持仓方向: {side}") logger.error(f" ⚠️ 警告: 没有交易所级别的止损保护,如果系统崩溃或网络中断,可能无法及时止损!") logger.error(f" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损") diff --git a/ws交易接口.txt b/ws交易接口.txt new file mode 100644 index 0000000..de0ff60 --- /dev/null +++ b/ws交易接口.txt @@ -0,0 +1,338 @@ +下单 (TRADE) +接口描述 +下单 + +方式 +order.place + +请求 +order.place + +{ + "id": "3f7df6e3-2df4-44b9-9919-d2f38f90a99a", + "method": "order.place", + "params": { + "apiKey": "HMOchcfii9ZRZnhjp2XjGXhsOBd6msAhKz9joQaWwZ7arcJTlD2hGPHQj1lGdTjR", + "positionSide": "BOTH", + "price": 43187.00, + "quantity": 0.1, + "side": "BUY", + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "timestamp": 1702555533821, + "type": "LIMIT", + "signature": "0f04368b2d22aafd0ggc8809ea34297eff602272917b5f01267db4efbc1c9422" + } +} + +请求权重 +0 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING YES 交易对 +side ENUM YES 买卖方向 SELL, BUY +positionSide ENUM NO 持仓方向,单向持仓模式下非必填,默认且仅可填BOTH;在双向持仓模式下必填,且仅可选择 LONG 或 SHORT +type ENUM YES 订单类型 LIMIT, MARKET, STOP, TAKE_PROFIT, STOP_MARKET, TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET +reduceOnly STRING NO true, false; 非双开模式下默认false;双开模式下不接受此参数; 使用closePosition不支持此参数。 +quantity DECIMAL NO 下单数量,使用closePosition不支持此参数。 +price DECIMAL NO 委托价格 +newClientOrderId STRING NO 用户自定义的订单号,不可以重复出现在挂单中。如空缺系统会自动赋值。必须满足正则规则 ^[\.A-Z\:/a-z0-9_-]{1,36}$ +stopPrice DECIMAL NO 触发价, 仅 STOP, STOP_MARKET, TAKE_PROFIT, TAKE_PROFIT_MARKET 需要此参数 +closePosition STRING NO true, false;触发后全部平仓,仅支持STOP_MARKET和TAKE_PROFIT_MARKET;不与quantity合用;自带只平仓效果,不与reduceOnly 合用 +activationPrice DECIMAL NO 追踪止损激活价格,仅TRAILING_STOP_MARKET 需要此参数, 默认为下单当前市场价格(支持不同workingType) +callbackRate DECIMAL NO 追踪止损回调比例,可取值范围[0.1, 10],其中 1代表1% ,仅TRAILING_STOP_MARKET 需要此参数 +timeInForce ENUM NO 有效方法 +workingType ENUM NO stopPrice 触发类型: MARK_PRICE(标记价格), CONTRACT_PRICE(合约最新价). 默认 CONTRACT_PRICE +priceProtect STRING NO 条件单触发保护:"TRUE","FALSE", 默认"FALSE". 仅 STOP, STOP_MARKET, TAKE_PROFIT, TAKE_PROFIT_MARKET 需要此参数 +newOrderRespType ENUM NO "ACK", "RESULT", 默认 "ACK" +priceMatch ENUM NO OPPONENT/ OPPONENT_5/ OPPONENT_10/ OPPONENT_20/QUEUE/ QUEUE_5/ QUEUE_10/ QUEUE_20;不能与price同时传 +selfTradePreventionMode ENUM NO NONE / EXPIRE_TAKER/ EXPIRE_MAKER/ EXPIRE_BOTH; 默认NONE +goodTillDate LONG NO TIF为GTD时订单的自动取消时间, 当timeInforce为GTD时必传;传入的时间戳仅保留秒级精度,毫秒级部分会被自动忽略,时间戳需大于当前时间+600s且小于253402300799000 +recvWindow LONG NO +timestamp LONG YES +根据 order type的不同,某些参数强制要求,具体如下: + +Type 强制要求的参数 +LIMIT timeInForce, quantity, price或priceMatch +MARKET quantity +STOP, TAKE_PROFIT quantity, stopPrice +STOP_MARKET, TAKE_PROFIT_MARKET stopPrice, price或priceMatch +TRAILING_STOP_MARKET callbackRate +条件单的触发必须: + +如果订单参数priceProtect为true: +达到触发价时,MARK_PRICE(标记价格)与CONTRACT_PRICE(合约最新价)之间的价差不能超过改symbol触发保护阈值 +触发保护阈值请参考接口GET /fapi/v1/exchangeInfo 返回内容相应symbol中"triggerProtect"字段 +STOP, STOP_MARKET 止损单: +买入: 最新合约价格/标记价格高于等于触发价stopPrice +卖出: 最新合约价格/标记价格低于等于触发价stopPrice +TAKE_PROFIT, TAKE_PROFIT_MARKET 止盈单: +买入: 最新合约价格/标记价格低于等于触发价stopPrice +卖出: 最新合约价格/标记价格高于等于触发价stopPrice +TRAILING_STOP_MARKET 跟踪止损单: +买入: 当合约价格/标记价格区间最低价格低于激活价格activationPrice,且最新合约价格/标记价高于等于最低价设定回调幅度。 +卖出: 当合约价格/标记价格区间最高价格高于激活价格activationPrice,且最新合约价格/标记价低于等于最高价设定回调幅度。 +TRAILING_STOP_MARKET 跟踪止损单如果遇到报错 {"code": -2021, "msg": "Order would immediately trigger."} +表示订单不满足以下条件: + +买入: 指定的activationPrice 必须小于 latest price +卖出: 指定的activationPrice 必须大于 latest price +newOrderRespType 如果传 RESULT: + +MARKET 订单将直接返回成交结果; +配合使用特殊 timeInForce 的 LIMIT 订单将直接返回成交或过期拒绝结果。 +STOP_MARKET, TAKE_PROFIT_MARKET 配合 closePosition=true: + +条件单触发依照上述条件单触发逻辑 +条件触发后,平掉当时持有所有多头仓位(若为卖单)或当时持有所有空头仓位(若为买单) +不支持 quantity 参数 +自带只平仓属性,不支持reduceOnly参数 +双开模式下,LONG方向上不支持BUY; SHORT 方向上不支持SELL +响应示例 +{ + "id": "3f7df6e3-2df4-44b9-9919-d2f38f90a99a", + "status": 200, + "result": { + "orderId": 325078477, + "symbol": "BTCUSDT", + "status": "NEW", + "clientOrderId": "iCXL1BywlBaf2sesNUrVl3", + "price": "43187.00", + "avgPrice": "0.00", + "origQty": "0.100", + "executedQty": "0.000", + "cumQty": "0.000", + "cumQuote": "0.00000", + "timeInForce": "GTC", + "type": "LIMIT", + "reduceOnly": false, + "closePosition": false, + "side": "BUY", + "positionSide": "BOTH", + "stopPrice": "0.00", + "workingType": "CONTRACT_PRICE", + "priceProtect": false, + "origType": "LIMIT", + "priceMatch": "NONE", + "selfTradePreventionMode": "NONE", + "goodTillDate": 0, + "updateTime": 1702555534435 + }, + "rateLimits": [ + { + "rateLimitType": "ORDERS", + "interval": "SECOND", + "intervalNum": 10, + "limit": 300, + "count": 1 + }, + { + "rateLimitType": "ORDERS", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200, + "count": 1 + }, + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 1 + } + ] +} + + + + +查询订单 +查询订单 (USER_DATA) +接口描述 +查询订单状态 + +请注意,如果订单满足如下条件,不会被查询到: +订单的最终状态为 CANCELED 或者 EXPIRED 并且 订单没有任何的成交记录 并且 订单生成时间 + 3天 < 当前时间 +订单创建时间 + 90天 < 当前时间 +方式 +order.status + +请求 +{ + "id": "0ce5d070-a5e5-4ff2-b57f-1556741a4204", + "method": "order.status", + "params": { + "apiKey": "HMOchcfii9ZRZnhjp2XjGXhsOBd6msAhKz9joQaWwZ7arcJTlD2hGPHQj1lGdTjR", + "orderId": 328999071, + "symbol": "BTCUSDT", + "timestamp": 1703441060152, + "signature": "ba48184fc38a71d03d2b5435bd67c1206e3191e989fe99bda1bc643a880dfdbf" + } +} + +请求权重 +1 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING YES 交易对 +orderId LONG NO 系统订单号 +origClientOrderId STRING NO 用户自定义的订单号 +recvWindow LONG NO +timestamp LONG YES +注意: + +至少需要发送 orderId 与 origClientOrderId中的一个 +orderId在symbol维度是自增的 +响应示例 +{ + "avgPrice": "0.00000", // 平均成交价 + "clientOrderId": "abc", // 用户自定义的订单号 + "cumQuote": "0", // 成交金额 + "executedQty": "0", // 成交量 + "orderId": 1573346959, // 系统订单号 + "origQty": "0.40", // 原始委托数量 + "origType": "TRAILING_STOP_MARKET", // 触发前订单类型 + "price": "0", // 委托价格 + "reduceOnly": false, // 是否仅减仓 + "side": "BUY", // 买卖方向 + "positionSide": "SHORT", // 持仓方向 + "status": "NEW", // 订单状态 + "stopPrice": "9300", // 触发价,对`TRAILING_STOP_MARKET`无效 + "closePosition": false, // 是否条件全平仓 + "symbol": "BTCUSDT", // 交易对 + "time": 1579276756075, // 订单时间 + "timeInForce": "GTC", // 有效方法 + "type": "TRAILING_STOP_MARKET", // 订单类型 + "activatePrice": "9020", // 跟踪止损激活价格, 仅`TRAILING_STOP_MARKET` 订单返回此字段 + "priceRate": "0.3", // 跟踪止损回调比例, 仅`TRAILING_STOP_MARKET` 订单返回此字段 + "updateTime": 1579276756075, // 更新时间 + "workingType": "CONTRACT_PRICE", // 条件价格触发类型 + "priceProtect": false // 是否开启条件单触发保护 +} + + +条件单下单 (TRADE) +接口描述 +条件单下单 + +方式 +algoOrder.place + +请求 +{ + "id": "7731f6b5-8d5e-419c-a424-016b0a5fe8d7", + "method": "algoOrder.place", + "params": { + "algoType": "CONDITIONAL", + "apiKey": "autoApiKey7mM4kPWaRuTUypdTEZKG8U8tDjO64xdBJBrmE1nXU2XSwdxGPyXcYx", + "newOrderRespType": "RESULT", + "positionSide": "SHORT", + "price": "160000", + "quantity": "1", + "recvWindow": "99999999", + "side": "SELL", + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "timestamp": 1762506268690, + "triggerprice": 120000, + "type": "TAKE_PROFIT", + "signature": "ec6e529c69fd8193b19484907bc713114eae06259fcab9728dafd5910f9cac5a" + } +} + +请求权重 +0 + +请求参数 +名称 类型 是否必需 描述 +algoType ENUM YES 仅支持 CONDITIONAL +symbol STRING YES 交易对 +side ENUM YES 买卖方向 SELL, BUY +positionSide ENUM NO 持仓方向,单向持仓模式下非必填,默认且仅可填BOTH;在双向持仓模式下必填,且仅可选择 LONG 或 SHORT +type ENUM YES 条件订单类型 STOP, TAKE_PROFIT, STOP_MARKET, TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET +timeInForce ENUM NO IOC or GTC or FOK, 默认 GTC +quantity DECIMAL NO 下单数量,使用closePosition不支持此参数。 +price DECIMAL NO 委托价格 +triggerPrice DECIMAL NO 触发价 +workingType ENUM NO 触发类型: MARK_PRICE(标记价格), CONTRACT_PRICE(合约最新价). 默认 CONTRACT_PRICE +priceMatch ENUM NO OPPONENT/ OPPONENT_5/ OPPONENT_10/ OPPONENT_20/QUEUE/ QUEUE_5/ QUEUE_10/ QUEUE_20;不能与price同时传 +closePosition STRING NO true, false;触发后全部平仓,仅支持STOP_MARKET和TAKE_PROFIT_MARKET;不与quantity合用;自带只平仓效果,不与reduceOnly 合用 +priceProtect STRING NO 条件单触发保护:"TRUE","FALSE", 默认"FALSE". +reduceOnly STRING NO true, false; 非双开模式下默认false;双开模式下不接受此参数; 使用closePosition不支持此参数。 +activatePrice DECIMAL NO 追踪止损激活价格,仅TRAILING_STOP_MARKET 需要此参数, 默认为下单当前市场价格(支持不同workingType) +callbackRate DECIMAL NO 追踪止损回调比例,可取值范围[0.1, 10],其中 1代表1% ,仅TRAILING_STOP_MARKET 需要此参数 +clientAlgoId STRING NO 用户自定义的条件订单号,不可以重复出现在挂单中。如空缺系统会自动赋值。必须满足正则规则 ^[\.A-Z\:/a-z0-9_-]{1,36}$ +newOrderRespType ENUM NO "ACK", "RESULT", 默认 "ACK" +selfTradePreventionMode ENUM NO EXPIRE_TAKER/ EXPIRE_MAKER/ EXPIRE_BOTH; 默认NONE +goodTillDate LONG NO TIF为GTD时订单的自动取消时间, 当timeInforce为GTD时必传;传入的时间戳仅保留秒级精度,毫秒级部分会被自动忽略,时间戳需大于当前时间+600s且小于253402300799000 +recvWindow LONG NO +timestamp LONG YES +条件单的触发必须: + +如果订单参数priceProtect为true: +达到触发价时,MARK_PRICE(标记价格)与CONTRACT_PRICE(合约最新价)之间的价差不能超过改symbol触发保护阈值 +触发保护阈值请参考接口GET /fapi/v1/exchangeInfo 返回内容相应symbol中"triggerProtect"字段 +STOP, STOP_MARKET 止损单: +买入: 最新合约价格/标记价格高于等于触发价stopPrice +卖出: 最新合约价格/标记价格低于等于触发价stopPrice +TAKE_PROFIT, TAKE_PROFIT_MARKET 止盈单: +买入: 最新合约价格/标记价格低于等于触发价stopPrice +卖出: 最新合约价格/标记价格高于等于触发价stopPrice +TRAILING_STOP_MARKET 跟踪止损单: +买入: 当合约价格/标记价格区间最低价格低于激活价格activatePrice,且最新合约价格/标记价高于等于最低价设定回调幅度。 +卖出: 当合约价格/标记价格区间最高价格高于激活价格activatePrice,且最新合约价格/标记价低于等于最高价设定回调幅度。 +TRAILING_STOP_MARKET 跟踪止损单如果遇到报错 {"code": -2021, "msg": "Order would immediately trigger."} +表示订单不满足以下条件: + +买入: 指定的activatePrice 必须小于 latest price +卖出: 指定的activatePrice 必须大于 latest price +STOP_MARKET, TAKE_PROFIT_MARKET 配合 closePosition=true: + +条件单触发依照上述条件单触发逻辑 +条件触发后,平掉当时持有所有多头仓位(若为卖单)或当时持有所有空头仓位(若为买单) +不支持 quantity 参数 +自带只平仓属性,不支持reduceOnly参数 +双开模式下,LONG方向上不支持BUY; SHORT 方向上不支持SELL +selfTradePreventionMode 仅在 timeInForce为IOC或GTC或GTD时生效. + +响应示例 +{ + "id": "06c9dbd8-ccbf-4ecf-a29c-fe31495ac73f", + "status": 200, + "result": { + "algoId": 3000000000003505, + "clientAlgoId": "0Xkl1p621E4EryvufmYre1", + "algoType": "CONDITIONAL", + "orderType": "TAKE_PROFIT", + "symbol": "BTCUSDT", + "side": "SELL", + "positionSide": "SHORT", + "timeInForce": "GTC", + "quantity": "1.000", + "algoStatus": "NEW", + "triggerPrice": "120000.00", + "price": "160000.00", + "icebergQuantity": null, + "selfTradePreventionMode": "EXPIRE_MAKER", + "workingType": "CONTRACT_PRICE", + "priceMatch": "NONE", + "closePosition": false, + "priceProtect": false, + "reduceOnly": false, + "createTime": 1762507264142, + "updateTime": 1762507264143, + "triggerTime": 0, + "goodTillDate": 0 + }, + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/ws行情推送.txt b/ws行情推送.txt new file mode 100644 index 0000000..7dfef29 --- /dev/null +++ b/ws行情推送.txt @@ -0,0 +1,318 @@ +全市场最优挂单信息 +数据流描述 +所有交易对交易对最优挂单信息 + +Stream Name +!bookTicker + +注意: 响应消息不包含RPI订单,其不可见。 + +更新速度 +5s + +响应示例 +{ + "e":"bookTicker", // 事件类型 + "u":400900217, // 更新ID + "E": 1568014460893, // 事件推送时间 + "T": 1568014460891, // 撮合时间 + "s":"BNBUSDT", // 交易对 + "b":"25.35190000", // 买单最优挂单价格 + "B":"31.21000000", // 买单最优挂单数量 + "a":"25.36520000", // 卖单最优挂单价格 + "A":"40.66000000" // 卖单最优挂单数量 +} + + +市场数据连接 +本篇所列出的所有wss接口需用下列方式连接: + +Base Url:wss://fstream.binance.com +订阅单一stream格式为 /ws/ +组合streams的URL格式为 /stream?streams=/// +连接样例: +wss://fstream.binance.com/ws/bnbusdt@aggTrade +wss://fstream.binance.com/stream?streams=bnbusdt@aggTrade/btcusdt@markPrice +订阅组合streams时,事件payload会以这样的格式封装 {"stream":"} + +stream名称中所有交易对均为小写。 + +每个链接有效期不超过24小时,请妥善处理断线重连。 + +服务端每3分钟会发送ping帧,客户端应当在10分钟内回复pong帧,否则服务端会主动断开链接。允许客户端发送不成对的pong帧(即客户端可以以高于15分钟每次的频率发送pong帧保持链接)。 + +Websocket服务器每秒最多接受10个订阅消息。 + +如果用户发送的消息超过限制,连接会被断开连接。反复被断开连接的IP有可能被服务器屏蔽。 + +单个连接最多可以订阅 1024 个Streams。 + + + +实时订阅/取消数据流 +以下数据可以通过websocket发送以实现订阅或取消订阅数据流。示例如下。 +响应内容中的id是无符号整数,作为往来信息的唯一标识。 +订阅一个信息流 +请求 + +{ + "method": "SUBSCRIBE", + "params": + [ + "btcusdt@aggTrade", + "btcusdt@depth" + ], + "id": 1 +} + +响应 + +{ + "result": null, + "id": 1 +} + +取消订阅一个信息流 +请求 + +{ + "method": "UNSUBSCRIBE", + "params": + [ + "btcusdt@depth" + ], + "id": 312 +} + +响应 + +{ + "result": null, + "id": 312 +} + +已订阅信息流 +请求 + +{ + "method": "LIST_SUBSCRIPTIONS", + "id": 3 +} + +响应 + +{ + "result": [ + "btcusdt@aggTrade" + ], + "id": 3 +} + +设定属性 +当前,唯一可以设置的属性是设置是否启用combined("组合")信息流。 +当使用/ws/("原始信息流")进行连接时,combined属性设置为false,而使用 /stream/进行连接时则将属性设置为true。 + +请求 + +{ + "method": "SET_PROPERTY", + "params": + [ + "combined", + true + ], + "id": 5 +} + +响应 + +{ + "result": null + "id": 5 +} + +检索属性 +请求 + +{ + "method": "GET_PROPERTY", + "params": + [ + "combined" + ], + "id": 2 +} + +响应 + +{ + "result": true, // Indicates that combined is set to true. + "id": 2 +} + +错误信息 +错误信息 描述 +{"code": 0, "msg": "Unknown property"} SET_PROPERTY 或 GET_PROPERTY中应用的参数无效 +{"code": 1, "msg": "Invalid value type: expected Boolean"} 仅接受true或false +{"code": 2, "msg": "Invalid request: property name must be a string"} 提供的属性名无效 +{"code": 2, "msg": "Invalid request: request ID must be an unsigned integer"} 参数id未提供或id值是无效类型 +{"code": 2, "msg": "Invalid request: unknown variant %s, expected one of SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, SET_PROPERTY, GET_PROPERTY at line 1 column 28"} 错字提醒,或提供的值不是预期类型 +{"code": 2, "msg": "Invalid request: too many parameters"} 数据中提供了不必要参数 +{"code": 2, "msg": "Invalid request: property name must be a string"} 未提供属性名 +{"code": 2, "msg": "Invalid request: missing field method at line 1 column 73"} 数据未提供method +{"code":3,"msg":"Invalid JSON: expected value at line %s column %s"} JSON 语法有误. + + + +按Symbol的完整Ticker +数据流描述 +按Symbol刷新的24小时完整ticker信息 + +Stream Name +@ticker + +更新速度 +2000ms + +响应示例 +{ + "e": "24hrTicker", // 事件类型 + "E": 123456789, // 事件时间 + "s": "BNBUSDT", // 交易对 + "p": "0.0015", // 24小时价格变化 + "P": "250.00", // 24小时价格变化(百分比) + "w": "0.0018", // 平均价格 + "c": "0.0025", // 最新成交价格 + "Q": "10", // 最新成交价格上的成交量 + "o": "0.0010", // 24小时内第一比成交的价格 + "h": "0.0025", // 24小时内最高成交价 + "l": "0.0010", // 24小时内最低成交价 + "v": "10000", // 24小时内成交量 + "q": "18", // 24小时内成交额 + "O": 0, // 统计开始时间 + "C": 86400000, // 统计关闭时间 + "F": 0, // 24小时内第一笔成交交易ID + "L": 18150, // 24小时内最后一笔成交交易ID + "n": 18151 // 24小时内成交数 +} + + +K线 +Stream Description +K线stream逐秒推送所请求的K线种类(最新一根K线)的更新。推送间隔250毫秒(如有刷新) + +订阅 Kline 需要提供间隔参数,最短为分钟线,最长为月线。支持以下间隔: + +m -> 分钟; h -> 小时; d -> 天; w -> 周; M -> 月 + +1m +3m +5m +15m +30m +1h +2h +4h +6h +8h +12h +1d +3d +1w +1M +Stream Name +@kline_ + +Update Speed +250ms + +Response Example +{ + "e": "kline", // 事件类型 + "E": 123456789, // 事件时间 + "s": "BNBUSDT", // 交易对 + "k": { + "t": 123400000, // 这根K线的起始时间 + "T": 123460000, // 这根K线的结束时间 + "s": "BNBUSDT", // 交易对 + "i": "1m", // K线间隔 + "f": 100, // 这根K线期间第一笔成交ID + "L": 200, // 这根K线期间末一笔成交ID + "o": "0.0010", // 这根K线期间第一笔成交价 + "c": "0.0020", // 这根K线期间末一笔成交价 + "h": "0.0025", // 这根K线期间最高成交价 + "l": "0.0015", // 这根K线期间最低成交价 + "v": "1000", // 这根K线期间成交量 + "n": 100, // 这根K线期间成交笔数 + "x": false, // 这根K线是否完结(是否已经开始下一根K线) + "q": "1.0000", // 这根K线期间成交额 + "V": "500", // 主动买入的成交量 + "Q": "0.500", // 主动买入的成交额 + "B": "123456" // 忽略此参数 + } +} + + +连续合约K线 +数据流描述 +K线stream逐秒推送所请求的K线种类(最新一根K线)的更新。 + +合约类型: + +perpetual 永续合约 +current_quarter 当季交割合约 +next_quarter 次季交割合约 +tradifi_perpetual 传统金融合约 +订阅Kline需要提供间隔参数,最短为分钟线,最长为月线。支持以下间隔: + +s -> 秒; m -> 分钟; h -> 小时; d -> 天; w -> 周; M -> 月 + +1s +1m +3m +5m +15m +30m +1h +2h +4h +6h +8h +12h +1d +3d +1w +1M +Stream Name +_@continuousKline_ + +更新速度 +250ms + +响应示例 +{ + "e":"continuous_kline", // 事件类型 + "E":1607443058651, // 事件时间 + "ps":"BTCUSDT", // 标的交易对 + "ct":"PERPETUAL", // 合约类型 + "k":{ + "t":1607443020000, // 这根K线的起始时间 + "T":1607443079999, // 这根K线的结束时间 + "i":"1m", // K线间隔 + "f":116467658886, // 这根K线期间第一笔更新ID + "L":116468012423, // 这根K线期间末一笔更新ID + "o":"18787.00", // 这根K线期间第一笔成交价 + "c":"18804.04", // 这根K线期间末一笔成交价 + "h":"18804.04", // 这根K线期间最高成交价 + "l":"18786.54", // 这根K线期间最低成交价 + "v":"197.664", // 这根K线期间成交量 + "n":543, // 这根K线期间成交笔数 + "x":false, // 这根K线是否完结(是否已经开始下一根K线) + "q":"3715253.19494", // 这根K线期间成交额 + "V":"184.769", // 主动买入的成交量 + "Q":"3472925.84746", // 主动买入的成交额 + "B":"0" // 忽略此参数 + } +} + + diff --git a/行情ws接口.txt b/行情ws接口.txt new file mode 100644 index 0000000..8c53366 --- /dev/null +++ b/行情ws接口.txt @@ -0,0 +1,206 @@ +深度信息 +接口描述 +获取有限档订单薄信息 + +方式 +depth + +注意: 响应消息不包含RPI订单,其不可见。 + +请求 +{ + "id": "51e2affb-0aba-4821-ba75-f2625006eb43", + "method": "depth", + "params": { + "symbol": "BTCUSDT" + } +} + +请求权重 +limit 权重 +5, 10, 20, 50 2 +100 5 +500 10 +1000 20 +请求参数 +名称 类型 是否必需 描述 +symbol STRING YES 交易对 +limit INT NO 默认 500; 可选值:[5, 10, 20, 50, 100, 500, 1000] +响应示例 +{ + "id": "51e2affb-0aba-4821-ba75-f2625006eb43", + "status": 200, + "result": { + "lastUpdateId": 1027024, + "E": 1589436922972, // 消息时间 + "T": 1589436922959, // 撮合引擎时间 + "bids": [ // 买单 + [ + "4.00000000", // 价格 + "431.00000000" // 数量 + ] + ], + "asks": [ // 卖单 + [ + "4.00000200", // 价格 + "12.00000000" // 数量 + ] + ] + }, + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 5 + } + ] +} + + +最新价格 +接口描述 +返回最近价格 + +方式 +ticker.price + +请求 +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "method": "ticker.price", + "params": { + "symbol": "BTCUSDT" + } +} + +请求权重 +单交易对1,无交易对2 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING NO 交易对 +不发送交易对参数,则会返回所有交易对信息 +响应示例 +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "status": 200, + "result": { + "symbol": "BTCUSDT", + "price": "6000.01", + "time": 1589437530011 + }, + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 2 + } + ] +} + +或(当不发送symbol) + +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "status": 200, + "result": [ + { + "symbol": "BTCUSDT", + "price": "6000.01", + "time": 1589437530011 + } + ], + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 2 + } + ] +} + + + +当前最优挂单 +接口描述 +返回当前最优的挂单(最高买单,最低卖单) + +方式 +ticker.book + +注意: 响应消息不包含RPI订单,其不可见。 + +请求 +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "method": "ticker.book", + "params": { + "symbol": "BTCUSDT" + } +} + +请求权重 +单交易对2,无交易对5 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING NO 交易对 +不发送交易对参数,则会返回所有交易对信息 +该接口返回头中的X-MBX-USED-WEIGHT-1M参数不准确,可以忽略 +响应示例 +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "status": 200, + "result": { + "lastUpdateId": 1027024, + "symbol": "BTCUSDT", // 交易对 + "bidPrice": "4.00000000", //最优买单价 + "bidQty": "431.00000000", //挂单量 + "askPrice": "4.00000200", //最优卖单价 + "askQty": "9.00000000", //挂单量 + "time": 1589437530011 // 撮合引擎时间 + }, + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 2 + } + ] +} + +或(当不发送symbol) + +{ + "id": "9d32157c-a556-4d27-9866-66760a174b57", + "status": 200, + "result": [ + { + "lastUpdateId": 1027024, + "symbol": "BTCUSDT", // 交易对 + "bidPrice": "4.00000000", //最优买单价 + "bidQty": "431.00000000", //挂单量 + "askPrice": "4.00000200", //最优卖单价 + "askQty": "9.00000000", //挂单量 + "time": 1589437530011 // 撮合引擎时间 + } + ] + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "count": 2 + } + ] +} + diff --git a/行情接口REST.txt b/行情接口REST.txt new file mode 100644 index 0000000..55ca20b --- /dev/null +++ b/行情接口REST.txt @@ -0,0 +1,243 @@ +获取服务器时间 +接口描述 +获取服务器时间 + +HTTP请求 +GET /fapi/v1/time + +请求权重 +1 + +请求参数 +NONE + +响应示例 +{ + "serverTime": 1499827319559 // 当前的系统时间 +} + + + +获取交易规则和交易对 +接口描述 +获取交易规则和交易对 + +HTTP请求 +GET /fapi/v1/exchangeInfo + +请求权重 +1 + +请求参数 +NONE + +响应示例 +{ + "exchangeFilters": [], + "rateLimits": [ // API访问的限制 + { + "interval": "MINUTE", // 按照分钟计算 + "intervalNum": 1, // 按照1分钟计算 + "limit": 2400, // 上限次数 + "rateLimitType": "REQUEST_WEIGHT" // 按照访问权重来计算 + }, + { + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200, + "rateLimitType": "ORDERS" // 按照订单数量来计算 + } + ], + "serverTime": 1565613908500, // 请忽略。如果需要获取当前系统时间,请查询接口 “GET /fapi/v1/time” + "assets": [ // 资产信息 + { + "asset": "BTC", + "marginAvailable": true, // 是否可用作保证金 + "autoAssetExchange": "-0.10" // 保证金资产自动兑换阈值 + }, + { + "asset": "USDT", + "marginAvailable": true, // 是否可用作保证金 + "autoAssetExchange": "0" // 保证金资产自动兑换阈值 + }, + { + "asset": "BNB", + "marginAvailable": false, // 是否可用作保证金 + "autoAssetExchange": null // 保证金资产自动兑换阈值 + } + ], + "symbols": [ // 交易对信息 + { + "symbol": "BLZUSDT", // 交易对 + "pair": "BLZUSDT", // 标的交易对 + "contractType": "PERPETUAL", // 合约类型 + "deliveryDate": 4133404800000, // 交割日期 + "onboardDate": 1598252400000, // 上线日期 + "status": "TRADING", // 交易对状态 + "maintMarginPercent": "2.5000", // 请忽略 + "requiredMarginPercent": "5.0000", // 请忽略 + "baseAsset": "BLZ", // 标的资产 + "quoteAsset": "USDT", // 报价资产 + "marginAsset": "USDT", // 保证金资产 + "pricePrecision": 5, // 价格小数点位数(仅作为系统精度使用,注意同tickSize 区分) + "quantityPrecision": 0, // 数量小数点位数(仅作为系统精度使用,注意同stepSize 区分) + "baseAssetPrecision": 8, // 标的资产精度 + "quotePrecision": 8, // 报价资产精度 + "underlyingType": "COIN", + "underlyingSubType": ["STORAGE"], + "settlePlan": 0, + "triggerProtect": "0.15", // 开启"priceProtect"的条件订单的触发阈值 + "filters": [ + { + "filterType": "PRICE_FILTER", // 价格限制 + "maxPrice": "300", // 价格上限, 最大价格 + "minPrice": "0.0001", // 价格下限, 最小价格 + "tickSize": "0.0001" // 订单最小价格间隔 + }, + { + "filterType": "LOT_SIZE", // 数量限制 + "maxQty": "10000000", // 数量上限, 最大数量 + "minQty": "1", // 数量下限, 最小数量 + "stepSize": "1" // 订单最小数量间隔 + }, + { + "filterType": "MARKET_LOT_SIZE", // 市价订单数量限制 + "maxQty": "590119", // 数量上限, 最大数量 + "minQty": "1", // 数量下限, 最小数量 + "stepSize": "1" // 允许的步进值 + }, + { + "filterType": "MAX_NUM_ORDERS", // 最多订单数限制 + "limit": 200 + }, + { + "filterType": "MIN_NOTIONAL", // 最小名义价值 + "notional": "5.0", + }, + { + "filterType": "PERCENT_PRICE", // 价格比限制 + "multiplierUp": "1.1500", // 价格上限百分比 + "multiplierDown": "0.8500", // 价格下限百分比 + "multiplierDecimal": "4" + } + ], + "OrderType": [ // 订单类型 + "LIMIT", // 限价单 + "MARKET", // 市价单 + "STOP", // 止损单 + "STOP_MARKET", // 止损市价单 + "TAKE_PROFIT", // 止盈单 + "TAKE_PROFIT_MARKET", // 止盈暑市价单 + "TRAILING_STOP_MARKET" // 跟踪止损市价单 + ], + "timeInForce": [ // 有效方式 + "GTC", // 成交为止, 一直有效 + "IOC", // 无法立即成交(吃单)的部分就撤销 + "FOK", // 无法全部立即成交就撤销 + "GTX" // 无法成为挂单方就撤销 + ], + "liquidationFee": "0.010000", // 强平费率 + "marketTakeBound": "0.30", // 市价吃单(相对于标记价格)允许可造成的最大价格偏离比例 + } + ], + "timezone": "UTC" // 服务器所用的时间区域 +} + +24hr价格变动情况 +接口描述 +请注意,不携带symbol参数会返回全部交易对数据,不仅数据庞大,而且权重极高 + +HTTP请求 +GET /fapi/v1/ticker/24hr + +请求权重 +带symbol为1, 不带为40 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING NO 交易对 +不发送交易对参数,则会返回所有交易对信息 +响应示例 +{ + "symbol": "BTCUSDT", + "priceChange": "-94.99999800", //24小时价格变动 + "priceChangePercent": "-95.960", //24小时价格变动百分比 + "weightedAvgPrice": "0.29628482", //加权平均价 + "lastPrice": "4.00000200", //最近一次成交价 + "lastQty": "200.00000000", //最近一次成交额 + "openPrice": "99.00000000", //24小时内第一次成交的价格 + "highPrice": "100.00000000", //24小时最高价 + "lowPrice": "0.10000000", //24小时最低价 + "volume": "8913.30000000", //24小时成交量 + "quoteVolume": "15.30000000", //24小时成交额 + "openTime": 1499783499040, //24小时内,第一笔交易的发生时间 + "closeTime": 1499869899040, //24小时内,最后一笔交易的发生时间 + "firstId": 28385, // 首笔成交id + "lastId": 28460, // 末笔成交id + "count": 76 // 成交笔数 +} + +或(当不发送交易对信息) + +[ + { + "symbol": "BTCUSDT", + "priceChange": "-94.99999800", //24小时价格变动 + "priceChangePercent": "-95.960", //24小时价格变动百分比 + "weightedAvgPrice": "0.29628482", //加权平均价 + "lastPrice": "4.00000200", //最近一次成交价 + "lastQty": "200.00000000", //最近一次成交额 + "openPrice": "99.00000000", //24小时内第一次成交的价格 + "highPrice": "100.00000000", //24小时最高价 + "lowPrice": "0.10000000", //24小时最低价 + "volume": "8913.30000000", //24小时成交量 + "quoteVolume": "15.30000000", //24小时成交额 + "openTime": 1499783499040, //24小时内,第一笔交易的发生时间 + "closeTime": 1499869899040, //24小时内,最后一笔交易的发生时间 + "firstId": 28385, // 首笔成交id + "lastId": 28460, // 末笔成交id + "count": 76 // 成交笔数 + } +] + +当前最优挂单 +接口描述 +返回当前最优的挂单(最高买单,最低卖单) + +HTTP请求 +GET /fapi/v1/ticker/bookTicker + +注意: 响应消息不包含RPI订单,其不可见。 + +请求权重 +单交易对2,无交易对5 + +请求参数 +名称 类型 是否必需 描述 +symbol STRING NO 交易对 +不发送交易对参数,则会返回所有交易对信息 +该接口返回头中的X-MBX-USED-WEIGHT-1M参数不准确,可以忽略 +响应示例 +{ + "symbol": "BTCUSDT", // 交易对 + "bidPrice": "4.00000000", //最优买单价 + "bidQty": "431.00000000", //挂单量 + "askPrice": "4.00000200", //最优卖单价 + "askQty": "9.00000000", //挂单量 + "time": 1589437530011 // 撮合引擎时间 +} + +或(当不发送symbol) + +[ + { + "symbol": "BTCUSDT", // 交易对 + "bidPrice": "4.00000000", //最优买单价 + "bidQty": "431.00000000", //挂单量 + "askPrice": "4.00000200", //最优卖单价 + "askQty": "9.00000000", //挂单量 + "time": 1589437530011 // 撮合引擎时间 + } +] + +