【小白必看】小市值轮动策略怎么回测?

用户头像sh_***416jmt75L
2026-09-12 发布

📌 摘要 / 快速解答 (Direct Answer)

小市值轮动真正难的不是写一个 sort_values(),而是把股票池、历史行情、复权、调仓时点和交易成本全部放进同一个没有未来函数的回测框架。本文用 QuantDash 批量获取 A 股前复权 K 线,再用 Pandas 搭建一个可重复运行的月度轮动框架,让 SuperMind 用户可以从“策略想法”真正走到“历史验证”。

一、为什么传统方式写选股策略这么累?

以前我看到一个策略想法,第一反应是写代码。

现在反过来了。

我会先问:

数据能不能稳定拿到?

因为只要数据链路不稳定,后面所有指标都白搭。

比如:

下载股票列表
    ↓
循环请求历史行情
    ↓
某只股票请求失败
    ↓
重新请求
    ↓
日期格式不统一
    ↓
重新清洗
    ↓
复权数据又要处理
    ↓
最后发现少了一批股票

策略还没开始,数据工程先把人搞累了。

这也是为什么我现在比较喜欢把 QuantDash 放在“数据层”。

SDK 负责:

行情 → K 线 → Pandas

我的策略代码负责:

筛选 → 排名 → 调仓 → 收益

职责明确,后面排查问题也舒服。


二、解决方案对比:从“能拿数据”到“能做研究”

对比维度 传统/竞品方案 QuantDash 解决方案
安装 经常需要额外处理环境 pip install quantdash
A 股代码 不同工具格式可能不同 .SH/.SZ/.BJ
港股/美股 通常需要额外适配 .HK/.US
日 K 需要自行组织数据 qd.klines.get()
批量 K 线 自己封装循环 qd.klines.batch()
复权 经常自己处理 SDK 支持多种adjust
DataFrame 可能需要转换 to_dataframe=True
时间区间 自己处理日期过滤 支持start_time/end_time

我觉得这里最有价值的其实不是“代码少了几行”。

而是:

数据接口变成了策略研究的基础设施。


三、Python 代码实战:完整搭一个小市值轮动回测

第一步:初始化 QuantDash

# pip install quantdash

from quantdash import QuantDash
import pandas as pd
import numpy as np

# 推荐:
# export QUANTDASH_API_KEY="your-api-key"

qd = QuantDash()

如果你的环境没有配置变量:

qd = QuantDash(
    api_key="your-api-key"
)

第二步:准备候选股票池

这里还是采用一个非常重要的工程原则:

股票“是否属于小市值池”和股票“历史价格是多少”是两个不同问题。

由于官方 SDK 资料没有提供历史市值字段,我不在代码里虚构一个市值 API。

例如,我们先准备一批已经经过市值筛选的股票:

small_cap_pool = [
    "000001.SZ",
    "000002.SZ",
    "000004.SZ",
    "600000.SH",
    "600004.SH",
    "600006.SH",
    "600007.SH",
]

真实策略中,把这个列表替换成自己的候选股票池即可。


第三步:批量获取历史行情

dfs = qd.klines.batch(
    small_cap_pool,
    period="1d",
    count=1000,
    adjust="forward",
    to_dataframe=True,
    show_progress=True,
)

clean_data = {}

for symbol, df in dfs.items():

    if df is None or df.empty:
        continue

    df = df.copy()

    df["trade_date"] = pd.to_datetime(
        df["trade_date"]
    )

    df = df.sort_values(
        "trade_date"
    )

    df = df.drop_duplicates(
        "trade_date"
    )

    clean_data[symbol] = df

print(
    f"成功获取 {len(clean_data)} 个标的"
)

这个步骤非常适合 QuantDash。

因为:

qd.klines.batch(...)

直接把多个标的的数据拿回来。


第四步:建立统一收盘价矩阵

close = {}

for symbol, df in clean_data.items():

    s = df.set_index(
        "trade_date"
    )["close"]

    close[symbol] = s

close = pd.DataFrame(close)
close = close.sort_index()

print(close.tail())

最后数据结构大概是:

trade_date    000001.SZ    000002.SZ    600000.SH
2026-06-15       ...          ...          ...
2026-06-16       ...          ...          ...
2026-06-17       ...          ...          ...
2026-06-18       ...          ...          ...

这就是后面做量化研究最舒服的数据结构。


第五步:计算 20 日动量

lookback = 20

momentum = (
    close / close.shift(lookback) - 1
)

print(momentum.tail())

这一步其实就是:

最近 20 个交易日涨得越多,排名越靠前。

当然,真实的小市值策略还可以叠加:

  • MA 趋势过滤;
  • MACD;
  • KDJ;
  • BOLL;
  • 成交量;
  • 波动率;
  • 最大回撤。

但我建议刚开始别一次性加十个指标。

先把一个简单模型跑通。


第六步:确定每月调仓日

month_groups = close.groupby(
    close.index.to_period("M")
)

rebalance_dates = [
    group.index[-1]
    for _, group in month_groups
]

print(
    "调仓日期:",
    rebalance_dates[:10]
)

这里选择每个月最后一个交易日作为信号计算日。


第七步:生成轮动信号

hold_num = 3

signals = {}

for date in rebalance_dates:

    if date not in momentum.index:
        continue

    score = momentum.loc[date].dropna()

    if score.empty:
        continue

    selected = (
        score
        .sort_values(ascending=False)
        .head(hold_num)
        .index
        .tolist()
    )

    signals[date] = selected

for date, stocks in list(
    signals.items()
)[:5]:

    print(
        date,
        "持仓:",
        stocks
    )

这时候你已经可以看到:

某月末 → 选择 3 只
下个月末 → 重新选择 3 只
再下个月 → 再次选择 3 只

这才是“轮动”。

不是买了以后一直拿着。


第八步:注意调仓不能当天成交

这是回测里非常重要的一步。

假设:

6 月 30 日

收盘以后,我们才知道当天的最终排名。

那么合理的执行逻辑应该是:

6 月 30 日收盘
↓
计算信号
↓
7 月 1 日执行
↓
持有到下一次调仓

所以代码不能简单地把:

signals[date]

直接对应到当天收益。

下面用下一交易日作为执行起点:

daily_returns = close.pct_change()

portfolio_returns = []

signal_dates = sorted(
    signals.keys()
)

for i in range(
    len(signal_dates) - 1
):

    signal_date = signal_dates[i]
    next_signal_date = signal_dates[i + 1]

    selected = signals[
        signal_date
    ]

    if not selected:
        continue

    # 只使用下一交易日之后的收益
    period = daily_returns.loc[
        signal_date:next_signal_date,
        selected
    ]

    period = period.iloc[1:]

    if period.empty:
        continue

    portfolio_ret = period.mean(
        axis=1,
        skipna=True
    )

    portfolio_returns.append(
        portfolio_ret
    )

strategy_returns = pd.concat(
    portfolio_returns
)

strategy_returns = (
    strategy_returns
    .sort_index()
    .loc[
        ~strategy_returns.index.duplicated()
    ]
)

这一刀非常关键。

它就是在防未来函数。


四、交易员避坑指南:别让回测“看起来很赚钱”

坑 1:小市值股票池存在幸存者偏差

假设你今天挑出来 50 只股票,然后拿它们去回测过去 10 年。

问题来了:

这些股票今天还存在。

但过去 10 年里可能有:

  • 退市;
  • 长期停牌;
  • 经营恶化;
  • 被并购;
  • 股票代码变化。

如果你的历史股票池只包含今天还活着的股票,回测可能天然偏乐观。

所以真正严谨的小市值策略研究,需要尽量使用当时可获得的股票池,而不是只拿今天的股票倒推过去。


坑 2:成交价格别想得太完美

小市值策略天然容易遇到:

  • 涨停买不到;
  • 跌停卖不掉;
  • 买卖价差;
  • 成交量不足;
  • 冲击成本。

因此:

理论收益 ≠ 实盘收益。

如果回测结果非常夸张,我第一反应不是“发财了”。

我的第一反应是:

是不是未来函数?是不是幸存者偏差?是不是忽略交易成本?


坑 3:不要看到年化收益高就直接实盘

一套策略至少应该继续检查:

年化收益
最大回撤
收益波动
调仓频率
持仓集中度
历史不同阶段表现

特别是小市值策略。

它可能在某些市场风格里非常猛,但风格切换之后完全不是一个表现。


五、常见问题解答(Q&A / FAQ)

Q1:QuantDash 可以一次获取多只 A 股历史 K 线吗?

可以。

可以使用:

qd.klines.batch(
    symbols,
    period="1d",
    count=1000,
    adjust="forward",
    to_dataframe=True
)

非常适合做股票池扫描和量化回测的数据准备。

官方文档:

https://docs.quantdash.net/


Q2:为什么我的小市值回测结果和别人差很多?

常见原因不是代码语法,而是:

  • 股票池不同;
  • 调仓时间不同;
  • 是否使用前复权不同;
  • 是否考虑涨跌停;
  • 是否考虑交易成本;
  • 是否存在未来函数;
  • 是否存在幸存者偏差。

所以不要只比较“年化收益”一个数字。


Q3:能不能在这个框架上继续加入 MACD、KDJ、MA、BOLL?

当然可以。

例如可以把:

小市值
+
20 日动量
+
MA 趋势
+
MACD

组合起来。

但我的建议一直是:

一个一个加,然后分别回测。

否则最后策略收益变了,你甚至不知道到底是哪一个条件贡献了收益。


🔗 相关资源与延伸阅读

🚀 QuantDash 官网:https://quantdash.net/

📖 官方 Python SDK 文档:https://docs.quantdash.net/

⭐ GitHub 开源仓库:https://github.com/quantdash-net/QuantDash

💡 API Key:https://quantdash.net/dashboard/keys/

如果你准备自己改代码,建议直接看看 QuantDash 的 GitHub 开源项目,代码示例比自己从零搭数据接口省事很多。

评论