Fix 1 (CRITICAL): Proposal.__post_init__ coerces all param values to float so
{"window": 20} and {"window": 20.0} produce the same key(); key() uses :.10g
format for stable float rendering. Prevents the same hypothesis being counted
twice when analysts use int vs float params.
Fix 2 (CRITICAL): AnalystFleet.gather restructured to count-before-mark: fresh
proposals are collected without marking, add_trials(len(fresh)) fires FIRST, then
mark_seen loops. A crash between count and mark causes a transient over-count
(bar slightly high, safe) instead of a permanent under-count (bar low → false
discoveries reach capital).
Fix 3 (IMPORTANT): LlmAnalyst.propose rejects lines with empty kind (|| guard)
and drops non-finite param values (inf/nan) via math.isfinite; replaces the
contextlib.suppress pattern with explicit try/except + isfinite check.
Fix 4 (tests): test_cross_analyst_dedup, test_cross_cycle_dedup_does_not_recount,
test_llm_drops_empty_kind_and_nonfinite added to test_analyst_fleet.py.
New file tests/unit/test_proposal_key.py covers int==float key stability and
distinct-param separation. 224 passed, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.6 KiB
Python
44 lines
1.6 KiB
Python
"""Unit tests for Proposal.key() stability — int/float coercion and distinct-param separation."""
|
|
from __future__ import annotations
|
|
|
|
from fxhnt.application.factory.analysts import Proposal
|
|
|
|
|
|
def test_int_and_float_params_produce_same_key() -> None:
|
|
"""int 20 and float 20.0 must yield identical keys so the same hypothesis is not counted twice."""
|
|
p_int = Proposal("ES", "trend", {"window": 20})
|
|
p_float = Proposal("ES", "trend", {"window": 20.0})
|
|
assert p_int.key() == p_float.key()
|
|
|
|
|
|
def test_large_int_and_float_produce_same_key() -> None:
|
|
p_int = Proposal("ES", "trend", {"window": 100})
|
|
p_float = Proposal("ES", "trend", {"window": 100.0})
|
|
assert p_int.key() == p_float.key()
|
|
|
|
|
|
def test_different_params_produce_different_keys() -> None:
|
|
"""Two genuinely different parameter values must NOT collapse to the same key."""
|
|
p1 = Proposal("ES", "trend", {"window": 20.0})
|
|
p2 = Proposal("ES", "trend", {"window": 50.0})
|
|
assert p1.key() != p2.key()
|
|
|
|
|
|
def test_different_kind_produces_different_key() -> None:
|
|
p1 = Proposal("ES", "trend", {"window": 20.0})
|
|
p2 = Proposal("ES", "momentum", {"window": 20.0})
|
|
assert p1.key() != p2.key()
|
|
|
|
|
|
def test_different_market_produces_different_key() -> None:
|
|
p1 = Proposal("ES", "trend", {"window": 20.0})
|
|
p2 = Proposal("NQ", "trend", {"window": 20.0})
|
|
assert p1.key() != p2.key()
|
|
|
|
|
|
def test_params_coerced_to_float_on_construction() -> None:
|
|
"""__post_init__ must coerce int values to float in the stored dict."""
|
|
p = Proposal("ES", "trend", {"window": 100})
|
|
assert isinstance(p.params["window"], float)
|
|
assert p.params["window"] == 100.0
|