量化投资研究服务平台股票逐bar回测说明,我们通过一些具体应用来进行说明。
编写策略
编写一个完整的策略, 我们需要实现三个策略方法:
-
initialize初始化,配置策略参数
-
build_universe编写股票/品种筛选逻辑
-
handle_data 策略择时算法,一般情况,此处编写逐bar逻辑
以下,经典实用的 双均线策略 为例,看一下策略的基本编写方法。
交易单只股票
[1]:
"""
10日均线上穿50日均线,买入持有;
10日均线跌破50日均线,卖出清仓。
"""
import numpy as np
def initialize(context):
"""
初始化
"""
# 设置初始金额
context.initial_capital = 100000.
# 订阅行情, 分别指定 bar周期 与 股票列表
context.freq = "1D"
context.set_scope(["000001.SZ"])
# 设定参照基准
context.benchmark = "000016.SH"
# 设定回测起始时间
context.start_date = "20170104"
context.end_date = "20171231"
def build_universe(context):
"""
设定自选股票池
"""
context.universe.add(["000001.SZ"])
def handle_data(context, data):
"""
策略逻辑
"""
account = context.account
close_10 = data["000001.SZ"].close(1, 10) # 取前10日收盘价
close_50 = data["000001.SZ"].close(1, 50) # 取前50日收盘价
ma_10 = close_10.mean()
ma_50 = close_50.mean()
# 10日均线上穿50日均线买入
if account["000001.SZ"].position == 0 and ma_10 > ma_50:
# 全仓买入
context.order_target_percent("000001.SZ", 1.)
context.log("买入, 当前持仓{}手".format(account["000001.SZ"].position))
# 10日均线跌破50日均线卖出
if account["000001.SZ"].position > 0 and ma_10 < ma_50:
# 清仓
context.order_target_value("000001.SZ", 0)
context.log("卖出, 当前持仓{}手".format(account["000001.SZ"].position))
[2]:
%matplotlib inline
# 运行策略
from dwtrader.core.engine import Engine
engine = Engine()
engine.run()
engine.plot()
add_data in 0.754407 secs
2017-01-23 09:30:00 000001.SZ 可用资金不足, 期望下单112手, 调整至111手
2017-01-23 09:30:00 买入, 当前持仓111手
2017-03-27 09:30:00 卖出, 当前持仓0手
2017-06-07 09:30:00 买入, 当前持仓113手
run in 0.789520 secs
交易多只股票
我们也可以对一篮子股票进行组合回测。
[3]:
"""
10日均线上穿50日均线,买入持有;
10日均线跌破50日均线,卖出清仓。
"""
import numpy as np
def initialize(context):
"""
初始化
"""
# 设置初始金额
context.initial_capital = 100000.
# 订阅行情, 分别指定 bar周期 与 股票列表
context.freq = "1D"
context.set_scope(["000001.SZ", "601933.SH", "002230.SZ"])
# 设定参照基准
context.benchmark = "000016.SH"
# 设定回测起始时间
context.start_date = "20170104"
context.end_date = "20171231"
def build_universe(context):
"""
设定自选股票池
"""
context.universe.set(["000001.SZ", "601933.SH", "002230.SZ"])
def handle_data(context, data):
"""
策略逻辑
"""
account = context.account
for s in context.universe:
close_10 = data[s].close(1, 10) # 取前10日收盘价
close_50 = data[s].close(1, 50) # 取前50日收盘价
ma_10 = close_10.mean()
ma_50 = close_50.mean()
# 10日均线上穿50日均线买入
if account[s].position == 0 and ma_10 > ma_50:
# 全仓买入
context.order_target_percent(s, 0.33)
context.log("买入{}, 当前持仓{}手".format(s, account[s].position))
# 10日均线跌破50日均线卖出
if account[s].position > 0 and ma_10 < ma_50:
# 清仓
context.order_target_value(s, 0)
context.log("卖出{}, 当前持仓{}手".format(s, account[s].position))
在 handle_data 方法中,由于universe现在包含了多只股票,我们添加了一个循环,来逐个检查当前的股票是否满足我们的买卖逻辑。
多周期回测
回测中,我们可以订阅多个时间频率的数据,比如:context.freq = '60min', '1D'
, 即会同时加载60分钟和日线数据。订阅多周期数据源后,将以最小周期的bar的时间频率来触发 handle_data
逻辑。
详细示例可以参见示例策略文件夹中的 双均线策略-多周期.ipynb
动态选股
假设我们现在想要在中证500中,每月选取 PE 排名前10名的股票,就可以如下改写上例中的 build_universe 方法:
[4]:
"""
选择ep因子排名最后的50只股票,每月初调整股票池
10日均线上穿50日均线,买入持有;
10日均线跌破50日均线,卖出清仓。
"""
import numpy as np
def initialize(context):
"""
初始化
"""
# 设置初始金额
context.initial_capital = 10000000.
# 订阅行情, 分别指定 bar周期 与 股票列表
context.freq = "1D"
# 订阅所有中证500成分股行情数据
context.set_scope("000905.SH")
# 设定参照基准
context.benchmark = "000905.SH"
# 设定回测起始时间
context.start_date = "20170104"
context.end_date = "20171231"
# 设定股票池刷新频率
context.universe_freq = "monthly"
def build_universe(context):
"""
择股逻辑
"""
# 先读取当前的中证500成分股
zz500 = context.get_components("000905.SH")
# 获取ep因子
ep = context.factors["EP_TTM"]
# 从因子中选出排名前50的股票
my_basket = ep.top(50, codes=zz500)
context.universe.set(my_basket)
def handle_data(context, data):
"""
策略逻辑
"""
account = context.account
# 统计现在需要观察的所有股票
codes = list(set(context.universe + context.holdings))
for s in codes:
close_10 = data[s].close(1, 10) # 取前10日收盘价
close_50 = data[s].close(1, 50) # 取前50日收盘价
ma_10 = close_10.mean()
ma_50 = close_50.mean()
# 10日均线上穿50日均线买入
if account[s].position == 0 and ma_10 > ma_50:
# 全仓买入
context.order_target_percent(s, 0.02)
context.log("买入{}, 当前持仓{}手".format(s, account[s].position))
# 10日均线跌破50日均线卖出
if account[s].position > 0 and ma_10 < ma_50:
# 清仓
context.order_target_value(s, 0)
context.log("卖出{}, 当前持仓{}手".format(s, account[s].position))
# 卖出在持仓中但不在股票池中的股票
if s in context.holdings:
if s not in context.universe:
context.order_target_value(s, 0)
可以看到,我们首先要在 initialize 中指定股票池刷新频率,同时在 build_universe 方法中实现相应的选股逻辑。
还需要注意的是,由于现在股票池是动态变化的,我们账户中目前持有的股票,有可能并不在最新的股票池中,所以我们需要同时跟踪股票池和持仓列表中的所有股票。在上面的策略中,一旦有持仓中的股票跌出股票池,我们就会清掉该仓位,但实际上我们也可以选择继续持有,直到它们达到了我们预先设定的卖出条件。
导入本地数据文件
除了使用平台上提供的数据以外,也可以导入本地的数据文件,进行回测分析。
# 数据文件路径
path = 'ag1806_5min.csv'
# 传入数据属性,导入数据
context.load_local_data(
path_or_df=path,
freq='5min', # 数据周期
code='ag1806', # 交易代码
min_diff=1, # 最小变动价位
contract_unit=15, # 合约乘数
attributes={
'name': '白银1806',
}
)
1. 支持导入的数据格式
csv文件 或 pandas.DataFrame 数据对象
2. 导入bar数据(切片数据)
必须传入的字段
datetime
: 时间索引,若传入DataFrame
,须为 datetime
类型open
: 开盘价序列close
: 收盘价序列3. 导入tick数据(原始行情数据)
必须传入的字段
datetime
: 时间索引,若传入DataFrame
,须为 datetime
类型last_price
: 最新价序列4. 传入合约属性
合约属性作为参数传入(必填属性)
code
:合约交易代码freq
: 数据周期。bar数据如:‘5min’, ‘30min’, ‘1D’, tick数据:‘tick’min_diff
: 最小变动价位。如A股股票最小变动价位为0.01contract_unit
: 交易单位/合约乘数。如A股为100,50eft期权为10000其它属性可以通过 attributes
参数(dict类型)传入,策略内引用:data['ag1806'].get_attribute('name')
,返回 ‘白银1806’
5. 关于bar时间戳
使用因子
context.factors
可以获取到当前平台上的所有因子数据。Factor
, 如果传入多个因子名,将会返回一个复合因子(等权合成)。# 获取单因子对象
context.factors['EP_TTM']
# 获取复合因子对象
context.factors['MonthlyReturn','RoeGrowth1', 'MonthlyTradeAmount']
# 获取多个因子的DataFrame,每列对应一个单独的因子序列
# 一级索引为时间,二级索引为股票代码
context.factors.get_multiple('MonthlyReturn','RoeGrowth1', 'MonthlyTradeAmount')
因子对象常用方法
# 获取ep因子
ep = context.factors['EP_TTM']
# 选择ep因子中排名最前的50支股票,默认从全A股票中选择
ep.top(size=50)
# 传入股票列表,在给定范围中排序
sz50 = context.get_components('0000016.SH')
# 在上证50中,选出ep因子排名前10的股票
ep.top(size=10, codes=sz50)
# 获取原始因子值,类型为 pd.Series
ep_value = ep.value
关于原始因子的详细信息,请参考平台上的 因子说明文档 。
理解bar推送机制
bar,即K线数据,行情数据的切片。
目前支持的切片频率:
- 1分钟(‘1min’), 3分钟(‘3min’), 5分钟(‘5min’), N分钟(N必须能整除30,或为30的倍数,如10、15、30、60、120等)。
- 日线(‘1D’)及其倍数周期
数据推送时,按订阅的最小周期的bar的频率进行。如订阅的最小周期为 ‘5min’,则会每5分钟触发一次 handle_data
方法。
如在日线频率上(context.freq='1D'
), 假设当前的理论交易日为 20180314
,则在开盘的同一时刻(9:30),即会生成当日的bar数据,时间戳为datetime.datetime(2018, 3, 14, 9, 30)
,同时系统理论时间也随之更新,指向 datetime.datetime(2018, 3, 14, 9, 30)
。在实时行情中,该bar属于正在形成的bar,只有开盘价为有效历史数据,对应到回测中,当前bar总是指向未完成的bar,除开盘价以外,均属于未来数据。比如取最近的10周期的收盘价序列,应为 close(1, 10)
, 1表示向前回溯1个周期,若取 close(0, 10)
, 则该序列中会引用到当前bar的收盘价,即引入了未来数据。
close(1)
表示最近一个已完成的bar的收盘价数据,close(n)
表示最近第n个完成bar的收盘价数据,close(0)
指向当前正在更新的bar的收盘价。
分钟周期上同理,每个分钟的当前bar数据,也属于未完成的bar,在引用历史价格时,除开盘价外,均需向前回溯。
常用对象
context – 策略上下文
StrategyContext 的实例,用来在用户的策略方法中传递,方便存取策略数据。
一些常用属性
context.now # 当前理论时间
context.current_date # 当前交易日
context.pre_date # 上一交易日
context.pre_dt # 上一个时间戳
context.data # 数据源仓库(可以类似字典一样,根据标的代码与频率行索引)
context.universe # 自选池
context.holdings # 当前持仓列表
context.accout # 理论账户
context.initial_capital # 理论账户初始权益,等价于 context.account.initial_capital
context.current_capital # 理论账户当前权益,等价于 context.account.current_capital
context.factors # 因子库
data – 数据源仓库
数据源仓库,handle_data 方法中,默认会接收到该对象,也可以通过 context.data
获取。
datasource – 数据源
stock = data['000001.SZ']
, 即为获取到的平安银行的数据源对象。“datasource“ 的常用属性
stock.open # 开盘价序列
stock.high # 最高价序列
stock.low # 最低价序列
stock.close # 收盘价序列
stock.volume # 成交量序列
stock.time # 时间序列(如为bar数据,则对应bar的起始时间)
stock.barpos # 当前股票的bar位置索引,第一根bar返回1,后续依次递增
序列数据的索引方法(开盘价序列为例)
stock.open(0) # 返回最新bar开盘价
stock.open(1) # 返回第一根历史bar开盘价
# 例如获取平安银行昨日的开盘价
stock = context.data['000001.SZ']
last_open = stock.open(1)
序列数据的切片方法
stock.open(ago=0, size)
- ago: 切片起点据当前的位置,0表示当前位置,1表示上一根bar的位置,以此类推
- size: 切片的长度
# 例如取平安银行历史5天的收盘价序列
stock = context.data['000001.SZ']
close5 = stock.close(1, 5)
获取相应股票交易状态
stock.is_st # 属性方法,是否为 ST
stock.is_suspended # 属性方法,是否停牌
stock.is_delisted # 属性方法,是否已退市
stock.ipo_date # 属性方法,上市日期
context.universe – 股票自选池
选股逻辑(build_universe
) 中的最终结果,应存入 context.universe
中,方便在择时逻辑(handle_data
)中引用。
stocks = context.get_components('0000016.SH')
context.universe.add(stocks) # 在现有自选池中加入新选股票
context.universe.set(stocks) # 重置自选池,加入新选股票
context.universe.drop_st() # 去除ST股票
context.universe.drop_suspended() # 去除停牌股票
context.universe.drop_delisted() # 去除退市股票
universe(自选池)的更新机制
可以在 build_universe
方法中定义选股逻辑,定期更新自选池。更新周期通过 context.universe_freq
设置,常用的如 'daily', 'weekly', 'monthly'
等,详见 API简介 – context.universe_freq 部分。
自选池更新的最小频率为 'daily'
。在更新日当天,build_universe
函数会在开盘前触发,此时当前理论时间(context.now
)已经更新, 但因为尚未开盘,可以通过 data['000001.SZ'].time(0)
看到,数据时间戳依然指向上一个交易日。同理,data['000001.SZ'].close(0)
等也均返回上一个交易日的价格数据。
context.account – 账户
可以查询理论账户的当前权益、可用余额等
context.account.initial_capital # 理论账户初始权益
context.account.current_capital # 理论账户当前权益
context.account.cash # 理论账户可用现金
我们可以从账户中查询特定股票的持仓情况
context.account['000001.SZ'].position # 当前持仓手数,0表示空仓,负数表示持有空头仓位
context.account['000001.SZ'].entry_price # 获取最近一次的开仓价格
context.account['000001.SZ'].exit_price # 获取最近一次的平仓价格
context.account['000001.SZ'].entry_time # 获取最近一次的开仓时间
context.account['000001.SZ'].exit_time # 获取最近一次的平仓时间
context.account['000001.SZ'].first_entry_price # 若有加仓,返回当前持仓中的首次开仓价格
context.account['000001.SZ'].first_exit_price # 若有减仓,返回当前持仓中的首次平仓价格
context.account['000001.SZ'].first_entry_time # 若有加仓,返回当前持仓中的首次开仓时间
context.account['000001.SZ'].first_exit_time # 若有减仓,返回当前持仓中的首次平仓时间
常见问题
个别股票停牌时,返回的价格序列会与其它股票数据保持对齐吗?
不会对齐。例如,取 close_10 = data['000001.SZ', '1D'].close(1, 10)
,如果该股票过去10个交易日中发生过停牌,则 close_10
中会跳过该停牌日期继续往前取值,尽可能返回一个长度为10的numpy
数组(如果历史数据足够,历史数据不足则最终的数组长度会小于10)。如果其它股票并未发生过停牌,则二者的 close_10
实际指向了不同的交易时间段。
如何对齐不同股票的价格序列?
时间戳本身也是一个序列,time_10 = data['000001.SZ', '1D'].time(1, 10)
,利用pandas可以很简单的生成时间序列 series_10 = pd.Series(close_10, index=time_10)
, 从而可以进行各种时间序列相关的处理。
API简介
以下方法默认通过策略方法中传递的 context 调用
set_cost – 设置交易费用
set_cost(ratio=0., slippage=0)
暂用
参数
- ratio: 每次交易的开平仓交易费率,买入卖出采用相同费率
- slippage:每次交易的滑点个数
示例
# 每次交易收取万二手续费,设置2个滑点
context.set_cost(ratio=0.0002, slippage=2)
benchmark – 设置参照基准
context.benchmark = '000016.SH'
set_scope – 设置股票数据订阅范围
context.set_scope(index)
参数
- index: 股票指数,类型 str;或传入股票列表,类型 list
说明
- 如果传入指数代码,则会加载回测区间内该指数的所有历史成分股
示例
# 订阅上证50成分股行情数据
context.set_scope('000016.SH')
# 订阅列表中的所有股票数据
context.set_scope(['000001.SZ', '002230.SZ', '600000.SH'])
freq – 设置策略运行周期
context.freq = ‘1D’
默认为日线级别 '1D'
。
可用周期:
- 1分钟(‘1min’), 3分钟(‘3min’), 5分钟(‘5min’), N分钟(N必须能整除30,或为30的倍数,如10、15、30、60、120等)。
- 日线(‘1D’)及其倍数周期
可以同时设置多个运行周期:
# 将同时订阅 10分钟 和 日线 bar数据
context.freq = '10min', '1D'
universe_freq – 设置股票池刷新频率
context.universe_freq = 'monthly'
可选项
- daily: 每日开盘前选股
- weekly: 每周选股
- week_start: 每周第一个交易日选股,与weekly等价
- week_end: 每周最后一个交易日选股
- monthly: 每月选股
- month_start: 每月第一个交易日选股,与monthly等价
- month_end: 每月最后一个交易日选股
get_components – 获取成分股
get_components(category, date=None, weights=False)
参数
- category: 分类代码,比如沪深300的 category 为 ‘000300.SH’
- date: 日期, 类型为 datetime.datetime,默认取当前交易日(回测理论时间)
- weights: 是否返回权重
返回值
- 默认返回成分股 list , 若 weights=True , 返回 pandas.DataFrame 对象
示例
# 获取当前沪深300成分股
context.get_components('000300.SH')
factors – 获取因子
factors 为因子仓库
设置参数
# 如需设置参数,可以在 initialize 接口方法中统一设置:
context.factors.set_params({
'MonthlyReturn': {
'factor_direction': 1
},
'RoeGrowth1': {
'factor_direction': -1
},
'MonthlyTradeAmount': {
'factor_direction': 1
}
})
获取因子
- 可以直接通过因子名来对因子仓库进行索引:context.factors[‘RoeGrowth1’]
- 传入多个因子名,则会返回复合因子
示例
# 获取因子 EP_TTM
pe = context.factors['EP_TTM']
# 获取复合因子
trinity = context.factors[
'MonthlyReturn',
'RoeGrowth1',
'MonthlyTradeAmount'
]
load_local_data – 加载本地行情数据文件
load_local_data(path_or_df, code, min_diff, contract_unit, attributes=None)
参数
- path_or_df: str 类型,csv文件地址;或 pd.DataFrame 数据对象
- code: 合约交易代码,str 类型
- freq: 数据周期/频率,str 类型
- min_diff: 最小变动价位,float 类型
- contract_unit: 交易乘数/合约单位 float/int 类型
- attributes: 自定义属性,dict 类型,可选参数
示例
context.load_local_data(
path_or_df='path/to/data/ag1806.csv',
freq='5min',
code='ag1806',
min_diff=1,
contract_unit=15,
attributes={
'name': '白银1806',
'end_date': datetime.datetime(2018, 6, 15)
}
)
order_target_value – 按目标金额调仓
order_target_value(stock, target, price=None)
参数
- stock: 股票代码或数据源对象,类型 str / DataSource
- target: 目标金额(单位:元)
- price: 报单价格,默认为None,即使用当前开盘价(加滑点)报单
返回值
- 暂时返回None,后续更新会返回报单结构
示例
# 已当前开盘价买入价值10000元的平安银行股票
context.order_target_value('000001.SZ', 10000.)
order_target_percent – 按目标比例调仓
order_target_percent(stock, target, price=None)
参数
- stock: 股票代码或数据源对象,类型 str / DataSource
- target: 目标持仓比例,为当前股票价值占账户总权益之比
- price: 报单价格,默认为None,即使用当前开盘价(加滑点)报单
返回值
- 暂时返回None,后续更新会返回报单结构
示例
# 已当前开盘价买入价值为1%账户总额的的平安银行股票
context.order_target_value('000001.SZ', 0.1)
order_tartget_size – 按目标手数调仓
order_target_percent(stock, target, price=None)
参数
- stock: 股票代码或数据源对象,类型 str / DataSource
- target: 目标持仓比例,为当前股票价值占账户总权益之比
- price: 报单价格,默认为None,即使用当前开盘价(加滑点)报单
返回值
- 暂时返回None,后续更新会返回报单结构
示例
# 已当前开盘价买入价值为1%账户总额的的平安银行股票
context.order_target_value('000001.SZ', 0.1)
context.data
属性方法,指向数据源仓库
示例
# 获取平安银行数据源对象,默认使用策略的timeframe
stock = context.data['000001.SZ']
# 获取平安银行1小时数据的数据源对象
stock_1H = context.data['000001.SZ', '1H']
获取当前(理论)时间
context.now
datetime.datetime
context.current_date
datetime.datetime
context.pre_date
datetime.datetime
factors.get_multiple – 获取多个因子数据
factors.get_multiple(*factor_names)
参数
- factor_names: 多个因子名称,逗号隔开即可,类型为 str
返回值
- pd.DataFrame, 索引为MultiIndex, 一级索引为时间,二级索引为股票代码, 每列对应一个因子序列
示例
# 获取一个包含指定的三个因子的dataframe
factors.get_multiple(
'MonthlyReturn',
'RoeGrowth1',
'MonthlyTradeAmount'
)
facotr.value – 返回原始因子序列
factor.value
返回值
- 原始因子序列,类型 pd.Series,索引为MultiIndex, 一级索引为时间,二级索引为股票代码
factor.top – 选取因子排名最前的N只股票
factor.top(size, date=None, codes=None, as_list=True)
参数
- size: 需要选出的股票数量,类型 int
- date: 日期,类型 datetime.datetime,默认为上一个交易日
- codes: 接收一个股票列表作为排序范围, 类型 list, 默认从全A股票中选择
- as_list: 结果是否作为列表返回,默认为True,为False将返回一个 pd.Series, 索引为股票代码,值为因子取值
返回值
- list / pd.Series
示例
# 从全A中选择 ep因子 排名最前的50只股票
ep = context.factors['EP_TTM']
top50 = ep.top(50)
factor.bottom – 选取因子排名最后的N只股票
factor.bottom(size, date=None, codes=None, as_list=True)
同 factor.top
factor.get_value – 获取因子数据
factor.get_value(date=None)
参数
- date: 日期,类型 datetime.datetime,默认为上一个交易日
返回值
- 返回某日(末日返回上个交易日)因子数据,index为股票代码,类型 pd.Series
示例
# 获取上一交易日因子数据
ep = context.factors['EP_TTM']
ss = ep.get_value()
factor.get_stock_weights – 生成股票权重
factor.get_weighted_stocks(size, date=None, codes=None, weighting=None)
根据因子数据,生成理论持仓权重
参数
- size: 需要选出的股票数量,类型 int
- date: 日期,类型 datetime.datetime,默认为上一个交易日
- codes: 接收一个股票列表作为排序范围, 类型 list, 默认从全A股票中选择
- weighting: 股票权重分配方法,默认使用分层加权
- ‘LayeredWeighting’: 分层加权
- ‘EqualWeighting’: 同等权重
- ‘TotalMktValueWeighting’: 总市值加权
- ‘TotalMktValueWeighting’: 流通市值加权
返回值
- 返回股票权重列表,类型 pd.Series
示例
# 在中证500中,根据ep因子,得出当日股票持仓权重
zz500 = context.get_components('000905.SH')
ep = context.factors['EP_TTM']
weights = ep.get_stock_weights(100, codes=zz500)