【Python量化数据工程实战 #02】只会看 K 线不会做特征?动量、波动、资金流三因子实战
本文是「Python 量化数据工程实战」系列第 2 篇。上篇我们把原始 K 线清洗完了,但清洗后的 OHLCV 对模型来说还是"太原始"。真正能产生预测力的,是从原始价格中提炼出来的"特征"——动量告诉你趋势有多强,波动告诉你风险有多大,资金流告诉你谁在进场谁在离场。这篇用 pandas 从零构造三类核心特征,代码可直接嵌入你的回测框架。
本文你将得到什么
- 动量特征:5日/10日/20日收益率的滚动计算与滞后处理(防未来函数)
- 波动特征:ATR(平均真实波幅)与滚动标准差的实战写法
- 资金流特征:OBV(能量潮)斜率与量价顶背离检测
- 特征矩阵的统一输出格式(一行为一日,一列为一特征)
- 一个可直接套用的 build_features(df) 函数,输入清洗后的 K 线,输出特征矩阵
一、动量特征:趋势强度的量化表达
动量是最直觉的特征——涨了 20 天的股票,明天继续涨的概率是否更高?我们用滚动收益率来量化它:
import pandas as pd
import numpy as np
from mairui import Client
client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")
rows = client.stock_history("000001", period="d", dividend="n", lt=120)
df = pd.DataFrame(rows)
for col in ["o", "h", "l", "c", "v"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
df = df.sort_values("t").reset_index(drop=True)
动量特征:N 日滚动收益率(%)
df["ret_5"] = df["c"].pct_change(5) * 100
df["ret_10"] = df["c"].pct_change(10) * 100
df["ret_20"] = df["c"].pct_change(20) * 100
print(df[["t", "c", "ret_5", "ret_10", "ret_20"]].tail(3))
真实输出:
t c ret_5 ret_10 ret_20
47 2025-11-27 11.71 0.78 2.10 2.27
48 2025-11-28 11.61 -0.77 1.31 1.92
49 2025-12-01 11.69 0.78 2.27 2.27
关键细节:pct_change(5) 计算的是"今天收盘价 vs 5 个交易日前的收盘价",不是"过去 5 天的累计收益"。这意味着 ret_5 在 T 日只能用到 T-5 日的价格,不存在未来函数——可以放心喂给模型。
如果你需要的是"过去 5 天每日收益的连乘"(即复利收益),要这样写:
df["ret_20_compound"] = (df["c"] / df["c"].shift(20) - 1) * 100
两者的区别在于:pct_change 是单期对数收益的近似,复利收益是真实几何收益。对短期(5 日)来说差距很小,对长期(1 年)来说复利收益更准确。
二、波动特征:风险不是感觉,是数字
波动率是量化投资里最重要的风险指标之一。这里介绍两个最实用的:ATR 和滚动标准差。
2.1 ATR(Average True Range)
ATR 衡量的是"真实波动范围",比单纯的"最高价 - 最低价"更能反映极端跳空:
真实波幅 TR = max(h-l, |h-c_prev|, |l-c_prev|)
df["tr"] = np.maximum(
df["h"] - df["l"],
np.maximum(
(df["h"] - df["c"].shift(1)).abs(),
(df["l"] - df["c"].shift(1)).abs()
)
)
14 日 ATR
df["atr_14"] = df["tr"].rolling(14).mean()
print(df[["t", "c", "tr", "atr_14"]].tail(3))
真实输出:
t c tr atr_14
47 2025-11-27 11.71 0.02 0.17
48 2025-11-28 11.61 0.10 0.17
49 2025-12-01 11.69 0.09 0.17
ATR 的实战用法:
- 止损位:买入价 - 2×ATR,随市场波动自适应调整
- 仓位管理:ATR 越大 → 单票仓位越小(凯利公式的简化版)
- 趋势过滤:ATR 突然放大 → 可能进入高波动 regime,策略需降频
2.2 滚动标准差(波动率)
df["vol_20"] = df["c"].pct_change().rolling(20).std() * 100
这是教科书式的"20 日年化波动率"的日频版本。注意 pct_change() 算的是日收益,rolling(20).std() 是日收益的标准差,乘以 100 是为了变成百分比。
三、资金流特征:量在价先
量价关系是技术分析的核心假设之一。OBV(On-Balance Volume)把成交量转化为方向性资金流:
OBV:涨日记 +v,跌日记 -v,平日记 0
df["obv"] = 0
for i in range(1, len(df)):
if df.loc[i, "c"] > df.loc[i-1, "c"]:
df.loc[i, "obv"] = df.loc[i-1, "obv"] + df.loc[i, "v"]
elif df.loc[i, "c"] < df.loc[i-1, "c"]:
df.loc[i, "obv"] = df.loc[i-1, "obv"] - df.loc[i, "v"]
else:
df.loc[i, "obv"] = df.loc[i-1, "obv"]
OBV 20 日斜率(简化版:差分 / 天数)
df["obv_slope_20"] = df["obv"].diff(20) / 20
3.1 量价顶背离检测
顶背离是经典的反转信号:价格创了 20 日新高,但 OBV 没有同步新高——说明上涨缺乏资金支撑,可能是"虚涨"。
df["c_20_high"] = df["c"].rolling(20).max()
df["obv_20_high"] = df["obv"].rolling(20).max()
顶背离:价格新高,但 OBV 未新高(留 2% 容忍区间防噪声)
df["divergence_top"] = (
(df["c"] == df["c_20_high"]) &
(df["obv"] < df["obv_20_high"] * 0.98)
)
tops = df[df["divergence_top"]]
print(f"顶背离信号: {len(tops)} 次")
四、特征矩阵:从 DataFrame 到模型输入
把上面所有特征拼起来,输出一个标准格式的特征矩阵:
def build_features(df):
"""输入:清洗后的单股 K 线 DataFrame(含 o,h,l,c,v,t)
输出:特征矩阵 DataFrame"""
df = df.sort_values("t").reset_index(drop=True)
for col in ["o", "h", "l", "c", "v"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
动量
df["ret_5"] = df["c"].pct_change(5) * 100
df["ret_10"] = df["c"].pct_change(10) * 100
df["ret_20"] = df["c"].pct_change(20) * 100
波动
df["tr"] = np.maximum(df["h"] - df["l"],
np.maximum((df["h"] - df["c"].shift(1)).abs(),
(df["l"] - df["c"].shift(1)).abs()))
df["atr_14"] = df["tr"].rolling(14).mean()
df["vol_20"] = df["c"].pct_change().rolling(20).std() * 100
资金流
df["obv"] = 0
for i in range(1, len(df)):
if df.loc[i, "c"] > df.loc[i-1, "c"]:
df.loc[i, "obv"] = df.loc[i-1, "obv"] + df.loc[i, "v"]
elif df.loc[i, "c"] < df.loc[i-1, "c"]:
df.loc[i, "obv"] = df.loc[i-1, "obv"] - df.loc[i, "v"]
else:
df.loc[i, "obv"] = df.loc[i-1, "obv"]
df["obv_slope_20"] = df["obv"].diff(20) / 20
c_20_high = df["c"].rolling(20).max()
obv_20_high = df["obv"].rolling(20).max()
df["divergence_top"] = (df["c"] == c_20_high) & (df["obv"] < obv_20_high * 0.98)
feat_cols = ["t", "c", "ret_5", "ret_10", "ret_20",
"atr_14", "vol_20", "obv", "obv_slope_20", "divergence_top"]
return df[feat_cols].copy()
批量生成
codes = ["000001", "600519", "000858"]
all_features = []
for code in codes:
rows = client.stock_history(code, period="d", dividend="n", lt=120)
raw = pd.DataFrame(rows)
raw["code"] = code
feat = build_features(raw)
feat["code"] = code
all_features.append(feat)
matrix = pd.concat(all_features, ignore_index=True)
matrix.to_csv("feature_matrix.csv", index=False, encoding="utf-8-sig")
print(f"特征矩阵: {len(matrix)} 行 x {len(matrix.columns)} 列")
print(matrix.groupby("code")[["ret_5", "ret_20", "vol_20"]].mean().round(2))
真实输出:
特征矩阵: 150 行 x 11 列
ret_5 ret_20 vol_20
code
000001 0.14 1.19 0.80
600519 0.14 1.19 0.80
000858 0.14 1.19 0.80
注:演示数据下三只股票特征均值相同,属于演示数据集的特性。真实全量数据下,不同股票会呈现显著的动量/波动/资金流差异。
五、特征使用的三个注意事项
- 未来函数是头号杀手:所有特征只能用 T 日及之前的数据。shift(1)、rolling()、pct_change() 都是安全的,但千万别写 df["c"].shift(-1)——那是用明天价格算今天特征,回测结果会好得离谱、实盘亏得稀烂。
- 标准化/归一化:不同特征量纲差异巨大(收益率是百分比,OBV 是成交量级,ATR 是价格级)。喂给线性模型/神经网络前,必须做标准化:z = (x - mean) / std。
- 特征不是越多越好:20 个高度相关的特征(比如 ret_5/ret_6/ret_7...)不会带来 20 倍信息,反而增加过拟合风险。建议先用相关性矩阵去重,再用递归特征消除(RFE)筛选。
六、小结
从原始 OHLCV 到可用特征,核心就三步:
- 动量:趋势有多强(ret_5/10/20)
- 波动:风险有多大(ATR、vol_20)
- 资金流:谁在进场(OBV、背离信号)
这三个维度覆盖了量化模型对"价格行为"的大部分信息需求。下一篇我们讲:数据验证——怎样确保你清洗后的数据和特征,没有逻辑错误和一致性漏洞。
注意:本文为技术分享,所有接口调用均使用官方演示证书返回的演示数据(需替换为自己的实际证书),不构成任何投资建议。市场有风险,决策需谨慎。
代码与文档:更多 SDK 用法与接口文档可参考 https://github.com/MaiRuiApi

浙公网安备 33010602011771号