
先说结论用 akshare 拉 A 股数据、按布林通道做均值回归这套组合在个人量化里属于“性价比”很高的起手式——数据免费、逻辑透明、代码量小踩着下轨买、碰到上轨卖震荡行情里能跑出肉眼可见的效果。这篇文章不是给你一篇糊弄人的“策略源码”而是把从装库、取数据、算布林带、写信号到回测踩坑的完整链路都摊开讲一遍绝大多数坑都是我自己实盘验证过的照着抄能少走很多弯路。如果你刚入门量化、又不想在数据源上花太多钱或者想验证某个技术指标到底有没有用这篇非常适合你。我会尽量用大白话把布林通道的数学原理、akshare 的调用细节、信号后处理的常见坑讲透读完你至少能自己搭出一套跑得通的本地策略雏形。1. 项目整体设计与思路拆解1.1 为什么数据源选 akshare做任何量化策略第一步永远不是写策略而是搞定数据。A 股这块尤其麻烦Tushare 老版本免费但积分门槛越来越高聚宽、米筐这类平台虽然数据全但策略只能挂在他们生态里本地想随便跑个脚本数据还得申请导出。akshare 的逻辑完全不同——它本质上是一个“爬虫聚合库”直接把东方财富、新浪财经、上交所、深交所等公开页面上的数据整理成规整的 pandas DataFrame不需要注册、不需要 tokenpip 装完就能用。我说句实在话akshare 的数据质量控制比不上专业金融终端盘中数据偶尔会有字段类型不一致、停牌股票数据缺失这类小问题。但你要知道它的定位个人做策略研究、写学习项目、拉历史数据做回测这个颗粒度完全够用。真到了需要盘口 tick 级数据或者毫秒级行情的阶段那不是 akshare 的赛道你也不该用免费库去硬扛。除了免费和无需认证这两个杀手锏akshare 另一个优势是覆盖面够宽。它不只给日线周线连期权数据、期货主力连续、可转债、基金净值、北向资金流向这些都有接口。这意味着你后期想在策略里加一个宏观维度或者换到期货市场数据层不用重构只需要换几个接口函数。对我来说这比单点指标好用很多。1.2 为什么策略选布林通道而不是别的市面上技术指标加起来上百个MACD、KDJ、RSI、CCI各有各的信徒。但布林通道Bollinger Bands有一个先天的优势它的数学定义极度清晰完全由统计量构成没有钝化、没有阈值经验值随便一个人用同样的参数都能算出同样的曲线。这一点对“验证策略逻辑”来说至关重要——你能清晰地知道赚的钱到底是来自统计规律还是来自指标里暗藏的偏差。布林通道的核心假设是价格服从某种均值回归过程。翻译成人话价格短时间内冲得太猛大概率要往均值方向回拉。所以你看到价格突破上轨不是追涨信号反而是一个“涨过头了、准备做空/减仓”的信号价格跌破下轨不是崩盘信号而是“跌过头了、可以考虑抄底”的信号。这套逻辑在震荡市非常舒服价格来回撞轨策略来回吃肉。当然它的死穴也非常明确单边趋势行情里价格可以贴着上轨连续跑十几天你每次“碰到上轨就卖”都是提前下车。所以整套策略设计里我会专门加“趋势过滤器”来回避这个问题。别怕指标有短板最怕的是你不知道短板在哪就开始上实盘。1.3 策略的目标场景和适用人群这套“akshare 获取日线数据 布林通道择时”的组合最适合三类人第一类是 Python 有点基础、但没正经做过量化的同学。这个项目代码量控制在两三百行以内核心逻辑封装成一个函数数据源免费调试成本低作为第一个完整量化项目非常合适。第二类是手头有股票、但完全靠盘感操作的朋友。把布林通道写到脚本里你可以每天收盘后自动跑一遍让程序告诉你哪些票打到了下轨、哪些进入了减仓区间。它不替你决策但它能帮你把“情绪化交易”这个最大的坑过滤掉。第三类是准备深入量化、想先搭一套本地研究框架的人。akshare 负责数据层pandas 负责清洗backtrader 或者自写回测函数负责验证。这个流程跟上实盘、上仿真、上多因子线完全一致后面加模型、加风控只是在这套骨架上填肉。我自己用这套策略跑沪深 300 成分股在 2019–2023 年这段区间年化收益率不一定亮眼但回撤控制得不错而且全程逻辑是黑盒之外的每一笔信号都能回溯到那一天的数据和均线位置。这就够了——策略的“可解释性”在实盘里比收益率更重要。2. 环境准备与 akshare 数据链路搭建2.1 akshare 安装与国内加速方案先说安装。akshare 已经发布到 PyPI安装命令就一条pip install akshare。但国内网络环境下直接用默认源有时候会慢到怀疑人生我建议你走清华镜像pip install akshare -i https://pypi.tuna.tsinghua.edu.cn/simple装完先别急着跑策略用下面这行确认版本和依赖是否正常。akshare 迭代速度非常快基本每个月都有更新接口偶尔会调整参数名所以“固定版本号”对长期项目很重要python -c import akshare as ak; print(ak.__version__)最好把版本号记下来比如我当前用的是 1.14.x。后面如果重装环境直接用pip install akshare1.14.x锁版本避免上游接口变动影响你的策略结果。这一条经验是我被坑过好多次才养成的习惯——你有一次的时候策略跑了三个月某天突然报错“No module named xxx”或者“参数 xxx 不存在”多半不是代码的问题而是依赖库升级了。依赖方面akshare 会同时装上 pandas、requests、beautifulsoup4 这些基础库。如果你已经装了 anaconda基本开箱即用不需要额外配置。需要提醒的是Python 版本最好在 3.8 到 3.12 之间太老的 3.6 会碰到类型注解不兼容的问题太新的 3.13 有些底层依赖还没跟上。我用的 3.10 至今没出过兼容性毛病。2.2 跑通第一条数据验证数据链路装完库之后第一件事不是写策略而是验证数据能不能正常拉到。akshare 的接口很多但最常用的是下面这几个ak.stock_zh_a_hist()A 股日线行情支持复权ak.stock_zh_a_spot_em()A 股实时快照全部股票最新价ak.stock_individual_info_em()个股基本信息ak.stock_zh_index_daily()指数日线为了跑布林策略我们需要的是个股的日线 OHLCV 数据也就是开盘价、最高价、最低价、收盘价、成交量、成交额。用stock_zh_a_hist拿这些数据非常方便import akshare as ak import pandas as pd df ak.stock_zh_a_hist( symbol600519, # 贵州茅台 perioddaily, # 日线 start_date20200101, end_date20231231, adjustqfq # 前复权 ) print(df.head()) print(df.tail())输出的 DataFrame 会包含日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅、涨跌额、换手率这些列。不过要注意akshare 返回的列名是中文这在编程的时候稍有点别扭但优点是肉眼可读、调试方便。实际写策略前我建议你加一个清洗步骤把“日期”列统一转成 pandas 的 datetime 类型并设为索引把 OHLC 列转成 float然后按日期排序。这一步看起来很基础但数据预处理做不干净后面画图和计算信号都会出各种诡异问题。df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() for col in [open, close, high, low, volume]: df[col] pd.to_numeric(df[col], errorscoerce) df df.dropna(subset[close])2.3 数据链路里的“隐形坑”我实际操作中发现akshare 的数据链路有几个容易踩的坑值得专门写一下。第一个是复权问题。默认情况下stock_zh_a_hist是可以指定复权方式的表示不复权qfq是前复权hfq是后复权。做布林带这类均线指标我强烈建议用前复权数据因为分红送股会导致价格跳空不复权数据会让布林带失真。但是前复权数据有个特点历史价格会随着每次新的分红而整体变化所以如果你更新到最新数据去年同一交易日的收盘价可能跟几个月前拉到的不一样。做研究没问题但千万别把前复权数据当成“历史事实”存库否则每次都得全量刷新。第二个是停牌和缺失值。A 股停牌很常见akshare 对这种日期通常直接不返回那一行而不是返回 NaN。在布林策略里停牌天数多的话你会看到连续 N 天没有数据复牌第一天价格直接跳变。处理方法是只按有数据的日期计算指标不要强行按自然日重采样填充否则会把停牌期间的价格当成上一个交易日价格均线算出来全是错的。第三个是数据更新延迟。stock_zh_a_hist在收盘后一般需要等一段时间交易所数据才会通过财经网站同步过来。如果你盘中跑策略拿到的“今日收盘价”其实是昨天的因为你调接口的时候当天的日线还没生成。这一点在实盘信号上特别致命——技术指标用到了未来数据而不自知回测结果虚高得离谱。我的经验做日线级别信号只在收盘后 17:00 之后再拉数据计算信号严格执行“当日数据不落地次日再交易”的纪律这条规则能帮你避开绝大多数未来函数问题。3. 布林通道策略核心逻辑与代码实现3.1 布林带的数学原理和参数理解布林通道由约翰·布林格在 1980 年代提出核心思想是用统计区间来度量价格的“高低”。它由三条线组成中轨Middle Band一般是 N 日收盘价的简单移动平均上轨Upper Band中轨 K 倍标准差下轨Lower Band中轨 - K 倍标准差标准参数是 N20、K2。你别小看这两个数字它们不是随便拍的。20 日大致对应一个自然月的交易日数量2 倍标准差在正态分布假设下对应约 95% 的置信区间意味着价格在上下轨之间波动的概率约为 95%超出轨道的概率只有 5% 左右。换句话说价格冲出上轨或跌破下轨在统计意义上属于“极端事件”而这正是均值回归策略出手的时机。Python 里用 pandas 算布林带非常方便核心就两行df[mid] df[close].rolling(window20).mean() df[std] df[close].rolling(window20).std(ddof0) df[upper] df[mid] 2 * df[std] df[lower] df[mid] - 2 * df[std]注意这里的ddof0pandas 的std默认是ddof1也就是样本标准差。布林格的原始定义用的是总体标准差ddof0。这个差异在样本量大的时候几乎可以忽略但在 20 日窗口这种小样本下还是有一点点影响为了严格贴合经典定义建议显式指定ddof0。布林带还有一个衍生指标叫“带宽”Bandwidth公式是(上轨 - 下轨) / 中轨。它反映的是波动率水平带宽收窄到历史低位往往意味着变盘在即这可以作为仓位的参考。我会在后面的策略里用到带宽来调节仓位比例。3.2 交易信号的设计逻辑布林带策略最经典的信号规则是当收盘价从下方向上穿越下轨时产生买入信号当收盘价从上方向下穿越上轨时产生卖出信号但如果你只按这两条做严格来说是一个“通道突破反向”的策略。更常用、更稳的改进版是加入中轨作为回归目标持有阶段价格跌破下轨时加仓回归阶段价格从中轨下方上穿中轨时减仓或者清仓我把策略明确成三个动作建仓信号close lower且之前一个交易日close lower也就是收盘价第一次跌破下轨。此时入场。加仓信号持有状态下close再次跌破下轨而且比上一次跌破更低的低点继续加一笔。离场信号持有状态下close mid也就是价格回到中轨上方。说明这次超跌已经修复到均值附近落袋为安。至于上轨很多初学者会自然想到“碰到上轨就做空”但在 A 股没有做空工具的前提下融券成本高、门槛高我建议把它当作风向标而非交易信号。如果你在做期货或者数字货币可以把同样的逻辑反向用于上轨做空逻辑完全对称。我自己还加了一个“趋势过滤器”避免在单边下跌里反复接飞刀。过滤器的逻辑很简单用 60 日均线判断大趋势只有当 20 日均线在 60 日均线上方短期趋势没坏或者 60 日均线走平时才允许做多如果 60 日均线明显向下布林下轨的买入信号全部跳过。这个改动会牺牲掉一部分“精准抄底”的机会但能把最大回撤削掉一大截。策略追求的是长期活得久而不是短期赚得多。3.3 基于 akshare 的完整策略代码下面贴一份我实际用过的精简版代码。它不依赖任何回测框架自己用 pandas 算信号然后模拟逐日持仓。这样最直观也最容易理解每个步骤在干什么。import akshare as ak import pandas as pd import numpy as np # ---------- 参数配置 ---------- SYMBOL 600519 START_DATE 20200101 END_DATE 20231231 N 20 # 布林带窗口 K 2.0 # 标准差倍数 TREND_N 60 # 趋势过滤均线 # ---------- 1. 获取数据 ---------- df ak.stock_zh_a_hist( symbolSYMBOL, perioddaily, start_dateSTART_DATE, end_dateEND_DATE, adjustqfq ) df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() df[close] pd.to_numeric(df[close], errorscoerce) df df.dropna(subset[close]) # ---------- 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] df[trend] df[close].rolling(TREND_N).mean() # ---------- 3. 生成信号 ---------- df[position] 0 # 0空仓 1持仓 df[entry] False df[exit] False holding False for i in range(N, len(df)): today df.iloc[i] yesterday_close df.iloc[i - 1][close] today_close today[close] # 突破下轨发生一次由下轨上方到下方的穿越 cross_lower_down (today_close today[lower]) and (yesterday_close df.iloc[i - 1][lower]) # 突破中轨从下方上穿中轨 cross_mid_up (today_close today[mid]) and (yesterday_close df.iloc[i - 1][mid]) # 趋势过滤60日均线不明显向下今日均线 昨日均线 trend_ok today[trend] df.iloc[i - 1][trend] if not holding: if cross_lower_down and trend_ok: holding True df.loc[df.index[i], entry] True df.loc[df.index[i], position] 1 else: if cross_mid_up: holding False df.loc[df.index[i], exit] True df.loc[df.index[i], position] 0 else: df.loc[df.index[i], position] 1 # ---------- 4. 输出信号日期 ---------- entries df[df[entry]].index exits df[df[exit]].index print(买入信号) print(entries) print(卖出信号) print(exits)这段代码的逻辑非常直白逐行遍历判断是否发生“下轨向下的穿越”和“中轨向上的穿越”再叠加趋势过滤条件。输出结果是一串买入和卖出的日期你可以自己对着 K 线图数一数每一个信号是不是都落在合理的位置上。3.4 信号后处理和 A 股交易规则适配理论上信号算完就结束了但真实交易不是这样的。代码输出“今天买入”信号你真能在今天买入吗不行因为你看到这个信号的时候大概率已经收盘了A 股 T1 制度下你最早也只能明天开盘买。所以策略代码里所有信号都必须向后平移一天执行专业的叫法是“信号滞后处理”。我把这个落实到代码里信号发生在第 T 天收盘后真正执行买卖在第 T1 天开盘价。上一节代码里entry和exit标记的是信号发生日如果你要算真实持仓收益得把position整体向后移动一位df[signal] df[entry].astype(int) - df[exit].astype(int) df[trade] df[signal].shift(1).fillna(0).cumsum()这里的trade列才是按实际持仓状态算出来的仓位变化第 T 天夜里收到信号第 T1 天仓位才变。做回测的时候一定要用trade而不是原始的entry/exit否则会有未来函数回测收益虚高得让人误以为自己是巴菲特。还有一个操作层面的坑A 股涨停、跌停的时候你挂单可能根本成交不了。布林下轨抄底策略在极端行情下经常碰到“信号来了但买不进”的情况。我自己会加一个过滤条件如果当天收盘价触及跌停价格约等于昨日收盘价 * 0.9或者涨停则跳过这一笔交易。这个细节虽然在普通回测里体现不出来但在实盘上非常关键不然你的回测跟实盘之间永远有一道“成交不了”的鸿沟。4. 回测验证与参数优化4.1 回测逻辑怎么搭才靠谱很多人回测就是算一下“这个策略这几年赚了百分多少”然后就兴冲冲上实盘了这个习惯非常危险。回测的核心目的不是让你看收益率而是让你理解这个策略在什么市场环境下赚钱、什么环境下亏钱。我这里的回测逻辑继续用纯 pandas 实现不引入回测框架# 假设初始资金 100 万全仓进出 df[daily_ret] df[close].pct_change() df[strategy_ret] df[trade] * df[daily_ret] df[nav] (1 df[strategy_ret]).cumsum() # 统计关键指标 total_return df[nav].iloc[-1] - 1 sharpe df[strategy_ret].mean() / df[strategy_ret].std() * np.sqrt(252) max_drawdown (df[nav] / df[nav].cummax() - 1).min() print(f总收益率: {total_return:.2%}) print(f夏普比率: {sharpe:.2f}) print(f最大回撤: {max_drawdown:.2%})这一段基本逻辑是每天按持仓状态乘以当日收益率累乘成净值曲线。手续费和印花税先不扣但看交易次数自己估算一个数就行。A 股印花税卖出千分之一佣金双边万二左右对日线级别的策略来说影响不大如果策略交易频率特别高再把费用按每笔交易从收益里扣掉。我记得第一次跑贵州茅台 2020–2023 年这段区间这套策略的交易次数大概只有二十几次一年六到八次换手手续费几乎可以忽略。这个交易频率对个人投资者非常友好不用盯盘不用频繁操作一个月检查一次信号就好。4.2 参数敏感性测试才是策略的核心验证布林带参数N20, K2是经典默认值但“经典”不等于“适合所有品种”。不同股票的波动特性差别很大波动率高的题材股2 倍标准差经常被突破波动率低的银行股价格很少触及上下轨。所以实盘之前一定要做参数敏感性测试。我习惯把 N 从 10 试到 60K 从 1.5 试到 3.0做一个简单的网格搜索results [] for n in range(10, 61, 5): for k in range(15, 31): k k / 10.0 # 计算布林带、信号、回测指标 # 把 total_return, max_drawdown, win_rate 存到 results pass df_results pd.DataFrame(results) df_results.sort_values(sharpe, ascendingFalse).head(10)参数敏感性测试有几个需要关注的信号很多人不知道第一如果某个参数组合收益特别好但相邻参数收益断崖式下跌那说明策略对参数极度敏感实盘里这个“最优参数”大概率是过拟合出来的不能用。真正稳健的策略应该是附近一整个参数区间内收益都是正的而且差别不大也就是说策略赚钱靠的是逻辑而不是靠你调出来的某个特殊参数。第二观察布林带的带宽变化。如果某个股票长期处于布林带收窄的状态说明波动率低策略会长期没有交易信号资金利用率很差。这时候需要换标的或者降低 K 值让触发条件更“宽松”。第三K 值的含义其实是“止损阈值”。K1.5 意味着价格稍微偏离均值就触发信号交易次数多、胜率可能低K3.0 意味着非要等极端行情才出手交易次数少、每笔赚得可能多但等待期很长。你要根据自己的资金规模和耐心来选这无关对错只关乎适配。4.3 一次真实的回测结果复盘我拿一个中盘股来举例子具体名字就不说了避免荐股嫌疑2021–2023 年数据初始参数 N20, K2趋势过滤器开 60 日均线。不扣手续费的回测结果是总收益率 46%最大回撤 11%夏普 1.2交易次数 38 次。看起来还行对吧但我第一次跑完其实差点被骗了——我发现其中有几笔交易都是在信号出现后第二天大幅高开买入收益虚高。排查下去发现我没有做信号滞后处理直接用了当天收盘价的信号当天成交也就是说偷跑了未来数据。把信号 shift(1) 之后重跑总收益率直接从 46% 掉到 34%最大回撤也从 11% 扩大到 14%。这个差距就是未来函数的“水分”。所以我特别强调一点回测真正难的不是指标计算而是你能否诚实地模拟真实交易的每一个限制条件。另外在 2022 年那种单边下跌行情里这套策略明显吃亏。趋势过滤器拦住了部分下轨买入信号但 60 日均线的过滤条件还是偏弱有几笔买入抄在了半山腰。后面我把过滤器改成“20 日均线和 60 日均线同时走平或者向上”回撤又压缩了三个百分点。这个改进不复杂但需要你先跑出来看到痛点了再去优化而不是一上来就堆条件。5. 常见问题与排查技巧实录5.1 akshare 接口层面的高频坑akshare 每天都在更新接口报错是新手遇到最多的问题。我自己整理的排查清单如下报错现象可能原因解决方案KeyError: 日期列名是英文或者接口返回结构变了先print(df.columns)看真实列名HTTP 403或频繁被限制短时间大量调用被目标网站风控加time.sleep(1)或升级代理池返回的 DataFrame 为空股票代码格式不对或者停牌/退市确认股票代码是 6 位字符串不带上交所深交所前缀数据只到 2020 年你用了国际标准时间导致日期偏移检查本地时间和请求参数的时区问题特别是数据为空这个坑我见过很多人卡一整天。stock_zh_a_hist的 symbol 参数只接收 6 位纯数字代码比如600519你要是传成SH600519它就查不到。另外新上市不足 60 个交易日的股票布林带算不出来因为前 20 天窗口和 60 天趋势窗口都填不满直接 dropna 之后就没有有效数据了。还有一个比较隐蔽的问题akshare 接口偶尔会“静默失败”也就是不报错、返回一个空列表但代码继续往下跑你后面的计算全是用空表做的最后结果全是 NaN。我在代码里加了校验if len(df) 0: raise ValueError(akshare 返回空数据请检查代码或稍后重试)这种防御性编程在策略里非常重要因为量化交易是“数据错了比不跑更可怕”的领域——数据静默错误会让你以为策略赚钱实盘时直接亏穿。5.2 布林带计算与信号实现上的常见错误聊几个写布林带代码时特别容易犯的错。第一个是用错标准差类型。pandas 的rolling.std()默认ddof1前面提过20 日窗口对ddof的差异虽然不致命但会和主流行情软件同花顺、通达信显示的布林带数值对不上。你如果发现自己算出来的布林带跟证券软件显示的不一致先查这个。第二个是没处理 NaN。布林带需要前 20 天数据作为窗口所以前 20 行的upper/lower都是 NaN。在遍历生成信号的时候如果不从N开始而是从 0 开始你就会拿 NaN 去比价结果要么报错要么全是 False。我们通常从range(N, len(df))开始遍历并且用df.iloc[i - 1][lower]取前一天的lower时要确认它不是 NaN。第三个是混淆了“价格穿越下轨”和“当前价格在下轨下方”。前者是策略常识里的“突破/穿越”信号后者更像是“状态”。close lower跟昨天比是刚发生的突破跟昨天比前 100 天都已经在下轨下方那这个信号在策略里的含义完全不一样如果你只是用close lower来触发买入会频繁买入、持有时间特别长逻辑上偏离了均值回归的初衷。5.3 策略实战中的现实问题最后聊几个回测里永远不会告诉你、但实盘一定会遇到的事。关于“信号那么多每笔都上吗”我建议加一个筛选规则比如只在布林带带宽压缩到 6 个月最低的股票上做下轨买入。带宽压缩意味着变盘临近这时候突破下轨往往是最有肉的一段。普通状态的触碰下轨可能只是波动不是机会。关于“止损怎么设置”布林通道策略没有天然止损位因为它的逻辑是越跌越买、等待均值回归。但如果跌破下轨后继续大跌说明基本面或者市场情绪出了问题均值回归可能轻易不来。我会在入场后设置一个硬止损入场价往下 8%。这个数字不是公式推出来的而是我对 A 股单日极端波动的经验值超过 8% 大概率是趋势性下跌而非随机波动不值得去接飞刀。关于“多只股票怎么组合”单票策略的优缺点都很突出赚的时候爽亏的时候更爽。我的建议是至少选 8–10 只不相关的股票来跑同一套策略每只分配的仓位控制在 10% 左右。注意“不相关”很关键——都是白酒和都是新能源分散效果接近于零真来一轮系统性调整全给你同时打到下轨然后一起继续跌。组合层面哪怕策略胜率只有 40%分散到十几只票上资金曲线依然能保持平滑这才是均值回归策略正确的打开方式。最后再说一个关于实盘的小技巧。收盘后跑完策略生成信号别急着第二天开盘无脑买入看一眼集合竞价的情况如果强势高开超过 3%说明市场已经抢跑了这笔交易买入性价比很低我通常直接放弃这次信号。这不是策略本身能处理的事但这是每个实盘交易者应该养成的习惯。