交易接口与行情数据获取:CTP接口简介、Python封装CTP接口、获取Tick级行情数据、行情数据缓存与预处理
做高频交易,第一关就是怎么跟交易所对上话。说白了,你得有个靠谱的接口,能拿到最原始、最实时的数据。国内期货市场,CTP(综合交易平台)就是那个绕不开的存在。我当年刚入行时,第一次接触CTP,看着那一堆C++的API文档,说实话有点懵。但用顺手之后你会发现,它其实很清晰,只是需要一点耐心去拆解。
CTP接口简介
CTP是上海期货信息技术有限公司开发的。它提供了交易和行情两套接口。交易接口负责下单、撤单、查持仓;行情接口负责推送实时数据。两者是独立的,你可以只接行情,不接交易,这在我们做策略回测或实时监控时很常见。
CTP的核心是回调机制。你发起一个请求,比如订阅某个合约,CTP不会立刻给你返回数据,而是等数据到了,主动调用你注册的回调函数。这种异步模式,在高频场景下非常关键——它不会阻塞你的主循环。
关键点:CTP的行情数据是Tick级别的。一个Tick包含最新价、成交量、持仓量、买卖盘口等。对于高频策略,Tick数据就是我们的原油。
我记得有一次,团队里新来的同事问:“为什么CTP的行情数据有时候会跳变?”其实不是跳变,是Tick本身就不连续。交易所只在有成交或挂单变化时才推送一个Tick。所以,两个Tick之间的时间间隔可能只有几毫秒,也可能长达几秒。这个特性,你在做数据预处理时一定要心里有数。
Python封装CTP接口
CTP原生是C++接口。直接用C++写策略当然可以,但开发效率低。我个人习惯用Python做快速原型,用C++做核心引擎。所以,把CTP封装成Python可调用的形式,是第一步。
常用的封装方式有两种:
- 使用ctp-python库:社区有人已经封装好了,比如
vnpy的底层,或者ctp这个pip包。直接pip install就能用,适合快速上手。 - 自己用Cython或pybind11封装:如果你需要极致性能,或者要定制某些行为,那就自己动手。我曾在项目中自己封装过一次,主要是为了控制内存分配和线程模型。
下面是一个简单的Python封装示例,展示如何初始化行情接口并订阅合约:
from ctp import MdApi
class MyMdApi(MdApi):
def __init__(self):
super().__init__()
self.connected = False
def OnFrontConnected(self):
print("行情前置连接成功")
self.connected = True
# 登录
req = {
"BrokerID": "9999",
"UserID": "your_user",
"Password": "your_pwd"
}
self.ReqUserLogin(req, 0)
def OnRtnDepthMarketData(self, data):
# 每个Tick都会回调这里
print(f"{data['InstrumentID']} 最新价: {data['LastPrice']}")
api = MyMdApi()
api.Create("tcp://180.168.146.187:10010", "tcp://180.168.146.187:10011")
api.SubscribeMarketData(["rb2401"])
小技巧:CTP的行情前置地址有多个,建议在代码里做自动切换。我曾经因为某个前置节点宕机,导致数据断了10分钟,后来加了心跳检测和自动重连,再也没出过问题。
获取Tick级行情数据
拿到Tick数据后,你会发现它比K线丰富得多。一个典型的Tick结构包含:
| 字段 | 含义 | 典型值 |
|---|---|---|
| InstrumentID | 合约代码 | rb2401 |
| LastPrice | 最新成交价 | 3982 |
| Volume | 当日累计成交量 | 1234567 |
| Turnover | 当日累计成交额 | 4920000000 |
| BidPrice1 | 买一价 | 3981 |
| AskPrice1 | 卖一价 | 3983 |
| BidVolume1 | 买一量 | 120 |
| AskVolume1 | 卖一量 | 85 |
为什么Tick数据这么重要?你想想看,K线是聚合后的结果,它丢失了微观结构。比如,一个1分钟K线收阳线,但内部可能经历了多次急涨急跌。只有Tick数据才能捕捉到这些细节。我做过一个统计:基于Tick的微观结构特征,对短线预测的准确率能提升15%以上。
获取Tick数据时,要注意时间戳的处理。CTP返回的时间是交易所时间,精确到秒,但Tick本身是毫秒级事件。所以,我一般会在本地记录一个高精度时间戳,用time.perf_counter_ns()来标记收到Tick的时刻。这样,即使交易所时间有延迟,我们也能知道数据到达本地的真实时间。
避坑指南:我曾经遇到过一个问题:CTP的Tick数据中,Volume字段是当日累计值,不是本Tick的成交量。如果你需要计算每个Tick的成交量,必须自己算差值。这个坑,新手很容易踩。
行情数据缓存与预处理
高频策略每秒可能收到几百个Tick。如果每个Tick都直接写入数据库或磁盘,I/O会成为瓶颈。所以,缓存是必须的。
我的做法是:在内存中维护一个环形缓冲区。每个合约对应一个缓冲区,大小固定,比如10000个Tick。满了就覆盖最旧的数据。这样,策略可以随时访问最近的历史Tick,而不用去查数据库。
预处理方面,我通常会做以下几件事:
- 去重:CTP偶尔会推送重复的Tick(比如网络重传)。用时间戳+价格+成交量做唯一键,去重。
- 计算衍生指标:比如买卖价差、成交量加权价格、Tick级别的波动率。这些指标在策略中直接使用,不用每次重复算。
- 对齐时间:不同合约的Tick到达时间不一致。我会用一个全局的时钟,把每个Tick打上统一的本地时间戳,方便后续做多合约分析。
下面是一个简单的缓存实现思路:
class TickBuffer:
def __init__(self, size=10000):
self.buffer = [None] * size
self.head = 0
self.count = 0
def add(self, tick):
self.buffer[self.head] = tick
self.head = (self.head + 1) % len(self.buffer)
if self.count < len(self.buffer):
self.count += 1
def get_latest(self, n=10):
# 获取最近n个Tick
if n > self.count:
n = self.count
start = (self.head - n) % len(self.buffer)
return [self.buffer[(start + i) % len(self.buffer)] for i in range(n)]
个人经验:缓存的大小要根据策略需求来定。如果策略只看最近100个Tick,那缓存设200就够了。别贪大,内存不是无限的。我见过有人把一天的所有Tick都放内存,结果程序跑着跑着就OOM了。
预处理还有一个容易被忽略的点:异常值过滤。比如,某个Tick的价格突然比前一个Tick高了10%,这很可能是数据错误。我会设置一个阈值,比如价格变化超过5%就丢弃,并记录日志。这样,策略就不会被脏数据带偏。
嗯,到这里,你应该对CTP接口和行情数据获取有了一个整体的认识。从C++原生接口到Python封装,再到Tick数据的获取、缓存和预处理,每一步都有细节。我个人觉得,预处理这一步最容易被忽视,但恰恰是它决定了策略的稳定性。数据干净了,策略才能跑得稳。