我们一般使用AKShare这个库来获取股票数据或策略中用得到的数据:
AKShare github主页:https://github.com/akfamily/akshare
使用Backtrader框架作为回测的框架:
Backtrader github主页:https://github.com/mementum/backtrader
使用quantstats库作为回测结果评价的库:
quantstats github主页:https://github.com/ranaroussi/quantstats
这一部分准备好之后,后续我们将关注点主要放在【策略】上,对于数据、评价指标这些如无特殊处理,将不再赘述。整个量化的框架构造起来不太容易,如果以前有所了解,可以用自己习惯的方式;如果觉得困难较大,也可以先跳过,等后面能力够了之后,再上手构建。
ps:大家要慎重的使用网上的量化平台,因为偷策略这种事太正常了,大家还是最好自己本地搭一个测试的平台~
文章目录
- 1. 数据准备
- 2. Backtrader回测框架准备
- 3. 评价指标
下面的数据准备与Backtrader回测准备,只是博主提供的一个参考,在开始正式介绍量化策略的时候是不会涉及到每个数据的采集,手把手的代码,这些都是不会提及的,只会提供一个backtrader的策略类,作为对策略的编程实现。
1. 数据准备
比如股票数据:
-
首先创建一个
data
文件夹,然后在文件夹里创建一个stock_data
的文件夹 -
创建一个
code
文件夹用来存放程序文件 -
然后新建一个python文件,使用如下代码:
import time import akshare as ak from tqdm import tqdm from loguru import loggerdef extract_data():start_date = "20150101"end_date = "20221101"stock_list = ak.stock_zh_a_spot_em() # 东方财富网-沪深京 A 股-实时行情数据for stock_code in tqdm(stock_list['代码']):time.sleep(1)stock_df = ak.stock_zh_a_hist(symbol=stock_code, period="daily", start_date=start_date, end_date=end_date, adjust="hfq") # 后复权stock_df.to_pickle("../data/stock_data/{}.pkl".format(stock_code))logger.debug("ADD DATA {}", stock_code)if __name__ == '__main__':extract_data()
这里博主把股票数据保存到data/stock_data/
文件夹下,以股票代码.pkl
的格式保存:
2. Backtrader回测框架准备
Backtrader足够简单,同时也非常接近实盘(国外可以一键切换实盘,国内没有接口)
Backtrader的使用请参考:Backtrader量化&回测1——基本的交易策略与挂单买卖
从策略到最终影响金额,都会经历四个步骤:
- 策略信号
- 委托
- 订单
- 金额与标的的置换
因此盯紧订单的变化就可以了解策略对金额变动的影响,为了将更多精力用于策略本身的编写上,我们写一个策略模版,然后以后的策略都可以通过继承这个模版,把与策略无关的变量、操作都写在模版里:
from loguru import logger
import backtrader as btclass TemplateStrategy(bt.Strategy):def __init__(self):# 记录用self.buy_bond_record = {} # 记录购买的订单self.sell_bond_record = {} # 记录卖出的订单def next(self):"""最核心的触发策略"""raisedef notify_order(self, order):"""通知订单状态,当订单状态变化时触发"""today_time_string = self.datetime.datetime().strftime('%Y-%m-%d')if order.status in [order.Submitted, order.Accepted]: # 接受订单交易,正常情况returnif order.status in [order.Completed]:if order.isbuy():self.buy_bond_record.setdefault(today_time_string, {})self.buy_bond_record[today_time_string].setdefault(order.data._name.replace(".", "_"), [])self.buy_bond_record[today_time_string][order.data._name.replace(".", "_")].append({"order_ref": order.ref,"bond_name": order.data._name,"size": order.size,"price": order.executed.price,"value": order.executed.value,"trade_date": self.datetime.datetime(0),})logger.debug('{} 订单{} 已购入 {} , 购入单价 {:.2f}, 数量 {}, 费用 {:.2f}, 手续费 {:.2f}'.format(self.datetime.date(), order.ref, order.data._name, order.executed.price, order.size,order.executed.value, order.executed.comm))elif order.issell():self.sell_bond_record.setdefault(today_time_string, {})self.sell_bond_record[today_time_string].setdefault(order.data._name.replace(".", "_"), [])self.sell_bond_record[today_time_string][order.data._name.replace(".", "_")].append({"order_ref": order.ref,"bond_name": order.data._name,"size": order.size,"price": order.executed.price,"value": - order.executed.price * order.size,"sell_type": order.info.sell_type,"trade_date": self.datetime.datetime(0),})logger.debug('{} 订单{} 已卖出 {}, 卖出金额 {:.2f}, 数量 {}, 费用 {:.2f}, 手续费 {:.2f}'.format(self.datetime.date(), order.ref, order.data._name, order.executed.price, order.size,-order.executed.price * order.size, order.executed.comm))elif order.status in [order.Margin, order.Rejected]:logger.warning('{} 订单{} 现金不足、金额不足拒绝交易', self.datetime.date(), order.ref)elif order.status in [order.Canceled]:logger.debug("{} 订单{} 已取消", self.datetime.date(), order.ref)elif order.status in [order.Expired]:logger.warning('{} 订单{} 超过有效期已取消, 订单开价 {}, 当天最高价{}, 最低价{}', self.datetime.date(), order.ref, order.price,order.data.high[0], order.data.low[0])
之后的策略均继承此TemplateStrategy
策略类,并覆写def next(self)
函数即可,这一部分会将所有的订单在日志中打印下来
在程序中,购买的订单可以使用如下代码:
self.buy(data=self.getdatabyname(stock_name), # 针对哪一个股票代码size=100, # 数量price=self.getdatabyname(stock_name).close[0], # 以当天的收盘价购买exectype=bt.Order.Limit, # 限价单valid=self.getdatabyname(stock_name).datetime.date(1), # 有效期1天)
3. 评价指标
我们使用quantstats
这个库来对回测结果进行评价,这个库里的计算方法简单粗暴,通过对已有的计算方法的封装,我们得到可以方便的进行评价的方法:
import pandas as pd
import quantstats as qsdef cal_daily_return(fund_values: pd.Series):"""根据资金变动,计算日资产的变化率:param fund_values: 每日的总资产"""fund_values = fund_values.sort_index()daily_re: pd.Series = (fund_values / fund_values.shift(1)) - 1daily_re.iloc[0] = 0return daily_redef cal_rolling_feature(daily_return_series: pd.Series, rf=0.02, record_dict: dict = None):"""计算各种指标:param daily_return_series: 日收益的变化率:param rf: 无风险收益,这里定为0.02:param record_dict: 指标的结果会追加到这个字典中"""if record_dict is None:record_dict = {}daily_return_series.index = pd.to_datetime(daily_return_series.index.values)feature_df = pd.DataFrame(index=daily_return_series.index)feature_df['累积收益率'] = qs.stats.compsum(daily_return_series).valuesfeature_df['回撤'] = qs.stats.to_drawdown_series(daily_return_series)record_dict.update({"累积收益率": feature_df['累积收益率'].iloc[-1]})feature_dict = {"复合年增长": qs.stats.cagr(daily_return_series, rf=rf),"夏普比率": qs.stats.sharpe(daily_return_series, rf=rf),"索蒂诺": qs.stats.sortino(daily_return_series, rf=rf),"omega": qs.stats.omega(pd.DataFrame(daily_return_series), rf=rf),"最大回撤": qs.stats.max_drawdown(daily_return_series),"最大回撤期(天)": int(qs.stats.drawdown_details(feature_df['回撤'])['days'].max()),"年波动率": qs.stats.volatility(daily_return_series),}record_dict.update(feature_dict)# 决定保留的小数for key, value in record_dict.items():if isinstance(value, float):record_dict[key] = value.round(3)return feature_df, record_dict