File size: 5,615 Bytes
cfe8ecb
 
 
 
 
 
 
 
 
 
 
 
 
c24b1eb
5b5d74d
c24b1eb
5b5d74d
 
 
 
cfe8ecb
 
 
 
 
c24b1eb
 
cfe8ecb
 
 
c24b1eb
 
 
 
 
cfe8ecb
c24b1eb
cfe8ecb
 
 
 
c24b1eb
 
 
 
5b5d74d
cfe8ecb
5b5d74d
cfe8ecb
 
 
 
 
 
 
 
 
 
 
c24b1eb
cfe8ecb
5b5d74d
cfe8ecb
c24b1eb
5b5d74d
cfe8ecb
5b5d74d
cfe8ecb
 
 
 
 
 
 
 
 
 
 
 
 
 
5b5d74d
cfe8ecb
 
 
 
 
 
 
 
 
5b5d74d
cfe8ecb
5b5d74d
cfe8ecb
5b5d74d
cfe8ecb
 
 
 
 
5b5d74d
cfe8ecb
 
 
 
 
5b5d74d
cfe8ecb
 
 
 
 
 
 
 
c24b1eb
 
5b5d74d
c24b1eb
5b5d74d
 
 
cfe8ecb
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
"""
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"