Rebalance Lab: a validation companionΒΆ
Principal findingΒΆ
Across six semiannual calendars, the 12-1 monthly-minus-semiannual CAGR difference changes sign. At 5 bps it ranges from -0.92 to +0.53 pp/year. The six-sleeve tranche has 8.24% CAGR and -40.91% daily maximum drawdown. The two primary paired 95% intervals cross zero.
This executed notebook recomputes statistics and editable figures from public derived portfolio inputs. It does not reconstruct portfolios from raw vendor prices or certify live execution. The owner has completed a trade reconciliation and written result/judgment review; exact signal-window explanation and observed oral presentation remain pending.
Context & MethodsΒΆ
Claim: more frequent momentum selection and reweighting improves net portfolio performance. Nine sector ETFs; 2001-2025; primary 12-1 signal; 5 bps per side; March/September reference. Other signals, calendars and costs are robustness scenarios.
Key AssumptionsΒΆ
- Equal tranche capital at formation in 1999, followed by 2000 burn-in. No sleeve transfer or re-equalization at the reporting anchor.
- Net monthly simple returns, paired by calendar month. Bootstrap resamples returns, not price histories or fresh strategy decisions.
- Captured French monthly RF; zero-interest portfolio cash. Sharpe/TE use sample standard deviation, annualized with sqrt(12); beta includes an intercept.
- Retrospective extension, prior history inspected; no prospective-registration or untouched-holdout claim.
Clone the repository, install requirements-research.txt in a virtual environment, and run this notebook with that environment's kernel. No credentials, downloads or absolute machine paths are needed.
1. Locate the repository and load analysis toolsΒΆ
The notebook uses project calculation helpers for reproduction, not as an independent check. The separately authored verifier and its receipt are linked below.
from pathlib import Path
import sys, json, hashlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
ROOT = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / "site/data/validation-study-summary.json").exists())
sys.path.insert(0, str(ROOT))
from pilot.validation_study import risk_metrics, paired_cagr_intervals
from pilot.validation_figures import calendar_figure, wealth_figure, interval_figure, cost_figure
get_ipython().run_line_magic("matplotlib", "inline")
DATA = ROOT / "site/data"
summary = json.loads((DATA / "validation-study-summary.json").read_text(encoding="utf-8"))
DataΒΆ
2. Verify identities and exact monthly alignmentΒΆ
Sources: public validation-monthly-returns.csv, validation-daily-nav.csv, result JSON and replay receipt. RF provenance records the official French archive URL, retrieval time and hashes. Derived strategy returns are not raw vendor observations.
receipt = json.loads((DATA / "validation-study-replay.json").read_text(encoding="utf-8"))
digest = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
assert receipt["passed"] and receipt["public_summary_sha256"] == digest(DATA / "validation-study-summary.json")
for name, record in summary["derived_inputs"].items():
assert digest(DATA / name) == record["sha256"], name
monthly = pd.read_csv(DATA / "validation-monthly-returns.csv")
expected = [f"{year}-{month:02d}" for year in range(2001, 2026) for month in range(1, 13)]
assert len(monthly) == 34800 and monthly.run_id.nunique() == 116
for identity, group in monthly.groupby("run_id", sort=False):
assert group.month.tolist() == expected, identity
returns = monthly.pivot(index="month", columns="run_id", values="simple_return")
RF = monthly[monthly.run_id == "SPY-5bps"].RF.to_numpy()
ids = summary["primary_ids"]
accounts = {r["run_id"]: r for r in summary["runs"]}
pd.DataFrame({"Verified item": ["Aligned months", "Source accounts", "Derived tranches", "Monthly rows"], "Count": [300, 108, 8, len(monthly)]})
| Verified item | Count | |
|---|---|---|
| 0 | Aligned months | 300 |
| 1 | Source accounts | 108 |
| 2 | Derived tranches | 8 |
| 3 | Monthly rows | 34800 |
fig = calendar_figure(summary)
display(fig)
plt.close(fig)
The primary 5 bps comparison wins against three calendars and loses against three. All scenarios share the same history; these are not independent replications.
4. Recompute growth and monthly riskΒΆ
The table combines daily maximum drawdown from the validated ledger with newly recomputed monthly risk. TE is annualized tracking error to SPY; cash earns zero.
risk_rows = []
for key in ["monthly", "semiannual", "tranched", "benchmark"]:
identity = ids[key]
vector = returns[identity].to_numpy()
calculated = risk_metrics(vector, returns[ids["benchmark"]].to_numpy(), RF)
CAGR = np.expm1(12 * np.log1p(vector).mean())
assert abs(CAGR - accounts[identity]["metrics"]["full_period"]["cagr"]) < 1e-12
for name, value in calculated.items():
if value is not None:
assert abs(value - accounts[identity]["monthly_risk"][name]) < 1e-10
risk_rows.append({"Portfolio": key, "CAGR %": 100*CAGR,
"Daily max DD %": 100*accounts[identity]["metrics"]["full_period"]["max_drawdown"],
"Sharpe": calculated["sharpe_monthly_annualized"],
"TE %": 100*calculated["tracking_error_monthly_annualized"],
"SPY beta": calculated["beta_to_SPY_excess_returns"]})
pd.DataFrame(risk_rows).round(3)
| Portfolio | CAGR % | Daily max DD % | Sharpe | TE % | SPY beta | |
|---|---|---|---|---|---|---|
| 0 | monthly | 8.016 | -30.316 | 0.519 | 9.970 | 0.671 |
| 1 | semiannual | 8.508 | -32.811 | 0.557 | 8.680 | 0.718 |
| 2 | tranched | 8.239 | -40.907 | 0.541 | 7.956 | 0.740 |
| 3 | benchmark | 8.763 | -55.189 | 0.522 | 0.000 | 1.000 |
fig = wealth_figure(summary, DATA / "validation-daily-nav.csv")
display(fig)
plt.close(fig)
The tranche's maximum drawdown is deeper than monthly or March/September in this history. The construction spreads a calendar allocation; it is not a guarantee of lower market risk.
5. Recompute the paired bootstrap intervalsΒΆ
Use 10,000 joint draws, seed 20261009, mean blocks 12 (primary), 6 and 24 months. Each estimate is a difference of sampled CAGRs. A shared monthly draw retains dependence between strategy returns.
labels = [ids[k] for k in ["monthly", "semiannual", "tranched", "weight_reset", "benchmark"]]
comparisons = [("monthly_minus_march_september", labels[0], labels[1]),
("monthly_minus_tranched", labels[0], labels[2])]
recomputed = paired_cagr_intervals(returns[labels].to_numpy(), labels, comparisons, summary["protocol"]["bootstrap"])
for calculated, saved in zip(recomputed, summary["primary_intervals"]):
assert calculated["comparison"] == saved["comparison"]
assert np.allclose(calculated["ci95_difference_pp"], saved["ci95_difference_pp"], atol=1e-10, rtol=0)
pd.DataFrame([{"Comparison": r["comparison"], "Block months": r["mean_block_months"],
"Estimate pp/yr": r["cagr_difference_pp"], "95% lower": r["ci95_difference_pp"][0],
"95% upper": r["ci95_difference_pp"][1]} for r in recomputed]).round(3)
| Comparison | Block months | Estimate pp/yr | 95% lower | 95% upper | |
|---|---|---|---|---|---|
| 0 | monthly_minus_march_september | 12 | -0.492 | -3.031 | 2.112 |
| 1 | monthly_minus_tranched | 12 | -0.223 | -2.439 | 2.328 |
| 2 | monthly_minus_march_september | 6 | -0.492 | -2.906 | 1.962 |
| 3 | monthly_minus_tranched | 6 | -0.223 | -2.360 | 2.180 |
| 4 | monthly_minus_march_september | 24 | -0.492 | -3.199 | 2.185 |
| 5 | monthly_minus_tranched | 24 | -0.223 | -2.428 | 2.383 |
fig = interval_figure(summary)
display(fig)
plt.close(fig)
Both primary intervals cross zero; that does not prove equivalence. Resampling assumptions and nonstationarity are material. A sampled return path does not recreate the strategy on an alternative market price path.
6. Keep trading costs attachedΒΆ
Each cost uses its separately computed accounts. The tranche retains gross sleeve costs without netting.
fig = cost_figure(summary)
display(fig)
plt.close(fig)
TakeawaysΒΆ
- Calendar choice changes the sign of the frequency comparison in the primary sample.
- The tranche is an explicit comparison, with a deeper observed drawdown rather than a promised improvement.
- The primary intervals do not establish a general frequency ranking.
- Computation is reconciled on the same vendor snapshot; source-price and execution accuracy are separate questions.
The same AI agent authored the calculation and separate verifier. Owner review is partial; it does not establish full owner notebook reproduction or independent external validation. The A/B/C appendix, 2026 case and exact hashes remain in the saved summary; macro-state and multi-factor extensions are deferred.
References: method protocol, validation memo, design decisions, reproduction guide, French Data Library, Politis and Romano stationary bootstrap.