LOADING
3569 字
18 分钟
bt_02信号交易

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

曲线图:
del02
解释:
竖线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

曲线图
del02
区域尾部的折线意味着开仓信号 > 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

结果图:
del02
可见: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

结果 del02
分析:
竖线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

结果图:
del02
可见: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

结果:
del01

结论:最下面一列是金叉死叉信号,可见信号为点状信号(大部分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 argparse
import backtrader as bt
class 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 obj
def 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进行参数透传调优

bt_02信号交易
/posts/quant/f71235f5/
作者
思想的巨人
发布于
2023-05-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时