diff --git a/backend/app/services/screener.py b/backend/app/services/screener.py index fac6731..cbb82a9 100644 --- a/backend/app/services/screener.py +++ b/backend/app/services/screener.py @@ -428,9 +428,15 @@ class ScreenerService: if "name" not in df_full.columns: df_full = df_full.join(instruments.select(inst_cols), on="symbol", how="left") - # 裁剪掉 warmup 部分, 只保留 lookback 范围 (减少 group_by 开销) - lookback_start = target_date - timedelta(days=lookback_days) + # 裁剪掉 warmup 部分, 只保留 lookback 范围 (减少 group_by 开销)。 + # 按交易日计数: 从数据里实际存在的交易日序列取最后 lookback_days 个交易日, + # 不能用 timedelta(days=N) (自然日), 否则周末/节假日会让窗口偏少, 与回测不一致。 if "date" in df_full.columns: + trading_dates = df_full["date"].unique().sort() + if len(trading_dates) > lookback_days: + lookback_start = trading_dates[-(lookback_days + 1)] + else: + lookback_start = trading_dates[0] df_full = df_full.filter(pl.col("date") >= lookback_start) df_full = df_full.sort(["symbol", "date"]) diff --git a/backend/app/tickflow/repository.py b/backend/app/tickflow/repository.py index d24f224..f54ffbe 100644 --- a/backend/app/tickflow/repository.py +++ b/backend/app/tickflow/repository.py @@ -942,12 +942,19 @@ class KlineRepository: cache_max = cache["date"].max() cache_min = cache["date"].min() from datetime import timedelta - # 验证缓存覆盖完整范围 (含 warmup) + # 验证缓存覆盖完整范围 (含 warmup)。lookback_days 是交易日语义, 用 ×2 日历日 + # 放宽确保覆盖 (节假日/周末), 与 warmup 60 一起留足余量。 warmup_start = target_date - timedelta(days=(lookback_days + 60) * 2) if cache_min > warmup_start or cache_max < target_date: return None - # 只返回 lookback 范围 (日历天数 ≈ 2/3 交易日, 足够覆盖) - lookback_start = target_date - timedelta(days=lookback_days) + # 按交易日计数裁剪: 从数据里实际存在的交易日序列取最后 lookback_days 个交易日。 + # 不能用 timedelta(days=N) (自然日), 否则周末/节假日会让窗口只有 ~N×5/7 个交易日, + # 导致 filter_history 策略的滚动窗口/行号差(_gap)漏算, 与回测结果不一致。 + trading_dates = cache["date"].unique().sort() + if len(trading_dates) > lookback_days: + lookback_start = trading_dates[-(lookback_days + 1)] + else: + lookback_start = trading_dates[0] return cache.filter((pl.col("date") >= lookback_start) & (pl.col("date") <= target_date)) def get_enriched_range(