""" veto.py — Hard filters. Any single veto cancels the setup entirely. Key fixes vs prior version: - Absorption is now a hard veto (was missing) - Climax volume is a hard veto (was only a score penalty) - Failed breakout in last N bars = hard veto - Price too extended from mean = hard veto (separate per direction) - Vol ratio threshold lowered to 2.2 (fires more reliably) - No volatility compression = veto (require setups to emerge from bases) - Regime confidence below minimum = veto (new structural gate) """ from typing import Dict, Any, Tuple from config import ( VETO_VOLUME_MIN, VETO_VOL_RATIO_MAX, VETO_STRUCTURE_MIN, VETO_CLIMAX, VETO_ABSORPTION, VETO_EXTENDED_PRICE, VETO_NO_COMPRESSION, REGIME_CONFIDENCE_MIN, VOL_COMPRESSION_LOOKBACK, ) _FAILED_BREAKOUT_LOOKBACK = 5 # bars to look back for recent fake signals _CONSEC_FAILED_MAX = 2 # veto if N or more recent fakes def apply_veto( regime_data: Dict[str, Any], volume_data: Dict[str, Any], structure_score: float, direction: int = 1, # +1 = evaluating long, -1 = evaluating short ) -> Tuple[bool, str]: """ Returns (vetoed: bool, reason: str). direction controls which extension check applies. """ reasons = [] volume_score = volume_data.get("volume_score", 0.0) vol_ratio = regime_data.get("vol_ratio", 1.0) trend = regime_data.get("trend", "ranging") vol_compressed = regime_data.get("vol_compressed", False) vol_expanding = regime_data.get("vol_expanding", False) regime_confidence = regime_data.get("regime_confidence", 0.0) price_extended_long = regime_data.get("price_extended_long", False) price_extended_short = regime_data.get("price_extended_short", False) adx = regime_data.get("adx", 0.0) spike = volume_data.get("spike", False) climax = volume_data.get("climax", False) absorption = volume_data.get("absorption", False) failed_breakout = volume_data.get("failed_breakout", False) recent_failed = volume_data.get("recent_failed_count", 0) weak = volume_data.get("weak", False) # ── VOLUME GATES ───────────────────────────────────────────────────────── if volume_score < VETO_VOLUME_MIN: reasons.append(f"WEAK_VOLUME_SCORE ({volume_score:.2f} < {VETO_VOLUME_MIN})") if weak: reasons.append(f"VOLUME_BELOW_MA (ratio={volume_data.get('vol_ratio', 0):.2f})") if VETO_CLIMAX and climax: reasons.append("CLIMAX_VOLUME — potential exhaustion, not entry") if VETO_ABSORPTION and absorption: reasons.append("ABSORPTION_DETECTED — institutional supply at resistance") # ── BREAKOUT INTEGRITY ──────────────────────────────────────────────────── if failed_breakout: reasons.append("FAILED_BREAKOUT — most recent breakout reversed") if recent_failed >= _CONSEC_FAILED_MAX: reasons.append(f"REPEATED_FAKE_BREAKOUTS ({recent_failed} in last 10 bars)") # ── VOLATILITY GATES ────────────────────────────────────────────────────── if vol_ratio > VETO_VOL_RATIO_MAX: reasons.append(f"EXTREME_VOLATILITY (ratio={vol_ratio:.2f} > {VETO_VOL_RATIO_MAX})") if VETO_NO_COMPRESSION and not vol_compressed and not vol_expanding: # Allow through if currently expanding — expansion from base is fine # Only veto if vol is neither compressed nor just broke out if vol_ratio < 0.9 or vol_ratio > 1.6: reasons.append( f"NO_VOLATILITY_BASE (ratio={vol_ratio:.2f}, compressed={vol_compressed})" ) # ── STRUCTURE GATE ──────────────────────────────────────────────────────── if structure_score < VETO_STRUCTURE_MIN: reasons.append(f"WEAK_STRUCTURE ({structure_score:.2f} < {VETO_STRUCTURE_MIN})") if trend == "bearish" and direction == 1: reasons.append("BEARISH_TREND — long entry contra-trend") if trend == "bullish" and direction == -1: reasons.append("BULLISH_TREND — short entry contra-trend") # ── REGIME CONFIDENCE GATE ──────────────────────────────────────────────── if regime_confidence < REGIME_CONFIDENCE_MIN: reasons.append( f"LOW_REGIME_CONFIDENCE ({regime_confidence:.2f} < {REGIME_CONFIDENCE_MIN})" ) # ── PRICE EXTENSION GATE ───────────────────────────────────────────────── if VETO_EXTENDED_PRICE: if direction == 1 and price_extended_long: dist = regime_data.get("dist_atr", 0.0) reasons.append(f"PRICE_EXTENDED_LONG ({dist:.2f} ATR above mean)") if direction == -1 and price_extended_short: dist = regime_data.get("dist_atr", 0.0) reasons.append(f"PRICE_EXTENDED_SHORT ({dist:.2f} ATR below mean)") if reasons: return True, " | ".join(reasons) return False, "" def veto_summary(vetoed: bool, reason: str) -> str: return f"VETOED — {reason}" if vetoed else "APPROVED"