feat(v2): 增强框架核心功能与ETF复权修复
- 修复 end_date=None 导致 Flask API 返回错误时间范围的 bug * strategy.py: 自动使用今天日期作为 end_date * 验证:回测区间从 77 天恢复到 1539 天 - ETF 收益计算从原始价格改为后复权价格 * flask_api_fetcher.py: adj='raw' → adj='hfq' * 自动处理 ETF 份额拆分事件,确保收益率准确 - V2 简单版添加 A 股交易日过滤 * simple.py: 获取 SSE 交易日历,过滤非交易日 * 验证:1999 天 → 1539 天(与 V1 一致) - 配置严格对齐 V1 config.yaml * config_simple.yaml: start_date 从 2020-01-01 改为 2020-01-10 * group 字段值严格映射 V1 的 market 字段 关键验证: - V2 简单版回测:1539 天,981.95% 收益(未计入交易成本) - V2 正式版回测:1539 天,135.63% 收益(已计入交易成本) - V1 旧版框架:1539 天,103.29% 收益(基准)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 类型
|
||||
)
|
||||
|
||||
|
||||
@@ -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 保持一致
|
||||
|
||||
# ============================================================
|
||||
# 因子配置
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user