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_s = train_end - pd.tseries.offsets.BDay(horizon)
validation_end_s = train_label_cutoff_s
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)
sector_assets = ["XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY"]
sector_universe_signature = ",".join(sector_assets)
sector_tickers = sector_assets + [cash_ticker, benchmark_ticker]
sector_panels = load_yfinance_panel(
[data_path / "sector_etfs.csv", data_path / "core_cross_asset_etfs.csv"],
fields=("close", "volume"),
tickers=sector_tickers,
source="yfinance_export",
start="2007-01-01",
)
close_s = sector_panels["close"].reindex(columns=sector_tickers).ffill(limit=3)
volume_s = sector_panels["volume"].reindex(index=close_s.index, columns=sector_tickers).ffill(limit=3)
close_s = close_s.dropna(how="all")
volume_s = volume_s.reindex(close_s.index)
first_common_sector_date = close_s[sector_assets + [cash_ticker, benchmark_ticker]].dropna(how="any").index.min()
close_s = close_s.loc[first_common_sector_date:].copy()
volume_s = volume_s.reindex(close_s.index).copy()
coverage_s = pd.DataFrame({
"first_date": close_s[sector_assets + [cash_ticker]].apply(pd.Series.first_valid_index),
"last_date": close_s[sector_assets + [cash_ticker]].apply(pd.Series.last_valid_index),
"observations": close_s[sector_assets + [cash_ticker]].notna().sum(),
"coverage": close_s[sector_assets + [cash_ticker]].notna().mean(),
})
r_s = prices_to_returns_panel(close_s, kind="simple").fillna(0.0)
r_log_s = np.log(close_s / close_s.shift(1))
fwd_s = np.log(close_s[sector_assets].shift(-horizon) / close_s[sector_assets])
rf_forward_log_s = horizon * np.log1p(rf_daily)
r_ex_21_s = fwd_s - rf_forward_log_s
sigma_21_s = r_log_s[sector_assets].rolling(vol_lookback, min_periods=vol_lookback).std(ddof=1) * np.sqrt(horizon)
z_21_s = (r_ex_21_s / sigma_21_s.replace(0.0, np.nan)).clip(-4.0, 4.0)
y_alpha_s = z_21_s.sub(z_21_s.median(axis=1), axis=0).clip(-4.0, 4.0)
rebalance_s = make_rebalance_dates(close_s.index, freq="ME", min_history_days=756)
target_s = (
pd.concat({"r_ex_21": r_ex_21_s, "sigma_21": sigma_21_s, "z_21": z_21_s, "y_alpha": y_alpha_s}, axis=1)
.stack(level=1)
.rename_axis(["date", "asset"])
.reset_index()
.replace([np.inf, -np.inf], np.nan)
.dropna()
)
target_s_counts = target_s.groupby("date")["asset"].nunique()
target_s_rows_before_full = len(target_s)
target_s = target_s[target_s["date"].isin(target_s_counts[target_s_counts.eq(len(sector_assets))].index)].copy()
target_s = target_s.sort_values(["date", "asset"]).reset_index(drop=True)
rebalance_s = pd.DatetimeIndex([d for d in rebalance_s if d <= target_s["date"].max()])
base_s = target_s[target_s["date"].isin(rebalance_s)].copy()
fixed_sector_universe = {
pd.Timestamp(dt): {"tickers": list(sector_assets), "avg_dollar_volume": pd.Series(1.0, index=sector_assets)}
for dt in rebalance_s
}
sector_grid = run_walkforward_grid(
returns=r_s[sector_assets],
close=close_s[sector_assets],
volume=volume_s[sector_assets],
rebalance_dates=rebalance_s,
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,
)
sector_dates = pd.DatetimeIndex(sector_grid.metadata["rebalance_dates"])
w_wasserstein_s = wasserstein_weight_frame(
sector_grid.cache,
sector_dates,
cov_model="EWMA",
mu_model="BayesStein",
radius=2.0,
mv_lambda=1.0,
w_min=0.0,
w_max=0.35,
)
w_regime_s = regime_probability_features(
close_s,
r_s,
assets=sector_assets,
cash_ticker=cash_ticker,
benchmark_ticker=benchmark_ticker,
model="GradientBoosting",
horizon=horizon,
rebalance_dates=sector_dates,
output="weights",
max_weight=0.35,
n_jobs=outer_n_jobs,
)
asset_features_s = build_asset_feature_block(close_s, volume_s, r_s, assets=sector_assets, rf_daily=rf_daily)
cross_features_s = build_cross_asset_feature_block(close_s, r_s, assets=sector_assets, cash_ticker=cash_ticker, benchmark_ticker=benchmark_ticker)
fci_features_s = build_fci_feature_block(macro_factors, nfci, index=close_s.index)
p_regime_s = regime_probability_features(
close_s,
r_s,
assets=sector_assets,
cash_ticker=cash_ticker,
benchmark_ticker=benchmark_ticker,
model="GradientBoosting",
horizon=horizon,
rebalance_dates=sector_dates,
output="features",
n_jobs=outer_n_jobs,
)
p_regime_s = p_regime_s.reindex(close_s.index.union(p_regime_s.index)).sort_index().ffill().reindex(close_s.index)
data_s = assemble_forecasting_table(target_s, asset_features_s, cross_features_s, fci_features_s, p_regime_s)
data_s = data_s.replace([np.inf, -np.inf], np.nan).dropna(subset=["y_alpha", "z_21", "sigma_21"])
data_s = data_s[data_s["asset"].isin(sector_assets)].copy()
full_sector_data_dates = data_s.groupby("date")["asset"].nunique()
data_s = data_s[data_s["date"].isin(full_sector_data_dates[full_sector_data_dates.eq(len(sector_assets))].index)].copy()
data_s = data_s.sort_values(["date", "asset"]).reset_index(drop=True)
feature_cols_s = [c for c in data_s.columns if c not in {"date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21"}]
initial_stage1_s = clean_feature_columns(data_s, feature_cols_s, max_missing=0.30, max_abs_corr=0.985)
data_s, first_model_date_s, sector_feature_availability = trim_feature_table_by_availability(
data_s,
initial_stage1_s,
target_cols=["y_alpha", "sigma_21"],
min_feature_coverage=0.75,
min_asset_count=len(sector_assets),
min_target_complete=1.0,
)
stage1_s = clean_feature_columns(data_s, feature_cols_s, max_missing=0.30, max_abs_corr=0.985)
for col in stage1_s:
data_s[col] = data_s.groupby("asset")[col].transform(lambda s: s.fillna(s.expanding().median().shift(1)))
data_s[stage1_s] = data_s[stage1_s].fillna(data_s.loc[data_s["date"] <= train_label_cutoff_s, stage1_s].median()).fillna(0.0)
train_s = data_s[data_s["date"] <= train_label_cutoff_s]
rf_screen_s = RandomForestRegressor(n_estimators=800, max_depth=6, min_samples_leaf=10, max_features="sqrt", random_state=42, n_jobs=-1)
rf_screen_s.fit(train_s[stage1_s], train_s["y_alpha"])
imp_s = pd.Series(rf_screen_s.feature_importances_, index=stage1_s).sort_values(ascending=False)
scaler_screen_s = StandardScaler()
enet_s = ElasticNet(alpha=0.003, l1_ratio=0.25, max_iter=8000, random_state=42)
enet_s.fit(scaler_screen_s.fit_transform(train_s[stage1_s]), train_s["y_alpha"])
coef_s = pd.Series(np.abs(enet_s.coef_), index=stage1_s).sort_values(ascending=False)
selected_s = pd.concat([imp_s.rank(ascending=False), coef_s.rank(ascending=False)], axis=1).mean(axis=1).sort_values().head(46).index.tolist()
selected_s_tree = selected_s[:46]
sector_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 sector_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_s = selected_s_tree
x_s = data_s[selected_s_tree]
x_nn_s = data_s[selected_s_nn]
y_s = data_s["y_alpha"]
dates_s = pd.to_datetime(data_s["date"])
asset_name_s = data_s["asset"]
asset_map_s = {asset: i for i, asset in enumerate(sector_assets)}
asset_id_s = asset_name_s.map(asset_map_s).astype(int)
wf_s = pd.DatetimeIndex([d for d in rebalance_s if d >= validation_start and d in set(dates_s)])
forecast_dates_s = pd.DatetimeIndex(pd.to_datetime(data_s.loc[data_s["date"] >= validation_start, "date"].drop_duplicates())).sort_values()
refit_s = pd.DatetimeIndex(
pd.Series(forecast_dates_s, index=forecast_dates_s)
.groupby(forecast_dates_s.to_period("Q"))
.first()
.to_list()
)
sector_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_s = walkforward_tabular_predictions(
x_s,
y_s,
dates_s,
asset_name_s,
refit_dates=refit_s,
prediction_dates=forecast_dates_s,
estimators=sector_estimators,
train_window=TABULAR_TRAIN_WINDOW,
horizon=horizon,
n_jobs=outer_n_jobs,
inner_threads=forest_inner_jobs,
min_train=500,
)
scaler_s = StandardScaler()
train_mask_s = dates_s <= train_label_cutoff_s
x_all_s = pd.DataFrame(scaler_s.fit_transform(x_nn_s.loc[train_mask_s]), index=x_nn_s.loc[train_mask_s].index, columns=selected_s_nn)
x_all_s = pd.DataFrame(scaler_s.transform(x_nn_s), index=x_nn_s.index, columns=selected_s_nn)
seq_s = data_s[["date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21"]].join(x_all_s[selected_s_nn]).copy()
seq_s["asset_id"] = asset_id_s
device_s = auto_device()
lookback_s = 63
tcn_models_s = []
for ens_seed in [11, 22, 33]:
torch.manual_seed(ens_seed)
model_s = TcnForecast(
n_features=len(selected_s_nn),
n_assets=len(sector_assets),
embedding_dim=4,
channels=(48, 48, 48),
kernel_size=3,
output_size=3,
dropout=0.16,
ordered_quantiles=True,
).to(device_s)
model_s, _ = train_torch_model(
model_s,
data=seq_s[seq_s["date"] <= train_label_cutoff_s],
features=selected_s_nn,
target="y_alpha",
asset_col="asset_id",
date_col="date",
lookback=lookback_s,
valid_start=validation_start,
valid_end=validation_end_s,
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_s,
)
tcn_models_s.append(model_s)
pred_s = []
for i, model_s in enumerate(tcn_models_s):
p_s = torch_predictions(
model_s,
data=seq_s,
features=selected_s_nn,
asset_col="asset_id",
date_col="date",
lookback=lookback_s,
batch_size=256,
device=device_s,
)
p_s.columns = [f"q10_tcn_{i}", f"q50_tcn_{i}", f"q90_tcn_{i}"]
pred_s.append(p_s)
pred_s = pd.concat(pred_s, axis=1)
q_s = pd.DataFrame({
"q10_tcn": pred_s.filter(like="q10").mean(axis=1),
"q50_tcn": pred_s.filter(like="q50").mean(axis=1),
"q90_tcn": pred_s.filter(like="q90").mean(axis=1),
"d_model": model_disagreement(pred_s.filter(like="q50")),
})
conf_s = data_s.loc[q_s.index, ["date", "asset", "y_alpha"]].join(q_s).reset_index(names="row_id")
conf_s_oos = conf_s[conf_s["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_s["q10_c"] = q_s["q10_tcn"].astype("float64")
q_s["q90_c"] = q_s["q90_tcn"].astype("float64")
q_s.loc[conf_s_adj["row_id"].to_numpy(dtype=int), "q10_c"] = conf_s_adj["q10_c"].to_numpy(dtype="float64")
q_s.loc[conf_s_adj["row_id"].to_numpy(dtype=int), "q90_c"] = conf_s_adj["q90_c"].to_numpy(dtype="float64")
forecast_s = data_s[["date", "asset", "y_alpha", "z_21", "r_ex_21", "sigma_21", "vol_63", "vol_126"]].copy()
forecast_s = forecast_s.merge(tab_s, on=["date", "asset"], how="left").join(q_s)
base_signal_cols_s = ["z_rf", "z_et", "z_gb", "z_hgb", "q50_gb", "q50_tcn"]
for col in base_signal_cols_s:
if col in forecast_s.columns:
forecast_s[f"{col}_rank"] = forecast_s.groupby("date")[col].rank(pct=True) - 0.5
forecast_s["q50_tcn_rank"] = forecast_s.groupby("date")["q50_tcn"].rank(pct=True) - 0.5
forecast_s["mu_hat"] = forecast_s["q50_tcn_rank"] * forecast_s["sigma_21"]
forecast_s["width"] = (forecast_s["q90_c"] - forecast_s["q10_c"]) * forecast_s["sigma_21"]
forecast_s["c_width"] = forecast_confidence(forecast_s["mu_hat"], forecast_s["width"]).values
forecast_s["c_model"] = disagreement_confidence(forecast_s["d_model"]).values
forecast_s["c_total"] = 0.50 + 0.50 * (0.50 * forecast_s["c_width"] + 0.30 * forecast_s["c_model"] + 0.20)
forecast_s["mu_adj"] = forecast_s["mu_hat"] * forecast_s["c_total"]
forecast_s["mu_adj_z"] = forecast_s["mu_adj"].div(forecast_s["sigma_21"].replace(0.0, np.nan))
sector_selection_cols = [
col for col in ["z_rf", "z_et", "z_gb", "z_hgb", "q50_gb", "q50_tcn"]
if col in forecast_s.columns
]
sector_selection_cols += [f"{col}_rank" for col in base_signal_cols_s if f"{col}_rank" in forecast_s.columns]
sector_signal_eval = forecast_s[forecast_s["date"].between(validation_start, validation_end_s)].dropna(subset=sector_selection_cols + ["y_alpha"])
if len(sector_signal_eval) >= 100 and sector_selection_cols:
sector_signal_rank = rank_metrics(
sector_signal_eval,
date_col="date",
asset_col="asset",
y_col="y_alpha",
prediction_cols=sector_selection_cols,
)
sector_signal_rank["selection_score"] = (
0.50 * sector_signal_rank["mean_rank_ic"].fillna(0.0)
+ 0.35 * sector_signal_rank["bucket_spread"].fillna(0.0)
+ 0.15 * (sector_signal_rank["top_k_hit_rate"].fillna(0.25) - 0.25)
)
top_sector_signals = sector_signal_rank["selection_score"].sort_values(ascending=False).head(3).index.tolist()
else:
sector_signal_rank = pd.DataFrame()
top_sector_signals = [col for col in ["q50_tcn_rank", "q50_gb", "z_et"] if col in forecast_s.columns]
sector_final_col = top_sector_signals[0] if top_sector_signals else "q50_tcn_rank"
rank_blend_cols_s = [col for col in top_sector_signals if col in forecast_s.columns]
forecast_s["forecast_blend"] = forecast_s.groupby("date")[rank_blend_cols_s].transform(lambda frame: frame.rank(pct=True)).mean(axis=1) - 0.5 if rank_blend_cols_s else forecast_s["q50_tcn_rank"]
sector_metric_cols = [c for c in sector_selection_cols + ["forecast_blend", "mu_adj_z"] if c in forecast_s.columns]
weight_dates_s = pd.DatetimeIndex([d for d in wf_s if d >= test_start and d in set(forecast_s["date"])])
validation_weight_dates_s = pd.DatetimeIndex([d for d in wf_s if validation_start <= d <= validation_end_s and d in set(forecast_s["date"])])
returns_sector = r_s[sector_assets + [cash_ticker]].copy()
def sector_kelly_weights(mu_col, *, mu_is_z=True, kelly_fraction=0.60, confidence_cols=None, smooth=0.00):
W = weights_from_forecasts(
forecast_s,
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_s,
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=sector_assets).fillna(0.0)
risky[cash_ticker] = 1.0 - risky.sum(axis=1)
return risky.reindex(columns=sector_assets + [cash_ticker]).fillna(0.0)
def select_sector_lambda(mu_col, *, mu_is_z=True):
rows = []
if len(validation_weight_dates_s) < 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_s,
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_s,
assets=sector_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
sector_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},
}
sector_signal_specs = {
name: spec for name, spec in sector_signal_specs.items()
if spec["col"] in forecast_s.columns and forecast_s[spec["col"]].notna().sum() > 100
}
sector_allocator_specs = ["Kelly", "Forecast-Gated MaxSharpe"]
sector_matrix_weights = {}
sector_lambda_rows = []
for signal_name, spec in sector_signal_specs.items():
col = spec["col"]
sector_matrix_weights[(signal_name, "Kelly")] = sector_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"])
sector_lambda_rows.append({"signal": signal_name, "selected_lambda_alpha": lam})
sector_matrix_weights[(signal_name, "Forecast-Gated MaxSharpe")] = forecast_gated_maxsharpe_weight_frame(
forecast_s,
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_s,
assets=sector_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,
)
sector_matrix_backtests = run_many_weights_backtests(
{f"{sig} | {alloc}": weights for (sig, alloc), weights in sector_matrix_weights.items()},
returns=returns_sector,
cost_bps=cost_bps,
rf_daily=rf_daily,
)
sector_matrix_summary = build_strategy_summary(sector_matrix_backtests, rf_daily=rf_daily, annualization=annualization)
sector_sharpe_matrix = pd.DataFrame(index=sector_signal_specs.keys(), columns=sector_allocator_specs, dtype=float)
for signal_name in sector_signal_specs:
for allocator in sector_allocator_specs:
key = f"{signal_name} | {allocator}"
if key in sector_matrix_summary.index:
sector_sharpe_matrix.loc[signal_name, allocator] = sector_matrix_summary.loc[key, "Sharpe"]
best_sector_signal_by_allocator = sector_sharpe_matrix.idxmax(axis=0).dropna()
selected_sector_model_weights = {
f"{allocator}: {signal_name}": sector_matrix_weights[(signal_name, allocator)]
for allocator, signal_name in best_sector_signal_by_allocator.items()
}
sector_lambda_selection = pd.DataFrame(sector_lambda_rows).set_index("signal") if sector_lambda_rows else pd.DataFrame()
if len(best_sector_signal_by_allocator):
sector_final_col = sector_signal_specs[best_sector_signal_by_allocator.iloc[0]]["col"]
sector_fallback_dates = weight_dates_s if len(weight_dates_s) else pd.DatetimeIndex([d for d in sector_dates if d >= test_start])
weight_dates_s = pd.DatetimeIndex(sector_fallback_dates)
benchmark_weights_s = {
"Equal Weight": align_weight_frame(sector_grid.weights["Equal Weight"], target_dates=weight_dates_s, fallback_dates=sector_fallback_dates, assets=sector_assets, cash_asset=cash_ticker),
"MaxSharpe (LedoitWolf, BayesStein)": align_weight_frame(sector_grid.weights["MaxSharpe (LedoitWolf, BayesStein)"], target_dates=weight_dates_s, fallback_dates=sector_fallback_dates, assets=sector_assets, cash_asset=cash_ticker),
"Wasserstein DRMV": align_weight_frame(w_wasserstein_s, target_dates=weight_dates_s, fallback_dates=sector_fallback_dates, assets=sector_assets, cash_asset=cash_ticker),
"ML Regime-Aware GradientBoosting": align_weight_frame(w_regime_s, target_dates=weight_dates_s, fallback_dates=sector_fallback_dates, assets=sector_assets, cash_asset=cash_ticker),
}
sector_model_weights_s = {
name: align_weight_frame(weights, target_dates=weight_dates_s, fallback_dates=sector_fallback_dates, assets=sector_assets, cash_asset=cash_ticker)
for name, weights in selected_sector_model_weights.items()
}
weights_s = {**benchmark_weights_s, **sector_model_weights_s}
w_final_s = next(iter(sector_model_weights_s.values())) if sector_model_weights_s else benchmark_weights_s["Equal Weight"]
backtests_s = run_many_weights_backtests(weights_s, returns=returns_sector, cost_bps=cost_bps, rf_daily=rf_daily)
sector_summary = build_strategy_summary(backtests_s, rf_daily=rf_daily, annualization=annualization).sort_values(["Sharpe", "Max Drawdown"], ascending=[False, False])
sector_returns = pd.DataFrame({name: result.net_returns for name, result in backtests_s.items()}).dropna(how="all")
sector_nav = pd.DataFrame({name: result.net_values for name, result in backtests_s.items()}).dropna(how="all")
display(coverage_s.loc[sector_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_s),
"sector_first_model_date": first_model_date_s,
"sector_rows": len(data_s),
}).to_frame("value"))
display(sector_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": sector_final_col,
"sector_selected_allocators": len(best_sector_signal_by_allocator),
"sector_final_allocation": next(iter(sector_model_weights_s.keys())) if sector_model_weights_s else "Equal Weight",
}).to_frame("selection"))
if not sector_signal_rank.empty:
display(sector_signal_rank.sort_values("selection_score", ascending=False).round(4))
display(forecast_metrics(forecast_s[forecast_s["date"] >= test_start], y_col="y_alpha", prediction_cols=sector_metric_cols).round(4))
display(rank_metrics(forecast_s[forecast_s["date"] >= test_start], date_col="date", asset_col="asset", y_col="y_alpha", prediction_cols=sector_metric_cols).round(4))
display(quantile_metrics(forecast_s[forecast_s["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(sector_lambda_selection.round(4))
display(sector_sharpe_matrix.round(4))
display(sector_matrix_summary.sort_values(["Sharpe", "Max Drawdown"], ascending=[False, False]).head(12).round(4))
display(sector_summary.round(4))
display(w_final_s.tail(1).T.rename(columns={w_final_s.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_s, raw_col="r_ex_21", scaled_col="y_alpha", title="Sector active target")
feature_importance_bars(axes[1], imp_s.head(15), title="Sector feature screen")
feature_correlation_map(axes[2], data_s[selected_s].corr(), title="Sector feature correlation")
forecast_scatter(axes[3], forecast_s, y_col="y_alpha", pred_col="q50_tcn", title="TCN active forecast")
forecast_buckets_chart(axes[4], forecast_buckets(forecast_s.dropna(subset=[sector_final_col]), date_col="date", y_col="r_ex_21", score_col=sector_final_col), title="Selected forecast buckets")
rolling_ic_chart(axes[5], forecast_s, date_col="date", asset_col="asset", y_col="y_alpha", pred_col=sector_final_col, title="Selected forecast rolling IC")
coverage_reliability(axes[6], forecast_s, y_col="y_alpha", low_col="q10_c", high_col="q90_c", title="Coverage")
uncertainty_error_map(axes[7], forecast_s, width_col="width", y_col="y_alpha", pred_col="q50_tcn", title="Uncertainty/error")
disagreement_error_map(axes[8], forecast_s, disagreement_col="d_model", y_col="y_alpha", pred_col="q50_tcn", title="Disagreement/error")
conformal_shift_chart(axes[9], forecast_s, 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_s.drop(columns=[cash_ticker], errors="ignore"), title="Final sector weights")
confidence_exposure_map(axes[11], forecast_s, weight_frame=w_final_s.drop(columns=[cash_ticker], errors="ignore"), title="Confidence/exposure")
kelly_shrinkage_curve(axes[12], forecast_s, mu_col="mu_hat", mu_adj_col="mu_adj", title="Kelly shrinkage")
forecast_to_weight_map(axes[13], forecast_s, weight_frame=w_final_s.drop(columns=[cash_ticker], errors="ignore"), title="Selected forecast to weight")
plot_strategy_nav(sector_nav, ax=axes[14], title="Sector NAV")
plot_strategy_drawdowns(sector_nav, ax=axes[15], title="Sector drawdowns")
fig.tight_layout()
plt.show()
stress_windows_s = {
"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_s = {
name: window
for name, window in stress_windows_s.items()
if sector_returns.index.min() <= pd.Timestamp(window[1])
and sector_returns.index.max() >= pd.Timestamp(window[0])
}
risk_sector = risk_report(
objects={name: sector_returns[name].dropna() for name in sector_returns.columns},
market_ret=r_s[benchmark_ticker].reindex(sector_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_s, "worst_only": False},
output={"display_tables": True, "show_figures": True, "display_table_keys": ["var_es", "var_backtest", "stress"], "short_labels": False},
)