Update scorer.py
Browse files
scorer.py
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import Dict, Any, List, Tuple
|
| 2 |
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
-
from config import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
def compute_structure_score(regime_data: Dict[str, Any]) -> float:
|
|
@@ -10,29 +29,38 @@ def compute_structure_score(regime_data: Dict[str, Any]) -> float:
|
|
| 10 |
structure = regime_data.get("structure", 0)
|
| 11 |
vol_expanding = regime_data.get("vol_expanding", False)
|
| 12 |
vol_contracting = regime_data.get("vol_contracting", False)
|
| 13 |
-
|
|
|
|
| 14 |
|
| 15 |
if trend == "bullish":
|
| 16 |
-
base = 0.
|
| 17 |
elif trend == "ranging":
|
| 18 |
-
base = 0.
|
| 19 |
else:
|
| 20 |
-
base = 0.
|
| 21 |
|
| 22 |
-
|
|
|
|
| 23 |
base = min(1.0, base + 0.15)
|
| 24 |
-
elif
|
| 25 |
-
base = max(0.0, base - 0.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
base = max(0.0, base - 0.05)
|
| 32 |
|
| 33 |
-
if atr_trend == "rising" and trend == "bullish":
|
| 34 |
-
base = min(1.0, base + 0.05)
|
| 35 |
-
|
| 36 |
return float(np.clip(base, 0.0, 1.0))
|
| 37 |
|
| 38 |
|
|
@@ -46,33 +74,50 @@ def score_token(
|
|
| 46 |
"regime_score": 0.0,
|
| 47 |
"volume_score": 0.0,
|
| 48 |
"structure_score": 0.0,
|
|
|
|
| 49 |
"total_score": 0.0,
|
| 50 |
}
|
| 51 |
|
| 52 |
regime_score = float(np.clip(regime_data.get("regime_score", 0.0), 0.0, 1.0))
|
| 53 |
-
|
| 54 |
structure_score = compute_structure_score(regime_data)
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
total_score = (
|
| 57 |
regime_score * WEIGHT_REGIME
|
| 58 |
+ volume_score * WEIGHT_VOLUME
|
| 59 |
+ structure_score * WEIGHT_STRUCTURE
|
|
|
|
| 60 |
)
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
return {
|
| 66 |
"regime_score": round(regime_score, 4),
|
| 67 |
"volume_score": round(volume_score, 4),
|
| 68 |
"structure_score": round(structure_score, 4),
|
| 69 |
-
"
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
|
| 73 |
-
def rank_tokens(
|
| 74 |
-
scored_map: Dict[str, Dict[str, Any]],
|
| 75 |
-
) -> List[Tuple[str, Dict[str, Any]]]:
|
| 76 |
return sorted(
|
| 77 |
scored_map.items(),
|
| 78 |
key=lambda item: item[1].get("total_score", 0.0),
|
|
@@ -80,6 +125,20 @@ def rank_tokens(
|
|
| 80 |
)
|
| 81 |
|
| 82 |
|
| 83 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
filled = int(round(score * width))
|
| 85 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
scorer.py — Multi-factor scoring with regime confidence as 4th dimension.
|
| 3 |
+
|
| 4 |
+
Key fixes vs prior version:
|
| 5 |
+
- WEIGHT_CONFIDENCE (0.15) added as explicit 4th score axis
|
| 6 |
+
- Absorption hard-zeroes volume_score regardless of other signals
|
| 7 |
+
- Failed breakout penalty applied at scoring level (defence in depth)
|
| 8 |
+
- Structure score uses ADX to weight trend quality, not just HH/HL
|
| 9 |
+
- format_score_bar returns richer display with quality tier label
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
from typing import Dict, Any, List, Tuple
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
|
| 16 |
+
from config import (
|
| 17 |
+
WEIGHT_REGIME,
|
| 18 |
+
WEIGHT_VOLUME,
|
| 19 |
+
WEIGHT_STRUCTURE,
|
| 20 |
+
WEIGHT_CONFIDENCE,
|
| 21 |
+
ADX_TREND_THRESHOLD,
|
| 22 |
+
ADX_STRONG_THRESHOLD,
|
| 23 |
+
REGIME_CONFIDENCE_MIN,
|
| 24 |
+
)
|
| 25 |
|
| 26 |
|
| 27 |
def compute_structure_score(regime_data: Dict[str, Any]) -> float:
|
|
|
|
| 29 |
structure = regime_data.get("structure", 0)
|
| 30 |
vol_expanding = regime_data.get("vol_expanding", False)
|
| 31 |
vol_contracting = regime_data.get("vol_contracting", False)
|
| 32 |
+
adx = regime_data.get("adx", 0.0)
|
| 33 |
+
vol_expanding_from_base = regime_data.get("vol_expanding_from_base", False)
|
| 34 |
|
| 35 |
if trend == "bullish":
|
| 36 |
+
base = 0.75
|
| 37 |
elif trend == "ranging":
|
| 38 |
+
base = 0.35
|
| 39 |
else:
|
| 40 |
+
base = 0.10
|
| 41 |
|
| 42 |
+
# ADX quality modifier
|
| 43 |
+
if adx >= ADX_STRONG_THRESHOLD:
|
| 44 |
base = min(1.0, base + 0.15)
|
| 45 |
+
elif adx < ADX_TREND_THRESHOLD:
|
| 46 |
+
base = max(0.0, base - 0.20)
|
| 47 |
+
|
| 48 |
+
# Structure alignment
|
| 49 |
+
if structure == 1 and trend == "bullish":
|
| 50 |
+
base = min(1.0, base + 0.12)
|
| 51 |
+
elif structure == -1 and trend == "bullish":
|
| 52 |
+
base = max(0.0, base - 0.20)
|
| 53 |
+
elif structure == -1 and trend == "bearish":
|
| 54 |
+
base = min(1.0, base + 0.12)
|
| 55 |
+
|
| 56 |
+
# Volatility context
|
| 57 |
+
if vol_expanding_from_base:
|
| 58 |
+
base = min(1.0, base + 0.08)
|
| 59 |
+
if vol_expanding and not vol_expanding_from_base:
|
| 60 |
+
base = max(0.0, base - 0.10)
|
| 61 |
+
if vol_contracting:
|
| 62 |
base = max(0.0, base - 0.05)
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
return float(np.clip(base, 0.0, 1.0))
|
| 65 |
|
| 66 |
|
|
|
|
| 74 |
"regime_score": 0.0,
|
| 75 |
"volume_score": 0.0,
|
| 76 |
"structure_score": 0.0,
|
| 77 |
+
"confidence_score": 0.0,
|
| 78 |
"total_score": 0.0,
|
| 79 |
}
|
| 80 |
|
| 81 |
regime_score = float(np.clip(regime_data.get("regime_score", 0.0), 0.0, 1.0))
|
| 82 |
+
confidence_score = float(np.clip(regime_data.get("regime_confidence", 0.0), 0.0, 1.0))
|
| 83 |
structure_score = compute_structure_score(regime_data)
|
| 84 |
|
| 85 |
+
raw_volume_score = float(np.clip(volume_data.get("volume_score", 0.0), 0.0, 1.0))
|
| 86 |
+
|
| 87 |
+
# Absorption hard-zeroes the volume signal regardless of other factors
|
| 88 |
+
if volume_data.get("absorption", False):
|
| 89 |
+
volume_score = 0.0
|
| 90 |
+
elif volume_data.get("failed_breakout", False):
|
| 91 |
+
# Failed breakout halves the volume score
|
| 92 |
+
volume_score = raw_volume_score * 0.5
|
| 93 |
+
else:
|
| 94 |
+
volume_score = raw_volume_score
|
| 95 |
+
|
| 96 |
+
# Climax penalty (not a veto here — defence in depth after veto layer)
|
| 97 |
+
if volume_data.get("climax", False):
|
| 98 |
+
volume_score = min(volume_score, 0.30)
|
| 99 |
+
|
| 100 |
total_score = (
|
| 101 |
regime_score * WEIGHT_REGIME
|
| 102 |
+ volume_score * WEIGHT_VOLUME
|
| 103 |
+ structure_score * WEIGHT_STRUCTURE
|
| 104 |
+
+ confidence_score * WEIGHT_CONFIDENCE
|
| 105 |
)
|
| 106 |
|
| 107 |
+
# Confidence multiplier: low confidence compresses total score
|
| 108 |
+
if confidence_score < REGIME_CONFIDENCE_MIN:
|
| 109 |
+
total_score *= confidence_score / REGIME_CONFIDENCE_MIN
|
| 110 |
|
| 111 |
return {
|
| 112 |
"regime_score": round(regime_score, 4),
|
| 113 |
"volume_score": round(volume_score, 4),
|
| 114 |
"structure_score": round(structure_score, 4),
|
| 115 |
+
"confidence_score": round(confidence_score, 4),
|
| 116 |
+
"total_score": round(float(np.clip(total_score, 0.0, 1.0)), 4),
|
| 117 |
}
|
| 118 |
|
| 119 |
|
| 120 |
+
def rank_tokens(scored_map: Dict[str, Dict[str, Any]]) -> List[Tuple[str, Dict[str, Any]]]:
|
|
|
|
|
|
|
| 121 |
return sorted(
|
| 122 |
scored_map.items(),
|
| 123 |
key=lambda item: item[1].get("total_score", 0.0),
|
|
|
|
| 125 |
)
|
| 126 |
|
| 127 |
|
| 128 |
+
def quality_tier(score: float) -> str:
|
| 129 |
+
if score >= 0.80:
|
| 130 |
+
return "A+"
|
| 131 |
+
if score >= 0.65:
|
| 132 |
+
return "A"
|
| 133 |
+
if score >= 0.50:
|
| 134 |
+
return "B"
|
| 135 |
+
if score >= 0.35:
|
| 136 |
+
return "C"
|
| 137 |
+
return "D"
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def format_score_bar(score: float, width: int = 18) -> str:
|
| 141 |
filled = int(round(score * width))
|
| 142 |
+
bar = "█" * filled + "░" * (width - filled)
|
| 143 |
+
tier = quality_tier(score)
|
| 144 |
+
return f"[{bar}] {score:.3f} ({tier})"
|