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,