跳至正文
返回文库全部文档

根据执行成本与换手率估算日内策略容量

笔记本 《交易机器学习》

总结

本笔记本估算执行成本如何侵蚀假设的日内策略收益。首先,使用分钟线报价测量 NASDAQ-100 成分股的成交量加权报价价差,再以成分股价差中位数作为穿价成本的经验基准。其他成本部分,包括市场冲击、滑点、佣金和费用,均为情景假设。笔记本将穿价、限价委托和被动执行的成本组合,与假设的毛夏普比率、波动率和每日往返投资组合 NAV 换手率特征进行比较。

该框架按交易名义金额比例年化成本,并在成本降低收益但不改变波动率的简化假设下计算净夏普比率。它还求出符合所选净夏普比率阈值的最高换手率。关键的实践启示是,应根据投资组合 NAV 换手率评估成本,而不是只看交易笔数。结果属于敏感性分析,并非经过测试的策略:毛收益特征是假设的,大多数成本输入仅用于说明,价差样本也只反映特定数据窗口,并未声称成分股按时点成分进行筛选。实际执行条件可能有很大差异。

核心观点

  • 年化执行损耗随投资组合往返 NAV 换手率和单位交易名义金额成本而变化。
  • 与单只超大市值股票的报价相比,测得的成分股价差中位数更能代表覆盖广泛股票池的策略。
  • 穿价、限价委托和被动执行情景展示了执行假设如何改变可行换手率。
  • 换手率上限取决于所选净夏普比率阈值,是诊断工具,并非普遍适用的规则。
  • 大多数成本输入和所有策略特征均为假设,因此部署前需根据实际执行情况进行验证。

标签

全文
# The Cost Cliff: Intraday Strategy Reality Check


# The Cost Cliff: Intraday Strategy Reality Check

**Docker image**: `ml4t`

This notebook demonstrates how transaction costs can overwhelm an intraday
strategy. A strategy that posts a high gross Sharpe can become unprofitable or
marginal once its turnover is charged an explicit execution-cost stack.

**Key Insight**: The "cost cliff" is the turnover level at which execution-cost
drag consumes the gross return. Cost must be charged to traded notional, so the
relevant activity measure is portfolio NAV turnover rather than a raw trade count.

**Why This Matters**:
- Intraday viability depends on the execution stack a strategy can actually achieve
- Cost assumptions must be separated from measured market inputs
- The same hypothetical gross return can survive or fail under different execution stacks

**Learning Objectives**
- Quantify the gross-to-net effect of explicit intraday cost stacks
- Compare crossing, worked-order, and passive execution assumptions on the same profile
- Estimate the turnover ceiling implied by a target net Sharpe
- Use the cost cliff as a publication-quality sanity check for intraday claims

**Book Reference:** Chapter 18: Section 18.8 (Designing practical cost guardrails)

**Prerequisites:** Read [`02_spread_estimation`](02_spread_estimation.ipynb) for spread realism and
[`09_frequency_tradeoff`](09_frequency_tradeoff.ipynb) for the slower-frequency guardrail framing.

## Setup

```python
"""The Cost Cliff - Intraday Sharpe collapse and break-even turnover analysis."""

import math
from dataclasses import dataclass
from typing import NamedTuple

import plotly.graph_objects as go
import polars as pl
from IPython.display import Markdown, display

import utils  # noqa: F401
from data import load_nasdaq100_bars
from utils.style import COLORS, show_plotly_with_alt
```

The retail half-spread that anchors the cost stacks is measured from real AlgoSeek
NASDAQ-100 minute-bar quotes over the window set below.

```python
SPREAD_START_DATE = "2021-12-01"
SPREAD_END_DATE = "2021-12-31"
```

## 1. Anchoring the Spread to Real NASDAQ-100 Quotes

The cost cliff is only credible if the spread that drives it is real. We load
AlgoSeek NASDAQ-100 minute bars, compute each interval's relative quoted spread,
and take the **cross-sectional** distribution of per-symbol volume-weighted
spreads. The median name, not the most liquid mega-cap, is the right anchor
for a strategy that trades the whole index.

```python
def measure_nasdaq100_spreads(start_date: str, end_date: str) -> pl.DataFrame:
    """Per-symbol volume-weighted relative quoted spread (bps) over the regular session."""
    return (
        load_nasdaq100_bars(
            start_date=start_date,
            end_date=end_date,
            include_microstructure=True,
            lazy=True,
        )
        .select("symbol", "timestamp", "volume", "close_bid_price", "close_ask_price")
        .filter(
            (pl.col("close_bid_price") > 0)
            & (pl.col("close_ask_price") >= pl.col("close_bid_price"))
            & (pl.col("volume") > 0)
        )
        .with_columns(
            minute_of_day=pl.col("timestamp").dt.hour().cast(pl.Int32) * 60
            + pl.col("timestamp").dt.minute().cast(pl.Int32),
            rel_spread_bps=(
                (pl.col("close_ask_price") - pl.col("close_bid_price"))
                / ((pl.col("close_ask_price") + pl.col("close_bid_price")) / 2)
                * 1e4
            ),
        )
        .filter((pl.col("minute_of_day") >= 570) & (pl.col("minute_of_day") < 960))
        .group_by("symbol")
        .agg(
            vw_rel_spread_bps=(pl.col("rel_spread_bps") * pl.col("volume")).sum()
            / pl.col("volume").sum()
        )
        .sort("vw_rel_spread_bps")
        .collect()
    )
```

### Measure the Empirical Spread Anchor

```python
spread_df = measure_nasdaq100_spreads(SPREAD_START_DATE, SPREAD_END_DATE)
if spread_df.is_empty():
    raise ValueError("no valid NASDAQ-100 spread observations were available")
median_rel_spread_bps = float(spread_df["vw_rel_spread_bps"].median())
if not math.isfinite(median_rel_spread_bps) or median_rel_spread_bps <= 0:
    raise ValueError("the measured median relative spread must be finite and positive")
# The cost of crossing once is half the quoted spread.
MEASURED_HALF_SPREAD_BPS = median_rel_spread_bps / 2.0

print(f"NASDAQ-100 symbols measured: {spread_df.height}")
print(f"Median per-symbol relative spread: {median_rel_spread_bps:.2f} bps")
print(f"Measured half-spread (cost to cross): {MEASURED_HALF_SPREAD_BPS:.2f} bps")
print(f"Most liquid name:  {spread_df['vw_rel_spread_bps'][0]:.2f} bps")
print(f"Least liquid name: {spread_df['vw_rel_spread_bps'][-1]:.2f} bps")
```

### Inspect the Cross-Sectional Distribution

```python
fig = go.Figure()
fig.add_histogram(
    x=spread_df["vw_rel_spread_bps"].to_list(), nbinsx=30, marker_color=COLORS["blue"]
)
fig.add_vline(
    x=median_rel_spread_bps,
    line_dash="dash",
    line_color=COLORS["neutral"],
    annotation_text=f"Median {median_rel_spread_bps:.1f} bps",
)
fig.update_layout(
    title="Relative spread across the NASDAQ-100 symbols",
    xaxis_title="Relative spread (bps)",
    yaxis_title="Number of symbols",
    height=380,
)
show_plotly_with_alt(
    fig,
    "A histogram of volume-weighted relative spread, one observation per index member, counts "
    "on the vertical axis. The distribution is right-skewed: most symbols fall in the "
    "narrow-spread bars on the left, and a thin tail of individual symbols runs out to roughly "
    "three times the median. A dashed vertical line marks the median and is labelled with it.",
)
```

**Finding**: The cross-sectional distribution shows why a mega-cap quote is not
a representative execution anchor for a broad-universe strategy. The median
per-symbol spread supplies the empirical crossing-cost input used below. This
volume-weighted estimate is most relevant when participation follows market volume.
The sample is the set of symbols present in the licensed data window; the notebook
makes no point-in-time index-membership or return-selection claim.

## 2. Intraday Cost Components

Intraday trading incurs costs at multiple levels. The spread term is the
measured half-spread above; market impact, slippage, and fees are institutional
cost assumptions layered on top.

```python
@dataclass
class IntradayCostStack:
    """Illustrative execution costs in bps of traded notional, per side."""

    spread_half: float = 3.0
    market_impact: float = 2.0
    slippage: float = 1.5
    commission: float = 0.5
    exchange_fee: float = 0.3
    clearing_fee: float = 0.1

    @property
    def one_way_bps(self) -> float:
        """Total one-way cost in bps of traded notional."""
        return (
            self.spread_half
            + self.market_impact
            + self.slippage
            + self.commission
            + self.exchange_fee
            + self.clearing_fee
        )

    @property
    def round_trip_bps(self) -> float:
        """Total round-trip cost in bps of round-trip notional."""
        return 2 * self.one_way_bps
```

### Cost Scenario Presets

Only the crossing spread is measured. The worked-order spread fraction,
passive spread cost, impact, slippage, and fees are illustrative assumptions.
They isolate sensitivity to execution quality; they are not estimates of a
particular broker, institution, or HFT strategy.

```python
CROSSING = IntradayCostStack(
    spread_half=MEASURED_HALF_SPREAD_BPS,  # crosses and pays the full measured half-spread
    market_impact=3.0,
    slippage=2.0,
    commission=0.0,  # Commission-free broker
    exchange_fee=0.3,
    clearing_fee=0.1,
)

WORKED_ORDER = IntradayCostStack(
    spread_half=0.375 * MEASURED_HALF_SPREAD_BPS,  # works orders to cross only partway
    market_impact=2.5,
    slippage=1.0,
    commission=0.3,
    exchange_fee=0.2,
    clearing_fee=0.05,
)

PASSIVE_LOW_COST = IntradayCostStack(
    spread_half=0.0,  # Assumes passive fills without adverse-selection spread drag
    market_impact=0.5,
    slippage=0.2,
    commission=0.05,
    exchange_fee=-0.2,  # Illustrative maker rebate
    clearing_fee=0.02,
)
```

### Compare Scenario Inputs

```python
print("Intraday Cost Comparison (bps):")
cost_rows = []
for component in [
    "spread_half",
    "market_impact",
    "slippage",
    "commission",
    "exchange_fee",
    "clearing_fee",
]:
    cost_rows.append(
        {
            "Component": component,
            "Crossing": getattr(CROSSING, component),
            "Worked order": getattr(WORKED_ORDER, component),
            "Passive low-cost": getattr(PASSIVE_LOW_COST, component),
        }
    )
cost_rows.append(
    {
        "Component": "TOTAL (one-way)",
        "Crossing": CROSSING.one_way_bps,
        "Worked order": WORKED_ORDER.one_way_bps,
        "Passive low-cost": PASSIVE_LOW_COST.one_way_bps,
    }
)
cost_rows.append(
    {
        "Component": "TOTAL (round-trip)",
        "Crossing": CROSSING.round_trip_bps,
        "Worked order": WORKED_ORDER.round_trip_bps,
        "Passive low-cost": PASSIVE_LOW_COST.round_trip_bps,
    }
)
pl.DataFrame(cost_rows)
```

**Finding**: The round-trip stack sets the baseline hurdle. The crossing
scenario starts several bps behind before alpha enters the picture. The two
lower-cost stacks are sensitivity cases whose assumptions must be validated
against an actual execution process before deployment.

## 3. Hypothetical Intraday Strategy Profiles

These profiles are deliberately hypothetical. They specify gross Sharpe,
annual volatility, and round-trip portfolio NAV turnover. No signal is fit or
tested here. Daily round-trip turnover measures opened-and-closed notional in
units of NAV, regardless of how many child orders implement that turnover.

```python
class IntradayStrategy(NamedTuple):
    """Hypothetical gross performance and portfolio-turnover assumptions."""

    name: str
    gross_sharpe: float  # Gross Sharpe ratio
    annual_vol: float  # Annual volatility
    round_trip_nav_turnover_per_day: float


HIGH_TURNOVER = IntradayStrategy(
    name="High turnover",
    gross_sharpe=2.5,
    annual_vol=0.20,
    round_trip_nav_turnover_per_day=1.0,
)

MODERATE_TURNOVER = IntradayStrategy(
    name="Moderate turnover",
    gross_sharpe=2.0,
    annual_vol=0.15,
    round_trip_nav_turnover_per_day=0.4,
)

LOW_TURNOVER = IntradayStrategy(
    name="Low turnover",
    gross_sharpe=1.5,
    annual_vol=0.12,
    round_trip_nav_turnover_per_day=0.1,
)
```

### Net Performance Calculator

Let $\tau$ denote daily round-trip NAV turnover, $c_{rt}$ the round-trip
execution cost in basis points, $S_g$ gross Sharpe, and $\sigma$ annual
volatility. With $D$ trading days and deterministic cost drag,

$$C_{ann} = D\tau\frac{c_{rt}}{10^4}, \qquad
S_n = \frac{S_g\sigma - C_{ann}}{\sigma}.$$

This approximation changes annual return but not annual volatility.

```python
def calculate_intraday_net_performance(
    strategy: IntradayStrategy,
    costs: IntradayCostStack,
    trading_days: int = 252,
) -> dict[str, float | str]:
    """Apply deterministic execution-cost drag to a hypothetical gross profile."""
    if strategy.annual_vol <= 0:
        raise ValueError("annual_vol must be positive")
    if strategy.round_trip_nav_turnover_per_day < 0:
        raise ValueError("round-trip NAV turnover cannot be negative")
    if trading_days <= 0:
        raise ValueError("trading_days must be positive")

    gross_return = strategy.gross_sharpe * strategy.annual_vol
    annual_round_trip_nav_turnover = strategy.round_trip_nav_turnover_per_day * trading_days

    annual_cost_bps = annual_round_trip_nav_turnover * costs.round_trip_bps
    annual_cost = annual_cost_bps / 10000
    net_return = gross_return - annual_cost

    net_sharpe = net_return / strategy.annual_vol

    return {
        "strategy": strategy.name,
        "gross_sharpe": strategy.gross_sharpe,
        "gross_return": gross_return,
        "daily_round_trip_nav_turnover": strategy.round_trip_nav_turnover_per_day,
        "annual_round_trip_nav_turnover": annual_round_trip_nav_turnover,
        "round_trip_cost_bps": costs.round_trip_bps,
        "annual_cost_bps": annual_cost_bps,
        "annual_cost": annual_cost,
        "net_return": net_return,
        "net_sharpe": net_sharpe,
    }
```

## 4. The Cost Cliff Demonstration

We now apply each illustrative execution stack to every hypothetical strategy
profile. The calculation treats cost as a deterministic return drag and holds
annual volatility fixed. It excludes financing, taxes, passive-fill risk, and
uncertainty in realized impact, so this is a sensitivity analysis rather than
a backtest or capacity estimate.

```python
strategies = [HIGH_TURNOVER, MODERATE_TURNOVER, LOW_TURNOVER]
cost_scenarios = [
    ("Crossing", CROSSING),
    ("Worked order", WORKED_ORDER),
    ("Passive low-cost", PASSIVE_LOW_COST),
]

results = []
for strategy in strategies:
    for cost_name, costs in cost_scenarios:
        perf = calculate_intraday_net_performance(strategy, costs)
        perf["cost_type"] = cost_name
        results.append(perf)

results_df = pl.DataFrame(results)
```

## 5. Visualizing the Cost Cliff

```python
colors = {
    "Crossing": COLORS["negative"],
    "Worked order": COLORS["amber"],
    "Passive low-cost": COLORS["positive"],
}
patterns = {"Crossing": "/", "Worked order": "x", "Passive low-cost": "."}
fig = go.Figure()
x_labels = [strategy.name for strategy in strategies]
fig.add_trace(
    go.Bar(
        x=x_labels,
        y=[strategy.gross_sharpe for strategy in strategies],
        name="Gross Sharpe",
        marker_color=COLORS["blue"],
        text=[f"{strategy.gross_sharpe:.2f}" for strategy in strategies],
        textposition="outside",
    )
)

for cost_name, _costs in cost_scenarios:
    subset = results_df.filter(pl.col("cost_type") == cost_name)
    fig.add_trace(
        go.Bar(
            x=x_labels,
            y=subset["net_sharpe"].to_list(),
            name=cost_name,
            marker_color=colors[cost_name],
            marker_pattern_shape=patterns[cost_name],
            text=[f"{value:.2f}" for value in subset["net_sharpe"]],
            textposition="outside",
        )
    )
```

### Add the Scenario Threshold

```python
fig.add_hline(
    y=0.5,
    line_dash="dash",
    line_color=COLORS["neutral"],
    annotation_text="Scenario threshold",
    annotation_position="top left",
)
fig.add_hline(y=0, line_dash="dot", line_color=COLORS["negative"])
fig.update_layout(
    title="Gross and net Sharpe by cost structure and turnover profile",
    yaxis_title="Sharpe Ratio",
    xaxis_title="Hypothetical strategy profile",
    barmode="group",
    height=450,
    showlegend=True,
)

show_plotly_with_alt(
    fig,
    "Grouped bars of Sharpe ratio, one group per turnover profile, each holding the gross Sharpe "
    "and the net Sharpe under three cost structures, every bar labelled with its value. The gross "
    "bar is the tallest in every group and the crossing bar the shortest, with passive low-cost "
    "the tallest of the three net bars. The spread between the net bars narrows sharply from the "
    "high-turnover group to the low-turnover one, where all three land close to the gross bar. A "
    "dashed horizontal line marks the scenario threshold, and the high-turnover crossing bar is "
    "the only one that falls below it.",
)
```

### Quantitative Reading

```python
high_crossing = results_df.filter(
    (pl.col("strategy") == HIGH_TURNOVER.name) & (pl.col("cost_type") == "Crossing")
).row(0, named=True)
display(
    Markdown(
        f"**Finding**: for the high-turnover profile, the crossing stack reduces "
        f"Sharpe from **{HIGH_TURNOVER.gross_sharpe:.2f}** to "
        f"**{high_crossing['net_sharpe']:.2f}**, with annual cost drag of "
        f"**{high_crossing['annual_cost']:.1%} of NAV**."
    )
)
```

The comparison isolates cost assumptions; it does not establish that passive
fills or worked-order execution are available to the strategy.

## 6. Annual Cost as Percentage of Gross Return

The ratio below is also the percentage reduction in Sharpe under the fixed-volatility
approximation, so a separate Sharpe-degradation chart would repeat the same information.

```python
cost_share_df = results_df.with_columns(
    (pl.col("annual_cost") / pl.col("gross_return") * 100).alias("cost_pct_gross")
)
```

### Compare Return-Budget Consumption

```python
fig = go.Figure()

strategy_colors = {
    "High turnover": COLORS["negative"],
    "Moderate turnover": COLORS["amber"],
    "Low turnover": COLORS["blue"],
}
strategy_patterns = {"High turnover": "/", "Moderate turnover": "x", "Low turnover": "."}

for strategy in strategies:
    subset = cost_share_df.filter(pl.col("strategy") == strategy.name)

    fig.add_trace(
        go.Bar(
            x=subset["cost_type"].to_list(),
            y=subset["cost_pct_gross"].to_list(),
            name=strategy.name,
            marker_color=strategy_colors[strategy.name],
            marker_pattern_shape=strategy_patterns[strategy.name],
            text=[f"{value:.0f}%" for value in subset["cost_pct_gross"]],
            textposition="outside",
        )
    )

fig.add_hline(
    y=100,
    line_dash="dash",
    line_color=COLORS["negative"],
    annotation_text="100% = gross return consumed",
)

fig.update_layout(
    title="Annual cost as a share of gross return, by cost structure",
    yaxis_title="Cost as % of Gross Return",
    xaxis_title="Cost Structure",
    barmode="group",
    height=400,
)

show_plotly_with_alt(
    fig,
    "Grouped bars of annual cost as a share of gross return, one group per cost structure and "
    "one bar per turnover profile, every bar labelled. Within each group the bars fall from high "
    "to low turnover. The crossing group is by far the tallest and its high-turnover bar nearly "
    "reaches the dashed line marking the whole of gross return; the passive low-cost group is "
    "barely off the axis.",
)
```

**Finding**: Cost as a share of gross return is the clearest sanity check for
intraday claims. Once annual cost approaches the whole of gross return, the strategy
has no margin for model error, slippage misses, or live degradation.

## 7. Break-Even Turnover Analysis

```python
def calculate_break_even_turnover(
    target_net_sharpe: float,
    gross_sharpe: float,
    annual_vol: float,
    costs: IntradayCostStack,
    trading_days: int = 252,
) -> float:
    """Maximum daily round-trip NAV turnover for a target net Sharpe."""
    if annual_vol <= 0:
        raise ValueError("annual_vol must be positive")
    if trading_days <= 0:
        raise ValueError("trading_days must be positive")

    gross_return = gross_sharpe * annual_vol
    target_net_return = target_net_sharpe * annual_vol
    max_annual_cost = max(0.0, gross_return - target_net_return)

    round_trip_cost = costs.round_trip_bps / 10000

    if round_trip_cost <= 0:
        return math.inf if max_annual_cost > 0 else 0.0

    max_annual_turnover = max_annual_cost / round_trip_cost
    return max_annual_turnover / trading_days
```

### Compute the Turnover Ceilings

```python
be_rows = []
for gs in [1.5, 2.0, 2.5, 3.0]:
    row = {"Gross Sharpe": gs}
    for cost_name, cost_obj in cost_scenarios:
        max_turnover = calculate_break_even_turnover(
            target_net_sharpe=0.5,
            gross_sharpe=gs,
            annual_vol=0.15,
            costs=cost_obj,
        )
        row[cost_name] = max_turnover
    be_rows.append(row)
break_even_df = pl.DataFrame(be_rows)
```

### Compare the Cost Scenarios

The low-cost case permits much more turnover than the crossing case, so the
vertical axis uses a log scale to keep all three curves legible.

```python
line_dashes = {
    "Crossing": "solid",
    "Worked order": "dash",
    "Passive low-cost": "dot",
}
marker_symbols = {
    "Crossing": "circle",
    "Worked order": "square",
    "Passive low-cost": "diamond",
}

fig = go.Figure()
for cost_name, _cost_obj in cost_scenarios:
    values = break_even_df[cost_name].to_list()
    fig.add_trace(
        go.Scatter(
            x=break_even_df["Gross Sharpe"].to_list(),
            y=values,
            name=cost_name,
            mode="lines+markers+text",
            line=dict(color=colors[cost_name], dash=line_dashes[cost_name]),
            marker=dict(symbol=marker_symbols[cost_name], size=8),
            text=["", "", "", f"{values[-1]:.2f}"],
            textposition="top center",
        )
    )

fig.update_layout(
    title="Maximum daily turnover against gross Sharpe, by cost structure",
    xaxis_title="Gross Sharpe",
    yaxis_title="Maximum daily round-trip NAV turnover (log scale)",
    yaxis_type="log",
    height=430,
)
show_plotly_with_alt(
    fig,
    "Three rising lines of maximum sustainable daily turnover against gross Sharpe, one per cost "
    "structure, on a logarithmic vertical axis with the right-hand endpoints labelled. The lines "
    "never cross: the passive low-cost line sits an order of magnitude above the crossing line "
    "across the whole Sharpe range, with the worked-order line between them.",
)
```

**Interpretation**: The break-even curves convert cost assumptions into a
portfolio-turnover ceiling. A trade-count ceiling would be invalid without the
notional size of each trade.

## 8. Cost-Structure Comparison

### Key Insights

1. **Crossing at high turnover is fragile**: the high-turnover profile can fall
   below the scenario threshold once spread, impact, and fees are applied.

2. **Execution quality matters**: lower costs per unit of traded notional
   materially expand viable turnover capacity relative to crossing.

3. **Viability threshold**: the selected net-Sharpe threshold is a scenario
   diagnostic, not a universal deployment rule.

4. **Why backtests overstate intraday edge**: optimistic slippage and
   incomplete spread/impact modeling can hide the true cost cliff.

5. **Practical policy**: reject an intraday proposal when conservative cost
   assumptions consume its return budget at the intended NAV turnover.

## Key Takeaways

- **The cost cliff is a turnover phenomenon**: annualized round-trip cost
  scales linearly with portfolio NAV turnover, not with an unscaled trade count.
- **Crossing is the demanding case**: with the spread anchored to the measured
  median NASDAQ-100 half-spread, the crossing stack supplies the largest return drag.
- **Cost as a share of gross return is the cleanest diagnostic**: once annual cost
  exceeds gross return outright, the strategy has no margin for model error,
  live degradation, or slippage misses.
- **Break-even turnover is a deployment guardrail**: solving for the maximum
  daily round-trip NAV turnover under a target net Sharpe gives a limit that can
  be compared directly with a proposed portfolio.
- **What is measured vs assumed**: the half-spread is measured from real
  NASDAQ-100 quotes; market impact, slippage, and fees are illustrative cost
  assumptions, and the gross Sharpe / turnover profiles are hypothetical
  sensitivity cases. The notebook does not estimate an OFI signal or HFT fills.

**Next**: See [`12_commission_slippage_comparison`](12_commission_slippage_comparison.ipynb)
for explicit fee decomposition.

**Book**: Chapter 18, Section 18.8 discusses practical guardrails for costs.
![notebook output](figures/p1_1.png)
![notebook output](figures/p1_2.png)
![notebook output](figures/p1_3.png)
![notebook output](figures/p1_4.png)

在遵守原作品许可的前提下,附作者信息全文展示。 许可协议: MIT

此摘要由 Stratmill 研究智能体根据原文撰写,并非原文副本。