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

@@ -484,11 +484,11 @@ def get_etf_nav():
"hint": "Only A股ETF (codes starting with 51/52/15/16) supported",
}), 400
# 获取净值
# 获取净值和溢价率
f = get_fetcher()
try:
with f:
price_df, nav_df = f.fetch_etf_with_nav(code, start, end)
price_df, nav_df, premium_series = f.fetch_etf_with_nav(code, start, end)
result = {
"code": code,
@@ -496,14 +496,30 @@ def get_etf_nav():
"nav": dataframe_to_json(nav_df) if nav_df else {"data": [], "count": 0},
}
# 计算最新溢价率
if nav_df is not None and len(nav_df) > 0 and price_df is not None and len(price_df) > 0:
latest_nav = nav_df['nav'].iloc[-1]
latest_price = price_df['close'].iloc[-1]
if latest_nav > 0:
premium = (latest_price - latest_nav) / latest_nav
result['premium_rate'] = premium
result['premium_date'] = nav_df.index[-1].strftime('%Y-%m-%d')
# 添加历史溢价率序列
if premium_series is not None and len(premium_series) > 0:
# 转换为日期-溢价率列表
premium_data = [
{"date": date.strftime('%Y-%m-%d'), "premium": round(premium, 6)}
for date, premium in premium_series.items()
]
result['premium_series'] = premium_data
# 最新溢价率
latest_premium = premium_series.iloc[-1]
latest_date = premium_series.index[-1].strftime('%Y-%m-%d')
result['latest_premium'] = round(latest_premium, 6)
result['premium_date'] = latest_date
# 溢价率统计
result['premium_stats'] = {
"mean": round(premium_series.mean(), 6),
"std": round(premium_series.std(), 6),
"min": round(premium_series.min(), 6),
"max": round(premium_series.max(), 6),
"median": round(premium_series.median(), 6),
}
return jsonify(result)