TqSdk2 与 VNPY 有哪些差别

TqSdk 与 VNPY 有非常多的差别. 如果您是一位有经验的 VNPY 用户, 刚开始接触 TqSdk, 下面的信息将帮助您尽快理解 TqSdk.

系统整体架构

VNPY 是一套 all-in-one 的结构, 在一个Python软件包中包含了数据库, 行情接收/存储, 交易接口, 图形界面等功能.

TqSdk2 则使用基于网络协作的组件设计. 如下图:

交易中继网关
交易中继网关
交易所行情
交易所行情
行情网关
Open Md Gateway
行情网关 Open Md Gateway
DIFF 协议
TqSdk
TqSdk
期货公司交易系统
CTP/FEMAS/UFX
期货公司交易系统 CTP/FEMAS/UFX
Viewer does not support full SVG 1.1

如图所示, 整个系统结构包括这些关键组件:

  • 行情网关 (Open Md Gateway) 负责提供实时行情和历史数据

  • 交易中继网关 (Open Trade Gateway) 负责连接到期货公司交易系统

  • 上面两个网关统一以 Diff 协议对下方提供服务

  • TqSdk2 按照Diff协议连接到行情网关和交易中继网关, 实现行情和交易功能

这样的结构可以给用户带来一些好处:

  • TqSdk2 很小, 安装也很方便, 只要简单 pip install tqsdk2 即可

  • 官方专门运维行情数据库, 用户可以直接使用, 不需要自己接收和存储数据

  • 交易相关接口被大幅度简化, 不再需要处理 CTP 接口的复杂回调, 也不需要发起任何查询请求

  • 任何语言只要支持 websocket 协议, 都可以用来进行策略开发

同时对于速度更加有要求的用户,TqSdk2 通过将中继服务器并入本地,来提供直连模式供用户使用:

期货公司交易系统
CTP/FEMAS/UFX
期货公司交易系统 CTP/FEMAS/UFX
交易所行情
交易所行情
行情网关
Open Md Gateway
行情网关 Open Md Gateway
DIFF 协议
TqSdk2
TqSdk2
Viewer does not support full SVG 1.1

因此在选择 TqSdk2 直连模式时:

  • 用户代码从中继模式切换到直连模式下的代码,只用修改一行

  • 省去了用户交易指令传输需要经过交易中继网关流程,减少了用户交易指令到达期货公司的延迟

to do:补充 example 和期货公司程序化接入流程

需要注意选择直连模式时,用户需要向期货公司申请程序化接入并且填写自己的接入信息

同时 TqSdk2 中将 TqSdk 里的底层代码全部用 C++ 进行了重构,这会给用户额外带来这些好处:

  • 维持了 TqSdk 中对外接口,让用户 TqSdk 中的代码可以在大多数情况下无缝迁移到 TqSdk2

  • 有效减少了系统内交易指令运算耗时

  • 将同等代码 TqSdk 的回测速度提升了十倍以上

每个策略是一个单独运行的py文件

在 VNPY 中, 要实现一个策略程序, 通常是从 CtaTemplate 等基类派生一个子类, 像这样:

class DoubleMaStrategy(CtaTemplate):

  parameters = ["fast_window", "slow_window"]
  variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]

  def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
    ...

  def on_tick(self, tick: TickData):
    ...

  def on_bar(self, bar: BarData):
    ...

这个 DoubleMaStrategy 类写好以后, 由VNPY的策略管理器负责加载运行. 整个程序结构中, VNPY作为调用方, 用户代码作为被调用方, 结构图是这样的:

Vnpy cta runner
Vnpy cta runner
调用事件响应函数
调用事件响应函数
策略1
策略1
接收行情和回单
接收行情和回单
发送交易指令
发送交易指令
调用下单函数
调用下单函数
调用事件响应函数
调用事件响应函数
策略2
策略2
调用下单函数
调用下单函数
调用事件响应函数
调用事件响应函数
策略3
策略3
调用下单函数
调用下单函数

而在 TqSdk2 中, 策略程序并没有一个统一的基类. TqSdk2 只是提供一些行情和交易函数, 用户可以任意组合它们来实现自己的策略程序, 还是以双均线策略为例:

'''
双均线策略
'''
from tqsdk2 import TqApi, TqAuth, TqSim, TargetPosTask
from tqsdk2.tafunc import ma

SHORT = 30
LONG = 60
SYMBOL = "SHFE.bu1912"

api = TqApi(auth=TqAuth("信易账户", "账户密码"))

data_length = LONG + 2
klines = api.get_kline_serial(SYMBOL, duration_seconds=60, data_length=data_length)
target_pos = TargetPosTask(api, SYMBOL)

while True:
    api.wait_update()

    if api.is_changing(klines.iloc[-1], "datetime"):  # 产生新k线:重新计算SMA
        short_avg = ma(klines.close, SHORT)  # 短周期
        long_avg = ma(klines.close, LONG)  # 长周期

        # 均线下穿,做空
        if long_avg.iloc[-2] < short_avg.iloc[-2] and long_avg.iloc[-1] > short_avg.iloc[-1]:
            target_pos.set_target_volume(-3)
            print("均线下穿,做空")

        # 均线上穿,做多
        if short_avg.iloc[-2] < long_avg.iloc[-2] and short_avg.iloc[-1] > long_avg.iloc[-1]:
            target_pos.set_target_volume(3)
            print("均线上穿,做多")

以上代码文件单独运行, 即可执行一个双均线交易策略. 整个程序结构中, 用户代码作为调用方, TqSdk2 库代码作为被调用方, 每个策略是完全独立的. 结构是这样:

TqSdk
TqSdk<br>
策略1
策略1
接收行情和回单
接收行情和回单
发送交易指令
发送交易指令
调用函数
调用函数
TqSdk
TqSdk<br>
策略2
策略2
接收行情和回单
接收行情和回单
发送交易指令
发送交易指令
调用函数
调用函数
TqSdk
TqSdk<br>
策略3
策略3
接收行情和回单
接收行情和回单
发送交易指令
发送交易指令
调用函数
调用函数

TqSdk2 将每个策略作为一个独立进程运行, 这样就可以:

  • 在运行多策略时可以充分利用多CPU的计算能力

  • 每个策略都可以随时启动/停止/调试/修改代码, 而不影响其它策略程序的运行

  • 可以方便的针对单个策略程序进行调试

在策略程序中, 用户代码可以随意调用 TqSdk2 包中的任意函数, 这带来了更大的自由度, 比如:

  • 在一个策略程序中使用多个合约或周期的K线数据, 盘口数据和Tick数据. 对于某些类型的策略来说这是很方便的

  • 对多个合约的交易指令进行精细管理

  • 管理复杂的子任务

  • 方便策略程序跟其它库或框架集成

以一个套利策略的代码为例:

'''
价差回归
当近月-远月的价差大于200时做空近月,做多远月
当价差小于150时平仓
'''
api = TqApi(auth=TqAuth("信易账户", "账户密码"))
quote_near = api.get_quote("SHFE.rb1910")
quote_deferred = api.get_quote("SHFE.rb2001")
# 创建 rb1910 的目标持仓 task,该 task 负责调整 rb1910 的仓位到指定的目标仓位
target_pos_near = TargetPosTask(api, "SHFE.rb1910")
# 创建 rb2001 的目标持仓 task,该 task 负责调整 rb2001 的仓位到指定的目标仓位
target_pos_deferred = TargetPosTask(api, "SHFE.rb2001")

while True:
    api.wait_update()
    if api.is_changing(quote_near) or api.is_changing(quote_deferred):
        spread = quote_near.last_price - quote_deferred.last_price
        print("当前价差:", spread)
        if spread > 250:
            print("目标持仓: 空近月,多远月")
            # 设置目标持仓为正数表示多头,负数表示空头,0表示空仓
            target_pos_near.set_target_volume(-1)
            target_pos_deferred.set_target_volume(1)
        elif spread < 200:
            print("目标持仓: 空仓")
            target_pos_near.set_target_volume(0)
            target_pos_deferred.set_target_volume(0)

在这个程序中, 我们同时跟踪两个合约的行情信息, 并为两个合约各创建一个调仓任务, 可以方便的实现套利策略

K线数据与指标计算

使用VNPY时, K线是由VNPY接收实时行情, 并在用户电脑上生成K线, 存储于用户电脑上的数据库中.

而在 TqSdk2 中, K线数据和其它行情数据一样是由行情网关生成并推送的. 这带来了一些差别:

  • 用户不再需要维护K线数据库. 用户电脑实时行情中断后, 也不再需要补历史数据

  • 行情服务器生成K线时, 采用了按K线时间严格补全对齐的算法. 这与VNPY或其它软件有明显区别, 详见 https://www.shinnytech.com/blog/why-our-kline-different/

  • 行情数据只在每次程序运行时通过网络获取, 不在用户硬盘保存. 如果策略研究工作需要大量静态历史数据, 我们推荐使用数据下载工具, 另行下载csv文件使用.

TqSdk2 中的K线序列采用 pandas.DataFrame 格式. pandas 提供了 非常丰富的数据处理函数 , 使我们可以非常方便的进行数据处理, 例如:

ks = api.get_kline_serial("SHFE.cu1901", 60)
print(ks.iloc[-1])            # <- 最后一根K线
print(ks.close)               # <- 收盘价序列
ks.high - ks.high.shift(1)    # <- 每根K线最高价-前一根K线最高价, 形成一个新序列

TqSdk 也通过 tqsdk2.tafunc 提供了一批行情分析中常用的计算函数, 例如:

from tqsdk2 import tafunc
ks = api.get_kline_serial("SHFE.cu1901", 60)
ms = tafunc.max(ks.open, ks.close)           # <- 取每根K线开盘价和收盘价的高者构建一个新序列
median3 = tafunc.median(ks.close, 100)       # <- 求最近100根K线收盘价的中间值
ss = tafunc.std(ks.close, 5)                 # <- 每5根K线的收盘价标准差

数据接收和更新

VNPY按照事件回调模型设计, 使用 CtaTemplate 的 on_xxx 回调函数进行行情数据和回单处理:

class DoubleMaStrategy(CtaTemplate):
  def on_tick(self, tick: TickData):
    ...
  def on_bar(self, bar: BarData):
    ...
  def on_order(self, order: OrderData):
    pass
  def on_trade(self, trade: TradeData):
    self.put_event()

TqSdk2 则不使用事件回调机制. wait_update() 函数设计用来获取任意数据更新, 像这样:

api = TqApi(auth=TqAuth("信易账户", "账户密码"))
ks = api.get_kline_serial("SHFE.cu1901", 60)

while True:
  api.wait_update()       # <- 这个 wait_update 将尝试更新所有数据. 如果没有任何新信息, 程序会阻塞在这一句. 一旦有任意数据被更新, 程序会继续往下执行
  print(ks.close.iloc[-1])      # <- 最后一根K线的收盘价

一次 wait_update 可能更新多个实体, 在这种情况下, is_changing() 被用来判断某个实体是否有变更:

api = TqApi(auth=TqAuth("信易账户", "账户密码"))
q = api.get_quote("SHFE.cu1901")
ks = api.get_kline_serial("SHFE.cu1901", 60)
x = api.insert_order("SHFE.cu1901", direction="BUY", offset="OPEN", volume=1, limit_price=50000)

while True:
  api.wait_update()      # <- 这个 wait_update 将尝试更新所有数据. 如果没有任何新信息, 程序会阻塞在这一句. 一旦有任意数据被更新, 程序会继续往下执行
  if api.is_changing(q): # <- 这个 is_changing 用来判定这次更新是否影响到了q
    print(q)
  if api.is_changing(x, "status"): # <- 这个 is_changing 用来判定这次更新是否影响到了报单的status字段
    print(x)

TqSdk2 针对行情数据和交易信息都采用相同的 wait_update/is_changing 方案. 用户需要记住的要点包括:

  • get_quote, get_kline_serial, insert_order 等业务函数返回的是一个引用(refrence, not value), 它们的值总是在 wait_update 时更新.

  • 用户程序除执行自己业务逻辑外, 需要反复调用 wait_update. 在两次 wait_update 间, 所有数据都不更新

  • 用 insert_order 函数下单, 报单指令实际是在 insert_order 后调用 wait_update 时发出的.

  • 用户程序中需要避免阻塞, 不要使用 sleep 暂停程序

关于 wait_update 机制的详细说明, 请见 策略程序结构

其它区别

此外, 还有一些差别值得注意

  • TqSdk 要求 Python 3.6.4 以上版本, 不支持 Python 2.x

要学习使用 TqSdk, 推荐从 十分钟快速入门 开始