多合约组合策略模板(StrategyTemplate)

多合约组合策略模板提供完整的信号生成和委托管理功能,用户可以基于该模板(位于site-packages\vnpy_portfoliostrategy\template中)自行开发多合约组合策略。

用户自行开发的策略可以放在用户运行文件夹下的strategies文件夹内。

请注意:

  1. 策略文件命名采用下划线模式,如portfolio_boll_channel_strategy.py,而策略类命名采用驼峰式,如PortfolioBollChannelStrategy。

  2. 自建策略的类名不要与示例策略的类名重合。如果重合了,图形界面上只会显示一个策略类名。

下面通过PortfolioBollChannelStrategy策略示例,来展示策略开发的具体步骤:

在基于策略模板编写策略逻辑之前,需要在策略文件的顶部载入需要用到的内部组件,如下方代码所示:

  1. from typing import List, Dict
  2. from datetime import datetime
  3. from vnpy.trader.utility import ArrayManager, Interval
  4. from vnpy.trader.object import TickData, BarData
  5. from vnpy_portfoliostrategy import StrategyTemplate, StrategyEngine
  6. from vnpy_portfoliostrategy.utility import PortfolioBarGenerator

其中,StrategyTemplate是策略模板,StrategyEngine是策略引擎,Interval是数据频率,TickData和BarData都是储存对应信息的数据容器,PortfolioBarGenerator是组合策略K线生成模块,ArrayManager是K线时间序列管理模块。

策略参数与变量

在策略类的下方,可以设置策略的作者(author),参数(parameters)以及变量(variables),如下方代码所示:

  1. author = "用Python的交易员"
  2. boll_window = 18
  3. boll_dev = 3.4
  4. cci_window = 10
  5. atr_window = 30
  6. sl_multiplier = 5.2
  7. fixed_size = 1
  8. price_add = 5
  9. parameters = [
  10. "boll_window",
  11. "boll_dev",
  12. "cci_window",
  13. "atr_window",
  14. "sl_multiplier",
  15. "fixed_size",
  16. "price_add"
  17. ]
  18. variables = []

虽然策略的参数和变量都从属于策略类,但策略参数是固定的(由交易员从外部指定),而策略变量则在交易的过程中随着策略的状态而变化,所以策略变量一开始只需要初始化为对应的基础类型。例如:整数设为0,浮点数设为0.0,而字符串则设为””。

如果需要策略引擎在运行过程中,将策略参数和变量显示在UI界面上,并在数据刷新、停止策略时保存其数值,则需把参数和变量的名字(以字符串的数据类型)添加进parameters和variables列表里。

请注意,该列表只能接受参数或变量以str、int、float和bool四种数据类型传入。如果策略里需要用到其他数据类型的参数与变量,请把该参数或变量的定义放到__init__函数下。

类的初始化

入参:strategy_engine: StrategyEngine, strategy_name: str, vt_symbols: List[str], setting: dict

出参:无

__init__函数是策略类的构造函数,需要与继承的StrategyTemplate保持一致。

在这个继承的策略类里,初始化一般分四步,如下方代码所示:

  1. def __init__(
  2. self,
  3. strategy_engine: StrategyEngine,
  4. strategy_name: str,
  5. vt_symbols: List[str],
  6. setting: dict
  7. ):
  8. """"""
  9. super().__init__(strategy_engine, strategy_name, vt_symbols, setting)
  10. self.boll_up: Dict[str, float] = {}
  11. self.boll_down: Dict[str, float] = {}
  12. self.cci_value: Dict[str, float] = {}
  13. self.atr_value: Dict[str, float] = {}
  14. self.intra_trade_high: Dict[str, float] = {}
  15. self.intra_trade_low: Dict[str, float] = {}
  16. self.targets: Dict[str, int] = {}
  17. self.last_tick_time: datetime = None
  18. self.ams: Dict[str, ArrayManager] = {}
  19. for vt_symbol in self.vt_symbols:
  20. self.ams[vt_symbol] = ArrayManager()
  21. self.targets[vt_symbol] = 0
  22. self.pbg = PortfolioBarGenerator(self.on_bars, 2, self.on_2hour_bars, Interval.HOUR)

1 . 通过super( )的方法继承策略模板,在__init__( )函数中传入策略引擎、策略名称、vt_symbols以及参数设置(以上参数均由策略引擎在使用策略类创建策略实例时自动传入,用户无需进行设置)。

2 . 创建策略所需的存放不同合约K线时间序列管理模块和策略变量的字典。

3 . 分别为策略交易的不同合约创建K线时间序列管理实例(ArrayManager)和目标仓位变量并放进字典里。

4 . 调用组合策略K线生成模块(PortfolioBarGenerator):通过时间切片将Tick数据合成1分钟K线数据。如有需求,还可合成更长的时间周期数据,如15分钟K线。

如果只基于on_bar进行交易,这里代码可以写成:

  1. self.pbg = PortfolioBarGenerator(self.on_bars)

而不用给pbg实例传入需要基于on_bars周期合成的更长K线周期,以及接收更长K线周期的函数名。

请注意,合成X分钟线时,X必须设为能被60整除的数(60除外)。对于小时线的合成没有这个限制。

PortfolioBarGenerator默认的基于on_bar函数合成长周期K线的数据频率是分钟级别,如果需要基于合成的小时线或者更长周期的K线交易,请在策略文件顶部导入Interval,并传入对应的数据频率给bg实例。如下方代码所示:

文件顶部导入Interval:

  1. from vnpy.trader.constant import Interval

__init__函数创建bg实例时传入数据频率:

  1. self.pbg = BarGenerator(self.on_bars, 2, self.on_2hour_bars, Interval.HOUR)

注意:self.on_hour_bars函数名在程序内部已使用,1小时请使用self.on_1_hour_bars或者其他命名,否则会产生意料之外的问题。

策略引擎调用的函数

StrategyTemplate中的update_setting函数和该函数后面四个以get开头的函数以及update_trade和update_order函数,都是策略引擎去负责调用的函数,一般在策略编写的时候是不需要调用的。

策略的回调函数

StrategyTemplate中以on开头的函数称为回调函数,在编写策略的过程中能够用来接收数据或者接收状态更新。回调函数的作用是当某一个事件发生的时候,策略里的这类函数会被策略引擎自动调用(无需在策略中主动操作)。回调函数按其功能可分为以下两类:

策略实例状态控制(所有策略都需要)

on_init

  • 入参:无

  • 出参:无

初始化策略时on_init函数会被调用,默认写法是先调用write_log函数输出“策略初始化”日志,再调用load_bars函数加载历史数据。如下方代码所示:

  1. def on_init(self):
  2. """
  3. Callback when strategy is inited.
  4. """
  5. self.write_log("策略初始化")
  6. self.load_bars(10)

与CTA策略不同,多合约组合策略只支持K线回测,所以多合约策略模板并没有load_ticks函数。

策略初始化时,策略的inited和trading状态都为【False】,此时只是调用ArrayManager计算并缓存相关的计算指标,不能发出交易信号。调用完on_init函数之后,策略的inited状态才变为【True】,策略初始化才完成。

on_start

  • 入参:无

  • 出参:无

启动策略时on_start函数会被调用,默认写法是调用write_log函数输出“策略启动”日志,如下方代码所示:

  1. def on_start(self):
  2. """
  3. Callback when strategy is started.
  4. """
  5. self.write_log("策略启动")

调用策略的on_start函数启动策略后,策略的trading状态变为【True】,此时策略才能够发出交易信号。

on_stop

  • 入参:无

  • 出参:无

停止策略时on_stop函数会被调用,默认写法是调用write_log函数输出“策略停止”日志,如下方代码所示:

  1. def on_stop(self):
  2. """
  3. Callback when strategy is stopped.
  4. """
  5. self.write_log("策略停止")

调用策略的on_stop函数停止策略后,策略的trading状态变为【False】,此时策略就不会发出交易信号了。

接收数据、计算指标、发出交易信号

on_tick

  • 入参:tick: TickData

  • 出参:无

绝大部分交易系统都只提供Tick数据的推送。即使一部分平台可以提供K线数据的推送,但是这些数据到达本地电脑的速度也会慢于Tick数据的推送,因为也需要平台合成之后才能推送过来。所以实盘的时候,vn.py里所有的策略的K线都是由收到的Tick数据合成的。

当策略收到实盘中最新的Tick数据的行情推送时,on_tick函数会被调用。默认写法是通过PortfolioBarGenerator的update_tick函数把收到的Tick数据推进前面创建的pbg实例中以便合成1分钟的K线,如下方代码所示:

  1. def on_tick(self, tick: TickData):
  2. """
  3. Callback of new tick data update.
  4. """
  5. self.pbg.update_tick(tick)

请注意,on_tick只有实盘中会调用,回测不支持。

on_bars

  • 入参:bars: Dict[str, BarData]

  • 出参:无

当策略收到最新的K线数据时(实盘时默认推进来的是基于Tick合成的一分钟的K线,回测时则取决于选择参数时填入的K线数据频率),on_bars函数就会被调用。

与CTA策略模块不同,多合约组合策略模块在接收K线推送时,是通过on_bars回调函数一次性接收该时间点上所有合约的K线数据,而不是通过on_bar函数一个个接收(无法判断当前时点的K线是否全部走完了 )。

示例策略里出现过的写法有两种:

1 . 如果策略基于on_bars推进来的K线交易,那么请把交易请求类函数都写在on_bars函数下(因本次示例策略类PortfolioBollChannelStrategy不是基于on_bars交易,故不作示例讲解。基于on_bars交易的示例代码可参考其他示例策略);

2 . 如果策略需要基于on_bars推进来的K线数据通过PortfolioBarGenerator合成更长时间周期的K线来交易,那么请在on_bars中调用PortfolioBarGenerator的update_bars函数,把收到的bars推进前面创建的pbg实例中即可,如下方代码所示:

  1. def on_bars(self, bars: Dict[str, BarData]):
  2. """
  3. Callback of new bars data update.
  4. """
  5. self.pbg.update_bars(bars)

示例策略类PortfolioBollChannelStrategy是通过2小时K线数据回报来生成信号的。一共有三部分,如下方代码所示:

  1. def on_2hour_bars(self, bars: Dict[str, BarData]):
  2. """"""
  3. self.cancel_all()
  4. for vt_symbol, bar in bars.items():
  5. am: ArrayManager = self.ams[vt_symbol]
  6. am.update_bar(bar)
  7. for vt_symbol, bar in bars.items():
  8. am: ArrayManager = self.ams[vt_symbol]
  9. if not am.inited:
  10. return
  11. self.boll_up[vt_symbol], self.boll_down[vt_symbol] = am.boll(self.boll_window, self.boll_dev)
  12. self.cci_value[vt_symbol] = am.cci(self.cci_window)
  13. self.atr_value[vt_symbol] = am.atr(self.atr_window)
  14. current_pos = self.get_pos(vt_symbol)
  15. if current_pos == 0:
  16. self.intra_trade_high[vt_symbol] = bar.high_price
  17. self.intra_trade_low[vt_symbol] = bar.low_price
  18. if self.cci_value[vt_symbol] > 0:
  19. self.targets[vt_symbol] = self.fixed_size
  20. elif self.cci_value[vt_symbol] < 0:
  21. self.targets[vt_symbol] = -self.fixed_size
  22. elif current_pos > 0:
  23. self.intra_trade_high[vt_symbol] = max(self.intra_trade_high[vt_symbol], bar.high_price)
  24. self.intra_trade_low[vt_symbol] = bar.low_price
  25. long_stop = self.intra_trade_high[vt_symbol] - self.atr_value[vt_symbol] * self.sl_multiplier
  26. if bar.close_price <= long_stop:
  27. self.targets[vt_symbol] = 0
  28. elif current_pos < 0:
  29. self.intra_trade_low[vt_symbol] = min(self.intra_trade_low[vt_symbol], bar.low_price)
  30. self.intra_trade_high[vt_symbol] = bar.high_price
  31. short_stop = self.intra_trade_low[vt_symbol] + self.atr_value[vt_symbol] * self.sl_multiplier
  32. if bar.close_price >= short_stop:
  33. self.targets[vt_symbol] = 0
  34. for vt_symbol in self.vt_symbols:
  35. target_pos = self.targets[vt_symbol]
  36. current_pos = self.get_pos(vt_symbol)
  37. pos_diff = target_pos - current_pos
  38. volume = abs(pos_diff)
  39. bar = bars[vt_symbol]
  40. boll_up = self.boll_up[vt_symbol]
  41. boll_down = self.boll_down[vt_symbol]
  42. if pos_diff > 0:
  43. price = bar.close_price + self.price_add
  44. if current_pos < 0:
  45. self.cover(vt_symbol, price, volume)
  46. else:
  47. self.buy(vt_symbol, boll_up, volume)
  48. elif pos_diff < 0:
  49. price = bar.close_price - self.price_add
  50. if current_pos > 0:
  51. self.sell(vt_symbol, price, volume)
  52. else:
  53. self.short(vt_symbol, boll_down, volume)
  54. self.put_event()
  • 清空未成交委托:为了防止之前下的单子在上一个2小时没有成交,但是下一个2小时可能已经调整了价格,就用cancel_all()方法立刻撤销之前未成交的所有委托,保证策略在当前这2小时开始时的整个状态是清晰和唯一的;

  • 调用K线时间序列管理模块:基于最新的2小时K线数据来计算相应的技术指标,如布林带上下轨、CCI指标、ATR指标等。首先获取ArrayManager对象,然后将收到的K线推送进去,检查ArrayManager的初始化状态,如果还没初始化成功就直接返回,没有必要去进行后续的交易相关的逻辑判断。因为很多技术指标计算对最少K线数量有要求,如果数量不够的话计算出来的指标会出现错误或无意义。反之,如果没有return,就可以开始计算技术指标了;

  • 信号计算:通过持仓的判断以及结合CCI指标、ATR指标在通道突破点挂出限价单委托(buy/sell),同时设置离场点(short/cover)。

    请注意:

    1. 在CTA策略模块中,通常都是通过访问策略的变量pos获取策略持仓来进行持仓判断。但在多合约组合策略模块中,是通过调用get_pos函数获取某一合约现在的持仓来进行逻辑判断,然后设定该合约的目标仓位,最后通过目标仓位和实际仓位的差别来进行逻辑判断进而发出交易信号的;

    2. 如果需要在图形界面刷新指标数值,请不要忘记调用put_event()函数。

委托状态更新

因为组合策略中需要对多合约同时下单交易,在回测时无法判断某一段K线内部每个合约委托成交的先后时间顺序,因此无法提供on_order和on_trade函数来获取委托成交推送,而只能在每次on_bars回调时通过get_pos和get_order来进行相关的状态查询。

主动函数

buy:买入开仓(Direction:LONG,Offset:OPEN)

sell:卖出平仓(Direction:SHORT,Offset:CLOSE)

short:卖出开仓(Direction:SHORT,Offset:OPEN)

cover:买入平仓(Direction:LONG,Offset:CLOSE)

  • 入参:vt_symbol: str, price: float, volume: float, lock: bool = False, net: bool = False

  • 出参:vt_orderids: List[str] / 无

buy/sell/short/cover都是策略内部的负责发单的交易请求类函数。策略可以通过这些函数给策略引擎发送交易信号来达到下单的目的。

以下方buy函数的代码为例,可以看到,具体要交易合约的代码,价格和数量是必填的参数,锁仓转换和净仓转换则默认为False。也可以看到,函数内部收到传进来的参数之后就调用了StrategyTemplate里的send_order函数来发单(因为是buy指令,则自动把方向填成了LONG,开平填成了OPEN)

如果lock设置为True,那么该笔订单则会进行锁仓委托转换(在有今仓的情况下,如果想平仓,则会先平掉所有的昨仓,然后剩下的部分都进行反向开仓来代替平今仓,以避免平今的手续费惩罚)。

如果net设置为True,那么该笔订单则会进行净仓委托转换(基于整体账户的所有仓位,根据净仓持有方式来对策略下单的开平方向进行转换)。但是净仓交易模式与锁仓交易模式互斥,因此net设置为True时,lock必须设置为False。

  1. def buy(self, vt_symbol: str, price: float, volume: float, lock: bool = False, net: bool = False) -> List[str]:
  2. """
  3. Send buy order to open a long position.
  4. """
  5. return self.send_order(vt_symbol, Direction.LONG, Offset.OPEN, price, volume, lock, net)

请注意,国内期货有开平仓的概念,例如买入操作要区分为买入开仓和买入平仓;但对于股票、外盘期货都是净持仓模式,没有开仓和平仓概念,所以只需使用买入(buy)和卖出(sell)这两个指令就可以了。

send_order

  • 入参:vt_symbol: str, direction: Direction, offset: Offset, price: float, volume: float, lock: bool = False, net: bool = False

  • 出参:vt_orderids: List[str] / 无

send_order函数是策略引擎调用的发送委托的函数。一般在策略编写的时候不需要单独调用,通过buy/sell/short/cover函数发送限价委托即可。

请注意,组合策略模块只支持限价单交易,不提供本地停止单功能。

实盘的时候,收到传进来的参数后,会调用round_to函数基于合约的pricetick和min_volume对委托的价格和数量进行处理。

请注意,要在策略启动之后,也就是策略的trading状态变为【True】之后,才能发出交易委托。如果策略的Trading状态为【False】时调用了该函数,只会返回[]。

cancel_order

  • 入参:vt_orderid: str

  • 出参:无

cancel_all

  • 入参:无

  • 出参:无

cancel_order和cancel_all都是负责撤单的交易请求类函数。cancel_order是撤掉策略内指定的活动委托,cancel_all是撤掉策略所有的活动委托。

请注意,要在策略启动之后,也就是策略的trading状态变为【True】之后,才能撤单。

功能函数

以下为策略以外的功能函数:

get_pos

  • 入参:vt_symbol: str

  • 出参:int / 0

在策略里调用get_pos函数,可以获取特定合约的持仓数据。

get_order

  • 入参:vt_orderid

  • 出参:OrderData / 无

在策略里调用get_order函数,可以获取特定合约的委托数据。

get_all_active_orderids

  • 入参:无

  • 出参:List[OrderData] / 无

在策略里调用get_all_active_orderids函数,可以获取当前全部活动委托号。

get_pricetick

  • 入参:vt_symbol

  • 出参:pricetick: float / None

在策略里调用get_price函数,可以获取特定合约的最小价格跳动。

write_log

  • 入参:msg: str

  • 出参:无

在策略中调用write_log函数,可以进行指定内容的日志输出。

load_bars

  • 入参:days: int, interval: Interval = Interval.MINUTE

  • 出参:无

在策略中调用load_bars函数,可以在策略初始化时加载K线数据。

如下方代码所示,load_bars函数调用时,默认加载的天数是10,频率是一分钟,对应也就是加载10天的1分钟K线数据。在回测时,10天指的是10个交易日,而在实盘时,10天则是指的是自然日,因此建议加载的天数宁可多一些也不要太少。

  1. def load_bars(self, days: int, interval: Interval = Interval.MINUTE) -> None:
  2. """
  3. Load historical bar data for initializing strategy.
  4. """
  5. self.strategy_engine.load_bars(self, days, interval)

put_event

  • 入参:无

  • 出参:无

在策略中调用put_event函数,可以通知图形界面刷新策略状态相关显示。

请注意,要策略初始化完成,inited状态变为【True】之后,才能刷新界面。

send_email

  • 入参:msg: str

  • 出参:无

配置好邮箱相关信息之后(配置方法详见基本使用篇的全局配置部分),在策略中调用send_email函数,可以发送指定内容的邮件到自己的邮箱。

请注意,要策略初始化完成,inited状态变为【True】之后,才能发送邮件。

sync_data

  • 入参:无

  • 出参:无

在策略中调用sync_data函数,可以在实盘的时候,每次停止或成交时都同步策略变量进json文件中进行本地缓存,方便第二天初始化时再进行读取还原(策略引擎会去调用,在策略里无需主动调用)。

请注意,要在策略启动之后,也就是策略的trading状态变为【True】之后,才能同步策略信息。