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})"
|