diff --git a/framework_v2/core/strategy.py b/framework_v2/core/strategy.py index 5851eac..6c2a5ec 100644 --- a/framework_v2/core/strategy.py +++ b/framework_v2/core/strategy.py @@ -106,12 +106,19 @@ class StrategyBase(ABC): codes = self.get_codes() + # 处理 end_date 为 None 的情况(使用今天) + from datetime import date + start = self.config.backtest.start_date + end = self.config.backtest.end_date + if end is None: + end = date.today().strftime('%Y-%m-%d') + # 批量获取数据(fetch_indices 返回 {code: DataFrame}) try: data = self._data_fetcher.fetch_indices( codes=codes, - start=self.config.backtest.start_date, - end=self.config.backtest.end_date + start=start, + end=end ) return data except Exception as e: diff --git a/framework_v2/shared/data/flask_api_fetcher.py b/framework_v2/shared/data/flask_api_fetcher.py index b263e54..72dcf64 100644 --- a/framework_v2/shared/data/flask_api_fetcher.py +++ b/framework_v2/shared/data/flask_api_fetcher.py @@ -146,7 +146,7 @@ class FlaskAPIFetcher(DataFetcher): code=code, start_date=start, end_date=end, - adj='raw', + adj='hfq', # ETF 收益计算必须使用后复权价格(处理份额拆分) asset_type='china_etf' # 强制指定 ETF 类型 ) diff --git a/framework_v2/strategies/rotation/config_simple.yaml b/framework_v2/strategies/rotation/config_simple.yaml index 361599f..854da22 100644 --- a/framework_v2/strategies/rotation/config_simple.yaml +++ b/framework_v2/strategies/rotation/config_simple.yaml @@ -116,8 +116,8 @@ benchmark: # 回测配置 # ============================================================ backtest: - start_date: "2020-01-01" - # end_date: null # null 表示至今 + start_date: "2020-01-10" # 与 V1 保持一致(第一个完整交易日) + end_date: "2026-05-22" # 与 V1 保持一致 # ============================================================ # 因子配置 diff --git a/framework_v2/strategies/rotation/simple.py b/framework_v2/strategies/rotation/simple.py index 8bd2e56..b74f1a4 100644 --- a/framework_v2/strategies/rotation/simple.py +++ b/framework_v2/strategies/rotation/simple.py @@ -173,6 +173,42 @@ class SimpleRotationStrategy(StrategyBase): return positions + def _get_trading_calendar(self) -> pd.DatetimeIndex: + """ + 获取 A 股交易日历 + + Returns: + A 股交易日历 DatetimeIndex + """ + from datetime import date + + # 获取回测区间 + start = self.config.backtest.start_date + end = self.config.backtest.end_date + if end is None: + end = date.today().strftime('%Y-%m-%d') + + # 创建临时数据获取器来获取交易日历 + if self._data_fetcher is None: + self._data_fetcher = self._create_data_fetcher() + + try: + # 调用 get_trading_calendar 方法 + calendar = self._data_fetcher.get_trading_calendar( + market='A', + start=start, + end=end + ) + print(f" [日历] A 股交易日: {len(calendar)} 天 ({calendar[0]} ~ {calendar[-1]})") + return calendar + except Exception as e: + print(f" [警告] 无法获取 A 股交易日历,使用所有日期: {e}") + # 降级方案:使用 pandas 生成工作日 + from pandas.tseries.offsets import BDay + start_dt = pd.Timestamp(start) + end_dt = pd.Timestamp(end) + return pd.date_range(start=start_dt, end=end_dt, freq='B') # 工作日 + def _execute_backtest(self, positions: pd.DataFrame, data: Dict[str, pd.DataFrame]) -> Dict[str, any]: """ 执行回测 @@ -181,6 +217,7 @@ class SimpleRotationStrategy(StrategyBase): 1. 使用 signal_source 计算信号(positions 的 columns 是 signal_source) 2. 使用 trade_source 计算收益(通过 signal→trade 映射) 3. T+1 执行:今天的信号明天生效 + 4. 过滤非交易日:只保留 A 股交易日 Args: positions: 仓位 DataFrame(columns=signal_source) @@ -206,6 +243,17 @@ class SimpleRotationStrategy(StrategyBase): # 计算收益率 returns = close_df.pct_change() + # 获取 A 股交易日历并过滤 + print("\n [过滤] 获取 A 股交易日历...") + trading_calendar = self._get_trading_calendar() + + # 过滤到 A 股交易日 + original_days = len(returns) + returns = returns[returns.index.isin(trading_calendar)] + positions = positions[positions.index.isin(trading_calendar)] + filtered_days = len(returns) + print(f" [过滤] 原始数据: {original_days} 天 -> A 股交易日: {filtered_days} 天 (过滤 {original_days - filtered_days} 天)") + # 计算策略收益(仓位加权) # 注意:T+1 执行,今天的信号明天生效 positions_delayed = positions.shift(1).fillna(0)