| | """ |
| | 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 |
| | _CONSEC_FAILED_MAX = 2 |
| |
|
| |
|
| | def apply_veto( |
| | regime_data: Dict[str, Any], |
| | volume_data: Dict[str, Any], |
| | structure_score: float, |
| | direction: int = 1, |
| | ) -> 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) |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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)") |
| |
|
| | |
| | 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: |
| | |
| | |
| | if vol_ratio < 0.9 or vol_ratio > 1.6: |
| | reasons.append( |
| | f"NO_VOLATILITY_BASE (ratio={vol_ratio:.2f}, compressed={vol_compressed})" |
| | ) |
| |
|
| | |
| | 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") |
| |
|
| | |
| | if regime_confidence < REGIME_CONFIDENCE_MIN: |
| | reasons.append( |
| | f"LOW_REGIME_CONFIDENCE ({regime_confidence:.2f} < {REGIME_CONFIDENCE_MIN})" |
| | ) |
| |
|
| | |
| | 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" |
| |
|