Any non-finite input to gross_scaler now returns 0.0 (flat) instead of silently
propagating through min()/max() to the non-NaN argument and potentially inverting
to maximum exposure. A negative/misconfigured gross_cap is clamped to 0.0 (not
negative). pretrade_block blocks on NaN session_pnl_pct ("session_pnl_unknown")
and on a misconfigured session_dd that is non-finite or non-negative
("bad_session_dd_config"). cap_weights zeros all outputs on non-finite NLV, zeros
individual symbols on non-finite weights, and skips the ADV cap when ADV is
non-finite (rather than weakening the per_symbol_cap). GateConfig gains kelly_cap
field for cohesion. Locked by 13 new NaN/edge-case unit tests (166 total, all pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.5 KiB
Python
102 lines
4.5 KiB
Python
"""capacity.cap_weights: never let a symbol's target notional exceed per_symbol_cap×NLV or a share of ADV."""
|
||
from __future__ import annotations
|
||
|
||
from fxhnt.domain.execution.capacity import cap_weights
|
||
|
||
|
||
def test_per_symbol_cap_binds() -> None:
|
||
w = {"A": 0.5, "B": -0.5}
|
||
out = cap_weights(w, nlv=10_000.0, adv_usd={"A": 1e9, "B": 1e9}, per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert abs(out["A"]) <= 0.05 + 1e-9 and abs(out["B"]) <= 0.05 + 1e-9
|
||
|
||
|
||
def test_adv_cap_binds_for_thin_symbol() -> None:
|
||
# B is thin: ADV 1000 USD, participation 0.05 -> max 50 USD; at NLV 10k that's weight 0.005
|
||
out = cap_weights({"A": 0.5, "B": 0.5}, nlv=10_000.0, adv_usd={"A": 1e9, "B": 1000.0},
|
||
per_symbol_cap=1.0, participation_rate=0.05)
|
||
assert abs(out["B"]) <= 0.005 + 1e-9
|
||
assert abs(out["A"]) > abs(out["B"])
|
||
|
||
|
||
def test_nlv_zero_returns_zeros() -> None:
|
||
"""NLV ≤ 0 means no capital — all weights must be zeroed."""
|
||
out = cap_weights({"A": 0.5, "B": -0.3}, nlv=0.0, adv_usd={"A": 1e9, "B": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert out == {"A": 0.0, "B": 0.0}
|
||
|
||
|
||
def test_nlv_negative_returns_zeros() -> None:
|
||
out = cap_weights({"X": 0.99}, nlv=-500.0, adv_usd={"X": 1e9},
|
||
per_symbol_cap=0.1, participation_rate=1.0)
|
||
assert out == {"X": 0.0}
|
||
|
||
|
||
def test_per_symbol_cap_preserves_sign() -> None:
|
||
"""Clamp must preserve sign: a short weight stays short after capping."""
|
||
out = cap_weights({"A": -0.8}, nlv=10_000.0, adv_usd={"A": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert out["A"] == -0.05
|
||
|
||
|
||
def test_symbol_missing_from_adv_uses_only_per_symbol_cap() -> None:
|
||
"""If a symbol has no ADV entry, only the per_symbol_cap applies (no ADV cap)."""
|
||
out = cap_weights({"Z": 0.99}, nlv=10_000.0, adv_usd={},
|
||
per_symbol_cap=0.10, participation_rate=0.05)
|
||
assert abs(out["Z"]) <= 0.10 + 1e-9
|
||
assert abs(out["Z"]) > 0.05 # ADV cap did NOT apply (no ADV entry)
|
||
|
||
|
||
def test_adv_cap_binding_vs_per_symbol_cap() -> None:
|
||
"""When ADV cap is tighter than per_symbol_cap, ADV cap wins."""
|
||
# ADV=100, participation=0.05 -> max 5 USD; NLV=10k -> max_w = 0.0005
|
||
# per_symbol_cap=0.5 (much larger) — ADV cap must bind
|
||
out = cap_weights({"A": 0.5}, nlv=10_000.0, adv_usd={"A": 100.0},
|
||
per_symbol_cap=0.5, participation_rate=0.05)
|
||
assert abs(out["A"]) <= 0.0005 + 1e-9
|
||
|
||
|
||
def test_empty_weights_returns_empty() -> None:
|
||
assert cap_weights({}, nlv=10_000.0, adv_usd={}, per_symbol_cap=0.1, participation_rate=1.0) == {}
|
||
|
||
|
||
def test_weight_within_caps_unchanged() -> None:
|
||
"""A weight already under both caps must pass through unmodified."""
|
||
w = {"A": 0.02}
|
||
out = cap_weights(w, nlv=10_000.0, adv_usd={"A": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert abs(out["A"] - 0.02) < 1e-12
|
||
|
||
|
||
# --- M1: NaN / non-finite safety tests ---
|
||
|
||
def test_nan_nlv_returns_all_zeros() -> None:
|
||
"""NaN NLV means capital is unknown — zero all weights rather than risk max exposure."""
|
||
out = cap_weights({"A": 0.5, "B": -0.3}, nlv=float("nan"), adv_usd={"A": 1e9, "B": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert out == {"A": 0.0, "B": 0.0}
|
||
|
||
|
||
def test_inf_nlv_returns_all_zeros() -> None:
|
||
"""inf NLV is not a valid capital figure — zero all weights."""
|
||
out = cap_weights({"A": 0.5}, nlv=float("inf"), adv_usd={"A": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert out == {"A": 0.0}
|
||
|
||
|
||
def test_nan_weight_zeroed_others_capped_normally() -> None:
|
||
"""A NaN weight for one symbol must be zeroed; other symbols still get normal cap treatment."""
|
||
out = cap_weights({"A": float("nan"), "B": 0.5}, nlv=10_000.0, adv_usd={"A": 1e9, "B": 1e9},
|
||
per_symbol_cap=0.05, participation_rate=1.0)
|
||
assert out["A"] == 0.0
|
||
assert abs(out["B"]) <= 0.05 + 1e-9
|
||
assert abs(out["B"]) > 0.0 # B is not zeroed — only A is affected
|
||
|
||
|
||
def test_nan_adv_falls_back_to_per_symbol_cap() -> None:
|
||
"""NaN ADV for a symbol must be ignored; only per_symbol_cap applies (no ADV weakening the cap)."""
|
||
out = cap_weights({"A": 0.99}, nlv=10_000.0, adv_usd={"A": float("nan")},
|
||
per_symbol_cap=0.10, participation_rate=0.05)
|
||
# ADV cap NOT applied (NaN ADV skipped) → per_symbol_cap=0.10 is the only ceiling
|
||
assert abs(out["A"]) <= 0.10 + 1e-9
|
||
assert abs(out["A"]) > 0.05 # participation cap (0.05 × NaN / NLV) was NOT applied
|