From 16affb236871578c61fdacf2077e1aa9ca809a4f Mon Sep 17 00:00:00 2001 From: aszerW Date: Tue, 12 May 2026 21:39:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20fetch=5Fetf=5Fwith=5Fnav=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=8E=86=E5=8F=B2=E6=BA=A2=E4=BB=B7=E7=8E=87=E5=BA=8F?= =?UTF-8?q?=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 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% --- datasource/flask_server.py | 36 +++++++++++++++------- datasource/universal_fetcher.py | 53 +++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/datasource/flask_server.py b/datasource/flask_server.py index 5e8458d..eedf9e4 100644 --- a/datasource/flask_server.py +++ b/datasource/flask_server.py @@ -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) diff --git a/datasource/universal_fetcher.py b/datasource/universal_fetcher.py index 6970541..0aa79d2 100644 --- a/datasource/universal_fetcher.py +++ b/datasource/universal_fetcher.py @@ -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,