So sánh nghiên cứu tình huống giao dịch, chi phí và quy trình kiểm thử cuốn chiếu
Tóm tắt
Bản tổng quan này hợp nhất dữ liệu cấu hình từ chín nghiên cứu tình huống giao dịch về cổ phiếu, crypto, ngoại hối, hợp đồng tương lai, quyền chọn và danh mục đa tài sản. Bản tổng quan so sánh nhóm tài sản, tần suất dữ liệu, nhịp ra quyết định, mô hình chi phí và cách chuyển tín hiệu thành vị thế trong từng nghiên cứu. Ví dụ cho thấy các lựa chọn này thay đổi thế nào giữa các chiến lược và tập dữ liệu, gồm cả những thiết lập chịu ảnh hưởng đáng kể từ chi phí giao dịch.
Notebook cũng tóm tắt các cửa sổ huấn luyện và xác thực kiểm thử cuốn chiếu, số fold và các giai đoạn giữ lại dành riêng. Bài học nghiên cứu chính là cần xem xét thiết kế đánh giá và giả định chi phí cùng với nhóm tài sản và khoảng thời gian giao dịch; giữ tập giữ lại ngoài quá trình phát triển giúp đánh giá cuối cùng đáng tin cậy hơn. Đây là tài liệu tham khảo ở cấp cấu hình chứ không phải so sánh hiệu suất: bản tổng quan không xác lập nghiên cứu tình huống hay quy trình nào tạo ra lợi suất tốt hơn. Các lựa chọn chi tiết về tính khả thi và đánh giá vẫn phụ thuộc vào thiết lập của từng nghiên cứu.
Ý chính
- Cấu hình nghiên cứu giao dịch khác nhau về loại tài sản, quy mô nhóm tài sản, tần suất dữ liệu và nhịp ra quyết định.
- Chế độ chi phí giúp xác định khoảng thời gian và tín hiệu nào có thể khả thi.
- Các fold kiểm thử cuốn chiếu và giai đoạn giữ lại dành riêng sắp xếp việc phát triển mô hình và đánh giá cuối cùng.
- Tổng quan nghiên cứu tình huống tóm tắt các lựa chọn thiết lập nhưng không so sánh kết quả thực hiện của chiến lược.
Thẻ
Toàn văn
# 03_case_study_overview.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]
# # 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.
# %%
"""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"]
# %% tags=["parameters"]
# Production defaults — Papermill injects overrides for CI
MAX_SYMBOLS = 0 # 0 = all
# %%
CASE_STUDIES_DIR = REPO_ROOT / "case_studies"
# %% [markdown]
# ## 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.
# %%
# 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",
}
# %%
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", ""),
},
}
# %% [markdown]
# ### Infer Asset Class
# %%
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")
# %% [markdown]
# ### Load All Case Study Configs
# %%
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
# %%
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))}")
# %% [markdown]
# ## Helper: Window Conversion
# %%
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
# %% [markdown]
# ---
#
# ## 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.
# %%
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
# %% [markdown]
# **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
# %% [markdown]
# ### Asset Class Distribution
# %%
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
# %% [markdown]
# **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
# %% [markdown]
# ---
#
# ## 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
# %%
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
# %% [markdown]
# **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
# %% [markdown]
# ---
#
# ## 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.
# %%
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
# %% [markdown]
# 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.
# %%
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()
# %% [markdown]
# **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
# %% [markdown]
# ---
#
# ## 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.
# %% [markdown]
# ### Compute Coverage
# %%
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
# %%
case_studies_coverage = compute_coverage(all_results)
# %% [markdown]
# ### Coverage Figure
# %%
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.",
)
# %%
if case_studies_coverage:
plot_coverage(case_studies_coverage)
else:
print("No coverage data available. Run setup notebooks first.")
# %% [markdown]
# ### Coverage Statistics (Computed)
# %%
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'}"
)
# %% [markdown]
# **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
# %% [markdown]
# ---
#
# ## 5. Quick Reference Table
#
# This table consolidates key information for quick reference when working
# with any case study in the book.
# %%
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
# %% [markdown]
# **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)
# %% [markdown]
# ### 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 |
# %% [markdown]
# ---
#
# ## 6. Setup Techniques Summary
#
# How each case study maps signals to positions:
# %%
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
# %% [markdown]
# ---
#
# ## 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.