过滤管道解决了「哪些票合格」,但合格票之间怎么排座次?「都行」等于没有结论------下游建仓、盯盘都需要一个明确的先后顺序。这一篇给流水线出口装一个三因子打分器:动量、低波、流动性,全部只用日线数据现算,z-score 标准化后加权合成,输出 Top-N 候选清单。
一、打分器在流水线里的位置
股票池 → 过滤管道(#02) → 打分排序(本篇) → Top-N 候选清单
过滤是「淘汰制」,打分是「排位赛」。两者分开的好处:过滤条件回答硬性门槛(趋势不在、流动性太差直接出局),打分回答「剩下的里面谁更优先」。合成公式本身可以很简单,关键是三件事:因子怎么算、量纲怎么统一、权重怎么给。
本文你将得到:
- 三个纯日线因子的参考实现:20 日动量、20 日低波(负波动率)、20 日均成交额;
- 截面 z-score 标准化 + 加权合成的完整代码;
- 一份实测跑通的 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调用即为真实行情,打分逻辑零改动。
五、三个必须懂的实现细节
- z-score 必须是「截面」的:标准化在同一时刻、同一批标的之间进行(对 12 只票的动量值求均值方差),不是对单只票的时间序列标准化。量纲不同的因子(百分比、波动率、成交额)只有经过截面标准化才能加权相加。
- 低波因子取负号:波动率越小越好,但 z-score 合成默认「越大分越高」,所以取负后参与合成,方向就统一了。
- 权重不是玄学也不是真理:0.4/0.3/0.3 只是演示起点,权重怎么定属于策略研究问题------严谨做法是用历史数据回看验证(下下篇之前,本系列 #04 先解决「怎么验收」)。
六、小结与下篇预告
至此流水线有了完整的「进数据 → 过滤 → 排序 → 出清单」能力。但打分器好不好用,不能自己说了算------得拿昨天的候选看今天的表现。
下篇预告:《【Python量化选股流水线 #04】选股器好不好用?「昨日候选→今日表现」回看验收》------用历史数据给你的选股器出一份体检报告。
免责声明
本文所有代码与数据仅用于技术学习与演示,不构成任何投资建议。股市有风险,投资需谨慎。
代码与文档
SDK 与更多语言版本:https://github.com/MaiRuiApi