GoshawkVortexAI's picture
Update veto.py
cfe8ecb verified
"""
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"