fix(datasource): yfinance时区标准化与NaN过滤修复
- yfinance_source.py: 用 tz_localize(None) 替代 pd.to_datetime(utc=True), 避免亚洲/欧洲市场因UTC转换导致日期回退一天(如日经225 5/25→5/24) - yfinance_source.py: 新增 _normalize_index() 静态方法统一处理时区剥除 - yfinance_source.py: fetch() 增加 close=NaN 行过滤(yfinance未收盘日返回不完整数据) - flask_api_source.py: 客户端同步增加 close=NaN 过滤防御 验证结果:N225 5/25-6/3 返回7个交易日数据,日期无偏移
This commit is contained in:
@@ -167,6 +167,15 @@ class FlaskAPIDataSource:
|
|||||||
standard_cols = ['code'] + standard_cols
|
standard_cols = ['code'] + standard_cols
|
||||||
df = df[standard_cols]
|
df = df[standard_cols]
|
||||||
|
|
||||||
|
# 过滤 yfinance 返回的不完整数据(未收盘日 close=NaN, volume=0)
|
||||||
|
nan_count = df['close'].isna().sum()
|
||||||
|
if nan_count > 0:
|
||||||
|
df = df.dropna(subset=['close'])
|
||||||
|
actual_count = len(df)
|
||||||
|
if actual_count == 0:
|
||||||
|
print(f"⚠ {code}: 所有数据 close 均为 NaN")
|
||||||
|
return None
|
||||||
|
|
||||||
# 使用 API 返回的实际数据范围(而非请求参数)
|
# 使用 API 返回的实际数据范围(而非请求参数)
|
||||||
actual_start = validated.date_range.start if validated.date_range else start_date
|
actual_start = validated.date_range.start if validated.date_range else start_date
|
||||||
actual_end = validated.date_range.end if validated.date_range else end_date
|
actual_end = validated.date_range.end if validated.date_range else end_date
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ class YFinanceSource:
|
|||||||
self.use_ssh_tunnel = use_ssh_tunnel
|
self.use_ssh_tunnel = use_ssh_tunnel
|
||||||
self._delay = 0.5 # 请求延迟(避免限流)
|
self._delay = 0.5 # 请求延迟(避免限流)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_index(index: pd.DatetimeIndex) -> pd.DatetimeIndex:
|
||||||
|
"""
|
||||||
|
标准化日期索引,保留交易所本地日期
|
||||||
|
|
||||||
|
yfinance 返回的时间戳带有交易所本地时区:
|
||||||
|
- 美股: 00:00-04:00 (US/Eastern) → 剥 tz 后 00:00,日期不变
|
||||||
|
- 日本: 00:00+09:00 (Asia/Tokyo) → 剥 tz 后 00:00,日期不变
|
||||||
|
- 欧洲: 00:00+02:00 (CET) → 剥 tz 后 00:00,日期不变
|
||||||
|
|
||||||
|
关键:直接 tz_localize(None) 剥除时区,不做 UTC 转换。
|
||||||
|
错误示范:pd.to_datetime(idx, utc=True) 会先把日本 00:00+09:00 转成
|
||||||
|
前一天 15:00 UTC,导致日期回退一天。
|
||||||
|
"""
|
||||||
|
if index.tz is not None:
|
||||||
|
# tz_localize(None) 直接剥除时区,保留本地时间部分
|
||||||
|
index = index.tz_localize(None)
|
||||||
|
return index.normalize()
|
||||||
|
|
||||||
def fetch(self, code: str, start_date: str, end_date: str, adj: str = 'raw') -> Optional[pd.DataFrame]:
|
def fetch(self, code: str, start_date: str, end_date: str, adj: str = 'raw') -> Optional[pd.DataFrame]:
|
||||||
"""
|
"""
|
||||||
获取数据(支持 adj 参数)
|
获取数据(支持 adj 参数)
|
||||||
@@ -108,10 +127,17 @@ class YFinanceSource:
|
|||||||
"Volume": "volume",
|
"Volume": "volume",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 确保索引是日期格式
|
# 确保索引是日期格式(保留交易所本地日期,避免 UTC 转换导致跨日偏移)
|
||||||
df.index = pd.to_datetime(df.index, utc=True).tz_localize(None).normalize()
|
df.index = self._normalize_index(df.index)
|
||||||
df.index.name = "date"
|
df.index.name = "date"
|
||||||
|
|
||||||
|
# 过滤 yfinance 返回的不完整数据(未收盘日 close=NaN, volume=0)
|
||||||
|
nan_mask = df['close'].isna()
|
||||||
|
if nan_mask.any():
|
||||||
|
df = df[~nan_mask]
|
||||||
|
if len(df) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
# 添加代码列
|
# 添加代码列
|
||||||
df["code"] = code
|
df["code"] = code
|
||||||
|
|
||||||
@@ -189,8 +215,8 @@ class YFinanceSource:
|
|||||||
"Volume": "volume",
|
"Volume": "volume",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 确保索引是日期格式
|
# 确保索引是日期格式(保留交易所本地日期,避免 UTC 转换导致跨日偏移)
|
||||||
df.index = pd.to_datetime(df.index, utc=True).tz_localize(None).normalize()
|
df.index = self._normalize_index(df.index)
|
||||||
df.index.name = "date"
|
df.index.name = "date"
|
||||||
|
|
||||||
# 添加代码列和标记
|
# 添加代码列和标记
|
||||||
|
|||||||
Reference in New Issue
Block a user