【Python量化数据工程实战 #05】多源数据怎么合?行情+基本面+公告的统一时间轴

你回测策略的时候,是不是经常遇到这种尴尬:

  • K线里明明涨得好好的,一查当天发布了业绩预减公告——你根本没把公告算进去。
  • 想做个"季报超预期"策略,但季度利润表的日期和日线对不上,不知道该填到哪一天。
  • 行情、财务、公告三个数据源各拉各的,最后靠 Excel 手动 VLOOKUP,一改数据源全崩。

这篇文章给你一套多源合并的标准做法:用 pandas 的 merge_asof 把不同频率、不同来源的数据,严丝合缝地对到同一天。读完你能直接跑通一段代码,把行情、季度利润、公司公告合成一张宽表。

本文你将得到什么

  1. 日频行情 + 季频利润 + 事件型公告的三层合并方案
  2. merge_asof 前向填充的正确用法(90% 的人第一次都用错方向)
  3. 一个可直接运行的完整脚本,输出统一时间轴的 DataFrame
  4. 多源合并的 3 个常见坑:时区、非交易日、重复公告

场景:三只数据源,三种节奏

做量化策略时,你面对的数据天然是"不同步"的:

数据源 频率 时间戳 示例
日线行情 日频 每个交易日 开盘、收盘、成交量
季度利润 季频 季度末 营业收入、净利润
公司公告 事件型 公告发布日 业绩预告、股东大会

核心问题:季报的截止日期是 9 月 30 日,但实际披露可能在 10 月 25 日;公告发布日可能是周末(非交易日)。如何把这三张表合到同一个交易日时间轴上?

代码实战:三源合一

下面这段代码演示完整的合并流程。证书使用官方演示证书,你可以直接复制运行:

from mairui import Client
import pandas as pd

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")
code = "600519"

1) 日线行情(日频)

raw_hist = client.stock_history(code, lt=60, dividend='n')
df_hist = pd.DataFrame(raw_hist)
df_hist['t'] = pd.to_datetime(df_hist['t'])
df_hist = df_hist.sort_values('t')

2) 季度利润(季频)

raw_profit = client.company_quarter_profit(code)
df_profit = pd.DataFrame(raw_profit)
df_profit['date'] = pd.to_datetime(df_profit['date'])
df_profit = df_profit.sort_values('date')

3) 公司公告(事件型)

raw_ann = client.stock_announcement(code, lt=20)
df_ann = pd.DataFrame(raw_ann)
df_ann['t'] = pd.to_datetime(df_ann['t'])

=== 第一层合并:季报 → 日线(前向填充) ===

策略逻辑:用"最近一期已披露财报"填充每个交易日

df_merged = pd.merge_asof(
df_hist[['t', 'o', 'c', 'h', 'l', 'v']],
df_profit[['date', 'profit', 'income']].rename(columns={'date': 't'}),
on='t',
direction='backward' # 关键:backward = 用最近的过去值填充
)

=== 第二层合并:公告 → 日线(当日计数) ===

注意:这里必须用 merge,不能用 merge_asof!

df_ann_daily = df_ann.groupby('t').size().reset_index(name='ann_count')
df_merged = pd.merge(df_merged, df_ann_daily, on='t', how='left')
df_merged['ann_count'] = df_merged['ann_count'].fillna(0).astype(int)

输出结果

print(df_merged.tail(10)[['t', 'c', 'profit', 'income', 'ann_count']])

运行后你会看到类似这样的输出:

t c profit income ann_count
2025-11-18 11.59 4,609,700.00 10,066,800.00 0
2025-11-19 11.80 4,609,700.00 10,066,800.00 0
2025-11-24 11.60 4,609,700.00 10,066,800.00 2
2025-12-01 11.69 4,609,700.00 10,066,800.00 5

每一行是一个交易日,"profit" 和 "income" 是该日适用的最新季度数据,"ann_count" 是当天发布的公告数量。

关键细节:为什么公告不能用 merge_asof?

季报用 merge_asof(..., direction='backward') 是正确的,因为一季度的财务数据在整季度都有效,可以前向填充。

但公告完全不同:10 月 15 日发了 3 条公告,这 3 条只属于 10 月 15 日,不能传播到 10 月 16 日。如果你误用 merge_asof,10 月 16 日也会显示 "3 条公告"——这是致命的逻辑错误。

正确做法:公告用 pd.merge(..., how='left') 严格按日匹配,无公告则填 0。

3 个常见坑

坑 1:时区不一致导致 merge 失败

有些数据源返回 UTC,有些返回本地时间。合并前统一做一次:

df['t'] = pd.to_datetime(df['t']).dt.tz_localize(None)

坑 2:公告日在非交易日(周末/节假日)

如果公告发布在周六,pd.merge 会匹配不到交易日。处理方案:

把公告日映射到下一个交易日(简化版)

trading_days = set(df_hist['t'])
df_ann['t'] = df_ann['t'].apply(
lambda d: d if d in trading_days else min([t for t in trading_days if t > d], default=d)
)

坑 3:除权除息日导致价格 discontinuity

合并后的行情如果直接做收益率计算,除权日会出现假跳空。下一篇文章会专门讲复权处理。

小结

多源合并的核心就两步:

  1. 低频 → 高频:用 merge_asof(..., direction='backward') 前向填充
  2. 事件 → 高频:用 pd.merge(..., how='left') 严格按日匹配

掌握这两个模式,行情、财务、公告、研报、宏观数据都能统一到一个时间轴上,策略的数据底座才算真正搭好。

下篇预告:行情合并完了,但除权除息日价格断崖怎么办?前复权和后复权到底该用哪个?#06 带你手动算一遍复权因子,搞懂回测数据准备的最佳实践。


免责声明

本文仅供技术学习交流,不构成任何投资建议。市场有风险,决策需谨慎。

代码与文档

更多接口文档和 SDK 使用示例,可查阅官方文档:https://github.com/MaiRuiApi/mairui

posted @ 2026-09-22 09:27  量化分析码农  阅读(9)  评论(0)    收藏  举报