在量化策略的研发迭代中,我们团队经常面临一个刚性需求:基于非标准时间周期的K线开发因子或信号。比如外汇套利中常用的2.5分钟K线、A股ETF轮动中使用的32分钟K线,又或者数字货币高频策略所需的15秒K线。这些周期在各大平台的标准化API里基本上是缺失的,继续套用常规周期往往会导致信号偏移或过度滞后。
为了解决这个高频刚需,我们把行情数据源下沉到最底层的Tick成交明细,自建了一条能够生成任意周期K线的数据流水线。这里就将我们的设计思路和核心代码共享出来,欢迎大家探讨、拍砖。
客户需求:策略想用多少分钟,就该有多少分钟的K线
我们内部服务的“客户”其实就是策略研究员和实盘交易员。他们对自己策略所适应的行情周期有着非常精准的认知,比如某位同事的动量策略在38秒K线上表现极佳,一旦强行改成1分钟K线,胜率立刻下滑。对于这些个性化需求,市面上的通用行情接口几乎无能为力。
更深一层的要求是,回测环境和实盘环境使用的K线生成规则必须完全一致。我们自己合成K线,就能够保证从历史Tick和实时Tick产出的K线完全同构,彻底消除因数据源聚合方式不同而导致的回测过拟合风险。
投顾痛点:标准K线接口的三个致命缺陷
在用标准K线接口做策略的这几年里,我们总结了三个几乎无法绕开的痛点:
- 周期粒度固定,无法微调。1分钟、5分钟、15分钟的阶梯完全不能满足策略对最优周期的搜索。我们经常需要用参数优化算法寻找最佳K线周期,而周期只能是接口支持的那几个离散值。
- 聚合逻辑不透明。不同数据商对开盘价、收盘价的定义可能不同。比如有的以第一笔成交为开盘,有的以第一笔挂单为开盘。在换数据源时,K线形态会突变,直接导致策略信号失真。
- 时区与交易时段适配差。同一品种在不同交易所的交易时间不同,标准K线往往按UTC整点切割,完全不考虑本土开盘时间,这让基于A股集合竞价或美股盘前盘后的策略非常头疼。
这些痛点让我们下决心全量接入Tick数据,让K线生成规则完全由策略方定义。
数据支撑:从Tick到K线的精确映射关系
Tick数据是所有行情分析的基础粒子。它精确记录了每笔成交的价格、成交量和时间戳。K线则是特定时间粒度下的统计摘要,映射关系如下:
| 字段 | 计算方式 |
|---|---|
| 开盘价 | 周期内第一笔成交价格 |
| 最高价 | 周期内最高成交价格 |
| 最低价 | 周期内最低成交价格 |
| 收盘价 | 周期内最后一笔成交价格 |
| 成交量 | 周期内成交数量累加 |
只要我们能按时间窗口把所有Tick分流,就能准确计算任意周期的K线。例如合成3分钟K线,只需将时间戳按180秒对齐,聚合即可。
服务升级:一条覆盖回测与实盘的自研合成管道
1. 离线批处理:用于历史回测
核心是时间对齐与分组。我们采用取整法实现O(1)的窗口分配:
period = 60
bar_time = timestamp - (timestamp % period)
分组后用defaultdict收集Tick,再统一聚合。典型的批量合成代码如下:
from collections import defaultdict
ticks = [
{"time":1710000001,"price":100,"volume":2},
{"time":1710000010,"price":102,"volume":3},
{"time":1710000030,"price":101,"volume":1}
]
period = 60
bars = defaultdict(list)
for tick in ticks:
key = tick["time"] - (tick["time"] % period)
bars[key].append(tick)
for timestamp, data in bars.items():
prices = [item["price"] for item in data]
volumes = [item["volume"] for item in data]
print({
"open": prices[0],
"high": max(prices),
"low": min(prices),
"close": prices[-1],
"volume": sum(volumes)
})
针对海量Tick数据,我们使用迭代器分段读取,并将生成的K线直接存入时序数据库(如DolphinDB或ClickHouse),供策略引擎快速拉取。
2. 实时推送:用于实盘低延迟合成
实盘中,我们依赖WebSocket协议从低延迟行情接口实时获取Tick流。例如接入AllTick实时行情,连接代码大致如下:
import websocket
url = "wss://quote.alltick.co/socket.io"
ws = websocket.create_connection(url)
ws.send('{"cmd":"subscribe","symbol":"BTCUSDT"}')
while True:
data = ws.recv()
print(data)
当Tick流进入系统后,我们会为每个关注的周期维护一个当前K线对象。每来一笔Tick,根据时间戳判断窗口归属:
- 若属于当前窗口:动态更新最高价、最低价、成交量;
- 若已跨入下一窗口:封装当前K线推送到策略,并初始化新窗口对象。
整个处理逻辑完全运行在内存中,采用无锁结构,实测从Tick到达到K线信号发出延迟可稳定在1毫秒以内,完全满足中高频策略的需求。
几个必须直面的技术细节
- 空窗口填充:回测中我们通常将空K线的开盘、收盘均设置为上一周期收盘价,成交量设为零,以保证时间序列完整;实盘则根据策略需求选择性下发。
- 成交量累积验证:首次接入新交易所Tick数据时,必须与官方公布的日成交量进行交叉比对,以防单笔/累计成交量混淆。
- 时间戳标准统一:所有时间戳在进入系统时一律转为毫秒级UTC,杜绝时区、夏令时带来的边界错误。
实战感悟
从依赖标准接口到自己掌控Tick合成K线,表面上看是多了一些代码量,但其带来的策略自由度和数据可靠性是质的飞跃。我们可以在参数优化时真正搜索连续的K线周期维度,而不再被几个离散选项束缚。
更重要的是,拥有了这条数据管道后,策略迁移到新的交易所、新的资产大类时,只需要更换Tick数据源,合成逻辑完全复用,开发效率大幅提升。如果你也正打算在量化系统上做深度定制,强烈建议把Tick合成K线作为基础设施的第一步。


