TqSdk 与 VNPY 有哪些差别

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

系统整体架构

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

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

[Not supported by viewer][Not supported by viewer]
期货公司交易系统
CTP / FEMAS / UFX
期货公司交易系统<br>CTP / FEMAS / UFX<br>
交易所行情系统
交易所行情系统<br>
[Not supported by viewer][Not supported by viewer][Not supported by viewer]

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

  • 行情网关 (Open Md Gateway) 负责提供实时行情和历史数据
  • 交易中继网关 (Open Trade Gateway) 负责连接到期货公司交易系统
  • 上面两个网关统一以 Diff 协议对下方提供服务
  • 天勤终端和TqSdk按照Diff协议连接到行情网关和交易中继网关, 实现行情和交易功能

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

  • TqSdk 很小, 安装也很方便, 只要简单 pip install tqsdk 即可
  • 官方专门运维行情数据库, 用户可以直接使用, 不需要自己接收和存储数据
  • 交易相关接口被大幅度简化, 不再需要处理CTP接口的复杂回调, 也不需要发起任何查询请求

也有一些不如VNPY方便的地方:

  • 由于交易指令经交易网关转发, 用户无法直接指定CTP服务器地址. 用户如果需要连接到官方交易网关不支持的期货公司, 需要自行部署交易网关.

每个策略是一个单独运行的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
调用下单函数
调用下单函数

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

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

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

api = TqApi(TqSim())

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("均线上穿,做多")

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

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

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

  • 在运行多策略时可以充分利用多CPU的计算能力
  • 每个策略都可以随时启动/停止/调试/修改代码, 而不影响其它策略程序的运行
  • 可以方便的针对单个策略程序进行调试

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

  • 在一个策略程序中使用多个合约或周期的K线数据, 盘口数据和Tick数据. 对于某些类型的策略来说这是很方便的
  • 对多个合约的交易指令进行精细管理
  • 管理复杂的子任务
  • 方便策略程序跟其它库或框架集成

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

'''
价差回归
当近月-远月的价差大于200时做空近月,做多远月
当价差小于150时平仓
'''
api = TqApi(TqSim())
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线, 存储于用户电脑上的数据库中.

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

  • 用户不再需要维护K线数据库. 用户电脑实时行情中断后, 也不再需要补历史数据
  • 行情服务器生成K线时, 采用了按K线时间严格补全对齐的算法. 这与VNPY或其它软件有明显区别, 详见 https://www.shinnytech.com/blog/why-our-kline-different/
  • 行情数据只在每次程序运行时通过网络获取, 不在用户硬盘保存. 如果策略研究工作需要大量静态历史数据, 我们推荐使用数据下载工具, 另行下载csv文件使用.

TqSdk中的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 也通过 tqsdk.tafunc 提供了一批行情分析中常用的计算函数, 例如:

from tqsdk 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()

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

api = TqApi()
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()
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)

TqSdk针对行情数据和交易信息都采用相同的 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 本身并不包含任何图形界面. 这部分功能由天勤软件提供支持:

  • 策略运行时, 提供交易记录/日志的监控表格. 交易记录和持仓记录自动在行情图上标记, 可以快速定位跳转, 可以跨周期缩放定位
  • 策略回测时, 提供回测报告/图上标记.
  • 策略运行和回测信息自动保存, 可事后随时查阅显示

TqSdk配合天勤使用时, 还支持自定义绘制行情图表, 像这样:

# 获取 cu1905 和 cu1906 的日线数据
klines = api.get_kline_serial("SHFE.cu1905", 86400)
klines2 = api.get_kline_serial("SHFE.cu1906", 86400)

# 算出 cu1906 - cu1905 的价差,并以折线型态显示在副图
klines["dif"] = klines2["close"] - klines["close"]
klines["dif.board"] = "DIF"
klines["dif.color"] = 0xFF00FF00
klines["dif.width"] = 3

关于 TqSdk 配合天勤使用的详细说明, 请见 与天勤软件配合工作

回测

使用TqSdk开发的策略可以回测:

  • 提供Tick及K线级别的回测.
  • TqSdk 允许在一个策略中使用任意多个数据序列. 回测框架将正确识别并处理这种情况.
  • 回测前不需要准备数据

关于策略回测的详细说明, 请见 策略程序回测

其它区别

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

  • TqSdk 要求 Python 3.6 以上版本, 不支持 Python 2.x
  • TqSdk 使用了Python3的async框架, 某些 IDE 不支持, 需要使用支持 async 的IDE, 例如 pycharm

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