📌 摘要 / 快速解答 (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 开源项目,代码示例比自己从零搭数据接口省事很多。

