ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

akshare数据获取与布林通道策略回测:量化入门实战

akshare数据获取与布林通道策略回测:量化入门实战 如果有人问我量化交易入门最难的一步是什么我的答案大概率不是策略设计也不是回测框架而是“搞到一份靠谱的历史数据”。早期我做回测光在数据获取上就折腾了一周有的平台要积分、有的接口限流、有的下载下来复权乱七八糟。直到换成akshare才把这条最烦人的路走通。今天这一篇就把akshare从安装、取数到布林通道策略回测的完整流程一次性讲明白适合刚接触Python量化、想自己动手验证一个技术指标策略的人。1. akshare安装与数据获取数据源是策略的地基1.1 为什么我选了akshare而不是tushare或者baostock数据获取这件事国内Python量化圈最常用的三个免费方案就是tushare、baostock和akshare我一开始全试过说点实际感受tushare老牌库但Pro版接口要积分初始用户每天调用次数有限制很多高频接口要攒积分才能开。如果你只是写个布林策略看看效果那点调用权限其实够用但每次换接口都要去查一下自己的权限等级有点累。baostock免费且稳定接口返回的数据质量不错但覆盖范围偏窄主要就是股票日线、周线、月线和少量指数数据想看实时行情、板块资金流、期货期权这类扩展数据就抓瞎了。akshare定位是“把公开网页数据变成结构化DataFrame”所以覆盖面极其夸张股票、基金、期货、债券、宏观、数字货币都有对应接口。不需要token、不需要注册直接pip安装就能用特别适合快速验证想法。真实使用中akshare的体验是“真香与无奈并存”。香在接口多、免费、随取随用无奈在它本质是爬虫封装数据源页面一改版某些接口会短期“失灵”。好在维护频率很高定期升级版本基本能跟上。做我们这种个人量化研究它已经足够了。1.2 安装过程里最容易踩的坑先说安装很多新手在网上搜“akshare下载”或“akshare安装”莫名其妙下载一些打包好的源码包其实完全没必要。akshare就在PyPI上正常用pip安装就行pip install akshare如果你在国内网络环境下装得慢或者超时直接换清华镜像源速度快非常多pip install akshare -i https://pypi.tuna.tsinghua.edu.cn/simple安装过程中有几个坑我印象深刻挨个说第一个坑是版本冲突。akshare依赖pandas、requests、beautifulsoup4、lxml这一堆库如果你机器上已经装了一个比较老的pandasakshare可能直接装不上或者装上之后某些接口一调用就报错。建议在虚拟环境里单独建一个项目环境比如用condaconda create -n quant python3.10 conda activate quant pip install akshare第二个坑是版本更新太快。akshare的接口参数经常跟着数据源网页结构调整比如某个接口上个月还叫stock_zh_a_daily这个月可能就推荐你用stock_zh_a_hist。所以社区里老教程的代码直接复制运行时容易报错。我的习惯是安装后先固定版本号写进requirements.txtakshare1.12.x这样至少能保证这一段时间跑代码不崩。等代码写稳了再考虑升级。第三个坑是接口返回的字段名有变动。同一个接口不同版本返回的列可能不完全一样这也是很多人复制代码报KeyError的原因。后面取数时会专门演示怎么处理。提示安装完成后建议先执行python -c import akshare; print(akshare.__version__)确认没问题再往下走。1.3 用stock_zh_a_hist拉取日线数据akshare里获取A股历史行情目前最常用、也最稳定的是stock_zh_a_hist接口。它拿的是东方财富的日线数据一次能拉很长一段区间非常适合回测。核心参数有这几个symbol股票代码比如贵州茅台是600519不需要带交易所前缀period频率填daily就是日线start_date与end_date起始和结束日期格式是YYYYMMDDadjust复权方式qfq前复权、hfq后复权、空字符串是不复权拿贵州茅台从2018年到2023年底的日线数据代码这样写import akshare as ak import pandas as pd import numpy as np df ak.stock_zh_a_hist( symbol600519, perioddaily, start_date20180101, end_date20231231, adjustqfq ) print(df.head())运行之后返回的DataFrame大概长这样日期 股票代码 开盘 收盘 最高 最低 ... 0 2018-01-02 600519 700.0 705.0 707.0 692.0 ... 1 2018-01-03 600519 705.5 710.0 715.0 698.0 ...不同akshare版本返回的列会有些许差异有的版本多返回“股票代码”这一列有的版本没有。稳妥的做法是拿到数据后先打印df.columns看一眼再统一重命名成自己好操作的英文列名# 根据实际返回列名做调整我这边版本返回的列是这些 df.columns [date, code, open, close, high, low, volume, amount, amplitude, pct_chg, change, turnover] df df[[date, open, close, high, low, volume]].copy() df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) print(df.head())这里提醒一句日期一定要转成datetime类型而且一定要按时间升序排序。akshare返回的数据通常已经是升序但我不止一次遇到过接口返回顺序变化的情况回测如果数据顺序错了所有信号和收益全是乱的。1.4 为什么回测必须用前复权数据这个点很多人忽略但它对布林通道策略的影响非常大。如果直接用不复权数据回测遇到除权除息日股价会凭空跳空一大截。比如一只股票10转10除权日当天股价可能直接从20元变成10元日收益率会算出一个恐怖的负值。布林带是用最近N天的收盘价算的这种跳空会瞬间击穿上轨或下轨产生一堆虚假交易信号。前复权qfq的逻辑是保持当前价格不变把历史价格按分红送股比例往下调这样价格序列连续技术指标计算才有意义。日常做技术指标回测默认都用前复权。后复权hfq则是保持上市首日价格不变把之后的价格往上调适合看长期真实走势但它在回测里算出的价格和真实成交价差很多不方便模拟交易。所以权衡下来回测布林策略我推荐用前复权。2. 布林通道计算公式简单但这两个细节常被忽略2.1 布林带的数学构成布林通道Bollinger Bands是John Bollinger在1980年代提出的波动率指标逻辑很好理解用一条均线表示中枢再用标准差表示股价围绕中枢的波动范围形成上下两条通道。公式分三步中轨MID最近N日收盘价的简单移动平均标准差STD最近N日收盘价的标准差上轨UPPER MID K × STD下轨LOWER MID - K × STD最经典的参数是N20、K2。也就是说用20日均线当基准上下各扩展2倍标准差。为什么K选2理论上如果股价服从正态分布大约95%的价格应该落在均值±2倍标准差范围内所以一旦价格跑到通道外就属于“小概率事件”要么是机会、要么是异常。当然真实市场的收益分布有厚尾特征这个后面调参再细说。代码实现很直白N 20 K 2 df[mid] df[close].rolling(N).mean() df[std] df[close].rolling(N).std(ddof0) df[upper] df[mid] K * df[std] df[lower] df[mid] - K * df[std]2.2 标准差用总体还是样本结果差异有多大一个很容易被忽略的细节是pandas的std()默认算的是样本标准差分母是N-1但布林带原始定义里用的标准差取的是总体标准差分母是N。代码里如果直接写df[close].rolling(N).std()算出来的是样本标准差K2时通道会比理论值略宽一点。N20时差异不算大但在参数N比较小比如N5或N10时通道宽度差5%以上会直接改变触线信号的数量和位置。我自己的习惯是显式写ddof0明确告诉读者这里用的是总体标准差避免歧义。如果你去对比不同平台算出来的布林带有时候数字对不上十有八九就是标准差这个细节造成的。再说一个和rolling有关的坑数据前20行会因为窗口不足出现NaN。回测时不能让NaN留在后续计算里否则信号和净值全是NaN。处理方式很简单df.dropna(inplaceTrue) df df.reset_index(dropTrue)2.3 均值回归vs突破先想清楚你要的是哪种逻辑布林通道策略有两套完全相反的逻辑新手经常混在一起均值回归型价格跌到下轨认为超跌了买入等反弹价格涨到上轨认为超涨了卖出或清仓。这套逻辑默认市场是震荡市价格会围绕均线来回摆动。突破型价格突破上轨认为强势启动买入追涨价格跌破下轨认为弱势向下卖出或做空。这套逻辑默认市场在趋势中通道外是行情的开始不是行情的结束。同一套布林带两种策略的收益特征完全不一样。震荡市里均值回归表现好趋势市里突破型能把肉吃满但如果你不分青红皂白随便选一种很容易得出“布林策略就是个垃圾”的结论。本文后面的代码以均值回归型为主因为它的信号逻辑最好理解也最能说明回测里那些细节坑。但我会在参数调优部分对比突破型的效果让大家直观看到差异。3. 极简回测框架不用回测库也能看出策略好坏3.1 为什么不直接上backtrader很多人一学回测就想着上backtrader、vn.py这类框架我反而不推荐在验证布林策略时一开始就用。原因很简单框架本身有学习成本而且框架会把你的注意力从“策略逻辑”拉到“框架调用方式”上。布林通道策略的逻辑就几行完全可以用pandas向量化验证先跑通逻辑搞清楚结果再谈迁移到重型框架。我自己写策略原型时一直坚持“先用最朴素的代码实现跑出结果再考虑工程化”。这样一旦结果不对人肉debug也容易。3.2 信号生成与次日执行核心信号逻辑当日收盘价低于或等于下轨产生买入信号当日收盘价高于或等于上轨产生卖出信号。df[signal] 0 df.loc[df[close] df[lower], signal] 1 df.loc[df[close] df[upper], signal] -1signal为1代表买入-1代表卖出0代表无操作。这里就涉及回测里最关键的问题什么时候执行。如果用当天收盘价计算布林带又用当天收盘价判断是否触发信号然后在当天收盘价上成交这看起来没什么问题但实际上已经引入了未来函数——因为你在用当天全部数据走完后的收盘价去模拟“当天收盘那一刻”的交易逻辑上很难自洽而且实际交易中你也几乎不可能在收盘价那一瞬间精准买入。更稳妥的处理方式第T天收盘后计算信号第T1天开盘执行。代码里通过shift(1)把信号向后挪一天df[exec_signal] df[signal].shift(1) # 持仓状态买入后一直持有直到卖出信号出现 position 0 positions [] for sig in df[exec_signal].fillna(0): if sig 1: position 1 elif sig -1: position 0 positions.append(position) df[position] positions这个循环其实是在模拟一种最简单的状态机碰到买入信号就全仓买入碰到卖出信号就清仓中间什么都不做。把它整理成向量化写法也行但循环版逻辑更透明几万行数据跑起来也很快。3.3 净值曲线与核心绩效指标持仓状态确定之后计算每日收益和策略净值df[daily_ret] df[close].pct_change().fillna(0) df[strategy_ret] df[position] * df[daily_ret] df[nav] (1 df[strategy_ret]).cumprod()这里有一个隐含假设我们用“当日收盘价相对前一日收盘价的收益率”乘以“当日持仓状态”来计算策略收益。由于position是T-1日信号决定的T日收益率是T-1到T的实际价格变化所以等于T日开盘买入、T日收盘结算的效果这个逻辑在日线回测里是合理的。再算几个关键绩效指标def calc_performance(nav, strategy_ret, annual_factor252): total_return nav.iloc[-1] - 1 years len(nav) / annual_factor annual_return (1 total_return) ** (1 / years) - 1 # 年化波动率 volatility strategy_ret.std() * np.sqrt(annual_factor) # 夏普比率假设无风险利率3% sharpe (strategy_ret.mean() * annual_factor - 0.03) / volatility # 最大回撤 max_drawdown (nav / nav.cummax() - 1).min() return { 累计收益: total_return, 年化收益: annual_return, 年化波动: volatility, 夏普比率: sharpe, 最大回撤: max_drawdown, } perf calc_performance(df[nav], df[strategy_ret]) print(perf)最大回撤这个指标尤其重要它衡量的是从任意高点跌下来的最大幅度。很多人只看累计收益结果实盘遇到30%回撤心态直接崩了所以回测一定要把回撤打出来看看。3.4 第一版回测结果长什么样用贵州茅台2018-2023年的前复权日线N20、K2、均值回归逻辑跑出来的结果大致是交易次数不算多但每次买入基本都买在阶段低点附近累计收益通常能跑赢“买入持有”但要区分市场环境。牛市里这种“涨到上轨就卖”的策略很容易卖飞反而跑不赢拿着不动震荡市里则优势明显。第一版回测结果出来后先别急着兴奋下一个章节才是真正决定这个策略可不可用的关键。4. 给回测“挤水分”交易成本、未来函数与停牌处理4.1 交易成本模型一个最容易被新手忽略的事实是如果没有交易成本任何频繁交易策略的回测都会漂亮得假。布林均值回归策略天然就是来回打脸的策略信号频率越高交易成本占比越大。真实A股交易成本至少包括三部分佣金默认万2.5到万3且多数券商有最低5元收费印花税卖出时单边征收2023年调降后是成交金额的万分之五滑点买入时实际成交价可能比信号价高一点卖出时低一点不是总成本但对回测影响不小把成本加进回测的方式很简单在每次交易时扣掉双边成本TRADE_COST 0.001 # 单边万五佣金滑点杂费粗略合并 df[trade] df[position].diff().abs().fillna(0) df[strategy_ret] df[position] * df[daily_ret] - df[trade] * TRADE_COSTposition.diff()表示持仓变化变化时说明发生了买入或卖出减去对应交易成本。这里的0.001是单边成本假设实际你可以调成0.0005或者0.002看结果变化。加完成本再看收益你会发现策略的夏普比率明显下降。如果某个策略在扣掉成本后收益直接从正变负那它基本不具备实盘价值。4.2 未来函数的三个隐蔽变种前面提到用shift(1)避免信号与收益同日对齐但未来函数还有其他形态排查起来很隐蔽第一种用当日最高最低价触发信号。比如有人把买入条件写成“最低价触及下轨就买”这在实盘里根本做不到。你能看到最低价已经是收盘后的事了盘中你根本不知道最低点在哪里。所以信号只能基于收盘价、开盘价这类明确可用的价格。第二种用未来的MACD、均线等二次指标。比如先用全量数据计算出布林带再拿这个布林带去筛选历史信号看似没问题但如果计算指标时用了未来数据填充缺失值结果就会前视。最典型的错误是fillna(methodbfill)用后面数据填前面缺口这在回测里绝对禁止。第三种在整个数据集上做标准化。有人喜欢先把全历史收益率标准化再做策略这等于把未来数据信息泄露了。正确的做法是只用截止到T日的数据计算T日的指标。写回测时建议把策略逻辑单独封装成一个函数强制传入“截至某个日期的数据”从架构上避免未来函数。4.3 停牌、缺失数据的处理akshare返回的数据偶尔会缺几天原因通常是停牌或数据源缺失。直接dropna()把指标NaN处理掉之后还要检查交易日期是否连续。一个简单办法是检查相邻交易日间隔df[date_diff] df[date].diff().dt.days print(df[date_diff].value_counts().sort_index())正常情况下A股间隔是1天或3天周末。如果出现5天、10天甚至更大的间隔说明有停牌或者数据缺失。停牌对策略的影响是停牌期间你根本卖不掉股票如果策略在停牌期间发出卖出信号等到复牌可能已经跌了一段。严谨的做法的确是为停牌日填充价格并标记不可交易状态但对布林策略这种低频策略来说对结果影响不算太大。只要数据别缺得太离谱日常研究可以接受。5. 参数寻优与多标的验证别把过拟合当圣杯5.1 参数网格N和K怎么配布林通道的两个核心参数是N均线周期和K标准差倍数。不同市场、不同股票最优参数可能完全不同。常见的粗糙调参办法是网格搜索import itertools results [] for N, K in itertools.product([10, 20, 30, 50], [1.5, 2.0, 2.5, 3.0]): df[mid] df[close].rolling(N).mean() df[std] df[close].rolling(N).std(ddof0) df[upper] df[mid] K * df[std] df[lower] df[mid] - K * df[std] df[signal] 0 df.loc[df[close] df[lower], signal] 1 df.loc[df[close] df[upper], signal] -1 df[exec_signal] df[signal].shift(1) position 0 positions [] for sig in df[exec_signal].fillna(0): if sig 1: position 1 elif sig -1: position 0 positions.append(position) df[position] positions df[strategy_ret] df[position] * df[daily_ret] - df[position].diff().abs().fillna(0) * 0.001 df[nav] (1 df[strategy_ret]).cumprod() final_nav df[nav].iloc[-1] results.append({N: N, K: K, final_nav: final_nav, sharpe: calc_sharpe(df[strategy_ret])}) best sorted(results, keylambda x: x[sharpe], reverseTrue)[:5] print(best)但网格搜索有一个致命陷阱在单一标的上最优的参数往往只是过拟合。你把茅台调到最优的N30、K2.5换到招商银行、宁德时代效果可能立刻变差。所以真正要看的不是“最优参数有多好”而是“参数高原有多大”。把热力图打出来如果最优参数周围一圈参数表现都不错说明策略在该区域是稳健的换参数不至于崩盘如果只有孤零零一个点表现爆表周围全是亏损那基本就是数据挖掘偏差实盘大概率失效。5.2 多标的、多时间段验证判断策略是否有效更扎实的做法是换标的、换时间段做笔试试卷选5-10只不同行业的股票比如消费、银行、新能源、医药、科技各来一只时间上分牛市段、熊市段、震荡段分别跑一遍看几个关键指标胜率、盈亏比、最大回撤、年化收益如果某个策略只在特定行业、特定时间区间有效它的应用价值就要打个问号。我自己的标准是策略在多数标的、多数市场环境下能跑赢买入持有或者在相同收益下回撤明显更小才算“有点意思”。布林均值回归策略在多标的验证后通常会发现一个规律它在高波动、震荡频繁的标的上表现好在长期单边牛股的标的上很容易卖飞。这不是策略“错了”而是它的适用边界本来就是这样。搞清楚自己策略适合什么市场比找到一个万能参数重要得多。5.3 稳健性判断参数高原vs参数孤岛再多说一句判断参数稳健性的方法。把N和K作为横纵坐标把策略年化收益或夏普比率作为颜色画一个热力图pivot_table pd.DataFrame(results).pivot_table(indexN, columnsK, valuessharpe)然后观察颜色分布。如果高收益区域是一片连续的“高原”说明参数选择不那么敏感如果只是稀疏的几个亮点“孤岛”大概率是噪声。面对孤岛型结果正确做法不是去深挖那一个点而是直接放弃这套参数组合换数据源或者换策略逻辑。注意网格搜索只是快速验证工具不是专业调参手段。真要精细化优化可以用贝叶斯优化或者walk-forward分析但那是后话先把网格跑明白对新手已经足够建立“参数敏感性”的概念了。5.4 期末小结不是必要的但最后说一个实操小习惯每次跑完一组回测我都会把关键结果追加到同一个CSV里包括标的代码、起止时间、N、K、交易次数、年化收益、最大回撤、夏普比率。日积月累这个表会变成自己最宝贵的历史档案。后面想验证新想法直接拉出来对照比现跑现记高效得多。布林通道策略本身不复杂同类技术指标策略的通用方法论都是这样数据要干净、信号不能前视、成本必须扣、参数必须看稳健性而不是单点最优。把这四件事做到位你的回测结果就具备了基本的参考价值。akshare负责提供数据弹药布林带负责产生交易想法两者结合足够支撑你建立起第一套完整的量化验证流程。
返回列表