第 2 章 · 01 EMA 与卡尔曼滤波平滑对比(对应 QS004) 速查摘要:技术分析里几乎所有指标都绕不开"平滑"——把噪声大的价格序列变成趋势清晰的曲线。本节手造 10 天股价数据,对比三种平滑器:简单移动平均(SMA,等权)、指数移动平均(EMA,近期权重高)、一维卡尔曼滤波(预测-更新递归估计)。三者平滑哲学完全不同:SMA 是"无脑平均"、EMA 是"重近轻远"、卡尔曼是"带模型的最优估计"。理解这三种,就理解了平滑的核心光谱。 涉及脚本:原项目 QS004-avoid-algo-trading-mistakes/01exponentialmovingaverage.
速查摘要:技术分析里几乎所有指标都绕不开"平滑"——把噪声大的价格序列变成趋势清晰的曲线。本节手造 10 天股价数据,对比三种平滑器:简单移动平均(SMA,等权)、指数移动平均(EMA,近期权重高)、一维卡尔曼滤波(预测-更新递归估计)。三者平滑哲学完全不同:SMA 是"无脑平均"、EMA 是"重近轻远"、卡尔曼是"带模型的最优估计"。理解这三种,就理解了平滑的核心光谱。
涉及脚本:原项目 QS004-avoid-algo-trading-mistakes/01_exponential_moving_average.py(约 93 行 Python)
⚠️ 注意:本脚本完全独立可跑——数据是手造的 10 个数,不依赖 OpenBB 或任何外部数据源,只要装了 pandas/numpy/matplotlib 就能跑,是第 2 章里环境要求最低的脚本。关键参数
ewm(span=window, adjust=False)里的adjust=False必须显式写,否则 pandas 默认adjust=True会在序列开头用不同的(非递归)公式,导致结果与教科书 EMA 不一致——这是新手常踩的坑。
平滑的目的是从含噪信号里估计真实趋势。三种方法假设不同:
1. 简单移动平均(SMA)——窗口内等权平均。
SMAₜ = (Pₜ + Pₜ₋₁ + ... + Pₜ₋ₙ₊₁) / n
窗口为 n 的 SMA 把最近 n 个观测值等权求平均。优点是简单稳定;缺点是对窗口内每个数据同等对待——昨天和 n 天前权重一样,所以对近期变化反应慢。pandas 实现:series.rolling(window=n).mean()。
2. 指数移动平均(EMA)——指数衰减加权,近期权重高。
EMAₜ = α·Pₜ + (1-α)·EMAₜ₋₁, 其中 α = 2/(span+1)
α 是平滑系数,span 越大 α 越小(平滑越强、反应越慢)。关键性质是无限记忆但指数衰减——理论上每个历史数据点都还影响当前 EMA,但权重随时间指数衰减到 0。所以 EMA 比 SMA 对近期更敏感、对转折反应更快,是 MACD、RSI 等指标的基础。pandas 实现:series.ewm(span=n, adjust=False).mean()。
3. 一维卡尔曼滤波——带过程模型的递归最优估计。
卡尔曼滤波假设系统有"真实状态"但观测含噪,通过"预测 + 测量更新"两步递归估计真实状态。一维(常数模型)版本的核心循环:
预测: prediction = state_estimate (假设状态不变) prediction_error = estimation_error + process_variance 更新: kalman_gain = prediction_error / (prediction_error + measurement_variance) state_estimate = prediction + kalman_gain·(measurement - prediction) estimation_error = (1 - kalman_gain)·prediction_error
精髓在 kalman_gain(卡尔曼增益):它是一个 0 到 1 之间的"信任权重"。增益大(接近 1)说明测量可信,新观测吸收得多;增益小(接近 0)说明测量噪声大,更依赖模型预测。增益由"预测误差 / (预测误差 + 测量方差)"自动算出,是自适应的——预测越准、测量噪声越大,越相信模型;反之越相信测量。
| 平滑器 | 权重分配 | 对近期反应 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| SMA | 窗口内等权 | 慢 | 最低 | 简单去噪、长期趋势 |
| EMA | 指数衰减 | 快 | 低 | 通用、多数技术指标 |
| 卡尔曼 | 自适应(按信噪比) | 最快且可调 | 中 | 含噪估计、追踪系统 |
直觉:EMA 比 SMA 更贴近期价、转折更快;卡尔曼在测量噪声大时能智能"忽略"异常观测,平滑效果可由 process_variance/measurement_variance 调节。
data = { 'date': pd.date_range('2020-01-01', periods=10, freq='B'), 'price': [100, 103, 107, 105, 108, 107, 115, 110, 108, 112] } df = pd.DataFrame(data) df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True)
要点:
pd.date_range('2020-01-01', periods=10, freq='B') 生成 10 个工作日(freq='B' = Business day,自动跳过周末),作为日期索引。set_index('date', inplace=True) 把日期列设为索引,变成标准时序 DataFrame。window_size = 3 df['sma'] = df['price'].rolling(window=window_size, min_periods=1).mean() df['ema'] = df['price'].ewm(span=window_size, adjust=False).mean()
两个关键参数:
rolling(window=3, min_periods=1):min_periods=1 表示窗口不满 3 个时也算(用 1 个、2 个观测也能算),所以序列开头不会有 NaN;默认 min_periods=window 会让前 n-1 行是 NaN。生产代码里要不要补全取决于场景,这里为了对比方便开了。ewm(span=3, adjust=False):span 是衰减期数(等价于 SMA 的窗口概念),adjust=False 是关键——它用纯递归公式 EMAₜ = α·Pₜ + (1-α)·EMAₜ₋₁,与教科书一致;默认 adjust=True 会在序列开头用补偿公式纠正"早期样本少"的偏差,但结果就不再是教科书 EMA。做技术指标务必 adjust=False。initial_state = df['price'][0] state_estimate = initial_state estimation_error = 0.5 # 初始估计误差 process_variance = 0.1 # 过程方差 measurement_variance = 0.1 # 测量方差 kf = [] for price in df['price']: # Prediction prediction = state_estimate prediction_error = estimation_error + process_variance # Update kalman_gain = prediction_error / (prediction_error + measurement_variance) state_estimate = prediction + kalman_gain * (price - prediction) estimation_error = (1 - kalman_gain) * prediction_error kf.append(state_estimate) df['kf'] = kf
逐段解读卡尔曼循环的两个阶段:
预测阶段(Prediction):一维常数模型"假设状态不变",所以预测值 = 上一轮状态估计(prediction = state_estimate)。但预测引入不确定性,所以预测误差 = 上轮估计误差 + 过程方差(prediction_error = estimation_error + process_variance)。process_variance 表达"状态本身随机漂移多少"。
更新阶段(Update):拿到新观测 price,计算卡尔曼增益 kalman_gain = prediction_error / (prediction_error + measurement_variance)。测量方差小(准)增益接近 1、大幅吸收新观测;测量方差大(噪)增益接近 0、更信模型。然后 state_estimate = prediction + kalman_gain·(price - prediction) 修正,estimation_error = (1 - kalman_gain)·prediction_error 更新(吸收新观测后不确定性下降)。
💡 核心心法:卡尔曼增益本质是个 0-1 的"信任分配器"。它动态决定"这一轮该多信测量还是多信模型",由预测误差和测量方差的相对大小自动算出,不需要人调——这是它比固定窗口的 SMA/EMA 高明的地方。三个参数(初始误差、过程方差、测量方差)是先验设定,可通过历史数据标定。
plt.style.use('dark_background') plt.figure(figsize=(10,6)) plt.plot(df.index, df['price'], label='Original Price', marker='o', alpha=0.5, color='lime') plt.plot(df.index, df['sma'], label=f'{window_size}-Day SMA', linestyle='dashed', color='cyan') plt.plot(df.index, df['ema'], label=f'{window_size}-Day EMA', linestyle='dotted', color='magenta') plt.plot(df.index, df['kf'], label='Kalman Filter', linestyle='solid', color='yellow') plt.title('Price with SMA, EMA and Kalman Filter', color='white') plt.legend() plt.grid(True, color='gray', linestyle='--', linewidth=0.5) plt.tight_layout() plt.show()
dark_background 深色主题,四条线用不同颜色和线型区分:原价是散点+半透明线,SMA 虚线,EMA 点线,卡尔曼实线。配图如下。

从图里能直观看到:EMA 比 SMA 更贴近原价(转折快),卡尔曼滤波在噪声大时更平滑、对突发尖峰有一定抑制。
技巧一:adjust=False 是递归 EMA。做技术指标(MACD、RSI 等用 EMA 的)必须显式写 ewm(span=n, adjust=False),否则结果与教科书不符。pandas 默认 adjust=True 是为统计估计设计的,不是为金融指标。
技巧二:min_periods 控制窗口不满时的行为。rolling(window=n, min_periods=1) 让序列开头也出值(用可得样本算);默认会让前 n-1 行是 NaN。研究早期数据要不要纳入,看场景决定。
技巧三:卡尔曼增益是 0-1 的信任权重。一维常数卡尔曼里,kalman_gain = prediction_error / (prediction_error + measurement_variance),直觉就是"测量越准(分母小)→增益越大→越信测量"。理解这一条,卡尔曼滤波的核心就掌握了。
技巧四:三种平滑器用 linestyle 区分。多曲线对比图用 dashed/dotted/solid 区分线型,比单靠颜色更清晰(尤其打印成黑白时)。
技巧五:plt.style.use('dark_background') 深色主题。一行切换整体风格,适合做演示;记得给 title、label 设白色字,否则黑底看不见。
💡 速查要点:SMA 用
rolling().mean()等权平均;EMA 用ewm(span=n, adjust=False).mean()指数衰减(adjust=False是金融指标必备);卡尔曼滤波用"预测-更新"递归,核心是 0-1 的卡尔曼增益——测量越准增益越大。三者平滑哲学:无脑平均 / 重近轻远 / 带模型自适应。
rolling(window=n).mean(),窗口内等权,反应慢,适合长期趋势去噪。ewm(span=n, adjust=False).mean(),指数衰减、近期权重高、转折快,是多数技术指标的基础。adjust=False 是金融指标的必备参数,对应教科书递归公式 EMAₜ = α·Pₜ + (1-α)·EMAₜ₋₁。