原始数据解析:JSON/CSV解析、时间戳标准化、价格与数量精度处理、缺失值识别
做量化交易的朋友都知道,订单簿数据是咱们吃饭的家伙。但说实话,交易所给过来的原始数据,那叫一个「原生态」——各种格式混着来,时间戳五花八门,精度问题能让你算错一个亿。今天我就把这几年的血泪经验掰开揉碎,跟你聊聊怎么把这堆「毛坯房」数据,装修成能直接用的「精装房」。
一、JSON与CSV解析:两种格式的「脾气」你得摸透
交易所的数据格式,说白了就两派:JSON派和CSV派。我个人习惯是,高频场景用CSV,灵活场景用JSON。为什么?你想想看,CSV解析起来快,但字段顺序一换就崩;JSON灵活,但解析开销大。
先看JSON解析。交易所返回的订单簿快照,通常是这样的:
{
"bids": [
["100.50", "2.0"],
["100.45", "5.0"]
],
"asks": [
["100.55", "3.0"],
["100.60", "1.5"]
],
"timestamp": 1623456789.123
}
这里有个坑——价格和数量都是字符串!我在项目中遇到过,直接当浮点数解析,结果精度丢了,算出来的订单簿深度差了十万八千里。正确的做法是:
import json
from decimal import Decimal
def parse_orderbook(json_str):
data = json.loads(json_str)
bids = [(Decimal(price), Decimal(qty)) for price, qty in data['bids']]
asks = [(Decimal(price), Decimal(qty)) for price, qty in data['asks']]
return bids, asks, data['timestamp']
CSV解析呢?交易所给的CSV文件,字段顺序你得先确认。我吃过一次亏,以为第一列是时间戳,结果是对价。后来我学乖了,先读表头:
import csv
from decimal import Decimal
def parse_csv(file_path):
with open(file_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
price = Decimal(row['price'])
qty = Decimal(row['quantity'])
timestamp = float(row['timestamp'])
# 处理逻辑...
二、时间戳标准化:把「北京时间」和「Unix时间」统一起来
时间戳这玩意儿,真是让人头大。有的交易所给Unix毫秒,有的给秒,还有的给带时区的字符串。我记得有一次,把毫秒当秒用了,结果回测出来的策略收益翻了三倍——嗯,那是假的。
标准化的思路其实很简单:统一转成Unix毫秒整数。为什么用毫秒?因为订单簿的更新频率通常是毫秒级的,用秒的话,同一秒内的多个更新就撞车了。
import pandas as pd
from datetime import datetime, timezone
def normalize_timestamp(ts, source_format='unix_ms'):
if source_format == 'unix_ms':
return int(ts)
elif source_format == 'unix_s':
return int(ts * 1000)
elif source_format == 'iso':
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
return int(dt.timestamp() * 1000)
else:
raise ValueError(f"未知的时间戳格式: {source_format}")
这里要注意时区问题。我曾经遇到一个交易所,给的ISO时间戳是UTC+8,但文档里写的是UTC。后来我统一用UTC+0做内部存储,展示的时候再转成本地时间。你想想看,如果策略里混着两个时区的时间,回测结果能准吗?
三、价格与数量精度处理:小数点后几位,决定了你的盈亏
做订单簿清洗,精度问题是绕不开的坎。比特币价格动辄几万美金,小数点后两位就够用?不对,有些交易所支持到小数点后八位。而数量呢,有的币种最小交易单位是0.0001个。
我的处理原则是:用Decimal,不用float。float的二进制浮点表示,在累加和比较时会有精度误差。你想想看,如果订单簿里几万个订单,每个都差一点点,最后算出来的买卖盘口能准吗?
from decimal import Decimal, ROUND_HALF_UP
def round_price(price, tick_size='0.01'):
"""按最小价格变动单位四舍五入"""
tick = Decimal(tick_size)
return (Decimal(price) / tick).quantize(Decimal('1'), rounding=ROUND_HALF_UP) * tick
def round_qty(qty, lot_size='0.001'):
"""按最小交易量单位四舍五入"""
lot = Decimal(lot_size)
return (Decimal(qty) / lot).quantize(Decimal('1'), rounding=ROUND_HALF_UP) * lot
这里有个细节:交易所的tick_size和lot_size,你得从交易所的「交易对信息」接口获取。我见过有人硬编码0.01,结果遇到一个0.001 tick_size的币种,所有价格都算错了。
| 交易所 | BTC/USDT tick_size | BTC/USDT lot_size |
|---|---|---|
| Binance | 0.01 | 0.00001 |
| OKX | 0.1 | 0.0001 |
| Coinbase | 0.01 | 0.00001 |
四、缺失值识别:那些「消失」的订单去哪了?
订单簿数据里,缺失值是个大问题。为什么会有缺失?可能是网络抖动丢包了,可能是交易所的撮合引擎没来得及推送,也可能是数据采集程序自己挂了。
我总结了几种常见的缺失模式:
- 整行缺失:某个时间点的快照完全没收到。这时候我会用前一个快照填充,但标记为「插值数据」。
- 部分字段缺失:比如价格有,但数量是空。这种情况我直接丢弃该行,因为数量缺失的订单没法用。
- 价格/数量为0:有些交易所用0表示「已成交」或「已撤单」。这不算缺失,但需要特殊处理。
import pandas as pd
import numpy as np
def detect_missing(df):
"""识别缺失值并分类"""
report = {}
# 整行缺失:检查时间戳是否连续
expected_ts = pd.date_range(start=df['ts_normalized'].min(),
end=df['ts_normalized'].max(), freq='100ms')
missing_ts = expected_ts.difference(df['ts_normalized'])
report['missing_timestamps'] = len(missing_ts)
# 字段缺失
for col in ['price', 'quantity']:
null_count = df[col].isnull().sum()
zero_count = (df[col] == 0).sum()
report[f'{col}_null'] = null_count
report[f'{col}_zero'] = zero_count
return report
我曾经遇到过一个奇葩情况:某个交易所的订单簿,在价格剧烈波动时,会「漏掉」中间几个价格档位的订单。后来发现是他们的推送机制有bug,只推送变化的部分,但变化太快时丢包了。解决方案是:用前后两个快照做差值,把缺失的档位补上。
知识体系总览
说了这么多,咱们用一张图把整个流程串起来。这张图是我做数据清洗时的「导航地图」,每次拿到新数据,先按这个流程走一遍:
这张图把整个流程分成了五个步骤:数据输入、格式解析、时间戳标准化、精度处理、缺失值识别。每一步都有对应的技术方案和注意事项。你下次拿到新数据,可以照着这个流程走一遍,基本不会漏掉关键环节。
好了,关于原始数据解析的核心内容就这些。记住一句话:数据清洗花的时间,会在后续建模和回测中十倍百倍地还回来。别嫌麻烦,每一步都做到位,后面才能睡得安稳。