diff --git a/framework/execution/__init__.py b/framework/execution/__init__.py index 671c131..f97d723 100644 --- a/framework/execution/__init__.py +++ b/framework/execution/__init__.py @@ -193,8 +193,8 @@ class BacktestExecutor(Executor): # 计算策略日收益率 result = self._calculate_daily_returns(signals, data, signal_col) - # 扣除交易成本 - result = self._apply_trade_cost(result, signals, signal_col) + # 扣除交易成本(同时记录调仓事件) + result, rebalance_events = self._apply_trade_cost_with_events(result, signals, signal_col) # 计算净值(起点归一化) result = self._calculate_net_value(result) @@ -208,6 +208,12 @@ class BacktestExecutor(Executor): # 存储回测结果 portfolio.backtest_result = result + portfolio.rebalance_events = rebalance_events # 新增:调仓事件记录 + + # 补充调仓事件的净值信息 + if not rebalance_events.empty: + rebalance_events = self._enrich_rebalance_events(rebalance_events, result) + portfolio.rebalance_events = rebalance_events return portfolio @@ -274,6 +280,162 @@ class BacktestExecutor(Executor): return result + def _apply_trade_cost_with_events(self, result: pd.DataFrame, signals: pd.DataFrame, signal_col: str = 'signal') -> tuple: + """ + 扣除交易成本并记录调仓事件 + + Returns: + (result, rebalance_events): 回测结果DataFrame和调仓事件DataFrame + """ + prev_signal = signals[signal_col].shift(1) + + # 记录调仓事件 + rebalance_events = [] + last_rebalance_date = None + + # 先计算累积收益率(用于计算调仓前后的净值) + cum_return_before_cost = result['策略日收益率'].copy() + + if self.select_num == 1: + # 单标的策略 + for i, (date, curr, prev) in enumerate(zip(signals.index, signals[signal_col], prev_signal)): + # 检查是否调仓 + is_rebalance = False + turnover = 0.0 + added = [] + removed = [] + + if pd.notna(prev) and curr != prev: + is_rebalance = True + turnover = 1.0 if prev else 0.0 + added = [curr] if curr else [] + removed = [prev] if prev else [] + # 扣除成本 + result.loc[date, '策略日收益率'] -= self.trade_cost + + # 记录调仓事件 + if is_rebalance: + # 计算持仓天数 + holding_days = 0 + if last_rebalance_date is not None: + holding_days = (date - last_rebalance_date).days + + event = { + '日期': date, + '调仓前持仓': prev if pd.notna(prev) else '', + '调仓后持仓': curr, + '调入标的': ','.join(added) if added else '', + '调出标的': ','.join(removed) if removed else '', + '换手率': turnover, + '调仓成本': self.trade_cost * turnover, + '持仓天数': holding_days, + '当日收益': result.loc[date, '策略日收益率'] + self.trade_cost * turnover, # 原始收益(扣除成本前) + } + rebalance_events.append(event) + last_rebalance_date = date + + else: + # 多标的策略 + turnover_list = [] + for i, (date, curr, prev) in enumerate(zip(signals.index, signals[signal_col], prev_signal)): + # 检查是否调仓 + is_rebalance = False + turnover = 0.0 + added = [] + removed = [] + + if pd.notna(prev) and curr != prev: + old = set(prev.split(',')) if prev else set() + new = set(curr.split(',')) if curr else set() + added = list(new - old) + removed = list(old - new) + swapped = len(removed) + turnover = swapped / len(old) if old else 0.0 + is_rebalance = len(added) > 0 or len(removed) > 0 + turnover_list.append(turnover) + # 扣除成本 + result.loc[date, '策略日收益率'] -= turnover * self.trade_cost + else: + turnover_list.append(0.0) + + # 记录调仓事件 + if is_rebalance: + # 计算持仓天数 + holding_days = 0 + if last_rebalance_date is not None: + holding_days = (date - last_rebalance_date).days + + event = { + '日期': date, + '调仓前持仓': prev if pd.notna(prev) else '', + '调仓后持仓': curr, + '调入标的': ','.join(added) if added else '', + '调出标的': ','.join(removed) if removed else '', + '换手率': turnover, + '调仓成本': self.trade_cost * turnover, + '持仓天数': holding_days, + '当日收益': result.loc[date, '策略日收益率'] + turnover * self.trade_cost, # 原始收益(扣除成本前) + } + rebalance_events.append(event) + last_rebalance_date = date + + result['换手率'] = turnover_list + + # 转换为DataFrame + rebalance_df = pd.DataFrame(rebalance_events) if rebalance_events else pd.DataFrame() + if not rebalance_df.empty: + rebalance_df['日期'] = pd.to_datetime(rebalance_df['日期']) + rebalance_df = rebalance_df.set_index('日期') + + return result, rebalance_df + + def _enrich_rebalance_events(self, rebalance_df: pd.DataFrame, result: pd.DataFrame) -> pd.DataFrame: + """ + 补充调仓事件的净值信息 + + Args: + rebalance_df: 调仓事件DataFrame + result: 回测结果DataFrame(含净值序列) + + Returns: + 补充净值信息后的调仓事件DataFrame + """ + # 计算调仓前后净值变化 + nav_before_list = [] + nav_after_list = [] + nav_change_list = [] + + for date in rebalance_df.index: + # 获取调仓日的净值 + if date in result.index: + # 调仓前净值:前一天收盘净值 + prev_date_idx = result.index.get_loc(date) - 1 + if prev_date_idx >= 0: + nav_before = result['策略净值'].iloc[prev_date_idx] + else: + nav_before = 1.0 + + # 调仓后净值:当天收盘净值 + nav_after = result.loc[date, '策略净值'] + + # 净值变化 + nav_change = (nav_after / nav_before - 1) * 100 + else: + nav_before = None + nav_after = None + nav_change = None + + nav_before_list.append(nav_before) + nav_after_list.append(nav_after) + nav_change_list.append(nav_change) + + # 添加净值信息列 + rebalance_df['调仓前净值'] = nav_before_list + rebalance_df['调仓后净值'] = nav_after_list + rebalance_df['净值变化%'] = nav_change_list + + return rebalance_df + def _calculate_net_value(self, result: pd.DataFrame) -> pd.DataFrame: """计算净值(起点归一化)""" result['策略净值'] = (1 + result['策略日收益率']).cumprod() diff --git a/strategies/rotation/strategy.py b/strategies/rotation/strategy.py index de0fd59..9f75129 100644 --- a/strategies/rotation/strategy.py +++ b/strategies/rotation/strategy.py @@ -451,17 +451,29 @@ class RotationStrategy(StrategyBase): print("\n回测结果:") print(f" 最终净值: {final_nav:.4f}\n 累计收益: {total_return:.2f}%") + # 获取调仓事件 + rebalance_events = getattr(portfolio, 'rebalance_events', pd.DataFrame()) + if not rebalance_events.empty: + print(f" 调仓次数: {len(rebalance_events)} 次") + # 保存报告 if save_path: result[['策略净值']].to_csv(f"{save_path}_nav.csv") signals.to_csv(f"{save_path}_signals.csv") - print(f" 报告保存: {save_path}_*.csv") + + # 保存调仓事件记录 + if not rebalance_events.empty: + rebalance_events.to_csv(f"{save_path}_rebalances.csv") + print(f" 报告保存: {save_path}_*.csv (含调仓记录)") + else: + print(f" 报告保存: {save_path}_*.csv") return { 'signals': signals, 'result': result, 'portfolio': portfolio, - 'total_return': total_return + 'total_return': total_return, + 'rebalance_events': rebalance_events } return {'signals': signals, 'result': None}