"""Pure equity-factor math: winsorized z-score, family/composite scores, 3 construction weightings, book return.""" from __future__ import annotations import pytest from fxhnt.domain.strategies import equity_factor as ef def test_robust_z_winsorizes_and_standardizes() -> None: z = ef.robust_z([1.0, 2.0, 3.0, 4.0, 100.0], k=2.0) # 100 is an outlier assert max(z) == pytest.approx(2.0) # winsorized at +k assert all(-2.0 - 1e-9 <= v <= 2.0 + 1e-9 for v in z) def test_robust_z_missing_is_neutral_zero() -> None: z = ef.robust_z([1.0, None, 3.0]) assert z[1] == 0.0 # missing -> neutral def test_composite_is_equal_weight_of_families() -> None: c = ef.composite(value_z=[1.0, -1.0], quality_z=[1.0, -1.0], momentum_z=[1.0, -1.0]) assert c == pytest.approx([1.0, -1.0]) def test_lowvol_score_low_vol_ranks_high() -> None: # low-volatility anomaly: lowest trailing vol -> highest score, highest vol -> lowest z = ef.lowvol_score([0.10, 0.20, 0.30, 0.40, 0.50, None]) assert z[0] == max(z[:5]) # lowest vol -> highest score assert z[4] == min(z[:5]) # highest vol -> lowest score assert z[5] == 0.0 # None -> neutral def test_composite_price_equal_weight() -> None: c = ef.composite_price(momentum_z=[1.0, -1.0], lowvol_z=[1.0, -1.0]) assert c == pytest.approx([1.0, -1.0]) # momentum and lowvol disagree -> equal-weight mean c2 = ef.composite_price(momentum_z=[2.0, 0.0], lowvol_z=[0.0, 2.0]) assert c2 == pytest.approx([1.0, 1.0]) def test_long_only_weights_top_quintile_sum_to_one() -> None: scores = [float(i) for i in range(10)] # 0..9; top quintile = top 2 (8,9) w = ef.construction_weights(scores, "long", quantile=0.2) assert sum(w) == pytest.approx(1.0) assert w[9] == pytest.approx(0.5) and w[8] == pytest.approx(0.5) assert all(w[i] == 0.0 for i in range(8)) def test_long_short_is_market_neutral_gross_two() -> None: scores = [float(i) for i in range(10)] w = ef.construction_weights(scores, "ls", quantile=0.2) assert sum(w) == pytest.approx(0.0) assert sum(abs(x) for x in w) == pytest.approx(2.0) assert w[9] == pytest.approx(0.5) and w[0] == pytest.approx(-0.5) def test_tilt_is_long_only_rank_weighted_sum_one() -> None: scores = [float(i) for i in range(10)] w = ef.construction_weights(scores, "tilt") assert sum(w) == pytest.approx(1.0) assert all(x >= 0.0 for x in w) assert w[9] > w[5] >= 0.0 def test_book_return_weighted_minus_short_borrow() -> None: prev_w = {"A": 0.5, "B": -0.5} prev_p = {"A": 100.0, "B": 100.0} cur_p = {"A": 110.0, "B": 90.0} r = ef.book_return(prev_w, prev_p, cur_p, borrow_daily=0.0) assert r == pytest.approx(0.5 * 0.10 + (-0.5) * (-0.10)) # +0.10 r2 = ef.book_return(prev_w, prev_p, cur_p, borrow_daily=0.001) assert r2 == pytest.approx(r - 0.001 * 0.5)