Pular para o conteúdo
Todos os documentos da biblioteca

Sobrevivência da estratégia diante dos custos de negociação

Código Machine Learning for Trading

Resumo

Este notebook compara o Sharpe bruto e líquido em varreduras de custos de estratégia derivadas de backtests de estudos de caso. Para cada configuração de estratégia usada nos backtests, ele lê dos artefatos registrados o custo presumido por perna e a frequência de rebalanceamento, interpola o Sharpe no custo presumido e estima o custo em que o Sharpe cruza zero. Se a varredura mantiver Sharpe positivo em seu limite superior, o ponto de equilíbrio é tratado como censurado, em vez de ser relatado como um cruzamento observado. Os agrupamentos por frequência usam multiplicadores explícitos de turnover, que o notebook identifica como pressupostos, não como turnover medido.

A análise ajuda a identificar como os custos de fricção alteram o desempenho aparente de uma estratégia e se ela continua positiva sob seu próprio pressuposto de custo. Entre suas limitações estão varreduras de custos disponíveis incompletas, custos proporcionais em pontos-base que podem não representar spreads ou outras fricções específicas do instrumento, interpolação entre pontos da grade e Sharpe dos folds de validação para configurações selecionadas com dados de validação. Portanto, os gráficos e conclusões descrevem as varreduras modeladas disponíveis, não um desempenho líquido futuro garantido em operações reais. Uma avaliação realista pode exigir testes executáveis com cotações, especialmente quando os spreads de compra e venda predominam.

Ideias principais

  • Compare o Sharpe bruto com o Sharpe no custo de negociação declarado pelo estudo de caso para medir o impacto dos custos.
  • Interpole o Sharpe líquido entre pontos vizinhos da varredura e informe o intervalo da grade.
  • Trate uma estratégia ainda lucrativa no teto da varredura como tendo um ponto de equilíbrio não observado acima desse teto.
  • Os multiplicadores de turnover por frequência são pressupostos e não devem ser interpretados como turnover medido.
  • Uma varredura proporcional em pontos-base pode não captar spreads e custos específicos do instrumento, enquanto a seleção na validação pode distorcer a margem disponível até o ponto de equilíbrio.

Tags

Texto completo
# 06_cost_survival.py


```py
# ---
# jupyter:
#   jupytext:
#     cell_metadata_filter: tags,-all
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.19.3
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---

# %% [markdown]
# # Friction Survival: Where the Edge Dies
#
# **Docker image**: `ml4t`
#
# This notebook cross-cuts the case studies by **failure mode**: gross-to-net
# Sharpe degradation, breakeven cost thresholds, cadence-frequency vulnerability,
# and cost-model realism caveats. Ch18 establishes the cost taxonomy and the
# per-asset-class machinery; this notebook reads the resulting Ch18 cost-sweep
# backtests directly out of each case study's registry and asks which
# strategies survive the friction of real trading.
#
# **Learning Objectives**:
# - Quantify each strategy's sensitivity to per-leg friction on a common scale
# - Identify breakeven cost thresholds per rebalance cadence
# - Recognize when a generic basis-point sweep is the wrong cost model
#   (single-name options, intraday equities)
#
# **Book Reference**: Chapter 20, Section 20.6 (Trading Realism)
#
# **Prerequisites**: Run [`01_aggregate_synthesis`](01_aggregate_synthesis.ipynb) first.
# Each case study's registry must contain Ch18 `cost_sensitivity`-stage backtests.

# %%
"""Ch20 Friction Survival — cross-case-study cost-sweep analysis from registry."""

import json

import matplotlib.pyplot as plt
import polars as pl
from IPython.display import Markdown, display
from matplotlib.patches import Patch

from case_studies.utils.analytics import (
    CASE_STUDY_IDS,
    SHORT_NAMES,
    load_carrier_cost_curves,
)
from utils.paths import get_chapter_dir
from utils.style import show_with_alt

pl.Config.set_tbl_rows(20)

# %% tags=["parameters"]
# 0 = all
MAX_CASE_STUDIES = 0

# %%
CS_LIST = CASE_STUDY_IDS[:MAX_CASE_STUDIES] if MAX_CASE_STUDIES else CASE_STUDY_IDS

# %% [markdown]
# ## Load Cost Sweep Results from Registry
#
# Ch18 backtests vary commission + slippage across a grid of cost levels
# while holding the signal and allocation constant. We read the sweep for
# each case study's **release configuration** -- the one declared across
# the signal, allocation, and risk-overlay stages --
# so the breakeven measured here is the cost survival of the strategy the
# chapter actually deploys, not of whichever allocator happened to be best
# at zero cost.
#
# A case study can hold cost-sensitivity backtests and still draw no curve here, because
# the sweep has to sit on the carrier's own training lineage *and* run the carrier's own
# strategy. The loader reports which check dropped each one, printed below the load, so an
# absence from the charts can be read rather than guessed at.

# %%
loaded = load_carrier_cost_curves(CS_LIST)
costs_df = loaded.curves
_exclusions = loaded.exclusion_lines()

if costs_df.is_empty():
    # The reasons go into the refusal rather than after it. Every case study being excluded is
    # the state that most needs them - a clean clone with no registries reaches it - and the
    # loader has already established each one.
    msg = "No Ch18 cost-sensitivity backtests found for any deployed carrier"
    raise RuntimeError("\n".join([msg, *_exclusions]) if _exclusions else msg)

n_cs = costs_df["case_study"].n_unique()
print(f"Loaded {len(costs_df)} carrier cost-sweep entries across {n_cs} case studies")
for _line in _exclusions:
    print(_line)
costs_df.head(5)

# %% [markdown]
# Assumed per-leg cost and rebalance cadence both come from each case study's
# `setup.yaml`, read back through the Ch20 artifacts that `01_aggregate_synthesis`
# writes, rather than being typed into this notebook.
#
# `setup.yaml` records a specific cadence such as `monthly_month_end` or
# `daily_ny_close`. The charts group by period, taken from the leading word, and
# anything unrecognised is grouped as unspecified rather than given a default.
# The turnover multipliers attached to each period say how often a book of that
# cadence is assumed to turn over relative to a daily one. They are an
# assumption, not turnover measured from the backtests.
#
# ## Gross-to-Net Sharpe Degradation
#
# For each case study we compare the zero-cost (gross) Sharpe with the Sharpe at
# the cost that case study actually assumes, which comes from its `setup.yaml`
# by way of `overview.parquet`. The assumed cost rarely falls on a grid point, so
# the net Sharpe is interpolated linearly between the two grid points that
# bracket it, and the bracketing points are reported alongside.
#

# %%
gross_df = costs_df.filter(pl.col("cost_bps") == 0)
net_df = costs_df.filter(pl.col("cost_bps") > 0)

# One allocator per case study (the selected configuration's); this selects it.
best_alloc = (
    gross_df.sort("sharpe", descending=True)
    .unique(subset=["case_study"], keep="first")
    .select("case_study", "allocator")
)

# %%
_overview = pl.read_parquet(get_chapter_dir(20) / "output" / "overview.parquet")
ASSUMED_COST_BPS = dict(_overview.select("cs_id", "cost_bps").iter_rows())
_synthesis = json.loads((get_chapter_dir(20) / "output" / "all_synthesis.json").read_text())
CADENCE_BY_CS = {
    cs: (data["meta"].get("cadence") or "unspecified") for cs, data in _synthesis.items()
}

CADENCE_PERIODS = ("15min", "hourly", "8_hour", "daily", "weekly", "monthly")
TURNOVER_MULTIPLIER = {
    "15min": 26.0,
    "hourly": 6.5,
    "8_hour": 3.0,
    "daily": 1.0,
    "weekly": 1.0 / 5,
    "monthly": 1.0 / 21,
}


def _cadence_period(cadence: str) -> str:
    for period in CADENCE_PERIODS:
        if cadence.startswith(period):
            return period
    return "unspecified"


def _sharpe_at(curve: pl.DataFrame, cost_bps: float) -> tuple[float, float, float]:
    """Sharpe at `cost_bps`, linearly interpolated on the sweep grid.

    Returns the interpolated Sharpe and the two grid costs it sits between. A
    cost beyond either end of the grid is clamped to that end, which is reported
    by the bracket coming back equal.
    """
    grid = curve["cost_bps"].to_list()
    vals = curve["sharpe"].to_list()
    if cost_bps <= grid[0]:
        return vals[0], grid[0], grid[0]
    if cost_bps >= grid[-1]:
        return vals[-1], grid[-1], grid[-1]
    for lo, hi, v_lo, v_hi in zip(grid, grid[1:], vals, vals[1:], strict=False):
        if lo <= cost_bps <= hi:
            w = 0.0 if hi == lo else (cost_bps - lo) / (hi - lo)
            return v_lo + w * (v_hi - v_lo), lo, hi
    return vals[-1], grid[-1], grid[-1]


def _breakeven(curve: pl.DataFrame) -> tuple[float, bool]:
    """Cost at which Sharpe crosses zero, and whether that crossing was observed.

    Interpolates between the last positive grid point and the first negative one.
    A curve still positive at the top of the grid is censored: the breakeven is
    somewhere above the ceiling and the ceiling is not it.
    """
    grid = curve["cost_bps"].to_list()
    vals = curve["sharpe"].to_list()
    for lo, hi, v_lo, v_hi in zip(grid, grid[1:], vals, vals[1:], strict=False):
        if v_lo > 0 >= v_hi:
            w = v_lo / (v_lo - v_hi)
            return lo + w * (hi - lo), True
    return (grid[-1], False) if vals[-1] > 0 else (0.0, True)


summary_rows = []
for row in best_alloc.iter_rows(named=True):
    cs = row["case_study"]
    alloc = row["allocator"]
    curve = costs_df.filter((pl.col("case_study") == cs) & (pl.col("allocator") == alloc)).sort(
        "cost_bps"
    )
    if curve.height < 2 or curve["cost_bps"].min() > 0:
        continue

    gross_sharpe = curve["sharpe"][0]
    assumed_cost = ASSUMED_COST_BPS.get(cs)
    if assumed_cost is None:
        msg = f"No cost_bps for {cs} in overview.parquet; re-run 01_aggregate_synthesis"
        raise RuntimeError(msg)
    net_sharpe, bracket_lo, bracket_hi = _sharpe_at(curve, assumed_cost)
    breakeven_bps, breakeven_observed = _breakeven(curve)

    summary_rows.append(
        {
            "case_study": cs,
            "display_name": SHORT_NAMES.get(cs, cs),
            "cadence": CADENCE_BY_CS.get(cs, "unspecified"),
            "cadence_period": _cadence_period(CADENCE_BY_CS.get(cs, "unspecified")),
            "allocator": alloc,
            "gross_sharpe": round(gross_sharpe, 3),
            "net_sharpe": round(net_sharpe, 3),
            "sharpe_drag": round(gross_sharpe - net_sharpe, 3),
            "drag_pct": round(100 * (gross_sharpe - net_sharpe) / gross_sharpe, 1)
            if gross_sharpe != 0
            else 0.0,
            "assumed_cost_bps": assumed_cost,
            "grid_bracket": f"{bracket_lo:g}-{bracket_hi:g}",
            "breakeven_bps": round(breakeven_bps, 1),
            "breakeven_observed": breakeven_observed,
            "survives": net_sharpe > 0,
        }
    )

# %%
summary = pl.DataFrame(summary_rows).sort("drag_pct", descending=True)
print("=== Gross-to-Net Sharpe Degradation ===")
summary.select(
    "display_name",
    "cadence",
    "gross_sharpe",
    "assumed_cost_bps",
    "grid_bracket",
    "net_sharpe",
    "sharpe_drag",
    "drag_pct",
    "breakeven_bps",
    "breakeven_observed",
    "survives",
)

# %% tags=["results"]
_dead = summary.filter(~pl.col("survives"))
_censored = summary.filter(~pl.col("breakeven_observed"))
display(
    Markdown(
        f"{summary.height} case studies have a carrier cost sweep. "
        + (
            f"**{', '.join(_dead['display_name'].to_list())}** "
            f"{'has' if _dead.height == 1 else 'have'} a negative Sharpe at the "
            "cost the case study assumes, so the strategy does not survive its "
            "own cost model. "
            if _dead.height
            else "All of them keep a positive Sharpe at the cost they assume. "
        )
        + f"Cost consumes between {summary['drag_pct'].min():.1f} and "
        f"{summary['drag_pct'].max():.1f} percent of gross Sharpe.\n\n"
        + (
            f"For {', '.join(_censored['display_name'].to_list())} the Sharpe is "
            f"still positive at the top of the swept grid, so the breakeven "
            "column is a lower bound rather than a measurement: it is at least "
            "that, and the grid does not say how much more."
            if _censored.height
            else "Every breakeven was observed inside the swept grid."
        )
    )
)

# %% [markdown]
# ## Cost Drag Visualization
#
# The horizontal bar chart shows Sharpe drag (gross minus net) for each
# case study, ordered by severity. Higher-frequency strategies typically
# suffer more because they accumulate turnover costs faster.

# %%
fig, ax = plt.subplots(figsize=(10, 6))

colors = [
    "#d62728" if drag > 50 else "#ff7f0e" if drag > 20 else "#2ca02c"
    for drag in summary["drag_pct"]
]

bars = ax.barh(
    range(len(summary)),
    summary["drag_pct"].to_list(),
    color=colors,
    edgecolor="none",
    height=0.6,
)
ax.set_yticks(range(len(summary)))
ax.set_yticklabels(summary["display_name"].to_list())
ax.set_xlabel("Sharpe Drag (%)")
ax.set_title("Cost Impact: Gross-to-Net Sharpe Degradation")
ax.invert_yaxis()

for bar, row in zip(bars, summary.iter_rows(named=True), strict=False):
    ax.annotate(
        f"BE: {'' if row['breakeven_observed'] else '>'}{row['breakeven_bps']:g} bps",
        xy=(bar.get_width() + 1, bar.get_y() + bar.get_height() / 2),
        va="center",
        fontsize=9,
        color="gray",
    )
# Headroom so the breakeven annotation on the widest bar (FX) is not clipped.
ax.set_xlim(right=max(summary["drag_pct"]) * 1.28)

show_with_alt(
    fig,
    "Horizontal bars giving the percentage of gross Sharpe consumed by each case "
    "study's assumed cost, ordered by severity, each annotated with the cost at "
    "which that strategy breaks even.",
)

# %% [markdown]
# ## Breakeven Cost Thresholds by Frequency
#
# Breakeven cost is the maximum per-leg cost (in bps) at which the
# deployed configuration still produces a positive Sharpe ratio. It is the cost
# budget that the signal supports before becoming unprofitable.

# %%
freq_order = list(CADENCE_PERIODS)
freq_colors = {
    "15min": "#d62728",
    "hourly": "#e8833a",
    "8_hour": "#ff7f0e",
    "daily": "#1f77b4",
    "weekly": "#5aa469",
    "monthly": "#2ca02c",
}

fig, ax = plt.subplots(figsize=(10, 5))

for i, row in enumerate(summary.sort("breakeven_bps").iter_rows(named=True)):
    color = freq_colors.get(row["cadence_period"], "gray")
    ax.barh(i, row["breakeven_bps"], color=color, height=0.6, edgecolor="none")

ax.set_yticks(range(len(summary)))
sorted_names = summary.sort("breakeven_bps")["display_name"].to_list()
ax.set_yticklabels(sorted_names)
ax.set_xlabel("Breakeven Cost (bps per leg)")
ax.set_title("Breakeven Cost Thresholds — Higher Is More Robust")

legend_handles = [Patch(facecolor=freq_colors[f], label=f) for f in freq_order if f in freq_colors]
ax.legend(handles=legend_handles, loc="lower right", title="Cadence")

show_with_alt(
    fig,
    "Horizontal bars of the breakeven per-leg cost for each case study, ordered "
    "from lowest to highest and coloured by rebalance cadence.",
)

# %% [markdown]
# ## Cost Drag Curves
#
# For each case study, plot Sharpe ratio as a function of per-leg
# cost. This reveals the "cost cliff" — the point where a profitable
# strategy becomes unprofitable.

# %%
best_alloc_map = dict(
    zip(best_alloc["case_study"].to_list(), best_alloc["allocator"].to_list(), strict=False)
)

fig, ax = plt.subplots(figsize=(12, 7))

for cs_id in CS_LIST:
    alloc = best_alloc_map.get(cs_id)
    if alloc is None:
        continue

    cs_data = costs_df.filter(
        (pl.col("case_study") == cs_id) & (pl.col("allocator") == alloc)
    ).sort("cost_bps")

    if cs_data.is_empty():
        continue

    ax.plot(
        cs_data["cost_bps"].to_list(),
        cs_data["sharpe"].to_list(),
        marker="o",
        markersize=4,
        label=SHORT_NAMES.get(cs_id, cs_id),
    )

ax.axhline(y=0, color="black", linestyle="--", alpha=0.3, linewidth=0.8)
ax.set_xlabel("Per-Leg Cost (bps)")
ax.set_ylabel("Sharpe Ratio")
ax.set_title("Cost sensitivity: Sharpe against per-leg cost")
ax.legend(loc="upper right", fontsize=9, ncol=2)

# Mark each case study's assumed cost so the curve can be read at the point that
# matters rather than across the whole grid.
for cs_id in best_alloc_map:
    _c = ASSUMED_COST_BPS.get(cs_id)
    if _c is not None:
        ax.axvline(_c, color="gray", alpha=0.25, linewidth=0.8, linestyle=":")

show_with_alt(
    fig,
    "Line chart of Sharpe against per-leg cost in basis points, one line per "
    "case study over the swept grid, with a reference line at zero Sharpe and "
    "faint vertical lines marking each case study's assumed cost.",
)

# %% [markdown]
# ## Cost Survival Classification
#
# Each case study is classified by cost resilience: the ratio of its breakeven to
# the per-leg cost it is assumed to pay. A higher ratio means more headroom once
# realistic frictions are imposed. A ratio below one means the breakeven sits
# under the assumed cost, so the strategy is already losing money at its own
# assumption, and it is classified apart from a thin but positive margin.
#
# The assumed cost is the one already in `summary`, read from each case study's
# own setup rather than declared again here, so this table and the degradation
# table above cannot disagree about what a case study is assumed to pay.

# %%
survival = summary.with_columns(
    cost_margin_bps=(pl.col("breakeven_bps") - pl.col("assumed_cost_bps")),
    cost_margin_ratio=(pl.col("breakeven_bps") / pl.col("assumed_cost_bps").clip(lower_bound=1)),
).with_columns(
    resilience=pl.when(pl.col("cost_margin_ratio") < 1)
    .then(pl.lit("does not survive"))
    .when(pl.col("cost_margin_ratio") >= 10)
    .then(pl.lit("very robust"))
    .when(pl.col("cost_margin_ratio") >= 3)
    .then(pl.lit("robust"))
    .when(pl.col("cost_margin_ratio") >= 1.5)
    .then(pl.lit("marginal"))
    .otherwise(pl.lit("fragile")),
)

print("=== Cost Survival Classification ===")
survival.select(
    "display_name",
    "cadence",
    "assumed_cost_bps",
    "net_sharpe",
    "breakeven_bps",
    "breakeven_observed",
    "cost_margin_ratio",
    "resilience",
)

# %% tags=["results"]
_res = survival.group_by("resilience").agg(cs=pl.col("display_name")).sort("resilience")
display(
    Markdown(
        "; ".join(
            f"**{r['resilience']}**: {', '.join(sorted(r['cs']))}"
            for r in _res.iter_rows(named=True)
        )
        + ". A ratio is only as good as the breakeven behind it, and where the "
        "sweep never crossed zero the breakeven is the grid ceiling rather than "
        "a crossing, so the ratio for those is a lower bound too."
    )
)

# %% [markdown]
# ## S&P 500 Options: Spread Realism Caveat
#
# The S&P 500 Options case study was validated using executable-label
# backtesting, pricing straddle entries and exits at actual bid/ask quotes rather
# than at an assumed bps cost. That case study has no selected configuration cost sweep, so it
# does not appear in any table above.
#
# It is described here for the structure of its cost problem rather than for its numbers, which
# its own evaluation and §18.8 carry. A single-name option's dominant execution cost is the
# bid-ask spread on the premium rather than a commission proportional to notional, so the cost
# scales with how wide the quote is and not with how much is traded. That is why its evaluation
# decomposes one prediction across three labels - priced at the mid and unhedged, delta-hedged
# at the mid, and priced at the quotes a desk would actually get - which separates the signal's
# contribution from the execution's, and why ranking on signal and spread jointly is a different
# strategy from ranking on signal alone rather than a refinement of it.
#
# A generic bps cost sweep misrepresents this case study for the same reason: it models a cost
# that is proportional to notional. The teaching point is that strategy design has to optimize
# for signal quality and execution cost together, because for this instrument the spread is what
# the signal has to pay for.

# %% [markdown]
# ## Cadence–Frequency–Cost Regime
#
# The same IC translates to very different tradability depending on
# rebalance cadence. A 15-minute strategy accumulates ~25× more turnover
# per day than a daily strategy, and ~500× more than a monthly one.
# This creates distinct cost regimes:

# %%
if not summary.is_empty():
    regime = summary.with_columns(
        turnover_mult=pl.col("cadence_period").replace_strict(
            TURNOVER_MULTIPLIER,
            default=1.0,
            return_dtype=pl.Float64,
        ),
    )

# %%
if not summary.is_empty():
    fig, ax = plt.subplots(figsize=(10, 6.5))

    # Turnover-mult on x (varies 0.05→26×); breakeven on y. Both log so the
    # high-frequency cluster (NQ100/Crypto) and the monthly cluster
    # separate cleanly instead of stacking on a constant-x degenerate column.
    assumed_floor = max(float(summary["assumed_cost_bps"].min()), 0.5)

    # Monthly selected configurations share x (turnover ≈ 0.05) and pair up on y: ETFs and
    # US Firms at 50, CME and SP500 Eq+Opt at 30. Fan their labels vertically
    # so the two pairs stay legible despite the superimposed markers.
    label_offsets = {
        "NQ100": (10, 4),
        "Crypto": (10, 4),
        "FX": (10, 4),
        "US Equities": (10, 4),
        "ETFs": (10, 16),
        "US Firms": (10, 2),
        "SP500 Eq+Opt": (10, -2),
        "CME Futures": (10, -16),
        "SP500 Options": (10, 4),
    }

    for row in regime.iter_rows(named=True):
        color = freq_colors.get(row["cadence_period"], "gray")
        size = max(60, min(360, row["turnover_mult"] ** 0.5 * 120))
        ax.scatter(
            row["turnover_mult"],
            max(row["breakeven_bps"], 0.5),
            s=size,
            c=color,
            edgecolors="white",
            linewidth=1.2,
            zorder=5,
        )
        dx, dy = label_offsets.get(row["display_name"], (8, 8))
        ax.annotate(
            row["display_name"],
            (row["turnover_mult"], max(row["breakeven_bps"], 0.5)),
            xytext=(dx, dy),
            textcoords="offset points",
            fontsize=9,
            zorder=6,
        )

    ax.axhline(
        assumed_floor,
        color="0.35",
        linestyle="--",
        linewidth=1.0,
        zorder=3,
        label=f"Survival floor ({assumed_floor:.0f} bps assumed cost)",
    )
    ax.set_xscale("log")
    ax.set_yscale("symlog", linthresh=1)
    ax.set_xlim(0.03, 60)
    ax.set_ylim(-0.5, 600)

    ax.set_xlabel("Turnover multiplier vs daily (log)")
    ax.set_ylabel("Breakeven cost — bps per leg (symlog)")
    ax.set_title("Cost Regimes: Higher-Frequency Strategies Face Steeper Cliffs")

    legend_handles = [
        Patch(facecolor=freq_colors[f], label=f) for f in freq_order if f in freq_colors
    ]
    ax.legend(
        handles=legend_handles + [ax.get_lines()[0]],
        loc="upper right",
        title="Cadence",
        framealpha=0.9,
    )

    show_with_alt(
        fig,
        "Log-log scatter of breakeven cost against assumed relative turnover, one "
        "marker per case study coloured by cadence, with a horizontal line at the "
        "lowest assumed cost in the panel.",
    )

# %% [markdown]
#
# %% tags=["results"]
_reg = regime.sort("turnover_mult", descending=True)
display(
    Markdown(
        "Marker x-position is assumed per-day turnover relative to a daily "
        "strategy, y is the cost at which net Sharpe crosses zero. The turnover "
        "multipliers are an assumption written into this notebook, not a "
        "measurement from the backtests: they say how often a book of a given "
        "cadence is expected to turn over, and the chart uses them to place the "
        "case studies rather than to test them.\n\n"
        + "; ".join(
            f"**{r['display_name']}** ({r['cadence']}), breakeven "
            f"{'' if r['breakeven_observed'] else 'at least '}"
            f"{r['breakeven_bps']:g} bps against an assumed "
            f"{r['assumed_cost_bps']:g}"
            for r in _reg.iter_rows(named=True)
        )
        + ".\n\nThe cadences present here span a narrow part of the range the "
        "chart is drawn for. The high-frequency corner is empty: NASDAQ-100's "
        "cost sweep ran a different strategy from its carrier, and the other "
        "sub-daily case studies have no cost sweep. Nothing here tests whether "
        "turnover or signal strength sets the breakeven, because the case "
        "studies that would separate them are the ones missing."
    )
)

# %% [markdown]
# ## Key Takeaways
#
# - **A cost sweep is only informative at the cost the strategy assumes.** The
#   gross Sharpe and the Sharpe at the top of the grid are both easy to read off
#   and neither is the number that decides whether the strategy is tradable. The
#   assumed cost comes from the case study's own setup, and the tables above
#   report the net Sharpe there.
# - **Breakeven and assumed cost have to be compared, not reported side by
#   side.** The ratio between them is the headroom, and a ratio below one means
#   the strategy is already under water at its own assumption. The computed
#   classification above says which case studies are where.
# - **A breakeven above the top of the swept grid is not a breakeven.** Where the
#   curve is still positive at the ceiling, the honest statement is that the
#   crossing is somewhere above it, and the tables mark those rows rather than
#   printing the ceiling as though it had been measured.
# - **A basis-point grid does not model every cost structure.** Where the
#   dominant cost is a wide bid-ask spread rather than a proportional fee, a bps
#   sweep understates it, and the answer is an executable backtest against
#   quotes. The S&P 500 Options section above is the worked case.
#
# ## Known Limitations
#
# - Only case studies whose *carrier* has a cost sweep appear. The loaded count and
#   one line per absent case study are printed at the top, so which check dropped a
#   case study is read off the run rather than reconstructed by hand. A case study can
#   hold cost-sensitivity backtests and still be absent, because the sweep has to sit
#   on the deployed carrier's own training lineage and run the carrier's own strategy.
#   Three are absent and each fails a different check. ETFs' carrier lineage carries no
#   cost sweep at all. S&P 500 Options has eight cost rows on its carrier's lineage,
#   all of them an `equal_weight_top_k` + `score_weighted` series rather than the
#   carrier. NASDAQ-100 has 24 on its carrier's lineage, all of them `equal_weight_top_k`
#   - the instrument its pass-1 ranking uses - while its carrier is a
#   `slot_persistent_signal_exit` strategy. All three absences are properties of what
#   was swept rather than of this chapter: a carrier with no cost sweep of its own has
#   no cost curve to draw.
# - The sweep applies one proportional per-leg cost to every trade. Real costs
#   vary with size, with the instrument, and with the state of the book, and the
#   spread realism section is where that assumption is checked rather than
#   assumed.
# - Net Sharpe at the assumed cost is interpolated between grid points; the
#   bracketing points are in the table so the interpolation can be checked.
# - The turnover multipliers used to place case studies on the cadence chart are
#   stated assumptions about how often each cadence trades, not turnover measured
#   from the backtests.
# - Every Sharpe here is a validation-fold number for a configuration chosen on
#   validation data, so the cost headroom inherits that selection.
#
# **Next**: [`07_regime_risk`](07_regime_risk.ipynb) examines regime
# robustness and risk overlays.

```

Exibido na íntegra, com atribuição conforme a licença da fonte. Licença: MIT

Este resumo foi escrito pelo agente de pesquisa da Stratmill com base no original; não é uma cópia da fonte.