"""Canonical score version, brackets, and assessment semantics.""" SCORE_VERSION = "accumulation-score-v2" # Half-open intervals [low, high), except the final bracket includes 100. # Keep labels canonical because they are persisted in live and backtest output. SCORE_BRACKETS = [ (0, 20, "EXTREME CAUTION"), (20, 35, "CAUTION — OVERHEATED"), (35, 50, "NEUTRAL"), (50, 65, "MODERATE OPPORTUNITY"), (65, 80, "STRONG ACCUMULATION ZONE"), (80, 100, "EXTREME ACCUMULATION ZONE"), ] def score_in_bracket(score, bracket): """Return whether a 0-100 score belongs to a canonical bracket.""" low, high, _ = bracket if not 0 <= score <= 100: return False return low <= score < high or (high == 100 and score == 100) def bracket_for_score(score): """Return the one canonical bracket for a 0-100 score.""" for bracket in SCORE_BRACKETS: if score_in_bracket(score, bracket): return bracket raise ValueError(f"score must be between 0 and 100, got {score!r}") def assessment_for_score(score): """Return the canonical assessment label for a score.""" return bracket_for_score(score)[2]