fix(datasource): 修正数据日期对齐与复权问题

- 修改yfinance获取历史数据时end_date加一天,auto_adjust设置为False,以获取不复权价格
- 调整ETF净值数据获取时end_date加一天,解决净值数据滞后问题
- 数据对齐策略改为以A股最新数据日期为基准,调整交易日历范围
- 移除对非A股数据的前向/后向填充,保持原始价格数据不填充
- ETF净值数据重新索引到A股交易日但不做缺失值填充,保持NaN以表示无数据
- 增加打印输出辅助调试数据日期及交易日信息
This commit is contained in:
2026-03-26 01:26:43 +08:00
parent ec9c808e6c
commit 5f4470d53e

View File

@@ -338,7 +338,11 @@ class HybridDataSource:
try:
ticker = yf.Ticker(yf_code)
data = ticker.history(start=start_date, end=end_date)
# auto_adjust=False 获取不复权价格,与网页显示一致
# end_date 需要加一天,因为 yfinance 的 end 是排他的
from datetime import timedelta
end_date_obj = pd.Timestamp(end_date) + timedelta(days=1)
data = ticker.history(start=start_date, end=end_date_obj.strftime('%Y-%m-%d'), auto_adjust=False)
if len(data) == 0:
return None
@@ -585,8 +589,10 @@ class HybridDataSource:
# 获取ETF价格数据
price_data = self._fetch_etf(etf_code, start_date, end_date)
# 获取ETF净值数据
nav_data = self._fetch_etf_nav(etf_code, start_date, end_date)
# 获取ETF净值数据净值通常滞后一天所以end_date+1
from datetime import timedelta
nav_end_date = (pd.Timestamp(end_date) + timedelta(days=1)).strftime('%Y-%m-%d')
nav_data = self._fetch_etf_nav(etf_code, start_date, nav_end_date)
if price_data is not None and len(price_data) > 0:
# 使用指数代码作为列名,保持与指数数据一致
@@ -633,42 +639,38 @@ class HybridDataSource:
aggfunc='first'
)
# 以A股交易日为基准对齐所有数据
# 强制从 Tushare 获取A股交易日历不管配置中是否有A股指数
start_str = index_data.index.min().strftime('%Y%m%d')
end_str = index_data.index.max().strftime('%Y%m%d')
# 数据对齐策略:使用各标的能获取到的最新数据
# 以A股最新数据日期为基准其他市场数据对齐到该日期
# 获取A股最新数据日期
china_codes = [c for c in valid_codes if self._is_china_index(c)]
if china_codes:
a_share_latest = index_data[china_codes].dropna().index.max()
else:
# 如果没有A股使用所有数据的最早最新日期
a_share_latest = index_data.dropna().index.max()
print(f" A股最新数据日期: {a_share_latest.strftime('%Y-%m-%d')}")
# 获取A股交易日历从start_date到a_share_latest
start_str = pd.Timestamp(start_date).strftime('%Y%m%d')
end_str = a_share_latest.strftime('%Y%m%d')
import tushare as ts
pro = ts.pro_api(self._get_tushare_token())
trade_cal = pro.trade_cal(start_date=start_str, end_date=end_str, is_open='1')
a_share_dates = pd.to_datetime(trade_cal['cal_date']).sort_values()
print(f" A股交易日历: {len(a_share_dates)} 天 ({start_str} ~ {end_str})")
# 重新索引到A股交易日
# 重新索引到A股交易日只到A股最新数据日期
index_data = index_data.reindex(a_share_dates)
# 对非A股指数进行数据对齐
# 港股、美股T日收盘T+1日09:00前使用T日数据前向填充
# 加密货币、期货含夜盘T+1日09:00前使用T+1日数据后向填充
# - 加密货币UTC 00:00收盘北京时间08:00
# - 期货AU.SHF夜盘02:30收盘数据标记为T+1日
# 策略:价格数据保持原始值,不做填充
# 指标计算后会和价格作为一个整体进行向前填充
non_a_codes = [c for c in valid_codes if not self._is_china_index(c)]
yf_codes = [c for c in non_a_codes if not self._is_crypto(c) and not self._is_futures(c)]
crypto_futures_codes = [c for c in non_a_codes if self._is_crypto(c) or self._is_futures(c)]
# 港股/美股前向填充使用T日数据
for code in yf_codes:
if code in index_data.columns:
index_data[code] = index_data[code].ffill().bfill()
# 加密货币/期货后向填充使用T+1日数据
for code in crypto_futures_codes:
if code in index_data.columns:
index_data[code] = index_data[code].bfill().ffill()
if non_a_codes:
print(f" 非A股标的: {len(non_a_codes)} 只 (港股/美股:ffill, 加密货币/期货:bfill)")
print(f" 非A股标的: {len(non_a_codes)} 只 (价格保持原始值)")
print(f" 时间范围: {index_data.index[0]} ~ {index_data.index[-1]}")
print(f" 交易日数: {len(index_data)}")
@@ -717,9 +719,9 @@ class HybridDataSource:
aggfunc='first'
)
# 对齐到A股交易日并前向填充缺失值(净值数据通常T+1更新
# 对齐到A股交易日但不填充缺失值(保持原始数据,让报告层决定是否显示溢价率
etf_nav_data = etf_nav_data.reindex(a_share_dates)
etf_nav_data = etf_nav_data.ffill() # 前向填充缺失的净值数据
# 注意不做ffill填充保持NaN表示当天无净值数据
print(f" ETF净值数据: {len(etf_nav_data.columns)}")