Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """ | |
| ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| ║ MAYTHOS — Multi-Architecture Yield-Tuned Hybrid Oscillation System ║ | |
| ║ Version : 1.1 ║ | |
| ║ Build : 1.1.0 ║ | |
| ║ Spec : 1.1 ║ | |
| ║ Single-file — all engines embedded, no runtime deps except numpy ║ | |
| ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| Architecture (10 layers): | |
| Layer 1 — Data Integrity Engine | |
| Layer 2 — Adaptive Market State + Asset Profile + Session Intelligence | |
| Layer 3 — Multi-Timeframe Fusion Engine | |
| Layer 4 — Real-Time Event Detection Engine | |
| Layer 5 — Liquidity + Pressure Engine | |
| Layer 6 — Technical Confirmation Stack (EMA200/RSI/MACD/BB/S&R) | |
| Layer 7 — Adaptive Scoring Engine | |
| Layer 8 — Warm-up Controller | |
| Layer 9 — Signal Lifecycle Engine | |
| Layer 10 — Output Formatter + Validator | |
| Usage: | |
| engine = MAYTHOS() | |
| output = engine.tick(candle) | |
| """ | |
| # ============================================================================== | |
| # PART A CONSTANTS UTILS BUFFERS | |
| # ============================================================================== | |
| from __future__ import annotations | |
| import math | |
| import time | |
| from collections import deque | |
| from dataclasses import dataclass, field | |
| from typing import Any, Deque, Dict, List, Optional, Tuple | |
| import numpy as np | |
| # ============================================================================== | |
| # VERSIONING | |
| # ============================================================================== | |
| SPEC_VERSION = "1.1" | |
| ENGINE_VERSION = "1.1.0" | |
| OUTPUT_VERSION = "1.1.0" | |
| # ============================================================================== | |
| # NUMERICAL CONSTANTS | |
| # ============================================================================== | |
| EPSILON = 1e-10 # guard against div-by-zero | |
| CLAMP_MIN = 0.0 | |
| CLAMP_MAX = 1.0 | |
| SCORE_CLAMP = (0.0, 1.0) | |
| # ============================================================================== | |
| # MARKET STATE LABELS (predefined, append-only) | |
| # ============================================================================== | |
| MARKET_STATES = frozenset({ | |
| "trend", "range", "compression", "expansion", | |
| "manipulation", "sweep", "absorption", "reversal", | |
| "continuation", "unstable", "noisy", "undefined", | |
| }) | |
| REGIME_LABELS = frozenset({ | |
| "bullish_trend", "bearish_trend", "bullish_range", "bearish_range", | |
| "breakout_attempt", "breakout_confirmed", "exhaustion", | |
| "distribution", "accumulation", "squeeze", "trap", | |
| "recovery", "transition", "neutral_dirty", | |
| }) | |
| MARKET_RECOGNITION_LABELS = frozenset({ | |
| "clean_trend", "dirty_trend", "clean_range", "dirty_range", | |
| "compression_before_expansion", "exhaustion_after_impulse", | |
| "manipulation_sweep", "absorption_zone", "reversal_attempt", | |
| "continuation_attempt", "unstable_noise_cluster", | |
| }) | |
| ASSET_MODES = frozenset({"crypto", "forex", "OTC", "hybrid", "unknown"}) | |
| EXECUTION_SUITABILITY = frozenset({"blocked", "weak", "moderate", "strong"}) | |
| SIGNAL_DIRECTIONS = frozenset({"BUY", "SELL"}) | |
| OPERATIONAL_MODES = frozenset({ | |
| "cold_start", "normal", "cautious", "degraded", | |
| "blocked", "recovery", "hot_response", | |
| }) | |
| SIGNAL_LIFECYCLE_STATES = frozenset({ | |
| "idle", "forming", "candidate", "confirmed", | |
| "cooling", "expired", "invalidated", | |
| }) | |
| REASON_CODES = frozenset({ | |
| "DATA_OK", "DATA_DEGRADED", "TF_ALIGNED", "TF_CONFLICT", | |
| "LIQUIDITY_SWEEP", "MANIP_HIGH", "MOMENTUM_OK", | |
| "TIMING_BAD", "STALE_DECAY", "SESSION_SUPPORT", "SESSION_RISK", | |
| }) | |
| EXPIRY_BUCKETS = frozenset({"ultra_short", "short", "medium"}) | |
| EVENT_SEVERITIES = frozenset({"minor", "moderate", "strong", "extreme"}) | |
| MARKET_TEMPERATURE_STATES = frozenset({"cold", "warm", "overheated", "unstable_explosive"}) | |
| SESSION_LABELS = frozenset({ | |
| "london", "ny", "asia", "ny_london_overlap", | |
| "asia_london_overlap", "rollover", "off_hours", | |
| }) | |
| TIMEFRAME_LABELS = frozenset({ | |
| "5s", "30s", "1m", "2m", "5m", "10m", "15m", "higher", | |
| }) | |
| # ============================================================================== | |
| # CONFIGURATION DEFAULTS | |
| # ============================================================================== | |
| DEFAULT_BUFFER_SIZE = 300 # candles (covers ~5h on 1m) | |
| MIN_WARM_CANDLES = 30 # cold-start gate | |
| FULL_WARM_CANDLES = 50 # full confidence unlock (FIX-3: halved from 100 → 50 to cut cold-start delay) | |
| MAX_CACHE_ENTRIES = 64 # bounded memory-behavior caches | |
| AGE_DECAY_TICKS = 50 # TIMING-FIX-5: ticks before cache entry ages out (used by BoundedCache if instantiated) | |
| SIGNAL_COOLDOWN_TICKS = 5 # TIMING-FIX-5: was 3 → 5 ticks minimum between same-direction signals | |
| # RSI periods and defaults | |
| RSI_PERIOD = 14 | |
| MACD_FAST = 12 | |
| MACD_SLOW = 26 | |
| MACD_SIGNAL = 9 | |
| BB_PERIOD = 20 | |
| BB_STD_DEV = 2.0 | |
| EMA200_PERIOD = 200 | |
| RSI_OB_CRYPTO = 80.0 | |
| RSI_OS_CRYPTO = 20.0 | |
| RSI_OB_DEFAULT = 70.0 | |
| RSI_OS_DEFAULT = 30.0 | |
| # Confidence bands (Section 17) | |
| CONF_BLOCKED_MAX = 0.24 | |
| CONF_WEAK_MAX = 0.44 | |
| CONF_MODERATE_MAX = 0.69 | |
| # above 0.69 → strong | |
| # Sudden candle expansion multiplier | |
| SUDDEN_EXPANSION_MULT = 3.0 | |
| # ============================================================================== | |
| # NUMERIC UTILITY FUNCTIONS | |
| # ============================================================================== | |
| def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float: | |
| """Clamp value to [lo, hi]. Handles NaN → lo.""" | |
| if not math.isfinite(value): | |
| return lo | |
| return max(lo, min(hi, value)) | |
| def safe_div(numerator: float, denominator: float, fallback: float = 0.0) -> float: | |
| """Division protected against zero/NaN/inf. Returns fallback on error.""" | |
| if not math.isfinite(numerator) or not math.isfinite(denominator): | |
| return fallback | |
| if abs(denominator) < EPSILON: | |
| return fallback | |
| result = numerator / denominator | |
| return result if math.isfinite(result) else fallback | |
| def safe_sqrt(value: float, fallback: float = 0.0) -> float: | |
| """Square root protected against negative and non-finite inputs.""" | |
| if not math.isfinite(value) or value < 0.0: | |
| return fallback | |
| return math.sqrt(value) | |
| def is_finite_positive(value: float) -> bool: | |
| return math.isfinite(value) and value > 0.0 | |
| def normalize_01(value: float, min_val: float, max_val: float, | |
| fallback: float = 0.5) -> float: | |
| """Normalize value to [0, 1] given range. Clamped. Returns fallback if range is zero.""" | |
| rng = max_val - min_val | |
| if abs(rng) < EPSILON: | |
| return fallback | |
| return clamp(safe_div(value - min_val, rng)) | |
| def weighted_mean(values: List[float], weights: List[float]) -> float: | |
| """Weighted average. Returns 0.0 if all weights are zero. | |
| FIX-5: Was returning 0.5 (neutral), which leaked a false mid-range confidence | |
| into blocked/cold outputs. 0.0 is correct: no weight → no evidence → no score.""" | |
| total_w = sum(weights) | |
| if total_w < EPSILON: | |
| return 0.0 | |
| return clamp(sum(v * w for v, w in zip(values, weights)) / total_w) | |
| def nan_safe(value: float, fallback: float = 0.0) -> float: | |
| """Replace NaN/inf with fallback.""" | |
| return value if math.isfinite(value) else fallback | |
| def _validate_candle_fields(o: float, h: float, l: float, c: float) -> bool: | |
| """Return True if OHLC relationships are self-consistent.""" | |
| if not all(math.isfinite(x) for x in (o, h, l, c)): | |
| return False | |
| if h < l - EPSILON: | |
| return False | |
| if c > h + EPSILON or c < l - EPSILON: | |
| return False | |
| if o > h + EPSILON or o < l - EPSILON: | |
| return False | |
| return True | |
| # ============================================================================== | |
| # ENGINE DIAGNOSTICS (AUDIT FIX — observability for the silent-exception path) | |
| # ============================================================================== | |
| # | |
| # Every engine below is deliberately fault-tolerant: its public entry point | |
| # wraps all real logic in try/except and falls back to a safe default on any | |
| # exception, so a single bad tick (or a genuine bug) can never crash the | |
| # pipeline or take signal generation down. That is the correct behavior for | |
| # 24/7 uptime -- but as originally written, those except blocks were | |
| # completely silent: nothing was ever counted, logged, or exposed anywhere. | |
| # A real, persistent logic bug in any engine would degrade that engine's | |
| # output to its safe-default forever with zero way to detect it was | |
| # happening. This lightweight, bounded recorder closes that gap without | |
| # changing any existing fallback behavior — it is purely additive and never | |
| # itself raises. Process-wide (shared across however many MAYTHOS instances | |
| # run in one process, e.g. one per symbol/feed), since "is something actually | |
| # broken" is most useful as a process-level health signal. Inspect via | |
| # MAYTHOS.engine_health() or the module-level ENGINE_DIAGNOSTICS singleton — | |
| # e.g. expose it on a /health endpoint in your HF Space. | |
| class _EngineDiagnostics: | |
| _MAX_RECENT = 200 | |
| def __init__(self) -> None: | |
| self.counts: Dict[str, int] = {} | |
| self.recent: "deque" = deque(maxlen=self._MAX_RECENT) | |
| def record(self, engine: str, exc: BaseException) -> None: | |
| try: | |
| self.counts[engine] = self.counts.get(engine, 0) + 1 | |
| self.recent.append({ | |
| "engine" : engine, | |
| "type" : type(exc).__name__, | |
| "message": str(exc)[:200], | |
| "time" : time.time(), | |
| }) | |
| except Exception: | |
| pass # diagnostics must never themselves raise (no recursive self-record) | |
| def snapshot(self) -> Dict: | |
| return { | |
| "error_counts" : dict(self.counts), | |
| "total_errors" : sum(self.counts.values()), | |
| "recent_errors": list(self.recent), | |
| } | |
| def reset(self) -> None: | |
| self.counts.clear() | |
| self.recent.clear() | |
| ENGINE_DIAGNOSTICS = _EngineDiagnostics() | |
| # ============================================================================== | |
| # ROLLING FIXED-SIZE BUFFERS | |
| # ============================================================================== | |
| class RollingBuffer: | |
| """ | |
| Fixed-size FIFO ring buffer backed by a pre-allocated numpy array. | |
| FIX-1 (memory): Eliminates the per-tick deque→np.array() conversion that | |
| created a new Python object on every mean/std/min/max call. The backing | |
| array is allocated once at construction and reused forever. | |
| FIX-2 (CPU): Uses pure-Python arithmetic for arrays with fewer than 10 | |
| elements, where numpy dispatch overhead exceeds computation cost on 2-core | |
| HuggingFace CPUs. | |
| Interface is fully backward-compatible with the old deque version. | |
| """ | |
| __slots__ = ("maxlen", "_data", "_head", "_count") | |
| def __init__(self, maxlen: int) -> None: | |
| assert maxlen > 0, "maxlen must be positive" | |
| self.maxlen: int = maxlen | |
| self._data: np.ndarray = np.zeros(maxlen, dtype=np.float64) | |
| self._head: int = 0 # next write slot (= oldest slot when buffer full) | |
| self._count: int = 0 # number of valid entries ≤ maxlen | |
| def push(self, value: float) -> None: | |
| self._data[self._head] = nan_safe(value) | |
| self._head = (self._head + 1) % self.maxlen | |
| if self._count < self.maxlen: | |
| self._count += 1 | |
| # ------------------------------------------------------------------ | |
| # _view(): zero-copy slice of active (unordered) data. | |
| # Use ONLY for commutative operations (mean/std/min/max). | |
| # ------------------------------------------------------------------ | |
| def _view(self) -> np.ndarray: | |
| if self._count < self.maxlen: | |
| return self._data[:self._count] | |
| return self._data # full ring: all slots valid, order irrelevant for stats | |
| def as_array(self) -> np.ndarray: | |
| """Ordered (oldest→newest) copy. Used for indexed slicing / diffs.""" | |
| n = self._count | |
| if n == 0: | |
| return np.empty(0, dtype=np.float64) | |
| if n < self.maxlen: | |
| return self._data[:n].copy() | |
| if self._head == 0: | |
| return self._data.copy() | |
| # Unroll ring so index 0 is the oldest written entry | |
| return np.concatenate([self._data[self._head:], self._data[:self._head]]) | |
| def last(self, fallback: float = 0.0) -> float: | |
| if self._count == 0: | |
| return fallback | |
| return float(self._data[(self._head - 1) % self.maxlen]) | |
| def __len__(self) -> int: | |
| return self._count | |
| def full(self) -> bool: | |
| return self._count == self.maxlen | |
| def is_ready(self, min_len: int) -> bool: | |
| return self._count >= min_len | |
| # ------------------------------------------------------------------ | |
| # Stats — pure Python for n < 10, numpy for larger arrays. | |
| # ------------------------------------------------------------------ | |
| def mean(self) -> float: | |
| n = self._count | |
| if n == 0: | |
| return 0.0 | |
| v = self._view() | |
| if n < 10: | |
| s = 0.0 | |
| for x in v: | |
| s += float(x) | |
| return s / n | |
| return float(v.mean()) | |
| def std(self) -> float: | |
| n = self._count | |
| if n < 2: | |
| return 0.0 | |
| v = self._view() | |
| if n < 10: | |
| s = 0.0 | |
| for x in v: | |
| s += float(x) | |
| m = s / n | |
| ss = 0.0 | |
| for x in v: | |
| d = float(x) - m | |
| ss += d * d | |
| return math.sqrt(ss / n) | |
| return float(v.std()) | |
| def min(self) -> float: | |
| n = self._count | |
| if n == 0: | |
| return 0.0 | |
| v = self._view() | |
| if n < 10: | |
| mn = math.inf | |
| for x in v: | |
| fx = float(x) | |
| if fx < mn: | |
| mn = fx | |
| return mn | |
| return float(v.min()) | |
| def max(self) -> float: | |
| n = self._count | |
| if n == 0: | |
| return 0.0 | |
| v = self._view() | |
| if n < 10: | |
| mx = -math.inf | |
| for x in v: | |
| fx = float(x) | |
| if fx > mx: | |
| mx = fx | |
| return mx | |
| return float(v.max()) | |
| class RollingObjectBuffer: | |
| """Fixed-size FIFO ring buffer for arbitrary Python objects (candles, events).""" | |
| __slots__ = ("_buf", "maxlen") | |
| def __init__(self, maxlen: int) -> None: | |
| assert maxlen > 0 | |
| self.maxlen = maxlen | |
| self._buf: Deque[Any] = deque(maxlen=maxlen) | |
| def push(self, obj: Any) -> None: | |
| self._buf.append(obj) | |
| def last(self, fallback: Any = None) -> Any: | |
| return self._buf[-1] if self._buf else fallback | |
| def to_list(self) -> List[Any]: | |
| return list(self._buf) | |
| def __len__(self) -> int: | |
| return len(self._buf) | |
| def is_ready(self, min_len: int) -> bool: | |
| return len(self._buf) >= min_len | |
| class BoundedCache: | |
| """ | |
| Bounded key-value cache with age-based eviction. | |
| Maximum MAX_CACHE_ENTRIES entries. | |
| """ | |
| def __init__(self, maxlen: int = MAX_CACHE_ENTRIES) -> None: | |
| self.maxlen = maxlen | |
| self._data: Dict[str, Tuple[Any, int]] = {} # key → (value, insert_tick) | |
| self._tick: int = 0 | |
| def tick(self) -> None: | |
| self._tick += 1 | |
| def set(self, key: str, value: Any) -> None: | |
| if len(self._data) >= self.maxlen and key not in self._data: | |
| self._evict_oldest() | |
| self._data[key] = (value, self._tick) | |
| def get(self, key: str, fallback: Any = None) -> Any: | |
| entry = self._data.get(key) | |
| return entry[0] if entry is not None else fallback | |
| def evict_stale(self, max_age: int = AGE_DECAY_TICKS) -> None: | |
| stale_keys = [k for k, (_, t) in self._data.items() | |
| if self._tick - t > max_age] | |
| for k in stale_keys: | |
| del self._data[k] | |
| def _evict_oldest(self) -> None: | |
| if not self._data: | |
| return | |
| oldest_key = min(self._data, key=lambda k: self._data[k][1]) | |
| del self._data[oldest_key] | |
| def __contains__(self, key: str) -> bool: | |
| return key in self._data | |
| def __len__(self) -> int: | |
| return len(self._data) | |
| # ============================================================================== | |
| # CANDLE DATACLASS — minimal validated representation | |
| # ============================================================================== | |
| class Candle: | |
| """ | |
| Validated OHLCV candle. Optional fields default to safe sentinels. | |
| Immutable once constructed. | |
| """ | |
| timestamp : float | |
| open : float | |
| high : float | |
| low : float | |
| close : float | |
| volume : float = 0.0 # 0 = absent, triggers proxy logic | |
| spread : float = 0.0 | |
| bid : float = 0.0 | |
| ask : float = 0.0 | |
| source_id : str = "default" | |
| session_label: str = "unknown" | |
| is_closed : bool = True # False = intrabar/live tick | |
| def __post_init__(self) -> None: | |
| # Clamp float fields against non-finite | |
| for attr in ("open", "high", "low", "close", "volume", "spread", "bid", "ask"): | |
| v = getattr(self, attr) | |
| if not math.isfinite(v): | |
| object.__setattr__(self, attr, 0.0) | |
| # Ensure OHLC consistency after sanitization; if broken, mark invalid | |
| object.__setattr__(self, "_valid", _validate_candle_fields( | |
| self.open, self.high, self.low, self.close)) | |
| def valid(self) -> bool: | |
| return self._valid # type: ignore[attr-defined] | |
| def body_size(self) -> float: | |
| return abs(self.close - self.open) | |
| def candle_range(self) -> float: | |
| return self.high - self.low | |
| def upper_wick(self) -> float: | |
| return self.high - max(self.open, self.close) | |
| def lower_wick(self) -> float: | |
| return min(self.open, self.close) - self.low | |
| def is_bullish(self) -> bool: | |
| return self.close >= self.open | |
| def body_ratio(self) -> float: | |
| r = self.candle_range | |
| return safe_div(self.body_size, r) if r > EPSILON else 0.5 | |
| def close_position(self) -> float: | |
| """Where the close sits within the full range [0=bottom, 1=top].""" | |
| r = self.candle_range | |
| return safe_div(self.close - self.low, r) if r > EPSILON else 0.5 | |
| # ============================================================================== | |
| # SAFE OUTPUT DEFAULTS (used when subsystems are unavailable/cold-start) | |
| # ============================================================================== | |
| def _default_output() -> Dict[str, Any]: | |
| """Return a fully-populated safe output dict in cold/blocked state.""" | |
| return { | |
| # Core | |
| "direction" : "BUY", | |
| "confidence" : 0.0, | |
| "internal_trust" : 0.0, | |
| "execution_suitability" : "blocked", | |
| "market_state" : "undefined", | |
| "regime_label" : "transition", | |
| "asset_mode" : "unknown", | |
| "timeframe_alignment" : 0.0, | |
| "manipulation_probability": 0.0, | |
| "liquidity_score" : 0.5, | |
| "volatility_score" : 0.5, | |
| "pressure_score" : 0.5, | |
| "momentum_score" : 0.5, | |
| "timing_score" : 0.0, | |
| "signal_freshness" : 0.0, | |
| "spread_health" : 1.0, | |
| "data_quality" : 1.0, | |
| "readability_score" : 0.5, | |
| "stale_signal_flag" : False, | |
| "degraded_mode_flag" : False, | |
| "blocked_flag" : True, | |
| "reason_summary" : "cold_start: insufficient history", | |
| "reason_codes" : ["DATA_OK"], | |
| # Optional | |
| "call_bias" : 0.5, | |
| "put_bias" : 0.5, | |
| "expiry_suitability_ultra_short" : 0.0, | |
| "expiry_suitability_short" : 0.0, | |
| "expiry_suitability_medium" : 0.0, | |
| "debug_trace" : None, | |
| # Warm-up state | |
| "warm_up_fraction" : 0.0, | |
| "operational_mode" : "cold_start", | |
| # Version | |
| "spec_version" : SPEC_VERSION, | |
| "engine_version" : ENGINE_VERSION, | |
| "output_version" : OUTPUT_VERSION, | |
| } | |
| # ============================================================================== | |
| # PART B DATA INTEGRITY | |
| # ============================================================================== | |
| import math | |
| import time | |
| from collections import deque | |
| from typing import Dict, Optional, Tuple | |
| # ============================================================================== | |
| # DATA INTEGRITY ENGINE | |
| # ============================================================================== | |
| class DataIntegrityEngine: | |
| """ | |
| Input contract: receives a Candle and processes it. | |
| Output contract: DataIntegrityResult dict. | |
| Responsibilities (Section 9): | |
| - Tick validation and deduplication. | |
| - Timestamp ordering and gap detection. | |
| - Spread / latency monitoring. | |
| - Tick structure analysis. | |
| - Noise control. | |
| - Source confidence scoring. | |
| """ | |
| # Configurable thresholds (adaptive, not static) | |
| _SPREAD_WINDOW = 30 | |
| _LATENCY_WINDOW = 30 | |
| _NOISE_WINDOW = 20 | |
| _GAP_SIGMA_THRESHOLD = 4.0 # z-score to flag timestamp gap | |
| _SPREAD_SIGMA_THRESHOLD = 3.5 | |
| _WICK_SIGMA_THRESHOLD = 3.0 | |
| _SYNTHETIC_TICK_BODY_RATIO_MIN = 0.97 # FIX 2: was 0.90 — 90% body ratio fires on real trend candles (7.6% false positive rate on live BTC). 0.97 limits to truly pathological cases. | |
| _CLUSTER_TICK_WINDOW = 5 # candles for cluster detection | |
| _DUPLICATE_TOLERANCE = 0.5 # seconds | |
| _MAX_SOURCES = 64 # AUDIT FIX: cap on distinct source_id entries tracked | |
| def __init__(self) -> None: | |
| # --- timestamp tracking --- | |
| self._last_ts : float = -1.0 | |
| self._ts_gaps : RollingBuffer = RollingBuffer(self._LATENCY_WINDOW) | |
| self._ts_intervals : RollingBuffer = RollingBuffer(50) # for gap z-score | |
| # --- spread tracking --- | |
| self._spread_buf : RollingBuffer = RollingBuffer(self._SPREAD_WINDOW) | |
| self._spread_events : int = 0 | |
| # --- latency / websocket --- | |
| self._receive_ts_buf : RollingBuffer = RollingBuffer(self._LATENCY_WINDOW) | |
| self._latency_buf : RollingBuffer = RollingBuffer(self._LATENCY_WINDOW) | |
| # --- range / wick --- | |
| self._range_buf : RollingBuffer = RollingBuffer(self._NOISE_WINDOW) | |
| self._upper_wick_buf : RollingBuffer = RollingBuffer(self._NOISE_WINDOW) | |
| self._lower_wick_buf : RollingBuffer = RollingBuffer(self._NOISE_WINDOW) | |
| self._body_buf : RollingBuffer = RollingBuffer(self._NOISE_WINDOW) | |
| # --- duplicate / ordering detection --- | |
| self._seen_ts_window : deque = deque(maxlen=10) | |
| # --- source confidence --- | |
| # AUDIT FIX (memory-leak risk): _source_scores is keyed by whatever | |
| # source_id the caller sets on each Candle. In normal use this is a | |
| # small fixed set (e.g. one per exchange/feed), but nothing previously | |
| # bounded its cardinality -- if source_id ever carried high-cardinality | |
| # values (a bug upstream, a per-tick id, a rotating feed id, etc.) on | |
| # a 24/7 deployment, this dict would grow without limit for the life | |
| # of the process. _MAX_SOURCES + _source_last_seen below cap it with | |
| # simple least-recently-seen eviction, mirroring the pattern already | |
| # used by BoundedCache elsewhere in this file. | |
| self._source_scores : Dict[str, RollingBuffer] = {} | |
| self._source_last_seen: Dict[str, int] = {} | |
| # --- noise counting --- | |
| self._noise_count : int = 0 | |
| self._tick_count : int = 0 | |
| self._invalid_count : int = 0 | |
| self._gap_count : int = 0 | |
| # --- instability level --- | |
| self._instability : float = 0.0 # 0=stable, 1=very unstable | |
| self._source_confidence: float = 1.0 | |
| # ------------------------------------------------------------------ | |
| # PUBLIC: process a new candle | |
| # ------------------------------------------------------------------ | |
| def process(self, candle: Candle, receive_time: Optional[float] = None) -> Dict: | |
| """ | |
| Main entry. Call on every new candle/tick. | |
| Returns DataIntegrityResult dict. | |
| Always returns a result; never raises. | |
| """ | |
| try: | |
| return self._process_inner(candle, receive_time) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("DataIntegrityEngine", exc) | |
| # Total fail-safe: return a fully safe degraded result | |
| return self._safe_fallback_result() | |
| # ------------------------------------------------------------------ | |
| # INTERNAL PROCESSING | |
| # ------------------------------------------------------------------ | |
| def _process_inner(self, candle: Candle, receive_time: Optional[float]) -> Dict: | |
| self._tick_count += 1 | |
| result: Dict = { | |
| "valid" : True, | |
| "duplicate" : False, | |
| "out_of_order" : False, | |
| "gap_detected" : False, | |
| "gap_severity" : 0.0, | |
| "spread_anomaly" : False, | |
| "spread_expansion" : False, | |
| "wick_anomaly" : False, | |
| "synthetic_tick" : False, | |
| "noise_burst" : False, | |
| "candle_distortion" : False, | |
| "latency_spike" : False, | |
| "latency_drift" : False, | |
| "source_confidence" : 1.0, | |
| "data_quality" : 1.0, | |
| "instability_level" : 0.0, | |
| "suppress_signal" : False, | |
| "has_volume" : candle.volume > EPSILON, | |
| "has_bid_ask" : candle.bid > EPSILON and candle.ask > EPSILON, | |
| } | |
| # 1. Basic candle validity | |
| if not candle.valid: | |
| result["valid"] = False | |
| result["suppress_signal"] = True | |
| self._invalid_count += 1 | |
| result["data_quality"] = clamp(1.0 - safe_div(self._invalid_count, max(1, self._tick_count))) | |
| return self._finalize(result, candle) | |
| # 2. Duplicate detection | |
| ts = candle.timestamp | |
| if self._is_duplicate(ts): | |
| result["duplicate"] = True | |
| result["suppress_signal"] = True | |
| return self._finalize(result, candle) | |
| # 3. Timestamp ordering and gap analysis | |
| if self._last_ts > 0.0: | |
| interval = ts - self._last_ts | |
| if interval < -self._DUPLICATE_TOLERANCE: | |
| result["out_of_order"] = True | |
| result["suppress_signal"] = True | |
| return self._finalize(result, candle) | |
| if interval > EPSILON: | |
| self._ts_intervals.push(interval) | |
| # Gap detection using z-score of intervals | |
| gap_severity = self._compute_gap_severity(interval) | |
| if gap_severity > 0.5: | |
| result["gap_detected"] = True | |
| result["gap_severity"] = gap_severity | |
| self._gap_count += 1 | |
| self._last_ts = ts | |
| self._seen_ts_window.append(ts) | |
| # 4. Spread analysis | |
| spread = candle.spread | |
| if spread > EPSILON: | |
| self._spread_buf.push(spread) | |
| s_anomaly, s_expansion = self._analyze_spread(spread) | |
| result["spread_anomaly"] = s_anomaly | |
| result["spread_expansion"] = s_expansion | |
| if s_anomaly: | |
| self._spread_events += 1 | |
| # 5. Range / wick / body updates | |
| cr = candle.candle_range | |
| uw = candle.upper_wick | |
| lw = candle.lower_wick | |
| body = candle.body_size | |
| if cr > EPSILON: | |
| self._range_buf.push(cr) | |
| if uw >= 0: | |
| self._upper_wick_buf.push(uw) | |
| if lw >= 0: | |
| self._lower_wick_buf.push(lw) | |
| if body >= 0: | |
| self._body_buf.push(body) | |
| # 6. Wick anomaly | |
| result["wick_anomaly"] = self._detect_wick_anomaly(candle) | |
| # 7. Synthetic tick detection (near-perfect body, near-zero wicks) | |
| result["synthetic_tick"] = self._detect_synthetic(candle) | |
| # 8. Candle distortion (extreme ratio) | |
| result["candle_distortion"] = self._detect_distortion(candle) | |
| # 9. Noise burst detection | |
| result["noise_burst"] = self._detect_noise_burst(candle) | |
| if result["noise_burst"]: | |
| self._noise_count += 1 | |
| # 10. Latency analysis (if receive_time provided) | |
| if receive_time is not None and math.isfinite(receive_time): | |
| lat = receive_time - ts | |
| if lat >= 0: | |
| self._latency_buf.push(lat) | |
| result["latency_spike"] = self._detect_latency_spike(lat) | |
| result["latency_drift"] = self._detect_latency_drift() | |
| # 11. Source confidence scoring | |
| sc = self._update_source_confidence(candle.source_id, result) | |
| result["source_confidence"] = sc | |
| self._source_confidence = sc | |
| return self._finalize(result, candle) | |
| # ------------------------------------------------------------------ | |
| # FINALIZE: compute aggregate data_quality and instability | |
| # ------------------------------------------------------------------ | |
| def _finalize(self, result: Dict, candle: Candle) -> Dict: | |
| penalties = 0.0 | |
| if not result["valid"]: | |
| penalties += 0.5 | |
| if result["duplicate"] or result["out_of_order"]: | |
| penalties += 0.3 | |
| if result["gap_detected"]: | |
| penalties += result["gap_severity"] * 0.2 | |
| if result["spread_anomaly"]: | |
| penalties += 0.1 | |
| if result["spread_expansion"]: | |
| penalties += 0.15 | |
| if result["wick_anomaly"]: | |
| penalties += 0.1 | |
| if result["synthetic_tick"]: | |
| penalties += 0.2 | |
| if result["noise_burst"]: | |
| penalties += 0.1 | |
| if result["candle_distortion"]: | |
| penalties += 0.15 | |
| if result["latency_spike"]: | |
| penalties += 0.1 | |
| if result["latency_drift"]: | |
| penalties += 0.1 | |
| dq = clamp(1.0 - penalties) | |
| # Blend with source confidence | |
| dq = clamp(0.7 * dq + 0.3 * result["source_confidence"]) | |
| # Instability: exponential moving average of penalty | |
| alpha = 0.15 | |
| self._instability = clamp(alpha * penalties + (1 - alpha) * self._instability) | |
| result["data_quality"] = dq | |
| result["instability_level"] = self._instability | |
| result["suppress_signal"] = result.get("suppress_signal", False) or dq < 0.25 | |
| return result | |
| # ------------------------------------------------------------------ | |
| # COMPONENT DETECTORS | |
| # ------------------------------------------------------------------ | |
| def _is_duplicate(self, ts: float) -> bool: | |
| if not self._seen_ts_window: | |
| return False | |
| return any(abs(ts - prev) < self._DUPLICATE_TOLERANCE | |
| for prev in self._seen_ts_window) | |
| def _compute_gap_severity(self, interval: float) -> float: | |
| """Z-score of the interval relative to rolling mean/std of intervals.""" | |
| if not self._ts_intervals.is_ready(5): | |
| return 0.0 | |
| mean = self._ts_intervals.mean() | |
| std = self._ts_intervals.std() | |
| if std < EPSILON: | |
| return 0.0 | |
| z = abs(safe_div(interval - mean, std)) | |
| # Normalize to [0,1]: z>=GAP_SIGMA_THRESHOLD → 1.0 | |
| return clamp(safe_div(z, self._GAP_SIGMA_THRESHOLD)) | |
| def _analyze_spread(self, spread: float) -> Tuple[bool, bool]: | |
| """Returns (anomaly, expansion).""" | |
| if not self._spread_buf.is_ready(5): | |
| return False, False | |
| mean = self._spread_buf.mean() | |
| std = self._spread_buf.std() | |
| if std < EPSILON or mean < EPSILON: | |
| return False, False | |
| z = safe_div(spread - mean, std) | |
| anomaly = z > self._SPREAD_SIGMA_THRESHOLD | |
| expansion = z > self._SPREAD_SIGMA_THRESHOLD * 0.6 | |
| return anomaly, expansion | |
| def _detect_wick_anomaly(self, candle: Candle) -> bool: | |
| if not self._range_buf.is_ready(5): | |
| return False | |
| mean_range = self._range_buf.mean() | |
| std_range = self._range_buf.std() | |
| if mean_range < EPSILON: | |
| return False | |
| max_wick = max(candle.upper_wick, candle.lower_wick) | |
| if std_range < EPSILON: | |
| return max_wick > mean_range * (self._WICK_SIGMA_THRESHOLD + 1) | |
| z = safe_div(max_wick - mean_range, std_range + EPSILON) | |
| return z > self._WICK_SIGMA_THRESHOLD | |
| def _detect_synthetic(self, candle: Candle) -> bool: | |
| """Suspiciously perfect body → synthetic candle flag.""" | |
| cr = candle.candle_range | |
| if cr < EPSILON: | |
| return False | |
| body_r = safe_div(candle.body_size, cr) | |
| return body_r >= self._SYNTHETIC_TICK_BODY_RATIO_MIN | |
| def _detect_distortion(self, candle: Candle) -> bool: | |
| """Candle that is massively larger than recent average.""" | |
| if not self._range_buf.is_ready(5): | |
| return False | |
| mean_r = self._range_buf.mean() | |
| if mean_r < EPSILON: | |
| return False | |
| ratio = safe_div(candle.candle_range, mean_r) | |
| return ratio > 5.0 | |
| def _detect_noise_burst(self, candle: Candle) -> bool: | |
| """Tiny random candle in a stable environment.""" | |
| if not self._range_buf.is_ready(5): | |
| return False | |
| mean_r = self._range_buf.mean() | |
| if mean_r < EPSILON: | |
| return False | |
| ratio = safe_div(candle.candle_range, mean_r) | |
| # Very tiny candle compared to mean → possible noise tick | |
| return ratio < 0.05 | |
| def _detect_latency_spike(self, latency: float) -> bool: | |
| if not self._latency_buf.is_ready(5): | |
| return False | |
| mean_lat = self._latency_buf.mean() | |
| std_lat = self._latency_buf.std() | |
| if std_lat < EPSILON: | |
| return latency > mean_lat * 3.0 | |
| z = safe_div(latency - mean_lat, std_lat) | |
| return z > 3.0 | |
| def _detect_latency_drift(self) -> bool: | |
| """Detect steady creep in latency over recent window.""" | |
| if not self._latency_buf.is_ready(10): | |
| return False | |
| arr = self._latency_buf.as_array() | |
| # Simple slope: compare first half mean vs second half mean | |
| half = len(arr) // 2 | |
| first_half = float(arr[:half].mean()) | |
| second_half = float(arr[half:].mean()) | |
| if first_half < EPSILON: | |
| return False | |
| return safe_div(second_half - first_half, first_half) > 0.5 | |
| def _update_source_confidence(self, source_id: str, result: Dict) -> float: | |
| """Rolling exponential confidence score per source.""" | |
| if source_id not in self._source_scores: | |
| # AUDIT FIX (memory-leak risk): evict the least-recently-seen | |
| # source before adding a new one once at capacity, so this dict | |
| # can never grow past _MAX_SOURCES entries regardless of how many | |
| # distinct source_id values are ever observed over the engine's | |
| # 24/7 lifetime. | |
| if len(self._source_scores) >= self._MAX_SOURCES: | |
| oldest = min(self._source_last_seen, key=self._source_last_seen.get) | |
| del self._source_scores[oldest] | |
| del self._source_last_seen[oldest] | |
| self._source_scores[source_id] = RollingBuffer(30) | |
| self._source_last_seen[source_id] = self._tick_count | |
| buf = self._source_scores[source_id] | |
| # Quality this tick: start at 1.0, deduct for bad flags | |
| quality = 1.0 | |
| if not result.get("valid", True): | |
| quality -= 0.5 | |
| if result.get("spread_anomaly"): | |
| quality -= 0.1 | |
| if result.get("wick_anomaly"): | |
| quality -= 0.1 | |
| if result.get("noise_burst"): | |
| quality -= 0.1 | |
| if result.get("gap_detected"): | |
| quality -= result.get("gap_severity", 0.0) * 0.2 | |
| quality = clamp(quality) | |
| buf.push(quality) | |
| return buf.mean() if len(buf) > 0 else 1.0 | |
| # ------------------------------------------------------------------ | |
| # FALLBACK | |
| # ------------------------------------------------------------------ | |
| def _safe_fallback_result() -> Dict: | |
| return { | |
| "valid": False, "duplicate": False, "out_of_order": False, | |
| "gap_detected": False, "gap_severity": 0.0, | |
| "spread_anomaly": False, "spread_expansion": False, | |
| "wick_anomaly": False, "synthetic_tick": False, | |
| "noise_burst": False, "candle_distortion": False, | |
| "latency_spike": False, "latency_drift": False, | |
| "source_confidence": 0.5, "data_quality": 0.3, | |
| "instability_level": 0.5, "suppress_signal": True, | |
| "has_volume": False, "has_bid_ask": False, | |
| } | |
| # ------------------------------------------------------------------ | |
| # READ-ONLY PROPERTIES (for downstream engines) | |
| # ------------------------------------------------------------------ | |
| def instability(self) -> float: | |
| return self._instability | |
| def source_confidence(self) -> float: | |
| return self._source_confidence | |
| def tick_count(self) -> int: | |
| return self._tick_count | |
| # ============================================================================== | |
| # PART C MARKET STATE ASSET SESSION | |
| # ============================================================================== | |
| import math | |
| import time | |
| from typing import Dict, List, Optional, Tuple | |
| # ============================================================================== | |
| # PART C1 — ADAPTIVE MARKET STATE ENGINE (Layer 2) | |
| # ============================================================================== | |
| class MarketStateEngine: | |
| """ | |
| Classifies market state and regime adaptively from rolling candle data. | |
| Input contract: | |
| update(candle, di_result) → dict | |
| Output contract: | |
| { | |
| "market_state": str (from MARKET_STATES), | |
| "regime_label": str (from REGIME_LABELS), | |
| "market_recognition": str (from MARKET_RECOGNITION_LABELS), | |
| "trend_direction": float, # -1=bear, 0=none, 1=bull | |
| "exhaustion_prob": float, | |
| "continuation_prob": float, | |
| "regime_shift_prob": float, | |
| "fake_trend_flag": bool, | |
| "readability_hint": float, # [0,1] partial local readability | |
| } | |
| No engine modifies this engine's state. No circular deps. | |
| """ | |
| _VOL_WINDOW = 20 | |
| _HH_HL_WINDOW = 10 # candles for higher-high / lower-low detection | |
| _MOMENTUM_WINDOW = 5 | |
| _COMPRESSION_THRESHOLD = 0.4 # BB bandwidth z-score to flag compression | |
| def __init__(self) -> None: | |
| self._closes : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._highs : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._lows : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._ranges : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._bodies : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._directions: RollingBuffer = RollingBuffer(50) # +1 bull / -1 bear | |
| self._tick_count: int = 0 | |
| def update(self, candle: Candle, di_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, di_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("MarketStateEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, di_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._tick_count += 1 | |
| self._closes.push(candle.close) | |
| self._highs.push(candle.high) | |
| self._lows.push(candle.low) | |
| self._ranges.push(candle.candle_range) | |
| self._bodies.push(candle.body_size) | |
| self._directions.push(1.0 if candle.is_bullish else -1.0) | |
| if not self._closes.is_ready(MIN_WARM_CANDLES): | |
| return self._cold_default() | |
| vol_score, mean_range, std_range = self._compute_volatility() | |
| momentum = self._compute_momentum() | |
| hh_hl = self._detect_hh_hl() | |
| lh_ll = self._detect_lh_ll() | |
| compression = self._detect_compression(mean_range, std_range) | |
| fake_trend = self._detect_fake_trend(momentum, mean_range, std_range) | |
| market_state, regime, recognition = self._classify( | |
| candle, momentum, hh_hl, lh_ll, compression, fake_trend, | |
| vol_score, di_result) | |
| trend_dir = self._resolve_trend_dir(hh_hl, lh_ll, momentum) | |
| exhaustion = self._compute_exhaustion(momentum, vol_score) | |
| continuation = self._compute_continuation(momentum, hh_hl, lh_ll, compression) | |
| regime_shift = self._compute_regime_shift(market_state, momentum, vol_score) | |
| return { | |
| "market_state" : market_state, | |
| "regime_label" : regime, | |
| "market_recognition": recognition, | |
| "trend_direction" : trend_dir, | |
| "exhaustion_prob" : exhaustion, | |
| "continuation_prob" : continuation, | |
| "regime_shift_prob" : regime_shift, | |
| "fake_trend_flag" : fake_trend, | |
| "readability_hint" : self._local_readability(compression, vol_score, di_result), | |
| } | |
| # --- helpers --- | |
| def _compute_volatility(self) -> Tuple[float, float, float]: | |
| """Returns (vol_score, mean_range, std_range).""" | |
| arr = self._ranges.as_array() | |
| mean_r = float(arr.mean()) if len(arr) > 0 else 0.0 | |
| std_r = float(arr.std()) if len(arr) > 1 else 0.0 | |
| # Normalize volatility using z-score of current vs history | |
| vol_score = clamp(safe_div(mean_r, mean_r + std_r + EPSILON)) | |
| return vol_score, mean_r, std_r | |
| def _compute_momentum(self) -> float: | |
| """EMA-like directional momentum in [-1, 1].""" | |
| if not self._directions.is_ready(self._MOMENTUM_WINDOW): | |
| return 0.0 | |
| arr = self._directions.as_array()[-self._MOMENTUM_WINDOW:] | |
| # Weight recent candles more | |
| weights = [1.5 ** i for i in range(len(arr))] | |
| total_w = sum(weights) | |
| momentum = sum(d * w for d, w in zip(arr, weights)) / (total_w + EPSILON) | |
| return clamp(momentum, -1.0, 1.0) | |
| def _detect_hh_hl(self) -> bool: | |
| """True if recent highs and lows are ascending (uptrend structure).""" | |
| if not self._highs.is_ready(self._HH_HL_WINDOW): | |
| return False | |
| h = self._highs.as_array()[-self._HH_HL_WINDOW:] | |
| l = self._lows.as_array()[-self._HH_HL_WINDOW:] | |
| hh = all(h[i] >= h[i-1] - EPSILON for i in range(1, len(h))) | |
| hl = all(l[i] >= l[i-1] - EPSILON for i in range(1, len(l))) | |
| return hh and hl | |
| def _detect_lh_ll(self) -> bool: | |
| """True if recent highs and lows are descending (downtrend structure).""" | |
| if not self._highs.is_ready(self._HH_HL_WINDOW): | |
| return False | |
| h = self._highs.as_array()[-self._HH_HL_WINDOW:] | |
| l = self._lows.as_array()[-self._HH_HL_WINDOW:] | |
| lh = all(h[i] <= h[i-1] + EPSILON for i in range(1, len(h))) | |
| ll = all(l[i] <= l[i-1] + EPSILON for i in range(1, len(l))) | |
| return lh and ll | |
| def _detect_compression(self, mean_range: float, std_range: float) -> bool: | |
| """Price compressed: range shrinking relative to its own mean. | |
| FIX 3: Require min 10 candles for recent window (was 5) and use | |
| threshold 0.5 (was 0.6) to avoid false positives on noisy 1m data.""" | |
| if not self._ranges.is_ready(self._VOL_WINDOW): | |
| return False | |
| arr = self._ranges.as_array() | |
| # Need at least 10 candles for a stable recent window | |
| if len(arr) < 10: | |
| return False | |
| recent = arr[-10:].mean() | |
| if mean_range < EPSILON: | |
| return False | |
| ratio = safe_div(recent, mean_range) | |
| return ratio < 0.5 | |
| def _detect_fake_trend(self, momentum: float, mean_range: float, | |
| std_range: float) -> bool: | |
| """One-candle burst does not qualify as trend.""" | |
| if not self._ranges.is_ready(5): | |
| return False | |
| recent_body = self._bodies.as_array()[-1] if len(self._bodies) > 0 else 0.0 | |
| # Large single-candle body but weak directional momentum overall → fake | |
| body_z = safe_div(recent_body - mean_range, std_range + EPSILON) | |
| return bool(body_z > 2.5 and abs(momentum) < 0.4) | |
| def _classify(self, candle: Candle, momentum: float, | |
| hh_hl: bool, lh_ll: bool, compression: bool, fake_trend: bool, | |
| vol_score: float, di_result: Dict) -> Tuple[str, str, str]: | |
| """Return (market_state, regime_label, market_recognition).""" | |
| dq = di_result.get("data_quality", 1.0) | |
| manip_hint = di_result.get("synthetic_tick", False) or di_result.get("wick_anomaly", False) | |
| # --- Market state --- | |
| if manip_hint: | |
| state = "manipulation" | |
| elif compression: | |
| state = "compression" | |
| elif di_result.get("instability_level", 0.0) > 0.5: | |
| state = "unstable" | |
| elif di_result.get("noise_burst", False): | |
| state = "noisy" | |
| elif hh_hl and abs(momentum) > 0.5 and not fake_trend: | |
| state = "trend" | |
| elif lh_ll and abs(momentum) > 0.5 and not fake_trend: | |
| state = "trend" | |
| elif di_result.get("wick_anomaly", False): | |
| state = "sweep" | |
| elif abs(momentum) < 0.2: | |
| state = "range" | |
| else: | |
| state = "continuation" | |
| # --- Regime --- | |
| if compression: | |
| regime = "squeeze" | |
| elif manip_hint: | |
| regime = "trap" | |
| elif hh_hl and momentum > 0.4 and not fake_trend: | |
| regime = "bullish_trend" | |
| elif lh_ll and momentum < -0.4 and not fake_trend: | |
| regime = "bearish_trend" | |
| elif hh_hl and momentum > 0.1: | |
| regime = "bullish_range" | |
| elif lh_ll and momentum < -0.1: | |
| regime = "bearish_range" | |
| elif fake_trend and momentum > 0: | |
| regime = "breakout_attempt" | |
| elif fake_trend and momentum < 0: | |
| regime = "breakout_attempt" | |
| elif abs(momentum) < 0.15: | |
| regime = "neutral_dirty" | |
| elif vol_score > 0.7 and abs(momentum) > 0.5: | |
| regime = "breakout_confirmed" | |
| else: | |
| regime = "transition" | |
| # --- Market recognition --- | |
| if compression: | |
| recognition = "compression_before_expansion" | |
| elif manip_hint: | |
| recognition = "manipulation_sweep" | |
| elif hh_hl and momentum > 0.6: | |
| recognition = "clean_trend" | |
| elif lh_ll and momentum < -0.6: | |
| recognition = "clean_trend" | |
| elif (hh_hl or lh_ll) and 0.2 < abs(momentum) <= 0.6: | |
| recognition = "dirty_trend" | |
| elif state == "range" and dq > 0.7: | |
| recognition = "clean_range" | |
| elif state == "range": | |
| recognition = "dirty_range" | |
| elif state == "sweep": | |
| recognition = "manipulation_sweep" | |
| elif fake_trend: | |
| recognition = "continuation_attempt" | |
| elif di_result.get("noise_burst"): | |
| recognition = "unstable_noise_cluster" | |
| else: | |
| recognition = "continuation_attempt" | |
| return state, regime, recognition | |
| def _resolve_trend_dir(self, hh_hl: bool, lh_ll: bool, momentum: float) -> float: | |
| if hh_hl and momentum > 0.3: | |
| return 1.0 | |
| if lh_ll and momentum < -0.3: | |
| return -1.0 | |
| return 0.0 | |
| def _compute_exhaustion(self, momentum: float, vol_score: float) -> float: | |
| """Higher exhaustion when momentum extreme + volatility waning.""" | |
| mom_extreme = abs(momentum) | |
| vol_waning = clamp(1.0 - vol_score) | |
| return clamp(mom_extreme * 0.5 + vol_waning * 0.5) | |
| def _compute_continuation(self, momentum: float, hh_hl: bool, | |
| lh_ll: bool, compression: bool) -> float: | |
| base = abs(momentum) * 0.6 | |
| if hh_hl or lh_ll: | |
| base += 0.3 | |
| if compression: | |
| base -= 0.2 | |
| return clamp(base) | |
| def _compute_regime_shift(self, state: str, momentum: float, vol_score: float) -> float: | |
| """Probability that market regime is about to shift.""" | |
| if state in ("compression", "squeeze"): | |
| return clamp(0.4 + vol_score * 0.3) | |
| if abs(momentum) > 0.8: | |
| return clamp(0.2 + (abs(momentum) - 0.8) * 1.5) | |
| return clamp(abs(momentum) * 0.2) | |
| def _local_readability(self, compression: bool, vol_score: float, di_result: Dict) -> float: | |
| score = 1.0 | |
| if compression: | |
| score -= 0.15 | |
| if di_result.get("noise_burst"): | |
| score -= 0.2 | |
| if di_result.get("instability_level", 0.0) > 0.3: | |
| score -= 0.2 | |
| if di_result.get("synthetic_tick"): | |
| score -= 0.15 | |
| return clamp(score) | |
| def _cold_default() -> Dict: | |
| return { | |
| "market_state": "undefined", "regime_label": "transition", | |
| "market_recognition": "unstable_noise_cluster", | |
| "trend_direction": 0.0, "exhaustion_prob": 0.5, | |
| "continuation_prob": 0.3, "regime_shift_prob": 0.5, | |
| "fake_trend_flag": False, "readability_hint": 0.3, | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "market_state": "undefined", "regime_label": "transition", | |
| "market_recognition": "unstable_noise_cluster", | |
| "trend_direction": 0.0, "exhaustion_prob": 0.5, | |
| "continuation_prob": 0.0, "regime_shift_prob": 0.5, | |
| "fake_trend_flag": False, "readability_hint": 0.2, | |
| } | |
| # ============================================================================== | |
| # PART C2 — ASSET PROFILE ENGINE | |
| # ============================================================================== | |
| class AssetProfileEngine: | |
| """ | |
| Detects asset type (crypto/forex/OTC) from candle characteristics. | |
| Outputs RSI thresholds, volatility profile, OTC flags. | |
| Input contract: | |
| update(candle, di_result) → dict | |
| Output contract: | |
| { | |
| "asset_mode": str, | |
| "rsi_overbought": float, | |
| "rsi_oversold": float, | |
| "otc_probability": float, | |
| "crypto_probability": float, | |
| "forex_probability": float, | |
| "volatility_profile": str, # "low"|"medium"|"high"|"extreme" | |
| "broker_controlled_prob": float, | |
| } | |
| No shared state with other engines. No circular deps. | |
| """ | |
| _VOL_WINDOW = 50 | |
| _OTC_SMOOTH_WINDOW = 20 | |
| _CRYPTO_VOL_RATIO = 0.015 # average daily move ratio for crypto baseline | |
| def __init__(self) -> None: | |
| self._return_buf : RollingBuffer = RollingBuffer(self._VOL_WINDOW) | |
| self._spread_buf : RollingBuffer = RollingBuffer(self._VOL_WINDOW) | |
| self._range_buf : RollingBuffer = RollingBuffer(self._VOL_WINDOW) | |
| self._body_ratio_buf: RollingBuffer = RollingBuffer(self._OTC_SMOOTH_WINDOW) | |
| self._tick_count : int = 0 | |
| # AUDIT FIX (return-buffer bug): tracks the previous close so the | |
| # return calc below no longer has to (mis)use _return_buf for that. | |
| self._last_close : float = 0.0 | |
| # Asset probabilities (exponential moving) | |
| self._crypto_prob : float = 0.33 | |
| self._forex_prob : float = 0.33 | |
| self._otc_prob : float = 0.33 | |
| def update(self, candle: Candle, di_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, di_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("AssetProfileEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, di_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._tick_count += 1 | |
| # Compute return (pct change vs previous close) | |
| # AUDIT FIX (return-buffer bug): this previously computed `ret` and | |
| # then discarded it, pushing candle.close (a raw price) into | |
| # _return_buf instead. Since _return_buf.std() further down is used | |
| # as "volatility", that meant volatility was actually std-dev of raw | |
| # price LEVEL, not of returns -- harmless near price~1.0 (forex) by | |
| # coincidence, but completely broken for any other price magnitude | |
| # (e.g. always reads "extreme" for BTC-like prices, since std-dev of | |
| # raw prices vastly exceeds the 0.002-0.02 fractional thresholds | |
| # _classify_vol() expects). Fix: actually push the return, and track | |
| # prev_close separately instead of overloading _return_buf for it. | |
| if self._last_close > EPSILON: | |
| ret = abs(safe_div(candle.close - self._last_close, self._last_close)) | |
| else: | |
| ret = 0.0 | |
| self._return_buf.push(ret) | |
| self._last_close = candle.close | |
| self._range_buf.push(candle.candle_range) | |
| self._body_ratio_buf.push(candle.body_ratio) | |
| if candle.spread > EPSILON: | |
| self._spread_buf.push(candle.spread) | |
| # Need some warmup | |
| if not self._return_buf.is_ready(10): | |
| return self._safe_default() | |
| vol = self._return_buf.std() | |
| avg_range = self._range_buf.mean() | |
| price = candle.close | |
| # Crypto signal: high volatility, price often large absolute value | |
| crypto_vol_signal = clamp(safe_div(vol, self._CRYPTO_VOL_RATIO)) | |
| # OTC signal: synthetic smoothness (very high body ratios) | |
| avg_body_ratio = self._body_ratio_buf.mean() | |
| otc_smooth_signal = clamp((avg_body_ratio - 0.5) * 2.0) | |
| otc_synthetic = float(di_result.get("synthetic_tick", False)) | |
| otc_signal = clamp(0.6 * otc_smooth_signal + 0.4 * otc_synthetic) | |
| # Forex signal: small spread relative to price, small absolute range | |
| if price > EPSILON and self._spread_buf.is_ready(5): | |
| spread_ratio = safe_div(self._spread_buf.mean(), price) | |
| forex_spread_signal = clamp(1.0 - spread_ratio * 1000.0) # pips | |
| else: | |
| forex_spread_signal = 0.3 | |
| # Update EMA probabilities | |
| alpha = 0.1 | |
| crypto_new = clamp(0.5 * crypto_vol_signal + 0.5 * (1.0 - otc_signal)) | |
| otc_new = otc_signal | |
| forex_new = clamp(forex_spread_signal * (1.0 - otc_signal) * 0.7) | |
| # Normalize to sum to 1 | |
| total = crypto_new + otc_new + forex_new + EPSILON | |
| crypto_new /= total | |
| otc_new /= total | |
| forex_new /= total | |
| self._crypto_prob = clamp(alpha * crypto_new + (1 - alpha) * self._crypto_prob) | |
| self._otc_prob = clamp(alpha * otc_new + (1 - alpha) * self._otc_prob) | |
| self._forex_prob = clamp(alpha * forex_new + (1 - alpha) * self._forex_prob) | |
| # Normalize again | |
| total = self._crypto_prob + self._otc_prob + self._forex_prob + EPSILON | |
| self._crypto_prob /= total | |
| self._otc_prob /= total | |
| self._forex_prob /= total | |
| # Determine dominant mode | |
| if max(self._crypto_prob, self._otc_prob, self._forex_prob) < 0.4: | |
| mode = "hybrid" | |
| elif self._crypto_prob > self._otc_prob and self._crypto_prob > self._forex_prob: | |
| mode = "crypto" | |
| elif self._otc_prob > self._forex_prob: | |
| mode = "OTC" | |
| else: | |
| mode = "forex" | |
| # RSI thresholds per mode | |
| if mode in ("crypto",): | |
| rsi_ob, rsi_os = RSI_OB_CRYPTO, RSI_OS_CRYPTO | |
| else: | |
| rsi_ob, rsi_os = RSI_OB_DEFAULT, RSI_OS_DEFAULT | |
| # Volatility profile | |
| vol_profile = self._classify_vol(vol) | |
| return { | |
| "asset_mode" : mode, | |
| "rsi_overbought" : rsi_ob, | |
| "rsi_oversold" : rsi_os, | |
| "otc_probability" : self._otc_prob, | |
| "crypto_probability" : self._crypto_prob, | |
| "forex_probability" : self._forex_prob, | |
| "volatility_profile" : vol_profile, | |
| "broker_controlled_prob": clamp(self._otc_prob * 0.8 + otc_smooth_signal * 0.2), | |
| } | |
| def _classify_vol(self, vol: float) -> str: | |
| if vol < 0.002: | |
| return "low" | |
| elif vol < 0.008: | |
| return "medium" | |
| elif vol < 0.020: | |
| return "high" | |
| else: | |
| return "extreme" | |
| def _safe_default() -> Dict: | |
| return { | |
| "asset_mode": "unknown", | |
| "rsi_overbought": RSI_OB_DEFAULT, | |
| "rsi_oversold": RSI_OS_DEFAULT, | |
| "otc_probability": 0.33, | |
| "crypto_probability": 0.33, | |
| "forex_probability": 0.33, | |
| "volatility_profile": "medium", | |
| "broker_controlled_prob": 0.33, | |
| } | |
| # ============================================================================== | |
| # PART C3 — SESSION INTELLIGENCE ENGINE | |
| # ============================================================================== | |
| class SessionIntelligenceEngine: | |
| """ | |
| Detects current forex/crypto trading session from UTC timestamp. | |
| Computes session-based timing penalty/boost and liquidity context. | |
| Input contract: | |
| update(candle) → dict | |
| Output contract: | |
| { | |
| "session_label": str, | |
| "session_liquidity": float, # [0,1] | |
| "timing_penalty": float, # [0,1] → higher = worse timing | |
| "weekend_flag": bool, | |
| "rollover_flag": bool, | |
| "session_transition_flag": bool, | |
| "session_score": float, # [0,1] timing quality | |
| } | |
| """ | |
| # UTC hour ranges (start, end) — intentionally simple, non-DST | |
| _SESSIONS = { | |
| "asia" : (0, 8), | |
| "london" : (7, 12), | |
| "ny_london_overlap": (12, 17), | |
| "ny" : (12, 21), | |
| "asia_london_overlap": (7, 9), | |
| "rollover" : (21, 23), | |
| "off_hours" : (23, 24), | |
| } | |
| # Session liquidity score | |
| _SESSION_LIQUIDITY = { | |
| "ny_london_overlap" : 1.0, | |
| "london" : 0.85, | |
| "ny" : 0.80, | |
| "asia_london_overlap": 0.70, | |
| "asia" : 0.55, | |
| "rollover" : 0.30, | |
| "off_hours" : 0.20, | |
| } | |
| def __init__(self) -> None: | |
| self._last_session : str = "unknown" | |
| self._session_tick : int = 0 | |
| def update(self, candle: Candle) -> Dict: | |
| try: | |
| return self._update_inner(candle) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("SessionIntelligenceEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle) -> Dict: | |
| self._session_tick += 1 | |
| ts = candle.timestamp | |
| if not math.isfinite(ts) or ts <= 0: | |
| return self._safe_default() | |
| import datetime | |
| try: | |
| dt = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc).replace(tzinfo=None) | |
| except (OSError, OverflowError, ValueError): | |
| # FIX-6: OS timezone broken — reuse the last known session rather than | |
| # returning an off_hours generic default which wrongly penalises timing. | |
| if self._last_session not in ("unknown", "off_hours"): | |
| liq = self._SESSION_LIQUIDITY.get(self._last_session, 0.4) | |
| tp = clamp(1.0 - liq) | |
| return { | |
| "session_label" : self._last_session, | |
| "session_liquidity" : liq, | |
| "timing_penalty" : tp, | |
| "weekend_flag" : False, | |
| "rollover_flag" : False, | |
| "session_transition_flag" : False, | |
| "session_score" : clamp(1.0 - tp), | |
| } | |
| return self._safe_default() | |
| hour = dt.hour | |
| weekday = dt.weekday() # 0=Mon … 6=Sun | |
| weekend = weekday >= 5 # Sat, Sun | |
| # Rollover window | |
| rollover = 20 <= hour < 22 | |
| # Determine session | |
| session = self._detect_session(hour, weekend) | |
| # Transition | |
| transition = session != self._last_session and self._last_session != "unknown" | |
| self._last_session = session | |
| liquidity = self._SESSION_LIQUIDITY.get(session, 0.4) | |
| if weekend: | |
| liquidity *= 0.3 | |
| # Timing penalty: inverse of liquidity, plus penalties | |
| timing_penalty = clamp(1.0 - liquidity) | |
| if rollover: | |
| timing_penalty = clamp(timing_penalty + 0.2) | |
| if transition: | |
| timing_penalty = clamp(timing_penalty + 0.1) | |
| if weekend: | |
| timing_penalty = clamp(timing_penalty + 0.4) | |
| session_score = clamp(1.0 - timing_penalty) | |
| return { | |
| "session_label" : session, | |
| "session_liquidity" : liquidity, | |
| "timing_penalty" : timing_penalty, | |
| "weekend_flag" : weekend, | |
| "rollover_flag" : rollover, | |
| "session_transition_flag" : transition, | |
| "session_score" : session_score, | |
| } | |
| def _detect_session(self, hour: int, weekend: bool) -> str: | |
| if weekend: | |
| return "off_hours" | |
| if 20 <= hour < 22: | |
| return "rollover" | |
| if hour == 23: | |
| return "off_hours" | |
| if 7 <= hour < 9: | |
| return "asia_london_overlap" | |
| if 12 <= hour < 17: | |
| return "ny_london_overlap" | |
| if 7 <= hour < 12: | |
| return "london" | |
| if 12 <= hour < 21: | |
| return "ny" | |
| if 0 <= hour < 8: | |
| return "asia" | |
| return "off_hours" | |
| def _safe_default() -> Dict: | |
| return { | |
| "session_label": "off_hours", | |
| "session_liquidity": 0.4, | |
| "timing_penalty": 0.3, | |
| "weekend_flag": False, | |
| "rollover_flag": False, | |
| "session_transition_flag": False, | |
| "session_score": 0.5, | |
| } | |
| # ============================================================================== | |
| # PART D MTF EVENTS LIQUIDITY | |
| # ============================================================================== | |
| import math | |
| from collections import deque | |
| from typing import Dict, List, Optional, Tuple | |
| # ============================================================================== | |
| # PART D1 — MULTI-TIMEFRAME FUSION ENGINE (Layer 3) | |
| # ============================================================================== | |
| class MultiTimeframeFusionEngine: | |
| """ | |
| Synthesizes three logical timeframe layers from a single OHLC stream. | |
| Low TF = short rolling window (recent momentum). | |
| Mid TF = medium rolling window (structural continuation). | |
| High TF = long rolling window (macro bias). | |
| Input contract: | |
| update(candle, ms_result) → dict | |
| Output contract: | |
| { | |
| "tf_alignment": float, # [0,1] all layers aligned | |
| "tf_conflict_score": float, # [0,1] how much layers disagree | |
| "lower_bias": float, # [-1,1] | |
| "mid_bias": float, # [-1,1] | |
| "higher_bias": float, # [-1,1] | |
| "trend_persistence": float, # [0,1] | |
| "reversal_probability": float, # [0,1] | |
| "timeframe_label": str, # detected base TF | |
| } | |
| """ | |
| _LOW_TF_WIN = 5 | |
| _MID_TF_WIN = 20 | |
| _HIGH_TF_WIN = 60 | |
| def __init__(self) -> None: | |
| self._closes : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._highs : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._lows : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._ts_buf : RollingBuffer = RollingBuffer(50) | |
| self._detected_tf_seconds: float = 60.0 | |
| def update(self, candle: Candle, ms_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, ms_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("MultiTimeframeFusionEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, ms_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._closes.push(candle.close) | |
| self._highs.push(candle.high) | |
| self._lows.push(candle.low) | |
| self._ts_buf.push(candle.timestamp) | |
| # Auto-detect timeframe from timestamp intervals | |
| if self._ts_buf.is_ready(3): | |
| arr = self._ts_buf.as_array() | |
| diffs = arr[1:] - arr[:-1] | |
| diffs = diffs[diffs > 0] | |
| if len(diffs) > 0: | |
| self._detected_tf_seconds = float(diffs.mean()) | |
| if not self._closes.is_ready(self._LOW_TF_WIN + 1): | |
| return self._safe_default() | |
| low_bias = self._compute_bias(self._LOW_TF_WIN) | |
| mid_bias = self._compute_bias(self._MID_TF_WIN) | |
| high_bias = self._compute_bias(self._HIGH_TF_WIN) | |
| # Alignment: product of normalized biases; max when all point same direction | |
| alignment = self._compute_alignment(low_bias, mid_bias, high_bias) | |
| conflict = clamp(1.0 - alignment) | |
| persistence = self._compute_persistence(ms_result) | |
| reversal_prob = self._compute_reversal(low_bias, high_bias, ms_result) | |
| tf_label = self._label_tf(self._detected_tf_seconds) | |
| return { | |
| "tf_alignment" : alignment, | |
| "tf_conflict_score" : conflict, | |
| "lower_bias" : low_bias, | |
| "mid_bias" : mid_bias, | |
| "higher_bias" : high_bias, | |
| "trend_persistence" : persistence, | |
| "reversal_probability": reversal_prob, | |
| "timeframe_label" : tf_label, | |
| } | |
| def _compute_bias(self, window: int) -> float: | |
| """EMA slope as directional bias in [-1,1].""" | |
| n = min(window, len(self._closes)) | |
| if n < 2: | |
| return 0.0 | |
| arr = self._closes.as_array()[-n:] | |
| if len(arr) < 2: | |
| return 0.0 | |
| # Bias: (last - first) / max_range | |
| rng = float(arr.max() - arr.min()) | |
| if rng < EPSILON: | |
| return 0.0 | |
| raw_bias = safe_div(float(arr[-1]) - float(arr[0]), rng) | |
| return clamp(raw_bias, -1.0, 1.0) | |
| def _compute_alignment(self, low: float, mid: float, high: float) -> float: | |
| """Alignment: all three biases same sign and magnitude.""" | |
| # Sign agreement | |
| signs = [math.copysign(1, x) if abs(x) > 0.1 else 0 for x in (low, mid, high)] | |
| nonzero_signs = [s for s in signs if s != 0] | |
| if not nonzero_signs: | |
| return 0.5 | |
| sign_agree = len(set(nonzero_signs)) == 1 | |
| if not sign_agree: | |
| return clamp(0.3 - abs(low + mid + high) * 0.1) | |
| # Magnitude alignment: average of absolute biases | |
| avg_mag = (abs(low) + abs(mid) + abs(high)) / 3.0 | |
| return clamp(0.5 + avg_mag * 0.5) | |
| def _compute_persistence(self, ms_result: Dict) -> float: | |
| cont = ms_result.get("continuation_prob", 0.5) | |
| exh = ms_result.get("exhaustion_prob", 0.5) | |
| return clamp(cont - exh * 0.3) | |
| def _compute_reversal(self, low: float, high: float, ms_result: Dict) -> float: | |
| """Reversal probability: low TF opposes high TF bias.""" | |
| diverge = abs(low - high) * 0.5 | |
| exh = ms_result.get("exhaustion_prob", 0.0) * 0.5 | |
| return clamp(diverge + exh) | |
| def _label_tf(seconds: float) -> str: | |
| if seconds <= 6: return "5s" | |
| if seconds <= 35: return "30s" | |
| if seconds <= 75: return "1m" | |
| if seconds <= 130: return "2m" | |
| if seconds <= 310: return "5m" | |
| if seconds <= 610: return "10m" | |
| if seconds <= 910: return "15m" | |
| return "higher" | |
| def detected_tf_seconds(self) -> float: | |
| """TIMING-FIX-1: Expose auto-detected candle interval for downstream | |
| timeframe-aware stale-signal and cooling computations.""" | |
| return self._detected_tf_seconds | |
| def _safe_default() -> Dict: | |
| return { | |
| "tf_alignment": 0.5, "tf_conflict_score": 0.5, | |
| "lower_bias": 0.0, "mid_bias": 0.0, "higher_bias": 0.0, | |
| "trend_persistence": 0.3, "reversal_probability": 0.3, | |
| "timeframe_label": "1m", | |
| } | |
| # ============================================================================== | |
| # PART D2 — REAL-TIME EVENT DETECTION ENGINE (Layer 4) | |
| # ============================================================================== | |
| _EVENT_FIELDS = ( | |
| "intensity", "confidence", "continuation_prob", | |
| "reversal_prob", "liquidity_impact", "pressure_impact", "manip_prob", | |
| ) | |
| def _make_event(name: str, severity: str, **kwargs) -> Dict: | |
| """Build a validated event dict.""" | |
| evt: Dict = { | |
| "name" : name, | |
| "severity" : severity if severity in EVENT_SEVERITIES else "minor", | |
| "intensity" : clamp(kwargs.get("intensity", 0.5)), | |
| "confidence" : clamp(kwargs.get("confidence", 0.5)), | |
| "continuation_prob": clamp(kwargs.get("continuation_prob", 0.5)), | |
| "reversal_prob" : clamp(kwargs.get("reversal_prob", 0.5)), | |
| "liquidity_impact" : clamp(kwargs.get("liquidity_impact", 0.3)), | |
| "pressure_impact" : clamp(kwargs.get("pressure_impact", 0.3)), | |
| "manip_prob" : clamp(kwargs.get("manip_prob", 0.1)), | |
| } | |
| return evt | |
| class EventDetectionEngine: | |
| """ | |
| Detects 14 real-time market events from candle structure. | |
| Each event carries full scoring metadata (Section 12). | |
| Input contract: | |
| update(candle, di_result, ms_result) → dict | |
| Output contract: | |
| { | |
| "events": [event_dict, ...], # active events this tick | |
| "dominant_event": str, # strongest event name | |
| "event_severity": str, # overall severity | |
| "event_cluster": bool, # multiple simultaneous events | |
| "direction_influence": float, # [-1,1] net directional push | |
| "manipulation_flag": bool, | |
| } | |
| """ | |
| _RANGE_WINDOW = 20 | |
| _MOMENTUM_WINDOW = 5 | |
| _CLUSTER_THRESHOLD = 2 # min events to be a cluster | |
| _DEDUP_WINDOW = 5 # FIX-8: suppress same event if seen within this many ticks | |
| def __init__(self) -> None: | |
| self._range_buf : RollingBuffer = RollingBuffer(self._RANGE_WINDOW) | |
| self._close_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._high_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._low_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._body_buf : RollingBuffer = RollingBuffer(self._RANGE_WINDOW) | |
| self._direction_buf: RollingBuffer = RollingBuffer(self._MOMENTUM_WINDOW) | |
| self._wick_zones : deque = deque(maxlen=10) # (high, low) memory | |
| self._event_history: RollingObjectBuffer = RollingObjectBuffer(20) | |
| # FIX-8: deduplication state — track last tick each event type was emitted | |
| self._tick_count : int = 0 | |
| self._last_event_tick : Dict[str, int] = {} | |
| def update(self, candle: Candle, di_result: Dict, ms_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, di_result, ms_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("EventDetectionEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, di_result: Dict, ms_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._tick_count += 1 # FIX-8: track absolute tick for dedup window | |
| self._range_buf.push(candle.candle_range) | |
| self._close_buf.push(candle.close) | |
| self._high_buf.push(candle.high) | |
| self._low_buf.push(candle.low) | |
| self._body_buf.push(candle.body_size) | |
| self._direction_buf.push(1.0 if candle.is_bullish else -1.0) | |
| self._wick_zones.append((candle.high, candle.low)) | |
| if not self._range_buf.is_ready(5): | |
| return self._safe_default() | |
| events: List[Dict] = [] | |
| mean_range = self._range_buf.mean() | |
| std_range = self._range_buf.std() | |
| mean_body = self._body_buf.mean() | |
| # AUDIT FIX (body_z statistical inconsistency): body_z previously | |
| # normalized by std_range (the RANGE buffer's spread) while its | |
| # numerator came from the BODY buffer's mean -- mixing two different | |
| # distributions. range_z, just below, correctly pairs mean_range with | |
| # std_range from the same buffer; body_z should likewise be a proper | |
| # z-score against the body buffer's own spread. This only changes the | |
| # sensitivity of how readily body_z crosses the 1.5/-0.5 thresholds | |
| # used below (events 1 and 14); it does not change what's being | |
| # measured conceptually. | |
| std_body = self._body_buf.std() | |
| # Precompute shared values | |
| cr = candle.candle_range | |
| body = candle.body_size | |
| uw = candle.upper_wick | |
| lw = candle.lower_wick | |
| body_z = safe_div(body - mean_body, std_body + EPSILON) | |
| range_z = safe_div(cr - mean_range, std_range + EPSILON) | |
| momentum = self._direction_buf.mean() if len(self._direction_buf) > 1 else 0.0 | |
| # 1. Sudden aggressive move | |
| if range_z > 2.5 and body_z > 1.5: | |
| events.append(_make_event("sudden_aggressive_move", "strong", | |
| intensity=clamp(range_z / 4.0), confidence=0.7, | |
| continuation_prob=0.55, reversal_prob=0.45, | |
| pressure_impact=0.8, liquidity_impact=0.6)) | |
| # 2. Fast rejection (large wick, small body) | |
| if cr > EPSILON: | |
| wick_dom = safe_div(max(uw, lw), cr) | |
| if wick_dom > 0.6 and range_z > 1.0: | |
| dir_rev = -1.0 if uw > lw else 1.0 # reject up → sell pressure | |
| events.append(_make_event("fast_rejection", "moderate", | |
| intensity=wick_dom, confidence=0.65, | |
| continuation_prob=0.35, reversal_prob=0.65, | |
| liquidity_impact=0.5, manip_prob=0.2)) | |
| # 3. Liquidity sweep (spike beyond then retrace) | |
| if self._close_buf.is_ready(3): | |
| closes = self._close_buf.as_array()[-3:] | |
| highs = self._high_buf.as_array()[-3:] | |
| lows = self._low_buf.as_array()[-3:] | |
| if highs[-1] > highs[-2] and candle.close < closes[-2]: | |
| events.append(_make_event("liquidity_sweep", "strong", | |
| intensity=0.8, confidence=0.75, | |
| continuation_prob=0.3, reversal_prob=0.7, | |
| liquidity_impact=0.9, manip_prob=0.5)) | |
| elif lows[-1] < lows[-2] and candle.close > closes[-2]: | |
| events.append(_make_event("liquidity_sweep", "strong", | |
| intensity=0.8, confidence=0.75, | |
| continuation_prob=0.3, reversal_prob=0.7, | |
| liquidity_impact=0.9, manip_prob=0.5)) | |
| # 4. Stop hunt (fast spike beyond key level + immediate reversal) | |
| if di_result.get("wick_anomaly", False) and range_z > 1.5: | |
| events.append(_make_event("stop_hunt", "strong", | |
| intensity=0.75, confidence=0.6, | |
| continuation_prob=0.25, reversal_prob=0.75, | |
| manip_prob=0.7)) | |
| # 5. Fake breakout (strong range but weak close) | |
| if range_z > 1.5 and candle.close_position < 0.3 and candle.is_bullish is False: | |
| events.append(_make_event("fake_breakout", "moderate", | |
| intensity=0.65, confidence=0.6, | |
| continuation_prob=0.2, reversal_prob=0.8, | |
| manip_prob=0.4)) | |
| elif range_z > 1.5 and candle.close_position > 0.7 and candle.is_bullish is True: | |
| # potentially fake if momentum was already exhausted | |
| if ms_result.get("exhaustion_prob", 0.0) > 0.6: | |
| events.append(_make_event("fake_breakout", "moderate", | |
| intensity=0.6, confidence=0.55, | |
| continuation_prob=0.25, reversal_prob=0.75, | |
| manip_prob=0.35)) | |
| # 6. Trap move | |
| if di_result.get("synthetic_tick", False) and range_z > 1.0: | |
| events.append(_make_event("trap_move", "moderate", | |
| intensity=0.7, confidence=0.55, | |
| continuation_prob=0.2, reversal_prob=0.8, | |
| manip_prob=0.75)) | |
| # 7. Spread spike | |
| if di_result.get("spread_expansion", False): | |
| events.append(_make_event("spread_spike", "moderate", | |
| intensity=0.6, confidence=0.8, | |
| continuation_prob=0.4, reversal_prob=0.4, | |
| manip_prob=0.3)) | |
| # 8. Unusual volatility burst | |
| if range_z > 3.0: | |
| events.append(_make_event("unusual_volatility_burst", "extreme", | |
| intensity=clamp(range_z / 5.0), confidence=0.9, | |
| continuation_prob=0.45, reversal_prob=0.55, | |
| pressure_impact=0.9)) | |
| # 9. Momentum flip | |
| if len(self._direction_buf) >= 3: | |
| dirs = self._direction_buf.as_array()[-3:] | |
| if dirs[-1] * dirs[0] < 0 and dirs[-1] * dirs[1] < 0: | |
| events.append(_make_event("momentum_flip", "moderate", | |
| intensity=0.6, confidence=0.65, | |
| continuation_prob=0.6, reversal_prob=0.4, | |
| pressure_impact=0.5)) | |
| # 10. Hidden absorption (large range, tiny body → buyers/sellers absorbing) | |
| if cr > EPSILON: | |
| body_to_range = safe_div(body, cr) | |
| if body_to_range < 0.2 and range_z > 0.5: | |
| events.append(_make_event("hidden_absorption", "moderate", | |
| intensity=1.0 - body_to_range, confidence=0.6, | |
| continuation_prob=0.45, reversal_prob=0.55, | |
| liquidity_impact=0.7)) | |
| # 11. Spoof-like behavior (OTC synthetic flag + spread anomaly) | |
| if di_result.get("synthetic_tick") and di_result.get("spread_anomaly"): | |
| events.append(_make_event("spoof_like_behavior", "moderate", | |
| intensity=0.65, confidence=0.5, | |
| continuation_prob=0.3, reversal_prob=0.7, | |
| manip_prob=0.8)) | |
| # 12. Micro reversal cluster (repeated wick rejections at same zone) | |
| rejection_zone = self._detect_rejection_cluster(candle) | |
| if rejection_zone: | |
| events.append(_make_event("micro_reversal_cluster", "moderate", | |
| intensity=0.7, confidence=0.65, | |
| continuation_prob=0.25, reversal_prob=0.75, | |
| liquidity_impact=0.6)) | |
| # 13. Liquidity exhaustion (momentum dying, range collapsing) | |
| if ms_result.get("exhaustion_prob", 0.0) > 0.7 and range_z < -0.5: | |
| events.append(_make_event("liquidity_exhaustion", "moderate", | |
| intensity=ms_result.get("exhaustion_prob", 0.5), | |
| confidence=0.6, continuation_prob=0.2, reversal_prob=0.8)) | |
| # 14. Fake continuation (gap detected + weak body) | |
| if di_result.get("gap_detected", False) and body_z < -0.5: | |
| events.append(_make_event("fake_continuation", "moderate", | |
| intensity=0.6, confidence=0.55, | |
| continuation_prob=0.2, reversal_prob=0.8, | |
| manip_prob=0.3)) | |
| # Store event names in history | |
| for e in events: | |
| self._event_history.push(e["name"]) | |
| # FIX-8: Deduplicate — suppress any event type that already fired within | |
| # _DEDUP_WINDOW ticks. Prevents liquidity_sweep (and others) from spamming | |
| # on consecutive candles when the triggering condition persists. | |
| tick = self._tick_count | |
| deduped: List[Dict] = [] | |
| for e in events: | |
| name = e["name"] | |
| if (tick - self._last_event_tick.get(name, -9999)) >= self._DEDUP_WINDOW: | |
| deduped.append(e) | |
| self._last_event_tick[name] = tick | |
| events = deduped | |
| # Aggregate result | |
| return self._aggregate(events) | |
| def _detect_rejection_cluster(self, candle: Candle) -> bool: | |
| """True if this candle's wick matches at least 2 prior rejection zones.""" | |
| if len(self._wick_zones) < 3: | |
| return False | |
| tolerance = candle.candle_range * 0.1 + EPSILON | |
| uw_zone = candle.high | |
| lw_zone = candle.low | |
| matches = 0 | |
| for (ph, pl) in list(self._wick_zones)[:-1]: | |
| if abs(ph - uw_zone) < tolerance or abs(pl - lw_zone) < tolerance: | |
| matches += 1 | |
| return matches >= 2 | |
| def _aggregate(self, events: List[Dict]) -> Dict: | |
| if not events: | |
| return self._no_event_result() | |
| # Dominant: highest intensity | |
| dominant = max(events, key=lambda e: e["intensity"]) | |
| # Overall severity | |
| severity_order = {"minor": 0, "moderate": 1, "strong": 2, "extreme": 3} | |
| max_sev = max(events, key=lambda e: severity_order.get(e["severity"], 0)) | |
| # Direction influence: weighted by intensity × reversal vs continuation | |
| dir_influence = 0.0 | |
| total_w = 0.0 | |
| for e in events: | |
| w = e["intensity"] | |
| # reversal events push direction opposite momentum, continuation forward | |
| if e["reversal_prob"] > e["continuation_prob"]: | |
| contrib = -w * (e["reversal_prob"] - 0.5) * 2.0 | |
| else: | |
| contrib = w * (e["continuation_prob"] - 0.5) * 2.0 | |
| dir_influence += contrib | |
| total_w += w | |
| dir_influence = clamp(safe_div(dir_influence, total_w + EPSILON), -1.0, 1.0) | |
| manip_flag = any(e["manip_prob"] > 0.6 for e in events) | |
| return { | |
| "events" : events, | |
| "dominant_event" : dominant["name"], | |
| "event_severity" : max_sev["severity"], | |
| "event_cluster" : len(events) >= self._CLUSTER_THRESHOLD, | |
| "direction_influence": dir_influence, | |
| "manipulation_flag" : manip_flag, | |
| } | |
| def _no_event_result() -> Dict: | |
| return { | |
| "events": [], "dominant_event": "none", | |
| "event_severity": "minor", "event_cluster": False, | |
| "direction_influence": 0.0, "manipulation_flag": False, | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "events": [], "dominant_event": "none", | |
| "event_severity": "minor", "event_cluster": False, | |
| "direction_influence": 0.0, "manipulation_flag": False, | |
| } | |
| # ============================================================================== | |
| # PART D3 — LIQUIDITY + PRESSURE ENGINE (Layer 5) | |
| # ============================================================================== | |
| class LiquidityPressureEngine: | |
| """ | |
| Tracks buying/selling pressure balance, liquidity reaction, | |
| micro-orderflow proxy (no order book), liquidity void/magnet detection. | |
| Input contract: | |
| update(candle, di_result, event_result) → dict | |
| Output contract: | |
| { | |
| "pressure_score": float, # [0,1] bullish pressure | |
| "liquidity_score": float, # [0,1] liquidity quality | |
| "buy_pressure": float, | |
| "sell_pressure": float, | |
| "absorption_score": float, # [0,1] absorption detected | |
| "void_score": float, # [0,1] liquidity void | |
| "magnet_zone": bool, # price near liquidity magnet | |
| "manipulation_proxy": float, # [0,1] | |
| "pressure_chain": float, # multi-candle pressure persistence | |
| "otc_fraud_score": float, # Synthetic/broker-control signal | |
| } | |
| """ | |
| _WINDOW = 20 | |
| _CHAIN_WINDOW = 7 | |
| def __init__(self) -> None: | |
| self._close_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._high_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._low_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._range_buf : RollingBuffer = RollingBuffer(self._WINDOW) | |
| self._body_buf : RollingBuffer = RollingBuffer(self._WINDOW) | |
| self._vol_buf : RollingBuffer = RollingBuffer(self._WINDOW) | |
| self._close_pos_buf: RollingBuffer = RollingBuffer(self._WINDOW) # close position [0,1] | |
| self._direction_buf: RollingBuffer = RollingBuffer(self._CHAIN_WINDOW) | |
| self._wick_ratio_buf: RollingBuffer = RollingBuffer(self._WINDOW) | |
| # OTC fraud tracking | |
| self._smoothness_buf: RollingBuffer = RollingBuffer(self._WINDOW) | |
| # Liquidity levels: track recent swing highs/lows | |
| self._swing_highs: deque = deque(maxlen=10) | |
| self._swing_lows : deque = deque(maxlen=10) | |
| def update(self, candle: Candle, di_result: Dict, event_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, di_result, event_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("LiquidityPressureEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, di_result: Dict, event_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| # Push to buffers | |
| self._close_buf.push(candle.close) | |
| self._high_buf.push(candle.high) | |
| self._low_buf.push(candle.low) | |
| self._range_buf.push(candle.candle_range) | |
| self._body_buf.push(candle.body_size) | |
| vol = candle.volume if candle.volume > EPSILON else 0.0 | |
| self._vol_buf.push(vol) | |
| self._close_pos_buf.push(candle.close_position) | |
| self._direction_buf.push(1.0 if candle.is_bullish else -1.0) | |
| cr = candle.candle_range | |
| if cr > EPSILON: | |
| uw_ratio = safe_div(candle.upper_wick, cr) | |
| lw_ratio = safe_div(candle.lower_wick, cr) | |
| self._wick_ratio_buf.push(uw_ratio - lw_ratio) # >0 = upper wick dominant | |
| # Smoothness: high body ratio = synthetic | |
| self._smoothness_buf.push(candle.body_ratio) | |
| else: | |
| self._wick_ratio_buf.push(0.0) | |
| self._smoothness_buf.push(0.5) | |
| # Update swing high/low | |
| self._update_swings(candle) | |
| # Need warmup | |
| if not self._range_buf.is_ready(5): | |
| return self._safe_default() | |
| # --- Pressure analysis --- | |
| buy_p, sell_p, pressure = self._compute_pressure(candle) | |
| # --- Absorption --- | |
| absorption = self._compute_absorption(candle) | |
| # --- Liquidity quality --- | |
| liquidity = self._compute_liquidity(candle, di_result) | |
| # --- Void map --- | |
| void_score = self._compute_void(candle) | |
| # --- Magnet zone --- | |
| magnet = self._detect_magnet(candle) | |
| # --- Manipulation proxy --- | |
| manip_proxy = self._compute_manip_proxy(candle, di_result, event_result) | |
| # --- Pressure chain (multi-candle) --- | |
| chain = self._compute_pressure_chain() | |
| # --- OTC fraud score --- | |
| otc_fraud = self._compute_otc_fraud(di_result) | |
| return { | |
| "pressure_score" : pressure, | |
| "liquidity_score" : liquidity, | |
| "buy_pressure" : buy_p, | |
| "sell_pressure" : sell_p, | |
| "absorption_score" : absorption, | |
| "void_score" : void_score, | |
| "magnet_zone" : magnet, | |
| "manipulation_proxy": manip_proxy, | |
| "pressure_chain" : chain, | |
| "otc_fraud_score" : otc_fraud, | |
| } | |
| # --- sub-computations --- | |
| def _compute_pressure(self, candle: Candle) -> Tuple[float, float, float]: | |
| """ | |
| Aggressive candle pressure proxy (no order book). | |
| Uses body position + close position + volume proxy. | |
| """ | |
| close_pos = candle.close_position # [0,1] | |
| body_size = candle.body_size | |
| mean_body = self._body_buf.mean() + EPSILON | |
| # Volume-normalized pressure (or body-based if no volume) | |
| # AUDIT FIX (volume-proxy bug): is_ready(3) only checks that 3 values | |
| # were pushed -- but candle.volume defaults to 0.0 and 0.0 is always | |
| # pushed (see _update_inner), so for any feed that never supplies | |
| # volume, is_ready(3) becomes True after 3 ticks even though every | |
| # value in the buffer is 0.0. That made mean_vol collapse to ~EPSILON | |
| # forever, which makes vol_factor (and therefore buy_p/sell_p) lock | |
| # at exactly 0.0 permanently -- the documented "0 = absent, triggers | |
| # proxy logic" fallback never re-engaged after the 3rd candle. Also | |
| # require the recent mean to be meaningfully non-zero. | |
| if self._vol_buf.is_ready(3) and self._vol_buf.mean() > EPSILON: | |
| vol = candle.volume | |
| mean_vol = self._vol_buf.mean() + EPSILON | |
| vol_factor = clamp(safe_div(vol, mean_vol)) | |
| else: | |
| vol_factor = clamp(safe_div(body_size, mean_body)) | |
| buy_p = clamp(close_pos * vol_factor) | |
| sell_p = clamp((1.0 - close_pos) * vol_factor) | |
| # Pressure score: normalized bullish bias | |
| pressure = clamp(close_pos * 0.6 + vol_factor * 0.4) | |
| return buy_p, sell_p, pressure | |
| def _compute_absorption(self, candle: Candle) -> float: | |
| """Hidden absorption: large range + tiny body = buyers/sellers absorbing.""" | |
| cr = candle.candle_range | |
| mean_r = self._range_buf.mean() + EPSILON | |
| if cr < EPSILON: | |
| return 0.0 | |
| body_to_range = safe_div(candle.body_size, cr) | |
| range_factor = clamp(safe_div(cr, mean_r) - 1.0) | |
| return clamp((1.0 - body_to_range) * (0.5 + range_factor * 0.5)) | |
| def _compute_liquidity(self, candle: Candle, di_result: Dict) -> float: | |
| """Aggregate liquidity quality from spread, data quality, range stability.""" | |
| base = di_result.get("source_confidence", 0.7) | |
| if di_result.get("spread_anomaly"): | |
| base -= 0.2 | |
| if di_result.get("spread_expansion"): | |
| base -= 0.15 | |
| if di_result.get("noise_burst"): | |
| base -= 0.1 | |
| if di_result.get("gap_detected"): | |
| base -= di_result.get("gap_severity", 0.0) * 0.2 | |
| return clamp(base) | |
| def _compute_void(self, candle: Candle) -> float: | |
| """Liquidity void: rapid price jump through thin zone.""" | |
| if not self._range_buf.is_ready(10): | |
| return 0.0 | |
| mean_r = self._range_buf.mean() | |
| std_r = self._range_buf.std() | |
| if mean_r < EPSILON: | |
| return 0.0 | |
| z = safe_div(candle.candle_range - mean_r, std_r + EPSILON) | |
| return clamp(z * 0.3) | |
| def _detect_magnet(self, candle: Candle) -> bool: | |
| """Price approaching recent swing high or low = liquidity magnet.""" | |
| if not self._swing_highs or not self._swing_lows: | |
| return False | |
| price = candle.close | |
| mean_r = self._range_buf.mean() | |
| tolerance = max(mean_r * 1.5, EPSILON) | |
| near_high = any(abs(price - sh) < tolerance for sh in self._swing_highs) | |
| near_low = any(abs(price - sl) < tolerance for sl in self._swing_lows) | |
| return near_high or near_low | |
| def _compute_manip_proxy(self, candle: Candle, di_result: Dict, event_result: Dict) -> float: | |
| """Aggregate manipulation probability from multiple signals.""" | |
| score = 0.0 | |
| if di_result.get("synthetic_tick"): score += 0.3 | |
| if di_result.get("wick_anomaly"): score += 0.15 | |
| if di_result.get("spread_anomaly"): score += 0.15 | |
| if event_result.get("manipulation_flag"): score += 0.25 | |
| # Velocity divergence proxy: large range + tiny volume | |
| # AUDIT FIX (volume-proxy bug): same is_ready(3)-only gating issue as | |
| # _compute_pressure -- without the mean>EPSILON check, a permanently | |
| # volume-absent feed makes vol_factor=0.0 forever, so this check | |
| # degenerates into firing on any range spike regardless of volume. | |
| if self._vol_buf.is_ready(3) and self._vol_buf.mean() > EPSILON: | |
| mean_vol = self._vol_buf.mean() + EPSILON | |
| mean_r = self._range_buf.mean() + EPSILON | |
| vol_factor = safe_div(candle.volume, mean_vol) | |
| range_factor = safe_div(candle.candle_range, mean_r) | |
| if range_factor > 2.0 and vol_factor < 0.4: | |
| score += 0.2 | |
| return clamp(score) | |
| def _compute_pressure_chain(self) -> float: | |
| """Multi-candle directional pressure persistence (3-7 candles).""" | |
| if not self._direction_buf.is_ready(3): | |
| return 0.5 | |
| arr = self._direction_buf.as_array() | |
| mean_dir = float(arr.mean()) | |
| return clamp(0.5 + mean_dir * 0.5) | |
| def _compute_otc_fraud(self, di_result: Dict) -> float: | |
| """Synthetic OTC fraud score (Section 15, engine 13).""" | |
| if not self._smoothness_buf.is_ready(5): | |
| return 0.0 | |
| avg_smooth = self._smoothness_buf.mean() | |
| fraud = 0.0 | |
| fraud += clamp((avg_smooth - 0.5) * 1.5) # high body ratio → synthetic | |
| if di_result.get("synthetic_tick"): | |
| fraud += 0.3 | |
| if di_result.get("wick_anomaly"): | |
| fraud += 0.1 | |
| # Candle repetition: check if range is abnormally uniform | |
| if self._range_buf.is_ready(10): | |
| std_r = self._range_buf.std() | |
| mean_r = self._range_buf.mean() + EPSILON | |
| cv = safe_div(std_r, mean_r) # coefficient of variation | |
| if cv < 0.05: # suspiciously uniform candles | |
| fraud += 0.2 | |
| return clamp(fraud) | |
| def _update_swings(self, candle: Candle) -> None: | |
| """Track recent swing highs/lows for magnet detection.""" | |
| if not self._high_buf.is_ready(3): | |
| return | |
| highs = self._high_buf.as_array() | |
| lows = self._low_buf.as_array() | |
| # Previous candle is a swing high if its high > neighbors | |
| if len(highs) >= 3: | |
| if highs[-2] > highs[-3] and highs[-2] > highs[-1]: | |
| self._swing_highs.append(float(highs[-2])) | |
| if lows[-2] < lows[-3] and lows[-2] < lows[-1]: | |
| self._swing_lows.append(float(lows[-2])) | |
| def _safe_default() -> Dict: | |
| return { | |
| "pressure_score": 0.5, "liquidity_score": 0.5, | |
| "buy_pressure": 0.5, "sell_pressure": 0.5, | |
| "absorption_score": 0.0, "void_score": 0.0, | |
| "magnet_zone": False, "manipulation_proxy": 0.0, | |
| "pressure_chain": 0.5, "otc_fraud_score": 0.0, | |
| } | |
| # ============================================================================== | |
| # PART E TECHNICAL STACK | |
| # ============================================================================== | |
| import math | |
| from collections import deque | |
| from typing import Dict, List, Optional, Tuple | |
| # ============================================================================== | |
| # SHARED EMA HELPER | |
| # ============================================================================== | |
| class EMATracker: | |
| """ | |
| Incremental Exponential Moving Average. | |
| Seeded from SMA on first `period` values, then online from there. | |
| Thread-safe state in pure Python. Never raises. | |
| """ | |
| __slots__ = ("period", "_ema", "_count", "_alpha", "_seed_sum") | |
| def __init__(self, period: int) -> None: | |
| assert period > 0 | |
| self.period = period | |
| self._alpha = 2.0 / (period + 1.0) | |
| self._ema : float = 0.0 | |
| self._count : int = 0 | |
| self._seed_sum: float = 0.0 | |
| def update(self, value: float) -> Optional[float]: | |
| """Push a new value. Returns current EMA or None if not yet seeded.""" | |
| v = nan_safe(value) | |
| self._count += 1 | |
| if self._count <= self.period: | |
| self._seed_sum += v | |
| if self._count == self.period: | |
| self._ema = self._seed_sum / self.period | |
| return self._ema | |
| return None | |
| self._ema = self._alpha * v + (1.0 - self._alpha) * self._ema | |
| return self._ema | |
| def value(self) -> Optional[float]: | |
| return self._ema if self._count >= self.period else None | |
| def is_ready(self) -> bool: | |
| return self._count >= self.period | |
| # ============================================================================== | |
| # PART E1 — EMA 200 (Macro Trend Direction) | |
| # ============================================================================== | |
| class EMA200Engine: | |
| """ | |
| Determines macro trend direction via EMA 200. | |
| Output: | |
| { "ema200": float | None, "price_above_ema200": bool, | |
| "ema200_slope": float, "macro_bias": float } # bias [-1,1] | |
| """ | |
| def __init__(self) -> None: | |
| self._ema = EMATracker(EMA200_PERIOD) | |
| self._prev_ema: Optional[float] = None | |
| def update(self, candle: Candle) -> Dict: | |
| try: | |
| return self._update_inner(candle) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("EMA200Engine", exc) | |
| return {"ema200": None, "price_above_ema200": False, "ema200_slope": 0.0, "macro_bias": 0.0} | |
| def _update_inner(self, candle: Candle) -> Dict: | |
| if not candle.valid: | |
| return {"ema200": None, "price_above_ema200": False, "ema200_slope": 0.0, "macro_bias": 0.0} | |
| ema_val = self._ema.update(candle.close) | |
| if ema_val is None: | |
| return {"ema200": None, "price_above_ema200": False, "ema200_slope": 0.0, "macro_bias": 0.0} | |
| slope = 0.0 | |
| if self._prev_ema is not None and self._prev_ema > EPSILON: | |
| slope = safe_div(ema_val - self._prev_ema, self._prev_ema) | |
| self._prev_ema = ema_val | |
| above = candle.close > ema_val | |
| # Macro bias: stronger when price significantly above/below EMA | |
| price_dist = safe_div(candle.close - ema_val, ema_val + EPSILON) | |
| macro_bias = clamp(price_dist * 10.0, -1.0, 1.0) # normalize | |
| return { | |
| "ema200" : ema_val, | |
| "price_above_ema200": above, | |
| "ema200_slope" : clamp(slope * 100.0, -1.0, 1.0), | |
| "macro_bias" : macro_bias, | |
| } | |
| # ============================================================================== | |
| # PART E2 — RSI ADAPTIVE ENGINE | |
| # ============================================================================== | |
| class RSIEngine: | |
| """ | |
| Adaptive RSI (period 14 by default). | |
| Thresholds sourced from asset_profile_result dynamically. | |
| RSI is a CONFIRMATION FILTER only — not standalone signal. | |
| Output: | |
| { "rsi": float, "rsi_ob_flag": bool, "rsi_os_flag": bool, | |
| "rsi_divergence": float, "rsi_momentum": float, | |
| "rsi_confirmation": float } # [-1,1] contribution to scoring | |
| """ | |
| _DIV_WINDOW: int = 10 # FIX-10: lookback window for divergence detection | |
| def __init__(self, period: int = RSI_PERIOD) -> None: | |
| self.period = period | |
| self._gains : deque = deque(maxlen=period * 2) | |
| self._losses : deque = deque(maxlen=period * 2) | |
| self._avg_gain: float = 0.0 | |
| self._avg_loss: float = 0.0 | |
| self._prev_close: Optional[float] = None | |
| self._rsi_buf : RollingBuffer = RollingBuffer(20) | |
| self._count : int = 0 | |
| # FIX-10: price close buffer for RSI divergence computation | |
| self._price_div_buf: RollingBuffer = RollingBuffer(self._DIV_WINDOW + 2) | |
| def update(self, candle: Candle, asset_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, asset_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("RSIEngine", exc) | |
| return self._safe_default(asset_result) | |
| def _update_inner(self, candle: Candle, asset_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default(asset_result) | |
| close = candle.close | |
| rsi_ob = asset_result.get("rsi_overbought", RSI_OB_DEFAULT) | |
| rsi_os = asset_result.get("rsi_oversold", RSI_OS_DEFAULT) | |
| if self._prev_close is not None: | |
| delta = close - self._prev_close | |
| gain = max(0.0, delta) | |
| loss = max(0.0, -delta) | |
| else: | |
| gain = loss = 0.0 | |
| self._prev_close = close | |
| self._count += 1 | |
| self._gains.append(gain) | |
| self._losses.append(loss) | |
| if self._count < self.period: | |
| return self._safe_default(asset_result) | |
| # Smoothed RS | |
| if self._count == self.period: | |
| self._avg_gain = sum(list(self._gains)[-self.period:]) / self.period | |
| self._avg_loss = sum(list(self._losses)[-self.period:]) / self.period | |
| else: | |
| alpha = 1.0 / self.period | |
| self._avg_gain = alpha * gain + (1 - alpha) * self._avg_gain | |
| self._avg_loss = alpha * loss + (1 - alpha) * self._avg_loss | |
| if self._avg_loss < EPSILON: | |
| rsi = 100.0 | |
| else: | |
| rs = safe_div(self._avg_gain, self._avg_loss) | |
| rsi = 100.0 - safe_div(100.0, 1.0 + rs) | |
| rsi = clamp(rsi, 0.0, 100.0) | |
| self._rsi_buf.push(rsi) | |
| # FIX-10: Compute real RSI divergence (was always 0.0) | |
| self._price_div_buf.push(close) | |
| divergence = self._compute_divergence() | |
| # Momentum: slope of RSI | |
| rsi_momentum = 0.0 | |
| if self._rsi_buf.is_ready(3): | |
| arr = self._rsi_buf.as_array()[-3:] | |
| rsi_momentum = clamp(safe_div(float(arr[-1]) - float(arr[0]), 50.0), -1.0, 1.0) | |
| # Divergence: see _compute_divergence() — populated above | |
| ob_flag = rsi >= rsi_ob | |
| os_flag = rsi <= rsi_os | |
| # RSI confirmation contribution [-1, 1] | |
| # OS zone → bullish confirmation; OB zone → bearish confirmation | |
| if os_flag: | |
| rsi_confirm = clamp((rsi_os - rsi) / (rsi_os + EPSILON) * 2.0) | |
| elif ob_flag: | |
| rsi_confirm = clamp(-(rsi - rsi_ob) / (100.0 - rsi_ob + EPSILON) * 2.0, -1.0, 0.0) | |
| else: | |
| # Neutral: contribution based on distance from 50 | |
| rsi_confirm = clamp((rsi - 50.0) / 50.0, -1.0, 1.0) * 0.3 | |
| return { | |
| "rsi" : rsi, | |
| "rsi_ob_flag" : ob_flag, | |
| "rsi_os_flag" : os_flag, | |
| "rsi_divergence" : divergence, | |
| "rsi_momentum" : rsi_momentum, | |
| "rsi_confirmation" : rsi_confirm, | |
| } | |
| def _compute_divergence(self) -> float: | |
| """ | |
| FIX-10: RSI divergence — compare price direction to RSI direction | |
| over the last _DIV_WINDOW candles. | |
| Bearish divergence (returns negative): price forms a higher high while | |
| RSI forms a lower high → momentum fading despite price rising. | |
| Bullish divergence (returns positive): price forms a lower low while | |
| RSI forms a higher low → downside momentum fading. | |
| Returns [-1, 1]. 0.0 when insufficient history or no divergence. | |
| """ | |
| n = self._DIV_WINDOW | |
| if not self._price_div_buf.is_ready(n) or not self._rsi_buf.is_ready(n): | |
| return 0.0 | |
| prices = self._price_div_buf.as_array()[-n:] | |
| rsi_vals = self._rsi_buf.as_array()[-n:] | |
| if len(prices) < n or len(rsi_vals) < n: | |
| return 0.0 | |
| half = n // 2 | |
| p_early_max = float(prices[:half].max()) | |
| p_late_max = float(prices[half:].max()) | |
| p_early_min = float(prices[:half].min()) | |
| p_late_min = float(prices[half:].min()) | |
| r_early_max = float(rsi_vals[:half].max()) | |
| r_late_max = float(rsi_vals[half:].max()) | |
| r_early_min = float(rsi_vals[:half].min()) | |
| r_late_min = float(rsi_vals[half:].min()) | |
| # Bearish divergence: price higher high + RSI lower high (≥2 RSI points) | |
| if p_late_max > p_early_max and r_late_max < r_early_max - 2.0: | |
| magnitude = safe_div(r_early_max - r_late_max, 50.0) | |
| return -clamp(magnitude) | |
| # Bullish divergence: price lower low + RSI higher low (≥2 RSI points) | |
| if p_late_min < p_early_min and r_late_min > r_early_min + 2.0: | |
| magnitude = safe_div(r_late_min - r_early_min, 50.0) | |
| return clamp(magnitude) | |
| return 0.0 | |
| def _safe_default(self, asset_result: Dict) -> Dict: | |
| return { | |
| "rsi": 50.0, "rsi_ob_flag": False, "rsi_os_flag": False, | |
| "rsi_divergence": 0.0, "rsi_momentum": 0.0, "rsi_confirmation": 0.0, | |
| } | |
| # ============================================================================== | |
| # PART E3 — MACD ADAPTIVE MOMENTUM ENGINE | |
| # ============================================================================== | |
| class MACDEngine: | |
| """ | |
| MACD (12/26/9) — detects momentum acceleration/exhaustion. | |
| Suppresses small/weak crossovers (false crossover suppression). | |
| Output: | |
| { "macd": float, "signal": float, "histogram": float, | |
| "hist_slope": float, "momentum_quality": float, | |
| "macd_confirmation": float } # [-1,1] | |
| """ | |
| def __init__(self) -> None: | |
| self._ema_fast = EMATracker(MACD_FAST) | |
| self._ema_slow = EMATracker(MACD_SLOW) | |
| self._ema_signal = EMATracker(MACD_SIGNAL) | |
| self._hist_buf : RollingBuffer = RollingBuffer(10) | |
| def update(self, candle: Candle) -> Dict: | |
| try: | |
| return self._update_inner(candle) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("MACDEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| fast_val = self._ema_fast.update(candle.close) | |
| slow_val = self._ema_slow.update(candle.close) | |
| if fast_val is None or slow_val is None: | |
| return self._safe_default() | |
| macd_line = fast_val - slow_val | |
| sig_val = self._ema_signal.update(macd_line) | |
| if sig_val is None: | |
| return self._safe_default() | |
| histogram = macd_line - sig_val | |
| self._hist_buf.push(histogram) | |
| # Histogram slope | |
| hist_slope = 0.0 | |
| if self._hist_buf.is_ready(3): | |
| arr = self._hist_buf.as_array()[-3:] | |
| hist_slope = clamp(float(arr[-1]) - float(arr[0]), -1.0, 1.0) | |
| # Momentum quality: slope magnitude + histogram magnitude | |
| hist_mag = abs(histogram) | |
| slope_mag = abs(hist_slope) | |
| momentum_quality = clamp(hist_mag * 5.0 + slope_mag * 2.0) | |
| # Confirmation: direction + magnitude, suppressing small crossovers | |
| mean_hist_mag = self._hist_buf.mean() | |
| if abs(histogram) < mean_hist_mag * 0.3: | |
| # Small crossover — suppress | |
| macd_confirm = clamp(histogram * 2.0, -0.2, 0.2) | |
| else: | |
| macd_confirm = clamp(histogram * 5.0 + hist_slope * 2.0, -1.0, 1.0) | |
| return { | |
| "macd" : macd_line, | |
| "signal" : sig_val, | |
| "histogram" : histogram, | |
| "hist_slope" : hist_slope, | |
| "momentum_quality" : momentum_quality, | |
| "macd_confirmation" : macd_confirm, | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "macd": 0.0, "signal": 0.0, "histogram": 0.0, | |
| "hist_slope": 0.0, "momentum_quality": 0.5, "macd_confirmation": 0.0, | |
| } | |
| # ============================================================================== | |
| # PART E4 — BOLLINGER VOLATILITY ENGINE | |
| # ============================================================================== | |
| class BollingerEngine: | |
| """ | |
| Bollinger Bands (20, 2). Identifies squeeze, band touch, expansion. | |
| NOT a standalone signal. Feeds adaptive scoring engine. | |
| Output: | |
| { "upper": float, "lower": float, "middle": float, | |
| "bandwidth": float, "bandwidth_z": float, | |
| "squeeze_flag": bool, "upper_touch": bool, "lower_touch": bool, | |
| "expansion_flag": bool, "bb_confirmation": float } | |
| """ | |
| _BW_WINDOW = 40 # bandwidth history for z-score | |
| def __init__(self) -> None: | |
| self._close_buf : RollingBuffer = RollingBuffer(BB_PERIOD * 3) | |
| self._bw_buf : RollingBuffer = RollingBuffer(self._BW_WINDOW) | |
| def update(self, candle: Candle) -> Dict: | |
| try: | |
| return self._update_inner(candle) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("BollingerEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._close_buf.push(candle.close) | |
| if not self._close_buf.is_ready(BB_PERIOD): | |
| return self._safe_default() | |
| arr = self._close_buf.as_array()[-BB_PERIOD:] | |
| middle = float(arr.mean()) | |
| std = float(arr.std()) | |
| upper = middle + BB_STD_DEV * std | |
| lower = middle - BB_STD_DEV * std | |
| bw = safe_div(upper - lower, middle + EPSILON) | |
| self._bw_buf.push(bw) | |
| # Bandwidth z-score for squeeze/expansion detection | |
| bw_z = 0.0 | |
| squeeze = False | |
| expansion = False | |
| if self._bw_buf.is_ready(10): | |
| bw_mean = self._bw_buf.mean() | |
| bw_std = self._bw_buf.std() | |
| bw_z = safe_div(bw - bw_mean, bw_std + EPSILON) | |
| squeeze = bw_z < -1.0 | |
| expansion = bw_z > 1.5 | |
| # Touch detection (with tolerance) | |
| price = candle.close | |
| tol = std * 0.1 | |
| upper_touch = price >= upper - tol | |
| lower_touch = price <= lower + tol | |
| # Confirmation: sell zone at upper touch, buy zone at lower touch | |
| bb_confirm = 0.0 | |
| if lower_touch: | |
| bb_confirm = clamp((lower - price + tol) / (std + EPSILON)) # bullish contribution | |
| elif upper_touch: | |
| bb_confirm = clamp(-(price - upper + tol) / (std + EPSILON)) # bearish | |
| if squeeze: | |
| bb_confirm *= 0.5 # squeeze doesn't strongly confirm direction | |
| return { | |
| "upper" : upper, | |
| "lower" : lower, | |
| "middle" : middle, | |
| "bandwidth" : bw, | |
| "bandwidth_z" : bw_z, | |
| "squeeze_flag" : squeeze, | |
| "upper_touch" : upper_touch, | |
| "lower_touch" : lower_touch, | |
| "expansion_flag": expansion, | |
| "bb_confirmation": clamp(bb_confirm, -1.0, 1.0), | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "upper": 0.0, "lower": 0.0, "middle": 0.0, | |
| "bandwidth": 0.0, "bandwidth_z": 0.0, | |
| "squeeze_flag": False, "upper_touch": False, "lower_touch": False, | |
| "expansion_flag": False, "bb_confirmation": 0.0, | |
| } | |
| # ============================================================================== | |
| # PART E5 — ADVANCED SUPPORT + RESISTANCE ENGINE | |
| # ============================================================================== | |
| class SupportResistanceEngine: | |
| """ | |
| Adaptive S/R via swing highs/lows, round numbers, session levels. | |
| Level strength = touch count + reaction strength + recency. | |
| Levels decay using confidence decay logic. | |
| Output: | |
| { "nearest_support": float, "nearest_resistance": float, | |
| "at_support": bool, "at_resistance": bool, | |
| "level_strength": float, # [0,1] proximity to strong level | |
| "stop_hunt_probability": float, | |
| "sr_confirmation": float } # [-1,1] | |
| """ | |
| _MAX_LEVELS = 20 | |
| _LEVEL_DECAY = 0.02 # per tick decay | |
| def __init__(self) -> None: | |
| self._close_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._high_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._low_buf : RollingBuffer = RollingBuffer(DEFAULT_BUFFER_SIZE) | |
| self._range_buf : RollingBuffer = RollingBuffer(50) | |
| # Level registry: {price: {"strength": float, "type": "support"|"resistance", "age": int}} | |
| self._levels : Dict[float, Dict] = {} | |
| self._tick : int = 0 | |
| def update(self, candle: Candle, session_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, session_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("SupportResistanceEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, session_result: Dict) -> Dict: | |
| if not candle.valid: | |
| return self._safe_default() | |
| self._tick += 1 | |
| self._close_buf.push(candle.close) | |
| self._high_buf.push(candle.high) | |
| self._low_buf.push(candle.low) | |
| self._range_buf.push(candle.candle_range) | |
| # Update levels from swing detection | |
| self._update_levels_from_swings() | |
| # Add round number levels | |
| self._add_round_numbers(candle.close) | |
| # Decay all levels | |
| self._decay_levels() | |
| # Prune weak levels | |
| self._prune_levels() | |
| if not self._levels: | |
| return self._safe_default() | |
| price = candle.close | |
| mean_range = self._range_buf.mean() if self._range_buf.is_ready(5) else candle.candle_range | |
| tolerance = max(mean_range * 1.0, EPSILON) | |
| supports = [(p, v) for p, v in self._levels.items() if v["type"] == "support" and p < price] | |
| resistances = [(p, v) for p, v in self._levels.items() if v["type"] == "resistance" and p > price] | |
| nearest_sup = max(supports, key=lambda x: x[0])[0] if supports else price * 0.99 | |
| nearest_res = min(resistances, key=lambda x: x[0])[0] if resistances else price * 1.01 | |
| at_support = abs(price - nearest_sup) < tolerance | |
| at_resistance = abs(price - nearest_res) < tolerance | |
| # Level strength: proximity-weighted strength of nearest levels | |
| level_strength = 0.0 | |
| for p, v in self._levels.items(): | |
| dist = abs(price - p) | |
| proximity = clamp(1.0 - safe_div(dist, tolerance * 3.0)) | |
| level_strength = max(level_strength, proximity * v["strength"]) | |
| # Stop hunt: price at level + spike behavior | |
| stop_hunt_prob = clamp(level_strength * 0.5 * (1.0 if at_support or at_resistance else 0.3)) | |
| # SR confirmation: buy near support, sell near resistance | |
| sr_confirm = 0.0 | |
| if at_support: | |
| sr_confirm = clamp(level_strength) | |
| elif at_resistance: | |
| sr_confirm = clamp(-level_strength) | |
| return { | |
| "nearest_support" : nearest_sup, | |
| "nearest_resistance" : nearest_res, | |
| "at_support" : at_support, | |
| "at_resistance" : at_resistance, | |
| "level_strength" : level_strength, | |
| "stop_hunt_probability": stop_hunt_prob, | |
| "sr_confirmation" : sr_confirm, | |
| } | |
| def _update_levels_from_swings(self) -> None: | |
| if not self._high_buf.is_ready(3): | |
| return | |
| highs = self._high_buf.as_array() | |
| lows = self._low_buf.as_array() | |
| if len(highs) < 3: | |
| return | |
| # Swing high | |
| if highs[-2] > highs[-3] and highs[-2] > highs[-1]: | |
| self._register_level(float(highs[-2]), "resistance") | |
| # Swing low | |
| if lows[-2] < lows[-3] and lows[-2] < lows[-1]: | |
| self._register_level(float(lows[-2]), "support") | |
| def _add_round_numbers(self, price: float) -> None: | |
| """Add proximity-based round number levels.""" | |
| # Snap to round numbers within 2% of current price | |
| if price < EPSILON: | |
| return | |
| magnitude = 10 ** (math.floor(math.log10(price)) - 1) | |
| rounded = round(price / magnitude) * magnitude | |
| if abs(rounded - price) < price * 0.02: | |
| self._register_level(rounded, "resistance" if rounded > price else "support") | |
| def _register_level(self, price: float, level_type: str) -> None: | |
| """Register or reinforce a level.""" | |
| if len(self._levels) >= self._MAX_LEVELS and price not in self._levels: | |
| # Evict weakest | |
| weakest = min(self._levels, key=lambda p: self._levels[p]["strength"]) | |
| del self._levels[weakest] | |
| if price in self._levels: | |
| self._levels[price]["strength"] = min(1.0, self._levels[price]["strength"] + 0.1) | |
| self._levels[price]["age"] = self._tick | |
| else: | |
| self._levels[price] = {"strength": 0.3, "type": level_type, "age": self._tick} | |
| def _decay_levels(self) -> None: | |
| for p in self._levels: | |
| self._levels[p]["strength"] = max(0.0, | |
| self._levels[p]["strength"] - self._LEVEL_DECAY) | |
| def _prune_levels(self) -> None: | |
| remove = [p for p, v in self._levels.items() if v["strength"] < 0.01] | |
| for p in remove: | |
| del self._levels[p] | |
| def _safe_default() -> Dict: | |
| return { | |
| "nearest_support": 0.0, "nearest_resistance": 0.0, | |
| "at_support": False, "at_resistance": False, | |
| "level_strength": 0.0, "stop_hunt_probability": 0.0, | |
| "sr_confirmation": 0.0, | |
| } | |
| # ============================================================================== | |
| # TECHNICAL CONFIRMATION STACK — AGGREGATOR | |
| # ============================================================================== | |
| class TechnicalConfirmationStack: | |
| """ | |
| Coordinates EMA200, RSI, MACD, Bollinger, S&R. | |
| Applies conflict resolution (Section 14). | |
| Minimum 2-layer alignment before signal. 4-layer = max confidence. | |
| Input contract: | |
| update(candle, asset_result, session_result) → dict | |
| Output contract: | |
| { | |
| "ema200_result": dict, | |
| "rsi_result": dict, | |
| "macd_result": dict, | |
| "bb_result": dict, | |
| "sr_result": dict, | |
| "layers_aligned": int, # 0–4 layers agreeing | |
| "tech_confidence": float, # [0,1] | |
| "tech_direction": float, # [-1,1] | |
| "conflict_flag": bool, | |
| } | |
| """ | |
| def __init__(self) -> None: | |
| self._ema200 = EMA200Engine() | |
| self._rsi = RSIEngine() | |
| self._macd = MACDEngine() | |
| self._bb = BollingerEngine() | |
| self._sr = SupportResistanceEngine() | |
| def update(self, candle: Candle, asset_result: Dict, session_result: Dict) -> Dict: | |
| try: | |
| return self._update_inner(candle, asset_result, session_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("TechnicalConfirmationStack", exc) | |
| return self._safe_default() | |
| def _update_inner(self, candle: Candle, asset_result: Dict, session_result: Dict) -> Dict: | |
| ema_r = self._ema200.update(candle) | |
| rsi_r = self._rsi.update(candle, asset_result) | |
| macd_r = self._macd.update(candle) | |
| bb_r = self._bb.update(candle) | |
| sr_r = self._sr.update(candle, session_result) | |
| # Gather directional signals per layer | |
| signals: List[float] = [] | |
| # EMA200 | |
| if ema_r.get("ema200") is not None: | |
| signals.append(1.0 if ema_r["price_above_ema200"] else -1.0) | |
| # RSI | |
| rsi_conf = rsi_r.get("rsi_confirmation", 0.0) | |
| if abs(rsi_conf) > 0.1: | |
| signals.append(math.copysign(1.0, rsi_conf)) | |
| # MACD | |
| macd_conf = macd_r.get("macd_confirmation", 0.0) | |
| if abs(macd_conf) > 0.1: | |
| signals.append(math.copysign(1.0, macd_conf)) | |
| # Bollinger | |
| bb_conf = bb_r.get("bb_confirmation", 0.0) | |
| if abs(bb_conf) > 0.1: | |
| signals.append(math.copysign(1.0, bb_conf)) | |
| # S&R | |
| sr_conf = sr_r.get("sr_confirmation", 0.0) | |
| if abs(sr_conf) > 0.1: | |
| signals.append(math.copysign(1.0, sr_conf)) | |
| if not signals: | |
| return self._compose(ema_r, rsi_r, macd_r, bb_r, sr_r, 0, 0.3, 0.0, False) | |
| # Count aligned layers | |
| bull_count = sum(1 for s in signals if s > 0) | |
| bear_count = sum(1 for s in signals if s < 0) | |
| aligned_count = max(bull_count, bear_count) | |
| # Conflict resolution (Section 14): | |
| # EMA bearish + RSI oversold → caution, wait for MACD | |
| ema_bear = ema_r.get("ema200") is not None and not ema_r.get("price_above_ema200", True) | |
| rsi_os = rsi_r.get("rsi_os_flag", False) | |
| if ema_bear and rsi_os: | |
| aligned_count = max(0, aligned_count - 1) | |
| conflict = bull_count > 0 and bear_count > 0 | |
| # Confidence: 2 layers min needed; scales to 4 | |
| tech_conf = clamp(safe_div(aligned_count - 1, 3)) # 1→0, 2→0.33, 3→0.67, 4→1.0 | |
| # Direction: weighted average of confirmations | |
| all_confs = [ | |
| rsi_r.get("rsi_confirmation", 0.0), | |
| macd_r.get("macd_confirmation", 0.0), | |
| bb_r.get("bb_confirmation", 0.0), | |
| sr_r.get("sr_confirmation", 0.0), | |
| ] | |
| if ema_r.get("ema200") is not None: | |
| ema_bias = ema_r.get("macro_bias", 0.0) | |
| all_confs.append(ema_bias * 0.5) | |
| tech_dir = clamp(sum(all_confs) / (len(all_confs) + EPSILON), -1.0, 1.0) | |
| # Manipulation override: if manipulation detected, suppress confirmation | |
| # (handled in scoring engine — passed through here) | |
| return self._compose(ema_r, rsi_r, macd_r, bb_r, sr_r, | |
| aligned_count, tech_conf, tech_dir, conflict) | |
| def _compose(ema_r, rsi_r, macd_r, bb_r, sr_r, | |
| aligned, conf, direction, conflict) -> Dict: | |
| return { | |
| "ema200_result" : ema_r, | |
| "rsi_result" : rsi_r, | |
| "macd_result" : macd_r, | |
| "bb_result" : bb_r, | |
| "sr_result" : sr_r, | |
| "layers_aligned" : aligned, | |
| "tech_confidence": conf, | |
| "tech_direction" : direction, | |
| "conflict_flag" : conflict, | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "ema200_result": {"ema200": None, "price_above_ema200": False, "ema200_slope": 0.0, "macro_bias": 0.0}, | |
| "rsi_result": {"rsi": 50.0, "rsi_ob_flag": False, "rsi_os_flag": False, | |
| "rsi_divergence": 0.0, "rsi_momentum": 0.0, "rsi_confirmation": 0.0}, | |
| "macd_result": {"macd": 0.0, "signal": 0.0, "histogram": 0.0, | |
| "hist_slope": 0.0, "momentum_quality": 0.5, "macd_confirmation": 0.0}, | |
| "bb_result": {"upper": 0.0, "lower": 0.0, "middle": 0.0, "bandwidth": 0.0, | |
| "bandwidth_z": 0.0, "squeeze_flag": False, "upper_touch": False, | |
| "lower_touch": False, "expansion_flag": False, "bb_confirmation": 0.0}, | |
| "sr_result": {"nearest_support": 0.0, "nearest_resistance": 0.0, | |
| "at_support": False, "at_resistance": False, | |
| "level_strength": 0.0, "stop_hunt_probability": 0.0, "sr_confirmation": 0.0}, | |
| "layers_aligned": 0, "tech_confidence": 0.3, | |
| "tech_direction": 0.0, "conflict_flag": False, | |
| } | |
| # ============================================================================== | |
| # PART F SCORING LIFECYCLE | |
| # ============================================================================== | |
| import math | |
| from typing import Dict, List, Optional, Tuple | |
| # ============================================================================== | |
| # PART F1 — ADAPTIVE SCORING ENGINE | |
| # ============================================================================== | |
| class AdaptiveScoringEngine: | |
| """ | |
| Computes the master confidence score and execution suitability | |
| from all upstream engine results. | |
| Weights adapt every tick based on market state, volatility, and | |
| manipulation risk — no static formula (Section 16). | |
| Input contract: | |
| score(di, ms, asset, session, tf, event, lp, tech, tick_count) → dict | |
| Output contract: | |
| { | |
| "confidence": float, | |
| "internal_trust": float, | |
| "execution_suitability": str, | |
| "momentum_confidence": float, | |
| "liquidity_confidence": float, | |
| "structure_confidence": float, | |
| "volatility_confidence": float, | |
| "manipulation_penalty": float, | |
| "noise_penalty": float, | |
| "spread_penalty": float, | |
| "latency_penalty": float, | |
| "regime_confidence": float, | |
| "continuation_probability":float, | |
| "reversal_probability": float, | |
| "tech_alignment_bonus": float, | |
| "raw_direction": float, # [-1,1] | |
| "warm_up_fraction": float, # [0,1] | |
| } | |
| """ | |
| def score(self, | |
| di_result : Dict, | |
| ms_result : Dict, | |
| asset_result : Dict, | |
| session_result: Dict, | |
| tf_result : Dict, | |
| event_result : Dict, | |
| lp_result : Dict, | |
| tech_result : Dict, | |
| tick_count : int) -> Dict: | |
| try: | |
| return self._score_inner( | |
| di_result, ms_result, asset_result, session_result, | |
| tf_result, event_result, lp_result, tech_result, tick_count) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("AdaptiveScoringEngine", exc) | |
| return self._safe_default() | |
| def _score_inner(self, di, ms, asset, session, tf, event, lp, tech, tick_count) -> Dict: | |
| # ---------------------------------------------------------------- | |
| # 1. Warm-up fraction — confidence gated during cold start | |
| # ---------------------------------------------------------------- | |
| warm_frac = clamp(safe_div(tick_count - MIN_WARM_CANDLES, | |
| FULL_WARM_CANDLES - MIN_WARM_CANDLES)) | |
| # ---------------------------------------------------------------- | |
| # 2. Penalty signals (from data quality) | |
| # ---------------------------------------------------------------- | |
| data_quality = di.get("data_quality", 1.0) | |
| instability = di.get("instability_level", 0.0) | |
| noise_penalty = clamp(instability * 0.5 + float(di.get("noise_burst", False)) * 0.2) | |
| spread_penalty = clamp( | |
| float(di.get("spread_anomaly", False)) * 0.3 + | |
| float(di.get("spread_expansion", False)) * 0.2 | |
| ) | |
| latency_penalty = clamp( | |
| float(di.get("latency_spike", False)) * 0.15 + | |
| float(di.get("latency_drift", False)) * 0.1 | |
| ) | |
| # ---------------------------------------------------------------- | |
| # 3. Manipulation penalty | |
| # ---------------------------------------------------------------- | |
| manip_proxy = lp.get("manipulation_proxy", 0.0) | |
| event_manip = float(event.get("manipulation_flag", False)) * 0.3 | |
| # FIX-7: OTC fraud score fires falsely on low-volatility forex whose high | |
| # body-ratio candles look "synthetic" by the smoothness heuristic. | |
| # Dampen by 0.6 for every asset class that is not crypto. | |
| otc_fraud_raw = lp.get("otc_fraud_score", 0.0) | |
| if asset.get("asset_mode", "unknown") != "crypto": | |
| otc_fraud_raw *= 0.6 | |
| otc_fraud = otc_fraud_raw * 0.5 | |
| manip_penalty = clamp(manip_proxy * 0.5 + event_manip + otc_fraud) | |
| # ---------------------------------------------------------------- | |
| # 4. Component confidence scores | |
| # ---------------------------------------------------------------- | |
| momentum_conf = clamp(tech.get("macd_result", {}).get("momentum_quality", 0.5) * 0.6 | |
| + abs(tf.get("lower_bias", 0.0)) * 0.4) | |
| liquidity_conf = lp.get("liquidity_score", 0.5) | |
| structure_conf = clamp( | |
| tf.get("tf_alignment", 0.5) * 0.5 + | |
| ms.get("continuation_prob", 0.5) * 0.3 + | |
| tech.get("sr_result", {}).get("level_strength", 0.0) * 0.2 | |
| ) | |
| vol_score = asset.get("volatility_profile", "medium") | |
| volatility_conf = {"low": 0.6, "medium": 0.8, "high": 0.5, "extreme": 0.2}.get(vol_score, 0.6) | |
| regime_conf = clamp( | |
| (1.0 - ms.get("exhaustion_prob", 0.5)) * 0.5 + | |
| ms.get("continuation_prob", 0.5) * 0.5 | |
| ) | |
| continuation_p = clamp( | |
| ms.get("continuation_prob", 0.5) * 0.4 + | |
| tf.get("trend_persistence", 0.5) * 0.3 + | |
| lp.get("pressure_chain", 0.5) * 0.3 | |
| ) | |
| reversal_p = clamp( | |
| ms.get("exhaustion_prob", 0.5) * 0.4 + | |
| tf.get("reversal_probability", 0.3) * 0.4 + | |
| event.get("direction_influence", 0.0) * -0.2 | |
| ) | |
| tech_align_bonus = clamp(safe_div(tech.get("layers_aligned", 0) - 1, 3) * 0.3) | |
| # ---------------------------------------------------------------- | |
| # 5. Adaptive weights — recalibrate from market state | |
| # ---------------------------------------------------------------- | |
| market_state = ms.get("market_state", "undefined") | |
| # In trend: weight structure + momentum more | |
| # In range: weight S/R + liquidity more | |
| # In manipulation/sweep: heavily penalize | |
| if market_state in ("trend", "continuation"): | |
| w_momentum = 0.30 | |
| w_liquidity = 0.15 | |
| w_structure = 0.30 | |
| w_regime = 0.25 | |
| elif market_state in ("range", "compression"): | |
| w_momentum = 0.15 | |
| w_liquidity = 0.25 | |
| w_structure = 0.35 | |
| w_regime = 0.25 | |
| elif market_state in ("manipulation", "sweep", "unstable", "noisy"): | |
| w_momentum = 0.10 | |
| w_liquidity = 0.10 | |
| w_structure = 0.10 | |
| w_regime = 0.10 | |
| manip_penalty = clamp(manip_penalty + 0.4) | |
| elif market_state == "reversal": | |
| w_momentum = 0.25 | |
| w_liquidity = 0.20 | |
| w_structure = 0.25 | |
| w_regime = 0.30 | |
| else: | |
| w_momentum = 0.25 | |
| w_liquidity = 0.20 | |
| w_structure = 0.25 | |
| w_regime = 0.30 | |
| # Base confidence (before penalties) | |
| base_conf = weighted_mean( | |
| [momentum_conf, liquidity_conf, structure_conf, regime_conf], | |
| [w_momentum, w_liquidity, w_structure, w_regime] | |
| ) | |
| # Apply penalties | |
| total_penalty = clamp( | |
| manip_penalty * 0.4 + | |
| noise_penalty * 0.25 + | |
| spread_penalty * 0.2 + | |
| latency_penalty * 0.15 | |
| ) | |
| base_conf = clamp(base_conf * (1.0 - total_penalty)) | |
| # Technical alignment bonus | |
| base_conf = clamp(base_conf + tech_align_bonus * 0.15) | |
| # Session timing penalty | |
| timing_penalty = session.get("timing_penalty", 0.0) | |
| base_conf = clamp(base_conf * (1.0 - timing_penalty * 0.3)) | |
| # Timeframe conflict penalty | |
| tf_conflict = tf.get("tf_conflict_score", 0.0) | |
| base_conf = clamp(base_conf * (1.0 - tf_conflict * 0.25)) | |
| # Gate by warm-up | |
| base_conf = clamp(base_conf * warm_frac) | |
| # ---------------------------------------------------------------- | |
| # 6. Internal trust (meta-confidence) | |
| # ---------------------------------------------------------------- | |
| internal_trust = clamp(weighted_mean( | |
| [data_quality, 1.0 - instability, 1.0 - manip_penalty, | |
| volatility_conf, 1.0 - tf_conflict], | |
| [0.30, 0.20, 0.20, 0.15, 0.15] | |
| ) * warm_frac) | |
| # ---------------------------------------------------------------- | |
| # 7. Execution suitability | |
| # ---------------------------------------------------------------- | |
| exec_suit = self._classify_exec_suitability(base_conf, di, ms, event) | |
| # ---------------------------------------------------------------- | |
| # 8. Raw direction [-1,1] | |
| # ---------------------------------------------------------------- | |
| raw_dir = self._compute_raw_direction(tf, lp, tech, event, ms) | |
| return { | |
| "confidence" : base_conf, | |
| "internal_trust" : internal_trust, | |
| "execution_suitability" : exec_suit, | |
| "momentum_confidence" : momentum_conf, | |
| "liquidity_confidence" : liquidity_conf, | |
| "structure_confidence" : structure_conf, | |
| "volatility_confidence" : volatility_conf, | |
| "manipulation_penalty" : manip_penalty, | |
| "noise_penalty" : noise_penalty, | |
| "spread_penalty" : spread_penalty, | |
| "latency_penalty" : latency_penalty, | |
| "regime_confidence" : regime_conf, | |
| "continuation_probability": continuation_p, | |
| "reversal_probability" : reversal_p, | |
| "tech_alignment_bonus" : tech_align_bonus, | |
| "raw_direction" : raw_dir, | |
| "warm_up_fraction" : warm_frac, | |
| } | |
| def _classify_exec_suitability(self, confidence: float, | |
| di: Dict, ms: Dict, event: Dict) -> str: | |
| # Blocked conditions override confidence | |
| if di.get("suppress_signal", False): | |
| return "blocked" | |
| if ms.get("market_state") in ("manipulation", "unstable"): | |
| return "blocked" if confidence < CONF_WEAK_MAX else "weak" | |
| if event.get("manipulation_flag", False): | |
| return "blocked" if confidence < CONF_WEAK_MAX else "weak" | |
| if confidence <= CONF_BLOCKED_MAX: | |
| return "blocked" | |
| elif confidence <= CONF_WEAK_MAX: | |
| return "weak" | |
| elif confidence <= CONF_MODERATE_MAX: | |
| return "moderate" | |
| else: | |
| return "strong" | |
| def _compute_raw_direction(self, tf: Dict, lp: Dict, | |
| tech: Dict, event: Dict, ms: Dict) -> float: | |
| """Aggregate direction signal from multiple engines [-1, 1].""" | |
| signals = [ | |
| (tf.get("lower_bias", 0.0), 0.20), | |
| (tf.get("mid_bias", 0.0), 0.15), | |
| (tf.get("higher_bias", 0.0), 0.15), | |
| (lp.get("pressure_chain", 0.5) * 2.0 - 1.0, 0.15), | |
| (tech.get("tech_direction", 0.0), 0.20), | |
| (event.get("direction_influence", 0.0), 0.10), | |
| (ms.get("trend_direction", 0.0), 0.05), | |
| ] | |
| total_w = sum(w for _, w in signals) | |
| if total_w < EPSILON: | |
| return 0.0 | |
| raw = sum(v * w for v, w in signals) / total_w | |
| return clamp(raw, -1.0, 1.0) | |
| def _safe_default() -> Dict: | |
| return { | |
| "confidence": 0.0, "internal_trust": 0.0, | |
| "execution_suitability": "blocked", | |
| "momentum_confidence": 0.5, "liquidity_confidence": 0.5, | |
| "structure_confidence": 0.5, "volatility_confidence": 0.5, | |
| "manipulation_penalty": 0.0, "noise_penalty": 0.0, | |
| "spread_penalty": 0.0, "latency_penalty": 0.0, | |
| "regime_confidence": 0.5, "continuation_probability": 0.5, | |
| "reversal_probability": 0.5, "tech_alignment_bonus": 0.0, | |
| "raw_direction": 0.0, "warm_up_fraction": 0.0, | |
| } | |
| # ============================================================================== | |
| # PART F2 — SIGNAL LIFECYCLE ENGINE | |
| # ============================================================================== | |
| class SignalLifecycleEngine: | |
| """ | |
| Manages signal state machine: idle → forming → candidate → | |
| confirmed → cooling → expired / invalidated. | |
| TIMING-FIX-2: All hardcoded tick thresholds replaced with | |
| timeframe-adaptive properties. Stale-signal lifetime now | |
| scales with detected candle interval so 30 s / 1 m / 5 m | |
| signals persist ~15 min wall-clock instead of disappearing | |
| after a fixed 10 ticks. | |
| Implements: | |
| - Confidence hysteresis (no flip without strong evidence). | |
| - Signal cooldown / spam control. | |
| - Signal fatigue, stability, and flip-flop suppression. | |
| - Stale signal decay (timeframe-adaptive). | |
| - Expiry bucket assignment. | |
| Input contract: | |
| update(score_result, event_result, di_result, ms_result, tick_count, tf_seconds) → dict | |
| Output contract: | |
| { | |
| "direction": str, # "BUY" | "SELL" | |
| "lifecycle_state": str, | |
| "signal_freshness": float, | |
| "stale_signal_flag":bool, | |
| "expiry_bucket": str, | |
| "confidence_final": float, # after hysteresis | |
| "cooling_flag": bool, | |
| "reason_codes": list, | |
| } | |
| """ | |
| # ------------------------------------------------------------------ | |
| # Timing constants — replaced hardcoded values with defaults used | |
| # only when tf_seconds is unknown (should never happen in normal | |
| # operation because MAYTHOS always passes it). | |
| # ------------------------------------------------------------------ | |
| _DFL_STALE_TICKS = 25 # was 10 → ~25 min @ 1 m | |
| _DFL_COOLING_TICKS = 8 # was 5 → ~8 min @ 1 m | |
| _DFL_FLIP_SUPPRESS = 5 # was 3 | |
| _FORMING_THRESH = 0.10 | |
| _CANDIDATE_THRESH = 0.25 | |
| _CONFIRMED_THRESH = 0.45 | |
| _HYSTERESIS_DELTA = 0.05 # min confidence change to flip direction | |
| def __init__(self) -> None: | |
| self._state : str = "idle" | |
| self._direction : str = "BUY" | |
| self._last_dir : str = "BUY" | |
| self._confidence : float = 0.0 | |
| self._tick_in_state : int = 0 | |
| self._tick_total : int = 0 | |
| self._cooling_tick : int = 0 | |
| self._last_confirmed_tick: int = -999 | |
| self._flip_count : int = 0 | |
| self._last_flip_tick : int = -999 | |
| # TIMING-FIX-2a: hold the detected candle interval (seconds) | |
| # so _stale_ticks and _max_cooling can adapt every tick. | |
| self._last_tf_seconds : float = 60.0 | |
| # Rolling confidence for hysteresis | |
| self._conf_buf : RollingBuffer = RollingBuffer(5) | |
| # ------------------------------------------------------------------ | |
| # TIMING-FIX-2b: Timeframe-adaptive thresholds (computed properties) | |
| # ------------------------------------------------------------------ | |
| def _stale_ticks(self) -> int: | |
| """Target ~15 min wall-clock, clamped [12, 40] ticks.""" | |
| tf = self._last_tf_seconds | |
| if tf <= 0: | |
| return self._DFL_STALE_TICKS | |
| ticks = int(900.0 / tf) # 15 minutes / candle_interval | |
| return max(12, min(40, ticks)) | |
| def _max_cooling(self) -> int: | |
| """Target ~5 min wall-clock, clamped [5, 15] ticks.""" | |
| tf = self._last_tf_seconds | |
| if tf <= 0: | |
| return self._DFL_COOLING_TICKS | |
| ticks = int(300.0 / tf) # 5 minutes / candle_interval | |
| return max(5, min(15, ticks)) | |
| def _flip_suppress_ticks(self) -> int: | |
| """Minimum ticks between direction flips.""" | |
| return self._DFL_FLIP_SUPPRESS | |
| def update(self, score_result: Dict, event_result: Dict, | |
| di_result: Dict, ms_result: Dict, tick_count: int, | |
| tf_seconds: float = 60.0) -> Dict: | |
| try: | |
| return self._update_inner(score_result, event_result, | |
| di_result, ms_result, tick_count, | |
| tf_seconds) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("SignalLifecycleEngine", exc) | |
| return self._safe_default() | |
| def _update_inner(self, score_result, event_result, di_result, ms_result, | |
| tick_count, tf_seconds) -> Dict: | |
| # TIMING-FIX-2c: remember the detected timeframe | |
| self._last_tf_seconds = tf_seconds if tf_seconds > 0 else 60.0 | |
| self._tick_total = tick_count | |
| self._tick_in_state += 1 | |
| raw_conf = score_result.get("confidence", 0.0) | |
| raw_dir = score_result.get("raw_direction", 0.0) | |
| exec_suit = score_result.get("execution_suitability", "blocked") | |
| suppress = di_result.get("suppress_signal", False) | |
| self._conf_buf.push(raw_conf) | |
| smoothed_conf = self._conf_buf.mean() | |
| reason_codes: List[str] = [] | |
| # ---------------------------------------------------------------- | |
| # 1. Suppression / blocked override | |
| # ---------------------------------------------------------------- | |
| if suppress or exec_suit == "blocked": | |
| if self._state not in ("idle", "forming"): | |
| self._transition("cooling") | |
| reason_codes.append("DATA_DEGRADED") | |
| return self._emit("BUY", "idle", 0.0, False, "short", False, reason_codes) | |
| # ---------------------------------------------------------------- | |
| # 2. Direction determination with hysteresis | |
| # ---------------------------------------------------------------- | |
| new_dir = "BUY" if raw_dir >= 0 else "SELL" | |
| # Flip suppression: don't flip direction rapidly without strong evidence | |
| if new_dir != self._direction: | |
| dir_flip_allowed = ( | |
| abs(raw_dir) > 0.35 and | |
| (tick_count - self._last_flip_tick) >= self._flip_suppress_ticks and | |
| smoothed_conf > self._HYSTERESIS_DELTA | |
| ) | |
| if not dir_flip_allowed: | |
| new_dir = self._direction # hold current direction | |
| else: | |
| # AUDIT FIX (flip_count bug): _flip_count previously only ever | |
| # incremented and was never reset/decayed anywhere in this | |
| # class. On a 24/7 deployment, once the engine accumulated 3 | |
| # flips at ANY point over its entire lifetime (completely | |
| # normal -- markets reverse many times over weeks/months), | |
| # _flip_count stayed >=3 permanently, so the "rapid flip-flop" | |
| # check below started misfiring on every future flip, however | |
| # isolated and well-evidenced, immediately halving confidence | |
| # and tagging TF_CONFLICT on signals that were never actually | |
| # flip-flopping. Fix: only treat this as a continuation of a | |
| # rapid-flip streak if it's close to the previous flip (reuse | |
| # the same "<5 ticks" window the check below already uses for | |
| # what counts as "rapid"); otherwise this is a fresh, isolated | |
| # flip and the streak counter restarts. | |
| if (tick_count - self._last_flip_tick) < 5: | |
| self._flip_count += 1 | |
| else: | |
| self._flip_count = 1 | |
| self._last_flip_tick = tick_count | |
| reason_codes.append("TF_ALIGNED") | |
| # Rapid flip-flop suppression | |
| if self._flip_count >= 3 and (tick_count - self._last_flip_tick) < 5: | |
| smoothed_conf = clamp(smoothed_conf * 0.5) | |
| reason_codes.append("TF_CONFLICT") | |
| self._direction = new_dir | |
| # ---------------------------------------------------------------- | |
| # 3. State machine transitions | |
| # ---------------------------------------------------------------- | |
| prev_state = self._state | |
| if self._state == "idle": | |
| if smoothed_conf >= self._FORMING_THRESH: | |
| self._transition("forming") | |
| elif self._state == "forming": | |
| if smoothed_conf >= self._CANDIDATE_THRESH: | |
| self._transition("candidate") | |
| elif smoothed_conf < self._FORMING_THRESH * 0.5: | |
| self._transition("idle") | |
| elif self._state == "candidate": | |
| if smoothed_conf >= self._CONFIRMED_THRESH: | |
| self._transition("confirmed") | |
| self._last_confirmed_tick = tick_count | |
| elif smoothed_conf < self._FORMING_THRESH: | |
| self._transition("idle") | |
| elif self._state == "confirmed": | |
| # Invalidation conditions | |
| if self._should_invalidate(event_result, ms_result, di_result): | |
| self._transition("invalidated") | |
| reason_codes.append("MANIP_HIGH") | |
| elif smoothed_conf < self._FORMING_THRESH: | |
| self._transition("expired") | |
| reason_codes.append("STALE_DECAY") | |
| elif self._tick_in_state >= self._stale_ticks: | |
| self._transition("cooling") | |
| reason_codes.append("STALE_DECAY") | |
| elif self._state == "cooling": | |
| self._cooling_tick += 1 | |
| if self._cooling_tick >= self._max_cooling: | |
| self._transition("idle") | |
| self._cooling_tick = 0 | |
| elif smoothed_conf >= self._CONFIRMED_THRESH and self._cooling_tick >= 1: | |
| self._transition("candidate") | |
| self._cooling_tick = 0 | |
| elif self._state in ("expired", "invalidated"): | |
| self._transition("idle") | |
| # ---------------------------------------------------------------- | |
| # 4. Signal freshness (TIMING-FIX-2d: gentler quadratic decay) | |
| # ---------------------------------------------------------------- | |
| stale_ticks = self._stale_ticks | |
| if self._state == "confirmed": | |
| progress = safe_div(self._tick_in_state, stale_ticks) | |
| # Quadratic decay: stays fresher longer early on, | |
| # drops faster only near the end. | |
| freshness = clamp(1.0 - progress * progress) | |
| elif self._state == "candidate": | |
| freshness = 0.5 | |
| elif self._state == "forming": | |
| freshness = 0.25 | |
| else: | |
| freshness = 0.0 | |
| stale = self._state == "confirmed" and self._tick_in_state >= stale_ticks - 2 | |
| # ---------------------------------------------------------------- | |
| # 5. Expiry bucket classification | |
| # ---------------------------------------------------------------- | |
| expiry = self._classify_expiry(smoothed_conf, tf_align=score_result.get("tech_alignment_bonus", 0.0)) | |
| # ---------------------------------------------------------------- | |
| # 6. Reason codes | |
| # ---------------------------------------------------------------- | |
| if di_result.get("data_quality", 1.0) > 0.8: | |
| reason_codes.append("DATA_OK") | |
| else: | |
| reason_codes.append("DATA_DEGRADED") | |
| if score_result.get("raw_direction", 0.0) > 0 and self._direction == "BUY": | |
| reason_codes.append("MOMENTUM_OK") | |
| # Deduplicate reason codes | |
| reason_codes = list(dict.fromkeys(reason_codes)) | |
| cooling_flag = self._state == "cooling" | |
| confidence_final = clamp(smoothed_conf if self._state == "confirmed" else smoothed_conf * 0.7) | |
| return self._emit( | |
| self._direction, self._state, freshness, | |
| stale, expiry, cooling_flag, reason_codes, | |
| confidence_final=confidence_final | |
| ) | |
| def _should_invalidate(self, event_result: Dict, ms_result: Dict, di_result: Dict) -> bool: | |
| if event_result.get("manipulation_flag"): | |
| return True | |
| if ms_result.get("market_state") in ("manipulation", "blocked"): | |
| return True | |
| if di_result.get("data_quality", 1.0) < 0.2: | |
| return True | |
| return False | |
| def _transition(self, new_state: str) -> None: | |
| if new_state != self._state: | |
| self._state = new_state | |
| self._tick_in_state = 0 | |
| def _classify_expiry(self, confidence: float, tf_align: float) -> str: | |
| if confidence > 0.65 and tf_align > 0.1: | |
| return "medium" | |
| elif confidence > 0.35: | |
| return "short" | |
| return "ultra_short" | |
| def _emit(self, direction: str, state: str, freshness: float, | |
| stale: bool, expiry: str, cooling: bool, | |
| reason_codes: List[str], confidence_final: float = 0.0) -> Dict: | |
| return { | |
| "direction" : direction, | |
| "lifecycle_state" : state, | |
| "signal_freshness" : freshness, | |
| "stale_signal_flag": stale, | |
| "expiry_bucket" : expiry, | |
| "confidence_final" : confidence_final, | |
| "cooling_flag" : cooling, | |
| "reason_codes" : reason_codes, | |
| } | |
| def _safe_default() -> Dict: | |
| return { | |
| "direction": "BUY", "lifecycle_state": "idle", | |
| "signal_freshness": 0.0, "stale_signal_flag": False, | |
| "expiry_bucket": "ultra_short", "confidence_final": 0.0, | |
| "cooling_flag": False, "reason_codes": ["DATA_DEGRADED"], | |
| } | |
| # ============================================================================== | |
| # PART G1 — WARM-UP CONTROLLER (Layer 8) | |
| # ============================================================================== | |
| class WarmUpController: | |
| """ | |
| Tracks candle count and enforces cold-start gating. | |
| Exposes operational_mode, degraded_mode_flag, blocked_flag. | |
| Warm-up stages: | |
| 0 → MIN_WARM_CANDLES-1 : cold_start (all signals blocked) | |
| MIN_WARM → FULL_WARM-1 : cautious (reduced confidence) | |
| FULL_WARM+ : normal (full operation) | |
| Also handles: | |
| - Data quality degradation → degraded mode | |
| - High manipulation → cautious mode | |
| - Blocked conditions → blocked mode | |
| """ | |
| _MANIP_DEGRADED_THRESHOLD = 0.6 | |
| _QUALITY_DEGRADED_THRESHOLD = 0.5 | |
| _RECOVERY_TICKS = 10 | |
| def __init__(self) -> None: | |
| self._tick_count : int = 0 | |
| self._op_mode : str = "cold_start" | |
| self._degraded_ticks: int = 0 | |
| self._blocked_ticks : int = 0 | |
| self._recovery_tick : int = 0 | |
| def tick(self, di_result: Dict, score_result: Optional[Dict] = None) -> Dict: | |
| """ | |
| Called ONCE per candle. Advances tick_count by exactly 1. | |
| FIX-A: Never call this twice per candle — use refine() for post-score update. | |
| """ | |
| try: | |
| return self._tick_inner(di_result, score_result) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("WarmUpController", exc) | |
| return self._safe_default() | |
| def refine(self, di_result: Dict, score_result: Dict) -> Dict: | |
| """ | |
| FIX-4: Post-score mode refinement WITHOUT advancing tick_count. | |
| Previous implementation called _tick_inner() (which always increments | |
| _tick_count by 1), then manually restored the counter. That pattern | |
| risks off-by-one errors and computes mode/warm_frac with a phantom | |
| N+1 tick. | |
| This rewrite reads _tick_count directly (already correct from tick()) | |
| and re-classifies operational_mode based on the post-score signals, | |
| with zero risk of double-incrementing. | |
| """ | |
| try: | |
| tc = self._tick_count # set correctly by tick() earlier this candle | |
| manip_proxy = score_result.get("manipulation_penalty", 0.0) if score_result else 0.0 | |
| data_quality = di_result.get("data_quality", 1.0) | |
| suppress = di_result.get("suppress_signal", False) | |
| # Re-evaluate mode now that we have score feedback (only past cold-start) | |
| if tc >= MIN_WARM_CANDLES: | |
| if suppress: | |
| self._blocked_ticks += 1 | |
| # AUDIT FIX (recovery-streak bug): _recovery_tick counts | |
| # CONSECUTIVE clean ticks toward exiting degraded/blocked | |
| # mode, but was never reset when a new bad tick interrupted | |
| # an in-progress streak -- so e.g. a "bad, clean, clean" | |
| # pattern repeating would let _recovery_tick climb past | |
| # _RECOVERY_TICKS purely from non-consecutive clean ticks, | |
| # falsely declaring full recovery moments after a fresh | |
| # bad tick. Any bad tick must reset the streak. | |
| self._recovery_tick = 0 | |
| self._op_mode = "blocked" | |
| elif data_quality < self._QUALITY_DEGRADED_THRESHOLD: | |
| self._degraded_ticks += 1 | |
| self._recovery_tick = 0 # AUDIT FIX: see above | |
| self._op_mode = "degraded" | |
| elif manip_proxy > self._MANIP_DEGRADED_THRESHOLD: | |
| # High manipulation: downgrade normal/recovery to cautious | |
| self._recovery_tick = 0 # AUDIT FIX: see above | |
| if self._op_mode in ("normal", "recovery"): | |
| self._op_mode = "cautious" | |
| warm_frac = clamp(safe_div(tc - MIN_WARM_CANDLES, | |
| FULL_WARM_CANDLES - MIN_WARM_CANDLES)) | |
| blocked = tc < MIN_WARM_CANDLES or self._op_mode == "blocked" | |
| degraded = self._op_mode == "degraded" | |
| return { | |
| "operational_mode" : self._op_mode, | |
| "tick_count" : tc, | |
| "warm_up_fraction" : warm_frac, | |
| "blocked_flag" : blocked, | |
| "degraded_mode_flag" : degraded, | |
| "cold_start_flag" : tc < MIN_WARM_CANDLES, | |
| "degraded_tick_count": self._degraded_ticks, | |
| "blocked_tick_count" : self._blocked_ticks, | |
| } | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("WarmUpController", exc) | |
| return self._safe_default() | |
| def _tick_inner(self, di_result: Dict, score_result: Optional[Dict]) -> Dict: | |
| self._tick_count += 1 | |
| tc = self._tick_count | |
| data_quality = di_result.get("data_quality", 1.0) | |
| manip_proxy = 0.0 | |
| if score_result: | |
| manip_proxy = score_result.get("manipulation_penalty", 0.0) | |
| # ---- Determine operational mode ---- | |
| if tc < MIN_WARM_CANDLES: | |
| mode = "cold_start" | |
| blocked = True | |
| degraded = False | |
| elif tc < FULL_WARM_CANDLES: | |
| mode = "cautious" | |
| blocked = False | |
| degraded = False | |
| else: | |
| # Full operation but may degrade | |
| # AUDIT FIX (double-counting bug): _blocked_ticks/_degraded_ticks | |
| # are intentionally NOT incremented here anymore. refine() is | |
| # always called immediately after tick() for every non-cold-start | |
| # candle (see MAYTHOS._tick_inner) using this exact same | |
| # di_result, and re-evaluates these identical suppress/ | |
| # data_quality conditions -- so incrementing in both places | |
| # counted every blocked/degraded candle twice. refine() is also | |
| # strictly more complete (it additionally has manip_proxy, which | |
| # is never available yet here), so it is now the sole place these | |
| # persistent counters are mutated; this branch still computes the | |
| # local mode/blocked/degraded for tick()'s own immediate return | |
| # value. | |
| if di_result.get("suppress_signal", False): | |
| mode = "blocked" | |
| blocked = True | |
| degraded = False | |
| elif data_quality < self._QUALITY_DEGRADED_THRESHOLD: | |
| mode = "degraded" | |
| blocked = False | |
| degraded = True | |
| elif manip_proxy > self._MANIP_DEGRADED_THRESHOLD: | |
| mode = "cautious" | |
| blocked = False | |
| degraded = True | |
| else: | |
| # Recovery check: exiting degraded state | |
| if self._degraded_ticks > 0 or self._blocked_ticks > 0: | |
| self._recovery_tick += 1 | |
| if self._recovery_tick >= self._RECOVERY_TICKS: | |
| self._degraded_ticks = 0 | |
| self._blocked_ticks = 0 | |
| self._recovery_tick = 0 | |
| mode = "normal" | |
| blocked = False | |
| degraded = False | |
| else: | |
| mode = "recovery" | |
| blocked = False | |
| degraded = False | |
| else: | |
| mode = "normal" | |
| blocked = False | |
| degraded = False | |
| self._op_mode = mode | |
| # Warm-up fraction: 0 during cold, ramps to 1.0 at FULL_WARM | |
| warm_frac = clamp(safe_div(tc - MIN_WARM_CANDLES, | |
| FULL_WARM_CANDLES - MIN_WARM_CANDLES)) | |
| return { | |
| "operational_mode" : mode, | |
| "tick_count" : tc, | |
| "warm_up_fraction" : warm_frac, | |
| "blocked_flag" : blocked, | |
| "degraded_mode_flag" : degraded, | |
| "cold_start_flag" : tc < MIN_WARM_CANDLES, | |
| "degraded_tick_count": self._degraded_ticks, | |
| "blocked_tick_count" : self._blocked_ticks, | |
| } | |
| def tick_count(self) -> int: | |
| return self._tick_count | |
| def is_warm(self) -> bool: | |
| return self._tick_count >= FULL_WARM_CANDLES | |
| def _safe_default() -> Dict: | |
| return { | |
| "operational_mode": "cold_start", "tick_count": 0, | |
| "warm_up_fraction": 0.0, "blocked_flag": True, | |
| "degraded_mode_flag": False, "cold_start_flag": True, | |
| "degraded_tick_count": 0, "blocked_tick_count": 0, | |
| } | |
| # ============================================================================== | |
| # PART G2 — DEBUG TRACE ENGINE | |
| # ============================================================================== | |
| class DebugTraceEngine: | |
| """ | |
| Assembles a full debug trace dict from all engine outputs. | |
| Only populated when debug_mode=True. Otherwise returns None quickly. | |
| Never raises. Never affects signal output. | |
| """ | |
| def build( | |
| candle : Any, | |
| di_result : Dict, | |
| ms_result : Dict, | |
| asset_result : Dict, | |
| session_result : Dict, | |
| tf_result : Dict, | |
| event_result : Dict, | |
| lp_result : Dict, | |
| tech_result : Dict, | |
| score_result : Dict, | |
| lifecycle_result: Dict, | |
| warmup_result : Dict, | |
| debug_mode : bool = False, | |
| ) -> Optional[Dict]: | |
| if not debug_mode: | |
| return None | |
| try: | |
| return { | |
| "candle": { | |
| "ts": getattr(candle, "timestamp", 0.0), | |
| "o": getattr(candle, "open", 0.0), | |
| "h": getattr(candle, "high", 0.0), | |
| "l": getattr(candle, "low", 0.0), | |
| "c": getattr(candle, "close", 0.0), | |
| "v": getattr(candle, "volume", 0.0), | |
| }, | |
| "data_integrity" : di_result, | |
| "market_state" : ms_result, | |
| "asset_profile" : asset_result, | |
| "session" : session_result, | |
| "timeframe_fusion" : tf_result, | |
| "events" : event_result, | |
| "liquidity_pressure": lp_result, | |
| "technical_stack" : tech_result, | |
| "score" : score_result, | |
| "lifecycle" : lifecycle_result, | |
| "warmup" : warmup_result, | |
| } | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("DebugTraceEngine", exc) | |
| return {"error": "debug_trace_failed"} | |
| # ============================================================================== | |
| # PART G3 — OUTPUT FORMATTER (Layer 10) | |
| # ============================================================================== | |
| class OutputFormatter: | |
| """ | |
| Assembles the final MAYTHOS output dict from all engine results. | |
| Output contract (Section 5, expanded): | |
| Required top-level fields: | |
| direction, confidence, internal_trust, execution_suitability, | |
| market_state, regime_label, asset_mode, timeframe_alignment, | |
| manipulation_probability, liquidity_score, volatility_score, | |
| pressure_score, momentum_score, timing_score, signal_freshness, | |
| spread_health, data_quality, readability_score, stale_signal_flag, | |
| degraded_mode_flag, blocked_flag, reason_summary, reason_codes, | |
| call_bias, put_bias, | |
| expiry_suitability_ultra_short, expiry_suitability_short, | |
| expiry_suitability_medium, | |
| debug_trace, spec_version, engine_version, output_version | |
| Never raises. Returns safe default on any failure. | |
| """ | |
| def format( | |
| di_result : Dict, | |
| ms_result : Dict, | |
| asset_result : Dict, | |
| session_result : Dict, | |
| tf_result : Dict, | |
| event_result : Dict, | |
| lp_result : Dict, | |
| tech_result : Dict, | |
| score_result : Dict, | |
| lifecycle_result: Dict, | |
| warmup_result : Dict, | |
| debug_trace : Optional[Dict] = None, | |
| ) -> Dict: | |
| try: | |
| return OutputFormatter._format_inner( | |
| di_result, ms_result, asset_result, session_result, | |
| tf_result, event_result, lp_result, tech_result, | |
| score_result, lifecycle_result, warmup_result, debug_trace) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("OutputFormatter", exc) | |
| return _default_output() | |
| def _format_inner(di, ms, asset, session, tf, event, lp, tech, | |
| score, lc, warmup, debug_trace) -> Dict: | |
| # FIX 1: blocked_flag must be consistent with execution_suitability. | |
| # During cautious warm-up, warmup.blocked_flag=False but low warm_up_fraction | |
| # can still yield exec_suit='blocked'. Unify both here for contract integrity. | |
| _warmup_blocked = warmup.get("blocked_flag", True) | |
| _exec_blocked = score.get("execution_suitability", "blocked") == "blocked" | |
| blocked = _warmup_blocked or _exec_blocked | |
| degraded = warmup.get("degraded_mode_flag", False) | |
| conf = score.get("confidence", 0.0) | |
| # If blocked, return safe output with reason | |
| if blocked: | |
| out = _default_output() | |
| out["blocked_flag"] = True | |
| out["degraded_mode_flag"] = degraded | |
| out["data_quality"] = di.get("data_quality", 1.0) | |
| out["reason_summary"] = OutputFormatter._reason_summary( | |
| warmup, di, score, lc, blocked=True) | |
| out["reason_codes"] = lc.get("reason_codes", ["DATA_OK"]) | |
| out["debug_trace"] = debug_trace | |
| out["warm_up_fraction"] = warmup.get("warm_up_fraction", 0.0) | |
| out["operational_mode"] = warmup.get("operational_mode", "cold_start") | |
| out["spec_version"] = SPEC_VERSION | |
| out["engine_version"] = ENGINE_VERSION | |
| out["output_version"] = OUTPUT_VERSION | |
| return out | |
| # ---- Core signals ---- | |
| direction = lc.get("direction", "BUY") | |
| # AUDIT FIX: was a bare `assert`, which Python silently strips | |
| # entirely when run with -O / PYTHONOPTIMIZE=1. Contract-enforcing | |
| # checks on data flowing through business logic should never depend | |
| # on an interpreter flag; this is also more graceful than a stripped | |
| # assert (which would let an invalid value leak straight through), | |
| # since it self-corrects instead of relying on an exception path. | |
| if direction not in SIGNAL_DIRECTIONS: | |
| direction = "BUY" | |
| # ---- Derived scores ---- | |
| momentum_score = clamp( | |
| tech.get("macd_result", {}).get("momentum_quality", 0.5) * 0.5 + | |
| abs(tf.get("lower_bias", 0.0)) * 0.5 | |
| ) | |
| volatility_score = clamp( | |
| {"low": 0.3, "medium": 0.6, "high": 0.8, "extreme": 1.0}.get( | |
| asset.get("volatility_profile", "medium"), 0.6) | |
| ) | |
| readability_score = clamp( | |
| ms.get("readability_hint", 0.5) * 0.4 + | |
| (1.0 - di.get("instability_level", 0.0)) * 0.3 + | |
| tf.get("tf_alignment", 0.5) * 0.3 | |
| ) | |
| timing_score = session.get("session_score", 0.5) | |
| spread_health = clamp( | |
| 1.0 - | |
| float(di.get("spread_anomaly", False)) * 0.5 - | |
| float(di.get("spread_expansion", False)) * 0.3 | |
| ) | |
| # ---- Call / Put bias ---- | |
| # BUY → higher call_bias; SELL → higher put_bias | |
| raw_dir = score.get("raw_direction", 0.0) | |
| call_bias = clamp(0.5 + raw_dir * 0.5) | |
| put_bias = clamp(1.0 - call_bias) | |
| # ---- Expiry suitability scores ---- | |
| base_conf = conf | |
| expiry_us = clamp(base_conf * 0.6) | |
| expiry_sh = clamp(base_conf * 0.8 * (1.0 + timing_score * 0.2)) | |
| expiry_me = clamp(base_conf * (0.9 + tf.get("tf_alignment", 0.5) * 0.1)) | |
| # ---- Reason summary ---- | |
| reason_summary = OutputFormatter._reason_summary(warmup, di, score, lc, blocked=False) | |
| # ---- Market recognition (from ms_result) ---- | |
| market_state = ms.get("market_state", "undefined") | |
| if market_state not in MARKET_STATES: | |
| market_state = "undefined" | |
| regime_label = ms.get("regime_label", "transition") | |
| if regime_label not in REGIME_LABELS: | |
| regime_label = "transition" | |
| asset_mode = asset.get("asset_mode", "unknown") | |
| if asset_mode not in ASSET_MODES: | |
| asset_mode = "unknown" | |
| return { | |
| # Core | |
| "direction" : direction, | |
| "confidence" : conf, | |
| "internal_trust" : score.get("internal_trust", 0.0), | |
| "execution_suitability" : score.get("execution_suitability", "blocked"), | |
| "market_state" : market_state, | |
| "regime_label" : regime_label, | |
| "asset_mode" : asset_mode, | |
| "timeframe_alignment" : tf.get("tf_alignment", 0.5), | |
| "manipulation_probability": lp.get("manipulation_proxy", 0.0), | |
| "liquidity_score" : lp.get("liquidity_score", 0.5), | |
| "volatility_score" : volatility_score, | |
| "pressure_score" : lp.get("pressure_score", 0.5), | |
| "momentum_score" : momentum_score, | |
| "timing_score" : timing_score, | |
| "signal_freshness" : lc.get("signal_freshness", 0.0), | |
| "spread_health" : spread_health, | |
| "data_quality" : di.get("data_quality", 1.0), | |
| "readability_score" : readability_score, | |
| "stale_signal_flag" : lc.get("stale_signal_flag", False), | |
| "degraded_mode_flag" : degraded, | |
| "blocked_flag" : blocked, | |
| "reason_summary" : reason_summary, | |
| "reason_codes" : lc.get("reason_codes", ["DATA_OK"]), | |
| # Optional | |
| "call_bias" : call_bias, | |
| "put_bias" : put_bias, | |
| "expiry_suitability_ultra_short" : expiry_us, | |
| "expiry_suitability_short" : expiry_sh, | |
| "expiry_suitability_medium" : expiry_me, | |
| "debug_trace" : debug_trace, | |
| # Warm-up (FIX-B: was missing from output) | |
| "warm_up_fraction" : warmup.get("warm_up_fraction", 0.0), | |
| "operational_mode" : warmup.get("operational_mode", "cold_start"), | |
| # Version | |
| "spec_version" : SPEC_VERSION, | |
| "engine_version" : ENGINE_VERSION, | |
| "output_version" : OUTPUT_VERSION, | |
| } | |
| def _reason_summary(warmup: Dict, di: Dict, score: Dict, lc: Dict, | |
| blocked: bool) -> str: | |
| parts: List[str] = [] | |
| if warmup.get("cold_start_flag"): | |
| parts.append("cold_start") | |
| if blocked: | |
| parts.append("blocked") | |
| if warmup.get("degraded_mode_flag"): | |
| parts.append("degraded_mode") | |
| if di.get("suppress_signal"): | |
| parts.append("data_suppressed") | |
| if di.get("spread_anomaly"): | |
| parts.append("spread_anomaly") | |
| if di.get("gap_detected"): | |
| parts.append("gap_detected") | |
| if score.get("manipulation_penalty", 0.0) > 0.4: | |
| parts.append("manipulation_risk") | |
| if lc.get("stale_signal_flag"): | |
| parts.append("stale_signal") | |
| if lc.get("cooling_flag"): | |
| parts.append("signal_cooling") | |
| exec_suit = score.get("execution_suitability", "blocked") | |
| state = lc.get("lifecycle_state", "idle") | |
| parts.append(f"exec:{exec_suit}") | |
| parts.append(f"state:{state}") | |
| return " | ".join(parts) if parts else "ok" | |
| def validate_output(output: Dict) -> List[str]: | |
| """ | |
| Validate that all required fields are present and within bounds. | |
| Returns list of validation errors (empty = OK). | |
| """ | |
| errors: List[str] = [] | |
| required_floats = [ | |
| "confidence", "internal_trust", "timeframe_alignment", | |
| "manipulation_probability", "liquidity_score", "volatility_score", | |
| "pressure_score", "momentum_score", "timing_score", | |
| "signal_freshness", "spread_health", "data_quality", | |
| "readability_score", "call_bias", "put_bias", | |
| "expiry_suitability_ultra_short", "expiry_suitability_short", | |
| "expiry_suitability_medium", | |
| ] | |
| required_strs = [ | |
| "direction", "execution_suitability", "market_state", | |
| "regime_label", "asset_mode", "reason_summary", | |
| ] | |
| required_bools = [ | |
| "stale_signal_flag", "degraded_mode_flag", "blocked_flag", | |
| ] | |
| required_lists = ["reason_codes"] | |
| for f in required_floats: | |
| if f not in output: | |
| errors.append(f"missing:{f}") | |
| elif not isinstance(output[f], (int, float)): | |
| errors.append(f"not_float:{f}") | |
| elif not math.isfinite(output[f]): | |
| errors.append(f"not_finite:{f}") | |
| elif not (0.0 <= output[f] <= 1.0): | |
| errors.append(f"out_of_range:{f}={output[f]}") | |
| for f in required_strs: | |
| if f not in output: | |
| errors.append(f"missing:{f}") | |
| elif not isinstance(output[f], str): | |
| errors.append(f"not_str:{f}") | |
| for f in required_bools: | |
| if f not in output: | |
| errors.append(f"missing:{f}") | |
| elif not isinstance(output[f], bool): | |
| errors.append(f"not_bool:{f}") | |
| for f in required_lists: | |
| if f not in output: | |
| errors.append(f"missing:{f}") | |
| elif not isinstance(output[f], list): | |
| errors.append(f"not_list:{f}") | |
| # Enum checks | |
| if output.get("direction") not in SIGNAL_DIRECTIONS: | |
| errors.append(f"invalid_direction:{output.get('direction')}") | |
| if output.get("execution_suitability") not in EXECUTION_SUITABILITY: | |
| errors.append(f"invalid_exec_suit:{output.get('execution_suitability')}") | |
| if output.get("market_state") not in MARKET_STATES: | |
| errors.append(f"invalid_market_state:{output.get('market_state')}") | |
| return errors | |
| # ============================================================================== | |
| # MAYTHOS ORCHESTRATOR — Top-level API | |
| # ============================================================================== | |
| class MAYTHOS: | |
| """ | |
| MAYTHOS v1.1 — Single-entry-point orchestrator. | |
| Instantiate once; call tick() on every new candle. | |
| Thread-safety: NOT thread-safe. Use one instance per symbol/feed. | |
| Parameters | |
| ---------- | |
| debug_mode : bool | |
| When True, populates debug_trace in every output dict and accumulates | |
| an internal bounded log (access via .debug_log). Max 1000 entries | |
| (FIX-9: prevents unbounded memory growth in long debug sessions). | |
| Example | |
| ------- | |
| >>> engine = MAYTHOS() | |
| >>> output = engine.tick(candle) | |
| >>> print(output["direction"], output["confidence"], output["execution_suitability"]) | |
| """ | |
| _MAX_DEBUG_LOG: int = 1000 # FIX-9: cap internal debug trace log | |
| def __init__(self, debug_mode: bool = False) -> None: | |
| self._debug_mode = debug_mode | |
| self._data_integrity = DataIntegrityEngine() | |
| self._market_state = MarketStateEngine() | |
| self._asset_profile = AssetProfileEngine() | |
| self._session = SessionIntelligenceEngine() | |
| self._timeframe_fusion = MultiTimeframeFusionEngine() | |
| self._event_detection = EventDetectionEngine() | |
| self._liquidity_pressure = LiquidityPressureEngine() | |
| self._tech_stack = TechnicalConfirmationStack() | |
| self._scorer = AdaptiveScoringEngine() | |
| self._warmup = WarmUpController() | |
| self._lifecycle = SignalLifecycleEngine() | |
| self._formatter = OutputFormatter() | |
| self._debug_engine = DebugTraceEngine() | |
| # FIX-9: bounded in-memory debug log (only populated when debug_mode=True) | |
| self._debug_log: List[Dict] = [] | |
| def tick(self, | |
| candle : "Candle", | |
| receive_time: Optional[float] = None, | |
| debug : Optional[bool] = None) -> Dict: | |
| """ | |
| Process one Candle. Returns the full MAYTHOS output dict. | |
| Parameters | |
| ---------- | |
| candle : Candle object. | |
| receive_time : Optional wall-clock receive timestamp (float, UTC epoch). | |
| debug : Override instance debug_mode for this tick only. | |
| """ | |
| _debug = self._debug_mode if debug is None else debug | |
| try: | |
| return self._tick_inner(candle, receive_time, _debug) | |
| except Exception as exc: | |
| ENGINE_DIAGNOSTICS.record("MAYTHOS", exc) | |
| out = _default_output() | |
| if _debug: | |
| out["debug_trace"] = {"exception": str(exc)} | |
| return out | |
| def _tick_inner(self, candle: "Candle", receive_time: Optional[float], | |
| debug: bool) -> Dict: | |
| # Layer 1 | |
| di = self._data_integrity.process(candle, receive_time) | |
| # Layer 8 — pre-score warm-up gate | |
| warmup = self._warmup.tick(di) | |
| if warmup["cold_start_flag"]: | |
| out = _default_output() | |
| out["data_quality"] = di.get("data_quality", 1.0) | |
| out["reason_summary"] = "cold_start: insufficient history" | |
| out["warm_up_fraction"] = warmup.get("warm_up_fraction", 0.0) | |
| out["operational_mode"] = warmup.get("operational_mode", "cold_start") | |
| if debug: | |
| out["debug_trace"] = {"warmup": warmup, "di": di} | |
| return out | |
| # Layer 2 | |
| ms = self._market_state.update(candle, di) | |
| asset = self._asset_profile.update(candle, di) | |
| sess = self._session.update(candle) | |
| # Layer 3 | |
| tf = self._timeframe_fusion.update(candle, ms) | |
| # Layer 4 | |
| ev = self._event_detection.update(candle, di, ms) | |
| # Layer 5 | |
| lp = self._liquidity_pressure.update(candle, di, ev) | |
| # Layer 6 | |
| tech = self._tech_stack.update(candle, asset, sess) | |
| # Layer 7 | |
| score = self._scorer.score( | |
| di, ms, asset, sess, tf, ev, lp, tech, | |
| tick_count=self._warmup.tick_count, | |
| ) | |
| # Layer 8 — post-score warm-up refinement (FIX-A: use refine(), not tick()) | |
| # refine() updates mode from manipulation_penalty WITHOUT advancing tick_count | |
| warmup = self._warmup.refine(di, score_result=score) | |
| # Layer 9 | |
| # TIMING-FIX-3: pass detected candle interval so lifecycle | |
| # stale-signal and cooling windows scale with chart timeframe. | |
| lc = self._lifecycle.update( | |
| score, ev, di, ms, | |
| tick_count=self._warmup.tick_count, | |
| tf_seconds=self._timeframe_fusion.detected_tf_seconds, | |
| ) | |
| # Layer 10 | |
| dtrace = self._debug_engine.build( | |
| candle, di, ms, asset, sess, tf, ev, lp, tech, | |
| score, lc, warmup, debug_mode=debug, | |
| ) | |
| # FIX-9: accumulate debug traces up to _MAX_DEBUG_LOG entries (prevents | |
| # unbounded list growth during long debug sessions) | |
| if debug and dtrace is not None: | |
| self._debug_log.append(dtrace) | |
| if len(self._debug_log) > self._MAX_DEBUG_LOG: | |
| del self._debug_log[:-self._MAX_DEBUG_LOG] | |
| output = self._formatter.format( | |
| di, ms, asset, sess, tf, ev, lp, tech, | |
| score, lc, warmup, debug_trace=dtrace, | |
| ) | |
| return output | |
| def validate(self, output: Dict) -> List[str]: | |
| """Validate output dict. Returns list of errors (empty = OK).""" | |
| return OutputFormatter.validate_output(output) | |
| def reset(self) -> None: | |
| """Hard-reset all state (for backtesting resets).""" | |
| self.__init__(debug_mode=self._debug_mode) | |
| def tick_count(self) -> int: | |
| return self._warmup.tick_count | |
| def is_warm(self) -> bool: | |
| return self._warmup.is_warm | |
| def debug_log(self) -> List[Dict]: | |
| """FIX-9: Read-only snapshot of the bounded debug trace log (last ≤1000 entries).""" | |
| return list(self._debug_log) | |
| def engine_health() -> Dict: | |
| """ | |
| AUDIT FIX: read-only snapshot of internal engine error counts/recent | |
| errors (see ENGINE_DIAGNOSTICS above). Every engine's main entry | |
| point is fault-tolerant by design and will never raise out of | |
| tick() -- this exists purely so an operator can detect when those | |
| fallback paths are actually firing (e.g. wire it into a health-check | |
| endpoint or a periodic log line in your HF Space). Process-wide | |
| across all MAYTHOS instances in this process; call | |
| ENGINE_DIAGNOSTICS.reset() to clear it (e.g. after acknowledging an | |
| alert). | |
| """ | |
| return ENGINE_DIAGNOSTICS.snapshot() | |
| # ============================================================================== | |
| # ENTRY POINT — smoke test | |
| # ============================================================================== | |
| if __name__ == "__main__": | |
| import sys | |
| print(f"MAYTHOS v{ENGINE_VERSION} — Single-file self-test") | |
| engine = MAYTHOS(debug_mode=False) | |
| import datetime | |
| base_ts = datetime.datetime(2024, 3, 12, 10, 30, 0).timestamp() | |
| errors_seen = [] | |
| for i in range(150): | |
| close = 1.0000 + i * 0.0005 | |
| candle = Candle( | |
| timestamp = base_ts + i * 60.0, | |
| open = close - 0.0002, | |
| high = close + 0.0010, | |
| low = close - 0.0008, | |
| close = close, | |
| volume = 500.0 + i * 2.0, | |
| spread = 0.0002, | |
| source_id = "test_feed", | |
| ) | |
| output = engine.tick(candle) | |
| errs = engine.validate(output) | |
| if errs: | |
| errors_seen.extend(errs) | |
| print(f" tick {i:3d}: VALIDATION ERRORS: {errs}") | |
| if errors_seen: | |
| print(f"\n[FAIL] {len(errors_seen)} validation error(s).") | |
| sys.exit(1) | |
| print(f" tick count : {engine.tick_count}") | |
| print(f" is_warm : {engine.is_warm}") | |
| print(f" direction : {output['direction']}") | |
| print(f" confidence : {output['confidence']:.4f}") | |
| print(f" exec_suit : {output['execution_suitability']}") | |
| print(f" market_state : {output['market_state']}") | |
| print(f" regime : {output['regime_label']}") | |
| print(f" asset_mode : {output['asset_mode']}") | |
| print(f" reason : {output['reason_summary']}") | |
| print(f"\n[PASS] All 150 ticks produced valid output. MAYTHOS v{ENGINE_VERSION} operational.") | |