signal模式入门
bt信号交易
大多数策略开发,本就是基于信号的,bt直接提供了基于信号的回测,且灵活度较高,可以极大的提高策略研发效率。
基础入门可参考这篇文章:https://mp.weixin.qq.com/s?__biz=MzAxNTc0Mjg0Mg==&mid=2653317634&idx=1&sn=e92fec0b0b5fd5f62805e7c2be5830f8
官方文档参考demo:https://www.backtrader.com/docu/signal_strategy/signal_strategy/
信号交易5种模式差异
add_signal(signal type, signal class, arg) 中的参数说明:
第 1 个参数:信号类型
分为 2 大类,共计 5 种信号类型:
开仓类
bt.SIGNAL_LONGSHORT:
多头信号和空头信号都会作为开仓信号;
对于多头信号,如果之前有空头仓位,会先对空仓进行平仓 close,再开多仓;
空头信号也类似,会在开空仓前对多仓进行平仓 close。
bt.SIGNAL_LONG:
多头信号用于做多,空头信号用于平仓 close;
如果系统中同时存在 LONGEXIT 信号类型,SIGNAL_LONG 中的空头信号将不起作用,将会使用 LONGEXIT 中的空头信号来平仓多头,如上面的多条交易信号的例子。
bt.SIGNAL_SHORT:
空头信号用于做空,多头信号用于平仓;
如果系统中同时存在 SHORTEXIT 信号类型,SIGNAL_SHORT 中的多头信号将不起作用,将会使用 SHORTEXIT 中的多头信号来平仓空头。
这部分该如何理解?
信号理解为发号施令的领导,而具体action取决于负责执行的人。
对于SIGNAL_LONGSHORT,可以看做负责执行的人充分实施领导命令。
对于SIGNAL_LONG,可以看做负责执行的人,选择性执行领导的开多仓和平多仓,而忽略领导的开空仓(和平空仓)。
对于SIGNAL_SHORT,可以看做负责执行的人,选择性执行领导的开空仓和平空仓,而忽略领导的开多仓(和平多仓)。
“选择性执行”,也可看做市场允许的交易类型, 比如A股股票市场,不允许做空。还有就是比如现在长期均线朝上,或者月均线高于年均线(上涨趋势为主),此时也不应该做空,只能选择做多或空仓。
平仓类
bt.SIGNAL_LONGEXIT:接收空头信号平仓多头;
bt.SIGNAL_SHORTEXIT:接收多头信号平仓空头;
上述 2 种信号类型主要用于确定平仓信号,在下达平仓指令时,优先级高于上面开仓类中的信号。
第 2 个参数:定义的信号指标类的名称,比如案例中的 SMACloseSignal 类 和 SMAExitSignal 类,直接传入类即可,不需要将类进行实例化;
第 3 个参数:对应信号指标类中的参数 params,直接通过 period=xxx 、p1=xxx, p2=xxx 形式修改参数取值。
这部分又该如何理解?
首先为何需要这个平仓信号,思考一个完整行情周期,
01,市场长期底部横盘,开始启动,连续突破最近1月,2月最高点,开仓信号。
02,市场持续上涨,但是速度(斜率)偏弱,涨幅小且回调频繁,虽然上涨依然大概率,但是概率较启动时小了,且积累了较大涨幅了,平多仓,止盈。
03,市场由于过分上涨,开始回调,长期看空,开空仓。
04,空仓预期释放,短期依然空,长期偏多,平空仓。
由于买卖信号并不完全对称。有些强力偏多的信号,则买入多。一旦强力多信号减弱到一定程度就要平多了。而此时,整体依然是多面大,只是为了避免波动,远离市场而已,而空头同样道理。假设市场合理价值为0。波动区间[-100,100],那么位于-80以及以下时,应当买多,而涨到-20时应当平多,市场在80以上,应该买空,而跌到20时,应该平空。所以买多和平多,与买空和平空,4个信号本来就是独立的(分别对应不同分值)。而非直观以为的,平多同时应该开空,因为信号不强烈时,极有可能长期波动横盘,导致资金效率低下且需承受不可避免的波动。
疑点解析
信号只识别正负,不区分True或False
比如:ma1在ma2上,开多仓,否则空仓。则是signal=ma1-ma2,不能采用signal=ma1>ma2
开仓类信号只负责买入,平仓类只负责卖出?
CASE01:SIGNAL_LONG + SIGNAL_LONGEXIT
开仓:SIGNAL_LONG
平仓:SIGNAL_LONGEXIT(SIGNAL_LONG +/- 5day)
信号特征:非正即负
信号源码:
class SMACloseSignal(bt.Indicator): lines = ('signal',) params = (('period', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = sma1 - sma2
class SMAExitSignal(bt.Indicator): lines = ('signal',) params = (('p1', 5), ('p2', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = (sma1 - sma2)(-5)
python ./signal_macd.py --plot --signal longonly --exitsignal longexit --fromdate 2013-01-10 --todate 2014-01-01曲线图:

解释:
竖线1:买入原因SMACloseSignal > 0
竖线2:卖出原因SMAExitSignal < 0
结论:
无持仓,盯着开仓信号线,开仓信号>0则买入(不管平仓信号状态),
有持仓,盯着平仓信号线,平仓信号<0则平仓(不管开仓信号状态)
图中,频繁开仓-平仓的锯齿部分(标记3),就是开仓信号线>0 但 平仓信号线<0,导致刚买入即卖出。
再次验证另一种情况
class SMACloseSignal(bt.Indicator): lines = ('signal',) params = (('period', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = (sma1 - sma2)(-5)
class SMAExitSignal(bt.Indicator): lines = ('signal',) params = (('p1', 5), ('p2', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = (sma1 - sma2)python ./signal_macd.py --plot --signal longonly --exitsignal longexit --fromdate 2013-01-10 --todate 2014-01-01曲线图

区域尾部的折线意味着开仓信号 > 0,平仓信号 < 0,会导致持续的开仓-平仓.
可见,上述结论依旧成立。
CASE02:SIGNAL_LONG + SIGNAL_SHORTEXIT
修复源码中一个bug再测试
EXITSIGNALS = { 'longexit': bt.SIGNAL_LONGEXIT, 'shortexit': bt.SIGNAL_SHORTEXIT,}
python ./signal_macd.py --plot --signal longonly --exitsignal shortexit --fromdate 2013-01-10 --todate 2014-01-01结果图:

可见:SMAExitSignal对仓位无影响,全凭SMACloseSignal决定。这个也符合逻辑,根据其定义,SIGNAL_SHORTEXIT接收多头信号平仓空头;由于单向做多,所以不存在空头仓位,所以属于无意义参数。
CASE03:SIGNAL_LONGSHORT+SIGNAL_LONGEXIT+SIGNAL_SHORTEXIT
开仓:SIGNAL_LONGSHORT
平仓:SIGNAL_LONGEXIT + SIGNAL_SHORTEXIT
代码调整:
class SMACloseSignal(bt.Indicator): lines = ('signal',) params = (('period', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = bt.If((sma1 - sma2)>0,1,-1)
class SMAExitSignal(bt.Indicator): lines = ('signal',) params = (('p1', 5), ('p2', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = bt.If((sma1 - sma2)>0,1,-1)(-5)
if args.exitsignal is not None: cerebro.add_signal(bt.SIGNAL_LONGEXIT, SMAExitSignal, p1=args.exitperiod, p2=args.smaperiod) cerebro.add_signal(bt.SIGNAL_SHORTEXIT, SMAExitSignal, p1=args.exitperiod, p2=args.smaperiod)
python ./signal_macd.py --plot --signal longshort --exitsignal shortexit --fromdate 2013-01-10 --todate 2014-01-01结果

分析:
竖线1区间:持有空头,原因:起点SMACloseSignal为负数,持有空头,由于SIGNAL_SHORTEXIT信号为负数,所以持续空头持仓
竖线2区间:SMACloseSignal负转正,触发持仓从空头变多头,变成多头后,由于SIGNAL_LONGEXIT信号依然为负,所以马上平仓,平仓后由于SMACloseSignal为正,会再次买入多头持仓,如此持续反复多次。
竖线3区间:SMACloseSignal稳定正,且SIGNAL_LONGEXIT信号也维持正,所以稳定持有多头
竖线4区间:SMACloseSignal正转负,引发空头持仓,但是SIGNAL_SHORTEXIT为正,所以马上清空空头持仓,清空后由于SMACloseSignal为负,会再次买入空头持仓,由于如此持续反复多次。
综上所述:
如果仓位为空:看信号SIGNAL_LONGSHORT,SMACloseSignal
如果仓位为多:看信号SIGNAL_LONGEXIT,决定是平多(归0)
如果仓位为空:看信号SIGNAL_SHORTEXIT,决定是平空(归0)
除此之外,SIGNAL_LONGSHORT的转向,依然会导致仓位切换,而忽略SIGNAL_LONGEXIT和SIGNAL_SHORTEXIT的屏蔽效应。
CASE03:SIGNAL_LONGSHORT+SIGNAL_LONGEXIT
开仓:SIGNAL_LONGSHORT
平仓:SIGNAL_LONGEXIT
代码:
class SMACloseSignal(bt.Indicator): lines = ('signal',) params = (('period', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = bt.If((sma1 - sma2)>0,1,-1)
class SMAExitSignal(bt.Indicator): lines = ('signal',) params = (('p1', 5), ('p2', 30),)
def __init__(self): sma1 = bt.indicators.SMA(period=10) sma2 = bt.indicators.SMA(period=30) self.lines.signal = bt.If((sma1 - sma2)>0,1,-1)(-5)
python ./signal_macd.py --plot --signal longshort --exitsignal longexit --fromdate 2013-01-10 --todate 2014-01-01结果图:

可见:SIGNAL_LONGSHORT ,信号决定了大体调仓方向,而Longexit信号造成锯齿类型的平仓记录。
开仓类信号只负责买入,平仓类只负责卖出?源码分析
def _next_signal(self): if self._sentinel is not None and not self.p._concurrent: return # order active and more than 1 not allowed
sigs = self._signals nosig = [[0.0]]
# 第一部分:注册为多空市场的信号 # Calculate current status of the signals ls_long = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONGSHORT] or nosig) ls_short = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONGSHORT] or nosig)
# 第二部分:注册为单看多市场的信号 l_enter0 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONG] or nosig) # >0表示看多 l_enter1 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONG_INV] or nosig) # _INV反向信号,<0,负数表示看多 l_enter2 = all(x[0] for x in sigs[bt.SIGNAL_LONG_ANY] or nosig) # _ANY只要非0就是true l_enter = l_enter0 or l_enter1 or l_enter2
s_enter0 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_SHORT] or nosig) s_enter1 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_SHORT_INV] or nosig) s_enter2 = all(x[0] for x in sigs[bt.SIGNAL_SHORT_ANY] or nosig) s_enter = s_enter0 or s_enter1 or s_enter2
# 第三部分:多头退出信号 l_ex0 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONGEXIT] or nosig) l_ex1 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONGEXIT_INV] or nosig) l_ex2 = all(x[0] for x in sigs[bt.SIGNAL_LONGEXIT_ANY] or nosig) l_exit = l_ex0 or l_ex1 or l_ex2
# 第四部分:空头退出信号 s_ex0 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_SHORTEXIT] or nosig) s_ex1 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_SHORTEXIT_INV] or nosig) s_ex2 = all(x[0] for x in sigs[bt.SIGNAL_SHORTEXIT_ANY] or nosig) s_exit = s_ex0 or s_ex1 or s_ex2
# Use oppossite signales to start reversal (by closing) # but only if no "xxxExit" exists # self._longexit=bool(_obj._signals[bt.SIGNAL_LONGEXIT]) # 未设置独立的多头退出信号,且空头进入信号为true,long_reverse信号为true l_rev = not self._longexit and s_enter # 未设置独立的空头退出信号,且多头进入信号为true,short_reverse信号为true s_rev = not self._shortexit and l_enter
# 这一部分是第一部分的反信号 # Opposite of individual long and short l_leav0 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONG] or nosig) l_leav1 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONG_INV] or nosig) l_leav2 = all(x[0] for x in sigs[bt.SIGNAL_LONG_ANY] or nosig) l_leave = l_leav0 or l_leav1 or l_leav2
# 这一部分是第二部分的反信号 s_leav0 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_SHORT] or nosig) s_leav1 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_SHORT_INV] or nosig) s_leav2 = all(x[0] for x in sigs[bt.SIGNAL_SHORT_ANY] or nosig) s_leave = s_leav0 or s_leav1 or s_leav2
# 未设置独立的多头退出信号,且多头leave为true # Invalidate long leave if longexit signals are available l_leave = not self._longexit and l_leave # 未设置独立的空头退出信号,且空头leave为true # Invalidate short leave if shortexit signals are available s_leave = not self._shortexit and s_leave
# Take size and start logic size = self.getposition(self._dtarget).size if not size: # 没有持仓,只能开仓操作,判断是否有满足的开仓条件 if ls_long or l_enter: self._sentinel = self.buy(self._dtarget)
elif ls_short or s_enter: self._sentinel = self.sell(self._dtarget)
elif size > 0: # current long position,当前多头持仓,判断是否满足平仓,开空仓条件 if ls_short or l_exit or l_rev or l_leave: # closing position - not relevant for concurrency self.close(self._dtarget)
if ls_short or l_rev: self._sentinel = self.sell(self._dtarget)
if ls_long or l_enter: if self.p._accumulate: self._sentinel = self.buy(self._dtarget)
elif size < 0: # current short position,当前空头持仓,判断是否满足平仓,开多仓条件 if ls_long or s_exit or s_rev or s_leave: # closing position - not relevant for concurrency self.close(self._dtarget)
if ls_long or s_rev: self._sentinel = self.buy(self._dtarget)
if ls_short or s_enter: if self.p._accumulate: self._sentinel = self.sell(self._dtarget)这一部分逻辑看起来有点难以理解,先只考虑单向做多市场相关信号,分3类:
第一类:多头进入信号(开仓信号),l_enter = l_enter0 or l_enter1 or l_enter2
第二类:多头退出信号(平仓信号),l_exit = l_ex0 or l_ex1 or l_ex2
第三类:多头进入非有效(失效)信号,l_leave = l_leav0 or l_leav1 or l_leav2
关于第一类,第二个类信号,非常显著,
l_enter只关联self.buy(self._dtarget)
l_exit只关联了self.close(self._dtarget)
而l_leave和l_exit类似,只关联了self.close(self._dtarget),所以l_leave和l_exit可看做邻近信号,二者或的关系(实际上,只会有一个有效,因为leave信号生效前提是未设置专用的 exit信号,而exit类信号必须是设置了专用exit信号)。
longshort类型交易,面对金叉和死叉会形成点状持仓还是状态持仓
代码:
class TestCrossover(bt.Indicator): lines = ('signal',) params = (('pfast', 5),('pslow', 20))
def __init__(self): # 定义一个长期均线和短期均线 sma1 = bt.ind.SMA(period=self.p.pfast) sma2 = bt.ind.SMA(period=self.p.pslow) # 创建均线交叉(买入卖出)信号 self.lines.signal = bt.ind.CrossOver(sma1, sma2)测试命令:
python ./signal_template.py --plot --market_type longshort --open_signal TestCrossover --fromdate 2022-01-10 --todate 2023-01-01结果:

结论:最下面一列是金叉死叉信号,可见信号为点状信号(大部分0,1和-1是点),但持仓为状态持仓(也就是不是状态为1的那一天有持仓,而是出现-1信号前都保有持仓)
累积和订单并发【Accumulation and Order Concurrency 】
上面显示的示例信号将不断发出longshort指示,因为它只是从收盘价减去SMA值,该值将始终为> 0和<0(0为数学上可能,但不太可能真正发生)
这将导致连续生成订单,从而产生两种情况:
积累:即使已经在市场上【即本身账户存在持仓情况】,信号也会产生新的订单会增加市场占有率。
并发:将生成新订单【之后】,而无需等待其他订单的执行订单
为了避免这种情况,默认行为是:不累积,不允许并发
从这点来讲,由于策略的过滤即便再严格,一天能够触发的成交信号肯定也是有很多,在这里bt默认在手里持仓没有被卖出之前,是不会再继续交易,直到手里的持仓结束以后即卖出以后,才考虑再执行下一个订单。
信号策略SignalStrategy和参数调优
Genetic Optimization:https://community.backtrader.com/topic/186/genetic-optimization/22?_=1630611041858 Strategy with Signals:https://community.backtrader.com/topic/462/strategy-with-signals/2
import backtrader as bt
class SmaCross(bt.SignalStrategy): params = (('pfast', 10), ('pslow', 30),) def __init__(self): sma1, sma2 = bt.ind.SMA(period=self.p.pfast), bt.ind.SMA(period=self.p.pslow) self.signal_add(bt.SIGNAL_LONG, bt.ind.CrossOver(sma1, sma2))
cerebro = bt.Cerebro()data = bt.feeds.YahooFinanceData(dataname='YHOO', fromdate=datetime(2011, 1, 1), todate=datetime(2012, 12, 31))cerebro.adddata(data)cerebro.addstrategy(SmaCross)cerebro.run()cerebro.plot()编写策略及优化方案的实例 – BACKTRADER中文教程:https://www.itbook5.com/11757/
from __future__ import (absolute_import, division, print_function, unicode_literals)import argparseimport backtrader as btclass St0(bt.SignalStrategy): def __init__(self): sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=30) crossover = bt.ind.CrossOver(sma1, sma2) self.signal_add(bt.SIGNAL_LONG, crossover)class St1(bt.SignalStrategy): def __init__(self): sma1 = bt.ind.SMA(period=10) crossover = bt.ind.CrossOver(self.data.close, sma1) self.signal_add(bt.SIGNAL_LONG, crossover)class StFetcher(object): _STRATS = [St0, St1] def __new__(cls, *args, **kwargs): idx = kwargs.pop('idx') obj = cls._STRATS[idx](*args, **kwargs) return objdef runstrat(pargs=None): args = parse_args(pargs) cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname=args.data) cerebro.adddata(data) cerebro.addanalyzer(bt.analyzers.Returns) cerebro.optstrategy(StFetcher, idx=[0, 1]) results = cerebro.run(maxcpus=args.maxcpus, optreturn=args.optreturn) strats = [x[0] for x in results] # flatten the result for i, strat in enumerate(strats): rets = strat.analyzers.returns.get_analysis() print('Strat {} Name {}:\n - analyzer: {}\n'.format( i, strat.__class__.__name__, rets))def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for strategy selection') parser.add_argument('--data', required=False, default='../../datas/2005-2006-day-001.txt', help='Data to be read in') parser.add_argument('--maxcpus', required=False, action='store', default=None, type=int, help='Limit the numer of CPUs to use') parser.add_argument('--optreturn', required=False, action='store_true', help='Return reduced/mocked strategy object') return parser.parse_args(pargs)if __name__ == '__main__': runstrat()本想尝试signalStrategy结合optstrategy,实际发现
self.signal_add(bt.SIGNAL_LONG, DoubleMA_11,short_period=5,long_period=10)此函数不支持传参,所以无法使用optstrategy进行参数透传调优
部分信息可能已经过时