基于权重偏离阈值的动态投资组合再平衡回溯测试问询
Great question—dynamic rebalancing based on weight thresholds is trickier than fixed-period rebalancing because the portfolio state depends on whether we rebalanced in prior periods, which simple lag() can't handle cleanly. Let's walk through a solution using iterative state tracking that meets all your requirements: custom target weights, multiple stocks/periods, and automatic rebalancing when weights drift beyond your threshold.
Step 1: Setup Base Data & Parameters
First, let's start with your existing data and define our rebalancing threshold (adjust this to your needs):
library(dplyr) library(purrr) library(tidyr) library(ggplot2) set.seed(3) n <- 6 rets <- tibble(period = rep(1:n, 3), stock = c(rep("A", n), rep("B", n), rep("C", n)), ret = c(rnorm(n, 0, 0.3), rnorm(n, 0, 0.2), rnorm(n, 0, 0.1))) target_weights <- tibble(stock = c("A", "B", "C"), target_weight = 1/3) # Define rebalancing threshold (e.g., 10% deviation from target) rebalance_threshold <- 0.1
Step 2: Iterative State Tracking with accumulate()
The core idea is to track the portfolio's state (current values per stock, whether we just rebalanced) across each period. We'll use purrr::accumulate() to iterate through each period, updating the state based on returns and our rebalancing rule.
First, Reshape Returns to Wide Format
This makes it easier to handle all stock returns for a single period at once:
rets_wide <- rets %>% pivot_wider(names_from = stock, values_from = ret) %>% arrange(period)
Define Initial Portfolio State
Our starting point is a fully rebalanced portfolio with each stock at its target weight (assuming an initial investment of 1 unit):
target_weights_vec <- target_weights %>% deframe() # Convert to named vector: A=1/3, B=1/3, C=1/3 initial_state <- list( period = 0, portfolio_values = target_weights_vec, rebalanced = TRUE # Mark initial state as rebalanced )
Define Period Processing Function
This function takes the previous portfolio state and current period returns, then calculates whether rebalancing is needed:
process_period <- function(prev_state, current_rets) { current_period <- current_rets$period # Calculate portfolio values if we DON'T rebalance new_values_no_rebal <- prev_state$portfolio_values * (1 + current_rets[c("A", "B", "C")]) total_value <- sum(new_values_no_rebal) current_weights_no_rebal <- new_values_no_rebal / total_value # Check if any weight deviates beyond the threshold deviations <- abs(current_weights_no_rebal - target_weights_vec) needs_rebalance <- any(deviations > rebalance_threshold) # Apply rebalancing if needed if (needs_rebalance) { # Reset values to match target weights (total value remains the same) final_values <- total_value * target_weights_vec final_weights <- target_weights_vec rebalanced_flag <- TRUE } else { final_values <- new_values_no_rebal final_weights <- current_weights_no_rebal rebalanced_flag <- FALSE } # Return updated state list( period = current_period, portfolio_values = final_values, actual_weights = final_weights, rebalanced = rebalanced_flag, deviations = deviations ) }
Run the Backtest
Now we'll iterate through each period to build our portfolio history:
# Convert wide returns to a list of individual period rows period_list <- split(rets_wide, rets_wide$period) # Run the iterative backtest portfolio_history <- accumulate(period_list, process_period, .init = initial_state) # Clean up results into a tidy data frame results <- portfolio_history[-1] %>% # Remove initial state map_dfr(function(state) { tibble( period = state$period, stock = names(state$actual_weights), actual_weight = state$actual_weights, target_weight = target_weights_vec[stock], deviation = state$deviations[stock], rebalanced = state$rebalanced ) })
Step 3: Inspect & Visualize Results
Let's check the output to see when rebalancing happened and how weights evolved:
# View key results print(results %>% select(period, stock, actual_weight, deviation, rebalanced)) # Visualize weights over time results %>% ggplot(aes(x = period, y = actual_weight, color = stock)) + geom_line(linewidth = 1) + geom_hline(aes(yintercept = target_weight), linetype = "dashed", color = "gray50") + # Mark rebalance periods with white-filled points geom_point(data = subset(results, rebalanced), size = 3, shape = 21, fill = "white") + labs( title = "Threshold-Based Dynamic Rebalancing", x = "Period", y = "Portfolio Weight", color = "Stock", caption = "Dashed lines = target weights; white points = rebalance events" ) + theme_minimal()
Key Notes & Extensions
- State Tracking: This approach works because we explicitly carry forward the portfolio's value state between periods, which
lag()can't do since it only looks at prior rows without accounting for rebalancing resets. - Customization: Adjust
rebalance_thresholdto tighten or loosen your rebalancing rule, or modifytarget_weightsto use non-equal weights. - Add-Ons: You can extend the
process_periodfunction to include transaction costs, minimum rebalance amounts, or other rules (e.g., only rebalance if multiple stocks are out of threshold).
内容的提问来源于stack exchange,提问作者Martin

