File size: 4,490 Bytes
d9b73e4
 
 
 
 
 
 
 
 
 
 
bd01515
 
4e05c95
bd01515
d9b73e4
 
 
 
 
 
 
 
 
4e05c95
 
 
 
 
bd01515
 
d9b73e4
 
4e05c95
 
d9b73e4
4e05c95
d9b73e4
4e05c95
d9b73e4
4e05c95
d9b73e4
 
bd01515
d9b73e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd01515
 
4e05c95
 
 
 
 
 
 
 
 
 
 
 
 
d9b73e4
4e05c95
 
 
 
d9b73e4
4e05c95
 
d9b73e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e05c95
bd01515
 
 
d9b73e4
4e05c95
 
d9b73e4
 
 
bd01515
4e05c95
 
 
 
d9b73e4
 
4e05c95
 
 
d9b73e4
bd01515
 
 
4e05c95
 
bd01515
 
d9b73e4
 
 
 
 
 
 
 
 
 
 
 
 
bd01515
d9b73e4
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""
scorer.py — Multi-factor scoring with regime confidence as 4th dimension.

Key fixes vs prior version:
- WEIGHT_CONFIDENCE (0.15) added as explicit 4th score axis
- Absorption hard-zeroes volume_score regardless of other signals
- Failed breakout penalty applied at scoring level (defence in depth)
- Structure score uses ADX to weight trend quality, not just HH/HL
- format_score_bar returns richer display with quality tier label
"""

from typing import Dict, Any, List, Tuple

import numpy as np

from config import (
    WEIGHT_REGIME,
    WEIGHT_VOLUME,
    WEIGHT_STRUCTURE,
    WEIGHT_CONFIDENCE,
    ADX_TREND_THRESHOLD,
    ADX_STRONG_THRESHOLD,
    REGIME_CONFIDENCE_MIN,
)


def compute_structure_score(regime_data: Dict[str, Any]) -> float:
    trend = regime_data.get("trend", "ranging")
    structure = regime_data.get("structure", 0)
    vol_expanding = regime_data.get("vol_expanding", False)
    vol_contracting = regime_data.get("vol_contracting", False)
    adx = regime_data.get("adx", 0.0)
    vol_expanding_from_base = regime_data.get("vol_expanding_from_base", False)

    if trend == "bullish":
        base = 0.75
    elif trend == "ranging":
        base = 0.35
    else:
        base = 0.10

    # ADX quality modifier
    if adx >= ADX_STRONG_THRESHOLD:
        base = min(1.0, base + 0.15)
    elif adx < ADX_TREND_THRESHOLD:
        base = max(0.0, base - 0.20)

    # Structure alignment
    if structure == 1 and trend == "bullish":
        base = min(1.0, base + 0.12)
    elif structure == -1 and trend == "bullish":
        base = max(0.0, base - 0.20)
    elif structure == -1 and trend == "bearish":
        base = min(1.0, base + 0.12)

    # Volatility context
    if vol_expanding_from_base:
        base = min(1.0, base + 0.08)
    if vol_expanding and not vol_expanding_from_base:
        base = max(0.0, base - 0.10)
    if vol_contracting:
        base = max(0.0, base - 0.05)

    return float(np.clip(base, 0.0, 1.0))


def score_token(
    regime_data: Dict[str, Any],
    volume_data: Dict[str, Any],
    vetoed: bool,
) -> Dict[str, float]:
    if vetoed:
        return {
            "regime_score": 0.0,
            "volume_score": 0.0,
            "structure_score": 0.0,
            "confidence_score": 0.0,
            "total_score": 0.0,
        }

    regime_score = float(np.clip(regime_data.get("regime_score", 0.0), 0.0, 1.0))
    confidence_score = float(np.clip(regime_data.get("regime_confidence", 0.0), 0.0, 1.0))
    structure_score = compute_structure_score(regime_data)

    raw_volume_score = float(np.clip(volume_data.get("volume_score", 0.0), 0.0, 1.0))

    # Absorption hard-zeroes the volume signal regardless of other factors
    if volume_data.get("absorption", False):
        volume_score = 0.0
    elif volume_data.get("failed_breakout", False):
        # Failed breakout halves the volume score
        volume_score = raw_volume_score * 0.5
    else:
        volume_score = raw_volume_score

    # Climax penalty (not a veto here — defence in depth after veto layer)
    if volume_data.get("climax", False):
        volume_score = min(volume_score, 0.30)

    total_score = (
        regime_score * WEIGHT_REGIME
        + volume_score * WEIGHT_VOLUME
        + structure_score * WEIGHT_STRUCTURE
        + confidence_score * WEIGHT_CONFIDENCE
    )

    # Confidence multiplier: low confidence compresses total score
    if confidence_score < REGIME_CONFIDENCE_MIN:
        total_score *= confidence_score / REGIME_CONFIDENCE_MIN

    return {
        "regime_score": round(regime_score, 4),
        "volume_score": round(volume_score, 4),
        "structure_score": round(structure_score, 4),
        "confidence_score": round(confidence_score, 4),
        "total_score": round(float(np.clip(total_score, 0.0, 1.0)), 4),
    }


def rank_tokens(scored_map: Dict[str, Dict[str, Any]]) -> List[Tuple[str, Dict[str, Any]]]:
    return sorted(
        scored_map.items(),
        key=lambda item: item[1].get("total_score", 0.0),
        reverse=True,
    )


def quality_tier(score: float) -> str:
    if score >= 0.80:
        return "A+"
    if score >= 0.65:
        return "A"
    if score >= 0.50:
        return "B"
    if score >= 0.35:
        return "C"
    return "D"


def format_score_bar(score: float, width: int = 18) -> str:
    filled = int(round(score * width))
    bar = "█" * filled + "░" * (width - filled)
    tier = quality_tier(score)
    return f"[{bar}] {score:.3f} ({tier})"