Files
fxhnt/tests/unit/test_capacity.py
jgrusewski df14b8a92f fix(exec): fail-safe risk gates — NaN/misconfig guards (no fail-open to max exposure)
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>
2026-06-14 17:11:58 +02:00

102 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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