从研究中启用回测能同时开启多个回测,不仅可以省去在回测模块一次次重复设定的麻烦,还可以将回测结果调回研究模块进行因子分组和参数优化分析,功能十分强大,本文主要介绍一下如何使用这个功能。
聚宽平台打通研究模块和回测模块的功能已经上线一段时间了,但是还有一些同学不太清楚具体该怎么使用,为此本文从零开始简单介绍一下如何使用这个功能,希望能帮助到有类似困惑的同学。已经对这个功能较为熟悉的同学可以直接参阅第三部分或本文参考的其他社区优秀帖子。在本文末尾链接的量化课堂文章里有泛用性更广、功能更强的多回测框架,在熟悉本文前三部分的内容之后可以进一步研究使用该框架。
刚使用聚宽平台的同学大概还没使用过或者不太熟悉回测功能,因此在一开始没有回测的情况下,需要点击页面上方“我的策略”里的“策略列表”,新建任何一个策略并点击运行回测,这样我们就得到了一个策略和它的一个回测结果。
在“回测详情”页面的上方地址栏末尾就能找到回测ID如:backtestId = daa0d1764a2f0a36e3339e78dc676435
在回测列表里也能找到历史回测的结果,同样的方法也能获得历史回测的ID。
再点击“编辑策略”回到策略页面,同样在上方地址栏末尾就能找到策略ID如:algorithmId = dd99237f48b472aa0d0beee15324b9a2
对于已有策略也可以用同样的方式获取策略ID。
利用策略ID在研究里调用创建回测函数:
create_backtest(algorithm_id, start_date, end_date, frequency="day", initial_cash=10000,
initial_positions=None, extras=None, name=None, code="", benchmark=None)
这里的参数:
initial_positions = [
{
'security':'000001.XSHE',
'amount':'100',
},
{
'security':'000063.XSHE',
'amount':'100',
'avg_cost': '1.0'
}
]
函数返回回测ID,即 backtest_id
create_backtest 函数示例如下:
code = """
# 导入函数库
from jqdata import *
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 输出内容到日志 log.info()
log.info('初始函数开始运行且全局只运行一次')
# 过滤掉order系列API产生的比info级别低的log
# log.set_level('order', 'info')
### 股票相关设定 ###
# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
# 开盘前运行
run_daily(before_market_open, time='09:00', reference_security='000300.XSHG')
# 开盘时运行
run_daily(market_open, time='09:30', reference_security='000300.XSHG')
# 收盘后运行
run_daily(after_market_close, time='15:30', reference_security='000300.XSHG')
## 开盘前运行函数
def before_market_open(context):
# 输出运行时间
log.info('函数运行时间(before_market_open):' str(context.current_dt.time()))
# 给微信发送消息(添加模拟交易,并绑定微信生效)
send_message('美好的一天~')
# 要操作的股票:平安银行(g.为全局变量)
g.security = '000001.XSHE'
## 开盘时运行函数
def market_open(context):
log.info('函数运行时间(market_open):' str(context.current_dt.time()))
security = g.security
# 获取股票的收盘价
close_data = attribute_history(security, 5, '1d', ['close'])
# 取得过去五天的平均价格
MA5 = close_data['close'].mean()
# 取得上一时间点价格
current_price = close_data['close'][-1]
# 取得当前的现金
cash = context.portfolio.available_cash
# 如果上一时间点价格高出五天平均价1%, 则全仓买入
if current_price > 1.01*MA5:
# 记录这次买入
log.info("价格高于均价 1%%, 买入 %s" % (security))
# 用所有 cash 买入股票
order_value(security, cash)
# 如果上一时间点价格低于五天平均价, 则空仓卖出
elif current_price < MA5 and context.portfolio.positions[security].closeable_amount > 0:
# 记录这次卖出
log.info("价格低于均价, 卖出 %s" % (security))
# 卖出所有股票,使这只股票的最终持有量为0
order_target(security, 0)
## 收盘后运行函数
def after_market_close(context):
log.info(str('函数运行时间(after_market_close):' str(context.current_dt.time())))
#得到当天所有成交记录
trades = get_trades()
for _trade in trades.values():
log.info('成交记录:' str(_trade))
log.info('一天结束')
log.info('##############################################################')
"""
algorithm_id = "xxxx" #用自己的策略ID
extra_vars = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
initial_positions = [
{
'security':'000001.XSHE',
'amount':'100',
},
{
'security':'000063.XSHE',
'amount':'100',
'avg_cost': '1.0'
},
]
params = {
"algorithm_id": algorithm_id,
"start_date": "2015-10-01",
"end_date": "2016-07-31",
"frequency": "day",
"initial_cash": "1000000",
"initial_positions": initial_positions,
"extras": extra_vars,
}
created_bt_id = create_backtest(code=code, **params)
在研究中还可以调用回测结果进行参数优化等进一步分析工作,用到的函数为:
gt = get_backtest(backtest_id)
这里的backtest_id即回测ID,在上一部分示例中即为created_bt_id。
结果调用方法:
示例如下:
gt = get_backtest(created_bt_id) #使用自己的回测ID
gt.get_status() # 获取回测状态
gt.get_params() # 获取回测参数
gt.get_results() # 获取收益曲线
gt.get_positions() # 获取所有持仓列表
gt.get_orders() # 获取交易列表
gt.get_records() # 获取所有record()记录
gt.get_risk() # 获取总的风险指标
gt.get_period_risks() # 获取分月计算的风险指标
# 此处函数的示例结果附在文章末尾
这里我们借助MACD指标实际展示一下利用该功能进行参数调优:
首先在回测模块调试代码,使其在调参过程中也可正常运行。本示例中调好的代码如下:
code = '''
# 导入函数库
from jqlib.technical_analysis import *
import datetime as dt
import pandas as pd
#初始化函数,设定基准等等
def initialize(context):
#选取上证50
g.stock = '000016.XSHG'
#设定标的本身作为基准
set_benchmark(g.stock)
#开启动态复权模式(真实价格)
set_option('use_real_price', True)
#过滤日志
log.set_level('order', 'error')
#设置默认参数
g.s = 12
g.l = 26
g.m = 9
#开盘前运行
run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
#开盘时运行
run_daily(market_open, time='open', reference_security='000300.XSHG')
#开盘前运行函数
def before_market_open(context):
#设置滑点、手续费
set_slip_fee(context)
#获取MACD信息
dif1, dea1, macd1 = MACD(g.stock, check_date = context.previous_date - dt.timedelta(days = 1), SHORT = g.s, LONG = g.l, MID = g.m)
dif2, dea2, macd2 = MACD(g.stock, check_date = context.previous_date , SHORT = g.s, LONG = g.l, MID = g.m)
#初始化信号变量
g.signal = False
#金叉买入、死叉卖出、否则不调仓
if dif1 >= dea1 and dif2 <= dea2 :
g.signal = 'sell'
elif dif1 <= dea1 and dif2 >= dea2:
g.signal = 'buy'
else:
g.signal = False
#根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
#将滑点设置为0
set_slippage(FixedSlippage(0))
#根据不同的时间段设置手续费
dt=context.current_dt
if dt>datetime.datetime(2013,1, 1):
set_commission(PerTrade(buy_cost=0.0003,
sell_cost=0.0013,
min_cost=5))
elif dt>datetime.datetime(2011,1, 1):
set_commission(PerTrade(buy_cost=0.001,
sell_cost=0.002,
min_cost=5))
elif dt>datetime.datetime(2009,1, 1):
set_commission(PerTrade(buy_cost=0.002,
sell_cost=0.003,
min_cost=5))
else:
set_commission(PerTrade(buy_cost=0.003,
sell_cost=0.004,
min_cost=5))
## 开盘时运行函数
def market_open(context):
#根据信号买入卖出标的
if g.signal == 'sell':
order_target_value(g.stock, 0)
elif g.signal == 'buy':
order_target_value(g.stock, context.portfolio.total_value)
else:
pass
## 收盘后运行函数
def after_market_close(context):
pass
'''
此处若以 遍历法 进行调优将得到最佳结果,但过程相当繁琐,有兴趣的同学可自己尝试一下。本文仅以示范目的,逐一对参数进行调优展示。
首先对第一个参数进行调优,一般以默认参数为中心,向两侧扩大范围尝试得到最优结果,跳过过程直接给出得到的最优参数为6,并且在进一步对后两个参数分别调优时发现,后两个参数不需改变,因此最优参数为(6,26,9)。运行下面代码可以看到右下角同时进行多个回测。
调优示例代码如下:
created_bt_ids = []
#以默认值为中心进行调优,每次最多进行10组回测
for i in range(-8, -3):
algorithm_id = "04da82c6238644df6f542f4332d911bc" #用自己的策略ID
#设置回测内全局变量参数
extra_vars = {'stock': '000016.XSHG', 's': 12 i, 'l': 26, 'm': 9}
params = {
"algorithm_id": algorithm_id,
"start_date": "2016-01-01",
"end_date": "2018-12-31",
"frequency": "day",
"initial_cash": "1000000",
"initial_positions": None,
"extras": extra_vars,
"name" : 'stock: 000016.XSHG' ', s:' str(12 i) ', l:26' ', m:9'
}
created_bt_ids.append(create_backtest(code = code, **params))
import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
b_return.append(r['benchmark_returns'])
t.append(r['time'])
#建立df存储数据
data = pd.DataFrame(index = t)
data['b_return'] = b_return
#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
gt = get_backtest(backtest_id = created_bt_ids[i])
res = gt.get_results()
name = gt.get_params()['name']
s_return = []
for r in res:
s_return.append(r['returns'])
data[name] = s_return
data.plot(figsize = (15,10))
从上图可以看出红线即参数为(6,26,9)时收益曲线表现最好,同时大致能看出几乎在所有参数下,MACD在2016年表现还可以,能拉开与蓝线(基准收益)的差距,但是在2016年以后表现越来越差。
另外,我们再展示一下从第二个参数开始调优的情况,一样先固定另两个参数,调整第二个参数得到最优结果为15,再分别调整另两个参数后确定最优结果为(12,15,9),示例代码如下:
created_bt_ids = []
#以默认值为中心进行调优,每次最多进行10组回测
for i in range(-13, -8):
algorithm_id = "04da82c6238644df6f542f4332d911bc" #用自己的策略ID
extra_vars = {'stock': '000016.XSHG', 's': 12, 'l': 26 i, 'm': 9}
params = {
"algorithm_id": algorithm_id,
"start_date": "2016-01-01",
"end_date": "2018-12-31",
"frequency": "day",
"initial_cash": "1000000",
"initial_positions": None,
"extras": extra_vars,
"name" : 'stock: 000016.XSHG' ', s:12' ', l:' str(26 i) ', m:9'
}
created_bt_ids.append(create_backtest(code = code, **params))
import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
b_return.append(r['benchmark_returns'])
t.append(r['time'])
#建立df存储数据
data = pd.DataFrame(index = t)
data['b_return'] = b_return
#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
gt = get_backtest(backtest_id = created_bt_ids[i])
res = gt.get_results()
name = gt.get_params()['name']
s_return = []
for r in res:
s_return.append(r['returns'])
data[name] = s_return
data.plot(figsize = (15,10))
这一组整体效果比前一组稍微好一点,但是依然能得到MACD在2016年表现尚可,2016年以后表现不及基准(蓝线)的结论。同时, 两种调优顺序结果不同,也证明此处调优应采用遍历法 得到最优结果,目前得到的两组最优结果可能只是局部最优。
这里我们再展示一下如何利用研究调用回测功能进行因子分组测试:
示例代码如下:
code2 = '''
# 导入函数库
from jqdata import *
from jqfactor import *
import datetime as dt
import numpy as np
import pandas as pd
import time
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
log.set_level('order', 'error')
#第几组
g.group = 1
## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
# 开盘前运行
run_monthly(before_market_open, monthday = 1, time='before_open', reference_security='000300.XSHG')
# 开盘时运行
run_monthly(market_open, monthday = 1, time='open', reference_security='000300.XSHG')
## 开盘前运行函数
def before_market_open(context):
#设置滑点、手续费
set_slip_fee(context)
#取沪深300作为股票池
all_stocks = get_index_stocks('000300.XSHG', date = context.current_dt)
feasible_stocks = set_feasible_stocks(context, all_stocks)
#查询市净率、代码
q = query(valuation.pb_ratio, valuation.code).filter(valuation.code.in_(feasible_stocks))
factor = get_fundamentals(q, context.current_dt)
#整理df
factor.index = factor['code']
factor['BP'] = 1/factor['pb_ratio']
del factor['pb_ratio'], factor['code']
#去极值
factor = winsorize(factor, scale = 3, axis = 0)
#中性化
factor = neutralize(factor, how = ['sw_l1', 'market_cap'], date = context.current_dt, axis = 0, fillna = 'sw_l1')
#标准化
factor = standardlize(factor, axis = 0)
#排序
factor.sort(columns = ['BP'], ascending = True, inplace = True)
n = int(len(factor)/10)
#分组取样
if g.group == 10:
g.tobuy_list = factor.index[(g.group - 1) * n :]
else:
g.tobuy_list = factor.index[(g.group - 1) * n : g.group * n]
#1
#设置可行股票池,剔除(金融类、)st、停牌股票,输入日期
def set_feasible_stocks(context,s):
#s = get_index_stocks('000905.XSHG', date=context.current_dt)
#print '输入股票个数为:%s'%len(s)
all_stocks = s
#得到是否停牌信息的dataframe,停牌得1,未停牌得0
suspended_info_df = get_price(list(all_stocks), end_date = context.current_dt, count = 1, frequency = 'daily', fields = 'paused')['paused'].T
#过滤未停牌股票 返回dataframe
suspended_index = suspended_info_df.iloc[:,0] == 1
#得到当日停牌股票的代码list:
suspended_stocks = suspended_info_df[suspended_index].index.tolist()
#剔除停牌股票
for stock in suspended_stocks:
if stock in all_stocks:
all_stocks.remove(stock)
#print '剔除后股票个数为:%s'%len(all_stocks)
return all_stocks
# 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
# 将滑点设置为0
set_slippage(FixedSlippage(0))
# 根据不同的时间段设置手续费
dt=context.current_dt
if dt>datetime.datetime(2013,1, 1):
set_commission(PerTrade(buy_cost=0.0003,
sell_cost=0.0013,
min_cost=5))
elif dt>datetime.datetime(2011,1, 1):
set_commission(PerTrade(buy_cost=0.001,
sell_cost=0.002,
min_cost=5))
elif dt>datetime.datetime(2009,1, 1):
set_commission(PerTrade(buy_cost=0.002,
sell_cost=0.003,
min_cost=5))
else:
set_commission(PerTrade(buy_cost=0.003,
sell_cost=0.004,
min_cost=5))
## 开盘时运行函数
def market_open(context):
#调仓,先卖出股票
for stock in context.portfolio.long_positions:
if stock not in g.tobuy_list:
order_target_value(stock, 0)
#再买入新股票
total_value = context.portfolio.total_value # 获取总资产
for i in range(len(g.tobuy_list)):
value = total_value / len(g.tobuy_list) # 确定每个标的的权重
order_target_value(g.tobuy_list[i], value) # 调整标的至目标权重
#查看本期持仓股数
print(len(context.portfolio.long_positions))
## 收盘后运行函数
def after_market_close(context):
pass
'''
created_bt_ids = []
#分十组进行回测
for i in range(1, 11):
algorithm_id = "d888d5a266b0f28f167d52a97a73dd31" #用自己的策略ID
extra_vars = {'group': i}
params = {
"algorithm_id": algorithm_id,
"start_date": "2016-01-01",
"end_date": "2018-12-31",
"frequency": "day",
"initial_cash": "3000000",
"initial_positions": None,
"extras": extra_vars,
"name" : 'BP group:' str(i)
}
created_bt_ids.append(create_backtest(code = code2, **params))
import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
b_return.append(r['benchmark_returns'])
t.append(r['time'])
#建立df存储数据
data = pd.DataFrame(index = t)
data['b_return'] = b_return
#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
gt = get_backtest(backtest_id = created_bt_ids[i])
res = gt.get_results()
name = gt.get_params()['name']
s_return = []
for r in res:
s_return.append(r['returns'])
data[name] = s_return
data.plot(figsize = (15,10), colormap = 'Spectral')
上方棕色线是基准线,蓝紫色线是第10组,对比这两条线能看出,在2017年上半年BP因子相对大盘能拉开明显的差距,而下半年差距被缩小至不复存在。2016年和2018年这两年两条线几乎不能拉开差距,说明这两年BP因子单独使用时获取超额收益效用不大。但是同时注意到下方两条线是第1组和第2组的收益曲线,说明BP因子多空差距明显,在2016年至2018年三年间即使不能相对大盘获得显著超额收益,也能防止空头出现的大幅回撤,控制风险。
另外,下方代码给出了十分组夏普比率(左)和最大回撤(右)情况,能明显看出第1组夏普比率和最大回撤都是最差的,而第10组都是相对较优的,多空组合间分化还是比较明显的。
sharpe = []
max_drawdown = []
name = range(1, 11)
for i in range(len(created_bt_ids)):
gt = get_backtest(backtest_id = created_bt_ids[i])
sharpe.append(gt.get_risk()['sharpe'])
max_drawdown.append(gt.get_risk()['max_drawdown'])
sharpe = pd.Series(sharpe, index = name)
max_drawdown = pd.Series(max_drawdown, index = name)
sharpe.plot.bar()
max_drawdown.plot.bar()
实际上聚宽量化课堂已经为用户准备了泛用性更广、功能更强的多回测框架。该框架能实现诸如,需要运行超过10个回测时可自动操作、轻松实现遍历法以及优化数据可视化等等功能。对该框架感兴趣的同学建议拓展阅读:【量化课堂】多回测运行和参数分析框架
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...