Quantitative Finance & Forecasting
Campbell-Thompson Out-of-Sample R-Squared and Truncated Forecast
Compute Campbell and Thompson’s (2008) out-of-sample R^2_OS = 1 - SSE_model / SSE_hist and verify how zero-truncating negative risk-premium forecasts MAX(0, r_hat) turns negative R^2_OS positive.
Why unconstrained equity-premium regressions lose out-of-sample — and how MAX(0, r_hat) fixes them
Goyal and Welch showed that unconstrained predictive regressions often deliver negative out-of-sample R^2_OS = 1 - SSE_model / SSE_hist relative to the prevailing historical mean r_hist because extreme valuation multiples occasionally extrapolate negative expected equity risk premia! Campbell and Thompson (2008) proved that imposing the economic restriction that expected risk premia cannot be negative — replacing each raw forecast r_raw_t with r_trunc_t = MAX(0, r_raw_t) — slashes out-of-sample squared error.
Audit a 5-year out-of-sample window on the sheet: realized excess returns B2:B6 = {0.04, 0.01, 0.06, 0.02, 0.02} against historical benchmark C2:C6 = {0.03, 0.03, 0.03, 0.03, 0.03} produce SSE_hist = 0.0016. Unconstrained forecasts D2:D6 = {0.04, -0.02, 0.05, -0.01, 0.02} suffer two wrong-signed negative predictions, inflating SSE_raw to 0.0019 and driving R^2_OS_raw = 1 - 0.0019 / 0.0016 = -0.1875 (-18.75%)! Applying row-by-row MAX(0, D2) in E2:E6 cuts SSE_trunc to 0.0006 and lifts R^2_OS_trunc to +0.6250 (+62.50%)!
| Forecast Specification | Sum of Squared Forecast Errors (SSE) | Out-of-Sample R^2_OS |
|---|---|---|
| Prevailing Historical Mean Benchmark | SSE_hist = 0.0016 | 0.0000 (Baseline) |
| Unconstrained Linear Predictor | SSE_raw = 0.0019 | -0.1875 (Underperforms Mean) |
| Campbell-Thompson MAX(0, r_hat) | SSE_trunc = 0.0006 | +0.6250 (Beats Benchmark) |
Campbell-Thompson Out-of-Sample R^2_OS = 1 - SSE_model / SSE_hist Formula
Important spreadsheet trap: writing MAX(0, D2:D6) aggregates the entire range into one scalar maximum rather than clamping each period! Always compute row-level MAX(0, D2) in E2:E6 and expand SUM((A - B)^2) via SUMPRODUCT(A, A) - 2*SUMPRODUCT(A, B) + SUMPRODUCT(B, B) for 100% engine-safe precision.
=LET(r_act, B2:B6, r_bmk, C2:C6, r_mod, E2:E6, sse_b, SUMPRODUCT(r_act, r_act) - 2 * SUMPRODUCT(r_act, r_bmk) + SUMPRODUCT(r_bmk, r_bmk), IF(sse_b <= 1E-12, "Zero Benchmark Error", LET(sse_m, SUMPRODUCT(r_act, r_act) - 2 * SUMPRODUCT(r_act, r_mod) + SUMPRODUCT(r_mod, r_mod), ROUND(1 - sse_m / sse_b, 4))))Truncated vs Unconstrained R^2_OS Improvement Gain Formula
Measure the exact out-of-sample R^2_OS lift achieved by enforcing the economic sign restriction MAX(0, r_raw_t).
=IF(OR(ISTEXT(H2), ISTEXT(H3)), "Zero Benchmark Error", ROUND(H3 - H2, 4))Try the example
Use the original values or change them to test your understanding.