Goshawk_Hedge_Pro / scorer.py
GoshawkVortexAI's picture
Update scorer.py
d9b73e4 verified
"""
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})"