feat(binance_client, position_manager): 优化价格获取逻辑与异常处理

在 `binance_client` 中引入 K线和最优挂单的 WebSocket 流,优先从缓存中获取价格数据,减少对 REST API 的依赖。同时,更新了价格获取逻辑,确保在未能获取价格时提供详细的错误信息。增强了异常处理,确保在请求超时或失败时记录相关日志,提升系统的稳定性和可追溯性。
This commit is contained in:
薇薇安 2026-02-16 17:11:25 +08:00
parent dfbdfee596
commit 30f4a22fb4
10 changed files with 1699 additions and 51 deletions

View File

@ -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}")

View File

@ -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:
估算滑点USDTNone 表示无法估算
"""
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: %s10s 后重连",
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}")

View File

@ -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'))

View File

@ -0,0 +1,256 @@
"""
K线 WebSocket 订阅 <symbol>@kline_<interval>维护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: %s10s 后重连",
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

View File

@ -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("程序已退出")

View File

@ -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" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损")

338
ws交易接口.txt Normal file
View File

@ -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
}
]
}

318
ws行情推送.txt Normal file
View File

@ -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 Urlwss://fstream.binance.com
订阅单一stream格式为 /ws/<streamName>
组合streams的URL格式为 /stream?streams=/<streamName1>/<streamName2>/<streamName3>
连接样例:
wss://fstream.binance.com/ws/bnbusdt@aggTrade
wss://fstream.binance.com/stream?streams=bnbusdt@aggTrade/btcusdt@markPrice
订阅组合streams时事件payload会以这样的格式封装 {"stream":"<streamName<","data":<rawPayload>}
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
<symbol>@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
<symbol>@kline_<interval>
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
<pair>_<contractType>@continuousKline_<interval>
更新速度
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" // 忽略此参数
}
}

206
行情ws接口.txt Normal file
View File

@ -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
}
]
}

243
行情接口REST.txt Normal file
View File

@ -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 // 撮合引擎时间
}
]