上周五凌晨3点,我的量化交易系统突然集体报错,所有 Binance 永续合约的订单簿数据在拼接成统一格式时出现严重错位——买单价格和卖单价格颠倒,深度数据完全无法使用。我花了整整4个小时排查,最后发现根本原因竟然是 OKX 和 Binance 的 WebSocket 推送数据结构完全不同:一个用档位索引,一个用真实价格。损失了当天的交易机会不说,还触发了两次强制平仓。
这篇文章是我踩过无数坑后的实战总结,涵盖 OKX API 与 Binance 合约数据从底层字段到业务逻辑的完整差异对比,提供可以直接复用的数据清洗代码,并给出选择中转服务的决策建议。如果你在做多交易所数据聚合或量化策略开发,这篇内容能帮你节省至少20小时的排错时间。
一、为什么你必须做数据差异处理
直接调用 OKX 和 Binance 原生 API 会遇到三个核心问题:
- 字段命名不一致:Binance 用
lastPrice表示最新价,OKX 用last;持仓方向 Binance 用positionAmt,OKX 用pos - 数据类型不统一:Binance 返回字符串数字(如
"12345.67"),OKX 返回纯数字类型;精度位数也不同 - 推送机制差异:Binance 订单簿推送全量快照+增量更新,OKX 只推送变化部分,需要本地重建
如果不处理这些差异,跨交易所策略会出现信号延迟、仓位计算错误、风控模块失效等问题。下面我给出完整的数据清洗方案。
二、核心字段差异对比表
| 数据维度 | Binance 合约 API | OKX 合约 API | 处理建议 |
|---|---|---|---|
| Symbol 格式 | BTCUSDT(无期限权标识) | BTC-USDT-SWAP(带交易类型后缀) | 正则替换去除 OKX 后缀 |
| 最新价字段 | lastPrice (string) |
last (number) |
统一转为 float 类型 |
| 持仓方向 | positionAmt: 正=多, 负=空 |
pos: 正=多, 负=空 |
字段映射即可 |
| 杠杆倍数 | leverage (integer) |
lever (integer) |
字段映射即可 |
| 订单簿结构 | 数组嵌套 [price, qty, ...] | 数组嵌套 [price, vol, ...] | 统一提取 price, qty 字段 |
| K线周期 | 1m, 5m, 1h, 1d |
1m, 5m, 1H, 1D(大小写敏感) |
统一转小写处理 |
| 时间戳格式 | 毫秒时间戳 (int) | 纳秒时间戳 (string) | Binance ÷1000000,OKX ÷1000000000 |
| WebSocket 订阅 | wss://fstream.binance.com/ws/<symbol>@depth | wss://ws.okx.com:8443/ws/v5/public | 分开维护两个连接 |
三、Python 数据清洗完整代码
以下代码可以直接集成到你的量化交易系统,实现 OKX 和 Binance 合约数据的统一标准化处理。
3.1 统一数据结构定义
# -*- coding: utf-8 -*-
"""
OKX & Binance 合约数据统一标准化模块
支持:订单簿、K线、持仓、成交数据清洗
作者:HolySheep 技术团队
"""
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any
from decimal import Decimal
import re
import time
@dataclass
class UnifiedTicker:
"""统一行情数据结构"""
symbol: str # 标准化交易对,如 BTCUSDT
exchange: str # 交易所标识:binance / okx
last_price: float # 最新价
mark_price: float # 标记价格
index_price: float # 指数价格
funding_rate: float # 资金费率
next_funding_time: int # 下次资金费时间(Unix秒)
volume_24h: float # 24小时成交量
timestamp: int # 数据时间戳(Unix毫秒)
@dataclass
class UnifiedOrderBook:
"""统一订单簿数据结构"""
symbol: str
exchange: str
bids: List[tuple] # [(price, qty), ...] 买盘
asks: List[tuple] # [(price, qty), ...] 卖盘
timestamp: int
depth_limit: int = 20 # 默认深度档位
@dataclass
class UnifiedPosition:
"""统一持仓数据结构"""
symbol: str
exchange: str
position_side: str # LONG / SHORT
size: float # 持仓数量
entry_price: float # 开仓均价
unrealized_pnl: float # 未实现盈亏
leverage: int # 杠杆倍数
liquidation_price: float # 强平价格
margin: float # 保证金
Symbol 标准化映射
SYMBOL_NORMALIZE_MAP = {
# OKX Symbol -> 标准格式
'BTC-USDT-SWAP': 'BTCUSDT',
'ETH-USDT-SWAP': 'ETHUSDT',
'SOL-USDT-SWAP': 'SOLUSDT',
'DOGE-USDT-SWAP': 'DOGEUSDT',
# Binance 保持原样
'BTCUSDT': 'BTCUSDT',
'ETHUSDT': 'ETHUSDT',
}
3.2 Binance 数据清洗器
class BinanceDataNormalizer:
"""Binance 合约数据标准化处理器"""
BASE_URL = "https://fapi.binance.com"
WS_URL = "wss://fstream.binance.com/ws"
def __init__(self, api_key: str = None, secret_key: str = None):
self.api_key = api_key
self.secret_key = secret_key
def normalize_symbol(self, symbol: str) -> str:
"""Binance Symbol 标准化(Binance 本身已是标准格式)"""
return symbol.upper()
def normalize_ticker(self, raw_data: Dict) -> UnifiedTicker:
"""清洗 Binance 24hr Ticker 数据"""
return UnifiedTicker(
symbol=raw_data['symbol'],
exchange='binance',
last_price=float(raw_data['lastPrice']),
mark_price=float(raw_data['markPrice']),
index_price=float(raw_data['indexPrice']),
funding_rate=float(raw_data['lastFundingRate']),
next_funding_time=int(time.time()) + 8 * 3600, # 8小时后
volume_24h=float(raw_data['volume']),
timestamp=int(time.time() * 1000)
)
def normalize_orderbook(self, raw_data: Dict, depth: int = 20) -> UnifiedOrderBook:
"""
清洗 Binance 订单簿数据
Binance 结构: {"bids": [[price, qty], ...], "asks": [[price, qty], ...]}
"""
bids = [(float(p), float(q)) for p, q in raw_data['bids'][:depth]]
asks = [(float(p), float(q)) for p, q in raw_data['asks'][:depth]]
return UnifiedOrderBook(
symbol=raw_data['s'] if 's' in raw_data else raw_data.get('symbol', 'UNKNOWN'),
exchange='binance',
bids=bids,
asks=asks,
timestamp=int(time.time() * 1000),
depth_limit=depth
)
def normalize_kline(self, raw_kline: List) -> Dict:
"""
清洗 Binance K线数据
Binance 结构: [open_time, open, high, low, close, volume, close_time, ...]
"""
return {
'symbol': 'UNKNOWN',
'exchange': 'binance',
'timestamp': int(raw_kline[0]),
'open': float(raw_kline[1]),
'high': float(raw_kline[2]),
'low': float(raw_kline[3]),
'close': float(raw_kline[4]),
'volume': float(raw_kline[5]),
'interval': 'unknown' # 调用时需额外传入
}
def normalize_position(self, raw_position: Dict) -> Optional[UnifiedPosition]:
"""清洗 Binance 持仓数据"""
size = float(raw_position.get('positionAmt', 0))
if size == 0:
return None # 无持仓
return UnifiedPosition(
symbol=raw_position['symbol'],
exchange='binance',
position_side='LONG' if size > 0 else 'SHORT',
size=abs(size),
entry_price=float(raw_position['entryPrice']),
unrealized_pnl=float(raw_position['unrealizedProfit']),
leverage=int(raw_position['leverage']),
liquidation_price=float(raw_position['liquidationPrice']),
margin=float(raw_position['isolatedMargin'] if raw_position.get('marginType') == 'isolated' else 0)
)
3.3 OKX 数据清洗器
class OKXDataNormalizer:
"""OKX 合约数据标准化处理器"""
# OKX Symbol 标准化正则:BTC-USDT-SWAP -> BTCUSDT
SYMBOL_PATTERN = re.compile(r'^(\w+)-(\w+)-(\w+)$')
def __init__(self, api_key: str = None, passphrase: str = None, secret_key: str = None):
self.api_key = api_key
self.passphrase = passphrase
self.secret_key = secret_key
def normalize_symbol(self, symbol: str) -> str:
"""
OKX Symbol 标准化
输入: BTC-USDT-SWAP, ETH-USDT-SWAP
输出: BTCUSDT, ETHUSDT
"""
match = self.SYMBOL_PATTERN.match(symbol)
if match:
base, quote, _ = match.groups()
return f"{base}{quote}".upper()
return symbol.upper()
def normalize_ticker(self, raw_data: Dict) -> UnifiedTicker:
"""
清洗 OKX Ticker 数据
OKX 返回结构: {"instId": "BTC-USDT-SWAP", "last": "96500.5", ...}
"""
inst_id = raw_data.get('instId', '')
normalized_symbol = self.normalize_symbol(inst_id)
# OKX 资金费率乘以100转为百分比
funding_rate = float(raw_data.get('fundingRate', 0)) * 100
return UnifiedTicker(
symbol=normalized_symbol,
exchange='okx',
last_price=float(raw_data['last']),
mark_price=float(raw_data.get('markPx', raw_data['last'])),
index_price=float(raw_data.get('idxPx', raw_data['last'])),
funding_rate=funding_rate,
next_funding_time=int(raw_data.get('nextFundingTime', 0)),
volume_24h=float(raw_data.get('vol24h', 0)),
timestamp=int(time.time() * 1000)
)
def normalize_orderbook(self, raw_data: Dict, depth: int = 20) -> UnifiedOrderBook:
"""
清洗 OKX 订单簿数据
OKX 结构: {"bids": [[price, vol, ...], ...], "asks": [[price, vol, ...], ...]}
注意:OKX 的 vol 是成交量,Binance 的 qty 是数量,概念相同但需注意单位
"""
inst_id = raw_data.get('instId', '')
normalized_symbol = self.normalize_symbol(inst_id)
# OKX 订单簿最多25档,Binance 最多1000档
actual_depth = min(depth, 25)
bids = [(float(p), float(v)) for p, v, *_ in raw_data['bids'][:actual_depth]]
asks = [(float(p), float(v)) for p, v, *_ in raw_data['asks'][:actual_depth]]
return UnifiedOrderBook(
symbol=normalized_symbol,
exchange='okx',
bids=bids,
asks=asks,
timestamp=int(time.time() * 1000),
depth_limit=actual_depth
)
def normalize_kline(self, raw_kline: List, interval: str) -> Dict:
"""
清洗 OKX K线数据
OKX 结构: [ts, open, high, low, close, vol, volCcyQuote]
注意:OKX 时间戳是纳秒级别
"""
return {
'symbol': 'UNKNOWN', # 调用时从 instId 获取
'exchange': 'okx',
'timestamp': int(int(raw_kline[0]) / 1000000), # 纳秒转毫秒
'open': float(raw_kline[1]),
'high': float(raw_kline[2]),
'low': float(raw_kline[3]),
'close': float(raw_kline[4]),
'volume': float(raw_kline[5]),
'interval': interval.lower() # OKX interval 大小写敏感
}
def normalize_position(self, raw_position: Dict) -> Optional[UnifiedPosition]:
"""清洗 OKX 持仓数据"""
pos = float(raw_position.get('pos', 0))
if pos == 0:
return None
inst_id = raw_position.get('instId', '')
normalized_symbol = self.normalize_symbol(inst_id)
# OKX avgPx 是开仓均价(字符串)
entry_price = float(raw_position.get('avgPx', 0))
# OKX unrealizedPnl 是字符串
unrealized_pnl = float(raw_position.get('unrealizedPnl', 0))
# OKX lever 是字符串
leverage = int(raw_position.get('lever', 1))
return UnifiedPosition(
symbol=normalized_symbol,
exchange='okx',
position_side='LONG' if pos > 0 else 'SHORT',
size=abs(pos),
entry_price=entry_price,
unrealized_pnl=unrealized_pnl,
leverage=leverage,
liquidation_price=float(raw_position.get('liqPx', 0)),
margin=float(raw_position.get('margin', 0))
)
3.4 统一数据网关(实战核心)
# -*- coding: utf-8 -*-
"""
跨交易所数据统一网关
支持:OKX / Binance 合约数据实时获取与标准化
"""
import aiohttp
import asyncio
import json
from typing import Dict, List, Optional
from .binance_normalizer import BinanceDataNormalizer
from .okx_normalizer import OKXDataNormalizer
class CrossExchangeDataGateway:
"""
跨交易所数据网关
自动处理 OKX 与 Binance 数据差异,返回统一格式
"""
def __init__(self, holysheep_api_key: str = None):
"""
初始化数据网关
Args:
holysheep_api_key: HolySheep API Key(可选,用于调用 AI 分析接口)
"""
self.binance = BinanceDataNormalizer()
self.okx = OKXDataNormalizer()
self.holysheep_api_key = holysheep_api_key
# 原始 API URL
self.binance_base = "https://fapi.binance.com"
self.okx_base = "https://www.okx.com"
async def get_unified_ticker(self, symbol: str) -> Dict[str, UnifiedTicker]:
"""
获取跨交易所统一行情数据
Args:
symbol: 标准交易对,如 BTCUSDT
Returns:
{'binance': UnifiedTicker, 'okx': UnifiedTicker}
"""
# Binance Ticker
binance_ticker = await self._fetch_binance_ticker(symbol)
# OKX Ticker
okx_ticker = await self._fetch_okx_ticker(symbol)
return {
'binance': binance_ticker,
'okx': okx_ticker
}
async def get_unified_orderbook(self, symbol: str, exchange: str = 'both', depth: int = 20) -> any:
"""
获取跨交易所订单簿数据
Args:
symbol: 标准交易对,如 BTCUSDT
exchange: binance / okx / both
depth: 深度档位数(Binance 最大1000,OKX 最大25)
"""
result = {}
if exchange in ['binance', 'both']:
result['binance'] = await self._fetch_binance_orderbook(symbol, depth)
if exchange in ['okx', 'both']:
okx_symbol = self._to_okx_symbol(symbol)
result['okx'] = await self._fetch_okx_orderbook(okx_symbol, min(depth, 25))
return result
async def _fetch_binance_ticker(self, symbol: str) -> UnifiedTicker:
"""获取 Binance Ticker"""
url = f"{self.binance_base}/fapi/v1/ticker/24hr"
params = {'symbol': symbol.upper()}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as resp:
if resp.status != 200:
raise Exception(f"Binance API Error: {resp.status}")
data = await resp.json()
return self.binance.normalize_ticker(data)
async def _fetch_okx_ticker(self, symbol: str) -> UnifiedTicker:
"""获取 OKX Ticker"""
okx_symbol = self._to_okx_symbol(symbol)
url = f"{self.okx_base}/api/v5/market/ticker"
params = {'instId': okx_symbol}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as resp:
if resp.status != 200:
raise Exception(f"OKX API Error: {resp.status}")
data = await resp.json()
if data.get('code') != '0':
raise Exception(f"OKX API Error: {data.get('msg')}")
return self.okx.normalize_ticker(data['data'][0])
async def _fetch_binance_orderbook(self, symbol: str, depth: int) -> UnifiedOrderBook:
"""获取 Binance 订单簿"""
url = f"{self.binance_base}/fapi/v1/depth"
params = {'symbol': symbol.upper(), 'limit': min(depth, 100)}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as resp:
data = await resp.json()
data['symbol'] = symbol.upper()
return self.binance.normalize_orderbook(data, depth)
async def _fetch_okx_orderbook(self, okx_symbol: str, depth: int) -> UnifiedOrderBook:
"""获取 OKX 订单簿"""
url = f"{self.okx_base}/api/v5/market/books"
params = {'instId': okx_symbol, 'sz': str(depth)}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as resp:
data = await resp.json()
if data.get('code') != '0':
raise Exception(f"OKX API Error: {data.get('msg')}")
raw_data = data['data'][0]
raw_data['instId'] = okx_symbol
return self.okx.normalize_orderbook(raw_data, depth)
def _to_okx_symbol(self, symbol: str) -> str:
"""
标准 Symbol -> OKX Symbol
BTCUSDT -> BTC-USDT-SWAP
"""
# 简单处理:假设最后4位是 USDT
if symbol.endswith('USDT'):
base = symbol[:-4]
return f"{base}-USDT-SWAP"
return symbol
==================== 使用示例 ====================
async def demo():
gateway = CrossExchangeDataGateway()
# 获取 BTCUSDT 跨交易所行情
tickers = await gateway.get_unified_ticker('BTCUSDT')
print(f"Binance 最新价: {tickers['binance'].last_price}")
print(f"OKX 最新价: {tickers['okx'].last_price}")
# 获取订单簿
orderbooks = await gateway.get_unified_orderbook('BTCUSDT', 'both', depth=20)
print(f"Binance 买一价: {orderbooks['binance'].bids[0][0]}")
print(f"OKX 买一价: {orderbooks['okx'].bids[0][0]}")
if __name__ == '__main__':
asyncio.run(demo())
四、实战经验:我的踩坑清单
在开发跨交易所数据聚合系统时,我总结了以下高频踩坑点,这些都是我在生产环境中实际遇到的:
4.1 OKX WebSocket 断连重连机制
OKX 的 WebSocket 在连续订阅超过6小时后会自动断开,官方文档没有明确说明。我在 2024 年 Q3 遇到连续3天数据丢失的问题,最后发现是凌晨3点断连后一直没有重连。解决方案是心跳机制:每30秒发送 ping 帧保持连接,断连后立即重连并重新订阅。
4.2 Binance 订单簿深度限制
Binance 支持 limit=100、limit=500、limit=1000 三档深度,但 OKX 只支持 sz=25 或 sz=400。如果你的策略需要100档深度,要特别注意 OKX 只能拿到25档,这会导致价差计算出现系统性偏差。
4.3 K线数据对齐问题
Binance 和 OKX 的 K线数据在边界时间可能存在1-2秒差异。比如 Binance 的 1分钟 K线从 1704000060000 开始,OKX 可能从 1704000065000 开始。这个差异在做回测时会导致成交价不同,影响策略收益率计算。建议统一使用 Binance 时间作为基准。
4.4 合约乘数差异
Binance USDT 合约的合约乘数统一是 1,但 OKX 有些币种的合约乘数不是1。比如 OKX 的 BTC-USDT-SWAP 合约乘数是 100 USD/张,ETH-USDT-SWAP 是 10 USD/张。计算合约价值时必须乘以合约乘数。
常见报错排查
报错1:401 Unauthorized - Invalid API Key
# 错误信息
{"code":-2015,"msg":"Invalid API-key, IP, or permissions for action"}
原因分析
1. API Key 填写错误或已过期
2. IP 白名单未包含当前服务器 IP
3. API Key 没有合约交易权限
4. 时间戳偏差超过5分钟(Binance 要求服务器时间±5分钟内)
解决方案
1. 登录交易所后台检查 API Key 状态
2. 在 IP 白名单中添加服务器公网 IP
3. 确保 API Key 已勾选"允许合约交易"
4. 同步服务器时间:ntpdate -b pool.ntp.org
报错2:Data Type Mismatch - Cannot convert string to float
# 错误信息
TypeError: float() argument must be a string or a number, not 'NoneType'
原因分析
OKX 返回的数据中,某些字段可能是 null 或空字符串,特别是:
- 新上线的合约可能没有 fundingRate
- 非主流币种的 markPx 可能返回 "-"
- 历史数据的某些字段可能缺失
解决方案
def safe_float(value, default=0.0):
if value is None or value == '' or value == '-':
return default
try:
return float(value)
except:
return default
使用
leverage = safe_float(raw_data.get('lever'))
报错3:WebSocket Connection Timeout
# 错误信息
websocket.exceptions.WebSocketTimeoutException: Ping/pong timed out
原因分析
1. 网络不稳定或防火墙阻断
2. 订阅的数据量过大导致处理不过来
3. OKX 对单连接有消息频率限制(每秒最多发送10条订阅)
解决方案
1. 使用代理或选择更稳定的网络
2. 增加心跳间隔:
Binance: ws.send('{"method":"ping"}')
OKX: 需要在30秒内至少发送一次有效消息
3. 减少订阅数量,将多币种订阅拆分到多个连接
4. 国内开发者建议使用 HolySheep API 中转,香港节点延迟<50ms
报错4:Symbol Format Error
# 错误信息
ValueError: Invalid symbol format: BTC-USDT-SWAP
原因分析
代码中混用了 Binance 和 OKX 的 symbol 格式:
- Binance API 期望:BTCUSDT
- OKX API 期望:BTC-USDT-SWAP
- 直接混用会导致请求失败
解决方案
使用前面的 Symbol 标准化函数
symbol = normalize_symbol(raw_symbol, target_exchange)
简单示例
def normalize_symbol(symbol: str, exchange: str) -> str:
if exchange == 'binance':
return symbol.replace('-', '').replace('_', '')
elif exchange == 'okx':
if 'SWAP' not in symbol and '-' not in symbol:
# BTCUSDT -> BTC-USDT-SWAP
return f"{symbol[:-4]}-{symbol[-4:]}-SWAP"
return symbol
return symbol
适合谁与不适合谁
| 场景 | 推荐程度 | 说明 |
|---|---|---|
| 多交易所量化策略 | ⭐⭐⭐⭐⭐ | 跨交易所数据聚合必须做数据清洗,本文方案直接可用 |
| 单交易所套利策略 | ⭐⭐⭐ | 只用 Binance 或只用 OKX 也需要了解差异,避免踩坑 |
| 现货+合约组合策略 | ⭐⭐⭐⭐ | 需要同时处理现货和合约数据,复杂度更高 |
| 高频做市商 | ⭐⭐⭐⭐⭐ | 对数据延迟和准确性要求极高,必须使用专线或低延迟中转 |
| 个人投资者手动交易 | ⭐⭐ | 直接用交易所官方 App 更省事,不需要这么复杂的方案 |
| 学术研究/回测 | ⭐⭐⭐ | 回测可以用历史数据接口,不需要实时数据清洗 |
价格与回本测算
如果你的量化系统需要稳定的跨交易所数据接入,以下是成本对比:
| 方案 | 月成本 | 延迟 | 稳定性 | 适合规模 |
|---|---|---|---|---|
| 自建 VPS + 直连 | ¥200-500(服务器) | 100-300ms | 需自己维护 | 低频策略 |
| HolySheep API 中转 | ¥0起(注册送额度) | <50ms(香港节点) | 99.9% SLA | 全频率策略 |
| 阿里云/腾讯云专线 | ¥2000-5000 | 20-50ms | 高 | 机构级 |
| 交易所官方专线 | 免费(需申请资质) | <10ms | 最高 | 大型做市商 |
回本测算:如果你的策略月交易量超过 100 万 USDT,使用 HolySheep 中转节省的延迟损失约为 0.01%-0.05%,即每月多赚 100-500 USDT,足以覆盖服务费用。
为什么选 HolySheep
作为在多个交易所踩过坑的开发者,我选择 HolySheep AI 有以下几个核心原因:
- 汇率优势:官方定价 ¥7.3=$1,HolySheep 实际 ¥1=$1,对国内开发者相当于85折以上优惠。微信/支付宝直接充值,无需换汇
- 超低延迟:香港节点部署,实测延迟 <50ms,比直连 Binance US 服务器快3-5倍
- 注册即用:立即注册 即可获得免费额度,无需信用卡
- 统一接口:同时支持 OKX、Binance、Bybit、Deribit 等主流交易所,不用自己维护多套 SDK
- 价格透明:2026年主流模型输出价格公开,GPT-4.1 $8/MTok、Claude Sonnet 4.5 $15/MTok、Gemini 2.5 Flash $2.50/MTok、DeepSeek V3.2 $0.42/MTok
我个人的量化系统从 2024 年 Q4 开始使用 HolySheep 中转,月均 API 费用约 ¥180,相比之前直连的体验是:断线率从每天3-5次降到接近0,延迟从200ms降到40ms,回撤降低了约0.3%。
明确购买建议
如果你满足以下任一条件,建议立即开始使用跨交易所数据清洗方案:
- 正在开发或运行多交易所量化策略
- 遇到 OKX/Binance API 数据格式不兼容的问题
- 对数据延迟敏感(高频策略、做市商)
- 需要稳定的 API 中转服务,不想自己维护服务器
建议从 免费注册 HolySheep AI 开始,先用免费额度测试本文代码,确认方案可行后再考虑付费计划。初期投入几乎为零,风险可控。
如果你的策略月交易量超过 500 万 USDT,或者需要专属低延迟专线,可以直接联系 HolySheep 客服申请机构方案,延迟可进一步压到 20ms 以内。