过滤和打分都写好了,选出来的票到底靠不靠谱?很多量化新手把选股器做完就急着实盘,却忘了最重要的一步:用历史数据给自己出一份体检报告。这篇教你用滚动回看的方法,给选股器做一个可量化的验收。
一、为什么选股器必须「回头看」
打分器输出 Top-5,看起来逻辑自洽,但「自洽」不等于「能赚钱」。一个常见陷阱是:因子在样本内看起来好,是因为你无意中拟合了这段数据的噪声,而不是发现了规律。
解决办法很简单------滚动回看:每天用过去的数据选股,观察次日这些候选的真实表现。累积足够多的「选股日-次日」样本后,胜率、平均涨幅、超额收益一目了然。
本文你将得到:
- 一套基于滚动窗口的选股回看框架(29 个交易日可复跑);
- 候选组合 vs 全池基准的多维验收指标(胜率、超额、命中率);
- 一份可直接落地的 CSV 明细,方便你做更细粒度的归因。
二、准备环境
bash
pip install mairui pandas numpy
三、完整代码(已实测跑通)
python
from mairui import Client
import random
import pandas as pd
import numpy as np
LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书,请换成你自己的正式证书
SEED = 20260918 # 与 #03 同种子,保证数据可互验
WINDOW = 21 # 因子窗口(含端点,20 日动量需要 21 个点)
TOP_N = 5
POOL = {
"600519": "贵州茅台", "000858": "五粮液",
"601398": "工商银行", "600036": "招商银行",
"300750": "宁德时代", "002594": "比亚迪",
"600276": "恒瑞医药", "603259": "药明康德",
"600030": "中信证券", "601688": "华泰证券",
"000333": "美的集团", "600900": "长江电力",
}
WEIGHTS = {"动量": 0.4, "低波": 0.3, "流动性": 0.3}
def perturb(bars, seed):
# 演示证书返回固定样例数据;确定性扰动模拟行情起伏。
# 换成正式证书后删掉本函数调用即可,其余逻辑零改动。
rng = random.Random(seed)
out = []
for b in bars:
k = 1.0 + (rng.random() - 0.5) * 0.10
kv = 1.0 + (rng.random() - 0.5) * 0.40
nb = dict(b)
for f in ("o", "c", "h", "l"):
if f in nb and nb[f] is not None:
nb[f] = round(float(nb[f]) * k, 2)
if nb.get("v") is not None:
nb["v"] = round(float(nb["v"]) * kv)
out.append(nb)
return out
def fetch_bars(code):
bars = Client(LICENCE).stock_history(code=code, st="", et="", period="d", dividend="f")
if not bars:
raise RuntimeError(f"{code} 未获取到数据,请检查证书是否有效")
return perturb(bars, seed=SEED + int(code))
def to_frame(bars):
df = pd.DataFrame(bars)
for col in ("o", "h", "l", "c", "v"):
df[col] = pd.to_numeric(df[col], errors="coerce")
df["t"] = df["t"].astype(str)
return df.sort_values("t").reset_index(drop=True)
def factor_scores(df):
c, v = df["c"], df["v"]
mom = c.iloc[-1] / c.iloc[0] - 1
lowvol = -c.pct_change().tail(20).std()
liq = (v * c).tail(20).mean()
return {"动量": mom, "低波": lowvol, "流动性": liq}
def zscore(vals):
a = np.asarray(vals, dtype=float)
sd = a.std()
return (a - a.mean()) / sd if sd > 0 else np.zeros_like(a)
def pick_top_n(frames, day):
rows = []
for code, df in frames.items():
hist = df.iloc[day - WINDOW:day]
rows.append({"代码": code, **factor_scores(hist)})
table = pd.DataFrame(rows)
for k in WEIGHTS:
table[k + "_z"] = zscore(table[k])
table["综合分"] = sum(table[k + "_z"] * w for k, w in WEIGHTS.items())
return table.sort_values("综合分", ascending=False).head(TOP_N)["代码"].tolist()
def main(out_csv="review_detail.csv"):
frames = {code: to_frame(fetch_bars(code)) for code in POOL}
n = min(len(df) for df in frames.values())
print(f"每只标的 {n} 条日线,因子窗口 {WINDOW},可回看 {n - WINDOW} 个交易日\n")
records = []
for day in range(WINDOW, n):
picks = pick_top_n(frames, day)
ret_all, ret_pick = [], []
for code, df in frames.items():
r = df["c"].iloc[day] / df["c"].iloc[day - 1] - 1
ret_all.append(r)
if code in picks:
ret_pick.append(r)
avg_all = float(np.mean(ret_all))
avg_pick = float(np.mean(ret_pick))
records.append({
"日期": frames["600519"]["t"].iloc[day],
"候选": "、".join(POOL[c] for c in picks),
"候选均涨": avg_pick,
"全池均涨": avg_all,
"超额": avg_pick - avg_all,
"候选胜": sum(1 for r in ret_pick if r > 0),
})
detail = pd.DataFrame(records)
detail.to_csv(out_csv, index=False, encoding="utf-8-sig")
show = detail.copy()
for col in ("候选均涨", "全池均涨", "超额"):
show[col] = (show[col] * 100).round(2).astype(str) + "%"
print(show.to_string(index=False))
win_rate = (detail["候选均涨"] > 0).mean()
ex_win = (detail["超额"] > 0).mean()
hit_rate = detail["候选胜"].sum() / (len(detail) * TOP_N)
print("\n===== 回看验收汇总 =====")
print(f"回看交易日数 : {len(detail)}")
print(f"候选组合日胜率 : {win_rate * 100:.2f}%")
print(f"候选次日平均涨幅 : {detail['候选均涨'].mean() * 100:+.4f}%")
print(f"全池同期平均涨幅 : {detail['全池均涨'].mean() * 100:+.4f}%")
print(f"平均超额 : {detail['超额'].mean() * 100:+.4f}%")
print(f"超额为正的天数占比 : {ex_win * 100:.2f}%")
print(f"个股次日上涨命中率 : {hit_rate * 100:.2f}%")
print(f"明细产出 : {out_csv}")
if __name__ == "__main__":
main()
四、真实运行结果
在 Python 3.9 环境实测运行,控制台输出汇总如下:
text
===== 回看验收汇总 =====
回看交易日数 : 29
候选组合日胜率 : 34.48%
候选次日平均涨幅 : -0.7776%
全池同期平均涨幅 : +0.1459%
平均超额 : -0.9235%
超额为正的天数占比 : 27.59%
个股次日上涨命中率 : 43.45%
明细产出 : review_detail.csv
这个结果值得认真看:候选组合在 29 个交易日中平均跑输全池基准约 0.92%。这不是代码错了------恰恰相反,它揭示了回看验收的核心价值:打分器好不好用,数据说了算。当前三因子权重(0.4/0.3/0.3)在这个样本期内不适应,如果不做回看,你可能一直带着一个帮倒忙的选股器实盘交易。
说明:演示证书返回固定样例数据,代码用确定性扰动模拟行情起伏;换成你自己的正式证书后删掉
perturb调用即为真实行情,回看逻辑零改动。
五、三个关键细节
- 回看必须用滚动窗口,不能用整条历史一次性选:整条历史一次性选股会泄露「未来信息」(你知道了整个区间的表现来挑因子),滚动窗口才是真实的「当日收盘后选股、次日观察」。
- 超额比绝对收益更重要:候选涨 1% 但全池涨 2%,你的选股器实际上在帮倒忙。超额 = 候选均涨 - 全池均涨,是验收选股器独立价值的核心指标。
- 样本量决定结论可信度:29 天是一个起点,实盘建议累积至少 60 个交易日以上再做决策。样本量不够时,一次大涨/大跌就能把平均拉偏。
六、小结与下篇预告
选股器好不好用,不能靠直觉,要靠滚动回看的数据说话。本篇文章的代码框架可以直接套用到你自己的因子模型上------只需要替换 factor_scores() 里的因子定义,其余逻辑完全不用改。
下篇预告:《【Python量化选股流水线 #05】过滤阈值拍脑袋?窗口与阈值的敏感度实验》------用参数扫描找到最适合你的窗口大小、Top-N 数量和因子权重。
免责声明
本文所有代码与数据仅用于技术学习与演示,不构成任何投资建议。股市有风险,投资需谨慎。
代码与文档
SDK 与更多语言版本:https://github.com/MaiRuiApi