Merge pull request #1824 for October 14 maintenance
This commit is contained in:
commit
767c16bba7
5 files changed
+129
-19
No files matched your search
@@ -870,14 +870,14 @@ that is a bug, not a pattern.
|
||||
| Module | What it covers |
|
||||
|--------|----------------|
|
||||
| `options` | Black-Scholes price + greeks, implied-volatility inversion |
|
||||
| `fixedincome` | Bond math, Nelson-Siegel / Svensson curve fitting |
|
||||
| `fixedincome` | Bond math, Nelson-Siegel / Svensson curve fitting; schedules, accrual, key-rate tenors and yield inversion reject non-finite inputs by name. |
|
||||
| `credit` | Altman Z-score, Merton / KMV distance-to-default; CDS premium dates retain their configured frequency, with a final stub when maturity falls between coupons. |
|
||||
| `timeseries` | Stationarity, cointegration, GARCH, bootstrap; OU fits use adjacent observed lag pairs; missing observations never become artificial one-step transitions. |
|
||||
| `risk` · `var_backtest` | VaR / CVaR / EVT and their backtests |
|
||||
| `attribution` | Brinson-Fachler decomposition |
|
||||
| `performance` · `fundmath` | TWR / MWR / Modified Dietz; XIRR / MOIC / DPI / TVPI |
|
||||
| `factormodel` · `eventstudy` | Factor regressions, event studies; portfolio style exposure reports incomplete factor rows as unmatched weight. |
|
||||
| `multipletesting` · `crossvalidation` | Deflated significance, purged CV; purged walk-forward splits omit folds with no training observations left after purging. |
|
||||
| `multipletesting` · `crossvalidation` | Deflated significance, purged CV; purged walk-forward splits omit folds with no training observations left after purging; Sharpe significance summaries reject non-finite inputs by name. |
|
||||
| `impact` | Market-impact models |
|
||||
| `volatility` | Heston (1993) stochastic-volatility pricing; Heston pricing requires finite inputs and a positive integration limit; invalid inputs return a domain error. |
|
||||
| `portfolio` | Hierarchical Risk Parity allocation |
|
||||
|
||||
@@ -173,10 +173,15 @@ def accrued_interest(
|
||||
Accrued interest in the same currency units as ``face``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``freq`` is not positive, if the coupon dates are not
|
||||
ordered, if ``settlement`` falls outside the coupon period, or if
|
||||
``day_count`` is not a recognised convention.
|
||||
ValueError: If any numeric input is non-finite, if ``freq`` is not
|
||||
positive, if the coupon dates are not ordered, if ``settlement``
|
||||
falls outside the coupon period, or if ``day_count`` is not a
|
||||
recognised convention.
|
||||
"""
|
||||
# Before the endpoint shortcuts: settling exactly on a coupon date returns
|
||||
# a constant, which would otherwise launder a NaN face or rate into a
|
||||
# clean-looking 0.0.
|
||||
_require_finite(face=face, coupon_rate=coupon_rate, freq=freq)
|
||||
if freq <= 0:
|
||||
raise ValueError(f"freq must be positive, got {freq}")
|
||||
if not last_coupon < next_coupon:
|
||||
@@ -227,10 +232,13 @@ def bond_cashflows(
|
||||
``periods / freq``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``face`` is not positive, if ``n_periods`` is not a
|
||||
positive integer, or if ``freq`` is not positive.
|
||||
ValueError: If any numeric input is non-finite, if ``face`` is not
|
||||
positive, if ``n_periods`` is not a positive integer, or if
|
||||
``freq`` is not positive.
|
||||
"""
|
||||
_require_finite(face=face, coupon_rate=coupon_rate)
|
||||
# freq too: with an infinite frequency every coupon collapses to zero and
|
||||
# the schedule reprices to a plausible-looking par.
|
||||
_require_finite(face=face, coupon_rate=coupon_rate, freq=freq)
|
||||
if face <= 0:
|
||||
raise ValueError(f"face must be positive, got {face}")
|
||||
if isinstance(n_periods, bool) or not isinstance(n_periods, (int, np.integer)) or n_periods <= 0:
|
||||
@@ -321,9 +329,13 @@ def ytm_solve(
|
||||
Annual yield to maturity as a decimal.
|
||||
|
||||
Raises:
|
||||
ValueError: If the price is not reachable inside ``bracket``.
|
||||
ValueError: If ``price`` or a bracket bound is non-finite, or if the
|
||||
price is not reachable inside ``bracket``.
|
||||
"""
|
||||
low, high = bracket
|
||||
# NaN fails ``low >= high`` and every solver evaluation; name the input
|
||||
# instead of reporting a bracket that "cannot reprice" it.
|
||||
_require_finite(price=price, bracket_low=low, bracket_high=high)
|
||||
if low >= high:
|
||||
raise ValueError(f"bracket must be increasing, got {bracket}")
|
||||
|
||||
@@ -424,11 +436,15 @@ def key_rate_duration(
|
||||
Dict mapping each key rate tenor (float) to its Key Rate Duration (float in years).
|
||||
|
||||
Raises:
|
||||
ValueError: If key_rates is empty or not strictly increasing, or schedule/compounding invalid.
|
||||
ValueError: If key_rates is empty, non-finite or not strictly
|
||||
increasing, or schedule/compounding invalid.
|
||||
"""
|
||||
kr = np.asarray(key_rates, dtype=float)
|
||||
if kr.size == 0:
|
||||
raise ValueError("key_rates must contain at least one tenor")
|
||||
# A NaN tenor is neither empty nor non-increasing, so it sailed past both
|
||||
# checks and came back as a NaN key in the result dict.
|
||||
_require_finite(key_rates=kr)
|
||||
if (np.diff(kr) <= 0.0).any():
|
||||
raise ValueError(f"key_rates must be strictly increasing, got {key_rates}")
|
||||
|
||||
@@ -528,10 +544,14 @@ def dv01(
|
||||
positive number.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``face`` is zero, or the schedule/compounding is invalid.
|
||||
ValueError: If ``face`` is zero, if ``par_amount`` is non-finite, or
|
||||
the schedule/compounding is invalid.
|
||||
"""
|
||||
if face == 0:
|
||||
raise ValueError("face must be non-zero")
|
||||
# face itself is admitted by the schedule checks downstream; par_amount
|
||||
# never reaches them, so a NaN position size would scale into a NaN hedge.
|
||||
_require_finite(par_amount=par_amount)
|
||||
d_mod = modified_duration(face, coupon_rate, ytm, n_periods, freq, compounding)
|
||||
price = bond_price(face, coupon_rate, ytm, n_periods, freq, compounding)
|
||||
return d_mod * (price / face) * 1e-4 * par_amount
|
||||
|
||||
@@ -243,11 +243,24 @@ def probabilistic_sharpe_ratio(
|
||||
Probability in ``[0, 1]``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_observations`` is below :data:`MIN_OBSERVATIONS`, if
|
||||
``kurtosis`` is below 1 (impossible for any distribution), or if the
|
||||
variance term is non-positive, which means the supplied moments are
|
||||
mutually inconsistent rather than merely extreme.
|
||||
ValueError: If any summary input is non-finite, if ``n_observations``
|
||||
is below :data:`MIN_OBSERVATIONS`, if ``kurtosis`` is below 1
|
||||
(impossible for any distribution), or if the variance term is
|
||||
non-positive, which means the supplied moments are mutually
|
||||
inconsistent rather than merely extreme.
|
||||
"""
|
||||
# Every check below is a comparison, and a comparison with NaN is False:
|
||||
# without this admission, a NaN benchmark returned NaN as "confidence" and
|
||||
# an infinite one returned a clean 0.0 or 1.0.
|
||||
for name, value in (
|
||||
("observed_sharpe", observed_sharpe),
|
||||
("n_observations", n_observations),
|
||||
("benchmark_sharpe", benchmark_sharpe),
|
||||
("skew", skew),
|
||||
("kurtosis", kurtosis),
|
||||
):
|
||||
if not np.isfinite(value):
|
||||
raise ValueError(f"{name} must be finite, got {value}")
|
||||
if n_observations < MIN_OBSERVATIONS:
|
||||
raise ValueError(
|
||||
f"need at least {MIN_OBSERVATIONS} observations, got {n_observations}"
|
||||
@@ -257,8 +270,6 @@ def probabilistic_sharpe_ratio(
|
||||
f"kurtosis must be the non-excess fourth moment (>= 1, 3 for a "
|
||||
f"Gaussian), got {kurtosis}. Excess kurtosis was probably passed."
|
||||
)
|
||||
if not np.isfinite(observed_sharpe):
|
||||
raise ValueError(f"observed_sharpe must be finite, got {observed_sharpe}")
|
||||
|
||||
variance_term = (
|
||||
1.0 - skew * observed_sharpe + (kurtosis - 1.0) / 4.0 * observed_sharpe**2
|
||||
@@ -300,9 +311,16 @@ def expected_maximum_sharpe(n_trials: int, trial_sharpe_std: float) -> float:
|
||||
``trial_sharpe_std`` is zero.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_trials`` is below 1 or ``trial_sharpe_std`` is
|
||||
negative.
|
||||
ValueError: If either input is non-finite, if ``n_trials`` is below 1
|
||||
or ``trial_sharpe_std`` is negative.
|
||||
"""
|
||||
# NaN fails every comparison below and norm.ppf(1 - 1/inf) is inf: the
|
||||
# first surfaces as a NaN bar, the second as an unbeatably infinite one,
|
||||
# and deflated_sharpe_ratio reports both as an ordinary pass/fail verdict.
|
||||
if not np.isfinite(n_trials):
|
||||
raise ValueError(f"n_trials must be finite, got {n_trials}")
|
||||
if not np.isfinite(trial_sharpe_std):
|
||||
raise ValueError(f"trial_sharpe_std must be finite, got {trial_sharpe_std}")
|
||||
if n_trials < 1:
|
||||
raise ValueError(f"n_trials must be at least 1, got {n_trials}")
|
||||
if trial_sharpe_std < 0.0:
|
||||
|
||||
@@ -575,6 +575,20 @@ def test_fit_yield_curve_rejects_nonfinite_observations(bad):
|
||||
(lambda bad: nelson_siegel(bad, 0.04, -0.01, 0.01, 2.0), "maturities"),
|
||||
(lambda bad: nelson_siegel(5.0, 0.04, bad, 0.01, 2.0), "beta1"),
|
||||
(lambda bad: svensson(5.0, 0.04, -0.01, 0.01, 0.01, 2.0, bad), "decay"),
|
||||
# An infinite frequency priced a bullet bond at par: every coupon
|
||||
# collapses to zero and each discount factor to one.
|
||||
(lambda bad: bond_price(100.0, 0.05, 0.05, 10, freq=bad), "freq"),
|
||||
(lambda bad: bond_cashflows(100.0, 0.05, 10, freq=bad), "freq"),
|
||||
(lambda bad: macaulay_duration(100.0, 0.05, 0.05, 10, freq=bad), "freq"),
|
||||
# NaN fails ``low >= high`` and every solver probe; the old error
|
||||
# blamed the bracket for an unreachable price.
|
||||
(lambda bad: ytm_solve(bad, 100.0, 0.05, 10), "price"),
|
||||
(lambda bad: ytm_solve(95.0, 100.0, 0.05, 10, bracket=(bad, 10.0)), "bracket_low"),
|
||||
(lambda bad: ytm_solve(95.0, 100.0, 0.05, 10, bracket=(-0.5, bad)), "bracket_high"),
|
||||
# Neither empty nor non-increasing, so a NaN tenor passed both checks.
|
||||
(lambda bad: key_rate_duration(100.0, 0.05, 0.05, 10, key_rates=[1.0, bad]), "key_rates"),
|
||||
# par_amount never reaches the schedule checks downstream.
|
||||
(lambda bad: dv01(100.0, 0.05, 0.05, 10, par_amount=bad), "par_amount"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("bad", [np.nan, np.inf])
|
||||
@@ -582,3 +596,30 @@ def test_a_non_finite_input_is_named_not_priced(call, name, bad):
|
||||
"""Each range check is a comparison, and NaN fails none of them: it came out as a NaN price."""
|
||||
with pytest.raises(ValueError, match=f"{name} must be finite"):
|
||||
call(bad)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["face", "coupon_rate", "freq"])
|
||||
@pytest.mark.parametrize("bad", [np.nan, np.inf, -np.inf])
|
||||
@pytest.mark.parametrize(
|
||||
"settlement", [dt.date(2026, 1, 1), dt.date(2026, 4, 1), dt.date(2026, 7, 1)]
|
||||
)
|
||||
def test_accrued_interest_rejects_non_finite_inputs(field, bad, settlement):
|
||||
"""The on-coupon-date shortcuts return constants, so admission must run
|
||||
before them or a NaN face laundered into a clean 0.0."""
|
||||
params = dict(
|
||||
face=100.0,
|
||||
coupon_rate=0.05,
|
||||
freq=2,
|
||||
last_coupon=dt.date(2026, 1, 1),
|
||||
settlement=settlement,
|
||||
next_coupon=dt.date(2026, 7, 1),
|
||||
)
|
||||
params[field] = bad
|
||||
with pytest.raises(ValueError, match=f"{field} must be finite"):
|
||||
accrued_interest(**params)
|
||||
|
||||
|
||||
def test_bond_price_with_infinite_frequency_is_not_par():
|
||||
"""freq=inf used to price every bullet bond at exactly face."""
|
||||
with pytest.raises(ValueError, match="freq must be finite"):
|
||||
bond_price(100.0, 0.05, 0.04, 4, freq=np.inf)
|
||||
@@ -340,3 +340,34 @@ def test_cscv_logits_and_pbo_agree():
|
||||
result = probability_of_backtest_overfitting(data, n_splits=6)
|
||||
assert result.pbo == pytest.approx((result.logits <= 0).mean())
|
||||
assert result.logits.size == result.n_splits
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
["observed_sharpe", "n_observations", "benchmark_sharpe", "skew", "kurtosis"],
|
||||
)
|
||||
@pytest.mark.parametrize("bad", [np.nan, np.inf, -np.inf])
|
||||
def test_psr_rejects_non_finite_summary_inputs(field, bad):
|
||||
"""A NaN benchmark returned NaN as 'confidence'; an infinite one returned
|
||||
a clean 0.0 or 1.0. Undefined inputs are named, not reported."""
|
||||
params = dict(
|
||||
observed_sharpe=0.1,
|
||||
n_observations=500,
|
||||
benchmark_sharpe=0.0,
|
||||
skew=0.0,
|
||||
kurtosis=3.0,
|
||||
)
|
||||
params[field] = bad
|
||||
with pytest.raises(ValueError, match=f"{field} must be finite"):
|
||||
probabilistic_sharpe_ratio(**params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["n_trials", "trial_sharpe_std"])
|
||||
@pytest.mark.parametrize("bad", [np.nan, np.inf, -np.inf])
|
||||
def test_expected_maximum_sharpe_rejects_non_finite_inputs(field, bad):
|
||||
"""NaN and inf sail through the comparisons below into a NaN or infinite
|
||||
bar, which deflated_sharpe_ratio then reports as an ordinary verdict."""
|
||||
params = dict(n_trials=100, trial_sharpe_std=0.1)
|
||||
params[field] = bad
|
||||
with pytest.raises(ValueError, match=f"{field} must be finite"):
|
||||
expected_maximum_sharpe(**params)
|
||||
Reference in new issue
Block a user