【Python量化选股流水线 #03】候选股怎么排序?动量+低波+流动性三因子打分取 Top-N

过滤管道解决了「哪些票合格」,但合格票之间怎么排座次?「都行」等于没有结论------下游建仓、盯盘都需要一个明确的先后顺序。这一篇给流水线出口装一个三因子打分器:动量、低波、流动性,全部只用日线数据现算,z-score 标准化后加权合成,输出 Top-N 候选清单。

一、打分器在流水线里的位置

复制代码
股票池 → 过滤管道(#02) → 打分排序(本篇) → Top-N 候选清单

过滤是「淘汰制」,打分是「排位赛」。两者分开的好处:过滤条件回答硬性门槛(趋势不在、流动性太差直接出局),打分回答「剩下的里面谁更优先」。合成公式本身可以很简单,关键是三件事:因子怎么算、量纲怎么统一、权重怎么给。

本文你将得到:

  1. 三个纯日线因子的参考实现:20 日动量、20 日低波(负波动率)、20 日均成交额;
  2. 截面 z-score 标准化 + 加权合成的完整代码;
  3. 一份实测跑通的 12 标的打分表与 Top-5 输出。

二、准备环境

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

POOL = {
    "600519": "贵州茅台", "000858": "五粮液",
    "601398": "工商银行", "600036": "招商银行",
    "300750": "宁德时代", "002594": "比亚迪",
    "600276": "恒瑞医药", "603259": "药明康德",
    "600030": "中信证券", "601688": "华泰证券",
    "000333": "美的集团", "600900": "长江电力",
}

WEIGHTS = {"动量": 0.4, "低波": 0.3, "流动性": 0.3}  # 权重和 = 1


def perturb(bars, seed):
    # 演示证书返回固定样例数据;确定性 ±5% 价格、±20% 成交量扰动模拟行情起伏。
    # 换成正式证书后删掉本函数调用即可,其余逻辑零改动。
    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):
    """三个因子,全部只用日线 OHLCV 计算。"""
    c, v = df["c"], df["v"]
    mom = c.iloc[-1] / c.iloc[-21] - 1                      # 20日动量:涨得多
    lowvol = -c.pct_change().tail(20).std()                 # 低波动:波动小(取负)
    liq = (v * c).tail(20).mean()                           # 流动性:20日均成交额
    return {"动量": mom, "低波": lowvol, "流动性": liq}


def zscore(vals):
    """截面 z-score 标准化:((x - 均值) / 标准差)。"""
    a = np.asarray(vals, dtype=float)
    sd = a.std()
    return (a - a.mean()) / sd if sd > 0 else np.zeros_like(a)


def main(out_csv="top_candidates.csv"):
    rows = []
    for code, name in POOL.items():
        df = to_frame(fetch_bars(code))
        f = factor_scores(df)
        rows.append({"代码": code, "名称": name, **f})
    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())
    table = table.sort_values("综合分", ascending=False).reset_index(drop=True)

    show = table[["代码", "名称", "动量", "低波", "流动性", "综合分"]].copy()
    show["动量"] = (show["动量"] * 100).round(2).astype(str) + "%"
    show["低波"] = show["低波"].round(5)
    show["流动性"] = (show["流动性"] / 1e6).round(2).astype(str) + "M"
    show["综合分"] = show["综合分"].round(3)
    print(show.to_string(index=False))

    top5 = table.head(5)[["代码", "名称", "综合分"]].copy()
    top5["综合分"] = top5["综合分"].round(3)
    top5.to_csv(out_csv, index=False, encoding="utf-8-sig")
    print(f"Top-5 候选: {'、'.join(top5['名称'] + '(' + top5['代码'] + ')')}")
    print(f"流水线产出: {out_csv}")


if __name__ == "__main__":
    main()

四、真实运行结果

在 Python 3.9 环境实测运行,控制台输出:

text 复制代码
    代码   名称     动量       低波    流动性    综合分
600519 贵州茅台  6.64% -0.03257 11.79M  1.198
002594  比亚迪  5.08% -0.04449 12.12M  0.793
000858  五粮液  6.56% -0.04389 11.83M  0.698
600030 中信证券  5.25% -0.03956 11.75M  0.645
601688 华泰证券 -1.21% -0.03653 12.03M  0.238
600036 招商银行  0.36% -0.04450 11.85M -0.117
603259 药明康德  3.33% -0.04574 11.44M -0.237
600276 恒瑞医药  0.43% -0.04498 11.75M -0.240
601398 工商银行 -0.89% -0.02954 11.21M -0.287
600900 长江电力  0.81% -0.04324 11.34M -0.553
000333 美的集团 -1.04% -0.05059 11.64M -0.819
300750 宁德时代  -1.6% -0.05243 11.32M -1.319
Top-5 候选: 贵州茅台(600519)、比亚迪(002594)、五粮液(000858)、中信证券(600030)、华泰证券(601688)
流水线产出: top_candidates.csv

两个值得看的点:① 贵州茅台动量最强(6.64%)且波动最低(-0.03257),双优拿下第一,综合分 1.198 远超第二名,打分器把「强而且稳」表达得很清楚;② 华泰证券动量是负的,却靠流动性冲进 Top-5------这正是打分器的价值,它把「各维度各有所长」的票放在一起权衡,而不是单一指标一刀切。

说明:演示证书返回固定样例数据,代码用确定性扰动模拟行情起伏;换成你自己的正式证书后删掉 perturb 调用即为真实行情,打分逻辑零改动。

五、三个必须懂的实现细节

  1. z-score 必须是「截面」的:标准化在同一时刻、同一批标的之间进行(对 12 只票的动量值求均值方差),不是对单只票的时间序列标准化。量纲不同的因子(百分比、波动率、成交额)只有经过截面标准化才能加权相加。
  2. 低波因子取负号:波动率越小越好,但 z-score 合成默认「越大分越高」,所以取负后参与合成,方向就统一了。
  3. 权重不是玄学也不是真理:0.4/0.3/0.3 只是演示起点,权重怎么定属于策略研究问题------严谨做法是用历史数据回看验证(下下篇之前,本系列 #04 先解决「怎么验收」)。

六、小结与下篇预告

至此流水线有了完整的「进数据 → 过滤 → 排序 → 出清单」能力。但打分器好不好用,不能自己说了算------得拿昨天的候选看今天的表现。

下篇预告:《【Python量化选股流水线 #04】选股器好不好用?「昨日候选→今日表现」回看验收》------用历史数据给你的选股器出一份体检报告。

免责声明

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

代码与文档

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

相关推荐
骑士雄师1 小时前
supervisor 中关于上下文多轮信息维护常用到的python语法
开发语言·python
青少儿编程课堂1 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C/C++考级:任意进制互转与大数多进制加法解析
c++·python·算法·bfs·信息学竞赛
曹牧1 小时前
C#:字符串占位符与值数量不一致
开发语言·数据库·c#
Orchestrator_me1 小时前
k8s网络与应用
网络·python·kubernetes
迅猛龙办公室1 小时前
Python函数实现反转一个只有3位数的整数
开发语言·前端·python
我星期八休息1 小时前
Redis 的原理和使用(2)
前端·数据库·redis·python·mysql·缓存·bootstrap
“AI国潮设计-小江”1 小时前
《【AIGC实战】Python+SDXL批量生成“英歌舞树莓香草布丁”IP,附保持角色一致性的代码逻辑》
开发语言·人工智能·python·prompt·aigc
闻缺陷则喜何志丹1 小时前
【C++特性及类库】多线程
开发语言·c++·多线程·语言特性·系统库
溪语流沙2 小时前
【Web全栈进阶】文件上传与图片展示:早报站的第一张图片
前端·vue.js·python·fastapi