from pathlib import Path
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
from IPython.display import display
from joblib import Parallel, delayed
from sklearn.ensemble import ExtraTreesRegressor, GradientBoostingRegressor, HistGradientBoostingRegressor, RandomForestRegressor
from sklearn.linear_model import ElasticNet
from sklearn.preprocessing import StandardScaler
from threadpoolctl import threadpool_limits
from quantfinlab.backtest.portfolio import run_many_weights_backtests
from quantfinlab.dataio import load_macro_factors, load_nfci, load_yfinance_panel, prices_to_returns_panel
from quantfinlab.ml.evaluation import forecast_buckets, forecast_metrics, quantile_metrics, rank_metrics, walkforward_tabular_predictions
from quantfinlab.ml.probabilistic import apply_rolling_conformal
from quantfinlab.ml.uncertainty import confidence_adjusted_mu, disagreement_confidence, forecast_confidence, model_disagreement
from quantfinlab.plotting.portfolio import (confidence_exposure_map, conformal_shift_chart, coverage_reliability,
disagreement_error_map, feature_correlation_map,
feature_importance_bars, forecast_buckets_chart, forecast_scatter,
forecast_to_weight_map, kelly_shrinkage_curve, kelly_weight_path,
plot_strategy_drawdowns, plot_strategy_nav, rolling_ic_chart,
target_distribution, uncertainty_error_map)
from quantfinlab.portfolio import covariance, expected_returns, optimizers
from quantfinlab.portfolio.robust import wasserstein_weight_frame
from quantfinlab.portfolio.selection import build_strategy_summary
from quantfinlab.portfolio.universe import make_rebalance_dates
from quantfinlab.portfolio.walkforward import run_walkforward_grid
from quantfinlab.reports.risk_report import risk_report
from quantfinlab.ml.features import (assemble_forecasting_table, build_asset_feature_block,
build_cross_asset_feature_block, build_fci_feature_block,
clean_feature_columns, feature_availability_by_date,
regime_probability_features, trim_feature_table_by_availability)
from quantfinlab.ml.sequence_models import TcnForecast, auto_device, torch_predictions, train_torch_model
from quantfinlab.portfolio.sizing import (align_weight_frame, cap_weights, forecast_gated_maxsharpe_weight_frame,
smooth_weights, weights_from_forecasts)
data_path = Path("../data") if Path("../data/sector_etfs.csv").exists() else Path("data")
macro_factors = load_macro_factors(data_path / "us_macro_factors.csv", start="1990-01-01")
nfci = load_nfci(data_path / "nfci.csv")
rf_annual = 0.04
rf_daily = (1.0 + rf_annual) ** (1.0 / 252.0) - 1.0
annualization = 252.0
cost_bps = 10.0
horizon = 21
vol_lookback = 63
train_end = pd.Timestamp("2018-12-31")
validation_start = pd.Timestamp("2017-01-01")
test_start = pd.Timestamp("2019-01-01")
train_label_cutoff = train_end - pd.tseries.offsets.BDay(horizon)
validation_end = train_label_cutoff
TABULAR_TRAIN_WINDOW = 252 * 8
cash_ticker = "SHY"
benchmark_ticker = "SPY"
cpu_count = os.cpu_count() or 1
outer_n_jobs = max(1, min(4, cpu_count))
forest_inner_jobs = max(1, cpu_count // outer_n_jobs)
assets = ["XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY"]
universe_signature = ",".join(assets)
tickers = assets + [cash_ticker, benchmark_ticker]
panels = load_yfinance_panel([data_path / "sector_etfs.csv", data_path / "core_cross_asset_etfs.csv"],
fields=("close", "volume"), tickers=tickers, source="yfinance_export",
start="2007-01-01")
close = panels["close"].reindex(columns=tickers).ffill(limit=3)
volume = panels["volume"].reindex(index=close.index, columns=tickers).ffill(limit=3)
close = close.dropna(how="all")
volume = volume.reindex(close.index)
first_common_sector_date = close[assets + [cash_ticker, benchmark_ticker]].dropna(how="any").index.min()
close = close.loc[first_common_sector_date:].copy()
volume = volume.reindex(close.index).copy()
coverage = pd.DataFrame({"first_date": close[assets + [cash_ticker]].apply(pd.Series.first_valid_index),
"last_date": close[assets + [cash_ticker]].apply(pd.Series.last_valid_index),
"observations": close[assets + [cash_ticker]].notna().sum(),
"coverage": close[assets + [cash_ticker]].notna().mean()})
r = prices_to_returns_panel(close, kind="simple").fillna(0.0)
r_log = np.log(close / close.shift(1))
fwd = np.log(close[assets].shift(-horizon) / close[assets])
rf_forward_log = horizon * np.log1p(rf_daily)
r_ex_21 = fwd - rf_forward_log
sigma_21 = r_log[assets].rolling(vol_lookback, min_periods=vol_lookback).std(ddof=1) * np.sqrt(horizon)
z_21 = (r_ex_21 / sigma_21.replace(0.0, np.nan)).clip(-4.0, 4.0)
y_alpha = z_21.sub(z_21.median(axis=1), axis=0).clip(-4.0, 4.0)
rebalance = make_rebalance_dates(close.index, freq="ME", min_history_days=756)
target = (
pd.concat({"r_ex_21": r_ex_21, "sigma_21": sigma_21, "z_21": z_21, "y_alpha": y_alpha}, axis=1)
.stack(level=1)
.rename_axis(["date", "asset"])
.reset_index()
.replace([np.inf, -np.inf], np.nan)
.dropna()
)
target_s_counts = target.groupby("date")["asset"].nunique()
target_s_rows_before_full = len(target)
target = target[target["date"].isin(target_s_counts[target_s_counts.eq(len(assets))].index)].copy()
target = target.sort_values(["date", "asset"]).reset_index(drop=True)
rebalance = pd.DatetimeIndex([d for d in rebalance if d <= target["date"].max()])
base = target[target["date"].isin(rebalance)].copy()
fixed_sector_universe = {
pd.Timestamp(dt): {"tickers": list(assets), "avg_dollar_volume": pd.Series(1.0, index=assets)}
for dt in rebalance
}
grid = run_walkforward_grid(
returns=r[assets],
close=close[assets],
volume=volume[assets],
rebalance_dates=rebalance,
universe_by_date=fixed_sector_universe,
cov_models={"LedoitWolf": covariance.ledoit_wolf_covariance, "EWMA": covariance.ewma_covariance},
mu_models={"BayesStein": expected_returns.bayes_stein_mu},
optimizers={"EW": optimizers.equal_weight, "MaxSharpe": optimizers.max_sharpe_slsqp},
strategy_specs=[
{"name": "Equal Weight", "optimizer": "EW"},
{"name": "MaxSharpe (LedoitWolf, BayesStein)", "optimizer": "MaxSharpe", "cov_model": "LedoitWolf",
"mu_model": "BayesStein"},
],
cov_lookback=756,
mu_lookback=252,
min_cov_observations=755,
min_mu_observations=251,
max_weight=0.35,
trading_cost_bps=cost_bps,
turnover_penalty_bps=10.0,
rf_daily=rf_daily,
annualization=annualization,
)
model_dates = pd.DatetimeIndex(grid.metadata["rebalance_dates"])
w_wasserstein = wasserstein_weight_frame(grid.cache, model_dates, cov_model="EWMA",
mu_model="BayesStein", radius=2.0, mv_lambda=1.0, w_min=0.0,
w_max=0.35)
w_regime = regime_probability_features(close, r, assets=assets, cash_ticker=cash_ticker,
benchmark_ticker=benchmark_ticker, model="GradientBoosting",
horizon=horizon, rebalance_dates=model_dates, output="weights",
max_weight=0.35, n_jobs=outer_n_jobs)
asset_features = build_asset_feature_block(close, volume, r, assets=assets, rf_daily=rf_daily)
cross_features = build_cross_asset_feature_block(close, r, assets=assets, cash_ticker=cash_ticker, benchmark_ticker=benchmark_ticker)
fci_features = build_fci_feature_block(macro_factors, nfci, index=close.index)
p_regime = regime_probability_features(close, r, assets=assets, cash_ticker=cash_ticker,
benchmark_ticker=benchmark_ticker, model="GradientBoosting",
horizon=horizon, rebalance_dates=model_dates, output="features",
n_jobs=outer_n_jobs)
p_regime = p_regime.reindex(close.index.union(p_regime.index)).sort_index().ffill().reindex(close.index)
data = assemble_forecasting_table(target, asset_features, cross_features, fci_features, p_regime)
data = data.replace([np.inf, -np.inf], np.nan).dropna(subset=["y_alpha", "z_21", "sigma_21"])
data = data[data["asset"].isin(assets)].copy()
full_sector_data_dates = data.groupby("date")["asset"].nunique()
data = data[data["date"].isin(full_sector_data_dates[full_sector_data_dates.eq(len(assets))].index)].copy()
data = data.sort_values(["date", "asset"]).reset_index(drop=True)
feature_cols = [c for c in data.columns if c not in {"date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21"}]
initial_stage1 = clean_feature_columns(data, feature_cols, max_missing=0.30, max_abs_corr=0.985)
data, first_model_date, feature_availability = trim_feature_table_by_availability(
data,
initial_stage1,
target_cols=["y_alpha", "sigma_21"],
min_feature_coverage=0.75,
min_asset_count=len(assets),
min_target_complete=1.0,
)
stage1 = clean_feature_columns(data, feature_cols, max_missing=0.30, max_abs_corr=0.985)
for col in stage1:
data[col] = data.groupby("asset")[col].transform(lambda s: s.fillna(s.expanding().median().shift(1)))
data[stage1] = data[stage1].fillna(data.loc[data["date"] <= train_label_cutoff, stage1].median()).fillna(0.0)
train = data[data["date"] <= train_label_cutoff]
rf_screen = RandomForestRegressor(n_estimators=800, max_depth=6, min_samples_leaf=10, max_features="sqrt", random_state=42, n_jobs=-1)
rf_screen.fit(train[stage1], train["y_alpha"])
imp = pd.Series(rf_screen.feature_importances_, index=stage1).sort_values(ascending=False)
scaler_screen = StandardScaler()
enet = ElasticNet(alpha=0.003, l1_ratio=0.25, max_iter=8000, random_state=42)
enet.fit(scaler_screen.fit_transform(train[stage1]), train["y_alpha"])
coef = pd.Series(np.abs(enet.coef_), index=stage1).sort_values(ascending=False)
selected = pd.concat([imp.rank(ascending=False), coef.rank(ascending=False)], axis=1).mean(axis=1).sort_values().head(46).index.tolist()
selected_s_tree = selected[:46]
nn_keywords = ["rank_", "xs_z_", "group_rel_", "r_",
"skip_r", "vol_", "sharpe_", "drawdown_",
"trend_consistency", "autocorr", "skew", "vol_of_vol",
"downside_asym", "corr_", "resid_", "p_risk",
"p_neutral", "p_defensive", "regime_confidence", "fci",
"stress"]
selected_s_nn = [f for f in selected_s_tree if any(k in f for k in nn_keywords)][:30]
selected_s_nn += [f for f in selected_s_tree if f not in selected_s_nn]
selected_s_nn = selected_s_nn[:30]
selected = selected_s_tree
x = data[selected_s_tree]
x_nn = data[selected_s_nn]
y = data["y_alpha"]
row_dates = pd.to_datetime(data["date"])
asset_name = data["asset"]
asset_map = {asset: i for i, asset in enumerate(assets)}
asset_id = asset_name.map(asset_map).astype(int)
wf = pd.DatetimeIndex([d for d in rebalance if d >= validation_start and d in set(row_dates)])
forecast_dates = pd.DatetimeIndex(pd.to_datetime(data.loc[data["date"] >= validation_start, "date"].drop_duplicates())).sort_values()
refit = pd.DatetimeIndex(
pd.Series(forecast_dates, index=forecast_dates)
.groupby(forecast_dates.to_period("Q"))
.first()
.to_list()
)
estimators = {
"z_rf": RandomForestRegressor(n_estimators=800, max_depth=6, min_samples_leaf=10, max_features="sqrt", random_state=42, n_jobs=forest_inner_jobs),
"z_et": ExtraTreesRegressor(n_estimators=900, max_depth=6, min_samples_leaf=10, max_features="sqrt", random_state=42, n_jobs=forest_inner_jobs),
"z_gb": GradientBoostingRegressor(n_estimators=700, learning_rate=0.012, max_depth=2, min_samples_leaf=30, subsample=0.75, random_state=42),
"z_hgb": HistGradientBoostingRegressor(loss="squared_error", max_iter=650, learning_rate=0.018,
max_leaf_nodes=21, min_samples_leaf=30, l2_regularization=0.05,
random_state=42),
"q10_gb": HistGradientBoostingRegressor(loss="quantile", quantile=0.10, max_iter=550, learning_rate=0.018,
max_leaf_nodes=21, min_samples_leaf=30, l2_regularization=0.05,
random_state=42),
"q50_gb": HistGradientBoostingRegressor(loss="quantile", quantile=0.50, max_iter=550, learning_rate=0.018,
max_leaf_nodes=21, min_samples_leaf=30, l2_regularization=0.05,
random_state=42),
"q90_gb": HistGradientBoostingRegressor(loss="quantile", quantile=0.90, max_iter=550, learning_rate=0.018,
max_leaf_nodes=21, min_samples_leaf=30, l2_regularization=0.05,
random_state=42),
}
tab = walkforward_tabular_predictions(x, y, row_dates, asset_name,
refit_dates=refit, prediction_dates=forecast_dates,
estimators=estimators, train_window=TABULAR_TRAIN_WINDOW,
horizon=horizon, n_jobs=outer_n_jobs, inner_threads=forest_inner_jobs,
min_train=500)
scaler = StandardScaler()
train_mask = row_dates <= train_label_cutoff
x_all = pd.DataFrame(scaler.fit_transform(x_nn.loc[train_mask]), index=x_nn.loc[train_mask].index, columns=selected_s_nn)
x_all = pd.DataFrame(scaler.transform(x_nn), index=x_nn.index, columns=selected_s_nn)
seq = data[["date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21"]].join(x_all[selected_s_nn]).copy()
seq["asset_id"] = asset_id
device = auto_device()
lookback = 63
tcn_models = []
for ens_seed in [11, 22, 33]:
torch.manual_seed(ens_seed)
model = TcnForecast(n_features=len(selected_s_nn), n_assets=len(assets), embedding_dim=4,
channels=(48, 48, 48), kernel_size=3, output_size=3, dropout=0.16,
ordered_quantiles=True).to(device)
model, _ = train_torch_model(model, data=seq[seq["date"] <= train_label_cutoff],
features=selected_s_nn, target="y_alpha", asset_col="asset_id",
date_col="date", lookback=lookback, valid_start=validation_start,
valid_end=validation_end, epochs=180, batch_size=256, lr=1e-3,
weight_decay=1e-4, loss_name="pinball", quantiles=[0.10, 0.50, 0.90],
patience=35, device=device)
tcn_models.append(model)
pred = []
for i, model in enumerate(tcn_models):
p = torch_predictions(model, data=seq, features=selected_s_nn, asset_col="asset_id",
date_col="date", lookback=lookback, batch_size=256, device=device)
p.columns = [f"q10_tcn_{i}", f"q50_tcn_{i}", f"q90_tcn_{i}"]
pred.append(p)
pred = pd.concat(pred, axis=1)
q = pd.DataFrame({"q10_tcn": pred.filter(like="q10").mean(axis=1),
"q50_tcn": pred.filter(like="q50").mean(axis=1),
"q90_tcn": pred.filter(like="q90").mean(axis=1),
"d_model": model_disagreement(pred.filter(like="q50"))})
conf = data.loc[q.index, ["date", "asset", "y_alpha"]].join(q).reset_index(names="row_id")
conf_s_oos = conf[conf["date"] >= validation_start]
conf_s_adj = apply_rolling_conformal(conf_s_oos, date_col="date", y_col="y_alpha", low_col="q10_tcn",
high_col="q90_tcn", alpha=0.20, lookback_days=504, gap_days=horizon,
min_obs=126, output_low="q10_c", output_high="q90_c")
q["q10_c"] = q["q10_tcn"].astype("float64")
q["q90_c"] = q["q90_tcn"].astype("float64")
q.loc[conf_s_adj["row_id"].to_numpy(dtype=int), "q10_c"] = conf_s_adj["q10_c"].to_numpy(dtype="float64")
q.loc[conf_s_adj["row_id"].to_numpy(dtype=int), "q90_c"] = conf_s_adj["q90_c"].to_numpy(dtype="float64")
forecast = data[["date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21", "vol_63", "vol_126"]].copy()
forecast = forecast.merge(tab, on=["date", "asset"], how="left").join(q)
base_signal_cols = ["z_rf", "z_et", "z_gb", "z_hgb", "q50_gb", "q50_tcn"]
for col in base_signal_cols:
if col in forecast.columns:
forecast[f"{col}_rank"] = forecast.groupby("date")[col].rank(pct=True) - 0.5
forecast["q50_tcn_rank"] = forecast.groupby("date")["q50_tcn"].rank(pct=True) - 0.5
forecast["mu_hat"] = forecast["q50_tcn_rank"] * forecast["sigma_21"]
forecast["width"] = (forecast["q90_c"] - forecast["q10_c"]) * forecast["sigma_21"]
forecast["c_width"] = forecast_confidence(forecast["mu_hat"], forecast["width"]).values
forecast["c_model"] = disagreement_confidence(forecast["d_model"]).values
forecast["c_total"] = 0.50 + 0.50 * (0.50 * forecast["c_width"] + 0.30 * forecast["c_model"] + 0.20)
forecast["mu_adj"] = forecast["mu_hat"] * forecast["c_total"]
forecast["mu_adj_z"] = forecast["mu_adj"].div(forecast["sigma_21"].replace(0.0, np.nan))
selection_cols = [
col for col in ["z_rf", "z_et", "z_gb", "z_hgb", "q50_gb", "q50_tcn"]
if col in forecast.columns
]
selection_cols += [f"{col}_rank" for col in base_signal_cols if f"{col}_rank" in forecast.columns]
signal_eval = forecast[forecast["date"].between(validation_start, validation_end)].dropna(subset=selection_cols + ["y_alpha"])
if len(signal_eval) >= 100 and selection_cols:
signal_rank = rank_metrics(signal_eval, date_col="date", asset_col="asset", y_col="y_alpha",
prediction_cols=selection_cols)
signal_rank["selection_score"] = (
0.50 * signal_rank["mean_rank_ic"].fillna(0.0)
+ 0.35 * signal_rank["bucket_spread"].fillna(0.0)
+ 0.15 * (signal_rank["top_k_hit_rate"].fillna(0.25) - 0.25)
)
top_sector_signals = signal_rank["selection_score"].sort_values(ascending=False).head(3).index.tolist()
else:
signal_rank = pd.DataFrame()
top_sector_signals = [col for col in ["q50_tcn_rank", "q50_gb", "z_et"] if col in forecast.columns]
final_col = top_sector_signals[0] if top_sector_signals else "q50_tcn_rank"
rank_blend_cols = [col for col in top_sector_signals if col in forecast.columns]
forecast["forecast_blend"] = (forecast.groupby("date")[rank_blend_cols]
.transform(lambda frame: frame.rank(pct=True)).mean(axis=1) - 0.5
if rank_blend_cols else forecast["q50_tcn_rank"])
metric_cols = [c for c in selection_cols + ["forecast_blend", "mu_adj_z"] if c in forecast.columns]
weight_dates = pd.DatetimeIndex([d for d in wf if d >= test_start and d in set(forecast["date"])])
validation_weight_dates = pd.DatetimeIndex([d for d in wf if validation_start <= d <= validation_end and d in set(forecast["date"])])
returns_sector = r[assets + [cash_ticker]].copy()
def kelly_weights(mu_col, *, mu_is_z=True, kelly_fraction=0.60, confidence_cols=None, smooth=0.00):
W = weights_from_forecasts(forecast, date_col="date", asset_col="asset", mu_col=mu_col,
sigma_col="sigma_21", mu_is_z=mu_is_z, returns=returns_sector,
rebalance_dates=weight_dates, kelly_fraction=kelly_fraction, max_weight=0.35,
cash_asset=cash_ticker, confidence_cols=confidence_cols, base_gross=1.0,
min_gross=0.20, max_gross=1.0, top_k=None, cov_model="ledoit_wolf")
if W.empty:
return W
risky = cap_weights(smooth_weights(W.drop(columns=[cash_ticker], errors="ignore"), strength=smooth), max_weight=0.35)
risky = risky.mul(W.drop(columns=[cash_ticker], errors="ignore").sum(axis=1).clip(0.0, 1.0), axis=0)
risky = risky.reindex(columns=assets).fillna(0.0)
risky[cash_ticker] = 1.0 - risky.sum(axis=1)
return risky.reindex(columns=assets + [cash_ticker]).fillna(0.0)
def select_sector_lambda(mu_col, *, mu_is_z=True):
rows = []
if len(validation_weight_dates) < 6:
return 0.10, pd.DataFrame()
for lam in [0.00, 0.05, 0.10, 0.20, 0.30, 0.50, 0.75, 1.00, 1.50]:
W = forecast_gated_maxsharpe_weight_frame(forecast, date_col="date", asset_col="asset",
alpha_col=mu_col, sigma_col="sigma_21", alpha_is_z=mu_is_z,
width_col="width",
confidence_cols=["c_width", "c_model", "c_total"],
returns=returns_sector,
rebalance_dates=validation_weight_dates,
assets=assets + [cash_ticker], cash_asset=cash_ticker,
prior_model="bayes_stein", lambda_alpha=lam, rf_daily=rf_daily,
annualization=annualization, horizon=horizon, max_weight=0.35)
if W.empty:
continue
bt = run_many_weights_backtests({f"lambda={lam:.2f}": W}, returns=returns_sector, cost_bps=cost_bps, rf_daily=rf_daily)
summary = build_strategy_summary(bt, rf_daily=rf_daily, annualization=annualization)
rows.append({"lambda_alpha": lam, "Sharpe": float(summary.iloc[0]["Sharpe"]) if len(summary) else np.nan})
table = pd.DataFrame(rows)
if table.empty or table["Sharpe"].dropna().empty:
return 0.10, table
return float(table.sort_values("Sharpe", ascending=False).iloc[0]["lambda_alpha"]), table
signal_specs = {"RandomForest": {"col": "z_rf", "mu_is_z": True},
"ExtraTrees": {"col": "z_et", "mu_is_z": True},
"GradientBoosting": {"col": "z_gb", "mu_is_z": True},
"HistGradientBoosting": {"col": "z_hgb", "mu_is_z": True},
"QuantileGBM": {"col": "q50_gb", "mu_is_z": True},
"TCNRank": {"col": "q50_tcn_rank", "mu_is_z": True},
"ValidationBlend": {"col": "forecast_blend", "mu_is_z": True},
"UncertaintyAdjusted": {"col": "mu_adj", "mu_is_z": False}}
signal_specs = {
name: spec for name, spec in signal_specs.items()
if spec["col"] in forecast.columns and forecast[spec["col"]].notna().sum() > 100
}
allocator_specs = ["Kelly", "Forecast-Gated MaxSharpe"]
matrix_weights = {}
lambda_rows = []
for signal_name, spec in signal_specs.items():
col = spec["col"]
matrix_weights[(signal_name, "Kelly")] = kelly_weights(
col,
mu_is_z=spec["mu_is_z"],
kelly_fraction=0.60,
confidence_cols=["c_total"] if col == "mu_adj" else None,
smooth=0.00,
)
lam, _ = select_sector_lambda(col, mu_is_z=spec["mu_is_z"])
lambda_rows.append({"signal": signal_name, "selected_lambda_alpha": lam})
matrix_weights[(signal_name, "Forecast-Gated MaxSharpe")] = forecast_gated_maxsharpe_weight_frame(
forecast,
date_col="date",
asset_col="asset",
alpha_col=col,
sigma_col="sigma_21",
alpha_is_z=spec["mu_is_z"],
width_col="width",
confidence_cols=["c_width", "c_model", "c_total"],
returns=returns_sector,
rebalance_dates=weight_dates,
assets=assets + [cash_ticker],
cash_asset=cash_ticker,
prior_model="bayes_stein",
lambda_alpha=lam,
rf_daily=rf_daily,
annualization=annualization,
horizon=horizon,
max_weight=0.35,
)
matrix_backtests = run_many_weights_backtests({f"{sig} | {alloc}": weights for (sig, alloc), weights in matrix_weights.items()},
returns=returns_sector, cost_bps=cost_bps,
rf_daily=rf_daily)
matrix_summary = build_strategy_summary(matrix_backtests, rf_daily=rf_daily, annualization=annualization)
sharpe_matrix = pd.DataFrame(index=signal_specs.keys(), columns=allocator_specs, dtype=float)
for signal_name in signal_specs:
for allocator in allocator_specs:
key = f"{signal_name} | {allocator}"
if key in matrix_summary.index:
sharpe_matrix.loc[signal_name, allocator] = matrix_summary.loc[key, "Sharpe"]
best_sector_signal_by_allocator = sharpe_matrix.idxmax(axis=0).dropna()
selected_sector_model_weights = {
f"{allocator}: {signal_name}": matrix_weights[(signal_name, allocator)]
for allocator, signal_name in best_sector_signal_by_allocator.items()
}
lambda_selection = pd.DataFrame(lambda_rows).set_index("signal") if lambda_rows else pd.DataFrame()
if len(best_sector_signal_by_allocator):
final_col = signal_specs[best_sector_signal_by_allocator.iloc[0]]["col"]
fallback_dates = weight_dates if len(weight_dates) else pd.DatetimeIndex([d for d in model_dates if d >= test_start])
weight_dates = pd.DatetimeIndex(fallback_dates)
benchmark_weights = {
"Equal Weight": align_weight_frame(grid.weights["Equal Weight"], target_dates=weight_dates,
fallback_dates=fallback_dates, assets=assets,
cash_asset=cash_ticker),
"MaxSharpe (LedoitWolf, BayesStein)": align_weight_frame(grid.weights["MaxSharpe (LedoitWolf, BayesStein)"],
target_dates=weight_dates,
fallback_dates=fallback_dates,
assets=assets, cash_asset=cash_ticker),
"Wasserstein DRMV": align_weight_frame(w_wasserstein, target_dates=weight_dates,
fallback_dates=fallback_dates, assets=assets,
cash_asset=cash_ticker),
"ML Regime-Aware GradientBoosting": align_weight_frame(w_regime, target_dates=weight_dates,
fallback_dates=fallback_dates,
assets=assets, cash_asset=cash_ticker),
}
model_weights = {
name: align_weight_frame(weights, target_dates=weight_dates, fallback_dates=fallback_dates, assets=assets, cash_asset=cash_ticker)
for name, weights in selected_sector_model_weights.items()
}
weights_by_strategy = {**benchmark_weights, **model_weights}
w_final = next(iter(model_weights.values())) if model_weights else benchmark_weights["Equal Weight"]
backtests = run_many_weights_backtests(weights_by_strategy, returns=returns_sector, cost_bps=cost_bps, rf_daily=rf_daily)
strategy_summary = (build_strategy_summary(backtests, rf_daily=rf_daily, annualization=annualization)
.sort_values(["Sharpe", "Max Drawdown"], ascending=[False, False]))
returns = pd.DataFrame({name: result.net_returns for name, result in backtests.items()}).dropna(how="all")
nav = pd.DataFrame({name: result.net_values for name, result in backtests.items()}).dropna(how="all")
display(coverage.loc[assets + [cash_ticker]].round(4))
display(pd.Series({"sector_first_common_price_date": first_common_sector_date,
"sector_raw_target_rows": target_s_rows_before_full,
"sector_full_cross_section_target_rows": len(target),
"sector_first_model_date": first_model_date, "sector_rows": len(data)}).to_frame("value"))
display(feature_availability.head(10).round(4))
display(pd.DataFrame({"selected_features_tree": selected_s_tree}))
display(pd.DataFrame({"selected_features_nn": selected_s_nn}))
display(pd.Series({
"sector_validation_signal": final_col,
"sector_selected_allocators": len(best_sector_signal_by_allocator),
"sector_final_allocation": next(iter(model_weights.keys())) if model_weights else "Equal Weight",
}).to_frame("selection"))
if not signal_rank.empty:
display(signal_rank.sort_values("selection_score", ascending=False).round(4))
display(forecast_metrics(forecast[forecast["date"] >= test_start], y_col="y_alpha", prediction_cols=metric_cols).round(4))
display(rank_metrics(forecast[forecast["date"] >= test_start], date_col="date", asset_col="asset",
y_col="y_alpha", prediction_cols=metric_cols).round(4))
display(quantile_metrics(
forecast[forecast["date"] >= test_start].dropna(subset=["q10_gb", "q50_gb", "q90_gb", "q10_c", "q90_c"]),
y_col="y_alpha", quantile_sets={"GBM Quantile": ("q10_gb", "q50_gb", "q90_gb"),
"TCN Ensemble Conformal": ("q10_c", "q50_tcn", "q90_c")}).round(4))
display(lambda_selection.round(4))
display(sharpe_matrix.round(4))
display(matrix_summary.sort_values(["Sharpe", "Max Drawdown"], ascending=[False, False]).head(12).round(4))
display(strategy_summary.round(4))
display(w_final.tail(1).T.rename(columns={w_final.index[-1]: "latest_weight"}).sort_values("latest_weight", ascending=False).round(4))
fig, axes = plt.subplots(4, 4, figsize=(22, 16))
axes = axes.ravel()
target_distribution(axes[0], forecast, raw_col="r_ex_21", scaled_col="y_alpha", title="Sector active target")
feature_importance_bars(axes[1], imp.head(15), title="Sector feature screen")
feature_correlation_map(axes[2], data[selected].corr(), title="Sector feature correlation")
forecast_scatter(axes[3], forecast, y_col="y_alpha", pred_col="q50_tcn", title="TCN active forecast")
forecast_buckets_chart(axes[4], forecast_buckets(forecast.dropna(subset=[final_col]), date_col="date",
y_col="r_ex_21", score_col=final_col), title="Selected forecast buckets")
rolling_ic_chart(axes[5], forecast, date_col="date", asset_col="asset",
y_col="y_alpha", pred_col=final_col, title="Selected forecast rolling IC")
coverage_reliability(axes[6], forecast, y_col="y_alpha", low_col="q10_c", high_col="q90_c", title="Coverage")
uncertainty_error_map(axes[7], forecast, width_col="width", y_col="y_alpha", pred_col="q50_tcn", title="Uncertainty/error")
disagreement_error_map(axes[8], forecast, disagreement_col="d_model", y_col="y_alpha", pred_col="q50_tcn", title="Disagreement/error")
conformal_shift_chart(axes[9], forecast, raw_low="q10_tcn", raw_high="q90_tcn", conf_low="q10_c", conf_high="q90_c", title="Conformal shift")
kelly_weight_path(axes[10], w_final.drop(columns=[cash_ticker], errors="ignore"), title="Final sector weights")
confidence_exposure_map(axes[11], forecast, weight_frame=w_final.drop(columns=[cash_ticker], errors="ignore"), title="Confidence/exposure")
kelly_shrinkage_curve(axes[12], forecast, mu_col="mu_hat", mu_adj_col="mu_adj", title="Kelly shrinkage")
forecast_to_weight_map(axes[13], forecast, weight_frame=w_final.drop(columns=[cash_ticker], errors="ignore"), title="Selected forecast to weight")
plot_strategy_nav(nav, ax=axes[14], title="Sector NAV")
plot_strategy_drawdowns(nav, ax=axes[15], title="Sector drawdowns")
fig.tight_layout()
plt.show()
stress_windows = {"2018 Q4": ("2018-10-01", "2018-12-31"), "COVID crash": ("2020-02-20", "2020-04-30"),
"2022 rates/inflation shock": ("2022-01-03", "2022-10-31"),
"2023 rebound": ("2023-01-01", "2023-12-31")}
available_windows = {
name: window
for name, window in stress_windows.items()
if returns.index.min() <= pd.Timestamp(window[1])
and returns.index.max() >= pd.Timestamp(window[0])
}
risk_sector = risk_report(
objects={name: returns[name].dropna() for name in returns.columns},
market_ret=r[benchmark_ticker].reindex(returns.index).dropna(),
rf_daily=rf_daily,
include={"performance_tables": False, "shape_tables": False, "drawdowns": False, "drawdown_episodes": False,
"var_es": True, "var_backtest": True, "stress": True, "capm": False,
"rolling_beta": False, "correlation": False, "attribution": False, "exec_bullets": False},
stress_settings={"windows": available_windows, "worst_only": False},
output={"display_tables": True, "show_figures": True, "display_table_keys": ["var_es", "var_backtest", "stress"], "short_labels": False},
)