clean(rotation): add simple rotation strategy and remove unused files
New: - rotation/simple_rotation.py: daily-iteration rotation strategy (584 lines) - rotation/config_loader.py: standalone config loader - rotation/config_simple.yaml: 11 assets, 7 groups - rotation/README_SIMPLE.md: usage guide - scripts/get_trading_calendar.py: trading calendar fetcher Removed: - rotation/example_usage.py, run_strategy.py (replaced by simple_rotation.py) - rotation/results/ output files (gitignored) - scripts/verify_*.py, calculate_returns_from_detail.py (one-off scripts) - scripts/README_TRADING_CALENDAR.md Backtest result (2020-01-10 ~ 2026-06-01): - Total return: 1237.6%, Annual: 52.66% - Max drawdown: -11.71%, Sharpe: 2.50
This commit is contained in:
@@ -391,31 +391,55 @@ class GlobalRotationStrategy(StrategyBase):
|
||||
aligner = CrossMarketAligner(target_calendar=trading_calendar)
|
||||
|
||||
# 提取交易标的的收盘价,并对齐到 A 股日历
|
||||
print(" [对齐] 对齐 ETF 价格到 A 股日历...")
|
||||
close_dict = {}
|
||||
print(" [对齐] 构建可实现价格序列(模拟真实交易)...")
|
||||
executable_close_dict = {}
|
||||
|
||||
for signal_code, trade_code in signal_to_trade.items():
|
||||
if trade_code in data:
|
||||
# 提取收盘价
|
||||
close_series = data[trade_code]['close']
|
||||
# 使用 signal_code 作为键(与 positions 列名一致)
|
||||
close_dict[signal_code] = close_series
|
||||
# 提取开盘价和收盘价
|
||||
etf_df = data[trade_code]
|
||||
open_series = etf_df['open'].reindex(trading_calendar, method='ffill')
|
||||
close_series = etf_df['close'].reindex(trading_calendar, method='ffill')
|
||||
|
||||
# 默认使用收盘价
|
||||
exec_close = close_series.copy()
|
||||
|
||||
# 检测调仓日,调整价格以反映真实交易
|
||||
for i in range(1, len(trading_calendar)):
|
||||
date = trading_calendar[i]
|
||||
prev_date = trading_calendar[i-1]
|
||||
|
||||
# 获取仓位变化
|
||||
prev_pos = positions.loc[prev_date, signal_code] if signal_code in positions.columns else 0
|
||||
curr_pos = positions.loc[date, signal_code] if signal_code in positions.columns else 0
|
||||
|
||||
# 买入日:修改前一天价格为当日开盘价
|
||||
# 这样收益率 = (close[t] - open[t]) / open[t] = 日内收益
|
||||
if pd.isna(prev_pos) or prev_pos == 0:
|
||||
if pd.notna(curr_pos) and curr_pos > 0:
|
||||
exec_close.loc[prev_date] = open_series.loc[date]
|
||||
|
||||
# 卖出日:不需要修改(因为 positions[t]=0,不会计算收益)
|
||||
|
||||
executable_close_dict[signal_code] = exec_close
|
||||
else:
|
||||
print(f" 警告: {trade_code} 数据不存在,跳过")
|
||||
|
||||
# 使用 CrossMarketAligner 对齐多标的收益率
|
||||
# 内部逻辑:先 ffill 价格到 A 股日历,再计算收益率
|
||||
print(" [对齐] 计算收益率(先对齐价格,再计算)...")
|
||||
returns_df = aligner.align_multi_asset(close_dict)
|
||||
print(" [对齐] 计算收益率(使用可实现价格)...")
|
||||
returns_df = aligner.align_multi_asset(executable_close_dict)
|
||||
print(f" [对齐] 收益率数据: {len(returns_df)} 天, {len(returns_df.columns)} 个标的")
|
||||
|
||||
# 对齐 positions 到 A 股日历
|
||||
# 注意:必须先 reindex 再 ffill,因为 reindex(method='ffill') 不会填充已有的 NaN
|
||||
positions = positions.reindex(trading_calendar)
|
||||
positions = positions.ffill()
|
||||
# 卖出日不向前填充(保持 0)
|
||||
positions = positions.ffill().fillna(0)
|
||||
|
||||
# 计算策略收益(仓位加权,T+1 执行)
|
||||
positions_delayed = positions.shift(1).fillna(0)
|
||||
strategy_returns = (positions_delayed * returns_df).sum(axis=1)
|
||||
# 计算策略收益(仓位加权,无需延迟)
|
||||
# 因为 positions[t] 已表示 t 日的实际持仓,且价格已调整为可实现价格
|
||||
strategy_returns = (positions * returns_df).sum(axis=1)
|
||||
|
||||
# 扣除交易成本
|
||||
strategy_returns, rebalance_count = self._apply_trade_cost(
|
||||
@@ -649,6 +673,33 @@ class GlobalRotationStrategy(StrategyBase):
|
||||
index_return_dict = {}
|
||||
etf_return_dict = {}
|
||||
|
||||
# 构建 ETF 可实现价格序列(与回测一致)
|
||||
executable_etf_close = {}
|
||||
for signal_code, trade_code in signal_to_trade.items():
|
||||
if trade_code in self._data:
|
||||
etf_df = self._data[trade_code]
|
||||
open_series = etf_df['open'].reindex(trading_calendar, method='ffill')
|
||||
close_series = etf_df['close'].reindex(trading_calendar, method='ffill')
|
||||
|
||||
# 默认使用 close
|
||||
exec_close = close_series.copy()
|
||||
|
||||
# 检测调仓日,调整价格
|
||||
for i in range(1, len(trading_calendar)):
|
||||
date = trading_calendar[i]
|
||||
prev_date = trading_calendar[i-1]
|
||||
|
||||
# 获取仓位变化
|
||||
prev_pos = positions.loc[prev_date, signal_code] if signal_code in positions.columns else 0
|
||||
curr_pos = positions.loc[date, signal_code] if signal_code in positions.columns else 0
|
||||
|
||||
# 买入日:修改前一天价格为 open
|
||||
if pd.isna(prev_pos) or prev_pos == 0:
|
||||
if pd.notna(curr_pos) and curr_pos > 0:
|
||||
exec_close.loc[prev_date] = open_series.loc[date]
|
||||
|
||||
executable_etf_close[signal_code] = exec_close
|
||||
|
||||
for signal_code, trade_code in signal_to_trade.items():
|
||||
# 指数收益率
|
||||
if signal_code in index_close_dict:
|
||||
@@ -656,10 +707,10 @@ class GlobalRotationStrategy(StrategyBase):
|
||||
idx_return = idx_close.pct_change(fill_method=None).fillna(0)
|
||||
index_return_dict[signal_code] = idx_return
|
||||
|
||||
# ETF 收益率
|
||||
if signal_code in etf_close_dict:
|
||||
etf_close = etf_close_dict[signal_code].reindex(trading_calendar, method='ffill')
|
||||
etf_return = etf_close.pct_change(fill_method=None).fillna(0)
|
||||
# ETF 收益率(使用可实现价格)
|
||||
if signal_code in executable_etf_close:
|
||||
etf_exec = executable_etf_close[signal_code]
|
||||
etf_return = etf_exec.pct_change(fill_method=None).fillna(0)
|
||||
etf_return_dict[signal_code] = etf_return
|
||||
|
||||
# 对齐因子
|
||||
|
||||
Reference in New Issue
Block a user