feat: fetch_etf_with_nav 返回历史溢价率序列

修改内容:
1. universal_fetcher.py
   - fetch_etf_with_nav 返回三值:(price_df, nav_df, premium_series)
   - 新增 _calculate_premium_series 方法:计算每一天的溢价率
   - 溢价率 = (ETF收盘价 - ETF净值) / ETF净值
   - 净值用ffill对齐价格日期(处理T+1延迟)

2. flask_server.py
   - /api/v1/etf/nav 端点返回历史溢价率序列
   - 添加 premium_series 字段:[{date, premium}]
   - 添加 latest_premium: 最新溢价率
   - 添加 premium_stats: 统计数据(mean/std/min/max/median)

测试结果(513100.SH 纳指100 ETF):
- 价格数据: 8条
- 净值数据: 8条
- 溢价率序列: 8条
- 最新溢价率: 0.1500%
- 溢价率均值: 1.1433%
- 溢价率范围: 0.15% ~ 1.69%
This commit is contained in:
2026-05-12 21:39:07 +08:00
parent 4e3aac5e0e
commit 16affb2368
2 changed files with 74 additions and 15 deletions

View File

@@ -189,21 +189,64 @@ class UniversalDataFetcher:
code: str,
start_date: str,
end_date: str
) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.Series]]:
"""
获取ETF价格 + 净值
获取ETF价格 + 净值 + 溢价率序列
用于计算溢价率
计算每一天的溢价率,用于分析溢价率走势
Args:
code: ETF代码
start_date: 开始日期
end_date: 结束日期
Returns:
(price_df, nav_df)
(price_df, nav_df, premium_series)
- price_df: ETF价格数据 (OHLCV)
- nav_df: ETF净值数据
- premium_series: 溢价率序列 (每天计算)
"""
price_df = self._tushare.fetch_etf(code, start_date, end_date)
nav_df = self._tushare.fetch_etf_nav(code, start_date, end_date)
return price_df, nav_df
# 计算历史溢价率序列
premium_series = None
if price_df is not None and nav_df is not None and len(nav_df) > 0:
premium_series = self._calculate_premium_series(price_df, nav_df)
return price_df, nav_df, premium_series
def _calculate_premium_series(
self,
price_df: pd.DataFrame,
nav_df: pd.DataFrame
) -> Optional[pd.Series]:
"""
计算历史溢价率序列
溢价率 = (ETF收盘价 - ETF净值) / ETF净值
注意净值数据通常T+1公布需要处理日期对齐问题
Args:
price_df: ETF价格数据索引为日期
nav_df: ETF净值数据索引为日期
Returns:
溢价率Series索引为日期值为溢价率
"""
# 对齐日期净值用ffill填充因为T+1公布
# 价格日期可能比净值日期多一天
aligned_nav = nav_df['nav'].reindex(price_df.index, method='ffill')
# 计算溢价率
close_prices = price_df['close']
premium = (close_prices - aligned_nav) / aligned_nav
# 过滤掉无效值(净值缺失的日期)
premium = premium.dropna()
return premium
def _fetch_us_index(
self,