能够自己复现研报,是我们学员的一个基本要求。在课程中我们详细介绍了 Alphalens 因子分析框架,它非常适合日线因子的快速回测。但是,对一些月线策略,Alphalens 在回测上就力有不逮。
这就是为什么我们开发了moonshot:不仅是要填补这一空白,更是要确保我们的学员能将所学的知识用以实战。
上一篇我们大致介绍了 moonshot 的核心思想:你将所有的一切(月开盘价、月收盘价、因子或者交易信号)放入一个以月和资产代码为索引的 DataFrame 中,通过 moonshot 就能执行回测,并输出报告。
现在,我们就从最基础的获取数据开始,一步步带大家掌握月线策略的回测。
数据总览
『基本面量化系列14』这篇研报要求的数据各类比较多,加上一些预处理步骤,所以整个工程量并不小。以此为例,也正好演示通过 Moonshot 框架,如何把复杂的项目条理化、简单化。
这是策略的数据清单:
- 行情数据。任何策略都默认需要它,至少会在计算远期收益时使用。
- 股息率,用来按股息率筛选个股,以及计算两年股息率均值因子。
- 分红数据。只有过去两年连续分红的公司才能入选。
- 审计意见。只有过去十年没有审计保留意见的公司才能入选。
- 市值数据。只有市值大于50亿的公司才能入选。
- 净利润、营业收入和营业利润数据,用来计算净利润稳定性因子。
- 股东数量变化
- 换手率。用来计算换手波动率。
- pe_ttm,用来计算 ep 因子。
- 经营现金流数据,用来计算经营现金流资产比因子。
- 资产总计数据,与10一起,用来计算经营现金流资产比因子。
- 盈余公积金数据。与11一起,用来计算留存收益资产比因子。
我们将一一介绍这些数据是什么意思,有何作用,从何处可以获取到。因此,跟随这个系列,你将获取比较完整的基本面月度调仓策略构建经验。
在这一期中,我们将介绍如何获得行情数据(tushare),并对数据进行复权处理。
日线行情数据及复权
我们需要获得回测区间所有个股的行情数据,这样通过重采样之后,就能用月初开盘价、月末收盘价计算出个股的月收益率。此外,我们还要介绍如何高效实现复权。

在 tushare 中,我们可以用 daily 或者 pro_bar 来获取行情数据。两者的区别是,在实现上,pro_bar 是一个集成接口,它在内部会(根据需要)调用 daily, adj_factor, daily_basic 等方法来获取数据,再进行合并对齐。
这里我们推荐掌握 daily 和 adj_factor 等基础 API 的用法。原因是,通过这两个 API,我们可以将历史数据存储到本地,然后以追加的方式完成行情数据的更新。一旦历史数据得到缓存,此后的更新就会比较快。这是 pro_bar 方法难以做到的。
下面是获取日线行情数据的方法,注意返回的数据中,已附带了复权因子。随后我们可以根据自己的需要,对任何时间段,自由进行前后复权。
def fetch_bars(start: datetime.date, end: datetime.date) -> pd.DataFrame | None:
"""通过 tushare 接口,获取日线行情数据
返回数据未复权,但包含了复权因子,因此可以增量获取叠加。返回数据为升序。
Args:
start: 开始日期
end: 结束日期
Returns:
DataFrame: 包含date, asset, open,high,low,close,volume,amount,adj_factor
"""
all_data = []
pro = pro_api()
for date in pd.bdate_range(start, end):
try:
str_date = date.strftime("%Y%m%d")
df = pro.daily(trade_date=str_date)
if df.empty:
continue
try:
adj_factor = pro.adj_factor(ts_code="", trade_date=str_date)
if adj_factor.empty:
continue
except Exception:
continue
df = pd.merge(df, adj_factor, on=["ts_code", "trade_date"], how="inner")
# 重命名列并转换数据类型
df = df.rename(
columns={
"trade_date": "date", "vol": "volume", "ts_code": "asset"}
)
# tushare返回的是字符串格式的日期,如'20231229'
df["date"] = pd.to_datetime(df["date"], format="%Y%m%d")
all_data.append(df)
except Exception as e:
print(f"Error loading data for {
date}: {
e}")
continue
if not all_data:
return None
# 合并所有数据。由获取数据逻辑知此时数据已为有序
result = pd.concat(all_data, ignore_index=True)
result = result[
[
"date",
"asset"



被折叠的 条评论
为什么被折叠?



