MicroFish/backend/tests/test_bias_control.py

90 lines
3.1 KiB
Python

import math
import bias_control as bc
def _dossiers():
return [
{"ticker": "AAA", "sector": "tech", "revenue_ttm": 100, "roe": 0.1, "pe": 20, "ps": 4},
{"ticker": "BBB", "sector": "tech", "revenue_ttm": 300, "roe": 0.3, "pe": 40, "ps": 8},
{"ticker": "CCC", "sector": "tech", "revenue_ttm": 200, "roe": 0.2, "pe": 30, "ps": 6},
{"ticker": "DDD", "sector": "energy", "revenue_ttm": 50, "roe": None, "pe": 10, "ps": 2},
{"ticker": "EEE", "sector": "energy", "revenue_ttm": 150, "roe": 0.15, "pe": 15, "ps": 3},
]
def test_sector_median_ignores_none_and_returns_medians():
medians = bc.sector_median_fundamentals(_dossiers())
assert set(medians) == {"tech", "energy"}
# tech sorted revenue 100,200,300 -> median 200; roe 0.1,0.2,0.3 -> 0.2
assert medians["tech"]["revenue_ttm"] == 200
assert medians["tech"]["roe"] == 0.2
# energy roe had one None -> only 0.15 survives -> median 0.15
assert medians["energy"]["roe"] == 0.15
def test_synthetic_neutral_dossier_pins_medians_and_marks_flag():
medians = bc.sector_median_fundamentals(_dossiers())
d = bc.synthetic_neutral_dossier("ZZZ", "tech", medians)
assert d["ticker"] == "ZZZ"
assert d["sector"] == "tech"
assert d["synthetic_neutral"] is True
assert d["revenue_ttm"] == medians["tech"]["revenue_ttm"] == 200
assert d["roe"] == 0.2
# field absent from medians stays None
assert d["net_margin"] is None
def test_deterministic_sample_reproducible():
items = list(range(1000))
a = bc.deterministic_sample(items, 500, 7)
b = bc.deterministic_sample(items, 500, 7)
assert a == b
assert len(a) == 500
def test_deterministic_sample_clamps_to_min_500():
items = list(range(600))
assert len(bc.deterministic_sample(items, 10, 1)) == 500
def test_deterministic_sample_returns_all_when_fewer_than_500():
items = list(range(30))
out = bc.deterministic_sample(items, 10, 1)
assert len(out) == 30
assert sorted(out) == items
def test_mcnemar_symmetric_passes_and_ci_contains_zero():
views = ["bullish"] * 100 + ["bearish"] * 100 + ["neutral"] * 50
r = bc.mcnemar_symmetry(views)
assert r["n_bull"] == 100 and r["n_bear"] == 100 and r["n_neutral"] == 50
assert r["n_active"] == 200
assert r["diff"] == 0.0
lo, hi = r["ci95"]
assert lo < 0 < hi
assert r["pass"] is True
def test_mcnemar_skewed_fails():
views = ["bullish"] * 300 + ["bearish"] * 10
r = bc.mcnemar_symmetry(views)
assert r["n_active"] == 310
assert r["diff"] < 0
lo, hi = r["ci95"]
assert hi < 0
assert r["pass"] is False
def test_run_bias_control_end_to_end():
companies = [
{"ticker": f"T{i}", "sector": "tech", "revenue_annual": i * 10, "roe": i / 100}
for i in range(600)
]
# balanced views -> no bearish bias detected
views = (["bullish", "bearish"] * 250) + ["neutral"] * 100
out = bc.run_bias_control(companies, views, n=500, seed=13)
assert out["n_sampled"] == 500
assert out["symmetry"]["pass"] is True
assert all(d["synthetic_neutral"] for d in out["dossiers"][:5])