بینچ مارک کی مطابقت اور اسٹریٹیجی کی نسبتی کارکردگی کی تشخیص
خلاصہ
دستاویز اسٹریٹیجی کی واپسیوں کا دو بیرونی بینچ مارکس سے موازنہ کرنے کے طریقے بتاتی ہے: روزانہ SPY کل واپسی اور فاما۔فرینچ مارکیٹ واپسی، جو مارکیٹ رسک پریمیم اور بے خطر شرح جمع کر کے بنتی ہے۔ اس میں تاریخ کی چھانٹی اور مطابقت کے معاون طریقے شامل ہیں، مثلاً ان ڈیٹا سیٹس کے لیے مہینے کی بنیاد پر جوڑ، جن کے ماہانہ ٹائم اسٹیمپس کے طریقے مختلف ہیں۔ تشخیصی پیمانے نکالنے سے پہلے کال کرنے والے کو واپسیوں کی مطابقت رکھنے والی تعدد یقینی بنا کر مشاہدات ملانے چاہئیں۔
تشخیصی حسابات میں انفارمیشن ریشو، بیٹا، باہمی تعلق اور سالانہ ٹریکنگ ایرر شامل ہیں، نیز اختیاری ذیلی مدت کے خلاصے: اسٹریٹیجی کی واپسی، سالانہ واپسی، شارپ ریشو اور زیادہ سے زیادہ ڈرا ڈاؤن۔ یہ جوڑی دار غائب قدریں ہٹا دیتے ہیں اور جب مشاہدات بہت کم ہوں یا تغیر صفر ہو تو غیر متعین پیمانے لوٹاتے ہیں۔ یہ پیمانے نسبتی رویہ بیان کرتے ہیں، یہ نہیں کہ اسٹریٹیجی قابلِ سرمایہ کاری ہے یا قائم رہنے کا امکان ہے۔ دستاویز تجرباتی نتائج کے بجائے نفاذ کی رہنمائی دیتی ہے، اور سالانہ بنانے کا حساب صارف کی فراہم کردہ مدت کی تعدد پر منحصر ہے۔
اہم خیالات
- SPY کے کلوز ٹو کلوز ریٹرنز اور فاما۔فرینچ مارکیٹ ریٹرنز متبادل تقابلی سلسلے فراہم کرتے ہیں۔
- روزانہ ٹائم اسٹیمپس براہِ راست جوڑے جا سکتے ہیں، جبکہ ماہانہ سلسلوں کے لیے کیلنڈر مہینے سے مطابقت درکار ہو سکتی ہے۔
- بینچ مارک تشخیص نسبتی واپسی، بیٹا، باہمی تعلق اور ٹریکنگ ایرر کا خلاصہ کرتی ہے۔
- ذیلی مدت کے حسابات دکھا سکتے ہیں کہ متعین تاریخوں کے گروپس میں اسٹریٹیجی کی کارکردگی کیسے بدلتی ہے۔
- تعدد، غائب مشاہدات اور صفر تغیر رپورٹ شدہ پیمانوں کی صحت کو متاثر کرتے ہیں۔
ٹیگز
مکمل متن
# external_benchmarks.py
```py
"""External benchmark loaders for strategy-analysis notebooks.
Provides SPY (broad-equity ETF) and FF-market (Ken French Mkt-RF + RF)
return series on the canonical schema (`timestamp`, `benchmark_return`).
Both flavors return polars DataFrames; alignment with strategy returns
is the caller's responsibility.
The diagnostic helper `compute_benchmark_diagnostics` returns the
information ratio, beta, correlation, and tracking error of a strategy
return series against a benchmark return series.
"""
from __future__ import annotations
import datetime as _dt
import numpy as np
import polars as pl
from data import load_etfs, load_ff_factors
def _date_or_none(value: str | _dt.date | _dt.datetime | None) -> _dt.date | None:
if value is None:
return None
if isinstance(value, _dt.date) and not isinstance(value, _dt.datetime):
return value
if isinstance(value, _dt.datetime):
return value.date()
return _dt.date.fromisoformat(str(value)[:10])
def load_spy_returns(
start: str | _dt.date | None = None,
end: str | _dt.date | None = None,
) -> pl.DataFrame:
"""SPY daily total returns (close-to-close pct change).
Returns columns: `timestamp` (Date), `benchmark_return` (Float64).
"""
df = load_etfs(symbols=["SPY"]).sort("timestamp").select("timestamp", "close")
df = (
df.with_columns(
benchmark_return=pl.col("close").pct_change(),
)
.drop("close")
.drop_nulls("benchmark_return")
)
s, e = _date_or_none(start), _date_or_none(end)
if s is not None:
df = df.filter(pl.col("timestamp") >= s)
if e is not None:
df = df.filter(pl.col("timestamp") <= e)
return df
def load_ff_market_returns(
start: str | _dt.date | None = None,
end: str | _dt.date | None = None,
frequency: str = "daily",
) -> pl.DataFrame:
"""Fama-French market return (Mkt-RF + RF, i.e. nominal market).
`frequency` is `"daily"` or `"monthly"`. Returns columns:
`timestamp` (Date), `benchmark_return` (Float64).
"""
df = load_ff_factors(dataset="ff5", frequency=frequency).sort("timestamp")
df = df.with_columns(
benchmark_return=pl.col("Mkt-RF") + pl.col("RF"),
).select("timestamp", "benchmark_return")
s, e = _date_or_none(start), _date_or_none(end)
if s is not None:
df = df.filter(pl.col("timestamp") >= s)
if e is not None:
df = df.filter(pl.col("timestamp") <= e)
return df
def align_to_strategy(
strategy_df: pl.DataFrame,
benchmark_df: pl.DataFrame,
timestamp_col: str = "ts",
strategy_col: str = "strategy",
benchmark_col: str = "benchmark_return",
) -> pl.DataFrame:
"""Inner-join a benchmark series onto strategy returns.
Both inputs must carry the canonical timestamp column (Date). The
result has columns `[timestamp_col, strategy_col, benchmark_col]`.
Caller-side alignment for monthly cadences should resample first.
"""
bench = benchmark_df.with_columns(
pl.col("timestamp").cast(pl.Date).alias(timestamp_col)
).select(timestamp_col, benchmark_col)
return strategy_df.join(bench, on=timestamp_col, how="inner").sort(timestamp_col)
def align_to_strategy_monthly(
strategy_df: pl.DataFrame,
benchmark_df: pl.DataFrame,
timestamp_col: str = "ts",
strategy_col: str = "strategy",
benchmark_col: str = "benchmark_return",
) -> pl.DataFrame:
"""Month-anchored inner join (FF monthly is first-of-month; benchmark
series often end-of-month). Joins on (year, month) key.
"""
s = strategy_df.with_columns(
_y=pl.col(timestamp_col).dt.year(),
_m=pl.col(timestamp_col).dt.month(),
)
b = benchmark_df.with_columns(
_y=pl.col("timestamp").dt.year(),
_m=pl.col("timestamp").dt.month(),
).select("_y", "_m", benchmark_col)
return s.join(b, on=["_y", "_m"], how="inner").drop("_y", "_m").sort(timestamp_col)
def compute_subperiod_diagnostics(
df: pl.DataFrame,
buckets: list[tuple[str, _dt.date | str, _dt.date | str]],
*,
timestamp_col: str = "ts",
strategy_col: str = "strategy",
benchmark_col: str | None = "benchmark",
periods_per_year: int = 252,
) -> pl.DataFrame:
"""Per-bucket diagnostics for sub-period decomposition tables.
`df` must carry an aligned `(timestamp, strategy[, benchmark])` triple.
`buckets` is a list of `(label, start_date, end_date)` triples; date
bounds are inclusive at both ends. Per-bucket metrics: `n`,
`cum_return`, `ann_return`, `sharpe`, `max_drawdown`. When
`benchmark_col` is provided, the table also includes `beta_vs_bm` and
`info_ratio_vs_bm`.
"""
rows: list[dict[str, object]] = []
for label, start, end in buckets:
s = _date_or_none(start)
e = _date_or_none(end)
sub = df.filter((pl.col(timestamp_col) >= s) & (pl.col(timestamp_col) <= e))
n = sub.height
if n == 0:
rows.append({"bucket": label, "n": 0})
continue
s_arr = sub[strategy_col].to_numpy()
cum = float(np.prod(1 + s_arr) - 1)
if periods_per_year > 0:
ann = (1 + cum) ** (periods_per_year / max(n, 1)) - 1
else:
ann = float("nan")
std = float(np.std(s_arr, ddof=1)) if n > 1 else 0.0
sharpe = (
float(np.mean(s_arr) / std * np.sqrt(periods_per_year)) if std > 0 else float("nan")
)
equity = np.cumprod(1 + s_arr)
peak = np.maximum.accumulate(equity)
max_dd = float(np.min(equity / peak - 1))
row: dict[str, object] = {
"bucket": label,
"n": n,
"cum_return": cum,
"ann_return": ann,
"sharpe": sharpe,
"max_drawdown": max_dd,
}
if benchmark_col is not None and benchmark_col in sub.columns:
b_arr = sub[benchmark_col].to_numpy()
diag = compute_benchmark_diagnostics(s_arr, b_arr, periods_per_year)
row["beta_vs_bm"] = diag["beta"]
row["info_ratio_vs_bm"] = diag["info_ratio"]
rows.append(row)
return pl.DataFrame(rows)
def compute_benchmark_diagnostics(
strategy: np.ndarray,
benchmark: np.ndarray,
periods_per_year: int = 252,
) -> dict[str, float | int]:
"""Information ratio, beta, correlation, tracking error vs benchmark.
Inputs are aligned period returns; lengths must match. NaNs are
dropped pairwise. Returns NaN for any metric whose inputs are
degenerate (all-NaN or zero variance).
"""
s = np.asarray(strategy, dtype=float)
b = np.asarray(benchmark, dtype=float)
if s.shape != b.shape:
raise ValueError(f"shape mismatch: strategy {s.shape} vs benchmark {b.shape}")
mask = np.isfinite(s) & np.isfinite(b)
s, b = s[mask], b[mask]
n = int(s.size)
out: dict[str, float | int] = {"n": n}
if n < 2:
return {
**out,
"info_ratio": float("nan"),
"beta": float("nan"),
"correlation": float("nan"),
"tracking_error": float("nan"),
}
excess = s - b
te_period = float(np.std(excess, ddof=1))
te_ann = te_period * np.sqrt(periods_per_year)
if te_period > 0:
ir = float(np.mean(excess) * periods_per_year / te_ann)
else:
ir = float("nan")
var_b = float(np.var(b, ddof=1))
beta = float(np.cov(s, b, ddof=1)[0, 1] / var_b) if var_b > 0 else float("nan")
if np.std(s, ddof=1) > 0 and np.std(b, ddof=1) > 0:
corr = float(np.corrcoef(s, b)[0, 1])
else:
corr = float("nan")
return {
"n": n,
"info_ratio": ir,
"beta": beta,
"correlation": corr,
"tracking_error": te_ann,
}
```ماخذ کا حوالہ دیتے ہوئے مکمل متن دکھایا گیا ہے، ماخذ کے لائسنس کے تحت۔ لائسنس: MIT
یہ خلاصہ اصل ماخذ سے Stratmill کے تحقیقی ایجنٹ نے لکھا ہے؛ یہ ماخذ کی نقل نہیں۔