fix(premium): 溢价率计算改用动态匹配原则

修复 _calculate_premium_series 方法,改为动态匹配净值日期:

原问题:
- 统一使用T-1净值规则导致A股/港股/商品ETF溢价率计算错误
- 如创业板ETF用T-1净值而非当天净值,溢价率从0.76%变成0.19%

修复方案:
- 优先使用当天净值(A股/港股/商品/债券/日本QDII)
- 当天净值不存在时使用T-1净值(美股QDII/欧洲QDII/原油QDII)

验证结果:
- 11只ETF全部验证通过,与集思录数据完全一致
This commit is contained in:
2026-05-16 10:27:07 +08:00
parent 06fc62c51b
commit bed92027fc

View File

@@ -236,8 +236,13 @@ class UniversalDataFetcher:
溢价率 = (ETF收盘价 - ETF净值) / ETF净值
关键QDII基金净值T+1披露需要用T日收盘价配T-1日净值
集思录做法:价格日期配前一日净值(净值日期滞后一天)
关键:不同QDII基金净值披露规则不同
- 部分基金净值当天披露如日经ETF价格日期=净值日期
- 部分基金净值T+1披露如纳指ETF价格日期配T-1日净值
集思录做法:根据基金特性选择匹配方式
- 如果有当天净值数据,优先使用当天净值
- 如果当天净值不存在使用T-1日净值
Args:
price_df: ETF价格数据索引为日期
@@ -255,22 +260,41 @@ class UniversalDataFetcher:
if nav_index.has_duplicates:
nav_df = nav_df[~nav_df.index.duplicated(keep='last')]
# QDII净值T+1披露T日收盘价配T-1日净值
# 将净值索引后移一天,使价格日期与前一日净值对齐
# 优先尝试使用当天净值如日经ETF
same_day_dates = price_df.index.intersection(nav_df.index)
# 对于没有当天净值的日期使用T-1日净值如纳指ETF
nav_df_shifted = nav_df.copy()
nav_df_shifted.index = nav_df_shifted.index + pd.Timedelta(days=1)
shifted_dates = price_df.index.intersection(nav_df_shifted.index)
# 找到对齐后的共同日期(价格日期)
common_dates = price_df.index.intersection(nav_df_shifted.index)
# 排除已有当天净值的日期
t1_dates = shifted_dates.difference(same_day_dates)
if len(common_dates) == 0:
premium_data = {}
# 使用当天净值计算
if len(same_day_dates) > 0:
close_same = price_df.loc[same_day_dates, 'close']
nav_same = nav_df.loc[same_day_dates, 'nav']
for date in same_day_dates:
if pd.notna(close_same.loc[date]) and pd.notna(nav_same.loc[date]):
premium_data[date] = (close_same.loc[date] - nav_same.loc[date]) / nav_same.loc[date]
# 使用T-1日净值计算仅用于没有当天净值的日期
if len(t1_dates) > 0:
close_t1 = price_df.loc[t1_dates, 'close']
nav_t1 = nav_df_shifted.loc[t1_dates, 'nav']
for date in t1_dates:
if pd.notna(close_t1.loc[date]) and pd.notna(nav_t1.loc[date]):
premium_data[date] = (close_t1.loc[date] - nav_t1.loc[date]) / nav_t1.loc[date]
if len(premium_data) == 0:
return None
# 计算溢价率T日收盘价 / T-1日净值已后移到T日索引
close_prices = price_df.loc[common_dates, 'close']
nav_values = nav_df_shifted.loc[common_dates, 'nav']
premium = (close_prices - nav_values) / nav_values
# 构建Series并按日期排序
premium = pd.Series(premium_data)
premium = premium.sort_index()
premium = premium.dropna()
return premium