From 5f4470d53ec669d3a412f6e842b7d8fbc0c9ac8c Mon Sep 17 00:00:00 2001 From: aszerW Date: Thu, 26 Mar 2026 01:26:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(datasource):=20=E4=BF=AE=E6=AD=A3=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=97=A5=E6=9C=9F=E5=AF=B9=E9=BD=90=E4=B8=8E=E5=A4=8D?= =?UTF-8?q?=E6=9D=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改yfinance获取历史数据时end_date加一天,auto_adjust设置为False,以获取不复权价格 - 调整ETF净值数据获取时end_date加一天,解决净值数据滞后问题 - 数据对齐策略改为以A股最新数据日期为基准,调整交易日历范围 - 移除对非A股数据的前向/后向填充,保持原始价格数据不填充 - ETF净值数据重新索引到A股交易日但不做缺失值填充,保持NaN以表示无数据 - 增加打印输出辅助调试数据日期及交易日信息 --- core/datasource/hybrid_source.py | 60 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/core/datasource/hybrid_source.py b/core/datasource/hybrid_source.py index 30dbcc7..f955487 100644 --- a/core/datasource/hybrid_source.py +++ b/core/datasource/hybrid_source.py @@ -338,7 +338,11 @@ class HybridDataSource: try: ticker = yf.Ticker(yf_code) - data = ticker.history(start=start_date, end=end_date) + # auto_adjust=False 获取不复权价格,与网页显示一致 + # end_date 需要加一天,因为 yfinance 的 end 是排他的 + from datetime import timedelta + end_date_obj = pd.Timestamp(end_date) + timedelta(days=1) + data = ticker.history(start=start_date, end=end_date_obj.strftime('%Y-%m-%d'), auto_adjust=False) if len(data) == 0: return None @@ -585,8 +589,10 @@ class HybridDataSource: # 获取ETF价格数据 price_data = self._fetch_etf(etf_code, start_date, end_date) - # 获取ETF净值数据 - nav_data = self._fetch_etf_nav(etf_code, start_date, end_date) + # 获取ETF净值数据(净值通常滞后一天,所以end_date+1) + from datetime import timedelta + nav_end_date = (pd.Timestamp(end_date) + timedelta(days=1)).strftime('%Y-%m-%d') + nav_data = self._fetch_etf_nav(etf_code, start_date, nav_end_date) if price_data is not None and len(price_data) > 0: # 使用指数代码作为列名,保持与指数数据一致 @@ -633,42 +639,38 @@ class HybridDataSource: aggfunc='first' ) - # 以A股交易日为基准,对齐所有数据 - # 强制从 Tushare 获取A股交易日历(不管配置中是否有A股指数) - start_str = index_data.index.min().strftime('%Y%m%d') - end_str = index_data.index.max().strftime('%Y%m%d') + # 数据对齐策略:使用各标的能获取到的最新数据 + # 以A股最新数据日期为基准,其他市场数据对齐到该日期 + + # 获取A股最新数据日期 + china_codes = [c for c in valid_codes if self._is_china_index(c)] + if china_codes: + a_share_latest = index_data[china_codes].dropna().index.max() + else: + # 如果没有A股,使用所有数据的最早最新日期 + a_share_latest = index_data.dropna().index.max() + + print(f" A股最新数据日期: {a_share_latest.strftime('%Y-%m-%d')}") + + # 获取A股交易日历(从start_date到a_share_latest) + start_str = pd.Timestamp(start_date).strftime('%Y%m%d') + end_str = a_share_latest.strftime('%Y%m%d') import tushare as ts pro = ts.pro_api(self._get_tushare_token()) trade_cal = pro.trade_cal(start_date=start_str, end_date=end_str, is_open='1') a_share_dates = pd.to_datetime(trade_cal['cal_date']).sort_values() - print(f" A股交易日历: {len(a_share_dates)} 天 ({start_str} ~ {end_str})") - - # 重新索引到A股交易日 + # 重新索引到A股交易日(只到A股最新数据日期) index_data = index_data.reindex(a_share_dates) # 对非A股指数进行数据对齐 - # 港股、美股:T日收盘,T+1日09:00前使用T日数据(前向填充) - # 加密货币、期货(含夜盘):T+1日09:00前使用T+1日数据(后向填充) - # - 加密货币UTC 00:00收盘(北京时间08:00) - # - 期货AU.SHF夜盘02:30收盘,数据标记为T+1日 + # 策略:价格数据保持原始值,不做填充 + # 指标计算后会和价格作为一个整体进行向前填充 non_a_codes = [c for c in valid_codes if not self._is_china_index(c)] - yf_codes = [c for c in non_a_codes if not self._is_crypto(c) and not self._is_futures(c)] - crypto_futures_codes = [c for c in non_a_codes if self._is_crypto(c) or self._is_futures(c)] - - # 港股/美股:前向填充(使用T日数据) - for code in yf_codes: - if code in index_data.columns: - index_data[code] = index_data[code].ffill().bfill() - - # 加密货币/期货:后向填充(使用T+1日数据) - for code in crypto_futures_codes: - if code in index_data.columns: - index_data[code] = index_data[code].bfill().ffill() if non_a_codes: - print(f" 非A股标的: {len(non_a_codes)} 只 (港股/美股:ffill, 加密货币/期货:bfill)") + print(f" 非A股标的: {len(non_a_codes)} 只 (价格保持原始值)") print(f" 时间范围: {index_data.index[0]} ~ {index_data.index[-1]}") print(f" 交易日数: {len(index_data)}") @@ -717,9 +719,9 @@ class HybridDataSource: aggfunc='first' ) - # 对齐到A股交易日,并前向填充缺失值(净值数据通常T+1更新) + # 对齐到A股交易日,但不填充缺失值(保持原始数据,让报告层决定是否显示溢价率) etf_nav_data = etf_nav_data.reindex(a_share_dates) - etf_nav_data = etf_nav_data.ffill() # 前向填充缺失的净值数据 + # 注意:不做ffill填充,保持NaN表示当天无净值数据 print(f" ETF净值数据: {len(etf_nav_data.columns)} 只")