Backtest the portfolio optimizers out of sample against equal weights (1/N) and the benchmark. On every rebalancing date (the last trading day of each month, quarter or year) each optimizer sets its weights from the daily returns of the lookback trading days up to and including that date, and the portfolio holds them from the next day on, so no weight is estimated with a return it is then measured on (walk-forward). Between rebalancing dates the weights drift with the prices, as those of a portfolio that does not trade. The 1/N portfolio is rebalanced to equal weights on the same dates and the benchmark is bought and held.

Each rebalancing trades from the drifted weights to the new ones. Its turnover is Σ |w_i,new - w_i,drifted| and its cost the transaction cost times the turnover, taken from the portfolio’s value, so the returns shown are after costs (DeMiguel, Garlappi and Uppal, 2009). With a maximum turnover, a portfolio trades towards its new weights only as far as the limit allows. DeMiguel et al. found that none of fourteen optimizers consistently beat 1/N out of sample, because the estimation error in the expected returns outweighs the gain from optimizing; a backtest shows whether that holds for these assets.

For more information, see the following paper:

  • DeMiguel, V., Garlappi, L. and Uppal, R. (2009). “Optimal Versus Naive Diversification: How Inefficient Is the 1/N Portfolio Strategy?” The Review of Financial Studies, 22(5), 1915-1953.

Also known as: walk-forward backtest, out-of-sample backtest, rolling optimization, 1/N comparison.

Optimization Backtest in Python or via the MCP

Install the Finance Toolkit with:

pip install financetoolkit -U

Then call get_optimization_backtest as shown below.

from financetoolkit import Toolkit

toolkit = Toolkit(
    ["AAPL", "MSFT", "JNJ", "XOM"],
    api_key="FINANCIAL_MODELING_PREP_KEY",
    start_date="2021-01-01",
    end_date="2025-12-31",
)

toolkit.portfolio.get_optimization_backtest(transaction_cost=0.001)

Or, after setting up the Finance Toolkit MCP, ask:

How did minimum variance, maximum Sharpe, risk parity and other optimized portfolios of Apple, Microsoft, Johnson & Johnson and ExxonMobil perform out of sample from 2021 to 2025, with transaction costs of 0.1%?

Which returns:

Portfolio Annualized Return Annualized Volatility Sharpe Ratio Maximum Drawdown Average Turnover Annualized Costs
Minimum Variance 0.0891 0.1375 0.4061 -0.1268 0.0667 0.0008
Maximum Sharpe 0.0828 0.2124 0.2971 -0.2776 0.4378 0.0052
Risk Parity 0.1242 0.1469 0.6056 -0.1439 0.0428 0.0005
Hierarchical Risk Parity 0.1003 0.1382 0.4784 -0.135 0.0474 0.0006
Maximum Diversification 0.1225 0.1426 0.6085 -0.1357 0.0617 0.0007
Inverse Volatility 0.122 0.151 0.5798 -0.1484 0.0415 0.0005
Equal Weight 0.1407 0.1624 0.6519 -0.1594 0.0388 0.0005
Benchmark 0.1285 0.1799 0.545 -0.221 nan nan

Parameters

get_optimization_backtest accepts the following parameters.

ParameterTypeDescriptionExample
methodslist[str] | str | NoneThe optimizers to backtest, as a list or comma separated: “minimum_variance”, “maximum_sharpe”, “mean_variance” (a risk aversion of 2.5), “risk_parity”, “hierarchical_risk_parity”, “hierarchical_equal_risk_contribution”, “nested_clustered_optimization” (minimum variance within and across the clusters), “inverse_volatility”, “maximum_diversification”, “minimum_cvar” (95%), “minimum_drawdown” (the 95% CDaR), “maximum_omega” (a minimum acceptable return of 0) and “maximum_cvar_ratio” (95%). Equal weights and the benchmark are always included. Defaults to None, which backtests minimum variance, maximum Sharpe, risk parity, Hierarchical Risk Parity, maximum diversification and inverse volatility.None, which backtests minimum variance, maximum Sharpe, risk parity, Hierarchical Risk Parity, maximum diversification and inverse volatility
rebalancingstrHow often the weights are set anew: “monthly”, “quarterly” or “yearly”. Defaults to “monthly”."monthly"
lookbackintThe number of daily returns each estimation uses. The backtest starts on the first rebalancing date with that many returns up to and including it. Defaults to 252, a year of trading days.252, a year of trading days
transaction_costfloatThe cost per unit of weight traded, e.g. 0.001 for 10 basis points. The first investment is free, so every portfolio starts from the same value. Defaults to 0.0
maximum_turnoverfloat | NoneThe highest turnover of a rebalancing, e.g. 0.2. Defaults to None, no limit.None, no limit
covariancestrThe estimator of the covariance matrix: “sample” (ddof=1), “ledoit_wolf” (towards the scaled identity, Ledoit and Wolf, 2004), “ledoit_wolf_constant_correlation” (towards the average correlation, Ledoit and Wolf, 2004), “ledoit_wolf_single_factor” (towards the covariance that the benchmark as a single factor implies, Ledoit and Wolf, 2003), “oas” (Chen et al., 2010), “semicovariance” (the co-movement below a zero return, Estrada, 2007), “exponential” (recent periods weigh most, RiskMetrics) or “denoised” (the eigenvalues of the correlation matrix that random returns could produce averaged, Laloux et al., 2000). The shrinkage and denoised estimators stabilize the weights when there are many assets compared to the number of periods. Defaults to “sample”."sample"
covariance_spanintThe span in periods of the “exponential” covariance; the weight of a period halves after about span / 2.9 periods. Defaults to 180.180
expected_returnsstrThe estimator of the expected returns: “mean” (the historical average), “exponential” (the exponentially weighted average, recent periods weigh most), “capm” (the returns the Capital Asset Pricing Model implies from each asset’s beta to the benchmark, Sharpe, 1964) or “james_stein” (the averages shrunk towards that of the minimum variance portfolio, Jorion, 1986). Defaults to “mean”."mean"
expected_returns_spanintThe span in periods of the “exponential” expected returns. Defaults to 180.180
minimum_weightfloat | dict[str, float]The lowest weight of every asset, or per asset, for the optimizers that take weight bounds (all but inverse volatility and nested clustered optimization). Defaults to 0.0
maximum_weightfloat | dict[str, float]The highest weight of every asset, or per asset, for the optimizers that take weight bounds. Defaults to 1.1
outputstr“summary” for the performance of each portfolio, “returns” for the daily returns after costs, or “weights” for the weights set on each rebalancing date. Defaults to “summary”."summary"
roundingint | NoneThe number of decimals to round the results to. Defaults to None, which uses the Toolkit’s rounding.2
Share