跳到主要内容

均值回归 · 布林带

直觉

趋势策略相信「会涨的继续涨」,均值回归则相反:「价格偏离均值太远,迟早会拉回来」布林带用「均线 ± k 倍标准差」画出一条正常波动带;当价格冲出带子,就视为过度反应,押注它回归均线。

布林带定义

中轨=SMAN,上轨=SMAN+kσN,下轨=SMANkσN\text{中轨} = \text{SMA}_N, \quad \text{上轨} = \text{SMA}_N + k\cdot\sigma_N, \quad \text{下轨} = \text{SMA}_N - k\cdot\sigma_N

其中 σN\sigma_NNN 期标准差,通常 k=2k=2N=20N=20

交易规则

  • 价格跌破下轨 → 超卖,做多(+1);
  • 价格涨破上轨 → 超买,做空/离场(−1);
  • ffill 维持仓位直到下一次触发。
「人话」解释:均值回归什么时候有效?

均值回归最适合区间震荡、无明显趋势的市场——价格在一个带子里来回弹。 一旦遇到强单边趋势(比如持续上涨),布林带会一路被打穿上轨,均值回归策略会反复「逆势抄顶」而巨亏。 所以趋势 vs 回归是两种对立信仰,择时/识别市场状态是关键。

可运行案例:布林带均值回归策略

import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['adj_close']

N, k = 20, 2.0
mid = close.rolling(N).mean()
sd = close.rolling(N).std()
upper, lower = mid + k*sd, mid - k*sd

# 信号:跌破下轨做多、涨破上轨做空,ffill 维持
raw = pd.Series(np.nan, index=close.index)
raw[close < lower] = 1.0
raw[close > upper] = -1.0
signal = raw.ffill().fillna(0.0)

res = quant.vector_backtest(close, signal, cost_bps=2.0, freq=252)
print(quant.performance_summary(res['equity'], res['returns'], freq=252).round(3))

# 画最近一年的带子与触发点
fig, ax = plt.subplots(2, 1, figsize=(9, 5.2), sharex=True,
                     gridspec_kw={'height_ratios':[2,1]})
recent_start = close.index.max() - pd.Timedelta(days=365)
rc, ru, rl, rm = [x.loc[recent_start:] for x in (close, upper, lower, mid)]
ax[0].plot(rc, color='#2563eb', lw=1, label='收盘价')
ax[0].plot(rm, color='#94a3b8', lw=1, label='中轨'); ax[0].fill_between(ru.index, ru, rl, alpha=0.15, label='布林带')
ax[0].plot(rc[rc < rl], 'g^', ms=5, label='做多触发')
ax[0].plot(rc[rc > ru], 'rv', ms=5, label='做空触发')
ax[0].legend(fontsize=8); ax[0].set_title('布林带与触发点(近1年)')
ax[1].plot(res['equity'].loc[recent_start:], color='crimson'); ax[1].set_title('策略资金曲线(近1年)')
plt.tight_layout(); plt.show()

动手改一改:拖动参数即时看回测

拖动窗口 NN、倍数 kk 与成本——更宽的带子(大 kk)触发少、换手低;过窄(k1k\approx1)则假信号泛滥、被成本拖垮。

# ParamSlider(SSR 预览)
参数: N, K, COST_BPS

import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['adj_close']
mid = close.rolling(N).mean(); sd = close.rolling(N).std()
upper, lower = mid + K*sd, mid - K*sd

raw = pd.Series(np.nan, index=close.index)
raw[close < lower] = 1.0
raw[close > upper] = -1.0
signal = raw.ffill().fillna(0.0)
r = quant.vector_backtest(close, signal, cost_bps=COST_BPS, freq=252)

print(quant.performance_summary(r['equity'], r['returns'], freq=252).round(3))
plt.figure(figsize=(9, 3.6))
plt.plot(r['equity'], color='crimson')
plt.title(f'布林带 N={N}, k={K}, 成本 {COST_BPS} bps'); plt.ylabel('净值')
plt.tight_layout(); plt.show()

小结

  • 布林带 = 均线 ± kk 倍标准差,刻画「正常波动带」;
  • 突破上下轨 → 押注回归均值(超卖做多、超买做空);
  • 均值回归怕强趋势——在单边行情里会持续逆势亏损。