614 lines
21 KiB
Python
614 lines
21 KiB
Python
"""Cross-dataset analytics for case study insights notebooks.
|
|
|
|
Replaces per-notebook JSON-loading patterns with direct registry queries.
|
|
Each function returns a Polars DataFrame ready for analysis and visualization.
|
|
|
|
Usage::
|
|
|
|
from case_studies.utils.analytics import (
|
|
CASE_STUDY_IDS, PRIMARY_LABELS, SHORT_NAMES, DATASET_META,
|
|
load_model_ic, load_best_ic_per_family, load_chapter_backtests,
|
|
)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
import polars as pl
|
|
|
|
from case_studies.utils.backtest_presets import cost_view, strategy_view
|
|
from case_studies.utils.notebook_contracts import degenerate_prediction_sql
|
|
from utils.paths import REPO_ROOT
|
|
|
|
CASE_STUDY_META = {
|
|
"etfs": {"display_name": "ETFs", "chapter_track": "Ch6 to Ch21"},
|
|
"crypto_perps_funding": {
|
|
"display_name": "Crypto Perps Funding",
|
|
"chapter_track": "Ch6 to Ch12",
|
|
},
|
|
"nasdaq100_microstructure": {
|
|
"display_name": "NASDAQ-100 Microstructure",
|
|
"chapter_track": "Ch6 to Ch12",
|
|
},
|
|
"sp500_equity_option_analytics": {
|
|
"display_name": "S&P 500 Equity+Options",
|
|
"chapter_track": "Ch6 to Ch21",
|
|
},
|
|
"us_firm_characteristics": {
|
|
"display_name": "US Firm Characteristics",
|
|
"chapter_track": "Ch6 to Ch14",
|
|
},
|
|
"fx_pairs": {"display_name": "FX Pairs", "chapter_track": "Ch6 to Ch17"},
|
|
"cme_futures": {"display_name": "CME Futures", "chapter_track": "Ch6 to Ch17"},
|
|
"sp500_options": {
|
|
"display_name": "S&P 500 Options",
|
|
"chapter_track": "Ch6 to Ch21",
|
|
},
|
|
"us_equities_panel": {
|
|
"display_name": "US Equities Panel",
|
|
"chapter_track": "Ch6 to Ch14",
|
|
},
|
|
}
|
|
|
|
DISPLAY_NAMES = {k: v["display_name"] for k, v in CASE_STUDY_META.items()}
|
|
CHAPTER_TRACKS = {k: v["chapter_track"] for k, v in CASE_STUDY_META.items()}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Canonical metadata (single source — replaces duplicated dicts in notebooks)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CASE_STUDY_IDS = list(CASE_STUDY_META.keys())
|
|
|
|
PRIMARY_LABELS = {
|
|
"etfs": "fwd_ret_21d",
|
|
"crypto_perps_funding": "fwd_ret_8h",
|
|
"nasdaq100_microstructure": "fwd_ret_15m",
|
|
"sp500_equity_option_analytics": "fwd_ret_5d",
|
|
"us_firm_characteristics": "fwd_ret_1m",
|
|
"fx_pairs": "fwd_ret_1d",
|
|
"cme_futures": "fwd_ret_5d",
|
|
"sp500_options": "ret_to_expiry",
|
|
"us_equities_panel": "fwd_ret_1d",
|
|
}
|
|
|
|
SHORT_NAMES = {
|
|
"etfs": "ETFs",
|
|
"crypto_perps_funding": "Crypto",
|
|
"nasdaq100_microstructure": "NQ100",
|
|
"sp500_equity_option_analytics": "SP500 Eq+Opt",
|
|
"us_firm_characteristics": "US Firms",
|
|
"fx_pairs": "FX",
|
|
"cme_futures": "CME Futures",
|
|
"sp500_options": "SP500 Options",
|
|
"us_equities_panel": "US Equities",
|
|
}
|
|
|
|
DATASET_META = {
|
|
"etfs": {"frequency": "Daily", "entities": 99, "horizon": "21d"},
|
|
"crypto_perps_funding": {"frequency": "8-hourly", "entities": 21, "horizon": "8h"},
|
|
"nasdaq100_microstructure": {"frequency": "15-min", "entities": 114, "horizon": "15m"},
|
|
"sp500_equity_option_analytics": {"frequency": "Daily", "entities": 638, "horizon": "5d"},
|
|
"us_firm_characteristics": {"frequency": "Monthly", "entities": 2483, "horizon": "1m"},
|
|
"fx_pairs": {"frequency": "Daily", "entities": 20, "horizon": "1d"},
|
|
"cme_futures": {"frequency": "Daily", "entities": 30, "horizon": "5d"},
|
|
"sp500_options": {"frequency": "Daily", "entities": 612, "horizon": "dh-10d"},
|
|
"us_equities_panel": {"frequency": "Daily", "entities": 3199, "horizon": "1d"},
|
|
}
|
|
|
|
CADENCE_MAP = {
|
|
"etfs": "monthly",
|
|
"crypto_perps_funding": "8-hourly",
|
|
"nasdaq100_microstructure": "15-min",
|
|
"cme_futures": "monthly",
|
|
"fx_pairs": "daily",
|
|
"sp500_equity_option_analytics": "monthly",
|
|
"sp500_options": "monthly",
|
|
"us_equities_panel": "daily",
|
|
"us_firm_characteristics": "monthly",
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _cs_dir(case_study: str | None = None) -> Path:
|
|
"""Resolve case study root, respecting ML4T_OUTPUT_DIR for test isolation."""
|
|
import os
|
|
|
|
output_dir = os.environ.get("ML4T_OUTPUT_DIR")
|
|
if output_dir:
|
|
base = Path(output_dir)
|
|
if case_study and (base / case_study / "run_log" / "registry.db").exists():
|
|
return base
|
|
return REPO_ROOT / "case_studies"
|
|
|
|
|
|
def _registry_path(case_study: str) -> Path:
|
|
return _cs_dir(case_study) / case_study / "run_log" / "registry.db"
|
|
|
|
|
|
def _query(db_path: Path, sql: str, params: tuple = ()) -> pl.DataFrame:
|
|
"""Execute SQL on a registry and return a Polars DataFrame."""
|
|
if not db_path.exists():
|
|
return pl.DataFrame()
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
rows = conn.execute(sql, params).fetchall()
|
|
if not rows:
|
|
return pl.DataFrame()
|
|
return pl.DataFrame([dict(r) for r in rows], infer_schema_length=None)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Model IC queries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def load_model_ic(
|
|
families: list[str] | str | None = None,
|
|
*,
|
|
split: str = "validation",
|
|
case_studies: list[str] | None = None,
|
|
) -> pl.DataFrame:
|
|
"""Load IC metrics across case studies for specified model families.
|
|
|
|
Returns a DataFrame with columns:
|
|
case_study, family, config_name, label, split,
|
|
checkpoint_value, ic_mean, ic_std
|
|
|
|
Parameters
|
|
----------
|
|
families : str or list of str, optional
|
|
Filter to specific families (e.g. "linear", ["gbm", "deep_learning"]).
|
|
None returns all families.
|
|
split : str
|
|
Prediction split to filter by ("validation" or "holdout").
|
|
case_studies : list of str, optional
|
|
Case studies to query. None = all.
|
|
"""
|
|
if isinstance(families, str):
|
|
families = [families]
|
|
cs_list = case_studies or CASE_STUDY_IDS
|
|
|
|
frames = []
|
|
for cs_id in cs_list:
|
|
db_path = _registry_path(cs_id)
|
|
if not db_path.exists():
|
|
continue
|
|
|
|
family_clause = ""
|
|
params: list = []
|
|
if families:
|
|
placeholders = ",".join("?" * len(families))
|
|
family_clause = f"AND t.family IN ({placeholders})"
|
|
params.extend(families)
|
|
|
|
params.append(split)
|
|
|
|
sql = f"""
|
|
SELECT
|
|
t.family,
|
|
t.config_name,
|
|
t.label,
|
|
p.split,
|
|
p.checkpoint_value,
|
|
p.prediction_hash,
|
|
pm.ic_mean,
|
|
pm.ic_std
|
|
FROM training_runs t
|
|
JOIN prediction_sets p ON t.training_hash = p.training_hash
|
|
JOIN prediction_metrics pm ON p.prediction_hash = pm.prediction_hash
|
|
WHERE 1=1 {family_clause}
|
|
AND p.split = ?
|
|
{degenerate_prediction_sql("p.prediction_hash")}
|
|
ORDER BY pm.ic_mean DESC NULLS LAST
|
|
"""
|
|
df = _query(db_path, sql, tuple(params))
|
|
if len(df) > 0:
|
|
# Cast columns to expected types (handles Null-type columns from empty aggregates)
|
|
col_types = {"checkpoint_value": pl.Int64, "ic_mean": pl.Float64, "ic_std": pl.Float64}
|
|
casts = [
|
|
pl.col(c).cast(t, strict=False) for c, t in col_types.items() if c in df.columns
|
|
]
|
|
if casts:
|
|
df = df.with_columns(casts)
|
|
frames.append(df.with_columns(pl.lit(cs_id).alias("case_study")))
|
|
|
|
if not frames:
|
|
return pl.DataFrame()
|
|
return pl.concat(frames, how="diagonal")
|
|
|
|
|
|
def resolve_best_prediction(
|
|
case_study: str,
|
|
label: str,
|
|
*,
|
|
family: str = "gbm",
|
|
split: str = "validation",
|
|
) -> dict:
|
|
"""Return rank-1 prediction-set metadata for (case_study, label, family).
|
|
|
|
Resolves the best-IC prediction set from `prediction_metrics` (sorted by
|
|
`ic_mean` descending). Useful for downstream notebooks (Ch16/17) that need
|
|
to consume registered upstream predictions without baking in a hash.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Keys: prediction_hash, config_name, ic_mean, ic_std, family, label, split.
|
|
|
|
Raises
|
|
------
|
|
RuntimeError
|
|
If no prediction set matches (case_study, label, family, split).
|
|
"""
|
|
df = (
|
|
load_model_ic([family], split=split, case_studies=[case_study])
|
|
.filter(pl.col("label") == label)
|
|
.filter(pl.col("ic_mean").is_not_null())
|
|
.sort("ic_mean", descending=True)
|
|
)
|
|
if df.is_empty():
|
|
raise RuntimeError(
|
|
f"No {family} predictions with non-null ic_mean for "
|
|
f"{case_study}/{label}/{split} in registry.db. "
|
|
f"Run the {family} training notebook for {case_study} before this notebook."
|
|
)
|
|
return df.row(0, named=True)
|
|
|
|
|
|
def load_classification_metrics(
|
|
families: list[str] | str | None = None,
|
|
*,
|
|
split: str = "validation",
|
|
case_studies: list[str] | None = None,
|
|
) -> pl.DataFrame:
|
|
"""Load classification metrics (AUC-ROC, accuracy, etc.) from registries.
|
|
|
|
Returns a DataFrame with columns:
|
|
case_study, family, config_name, label, split,
|
|
ic_mean, auc_roc, accuracy, balanced_accuracy, log_loss, brier_score, auc_pr
|
|
"""
|
|
if isinstance(families, str):
|
|
families = [families]
|
|
cs_list = case_studies or CASE_STUDY_IDS
|
|
|
|
frames = []
|
|
for cs_id in cs_list:
|
|
db_path = _registry_path(cs_id)
|
|
if not db_path.exists():
|
|
continue
|
|
|
|
family_clause = ""
|
|
params: list = []
|
|
if families:
|
|
placeholders = ",".join("?" * len(families))
|
|
family_clause = f"AND t.family IN ({placeholders})"
|
|
params.extend(families)
|
|
|
|
params.append(split)
|
|
|
|
sql = f"""
|
|
SELECT
|
|
t.family,
|
|
t.config_name,
|
|
t.label,
|
|
p.split,
|
|
pm.ic_mean,
|
|
pm.auc_roc,
|
|
pm.accuracy,
|
|
pm.balanced_accuracy,
|
|
pm.log_loss,
|
|
pm.brier_score,
|
|
pm.auc_pr,
|
|
pm.task_type
|
|
FROM training_runs t
|
|
JOIN prediction_sets p ON t.training_hash = p.training_hash
|
|
JOIN prediction_metrics pm ON p.prediction_hash = pm.prediction_hash
|
|
WHERE 1=1 {family_clause}
|
|
AND p.split = ?
|
|
AND pm.task_type = 'classification'
|
|
ORDER BY pm.auc_roc DESC NULLS LAST
|
|
"""
|
|
df = _query(db_path, sql, tuple(params))
|
|
if len(df) > 0:
|
|
frames.append(df.with_columns(pl.lit(cs_id).alias("case_study")))
|
|
|
|
if not frames:
|
|
return pl.DataFrame()
|
|
return pl.concat(frames, how="diagonal")
|
|
|
|
|
|
def load_best_ic_per_family(
|
|
families: list[str] | None = None,
|
|
*,
|
|
split: str = "validation",
|
|
case_studies: list[str] | None = None,
|
|
use_primary_label: bool = True,
|
|
) -> pl.DataFrame:
|
|
"""Best IC per family per case study (one row per family-case_study pair).
|
|
|
|
Returns: case_study, display_name, family, config_name, label, ic_mean
|
|
"""
|
|
all_ic = load_model_ic(families, split=split, case_studies=case_studies)
|
|
if all_ic.is_empty():
|
|
return pl.DataFrame()
|
|
|
|
if use_primary_label:
|
|
label_rows = [
|
|
{"case_study": cs, "primary_label": lbl} for cs, lbl in PRIMARY_LABELS.items()
|
|
]
|
|
label_df = pl.DataFrame(label_rows)
|
|
all_ic = all_ic.join(label_df, on="case_study").filter(
|
|
pl.col("label") == pl.col("primary_label")
|
|
)
|
|
|
|
best = (
|
|
all_ic.sort("ic_mean", descending=True, nulls_last=True)
|
|
.group_by(["case_study", "family"])
|
|
.first()
|
|
.select("case_study", "family", "config_name", "label", "ic_mean")
|
|
)
|
|
|
|
# Add display names
|
|
name_rows = [{"case_study": k, "display_name": v} for k, v in SHORT_NAMES.items()]
|
|
name_df = pl.DataFrame(name_rows)
|
|
best = best.join(name_df, on="case_study", how="left")
|
|
|
|
return best.sort(["case_study", "family"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backtest queries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_CHAPTER_TO_STAGE = {
|
|
"ch16": "signal",
|
|
"ch17": "allocation",
|
|
"ch18": "cost_sensitivity",
|
|
"ch19": "risk_overlay",
|
|
}
|
|
|
|
|
|
def load_chapter_backtests(
|
|
chapter: str,
|
|
*,
|
|
stage: str | None = None,
|
|
case_studies: list[str] | None = None,
|
|
metrics: list[str] | None = None,
|
|
) -> pl.DataFrame:
|
|
"""Load backtest results for a pipeline stage across case studies.
|
|
|
|
Returns a DataFrame with columns:
|
|
case_study, display_name, backtest_hash, prediction_hash,
|
|
family, config_name, label, spec_json,
|
|
plus one column per metric (sharpe, sortino, etc.)
|
|
|
|
Parameters
|
|
----------
|
|
chapter : str
|
|
Legacy chapter tag (e.g. "ch16"). Mapped to stage automatically.
|
|
stage : str, optional
|
|
Pipeline stage to filter by. Overrides chapter mapping.
|
|
case_studies : list of str, optional
|
|
Case studies to query. None = all.
|
|
metrics : list of str, optional
|
|
Metrics to pivot. None = all available.
|
|
"""
|
|
cs_list = case_studies or CASE_STUDY_IDS
|
|
resolved_stage = stage or _CHAPTER_TO_STAGE.get(chapter, chapter)
|
|
|
|
frames = []
|
|
for cs_id in cs_list:
|
|
db_path = _registry_path(cs_id)
|
|
if not db_path.exists():
|
|
continue
|
|
|
|
# Prefer stage column; fall back to spec_json LIKE for un-migrated DBs
|
|
sql = """
|
|
SELECT
|
|
b.backtest_hash,
|
|
b.prediction_hash,
|
|
b.spec_json,
|
|
t.family,
|
|
t.config_name,
|
|
t.label,
|
|
bm.*
|
|
FROM backtest_runs b
|
|
JOIN backtest_metrics bm ON b.backtest_hash = bm.backtest_hash
|
|
JOIN prediction_sets p ON b.prediction_hash = p.prediction_hash
|
|
JOIN training_runs t ON p.training_hash = t.training_hash
|
|
WHERE b.stage = ?
|
|
"""
|
|
df = _query(db_path, sql, (resolved_stage,))
|
|
if len(df) > 0:
|
|
frames.append(df.with_columns(pl.lit(cs_id).alias("case_study")))
|
|
|
|
if not frames:
|
|
return pl.DataFrame()
|
|
|
|
result = pl.concat(frames, how="diagonal_relaxed")
|
|
|
|
# Filter to requested metrics (select only those columns)
|
|
if metrics:
|
|
# Keep meta columns + requested metric columns
|
|
meta_cols = [
|
|
"case_study",
|
|
"backtest_hash",
|
|
"prediction_hash",
|
|
"spec_json",
|
|
"family",
|
|
"config_name",
|
|
"label",
|
|
]
|
|
keep = [c for c in meta_cols if c in result.columns]
|
|
keep += [c for c in metrics if c in result.columns]
|
|
result = result.select(keep)
|
|
|
|
# Drop internal columns from bm.* join
|
|
drop_cols = [c for c in ["computed_at"] if c in result.columns]
|
|
if drop_cols:
|
|
result = result.drop(drop_cols)
|
|
|
|
# Add display names
|
|
name_rows = [{"case_study": k, "display_name": v} for k, v in SHORT_NAMES.items()]
|
|
name_df = pl.DataFrame(name_rows)
|
|
result = result.join(name_df, on="case_study", how="left")
|
|
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Spec parsing helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parse_backtest_spec(spec_json: str) -> dict:
|
|
"""Parse a backtest spec_json string into a dict."""
|
|
return json.loads(spec_json)
|
|
|
|
|
|
def extract_cost_bps(spec_json: str) -> float:
|
|
"""Extract total cost (commission + slippage) in bps from a backtest spec."""
|
|
spec = parse_backtest_spec(spec_json)
|
|
costs = cost_view(spec)
|
|
return float(costs.get("commission_bps", 0.0)) + float(costs.get("slippage_bps", 0.0))
|
|
|
|
|
|
def extract_allocator(spec_json: str) -> str:
|
|
"""Extract allocation method from a backtest spec."""
|
|
spec = parse_backtest_spec(spec_json)
|
|
allocation = strategy_view(spec).get("allocation") or {}
|
|
return allocation.get("method", "unknown")
|
|
|
|
|
|
# nasdaq100_microstructure deploys the cost-feasible *ensemble* carrier
|
|
# (developed in 20.4). resolve_canonical_rank1_lineage returns the
|
|
# non-deployed full-universe val-max gbm instead, so the ensemble's training
|
|
# hash and validation backtest are pinned here. See agents
|
|
# UNCERTAINTY_ARCHITECTURE / Ch20 audit.
|
|
_NASDAQ_ENSEMBLE_TRAINING_HASH = "a9f04b886b9a"
|
|
_NASDAQ_ENSEMBLE_VAL_HASH = "4e939dee0a5f"
|
|
|
|
|
|
def _strategy_signature(spec_json: str) -> str:
|
|
"""Identity of a backtest's signal + allocation config, ignoring cost.
|
|
|
|
The cost sweep holds the strategy fixed and varies only commission +
|
|
slippage, so every cost-grid run for one carrier shares this signature.
|
|
A single training hash can host more than one carrier (e.g. a signal-only
|
|
eq-weight series alongside an allocator series); matching the signature
|
|
selects the deployed one rather than a sibling.
|
|
"""
|
|
sv = strategy_view(parse_backtest_spec(spec_json))
|
|
return json.dumps([sv.get("signal"), sv.get("allocation")], sort_keys=True)
|
|
|
|
|
|
def load_carrier_cost_curves(case_studies: list[str] | None = None) -> pl.DataFrame:
|
|
"""Cost-sensitivity curves for each case study's *deployed carrier*.
|
|
|
|
The carrier is the highest-validation-Sharpe configuration across the
|
|
signal, allocation, and risk-overlay stages, resolved via
|
|
``resolve_canonical_rank1_lineage``. The cost sweep (Ch18) holds that
|
|
carrier's signal and allocation fixed while varying commission +
|
|
slippage, so the breakeven implied here is the carrier's own cost
|
|
survival — not that of whichever allocator happened to be best at zero
|
|
cost (which need not be the deployed strategy). Rows are matched to the
|
|
carrier's exact strategy signature, so a sibling series sharing the
|
|
training hash (e.g. a signal-only eq-weight run) is excluded.
|
|
|
|
Returns tidy rows ``[case_study, display_name, cadence, label,
|
|
allocator, cost_bps, sharpe, total_return, max_drawdown]`` on the
|
|
validation split, sorted by ``cost_bps`` within each case study.
|
|
"""
|
|
# Lazy import avoids a module-load cycle (strategy_analysis is heavier).
|
|
from case_studies.utils.strategy_analysis import resolve_canonical_rank1_lineage
|
|
|
|
cs_list = case_studies or CASE_STUDY_IDS
|
|
cost_sql = """
|
|
SELECT b.spec_json, t.label, bm.sharpe, bm.total_return, bm.max_drawdown
|
|
FROM backtest_runs b
|
|
JOIN backtest_metrics bm ON b.backtest_hash = bm.backtest_hash
|
|
JOIN prediction_sets p ON b.prediction_hash = p.prediction_hash
|
|
JOIN training_runs t ON p.training_hash = t.training_hash
|
|
WHERE b.stage = 'cost_sensitivity' AND p.split = 'validation'
|
|
AND p.training_hash = ?
|
|
"""
|
|
|
|
frames = []
|
|
for cs_id in cs_list:
|
|
db_path = _registry_path(cs_id)
|
|
if not db_path.exists():
|
|
continue
|
|
|
|
# Resolve the deployed carrier's training hash and its strategy spec.
|
|
if cs_id == "nasdaq100_microstructure":
|
|
training_hash = _NASDAQ_ENSEMBLE_TRAINING_HASH
|
|
spec_df = _query(
|
|
db_path,
|
|
"SELECT spec_json FROM backtest_runs WHERE backtest_hash LIKE ?",
|
|
(_NASDAQ_ENSEMBLE_VAL_HASH + "%",),
|
|
)
|
|
else:
|
|
try:
|
|
lin = resolve_canonical_rank1_lineage(cs_id)
|
|
except Exception:
|
|
continue
|
|
training_hash = lin.get("training_hash")
|
|
spec_df = _query(
|
|
db_path,
|
|
"SELECT spec_json FROM backtest_runs WHERE backtest_hash = ?",
|
|
(lin.get("val_backtest_hash"),),
|
|
)
|
|
if not training_hash or spec_df.is_empty():
|
|
continue
|
|
carrier_sig = _strategy_signature(spec_df["spec_json"][0])
|
|
|
|
df = _query(db_path, cost_sql, (training_hash,))
|
|
if df.is_empty():
|
|
continue
|
|
df = df.with_columns(
|
|
cost_bps=pl.col("spec_json").map_elements(extract_cost_bps, return_dtype=pl.Float64),
|
|
allocator=pl.col("spec_json").map_elements(extract_allocator, return_dtype=pl.Utf8),
|
|
signature=pl.col("spec_json").map_elements(_strategy_signature, return_dtype=pl.Utf8),
|
|
case_study=pl.lit(cs_id),
|
|
).filter(pl.col("signature") == carrier_sig)
|
|
if df.is_empty():
|
|
continue
|
|
frames.append(
|
|
df.select(
|
|
"case_study",
|
|
"label",
|
|
"allocator",
|
|
"cost_bps",
|
|
"sharpe",
|
|
"total_return",
|
|
"max_drawdown",
|
|
)
|
|
)
|
|
|
|
if not frames:
|
|
return pl.DataFrame()
|
|
|
|
result = pl.concat(frames, how="diagonal_relaxed")
|
|
# A signature-matched carrier should be one row per cost level, but guard
|
|
# against any residual duplicate deterministically (keep best Sharpe).
|
|
result = result.sort(["case_study", "cost_bps", "sharpe"]).unique(
|
|
subset=["case_study", "cost_bps"], keep="last", maintain_order=True
|
|
)
|
|
name_df = pl.DataFrame([{"case_study": k, "display_name": v} for k, v in SHORT_NAMES.items()])
|
|
return (
|
|
result.join(name_df, on="case_study", how="left")
|
|
.with_columns(cadence=pl.col("case_study").replace(CADENCE_MAP))
|
|
.sort("case_study", "cost_bps")
|
|
)
|