本地量化系列教程 · 第四篇。配套笔记本:
基于动量的交易策略.ipynb;数据:stock_data/000063_中兴通讯.csv(本地日线,替代原笔记本的 akshare 美股接口数据,方法完全一致)。
动量(momentum)是实证金融里最坚实、也最反直觉的异象之一:过去一段时间涨幅大的资产,未来短期往往继续跑赢。Jegadeesh & Titman 1993 年在美股横截面层面发现它,此后在几乎所有市场、几乎所有资产类别上被反复验证。这一篇实现它最简单的时间序列版本:以过去 N 日平均收益的符号作为方向信号,并回答一个关键问题——动量窗口 N 到底该取多大?
1. 策略原理
时间序列动量(time-series momentum)只看资产自己:
$$ \text{position}_t = \mathrm{sign}\left( \frac{1}{N}\sum_{i=1}^{N} r_{t-i} \right) $$
过去 N 天平均收益为正 → 做多;为负 → 做空(或空仓)。注意信号取的是符号,仓位永远是 +1 或 -1,与涨的幅度无关——动量策略赚的是方向的钱,不是幅度的钱。
与双均线的微妙差别:双均线比较的是价格的两层平均,动量比较的是收益的累计方向;两者经常同向,但对趋势起点和终点的反应速度不同。
滞后执行是动量策略的第二个易错点。 原笔记本中有一处 position.shift(i)——信号回看窗口是 N 天,那这组信息最早也要 N 天后才完全可用,保守起见延迟 N 天执行。比第二篇的 shift(1) 更保守。严格说,rolling(N).mean() 的值在每一天收盘都可用,shift(1) 已经足够防未来函数;shift(N) 牺牲了时效性换取更强的防作弊保险。两种做法都常见,关键是写清楚自己的假设,并保证信号只用得到信号那一刻为止的信息。
2. 实现
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
z = pd.read_csv('./stock_data/000063_中兴通讯.csv')
z['日期'] = pd.to_datetime(z['日期'])
z = z.set_index('日期').sort_index()
z = pd.DataFrame({'close': z.loc['2020-01-01':, '收盘']})
z['returns'] = np.log(z['close'] / z['close'].shift(1))
# 基准版本:5 日动量
z['position'] = np.sign(z['returns'].rolling(5).mean())
z['strategy'] = z['position'].shift(5) * z['returns']
np.sign() 直接输出 -1/0/+1 三档信号(收盘价不动时为 0)。
3. 窗口 N 是动量策略的生命线
动量研究里最重要的经验参数就是回看窗口。经典结论:横截面动量的甜点区在 3~12 个月,1 个月以内和 1 年以上效果都会衰减;但在日线级别、单只股票上,这个结论完全要重新标定。直接扫一遍:
for i in [1, 3, 5, 7, 9]:
z[f'position_{i}'] = np.sign(z['returns'].rolling(i).mean())
z[f'strategy_{i}'] = z[f'position_{i}'].shift(i) * z['returns']

实际结果(2020-01 ~ 2025-07,中兴通讯):
| 窗口 N | 累计收益 | 年化夏普 |
|---|---|---|
| 买入持有 | -3.5% | — |
| 1 天 | -29.7% | -0.16 |
| 3 天 | -74.6% | -0.62 |
| 5 天 | -37.0% | -0.21 |
| 7 天 | +228.2% | +0.54 |
| 9 天 | -6.3% | -0.03 |
三个信息量很大的现象:
① 峰是尖的。 7 天窗口 +228%,左右两侧的 5 天、9 天却是负的。相邻参数天差地别,几乎可以断定这个"最优窗口"主要是噪声——它恰好拟合了这段历史里的几波节奏,而不是发现了什么稳定的结构。这就是第二篇说过的参数敏感 = 过拟合警告,在动量策略上表现得更赤裸。
② 短窗口普遍亏钱。 1~3 天窗口在这个标的上系统性亏损:日频以下的高频动量在 A 股 T+1、涨跌停、隔夜跳空的约束下基本没有获利空间,交易摩擦全吃掉了。
③ 同一个"动量",不同资产结论不同。 原笔记本在 NVDA(2019-2025,十倍股)上跑 5 日窗口,动量策略累计收益显著为正——因为强趋势股动量信号频繁正确;而阴跌的中兴通讯上同样参数就是灾难。动量的有效性依赖标的处于趋势市,这是策略的隐含前提,不是普适规律。
4. 信号长什么样

信号在 ±1 之间高频翻转,5 日窗口下大约每两三天就换一次方向。这直接指向动量策略的命门——换手率。实盘中每次翻转都是一买一卖,即便 0.05% 的单边费率,一年几百次翻转就是每年百分之几的确定性损耗。改进方向很直接:给信号加迟滞(hysteresis),比如只有当信号连续 3 天同向才换仓。
5. 与前两篇策略的风格对照
| 双均线(02) | 均值回归(03) | 动量(04) | |
|---|---|---|---|
| 逻辑 | 趋势确认后跟随 | 极端偏离后回归 | 近期方向延续 |
| 仓位 | 永远 ±1 | ±1/0 三态 | ±1(近 EOD 翻转) |
| 怕什么行情 | 震荡 | 单边趋势 | 突然反转 |
| 关键参数 | 短/长窗口 | N、k | 回看窗口 N |
| 换手率 | 中 | 中 | 高 |
三种策略是同一枚硬币的三面:都是对"价格自相关性"下注,只是赌的相关性存在于不同时间尺度。
6. 小结与练习
- 时间序列动量 =
sign(rolling_mean(returns, N)),方向信号,与幅度无关; - 窗口 N 是生命线,而日线级别单票的"最优 N"大概率是噪声——参数邻域是否平滑,比参数本身的收益更重要;
- 动量的有效性以趋势行情为前提,换标的必须重新验证;
- 高换手是动量的天然成本,实盘必须加迟滞或换仓缓冲。
练习:
- (基础)画出 1~30 天每个窗口的夏普热力图(而不是离散 5 个点),观察 7 天峰的邻域是否平滑;
- (进阶)给信号加"连续 3 日同向才换仓"的迟滞逻辑(提示:滚动窗口内
min == max),对比换手次数和净收益; - (进阶)在
data/BTCUSDT_1h.csv上用小时级窗口(如 24/72/168 小时)重跑动量,与日线结论对比; - (挑战)实现横截面动量:用
stock_data/里全部股票计算过去 20 日收益,每月末买入最强 10%、卖出最弱 10%(这需要第六篇的批量回测基础设施)。
上一篇:03 · 均值回归 · 下一篇:05 · 事件驱动回测:RSI 策略与资金曲线