金融量化交易的回测是一种评估投资策略有效性的方法。它涉及在已知的市场数据上运行交易策略,并估计该策略在未来可能产生的收益。回测的目的是了解策略在历史数据上的表现,并预测其在未来实际交易中的表现。
回测的过程通常包括以下步骤:
确定投资策略:根据投资者的风险偏好、投资目标等因素,制定合适的投资策略,包括投资品种、投资比例、买卖规则等。
收集市场数据:收集与投资策略相关的市场数据,如股票价格、成交量、利率等。
构建回测模型:利用历史数据和量化分析工具,构建回测模型,模拟投资策略的执行和收益情况。
运行回测模型:将回测模型应用于历史数据,模拟投资策略的执行过程,并计算出模拟收益。
分析回测结果:根据回测结果,分析投资策略的有效性,包括收益率、波动性、最大回撤等指标,以及策略在不同市场环境下的表现。
调整投资策略:根据回测结果和分析结果,对投资策略进行优化和调整,以提高策略的有效性和适应性。
需要注意的是,回测结果是历史结果并不代表未来实际交易的结果,因为市场环境是不断变化的。因此,投资者在进行量化交易时,需要谨慎对待回测结果,并结合实际情况进行决策。
Python 上用于回测的量化框架有 Backtrader,这也是我在2020年最早时候接触到的量化框架,已经很久没更新了,并且与新版 Matplotlib 不兼容。
使用第三方 Python 库,其实并没有想象中方便,因为你需要理解作者逻辑,查阅作者文档,可能仅仅为了回测这一件小事,而花费大量精力和时间阅读文档,查阅教程,得不偿失。
我写这篇文章的目的就是为了实现掌握较少的工具,花费较少的精力,少走弯路,达到目的。
熟悉 Excel 等表格类型的工具 (便于理解股票回测的过程)
安装 Python 3.8.0
安装 Pandas 1.4.1
由于本人使用的 Python 3.8.0 为了避免兼容性问题,推荐安装这个版本,本文不过多涉及 Python 的安装,下载官方的安装包安装即可。
https://www.python.org/
由于本人使用的 Pandas 1.4.1 为了避免函数改动的问题,推荐安装这个版本,命令行使用 Pip 安装即可。
pip install pandas==1.4.1
本篇文章以梳理思路,演示方法为主,所以直接使用以下简单的测试数据即可。
正常情况下的股票量化回测,可通过 API 等接口获取股票历年数据。
测试数据 (前 5 行和后 5 行预览):
[{"Date":"2015/12/31", "Code":"'000422", "Open":7.93, "High":7.95, "Low":7.76, "Close":7.77, "Pre_Close":7.93, "Change":-0.020177, "Volume":13915200},
{"Date":"2015/12/30", "Code":"'000422", "Open":7.86, "High":7.93, "Low":7.75, "Close":7.93, "Pre_Close":7.84, "Change":0.01148, "Volume":16755900},
{"Date":"2015/12/29", "Code":"'000422", "Open":7.72, "High":7.85, "Low":7.69, "Close":7.84, "Pre_Close":7.71, "Change":0.016861, "Volume":14263800},
{"Date":"2015/12/28", "Code":"'000422", "Open":8.03, "High":8.08, "Low":7.7, "Close":7.71, "Pre_Close":8.03, "Change":-0.039851, "Volume":27672800},
{"Date":"2015/12/25", "Code":"'000422", "Open":8.03, "High":8.05, "Low":7.93, "Close":8.03, "Pre_Close":7.99, "Change":0.005006, "Volume":18974000},
{"Date":"2015/12/24", "Code":"'000422", "Open":7.93, "High":8.16, "Low":7.87, "Close":7.99, "Pre_Close":7.92, "Change":0.008838, "Volume":23781900},
......
{"Date":"2015/08/11", "Code":"'000422", "Open":8.41, "High":8.68, "Low":8.32, "Close":8.48, "Pre_Close":8.49, "Change":-0.001178, "Volume":42343900},
{"Date":"2015/08/10", "Code":"'000422", "Open":8.28, "High":8.58, "Low":8.18, "Close":8.49, "Pre_Close":8.21, "Change":0.034105, "Volume":36071600},
{"Date":"2015/08/07", "Code":"'000422", "Open":8.15, "High":8.28, "Low":8.08, "Close":8.21, "Pre_Close":8.07, "Change":0.017348, "Volume":22599800},
{"Date":"2015/08/06", "Code":"'000422", "Open":7.88, "High":8.21, "Low":7.8, "Close":8.07, "Pre_Close":8.03, "Change":0.004981, "Volume":17546700},
{"Date":"2015/08/05", "Code":"'000422", "Open":8.1, "High":8.35, "Low":7.95, "Close":8.03, "Pre_Close":8.18, "Change":-0.018337, "Volume":23875500}]
下面使用 Pandas 加载数据时会有完整的数据,方便排版,这里只给出数据前 5 行和后 5 行的预览。
代码释义:
import pandas as pd: 导入 Pandas 库,并取别名为 pd。
pd.DataFrame(): 使用 Pandas 的数据框函数构建数据,可传入字典 (Dict),也可传入列表 (List),这里以列表的形式传入键值对 (Key : Value) 的行。
Stock.head(5): 显示前 5 行数据。
import pandas as pd
Stock = pd.DataFrame([{"Date":"2015/12/31", "Code":"'000422", "Open":7.93, "High":7.95, "Low":7.76, "Close":7.77, "Pre_Close":7.93, "Change":-0.020177, "Volume":13915200},
{"Date":"2015/12/30", "Code":"'000422", "Open":7.86, "High":7.93, "Low":7.75, "Close":7.93, "Pre_Close":7.84, "Change":0.01148, "Volume":16755900},
{"Date":"2015/12/29", "Code":"'000422", "Open":7.72, "High":7.85, "Low":7.69, "Close":7.84, "Pre_Close":7.71, "Change":0.016861, "Volume":14263800},
{"Date":"2015/12/28", "Code":"'000422", "Open":8.03, "High":8.08, "Low":7.7, "Close":7.71, "Pre_Close":8.03, "Change":-0.039851, "Volume":27672800},
{"Date":"2015/12/25", "Code":"'000422", "Open":8.03, "High":8.05, "Low":7.93, "Close":8.03, "Pre_Close":7.99, "Change":0.005006, "Volume":18974000},
{"Date":"2015/12/24", "Code":"'000422", "Open":7.93, "High":8.16, "Low":7.87, "Close":7.99, "Pre_Close":7.92, "Change":0.008838, "Volume":23781900},
{"Date":"2015/12/23", "Code":"'000422", "Open":7.97, "High":8.11, "Low":7.88, "Close":7.92, "Pre_Close":7.89, "Change":0.003802, "Volume":38033600},
{"Date":"2015/12/22", "Code":"'000422", "Open":7.86, "High":7.93, "Low":7.76, "Close":7.89, "Pre_Close":7.83, "Change":0.007663, "Volume":24178700},
{"Date":"2015/12/21", "Code":"'000422", "Open":7.59, "High":7.89, "Low":7.56, "Close":7.83, "Pre_Close":7.63, "Change":0.026212, "Volume":27633600},
{"Date":"2015/12/18", "Code":"'000422", "Open":7.71, "High":7.74, "Low":7.57, "Close":7.63, "Pre_Close":7.74, "Change":-0.014212, "Volume":22234900},
{"Date":"2015/12/17", "Code":"'000422", "Open":7.58, "High":7.75, "Low":7.57, "Close":7.74, "Pre_Close":7.55, "Change":0.025166, "Volume":25188400},
{"Date":"2015/12/16", "Code":"'000422", "Open":7.57, "High":7.62, "Low":7.53, "Close":7.55, "Pre_Close":7.55, "Change":0, "Volume":18601600},
{"Date":"2015/12/15", "Code":"'000422", "Open":7.63, "High":7.66, "Low":7.52, "Close":7.55, "Pre_Close":7.62, "Change":-0.009186, "Volume":23256600},
{"Date":"2015/12/14", "Code":"'000422", "Open":7.4, "High":7.64, "Low":7.36, "Close":7.62, "Pre_Close":7.51, "Change":0.014647, "Volume":18860100},
{"Date":"2015/12/11", "Code":"'000422", "Open":7.65, "High":7.7, "Low":7.41, "Close":7.51, "Pre_Close":7.67, "Change":-0.02086, "Volume":18385900},
{"Date":"2015/12/10", "Code":"'000422", "Open":7.78, "High":7.87, "Low":7.65, "Close":7.67, "Pre_Close":7.83, "Change":-0.020434, "Volume":17931900},
{"Date":"2015/12/09", "Code":"'000422", "Open":7.76, "High":8, "Low":7.75, "Close":7.83, "Pre_Close":7.77, "Change":0.007722, "Volume":22569700},
{"Date":"2015/12/08", "Code":"'000422", "Open":8.08, "High":8.18, "Low":7.76, "Close":7.77, "Pre_Close":8.24, "Change":-0.057039, "Volume":32948200},
{"Date":"2015/12/07", "Code":"'000422", "Open":8.12, "High":8.39, "Low":7.94, "Close":8.24, "Pre_Close":8.23, "Change":0.001215, "Volume":57993100},
{"Date":"2015/12/04", "Code":"'000422", "Open":7.85, "High":8.48, "Low":7.8, "Close":8.23, "Pre_Close":7.92, "Change":0.039141, "Volume":89881900},
{"Date":"2015/12/03", "Code":"'000422", "Open":7.42, "High":8.09, "Low":7.38, "Close":7.92, "Pre_Close":7.43, "Change":0.065949, "Volume":40777500},
{"Date":"2015/12/02", "Code":"'000422", "Open":7.35, "High":7.48, "Low":7.2, "Close":7.43, "Pre_Close":7.36, "Change":0.009511, "Volume":14337600},
{"Date":"2015/12/01", "Code":"'000422", "Open":7.28, "High":7.39, "Low":7.23, "Close":7.36, "Pre_Close":7.33, "Change":0.004093, "Volume":11050700},
{"Date":"2015/11/30", "Code":"'000422", "Open":7.18, "High":7.36, "Low":6.95, "Close":7.33, "Pre_Close":7.11, "Change":0.030942, "Volume":18247500},
{"Date":"2015/11/27", "Code":"'000422", "Open":7.59, "High":7.59, "Low":6.95, "Close":7.11, "Pre_Close":7.6, "Change":-0.064474, "Volume":24846700},
{"Date":"2015/11/26", "Code":"'000422", "Open":7.63, "High":7.73, "Low":7.58, "Close":7.6, "Pre_Close":7.63, "Change":-0.003932, "Volume":22299800},
{"Date":"2015/11/25", "Code":"'000422", "Open":7.56, "High":7.64, "Low":7.51, "Close":7.63, "Pre_Close":7.59, "Change":0.00527, "Volume":18782900},
{"Date":"2015/11/24", "Code":"'000422", "Open":7.6, "High":7.63, "Low":7.48, "Close":7.59, "Pre_Close":7.62, "Change":-0.003937, "Volume":13348200},
{"Date":"2015/11/23", "Code":"'000422", "Open":7.59, "High":7.72, "Low":7.55, "Close":7.62, "Pre_Close":7.61, "Change":0.001314, "Volume":25505000},
{"Date":"2015/11/20", "Code":"'000422", "Open":7.59, "High":7.71, "Low":7.53, "Close":7.61, "Pre_Close":7.59, "Change":0.002635, "Volume":25389100},
{"Date":"2015/11/19", "Code":"'000422", "Open":7.45, "High":7.62, "Low":7.41, "Close":7.59, "Pre_Close":7.39, "Change":0.027064, "Volume":34691700},
{"Date":"2015/11/18", "Code":"'000422", "Open":7.53, "High":7.54, "Low":7.38, "Close":7.39, "Pre_Close":7.51, "Change":-0.015979, "Volume":12725000},
{"Date":"2015/11/17", "Code":"'000422", "Open":7.53, "High":7.63, "Low":7.44, "Close":7.51, "Pre_Close":7.5, "Change":0.001333, "Volume":25714500},
{"Date":"2015/11/16", "Code":"'000422", "Open":7.27, "High":7.52, "Low":7.24, "Close":7.5, "Pre_Close":7.38, "Change":0.01626, "Volume":14572000},
{"Date":"2015/11/13", "Code":"'000422", "Open":7.49, "High":7.55, "Low":7.36, "Close":7.38, "Pre_Close":7.54, "Change":-0.02122, "Volume":26214400},
{"Date":"2015/11/12", "Code":"'000422", "Open":7.65, "High":7.68, "Low":7.49, "Close":7.54, "Pre_Close":7.61, "Change":-0.009198, "Volume":23794800},
{"Date":"2015/11/11", "Code":"'000422", "Open":7.57, "High":7.64, "Low":7.52, "Close":7.61, "Pre_Close":7.57, "Change":0.005284, "Volume":23445900},
{"Date":"2015/11/10", "Code":"'000422", "Open":7.51, "High":7.61, "Low":7.45, "Close":7.57, "Pre_Close":7.55, "Change":0.002649, "Volume":22427700},
{"Date":"2015/11/09", "Code":"'000422", "Open":7.51, "High":7.62, "Low":7.45, "Close":7.55, "Pre_Close":7.53, "Change":0.002656, "Volume":29959500},
{"Date":"2015/11/06", "Code":"'000422", "Open":7.47, "High":7.53, "Low":7.37, "Close":7.53, "Pre_Close":7.45, "Change":0.010738, "Volume":33273100},
{"Date":"2015/11/05", "Code":"'000422", "Open":7.34, "High":7.54, "Low":7.32, "Close":7.45, "Pre_Close":7.37, "Change":0.010855, "Volume":36330200},
{"Date":"2015/11/04", "Code":"'000422", "Open":7.1, "High":7.38, "Low":7.07, "Close":7.37, "Pre_Close":7.05, "Change":0.04539, "Volume":31260800},
{"Date":"2015/11/03", "Code":"'000422", "Open":7.08, "High":7.13, "Low":7.02, "Close":7.05, "Pre_Close":7.06, "Change":-0.001416, "Volume":13412400},
{"Date":"2015/11/02", "Code":"'000422", "Open":7.11, "High":7.26, "Low":7.05, "Close":7.06, "Pre_Close":7.26, "Change":-0.027548, "Volume":15142100},
{"Date":"2015/10/30", "Code":"'000422", "Open":7.22, "High":7.38, "Low":7.1, "Close":7.26, "Pre_Close":7.24, "Change":0.002762, "Volume":20490200},
{"Date":"2015/10/29", "Code":"'000422", "Open":7.27, "High":7.33, "Low":7.16, "Close":7.24, "Pre_Close":7.16, "Change":0.011173, "Volume":23098500},
{"Date":"2015/10/28", "Code":"'000422", "Open":7.32, "High":7.4, "Low":7.09, "Close":7.16, "Pre_Close":7.42, "Change":-0.03504, "Volume":31938500},
{"Date":"2015/10/27", "Code":"'000422", "Open":7.21, "High":7.48, "Low":7.08, "Close":7.42, "Pre_Close":7.18, "Change":0.033426, "Volume":51769300},
{"Date":"2015/10/26", "Code":"'000422", "Open":7.2, "High":7.25, "Low":7.01, "Close":7.18, "Pre_Close":7.17, "Change":0.001395, "Volume":33077800},
{"Date":"2015/10/23", "Code":"'000422", "Open":6.84, "High":7.22, "Low":6.81, "Close":7.17, "Pre_Close":6.8, "Change":0.054412, "Volume":42351500},
{"Date":"2015/10/22", "Code":"'000422", "Open":6.68, "High":6.81, "Low":6.64, "Close":6.8, "Pre_Close":6.65, "Change":0.022556, "Volume":18503800},
{"Date":"2015/10/21", "Code":"'000422", "Open":7.08, "High":7.11, "Low":6.61, "Close":6.65, "Pre_Close":7.09, "Change":-0.062059, "Volume":35365300},
{"Date":"2015/10/20", "Code":"'000422", "Open":7, "High":7.09, "Low":6.94, "Close":7.09, "Pre_Close":7.03, "Change":0.008535, "Volume":21972900},
{"Date":"2015/10/19", "Code":"'000422", "Open":7.09, "High":7.13, "Low":6.92, "Close":7.03, "Pre_Close":7.08, "Change":-0.007062, "Volume":28068800},
{"Date":"2015/10/16", "Code":"'000422", "Open":6.97, "High":7.08, "Low":6.91, "Close":7.08, "Pre_Close":6.93, "Change":0.021645, "Volume":35584700},
{"Date":"2015/10/15", "Code":"'000422", "Open":6.77, "High":6.94, "Low":6.75, "Close":6.93, "Pre_Close":6.77, "Change":0.023634, "Volume":28412700},
{"Date":"2015/10/14", "Code":"'000422", "Open":6.87, "High":6.94, "Low":6.74, "Close":6.77, "Pre_Close":6.89, "Change":-0.017417, "Volume":24445500},
{"Date":"2015/10/13", "Code":"'000422", "Open":6.86, "High":6.96, "Low":6.8, "Close":6.89, "Pre_Close":6.88, "Change":0.001453, "Volume":25771900},
{"Date":"2015/10/12", "Code":"'000422", "Open":6.62, "High":6.91, "Low":6.58, "Close":6.88, "Pre_Close":6.61, "Change":0.040847, "Volume":33254300},
{"Date":"2015/10/09", "Code":"'000422", "Open":6.54, "High":6.65, "Low":6.45, "Close":6.61, "Pre_Close":6.54, "Change":0.010703, "Volume":16635900},
{"Date":"2015/10/08", "Code":"'000422", "Open":6.45, "High":6.7, "Low":6.37, "Close":6.54, "Pre_Close":6.26, "Change":0.044728, "Volume":16931000},
{"Date":"2015/09/30", "Code":"'000422", "Open":6.25, "High":6.3, "Low":6.22, "Close":6.26, "Pre_Close":6.23, "Change":0.004815, "Volume":6579090},
{"Date":"2015/09/29", "Code":"'000422", "Open":6.3, "High":6.32, "Low":6.18, "Close":6.23, "Pre_Close":6.4, "Change":-0.026562, "Volume":8072900},
{"Date":"2015/09/28", "Code":"'000422", "Open":6.35, "High":6.42, "Low":6.25, "Close":6.4, "Pre_Close":6.34, "Change":0.009464, "Volume":7922890},
{"Date":"2015/09/25", "Code":"'000422", "Open":6.51, "High":6.56, "Low":6.25, "Close":6.34, "Pre_Close":6.53, "Change":-0.029096, "Volume":11298800},
{"Date":"2015/09/24", "Code":"'000422", "Open":6.48, "High":6.56, "Low":6.45, "Close":6.53, "Pre_Close":6.45, "Change":0.012403, "Volume":10180900},
{"Date":"2015/09/23", "Code":"'000422", "Open":6.51, "High":6.6, "Low":6.41, "Close":6.45, "Pre_Close":6.67, "Change":-0.032984, "Volume":14294100},
{"Date":"2015/09/22", "Code":"'000422", "Open":6.58, "High":6.73, "Low":6.54, "Close":6.67, "Pre_Close":6.58, "Change":0.013678, "Volume":20970200},
{"Date":"2015/09/21", "Code":"'000422", "Open":6.34, "High":6.61, "Low":6.29, "Close":6.58, "Pre_Close":6.44, "Change":0.021739, "Volume":15295900},
{"Date":"2015/09/18", "Code":"'000422", "Open":6.52, "High":6.58, "Low":6.3, "Close":6.44, "Pre_Close":6.44, "Change":0, "Volume":14924700},
{"Date":"2015/09/17", "Code":"'000422", "Open":6.59, "High":6.76, "Low":6.43, "Close":6.44, "Pre_Close":6.68, "Change":-0.035928, "Volume":17523900},
{"Date":"2015/09/16", "Code":"'000422", "Open":6.21, "High":6.76, "Low":6.17, "Close":6.68, "Pre_Close":6.15, "Change":0.086179, "Volume":17662300},
{"Date":"2015/09/15", "Code":"'000422", "Open":6.24, "High":6.38, "Low":6.05, "Close":6.15, "Pre_Close":6.26, "Change":-0.017572, "Volume":13771200},
{"Date":"2015/09/14", "Code":"'000422", "Open":6.89, "High":6.95, "Low":6.18, "Close":6.26, "Pre_Close":6.87, "Change":-0.088792, "Volume":18559600},
{"Date":"2015/09/11", "Code":"'000422", "Open":6.87, "High":6.96, "Low":6.77, "Close":6.87, "Pre_Close":6.84, "Change":0.004386, "Volume":9486290},
{"Date":"2015/09/10", "Code":"'000422", "Open":6.95, "High":7.01, "Low":6.76, "Close":6.84, "Pre_Close":7.06, "Change":-0.031161, "Volume":15229100},
{"Date":"2015/09/09", "Code":"'000422", "Open":6.9, "High":7.09, "Low":6.86, "Close":7.06, "Pre_Close":6.88, "Change":0.026163, "Volume":25325600},
{"Date":"2015/09/08", "Code":"'000422", "Open":6.65, "High":6.91, "Low":6.55, "Close":6.88, "Pre_Close":6.62, "Change":0.039275, "Volume":15609100},
{"Date":"2015/09/07", "Code":"'000422", "Open":6.5, "High":6.81, "Low":6.5, "Close":6.62, "Pre_Close":6.38, "Change":0.037618, "Volume":15602600},
{"Date":"2015/09/02", "Code":"'000422", "Open":6.45, "High":6.88, "Low":6.3, "Close":6.38, "Pre_Close":6.74, "Change":-0.053412, "Volume":19480100},
{"Date":"2015/09/01", "Code":"'000422", "Open":6.88, "High":6.99, "Low":6.67, "Close":6.74, "Pre_Close":6.81, "Change":-0.010279, "Volume":22576700},
{"Date":"2015/08/31", "Code":"'000422", "Open":6.9, "High":6.97, "Low":6.71, "Close":6.81, "Pre_Close":7.07, "Change":-0.036775, "Volume":16069600},
{"Date":"2015/08/28", "Code":"'000422", "Open":6.75, "High":7.08, "Low":6.71, "Close":7.07, "Pre_Close":6.67, "Change":0.05997, "Volume":23330800},
{"Date":"2015/08/27", "Code":"'000422", "Open":6.53, "High":6.67, "Low":6.34, "Close":6.67, "Pre_Close":6.32, "Change":0.05538, "Volume":19627900},
{"Date":"2015/08/26", "Code":"'000422", "Open":6.31, "High":6.77, "Low":6.09, "Close":6.32, "Pre_Close":6.25, "Change":0.0112, "Volume":26190200},
{"Date":"2015/08/25", "Code":"'000422", "Open":6.4, "High":6.77, "Low":6.25, "Close":6.25, "Pre_Close":6.94, "Change":-0.099424, "Volume":25778600},
{"Date":"2015/08/24", "Code":"'000422", "Open":7.49, "High":7.49, "Low":6.94, "Close":6.94, "Pre_Close":7.71, "Change":-0.09987, "Volume":31949900},
{"Date":"2015/08/21", "Code":"'000422", "Open":8, "High":8.11, "Low":7.6, "Close":7.71, "Pre_Close":8.17, "Change":-0.056304, "Volume":28144800},
{"Date":"2015/08/20", "Code":"'000422", "Open":8.38, "High":8.56, "Low":8.14, "Close":8.17, "Pre_Close":8.53, "Change":-0.042204, "Volume":27764200},
{"Date":"2015/08/19", "Code":"'000422", "Open":7.73, "High":8.57, "Low":7.72, "Close":8.53, "Pre_Close":7.96, "Change":0.071608, "Volume":45619900},
{"Date":"2015/08/18", "Code":"'000422", "Open":8.81, "High":8.86, "Low":7.92, "Close":7.96, "Pre_Close":8.8, "Change":-0.095455, "Volume":49105500},
{"Date":"2015/08/17", "Code":"'000422", "Open":8.49, "High":8.83, "Low":8.42, "Close":8.8, "Pre_Close":8.52, "Change":0.032864, "Volume":42096900},
{"Date":"2015/08/14", "Code":"'000422", "Open":8.48, "High":8.65, "Low":8.43, "Close":8.52, "Pre_Close":8.44, "Change":0.009479, "Volume":35985000},
{"Date":"2015/08/13", "Code":"'000422", "Open":8.2, "High":8.45, "Low":8.15, "Close":8.44, "Pre_Close":8.24, "Change":0.024272, "Volume":26019600},
{"Date":"2015/08/12", "Code":"'000422", "Open":8.38, "High":8.48, "Low":8.21, "Close":8.24, "Pre_Close":8.48, "Change":-0.028302, "Volume":30960700},
{"Date":"2015/08/11", "Code":"'000422", "Open":8.41, "High":8.68, "Low":8.32, "Close":8.48, "Pre_Close":8.49, "Change":-0.001178, "Volume":42343900},
{"Date":"2015/08/10", "Code":"'000422", "Open":8.28, "High":8.58, "Low":8.18, "Close":8.49, "Pre_Close":8.21, "Change":0.034105, "Volume":36071600},
{"Date":"2015/08/07", "Code":"'000422", "Open":8.15, "High":8.28, "Low":8.08, "Close":8.21, "Pre_Close":8.07, "Change":0.017348, "Volume":22599800},
{"Date":"2015/08/06", "Code":"'000422", "Open":7.88, "High":8.21, "Low":7.8, "Close":8.07, "Pre_Close":8.03, "Change":0.004981, "Volume":17546700},
{"Date":"2015/08/05", "Code":"'000422", "Open":8.1, "High":8.35, "Low":7.95, "Close":8.03, "Pre_Close":8.18, "Change":-0.018337, "Volume":23875500}])
Stock.head(5)
输出:
>>> Stock.head(5)
Date Code Open High Low Close Pre_Close Change Volume
0 2015/12/31 '000422 7.93 7.95 7.76 7.77 7.93 -0.020177 13915200
1 2015/12/30 '000422 7.86 7.93 7.75 7.93 7.84 0.011480 16755900
2 2015/12/29 '000422 7.72 7.85 7.69 7.84 7.71 0.016861 14263800
3 2015/12/28 '000422 8.03 8.08 7.70 7.71 8.03 -0.039851 27672800
4 2015/12/25 '000422 8.03 8.05 7.93 8.03 7.99 0.005006 18974000
为了方便在测试数据上演示用 Pandas 做量化回测的方法,这里我将使用简单的买卖信号作为入场点和出场点。
买入信号:
Close <= 7.00
卖出信号:
Close >= 8.00
由于测试数据的最低价为 6.15,最高价为 8.80,我们低买高卖就行了,也就是收盘价 (Close) 低于 7.00 就一直买,高于 8.00 就一直卖。
接下来,我们将买卖信号标记出来,符合条件的用 1 表示,不符合条件的用 0 表示。
代码释义:
Stock[“Signal_Buy”] = None: 直接对 Stock 数据框对象不存在的列名赋值,则新建一列,列名为 Signal_Buy,这一列的值全部是 None 值。
Stock[“Signal_Buy”] = Stock[“Close”].apply(): 这里调用了 Pandas 的 apply 方法,意思是映射生效到 Close 列中的每一个值,并把生效后返回的新列赋值给 Signal_Buy 列。
Stock[“Signal_Buy”] = Stock[“Close”].apply(lambda X: 1 if X <= 7.00 else 0): 这里往 Pandas 的 apply 方法中传入了一个 lambda 函数 (一次性函数),整个操作相当于 Excel 中,Close 在 D 列,而你在 E 列写了个 IF(A1 <= 7.00, 1, 0) 然后下拉填充到最后一行。
Stock.tail(15): 显示 Stock 数据框对象的最后 15 行。
# 用 Pandas 的 apply 方法传入 lambda 一次性函数计算买点。
Stock["Signal_Buy"] = Stock["Close"].apply(lambda X: 1 if X <= 7.00 else 0)
# 用 Pandas 的 apply 方法传入 lambda 一次性函数计算卖点。
Stock["Signal_Sell"] = Stock["Close"].apply(lambda X: 1 if X >= 8.00 else 0)
Stock.tail(15)
输出:
>>> Stock.tail(15)
Date Code Open ... Volume Signal_Buy Signal_Sell
85 2015/08/25 '000422 6.40 ... 25778600 1 0
86 2015/08/24 '000422 7.49 ... 31949900 1 0
87 2015/08/21 '000422 8.00 ... 28144800 0 0
88 2015/08/20 '000422 8.38 ... 27764200 0 1
89 2015/08/19 '000422 7.73 ... 45619900 0 1
90 2015/08/18 '000422 8.81 ... 49105500 0 0
91 2015/08/17 '000422 8.49 ... 42096900 0 1
92 2015/08/14 '000422 8.48 ... 35985000 0 1
93 2015/08/13 '000422 8.20 ... 26019600 0 1
94 2015/08/12 '000422 8.38 ... 30960700 0 1
95 2015/08/11 '000422 8.41 ... 42343900 0 1
96 2015/08/10 '000422 8.28 ... 36071600 0 1
97 2015/08/07 '000422 8.15 ... 22599800 0 1
98 2015/08/06 '000422 7.88 ... 17546700 0 1
99 2015/08/05 '000422 8.10 ... 23875500 0 1
[15 rows x 11 columns]
这样一来,我们的数据中就有买卖信号了,下一步我们将使用这些买卖信号。
因为我们现在能看到整体数据上价格的变化趋势,是完全知道什么价格买入、什么价格卖出是最合适的。
我们还是以演示用 Pandas 做量化回测的方法为主,一些变量我们就把它固定下来,最简化函数,以便于理解。
根据前面买卖信号的确定,我们现在有如下交易思路:
(1) 回测周期: 从2015年08月05日开始,到2015年12月31日结束,历时100个交易日。
(2) 单笔数量: 沪深市场的默认单笔交易数量 (手数) 为 100 股,我们沿用这个规则就行了。
(3) 起始资金: 这里我们假设带着 10,000 元于2015年08月05日入市交易,希望在2015年12月31日时,资产能够大于 10,000 元。
(4) 买入次数: 这里我们做个简单的资金管理,分 6 次买入,每次买入 200 股,因为可能在买入后,后续还会出现更低的价格。
(5) 卖出次数: 这里我们做个简单的资产管理,分 3 次卖出,每次卖出 400 股,因为可能在卖出后,后续还会出现更高的价格。
(6) 买入链条: [信号成立] -> [买入 200 股] -> [花费资金 = 收盘价 (Close) * 200] -> [起始资金 - 花费资金] -> [持仓数 + 200] -> [买入完成]
(7) 卖出链条: [信号成立] -> [卖出 400 股] -> [收入资金 = 收盘价 (Close) * 400] -> [起始资金 + 收入资金] -> [持仓数 - 400] -> [卖出完成]
这里需要完善的部分其实还有很多,比如计算剩余资金是否足够买入,判断可卖数量是否小于或等于持仓数量,是否全仓进出, 卖出后计算单笔盈亏等等…
但我们为了简化逻辑,便于演示,不做过多讨论。
接下来,我们编写买卖的函数:
# 全局变量: 起始资金 10000.00 元。
Funds:float = 10000.00
# 全局变量: 持仓数量 0 (初始状态)。
Holdings:int = 0
# 全局变量: 买入计数器。
Buy_Count:int = 1
# 全局变量: 卖出计数器。
Sell_Count:int = 1
def Trade(Signal_Buy:int, Signal_Sell:int, Close:float) -> tuple:
# global 关键字用于访问全局变量。
global Funds
global Holdings
global Buy_Count
global Sell_Count
# 首先判断: 买入信号为 1,卖出信号为 0,买入计数器小于等于 6。
if ((Signal_Buy == 1) and (Signal_Sell == 0) and (Buy_Count <= 6)):
# 花费资金。
Spend = (Close * 200)
# 起始资金变化。
Funds = (Funds - Spend)
# 持仓数量变化。
Holdings = (Holdings + 200)
# 买入计数器 + 1。
Buy_Count = (Buy_Count + 1)
# 然后判断: 买入信号为 0,卖出信号为 1,卖出计数器小于等于 3,持仓大于等于 400。
if ((Signal_Buy == 0) and (Signal_Sell == 1) and (Sell_Count <= 3) and (Holdings >= 400)):
# 收入资金。
Incom = (Close * 400)
# 起始资金变化。
Funds = (Funds + Incom)
# 持仓数量变化。
Holdings = (Holdings - 400)
# 卖出计数器 + 1。
Sell_Count = (Sell_Count + 1)
# 返回值 (元组)。
return (Funds, Holdings)
经过前面的步骤,股票量化回测所需要用到的行情数据、买卖信号、交易函数,都已经准备好了,接下来就是查看实时的资金、持仓的变动情况了。
代码释义:
Stock.index: 返回 Stock 数据框的索引列表,是一个可迭代对象,Stock 有 100 行,则返回 [0, 1, 2, … 98, 99] 这样的 NdArray 列表。
Stock.loc[Idx, “Signal_Buy”]: 定位 Stock 数据框中的值,如 Stock.loc[1, “Signal_Buy”],则定位到 Signal_Buy 列下的第 2 行。
Curr_Sig_Buy = Stock.loc[Idx, “Signal_Buy”]:将定位到的 Stock 数据框的值赋值给 Curr_Sig_Buy 变量。
Stock.loc[1, “Funds”] = Account[0]: 将 Account 元组中的第 1 个值赋值给 Stock 数据框的 Funds 列的第 2 行。
Stock = Stock[[“Date”, “Code”, … “Value”, “Asset”]]: 筛选列,由于列数太多,横向显示不方便查看,只筛选出指定几列查看。
# 对 Stock 数据框进行排序,因为买卖操作是按时间顺序进行。
Stock = Stock.sort_values("Date", ascending=True)
# 对 Stock 数据框重设索引,因为排序后,原索引被打乱。
Stock = Stock.reset_index(drop=True)
# 为 Stock 数据框新建 1 列,命名为 Funds。
Stock["Funds"] = None
# 为 Stock 数据框新建 1 列,命名为 Holdings。
Stock["Holdings"] = None
# 使用 for 循环遍历 Stock 数据框的索引 (Index)。
for Idx in Stock.index:
# 取出每 1 行的 Signal_Buy 的值。
Curr_Sig_Buy = Stock.loc[Idx, "Signal_Buy"]
# 取出每 1 行的 Signal_Sell 的值。
Curr_Sig_Sell = Stock.loc[Idx, "Signal_Sell"]
# 取出每 1 行的 Close 的值。
Curr_Close = Stock.loc[Idx, "Close"]
# 调用函数并把函数的返回值放在 Account 元组内。
Account:tuple = Trade(Signal_Buy=Curr_Sig_Buy, Signal_Sell=Curr_Sig_Sell, Close=Curr_Close)
# 将 Account 元组内的第 0 个值 (Funds) 赋值给 Funds 列的 Idx 行。
Stock.loc[Idx, "Funds"] = Account[0]
# 将 Account 元组内的第 1 个值 (Holdings) 赋值给 Holdings 列的 Idx 行。
Stock.loc[Idx, "Holdings"] = Account[1]
# 计算持仓市值: 持仓市值 = 当前价 * 持仓数量。
Stock["Value"] = Stock["Close"] * Stock["Holdings"]
# 计算总资产: 总资产 = 剩余资金 + 持仓市值。
Stock["Asset"] = Stock["Funds"] + Stock["Value"]
# 筛选列: 只显示关键信息。
Stock = Stock[["Date", "Code", "Close", "Signal_Buy", "Signal_Sell", "Funds", "Holdings", "Value", "Asset"]]
print(Stock.head(15))
print(Stock.tail(15))
输出:
Date Code Close Signal_Buy Signal_Sell Funds Holdings Value Asset
0 2015/08/05 '000422 8.03 0 1 10000.0 0 0.0 10000.0
1 2015/08/06 '000422 8.07 0 1 10000.0 0 0.0 10000.0
2 2015/08/07 '000422 8.21 0 1 10000.0 0 0.0 10000.0
3 2015/08/10 '000422 8.49 0 1 10000.0 0 0.0 10000.0
4 2015/08/11 '000422 8.48 0 1 10000.0 0 0.0 10000.0
5 2015/08/12 '000422 8.24 0 1 10000.0 0 0.0 10000.0
6 2015/08/13 '000422 8.44 0 1 10000.0 0 0.0 10000.0
7 2015/08/14 '000422 8.52 0 1 10000.0 0 0.0 10000.0
8 2015/08/17 '000422 8.80 0 1 10000.0 0 0.0 10000.0
9 2015/08/18 '000422 7.96 0 0 10000.0 0 0.0 10000.0
10 2015/08/19 '000422 8.53 0 1 10000.0 0 0.0 10000.0
11 2015/08/20 '000422 8.17 0 1 10000.0 0 0.0 10000.0
12 2015/08/21 '000422 7.71 0 0 10000.0 0 0.0 10000.0
13 2015/08/24 '000422 6.94 1 0 8612.0 200 1388.0 10000.0
14 2015/08/25 '000422 6.25 1 0 7362.0 400 2500.0 9862.0
Date Code Close Signal_Buy Signal_Sell Funds Holdings Value Asset
85 2015/12/11 '000422 7.51 0 0 8642.0 400 3004.0 11646.0
86 2015/12/14 '000422 7.62 0 0 8642.0 400 3048.0 11690.0
87 2015/12/15 '000422 7.55 0 0 8642.0 400 3020.0 11662.0
88 2015/12/16 '000422 7.55 0 0 8642.0 400 3020.0 11662.0
89 2015/12/17 '000422 7.74 0 0 8642.0 400 3096.0 11738.0
90 2015/12/18 '000422 7.63 0 0 8642.0 400 3052.0 11694.0
91 2015/12/21 '000422 7.83 0 0 8642.0 400 3132.0 11774.0
92 2015/12/22 '000422 7.89 0 0 8642.0 400 3156.0 11798.0
93 2015/12/23 '000422 7.92 0 0 8642.0 400 3168.0 11810.0
94 2015/12/24 '000422 7.99 0 0 8642.0 400 3196.0 11838.0
95 2015/12/25 '000422 8.03 0 1 11854.0 0 0.0 11854.0
96 2015/12/28 '000422 7.71 0 0 11854.0 0 0.0 11854.0
97 2015/12/29 '000422 7.84 0 0 11854.0 0 0.0 11854.0
98 2015/12/30 '000422 7.93 0 0 11854.0 0 0.0 11854.0
99 2015/12/31 '000422 7.77 0 0 11854.0 0 0.0 11854.0
代码释义:
Stock.apply(lambda Row: …): 与 Stock[“Close”].apply(lambda X: …) 不同,针对数据框的 Stock.apply 将映射每 1 行,映射的每 1 行又可以通过列名取值,需要指定轴线参数 axis = 1 来纵向映射。
Stock[“Funds”] = Stock[“Account”].apply(lambda X: X[0]): 取 Account 列中每个元组值中的元素,因为 Account 列包含的值都是元组,使用映射方法传入元素下标取值。
# 对 Stock 数据框进行排序,因为买卖操作是按时间顺序进行。
Stock = Stock.sort_values("Date", ascending=True)
# 用 Pandas 的 apply 方法传入编写好的交易函数计算卖点。
Stock["Account"] = Stock.apply(lambda Row: Trade(Signal_Buy=Row["Signal_Buy"], Signal_Sell=Row["Signal_Sell"], Close=Row["Close"]), axis=1)
# 用 Pandas 的 apply 方法传入 lambda 一次性函数,将 Account 元组中的第 0 个值取出来。
Stock["Funds"] = Stock["Account"].apply(lambda X: X[0])
# 用 Pandas 的 apply 方法传入 lambda 一次性函数,将 Account 元组中的第 1 个值取出来。
Stock["Holdings"] = Stock["Account"].apply(lambda X: X[1])
# 计算持仓市值: 持仓市值 = 当前价 * 持仓数量。
Stock["Value"] = Stock["Close"] * Stock["Holdings"]
# 计算总资产: 总资产 = 剩余资金 + 持仓市值。
Stock["Asset"] = Stock["Funds"] + Stock["Value"]
# 筛选列: 只显示关键信息。
Stock = Stock[["Date", "Code", "Close", "Signal_Buy", "Signal_Sell", "Account", "Funds", "Holdings", "Value", "Asset"]]
print(Stock.head(15))
print(Stock.tail(15))
输出:
Date Code Close Signal_Buy Signal_Sell Account Funds Holdings Value Asset
0 2015/08/05 '000422 8.03 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
1 2015/08/06 '000422 8.07 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
2 2015/08/07 '000422 8.21 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
3 2015/08/10 '000422 8.49 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
4 2015/08/11 '000422 8.48 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
5 2015/08/12 '000422 8.24 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
6 2015/08/13 '000422 8.44 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
7 2015/08/14 '000422 8.52 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
8 2015/08/17 '000422 8.80 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
9 2015/08/18 '000422 7.96 0 0 (10000.0, 0) 10000.0 0 0.0 10000.0
10 2015/08/19 '000422 8.53 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
11 2015/08/20 '000422 8.17 0 1 (10000.0, 0) 10000.0 0 0.0 10000.0
12 2015/08/21 '000422 7.71 0 0 (10000.0, 0) 10000.0 0 0.0 10000.0
13 2015/08/24 '000422 6.94 1 0 (8612.0, 200) 8612.0 200 1388.0 10000.0
14 2015/08/25 '000422 6.25 1 0 (7362.0, 400) 7362.0 400 2500.0 9862.0
Date Code Close Signal_Buy Signal_Sell Account Funds Holdings Value Asset
85 2015/12/11 '000422 7.51 0 0 (8642.0, 400) 8642.0 400 3004.0 11646.0
86 2015/12/14 '000422 7.62 0 0 (8642.0, 400) 8642.0 400 3048.0 11690.0
87 2015/12/15 '000422 7.55 0 0 (8642.0, 400) 8642.0 400 3020.0 11662.0
88 2015/12/16 '000422 7.55 0 0 (8642.0, 400) 8642.0 400 3020.0 11662.0
89 2015/12/17 '000422 7.74 0 0 (8642.0, 400) 8642.0 400 3096.0 11738.0
90 2015/12/18 '000422 7.63 0 0 (8642.0, 400) 8642.0 400 3052.0 11694.0
91 2015/12/21 '000422 7.83 0 0 (8642.0, 400) 8642.0 400 3132.0 11774.0
92 2015/12/22 '000422 7.89 0 0 (8642.0, 400) 8642.0 400 3156.0 11798.0
93 2015/12/23 '000422 7.92 0 0 (8642.0, 400) 8642.0 400 3168.0 11810.0
94 2015/12/24 '000422 7.99 0 0 (8642.0, 400) 8642.0 400 3196.0 11838.0
95 2015/12/25 '000422 8.03 0 1 (11854.0, 0) 11854.0 0 0.0 11854.0
96 2015/12/28 '000422 7.71 0 0 (11854.0, 0) 11854.0 0 0.0 11854.0
97 2015/12/29 '000422 7.84 0 0 (11854.0, 0) 11854.0 0 0.0 11854.0
98 2015/12/30 '000422 7.93 0 0 (11854.0, 0) 11854.0 0 0.0 11854.0
99 2015/12/31 '000422 7.77 0 0 (11854.0, 0) 11854.0 0 0.0 11854.0
到此,回测就结束了。
当然还有一些其它有关回测的信息,可以从这份回测数据中扩展出来。
比如资产收益率、夏普比率、最大回撤等等,只需要通过进一步的计算或即可得到。
再比如买点分布、买卖频谱、资产变化趋势图等等,只需要通过借助 Matplotlib 即可快速绘制。
本例只是针对如何使用 Pandas 做股票量化回测,其余部分就不展开叙述了,这里只拿资产收益率做个示例。
资产收益率:
print("资产收益率: %.2f" % ((Stock["Asset"][99] - Stock["Asset"][0]) / Stock["Asset"][99]))
输出:
资产收益率: 0.16
以上就是使用 Pandas 进行股票量化回测 (Quantitative Backtesting) 的全部内容。
更多内容可以访问我的代码仓库:
https://gitee.com/goufeng928/finance
https://github.com/goufeng928/finance