So sánh nghiên cứu tình huống giao dịch theo chi phí và thiết kế đánh giá
Tóm tắt
Tổng quan này sắp xếp một nhóm nghiên cứu tình huống giao dịch theo loại tài sản, quy mô tập hợp, tần suất dữ liệu, nhịp ra quyết định, chế độ chi phí và quy trình đánh giá. Tài liệu đọc thiết lập được công bố của từng nghiên cứu và trình bày các bảng so sánh, bao gồm các giai đoạn huấn luyện và xác thực, số fold, thời kỳ giữ lại để kiểm tra và cách ánh xạ tín hiệu thành vị thế. Danh mục trải rộng từ cổ phiếu, quyền chọn, tiền mã hóa, ngoại hối, hợp đồng tương lai đến ETF đa tài sản, cho thấy lựa chọn nghiên cứu thay đổi theo thị trường và dữ liệu sẵn có.
Các bài học nghiên cứu chính là đánh giá chi phí giao dịch theo kỳ hạn chiến lược, tính đến khác biệt về lịch sử và tần suất lấy mẫu, đồng thời dành dữ liệu giữ lại cho đánh giá cuối cùng thay vì phát triển mô hình. Các chế độ chi phí được mô tả là chi phối đòi hỏi tín hiệu đặc biệt mạnh, còn các giai đoạn huấn luyện khác nhau phản ánh độ sâu dữ liệu và giả định về tính dừng khác nhau. Đây là tài liệu tham khảo dựa trên siêu dữ liệu và tổng quan về khung phương pháp, không phải nghiên cứu hiệu suất: tài liệu không chứng minh chiến lược nào trong các nghiên cứu tình huống tạo ra lợi suất. Các phép so sánh cũng phụ thuộc vào độ chính xác và tính cập nhật của những tệp thiết lập nền tảng.
Ý chính
- Thiết lập nghiên cứu giao dịch khác nhau giữa các thị trường về tập hợp, tần suất, nhịp quyết định và giả định chi phí.
- So sánh các giai đoạn huấn luyện, các fold xác thực và thời kỳ giữ lại khi đánh giá thiết kế đánh giá.
- Chi phí giao dịch giới hạn các kỳ hạn tín hiệu khả thi, đặc biệt khi chi phí lấn át cơ hội kỳ vọng.
- Dữ liệu giữ lại dành riêng hỗ trợ xác nhận cuối cùng khi không được dùng trong các quyết định phát triển.
- Danh mục tóm tắt phạm vi và quy trình nghiên cứu chứ không phải hiệu suất chiến lược.
Thẻ
Toàn văn
# Case Study Overview: Cross-Strategy Summary
# Case Study Overview: Cross-Strategy Summary
**ML4T Third Edition - Chapter 6: Strategy Research Framework**
**Docker image**: `ml4t`
This notebook provides a unified view of all 9 case studies used throughout this book.
It consolidates key information that readers need to understand:
- **What datasets we cover**: Asset classes, universes, and time periods
- **Trading setup constraints**: Cost models, horizons, and feasibility analysis
- **Evaluation protocols**: Walk-forward configurations and holdout policies
- **Prediction coverage**: Calendar-year spans for training, validation, and holdout
**Book Reference**: Chapter 6, Sections 6.3 and 6.5
**Prerequisites**: Each case study must have a `config/setup.yaml` defining
the trading setup, universe, evaluation protocol, and cost model.
```python
"""Case Study Overview: Cross-strategy summary for Chapter 6."""
from typing import Any
import matplotlib.pyplot as plt
import polars as pl
import yaml
from matplotlib.patches import Patch
from utils.paths import REPO_ROOT
from utils.style import COLORS, show_with_alt
# ML4T role colors, matching the CV schematics in 02_cv_foundations: training is
# the slate main series, validation the amber highlight, the holdout a muted neutral.
TRAIN_C, VAL_C, HOLDOUT_C = COLORS["slate"], COLORS["amber"], COLORS["silver_muted"]
```
```python
# Production defaults — Papermill injects overrides for CI
MAX_SYMBOLS = 0 # 0 = all
```
```python
CASE_STUDIES_DIR = REPO_ROOT / "case_studies"
```
## Load Results
Each case study's `config/setup.yaml` defines the trading setup, universe,
evaluation protocol, and cost model. We load all available configs and build
comparative tables from them.
```python
# Display names and chapter tracks — book-structural metadata, not per-run data
DISPLAY_NAMES = {
"etfs": "ETFs",
"crypto_perps_funding": "Crypto Perps Funding",
"nasdaq100_microstructure": "NASDAQ-100 Microstructure",
"sp500_equity_option_analytics": "S&P 500 Equity+Options",
"us_firm_characteristics": "US Firm Characteristics",
"fx_pairs": "FX Pairs",
"cme_futures": "CME Futures",
"sp500_options": "S&P 500 Options",
"us_equities_panel": "US Equities Panel",
}
CHAPTER_TRACKS = {
"etfs": "Ch6 to Ch21",
"crypto_perps_funding": "Ch6 to Ch12",
"nasdaq100_microstructure": "Ch6 to Ch12",
"sp500_equity_option_analytics": "Ch6 to Ch21",
"us_firm_characteristics": "Ch6 to Ch14",
"fx_pairs": "Ch6 to Ch17",
"cme_futures": "Ch6 to Ch17",
"sp500_options": "Ch6 to Ch21",
"us_equities_panel": "Ch6 to Ch14",
}
```
```python
def _fmt_window(value: Any) -> Any:
"""Normalize an evaluation-window string for display.
A few configs (fx_pairs) write ISO-8601 durations like ``P5Y``/``P1Y``;
strip the leading ``P`` so the quick-reference table reads uniformly
(``5Y``/``1Y``) alongside the bare ``8Y``/``6M`` values used elsewhere.
"""
if isinstance(value, str) and len(value) > 1 and value[0] in ("P", "p"):
return value[1:]
return value
def _normalize_setup_yaml(case_id: str, cfg: dict) -> dict:
"""Convert setup.yaml structure to the summary/diagnostics format the notebook expects."""
universe = cfg.get("universe", {})
decision = cfg.get("decision", {})
costs = cfg.get("costs", {})
ev = cfg.get("evaluation", {})
mapping = cfg.get("mapping", {})
n_assets = universe.get("n_assets", 0) or universe.get("n_products", 0)
if not n_assets:
n_assets = len(universe.get("assets", universe.get("symbols", [])))
# Decision cadence — case studies use different keys: `cadence`,
# `entry_cadence` (sp500_options), or `bar_frequency` (microstructure).
cadence = (
decision.get("cadence")
or decision.get("entry_cadence")
or decision.get("bar_frequency")
or ""
)
freq_map = {
"monthly_month_end": "Daily",
"8_hour_funding_aligned": "8-hourly",
"daily_close": "Daily",
"daily_ny_close": "Daily",
"weekly_friday_close": "Weekly",
"weekly_friday": "Weekly",
"15_minute": "15-min",
"15_min": "15-min",
}
data_freq = freq_map.get(cadence, cadence)
holdout_start = ev.get("holdout_start", "")
holdout_end = ev.get("holdout_end", "")
return {
"summary": {
"asset_class": _infer_asset_class(case_id),
"universe_size": n_assets,
"data_frequency": data_freq,
"decision_cadence": cadence.replace("_", " "),
"cost_model": costs.get("class", "").title(),
},
# The declared cost block, kept verbatim: the discussion below turns on the
# spread estimates in it, and quoting them in prose would put a number on the
# page that no run can correct when a config changes.
"costs": costs,
"diagnostics": {
"train_size": _fmt_window(ev.get("train_size", "N/A")),
"test_size": _fmt_window(ev.get("val_size", "N/A")),
"n_splits": ev.get("n_splits", 0),
"holdout_start": holdout_start,
"holdout_end": holdout_end,
},
"techniques": {
"setup_type": mapping.get("class", ""),
"position_mapping": mapping.get("entry_logic", ""),
},
}
```
### Infer Asset Class
```python
def _infer_asset_class(case_id: str) -> str:
"""Infer asset class from case study ID."""
mapping = {
"etfs": "Multi-Asset",
"crypto_perps_funding": "Crypto",
"nasdaq100_microstructure": "Equities",
"sp500_equity_option_analytics": "Equities+Options",
"us_firm_characteristics": "Equities",
"fx_pairs": "FX",
"cme_futures": "Futures",
"sp500_options": "Options",
"us_equities_panel": "Equities",
}
return mapping.get(case_id, "Unknown")
```
### Load All Case Study Configs
```python
def load_setup_results() -> dict[str, dict]:
"""Load config/setup.yaml from all case studies."""
results = {}
for case_dir in sorted(CASE_STUDIES_DIR.iterdir()):
if case_dir.name.startswith("_") or not case_dir.is_dir():
continue
setup_path = case_dir / "config" / "setup.yaml"
if not setup_path.exists():
continue
cfg = yaml.safe_load(setup_path.read_text())
results[case_dir.name] = _normalize_setup_yaml(case_dir.name, cfg)
return results
```
```python
all_results = load_setup_results()
print(f"Loaded results for {len(all_results)}/{len(DISPLAY_NAMES)} case studies")
if len(all_results) < len(DISPLAY_NAMES):
missing = set(DISPLAY_NAMES) - set(all_results)
print(f"Missing: {', '.join(sorted(missing))}")
```
## Helper: Window Conversion
```python
def _window_to_years(value: Any) -> float | None:
"""Convert window spec to years.
Supports numeric trading days or strings like 6M, 2Q, 10D, 26W, 1Y.
"""
if value is None:
return None
if isinstance(value, (int, float)):
return float(value) / 252.0
if isinstance(value, str):
s = value.strip().upper()
if s.startswith("P"): # ISO 8601 duration prefix used by some configs
s = s[1:]
try:
if s.endswith("Y"):
return float(s[:-1])
if s.endswith("Q"):
return float(s[:-1]) * 0.25
if s.endswith("M"):
return float(s[:-1]) / 12.0
if s.endswith("W"):
return float(s[:-1]) / 52.0
if s.endswith("D"):
return float(s[:-1]) / 252.0
except ValueError:
return None
return None
```
---
## 1. Case Study Inventory
The book uses 9 case studies that span different asset classes, frequencies,
and time horizons. This diversity demonstrates how the same ML4T workflow
adapts to different trading contexts.
```python
overview_rows = []
for case_id, r in all_results.items():
s = r.get("summary", {})
overview_rows.append(
{
"Case Study": DISPLAY_NAMES.get(case_id, case_id),
"Asset Class": s.get("asset_class", ""),
"Universe": s.get("universe_size", 0),
"Data Freq": s.get("data_frequency", ""),
"Decision": s.get("decision_cadence", ""),
"Cost Model": s.get("cost_model", ""),
}
)
overview_df = pl.DataFrame(overview_rows)
overview_df
```
**What to notice**:
- Universe sizes range widely: from 19 (Crypto) and 20 (FX) through the low
hundreds (ETFs 100, NASDAQ-100 114, the S&P 500 option books ~600-630) up to
the multi-thousand equity panels (US Firm Characteristics ~2,500, US Equities
Panel 3,199) - a span that reshapes cross-sectional signal construction
- Data frequencies span 15-minute bars (NASDAQ-100) to weekly (CME Futures, S&P 500)
- Cost models are either "Material" (7 case studies) or "Dominant" (2),
where dominant costs require exceptionally strong signals
### Asset Class Distribution
```python
asset_counts: dict[str, int] = {}
for r in all_results.values():
ac = r.get("summary", {}).get("asset_class", "Unknown")
asset_counts[ac] = asset_counts.get(ac, 0) + 1
asset_df = pl.DataFrame(
[
{"Asset Class": ac, "Count": count}
for ac, count in sorted(asset_counts.items(), key=lambda x: -x[1])
]
)
asset_df
```
**What to notice**:
- Equities dominate (3 pure + 1 hybrid), reflecting their importance in ML4T
- "Equities+Options" is a hybrid: trades equities using options-derived features
- Each non-equity asset class (Crypto, FX, Futures, Options, Multi-Asset) has
one dedicated case study showing unique mechanics
---
## 2. Evaluation Protocol Summary
Each case study defines a walk-forward evaluation protocol. The key parameters are:
- **Training window**: How much history to use for model fitting
- **Test window**: Validation fold duration
- **Holdout period**: Data set aside for final confirmation
```python
protocol_rows = []
for case_id, r in all_results.items():
d = r.get("diagnostics", {})
ho_s = d.get("holdout_start", "?")
ho_e = d.get("holdout_end", "?")
protocol_rows.append(
{
"Case Study": DISPLAY_NAMES.get(case_id, case_id),
"Train": d.get("train_size", "N/A"),
"Test": d.get("test_size", "N/A"),
"Folds": d.get("n_splits", 0),
"Holdout": f"{ho_s}-{ho_e}",
}
)
protocol_df = pl.DataFrame(protocol_rows)
protocol_df
```
**What to notice**:
- Training windows range from 6M (microstructure) to 10Y (firm characteristics),
reflecting both data availability and stationarity assumptions
- Fold counts vary from 2 (shorter histories: crypto, microstructure, options) to 16 (US equities)
- Every case study sets a holdout aside; this discipline is non-negotiable
---
## 3. Cost Model and Horizon Feasibility
Trading costs constrain viable horizons. This section summarizes the cost-horizon
analysis from each setup notebook.
### Cost Model Classes
| Class | Description | Implication |
|-------|-------------|-------------|
| **Dominant** | Costs are first-order; small edges live near the spread | Need very strong predictability; costs dominate feasibility |
| **Material** | Costs affect profitability but don't rule out trading | Horizon choice depends on signal decay vs cost hurdle |
The **dominant** cost regime (NASDAQ-100 microstructure, S&P 500 options) requires
unusually strong signals to overcome friction.
```python
cost_rows = []
for case_id, r in all_results.items():
s = r.get("summary", {})
cost_rows.append(
{
"Case Study": DISPLAY_NAMES.get(case_id, case_id),
"Cost Class": s.get("cost_model", ""),
"Decision Cadence": s.get("decision_cadence", ""),
}
)
cost_df = pl.DataFrame(cost_rows)
cost_df
```
The class alone does not say what the friction is. Each `setup.yaml` declares its own
cost components, in whatever unit that market quotes: basis points a leg, ticks,
a percentage of the option premium. Printing the declared block for the two extremes
is the only way to compare them without a number going stale in the prose here.
```python
def print_costs(block: dict, indent: int = 2) -> None:
"""Print a declared cost block, one entry per line, nesting by indent."""
for key, value in block.items():
if isinstance(value, dict):
print(f"{' ' * indent}{key}:")
print_costs(value, indent + 2)
else:
print(f"{' ' * indent}{key}: {value}")
for case_id in ("fx_pairs", "sp500_options"):
print(f"{DISPLAY_NAMES[case_id]} - costs declared in config/setup.yaml:")
print_costs(all_results[case_id]["costs"])
print()
```
**What to notice**:
- FX carries the tightest quoted spreads of the nine, in single-digit basis points a
leg and tighter on the majors than the crosses, which is what lets it decide daily
- An option's spread is quoted against its own premium rather than against notional,
and at the fraction printed above the cost is the binding constraint on the strategy
- Horizon choice aligns with cost: higher costs push toward longer holding periods
---
## 4. Prediction Coverage Across Case Studies
This figure shows the calendar-year data spans for all 9 case studies,
highlighting training, validation, and holdout periods.
### Compute Coverage
```python
def compute_coverage(results: dict[str, dict]) -> list[dict]:
"""Compute prediction coverage spans from results JSON data."""
coverage_data = []
for case_id, r in results.items():
d = r.get("diagnostics", {})
holdout_start = d.get("holdout_start")
holdout_end = d.get("holdout_end")
try:
holdout_start_year = int(str(holdout_start)[:4]) if holdout_start else None
holdout_end_year = int(str(holdout_end)[:4]) if holdout_end else None
except (ValueError, TypeError):
continue
if holdout_start_year is None or holdout_end_year is None:
continue
n_splits = d.get("n_splits", 5)
test_size = d.get("test_size", "1Y")
test_years = _window_to_years(test_size)
if test_years is None:
test_years = 1.0
val_span = n_splits * test_years
val_start_year = holdout_start_year - val_span
# Training starts before validation by the training window size
train_size = d.get("train_size", "1Y")
train_years = _window_to_years(train_size)
if train_years is None:
train_years = 1.0
data_start_year = val_start_year - train_years
coverage_data.append(
{
"id": case_id,
"name": DISPLAY_NAMES.get(case_id, case_id),
"data_start": data_start_year,
"val_start": val_start_year,
"holdout_start": holdout_start_year,
"holdout_end": holdout_end_year,
}
)
coverage_data.sort(key=lambda x: (x["data_start"], x["name"]))
return coverage_data
```
```python
case_studies_coverage = compute_coverage(all_results)
```
### Coverage Figure
```python
def plot_coverage(coverage_data):
"""Plot prediction coverage spans as horizontal stacked bars."""
fig, ax = plt.subplots(figsize=(12, 5.5))
bar_height = 0.65
for i, cs in enumerate(coverage_data):
y = len(coverage_data) - 1 - i
ax.barh(
y,
cs["val_start"] - cs["data_start"],
left=cs["data_start"],
height=bar_height,
color=TRAIN_C,
edgecolor="white",
linewidth=0.5,
)
ax.barh(
y,
cs["holdout_start"] - cs["val_start"],
left=cs["val_start"],
height=bar_height,
color=VAL_C,
edgecolor="white",
linewidth=0.5,
)
ax.barh(
y,
cs["holdout_end"] - cs["holdout_start"] + 1,
left=cs["holdout_start"],
height=bar_height,
color=HOLDOUT_C,
edgecolor="white",
linewidth=0.5,
)
ax.set_yticks(range(len(coverage_data)))
ax.set_yticklabels([cs["name"] for cs in reversed(coverage_data)])
ax.set_ylim(-0.7, len(coverage_data) - 0.3)
min_year = min(cs["data_start"] for cs in coverage_data) - 2
max_year = max(cs["holdout_end"] for cs in coverage_data) + 2
ax.set_xlim(min_year, max_year)
ax.set_xlabel("Year")
ax.tick_params(left=False)
legend_elements = [
Patch(facecolor=TRAIN_C, label="Training"),
Patch(facecolor=VAL_C, label="Validation"),
Patch(facecolor=HOLDOUT_C, label="Holdout (set aside)"),
]
ax.legend(
handles=legend_elements,
loc="upper left",
bbox_to_anchor=(1.01, 1.0),
frameon=True,
fancybox=False,
edgecolor="gray",
)
ax.set_title("Prediction Coverage Across Case Studies")
show_with_alt(
fig,
"One horizontal bar per case study on a shared year axis, each bar split into "
"a slate training span, an amber validation span and a pale holdout span in "
"that order. Both edges are ragged: the bars begin anywhere from 1990 to 2020 "
"and end anywhere from 2017 to 2026, with four of them reaching the same right "
"edge while the shortest, NASDAQ-100 microstructure, is a sliver beside them.",
)
```
```python
if case_studies_coverage:
plot_coverage(case_studies_coverage)
else:
print("No coverage data available. Run setup notebooks first.")
```
### Coverage Statistics (Computed)
```python
if case_studies_coverage:
earliest_start = min(cs["data_start"] for cs in case_studies_coverage)
latest_end = max(cs["holdout_end"] for cs in case_studies_coverage)
max_span = latest_end - earliest_start
longest_val = max(cs["holdout_start"] - cs["val_start"] for cs in case_studies_coverage)
shortest_val = min(cs["holdout_start"] - cs["val_start"] for cs in case_studies_coverage)
holdout_lengths = [cs["holdout_end"] - cs["holdout_start"] + 1 for cs in case_studies_coverage]
max_holdout = max(holdout_lengths)
min_holdout = min(holdout_lengths)
recent_datasets = [cs["name"] for cs in case_studies_coverage if cs["data_start"] >= 2020]
long_datasets = [cs["name"] for cs in case_studies_coverage if cs["data_start"] <= 1995]
print(f"Coverage spans {int(earliest_start)} to {int(latest_end)} ({int(max_span)} years)")
print(f"Validation periods: {shortest_val:.0f} to {longest_val:.0f} years")
print(f"Holdout periods: {min_holdout} to {max_holdout} years")
print(f"Recent datasets (2020+): {', '.join(recent_datasets) if recent_datasets else 'None'}")
print(
f"Long-history datasets (pre-1995): {', '.join(long_datasets) if long_datasets else 'None'}"
)
```
**Interpretation** (reconstructed from each protocol):
The spans above are implied by each walk-forward protocol (holdout, fold count,
and train/test windows), not raw data-availability dates. Key observations:
- **Longest histories** (US Equities, Firm Characteristics) provide deep validation
but may include regime changes that affect stationarity
- **Recent datasets** (Crypto, Microstructure) limit walk-forward depth but
reflect current market conditions
- **Holdout variation** reflects data availability: options data ends 2021,
constraining holdout to 1 year vs 2 years for other case studies
---
## 5. Quick Reference Table
This table consolidates key information for quick reference when working
with any case study in the book.
```python
reference_rows = []
for case_id, r in all_results.items():
s = r.get("summary", {})
d = r.get("diagnostics", {})
ho_s = d.get("holdout_start", "?")
ho_e = d.get("holdout_end", "?")
reference_rows.append(
{
"Case Study": DISPLAY_NAMES.get(case_id, case_id),
"Asset": s.get("asset_class", ""),
"N": s.get("universe_size", 0),
"Freq": s.get("data_frequency", ""),
"Cost": s.get("cost_model", "")[:3],
"Train": d.get("train_size", "N/A"),
"Folds": d.get("n_splits", 0),
"Holdout": f"{ho_s}-{ho_e}",
"Track": CHAPTER_TRACKS.get(case_id, ""),
}
)
reference_df = pl.DataFrame(reference_rows)
reference_df
```
**What to notice**:
- "Track" column shows which chapters use each case study, enabling readers
to follow specific datasets through the book
- Dominant-cost case studies (NASDAQ-100, Options) have shorter tracks,
reflecting their specialized, educational role
- Material-cost case studies carry through to later chapters (Ch14, Ch17, Ch21)
### Column Descriptions
| Column | Description |
|--------|-------------|
| **N** | Universe size (number of tradable assets) |
| **Freq** | Native data frequency |
| **Cost** | Cost model class (Dom=Dominant, Mat=Material) |
| **Train** | Training window size |
| **Folds** | Number of walk-forward validation folds |
| **Holdout** | Years of the holdout period |
| **Track** | Chapter sequence where this case study appears |
---
## 6. Setup Techniques Summary
How each case study maps signals to positions:
```python
technique_rows = []
for case_id, r in all_results.items():
t = r.get("techniques", {})
technique_rows.append(
{
"Case Study": DISPLAY_NAMES.get(case_id, case_id),
"Setup Type": t.get("setup_type", ""),
"Position Mapping": t.get("position_mapping", ""),
}
)
technique_df = pl.DataFrame(technique_rows)
technique_df
```
---
## Key Takeaways
1. **Diversity by design**: The 9 case studies span equities, crypto, FX, futures,
options, and multi-asset ETFs, demonstrating ML4T workflow adaptability.
2. **Cost models matter**: The cost regime (dominant vs material) determines
viable horizons. Microstructure and options strategies face dominant costs
that require exceptionally strong signals.
3. **Protocol heterogeneity**: Training windows range from 6 months (microstructure)
to 10 years (firm characteristics), reflecting data availability and
stationarity assumptions.
4. **Holdout discipline**: Every case study reserves a holdout period that is set
aside and never used for development decisions. This discipline is essential for
honest performance estimation.
5. **Coverage varies**: Historical depth ranges from recent (2020+ for crypto)
to decades (1990 for US equities), affecting the reliability
of walk-forward estimates.
**Next**: Individual setup notebooks (`case_studies/*/01_feasibility_analysis.py`) contain
the detailed trading setup and evaluation protocol for each case study.
Hiển thị toàn văn kèm ghi nguồn theo giấy phép của tài liệu gốc. Giấy phép: MIT
Bản tóm tắt này do tác nhân nghiên cứu của Stratmill biên soạn từ tài liệu gốc; đây không phải bản sao của tài liệu.