之前的文章,介绍了回测系统的总体架构,以及DataFeed的核心代码。这篇文章重点讲目前这个回测框架最有意思的地方——策略模块化。就是我们不需要写大量的模板代码去实现一个策略,基本重点的模块都拆分成了小块,像搭积木一下,重新组合起来即可。
aogos.py
Algo是一个基类,只实现了一个抽象函数call。
'''
算法基类,是一个可以复用的功能单元。
代码很简单,只一个name属性,以及一个需要子类实现的功能__call__。
'''
class Algo(object):
def __init__(self, name=None):
self._name = name
@property
def name(self):
if self._name is None:
self._name = self.__class__.__name__
return self._name
def __call__(self, context):
raise NotImplementedError("%s 没有实现!" % self.name)
我们看一个最简单的Alog实现PrintBar——就是把当期context里的一些变量打印出来。模块化算法,最核心的就是把各块之间的传递,以及与回测框架环境的交互放在context里,context是一个dict类型。回测每走一步,会更新context里相应的数据,比如最新的idx,now等。
class PrintBar(Algo):
def __call__(self, context):
logger.info('当前索引:{},当前日期:{}'.format(context['idx'],context['now']))
return True
再个一个选择集合里全部股票的Algo——SelectAll。 其实就是把context['LONG'] = context['universe'],就是生成LONG指令,目标是全体标的。
class SelectAll(Algo):
def __init__(self):
super(SelectAll, self).__init__()
def __call__(self,context,direction='LONG'):
context[direction] = context['universe']
return True
SelectWhere是按信号选股,这个计算会复杂一些。 初始化传入的参数是signal,signal是dataframe类型。df:['AAPL':[1,0,-1],'AMZN':[…]],每列对应相应证券的交易信号,1为买入,0为不变,-1为平仓。根据信号计算结果,保存在context['LONG'],context['FLAT']里。
class SelectWhere(Algo):
def __init__(self, signal):
self.signal = signal #df:['AAPL':[1,0,-1],'AMZN':[...]]
def __call__(self, context):
#这里得到某一天的信号,是一个Series, index = ['AAPL'...]
day_signal = self.signal.loc[context['now']]
#LONG or FLAT
day_signal_long = day_signal[day_signal==1]
day_signal_flat = day_signal[day_signal == -1]
#按方向过滤完信号后,取索引就是证券代码列表
selected = day_signal.index
context['LONG'] = list(day_signal_long.index)
context['FLAT'] = list(day_signal_flat.index)
return True
SelectByExpr在SelectWhere的基础上更进一步,它根据表达式生成交易信号。表达式示例:
表示5日与10日均线金叉时买入,死叉时卖出。
long_expr = 'cross_up(ma(close,5),ma(close,10))'
flat_expr = 'cross_down(ma(close,5),ma(close,10))'
算法会根据表达式,计算出signal,然后在算法内部调用SelectWhere生成交易指令。
class SelectByExpr(Algo):
def __init__(self,long_expr,flat_expr):
self.long_expr = long_expr
self.flat_expr = flat_expr
self.run_once = RunOnce()
def __call__(self, context):
if self.run_once(context) is True: #运行过了,会访问False表示不用继续,本算法返回True,continue
codes = context['universe']
all_close = context['all_close']
all_data = context['all_data']
# price_keys= ['open','high','low','close']
sig = pd.DataFrame(index=all_close.index, columns=all_close.columns)
for symbol in codes:
df = all_data[symbol]
close = df['Close']
high = df['High']
low = df['Low']
open = df['Open']
long_sig = eval(self.long_expr) # eval('cross_up(ma(close,5),ma(close,10')
flat_sig = eval(self.flat_expr) # eval('cross_down(ma(close,5),ma(close,10)')
sig[symbol] = long_sig + flat_sig
context['sig'] = sig
#print(sig[sig>0])
return True
SelectWhere(signal=context['sig'])(context)
return True
WeighEqually是等权分配cash的算法,结果变量保存在context['weights']里。
class WeighEqually(Algo):
def __init__(self):
super(WeighEqually, self).__init__()
def __call__(self, context):
#FLAT不用权重,这个列表里的都平仓,rebalance会自动处理
if 'LONG' in context.keys():
selected = context['LONG']
n = len(selected)
if n == 0:
context['weights'] = {}
else:
w = 1.0 / n
context['weights'] = {x: w for x in selected}
return True
有了上面列举的模块,我们就可以轻松实现经典的回测交易算法。
buy_and_hold = Strategy([
RunOnce(),
PrintBar(),
SelectAll(),
WeighEqually(),
], name='买入并持有-基准策略')
long_expr = 'cross_up(ma(close,5),ma(close,10))'
flat_expr = 'cross_down(ma(close,5),ma(close,10))'
ma_cross = Strategy([
SelectByExpr(long_expr=long_expr, flat_expr=flat_expr),
WeighEqually(),
], name='均线交叉策略')
明天我们继续讲解投资组合管理类——Portfolio。