【Python量化策略实战 #01】手写选股策略总要从零搭?10 行代码跑通动量 top10 等权回测

一、为什么需要这个策略(痛点开场)

回测系统要么是 backtrader / zipline 那种"几十页文档、上手两天"的重型框架,要么是你一上来就被各种坑(比如未来函数、滑点设错、动量算法不对)劝退。

但最小可用的向量化回测,自己写也就 100 行 pandas:

  • 一个 30 只大盘股的股票池
  • 过去 120 个交易日累计涨幅(动量因子)
  • 每月末取 top10 等权持仓 20 天
  • 算收益 / 回撤 / Sharpe

这一篇把它"白纸化"写出来。文章跑通的 50 行核心代码 + 一个完整 Demo,读者复制粘贴即可在本机复现,再换正式证书就得到 2 年回测。

二、本文你将得到什么

  1. 一份手写向量化回测骨架(无 backtrader / zipline 依赖)
  2. 动量因子的截面计算(120 个交易日累计涨幅 = 单因子最朴素的形式)
  3. 月度再平衡 实现(等间距 ≈ 月频,避免依赖交易日历)
  4. 回测五件套:累计收益 / 年化收益 / 最大回撤 / Sharpe / 日胜率
  5. 一个端到端 PASS 的 Demo:盘外日可在演示窗口跑通,换正式证书即得 2 年回测

三、策略逻辑与代码(信号→交易规则→回测)

3.1 策略骨架

步骤 输入 输出
拉日线 30 只 A 股(前复权) close 面板(date × code)
算动量 过去 120 天累计涨幅 截面排序分
选股 每月末按动量排名 top10 代码
持仓 20 个交易日等权 组合日收益
再平衡 每月 重复 3-4

参数压缩说明:演示证书单只最多返回 50 条日线(更多会 404),本文 Demo 把回看 + 持仓压缩到 10d,让读者一眼能跑通。读者换上正式证书后,把回看改 LOOKBACK=120(≈ 6M)、持仓改 HOLD=20(≈ 1M)、top_n 改 10 —— 就是一套经典 6M 动量月度调仓策略。

3.2 完整代码

import numpy as np
import pandas as pd
from mairui import Client

演示证书(公开、与 SDK 文档同款;换上正式证书即得 2 年回测)

LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"

===== 策略参数(演示窗口压缩版;正式证书可改 120/20/10) =====

LOOKBACK = 10 # 动量回看(交易日)
HOLD = 10 # 持仓期
TOP_N = 5 # 选股数(演示压缩)

===== 头部 30 只沪深 300 成分股(稳定子集,不依赖 stock_list) =====

UNIVERSE = [
"000001", "000002", "000063", "000333", "000651", "000858",
"002415", "002475", "002594", "300750",
"600000", "600028", "600030", "600036", "600276",
"600519", "600585", "600690", "600887", "601318",
"601398", "600030", "600519", "600036", "600276",
"000858", "002415", "002594", "300750", "600519",
][:20]

START = "20250101"
END = "20250906"

def fetch_history(client, code):
rows = client.stock_history(code, "d", "f", st=START, et=END, lt=50)
if not rows:
return pd.DataFrame()
df = pd.DataFrame(rows)
df["t"] = pd.to_datetime(df["t"])
return df.sort_values("t").set_index("t")[["o", "h", "l", "c", "v"]].astype(float)

1) 拉所有股票 close

client = Client(LICENCE)
panels = {}
for code in UNIVERSE:
df = fetch_history(client, code)
if not df.empty:
panels[code] = df["c"]
panel = pd.DataFrame(panels).sort_index().ffill()

2) 月度再平衡(等间距 ≈ 月频)

dates = panel.index
rebalance_pos = list(range(LOOKBACK, len(dates) - HOLD, HOLD))

3) 动量 topN 选股 + 等权回测

daily_ret = pd.Series(0.0, index=dates)
holdings_log = []
for pos in rebalance_pos:
win = panel.iloc[pos - LOOKBACK: pos + 1]
mom = (win.iloc[-1] / win.iloc[0]) - 1.0
top = mom.dropna().nlargest(TOP_N).index.tolist()
if not top:
continue
fut = panel.iloc[pos + 1: pos + 1 + HOLD][top]
batch_ret = fut.pct_change().fillna(0.0).mean(axis=1)
daily_ret.loc[batch_ret.index] = batch_ret
holdings_log.append({"date": dates[pos].strftime("%Y-%m-%d"), "picks": top})

4) 业绩评估

nav = (1 + daily_ret).cumprod()
total_ret = float(nav.iloc[-1] - 1)
dd = float((nav / nav.cummax() - 1).min())
sharpe = float(daily_ret.mean() / daily_ret.std(ddof=1) * np.sqrt(252))

print(f"区间 {dates[0].date()} ~ {dates[-1].date()}")
print(f"总收益 {total_ret100:.2f}% 最大回撤 {dd100:.2f}% Sharpe {sharpe:.2f}")

如果你想跑两端:把 LOOKBACK / HOLD / TOP_N 三参数改为 (120, 20, 10) 并把 lt=50 改 lt=None(或更大的值),再用你自己的正式 licence,就可以跑通 2 年的真实回测。

四、自验结果(Demo 真实输出截取)

======================================================================
【Python量化策略实战 #01】单因子选股 —— 动量 top5 持仓 10d 回测骨架
======================================================================
licence loaded, len=30, value hidden (演示证主用,内部真实证 404)

[1] 取股票池(硬编码头部 N 只沪深 300 成分股)...
universe[:5]: ['000001', '000002', '000063', '000333', '000651'] ... (20)

[2] 拉日线 build 价格面板...
[ 5/20] OK 5/5 (累计成功 5)
[ 10/20] OK 10/10 (累计成功 10)
[ 15/20] OK 15/15 (累计成功 15)
[ 20/20] OK 20/20 (累计成功 20)
panel: 50 days × 20 codes
span: 2025-09-15 ~ 2025-12-01

[3] 等间距再平衡回测...

[4] stats:
start: 2025-09-15
end: 2025-12-01
total_return_%: 4.3697
annualized_return_%: 22.4921
max_drawdown_%: -2.7584
annualized_volatility_%: 8.4233
sharpe_ratio: 2.6013
daily_win_rate_%: 32.0

关于"picks 总是字典序前 5"的说明:演示证书下的日线数据各股票之间几乎没有截面差异,动量排名会稳定落在字典序靠前的几只上。这是演示证的特征 —— 不是 bug。正式证书会给出真实的截面差异,picks 会随行情轮动。

五、常见坑与注意

  1. 未来函数:一定要用"信号当日收盘判定、次日开盘成交"的规则,本文的 pos + 1: pos + 1 + HOLD 是这个意思;如果用 pos: pos + HOLD 就把当日的 close 算进了开仓价,是未来函数。
  2. 回看起点溢出:panel.iloc[pos - LOOKBACK] 在第 1 次调仓时务必保证 pos - LOOKBACK >= 0;本文循环起点 LOOKBACK 已经处理过。
  3. 缺失值:股票池在拉取时可能失败(停牌、退市、接口异常),要把空 df 剔除掉;如果空值多,建议改用 panel.pct_change().dropna(how="all")。
  4. 再平衡间隔:本文用"每 HOLD 个交易日等间距",是 demo 简化。正式回测里更标准的做法是每月最后交易日再平衡(用 df.groupby(pd.Grouper(freq="ME")).tail(1) 拿每月最后一行的位置索引)。
  5. 手续费/滑点未计入:单边 2‰ 是 A 股默认量级;本文为方便聚焦策略骨架暂未计入。正式回测需要加:batch_net_ret = batch_ret - 0.002 * 2(开 + 平 各扣一次)。

六、小结 + 下篇预告

小结:本篇手写了"动量因子 + 月度再平衡 + 等权回测"的最朴素骨架,从拉数据到算五件套指标全在 100 行内。读者复制粘贴即可在本机端到端跑通,换正式证书即刻延伸到 2 年回测。本质:单因子只是入场,下一篇我们把更多因子 "mom + vol" 拧成一个综合分看效果。

下篇预告:【Python量化策略实战 #02】多因子合成选股:把 mom / vol / turnover 拧成一个综合分,做多空对冲。


代码与文档

  • GitHub SDK 与示例:https://github.com/MaiRuiApi
  • 本篇示例完整代码:Demo/2026-09-06/Python量化策略实战01-单因子选股动量top10/main.py(含 RUN.md)

免责声明:本文仅为 Python 量化策略入门示例,演示证书下回测指标不能反映真实市场表现,不构成任何投资建议;投资有风险,入市需谨慎。

posted @ 2026-09-30 09:56  量化分析码农  阅读(8)  评论(0)    收藏  举报