【Python量化选股流水线 #04】选股器好不好用?「昨日候选→今日表现」回看验收

过滤和打分都写好了,选出来的票到底靠不靠谱?很多量化新手把选股器做完就急着实盘,却忘了最重要的一步:用历史数据给自己出一份体检报告。这篇教你用滚动回看的方法,给选股器做一个可量化的验收。

一、为什么选股器必须「回头看」

打分器输出 Top-5,看起来逻辑自洽,但「自洽」不等于「能赚钱」。一个常见陷阱是:因子在样本内看起来好,是因为你无意中拟合了这段数据的噪声,而不是发现了规律。

解决办法很简单------滚动回看:每天用过去的数据选股,观察次日这些候选的真实表现。累积足够多的「选股日-次日」样本后,胜率、平均涨幅、超额收益一目了然。

本文你将得到:

  1. 一套基于滚动窗口的选股回看框架(29 个交易日可复跑);
  2. 候选组合 vs 全池基准的多维验收指标(胜率、超额、命中率);
  3. 一份可直接落地的 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. 超额比绝对收益更重要:候选涨 1% 但全池涨 2%,你的选股器实际上在帮倒忙。超额 = 候选均涨 - 全池均涨,是验收选股器独立价值的核心指标。
  3. 样本量决定结论可信度:29 天是一个起点,实盘建议累积至少 60 个交易日以上再做决策。样本量不够时,一次大涨/大跌就能把平均拉偏。

六、小结与下篇预告

选股器好不好用,不能靠直觉,要靠滚动回看的数据说话。本篇文章的代码框架可以直接套用到你自己的因子模型上------只需要替换 factor_scores() 里的因子定义,其余逻辑完全不用改。

下篇预告:《【Python量化选股流水线 #05】过滤阈值拍脑袋?窗口与阈值的敏感度实验》------用参数扫描找到最适合你的窗口大小、Top-N 数量和因子权重。

免责声明

本文所有代码与数据仅用于技术学习与演示,不构成任何投资建议。股市有风险,投资需谨慎。

代码与文档

SDK 与更多语言版本:https://github.com/MaiRuiApi

相关推荐
田里的水稻1 小时前
PyTorch_Dataloader\TensorBoard---数据加载与可视化
人工智能·pytorch·python·神经网络·机器学习
Dmz丶2 小时前
前端转 AI 100 天 Day 27:Docker 容器化——从“我电脑能跑“到“到处能跑“
python
卿卿的产品经理日记2 小时前
【AI产品经理实战】Day 38|try-except 踩坑实录:报错是数据
人工智能·笔记·python·学习·产品经理
全栈练习生2 小时前
文档智能与实践
python·ai
爱喝热水的呀哈喽2 小时前
HEJIAN RESEARCH
开发语言·matlab
白露与泡影2 小时前
后端 Java 面试核心八股汇总|高频面试题 + 解析,覆盖基础到进阶
java·开发语言·面试
xifangge20252 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
隐擎fox2 小时前
深入 Android 移动端风控与协议层对抗:内核级虚拟环境检测、网络堆栈特征伪装与住宅 IP 协同实战
android·网络·python·网络协议·tcp/ip
“AI国潮设计-小江”2 小时前
【AIGC实战】Python+SDXL批量生成「潮汕英歌舞冰淇淋蛋糕&红桃粿咖啡」IP,固定角色形象批量出甜品商业效果图
开发语言·人工智能·python·prompt·aigc