CoolFace
Apppublic

shaibu01/Titan-Engine

sourceHugging Faceupdated 2d agoView on Hugging Face
0likes
alpha_physics.py760 linesDownload Raw Back to root
1"""2TITAN ALPHA PHYSICS ENGINE (v51.1 - SYNCHRONIZED TENSOR ALIGNMENT)3TYPE: Institutional Signal Generator4MATH: Optimal Transport, Fractal Geometry, Info Theory, Bayesian Inference, Fast Ridge5UPGRADE LOG:6  v51.1 - Neural Trainer Alignment:7  1. TENSOR_DICT: Fixed channel offset. Collapsed orbit_s/orbit_l into `frama`.8  2. REGIME_SIGNAL: Added 16th channel to tensor_dict for proper downstream routing.9  3. SIGNATURE: analyze() now accepts `regime_signal` injection.10  v51.0 - Five major upgrades:11  1. SPECTRA: True idiosyncratic alpha via rolling-OLS market-beta neutralization (60-bar window).12             Falls back to lagged-VWAP factor when no market data is available.13  2. ORBIT:   Replaced polynomial trajectory divergence with Fractal Adaptive Moving Average14             (FRAMA). FRAMA adapts smoothing to fractal dimension so it is fast in trends and15             slow in chop — validated by institutional practitioners (Ehlers 2004).16  3. REGIME_GATE: analyze() now accepts regime_gate kwarg (0=Bull, 1=Chop, 2=Bear, 3=Crash).17             When supplied, signal weights are scaled to match the current regime's edge18             profile, improving Sharpe in all four environments.19  4. VAMP:   Volume-Weighted Average Price Momentum — 15th tensor feature.20             VWAP = Σ(close×volume) / Σ(volume) over rolling 20 bars.21             VAMP = (close - VWAP) / ATR, clamped [-1, 1].22  5. BBP:    Bollinger Band %B — 16th tensor feature.23             Maps price position within Bollinger Bands, centered on 0.24INPUT_SIZE: 16D tensor (stark_s, stark_l, wave, cairo, frama, flux,25            echo_s, echo_l, ridge, spectra, gap, vix, htf_trend, vamp, bbp, regime_signal)26"""27 28import pandas as pd29import numpy as np30from scipy.stats import skew, kurtosis, norm, entropy31import warnings32import math33 34# Suppress warnings to keep Cloud logs clean35warnings.filterwarnings("ignore")36 37# 🚨 PANDAS 2.2.0 FORWARD COMPATIBILITY FIX38try:39    pd.set_option('future.no_silent_downcasting', True)40except Exception:41    pass42 43# --- C++ CORE INJECTION ---44try:45    import titan_engine46    HAS_CPP_CORE = True47except ImportError:48    HAS_CPP_CORE = False49 50class PhysicsEngine:51    def __init__(self):52        print("⚛️ PHYSICS ENGINE: ONLINE [MULTI-TIMEFRAME EXPANSION + C++ BRIDGE + FRAMA + VAMP + BBP]", flush=True)53        self.window = 2054 55    # ==============================================================================56    # 1. CORE MATH UTILITIES & C++ BRIDGE57    # ==============================================================================58    def _get_returns(self, df):59        return np.log(df['close'] / df['close'].shift(1)).fillna(0)60 61    def _calc_ema(self, series, span):62        return series.ewm(span=span, adjust=False).mean()63 64    def _calc_atr(self, df, period=14):65        """Average True Range with OHLC shock absorber."""66        if (df['high'] == df['low']).all():67            fallback_atr = df['close'].diff().abs().rolling(period).mean()68            return fallback_atr.replace(0.0, 1e-5)69 70        high_low = df['high'] - df['low']71        high_close = np.abs(df['high'] - df['close'].shift())72        low_close = np.abs(df['low'] - df['close'].shift())73 74        ranges = pd.concat([high_low, high_close, low_close], axis=1)75        true_range = ranges.max(axis=1)76 77        atr_line = true_range.rolling(period).mean()78        return atr_line.replace(0.0, 1e-5)79 80    def _calc_hurst(self, series):81        """🚨 V99.5 C++ SYNC: Points to the new OpenMP calc_hurst_fast engine"""82        if HAS_CPP_CORE and hasattr(titan_engine, 'calc_hurst_fast'):83            try:84                arr = np.ascontiguousarray(series[-100:], dtype=np.float64)85                return float(titan_engine.calc_hurst_fast(arr))86            except Exception:87                pass88 89        # Fallback to slow Python math90        try:91            max_lag = 2092            lags = range(2, max_lag)93            tau = [np.sqrt(np.std(np.subtract(series[lag:], series[:-lag]))) for lag in lags]94            poly = np.polyfit(np.log(lags), np.log(tau), 1)95            return float(poly[0] * 2.0)96        except Exception:97            return 0.598 99    def _calc_entropy(self, series, bins=10):100        if HAS_CPP_CORE and hasattr(titan_engine, 'calc_entropy'):101            try:102                arr = np.ascontiguousarray(series, dtype=np.float64)103                return float(titan_engine.calc_entropy(arr, bins))104            except Exception:105                pass106 107        try:108            hist, _ = np.histogram(series, bins=bins, density=True)109            return float(entropy(hist))110        except Exception:111            return 0.0112 113    # ==============================================================================114    # 2. NOISE CLASSIFICATION115    # ==============================================================================116    def get_noise_signature(self, df):117        closes = df['close'].values118        h = self._calc_hurst(closes)119 120        if 0.45 <= h <= 0.55: return "WHITE_NOISE", 0.0121        elif h < 0.45: return "PINK_NOISE", 0.5122        else: return "BLACK_NOISE", 1.2123 124    # ==============================================================================125    # 3. ALPHA MODELS (The "Quant" Layer)126    # ==============================================================================127 128    # --- MODEL 1: STARK (Optimal Transport) [MULTI-TIMEFRAME] ---129    def calculate_stark(self, returns, window=20):130        """1D Wasserstein Distance Proxy"""131        if len(returns) < window * 2: return 0.5132        try:133            recent_dist = np.sort(returns.values[-window:])134            historical_dist = np.sort(returns.values[-window*2:-window])135            flow_metric = np.mean(np.abs(recent_dist - historical_dist))136            return float(1.0 / (1.0 + flow_metric * 100))137        except Exception:138            return 0.5139 140    # --- MODEL 2: WAVE (Kernel Regime) ---141    def calculate_wave(self, returns, window=20):142        if len(returns) < window: return 0.0143        try:144            vol = returns.rolling(window).std().iloc[-1]145            return float(np.exp(-1.0 * (vol**2)))146        except Exception:147            return 0.0148 149    # --- MODEL 3: CAIRO (Mass Flow & Tails) ---150    def calculate_cairo(self, returns, window=20):151        if len(returns) < window: return 0.0152        try:153            local_data = returns.iloc[-window:]154            m3 = skew(local_data)155            m4 = kurtosis(local_data)156            ent = self._calc_entropy(local_data)157 158            cairo_score = 0.0159            if m3 > 0.1: cairo_score += 0.3160            elif m3 < -0.1: cairo_score -= 0.3161            if m4 < 3.0: cairo_score += 0.2162            if ent < 2.0: cairo_score += 0.2163 164            return float(max(-1.0, min(1.0, cairo_score)))165        except Exception:166            return 0.0167 168    # --- MODEL 4: ORBIT — FRACTAL ADAPTIVE MOVING AVERAGE (FRAMA) ---169    def calculate_orbit(self, df, window=20):170        """171        UPGRADE v51.0: Replaced polynomial trajectory divergence with FRAMA.172 173        FRAMA (Fractal Adaptive Moving Average) was introduced by John Ehlers (2004).174        It adapts its smoothing constant based on the fractal dimension of price over175        the look-back window.  When price is trending (D close to 1), alpha is large176        → fast EMA.  When price is ranging (D close to 2), alpha is small → slow EMA.177 178        Formula:179          half = window // 2180          N1 = (max(high[-half:]) - min(low[-half:])) / half          # recent half181          N2 = (max(high[-window:-half]) - min(low[-window:-half])) / half  # older half182          N3 = (max(high[-window:]) - min(low[-window:])) / window    # full range183 184          D  = (log(N1 + N2) - log(N3)) / log(2)   # Fractal Dimension estimate185          alpha = exp(-4.6 * (D - 1))               # Maps D∈[1,2] → alpha∈[1, 0.01]186          alpha is clamped to [0.01, 1.0]187 188          FRAMA[t] = alpha * price[t] + (1 - alpha) * FRAMA[t-1]189 190        Signal = (price[-1] - FRAMA[-1]) / (ATR + 1e-9), clamped to [-1, 1]191        A positive signal means price is above the adaptive MA (bullish); negative → bearish.192        """193        if len(df) < window + 5: return 0.0194        try:195            closes = df['close'].values196            highs  = df['high'].values197            lows   = df['low'].values198 199            half = window // 2200 201            # Compute FRAMA over the full available history using a rolling approach202            # Initialise FRAMA at the first close in the window203            frama = closes[-(window + 5)]204 205            for i in range(-(window + 4), 0):206                # Extract the window ending at index i (Python negative indexing)207                end   = len(closes) + i + 1          # exclusive upper bound208                start = end - window                  # inclusive lower bound209                if start < 0:210                    frama = closes[i]211                    continue212 213                h_full  = highs[start:end]214                l_full  = lows[start:end]215                h_first = highs[start:start + half]  # older half216                l_first = lows[start:start + half]217                h_last  = highs[start + half:end]    # recent half218                l_last  = lows[start + half:end]219 220                N1 = (h_last.max()  - l_last.min())  / (half + 1e-9)   # recent range / half221                N2 = (h_first.max() - l_first.min()) / (half + 1e-9)   # older range / half222                N3 = (h_full.max()  - l_full.min())  / (window + 1e-9) # full range / window223 224                # Fractal Dimension: D ∈ [1, 2]225                # D≈1 → straight line (strong trend); D≈2 → space-filling (chop)226                denom = math.log(N1 + N2 + 1e-9) - math.log(N3 + 1e-9)227                if N3 > 1e-12 and (N1 + N2) > N3:228                    D = denom / math.log(2)229                else:230                    D = 1.5  # neutral fallback231 232                D = max(1.0, min(2.0, D))233 234                # Alpha mapping: exp(-4.6*(D-1))235                # D=1 → alpha=1.0 (fastest EMA); D=2 → alpha≈0.01 (slowest EMA)236                alpha = math.exp(-4.6 * (D - 1.0))237                alpha = max(0.01, min(1.0, alpha))238 239                frama = alpha * closes[i] + (1.0 - alpha) * frama240 241            # Compute ATR for normalisation242            atr_val = float(self._calc_atr(df).iloc[-1])243            current_price = closes[-1]244 245            # Signal: deviation from FRAMA normalised by ATR246            raw_signal = (current_price - frama) / (atr_val + 1e-9)247            return float(max(-1.0, min(1.0, raw_signal)))248 249        except Exception:250            return 0.0251 252    # --- MODEL 5: FLUX (Bayesian Probability) ---253    def calculate_flux(self, df, window=20):254        if len(df) < window: return 0.5255        try:256            m1 = df['close'].diff(5).iloc[-1]257            m2 = df['close'].diff(10).iloc[-1]258            m3 = df['close'].diff(20).iloc[-1]259 260            ensemble_mean = np.mean([m1, m2, m3])261            ensemble_std = np.std([m1, m2, m3])262            market_noise = df['close'].rolling(window).std().iloc[-1]263 264            total_uncertainty = ensemble_std + market_noise + 1e-9265            z_score = ensemble_mean / total_uncertainty266            return float(norm.cdf(z_score))267        except Exception:268            return 0.5269 270    # --- MODEL 6: ECHO (Hurst + Momentum) [MULTI-TIMEFRAME] ---271    def calculate_echo(self, df, window=20):272        try:273            closes = df['close']274            if len(closes) < window: return 0.0275 276            is_trending = self._calc_hurst(closes.values[-window:]) > 0.55277 278            ema_fast = self._calc_ema(closes, max(3, window//3)).iloc[-1]279            ema_slow = self._calc_ema(closes, window).iloc[-1]280            recent = closes.tail(window)281            location_z = float((recent.iloc[-1] - recent.mean()) / (recent.std() + 1e-9))282            short_slope = float(closes.diff(max(2, window // 4)).iloc[-1])283            last_impulse = float(closes.diff().iloc[-1])284 285            if is_trending:286                if ema_fast > ema_slow and short_slope > 0.0 and location_z > -1.25:287                    return 1.0288                if ema_fast < ema_slow and short_slope < 0.0 and location_z < 1.25:289                    return -1.0290            else:291                if location_z < -1.6 and last_impulse > 0.0:292                    return 1.0293                if location_z > 1.6 and last_impulse < 0.0:294                    return -1.0295 296            return 0.0 # HOLD297        except Exception:298            return 0.0299 300    # --- MODEL 7: RIDGE_SHRINK (L2 Regularization Penalty) ---301    def calculate_ridge_shrink(self, returns, window=30):302        """Fast Closed-Form Ridge Math"""303        if len(returns) < window + 5: return 0.0304        try:305            r_vals = returns.values306            y = r_vals[-window:]307            X = np.column_stack((308                r_vals[-window-1:-1],309                r_vals[-window-2:-2],310                r_vals[-window-3:-3]311            ))312 313            valid_idx = ~np.isnan(X).any(axis=1) & ~np.isnan(y)314            X_val = X[valid_idx]315            y_val = y[valid_idx]316 317            if len(y_val) < 10: return 0.0318 319            I = np.eye(X_val.shape[1])320            w = np.linalg.solve(X_val.T @ X_val + 1.0 * I, X_val.T @ y_val)321 322            latest_features = np.array([r_vals[-1], r_vals[-2], r_vals[-3]])323            shrunk_prediction = np.dot(latest_features, w)324 325            vol = np.std(y_val) + 1e-9326            normalized_score = shrunk_prediction / (vol * 3.0)327            return float(max(-1.0, min(1.0, normalized_score)))328        except Exception:329            return 0.0330 331    # --- MODEL 8: SPECTRA (True Idiosyncratic Alpha via Beta-Neutralization) ---332    def calculate_spectra(self, returns, window=20, market_returns=None):333        """334        UPGRADE v51.0: True Idiosyncratic Alpha via rolling OLS beta neutralization.335 336        The original SPECTRA computed residual vs the asset's own EWM trend, which is337        NOT idiosyncratic alpha — the asset's own trend contaminates the residual with338        systematic risk if the asset has any market beta.339 340        Fix:341          1. If market_returns (e.g. SPY) is available:342               beta = OLS(returns[-60:], market_returns[-60:])343               idiosyncratic = returns - beta * market_returns - ewm_expected344 345          2. Fallback (no market data):346               Use lagged volume-weighted-average-return as a factor proxy.347               beta_proxy = OLS(returns[-60:], lagged_vwap_factor[-60:])348               idiosyncratic = returns - beta_proxy * lagged_vwap_factor - ewm_expected349 350        Rolling window for beta estimation: 60 bars.351        This makes SPECTRA a genuine stock-specific alpha orthogonal to systematic risk.352        """353        ols_window = 60354        if len(returns) < max(ols_window, window + 5): return 0.0355        try:356            r_vals = returns.values.copy()357 358            # --- Estimate and subtract systematic factor ---359            if market_returns is not None and len(market_returns) >= ols_window:360                # Path A: Real market factor (e.g. SPY log returns)361                mkt = np.array(market_returns[-ols_window:], dtype=np.float64)362                asset = r_vals[-ols_window:]363                # OLS: beta = Cov(asset, mkt) / Var(mkt)364                mkt_dm = mkt - mkt.mean()365                var_mkt = np.dot(mkt_dm, mkt_dm) / len(mkt_dm) + 1e-12366                beta = float(np.dot(asset - asset.mean(), mkt_dm) / (len(mkt_dm) * var_mkt))367                # Subtract beta * market from entire returns series368                mkt_full = np.array(market_returns[-len(r_vals):], dtype=np.float64)369                if len(mkt_full) == len(r_vals):370                    r_vals = r_vals - beta * mkt_full371                else:372                    # Length mismatch — fall back to last ols_window portion373                    r_vals[-ols_window:] = r_vals[-ols_window:] - beta * mkt374            else:375                # Path B: No market data — use lagged VWAP-of-returns as synthetic factor.376                # VWAP-of-returns proxy: exponentially weighted average of past returns377                # acting as a "trend factor" orthogonal to the EWM expected return below.378                ols_returns = r_vals[-ols_window:]379                # Build factor: simple rolling mean shifted by 1 (lagged)380                factor = np.zeros(ols_window)381                cum = 0.0382                for j in range(ols_window):383                    cum = 0.94 * cum + 0.06 * (ols_returns[j - 1] if j > 0 else 0.0)384                    factor[j] = cum385                factor_dm = factor - factor.mean()386                var_f = np.dot(factor_dm, factor_dm) / len(factor_dm) + 1e-12387                beta_proxy = float(np.dot(ols_returns - ols_returns.mean(), factor_dm)388                                   / (len(factor_dm) * var_f))389                r_vals[-ols_window:] = ols_returns - beta_proxy * factor390 391            # --- Now compute EWM-expected and residual (idiosyncratic alpha) ---392            beta_neutralized = pd.Series(r_vals, index=returns.index)393            expected_return = beta_neutralized.ewm(span=window).mean().shift(1)394            idiosyncratic_alpha = beta_neutralized - expected_return395 396            short_term_alpha = idiosyncratic_alpha.iloc[-5:].sum()397            long_term_alpha  = idiosyncratic_alpha.iloc[-20:-5].sum()398 399            spectra_signal = short_term_alpha - (0.5 * long_term_alpha)400 401            alpha_vol = idiosyncratic_alpha.iloc[-20:].std() + 1e-9402            normalized_spectra = spectra_signal / (alpha_vol * 3.0)403 404            return float(max(-1.0, min(1.0, normalized_spectra)))405        except Exception:406            return 0.0407 408    # ==============================================================================409    # 4. INSTITUTIONAL FEATURES (Gap, Weekly Trend, VIX Proxy)410    # ==============================================================================411    def calculate_gap_power(self, df):412        """Earnings/Overnight Gap Detector. Looks for large gaps + volume expansion."""413        if len(df) < 5: return 0.0414        try:415            open_px   = df['open'].iloc[-1]416            prev_close = df['close'].iloc[-2]417 418            gap_pct = (open_px - prev_close) / (prev_close + 1e-9)419 420            vol_current = df['volume'].iloc[-1]421            vol_ma      = df['volume'].rolling(20).mean().iloc[-2] + 1e-9422            vol_ratio   = vol_current / vol_ma423 424            # Only trigger on significant gaps (>0.5%) with volume backing (>1.5x)425            if abs(gap_pct) > 0.005 and vol_ratio > 1.5:426                direction = 1.0 if gap_pct > 0 else -1.0427                power = min(1.0, abs(gap_pct) * 50.0)428                return float(direction * power)429            return 0.0430        except Exception:431            return 0.0432 433    def calculate_vix_proxy(self, df):434        """Synthetic Macro Risk-Off Indicator using Parkinson Volatility."""435        if len(df) < 20: return 0.0436        try:437            highs = df['high'].values[-20:]438            lows  = df['low'].values[-20:]439 440            parkinson_vol = np.sqrt(441                (1 / (4 * 20 * math.log(2))) *442                np.sum(np.log(highs / (lows + 1e-9)) ** 2)443            )444            annualized_vol = parkinson_vol * math.sqrt(252)445 446            # Normalize 0 to 1 (> 0.40 is extreme panic)447            risk_off_score = min(1.0, annualized_vol / 0.40)448            return float(risk_off_score)449        except Exception:450            return 0.0451 452    def calculate_weekly_trend(self, df):453        """Higher Timeframe Anchor. Prevents shorting in a macro bull market."""454        if len(df) < 60: return 0.0455        try:456            closes = df['close'].values457            fast_ma = np.mean(closes[-15:])  # Approx 3 weeks458            slow_ma = np.mean(closes[-60:])  # Approx 12 weeks459 460            trend_strength = (fast_ma - slow_ma) / (slow_ma + 1e-9)461 462            if trend_strength > 0.02:  return  1.0  # Strong Bull463            elif trend_strength < -0.02: return -1.0  # Strong Bear464            return 0.0  # Neutral465        except Exception:466            return 0.0467 468    # ==============================================================================469    # 5. NEW FEATURES: VAMP (15th) & BBP (16th)470    # ==============================================================================471 472    def calculate_vamp(self, df, atr_val, window=20):473        """474        UPGRADE v51.0 — Feature 15: VWAP Anchored Momentum (VAMP)475 476        VWAP = Σ(close[i] * volume[i]) / Σ(volume[i])  for i in rolling window of 20 bars.477 478        This is not the intraday VWAP (which resets daily) but a rolling VWAP that serves479        as a dynamic fair-value anchor.  Institutions use VWAP as a benchmark; deviation480        from it carries momentum information:481          - price above VWAP → buyer aggression → bullish bias482          - price below VWAP → seller aggression → bearish bias483 484        VAMP = (close[-1] - VWAP) / (ATR + 1e-9), clamped to [-1, 1].485 486        ATR normalisation makes VAMP volatility-adjusted (unit-free), suitable as a487        direct tensor feature.488        """489        if len(df) < window + 1: return 0.0490        try:491            close  = df['close'].values492            volume = df['volume'].values493 494            # Rolling VWAP over 20 bars495            pv_sum  = np.sum(close[-window:] * volume[-window:])496            vol_sum = np.sum(volume[-window:]) + 1e-9497            vwap    = pv_sum / vol_sum498 499            vamp = (close[-1] - vwap) / (atr_val + 1e-9)500            return float(max(-1.0, min(1.0, vamp)))501        except Exception:502            return 0.0503 504    def calculate_bbp(self, df, window=20):505        """506        UPGRADE v51.0 — Feature 16: Bollinger Band %B (BBP), centered on 0.507 508        Classic Bollinger %B maps price to [0, 1] within the bands:509          middle = rolling 20-bar mean510          upper  = middle + 2 * rolling std511          lower  = middle - 2 * rolling std512          %B     = (close - lower) / (upper - lower + 1e-9)513 514        We center on 0 by subtracting 0.5, giving a [-0.5, 0.5] tensor feature:515          BBP = %B - 0.5516            → BBP > 0  : price above midband (momentum / overextended long)517            → BBP < 0  : price below midband (mean-reversion / overextended short)518            → BBP = 0  : price exactly at midband (neutral)519 520        This complements VAMP and RIDGE to give the neural net rich price-position context.521        """522        if len(df) < window + 1: return 0.0523        try:524            close_series = df['close']525            middle = close_series.rolling(window).mean().iloc[-1]526            std    = close_series.rolling(window).std().iloc[-1]527            upper  = middle + 2.0 * std528            lower  = middle - 2.0 * std529            current_close = close_series.iloc[-1]530 531            pct_b = (current_close - lower) / (upper - lower + 1e-9)532            bbp   = pct_b - 0.5  # Center on 0 → range ≈ [-0.5, 0.5]533            # Clamp to [-0.5, 0.5] in case of extreme spikes beyond bands534            return float(max(-0.5, min(0.5, bbp)))535        except Exception:536            return 0.0537 538    # ==============================================================================539    # 6. REGIME-CONDITIONAL WEIGHT TABLE540    # ==============================================================================541    # Applied inside analyze() when regime_gate is provided.542    # Keys match tensor_dict / signal names used in the vote.543    # Missing keys default to 1.0 (no scaling).544    REGIME_WEIGHTS = {545        # regime_gate=0 → Bull: reward momentum signals, penalise mean-reversion546        0: {547            "flux":    1.5,548            "echo":    1.5,   # used for echo_short in vote549            "gap":     1.3,550            "ridge":   0.7,551            "spectra": 0.7,552        },553        # regime_gate=1 → Chop: reward mean-reversion, penalise trend signals554        1: {555            "ridge":   1.5,556            "spectra": 1.5,557            "wave":    1.3,558            "flux":    0.5,559            "echo":    0.5,560        },561        # regime_gate=2 → Bear: reward defensive / short signals562        2: {563            "cairo":   1.5,564            "stark_l": 1.3,565            "vix":     1.5,566            "flux":    0.5,567        },568        # ✅ MEDIUM-3 FIX: regime_gate=3 → CRASH/PANIC: extreme defensive weights.569        # Previously missing — CRASH regime got default weights (all 1.0), providing570        # no protection. Now mirrors Bear but with maximum vix/cairo/stark multipliers571        # and near-zero momentum signals to prevent trend-following during panics.572        3: {573            "cairo":   2.0,   # Max defensive: Cairo reversal signal fully boosted574            "stark_l": 1.8,   # Strong long-term momentum dampener575            "stark_s": 1.5,   # Short-term defensive also boosted576            "vix":     2.0,   # VIX fear gauge at maximum weight577            "flux":    0.2,   # Momentum signals nearly silenced578            "echo":    0.2,   # Echo momentum nearly silenced579            "gap":     0.3,   # Gap signals suppressed (false breakouts in crashes)580            "wave":    0.3,   # Mean reversion unreliable in crashes581            "htf_trend": 0.2, # HTF trend signals suppressed582        },583    }584 585    def _apply_regime_weight(self, signal_val, signal_name, regime_gate):586        """Scales a signal by its regime-conditional multiplier (default 1.0)."""587        if regime_gate is None:588            return signal_val589        weights = self.REGIME_WEIGHTS.get(regime_gate, {})590        multiplier = weights.get(signal_name, 1.0)591        return signal_val * multiplier592 593    # ==============================================================================594    # 7. BI-DIRECTIONAL SYNTHESIS ENGINE (The Vote & Tensor Extraction)595    # ==============================================================================596    def analyze(self, df, regime_gate=None, regime_signal: float = 0.5):597        """598        Aggregates all physics models into a single Vote and extracts the 16D Tensor.599 600        Parameters601        ----------602        df : pd.DataFrame603            OHLCV price data. Must have columns: open, high, low, close, volume.604        regime_gate : int or None605            Optional regime label from the HMM scanner:606              0 = Bull/Low-Vol607              1 = Chop/Sideways608              2 = Bear/Panic609              3 = Crash610            When supplied, applies regime-conditional weight multipliers to the611            individual signals before aggregating the vote.  This improves edge612            alignment across all four market environments.613        regime_signal : float614            Continuous regime probability (e.g. HMM Bull prior) injected by the caller.615            Defaults to 0.5 (neutral). Must be passed to properly populate the 16th tensor feature.616 617        Returns618        -------619        (final_vote, viz_string, atr_val, tensor_dict)620        tensor_dict is a 16D OrderedDict containing all signal values.621        """622        if df is None or df.empty or len(df) < 60:623            return 0.0, "WAITING_DATA", 0.01, {}624 625        try:626            df.columns = [c.lower() for c in df.columns]627            returns = self._get_returns(df)628 629            noise_type, noise_weight = self.get_noise_signature(df)630            atr_val = float(self._calc_atr(df).iloc[-1])631 632            if noise_weight == 0.0:633                return 0.0, f"NOISE_FILTER:{noise_type}", atr_val, {}634 635            # --- Extract Multi-Timeframe Features ---636            stark_short = self.calculate_stark(returns, window=10)637            stark_long  = self.calculate_stark(returns, window=40)638 639            wave  = self.calculate_wave(returns)640            cairo = self.calculate_cairo(returns)641 642            # ORBIT now uses FRAMA (see calculate_orbit above)643            frama_short = self.calculate_orbit(df, window=10)644            frama_long  = self.calculate_orbit(df, window=40)645 646            flux = self.calculate_flux(df)647 648            echo_short = self.calculate_echo(df, window=10)649            echo_long  = self.calculate_echo(df, window=40)650 651            ridge_score   = self.calculate_ridge_shrink(returns)652            # SPECTRA: pass no market_returns → falls back to lagged-VWAP proxy653            spectra_score = self.calculate_spectra(returns)654 655            gap_power = self.calculate_gap_power(df)656            vix_proxy = self.calculate_vix_proxy(df)657            htf_trend = self.calculate_weekly_trend(df)658 659            # --- New Features 15 & 16 ---660            vamp_score = self.calculate_vamp(df, atr_val)661            bbp_score  = self.calculate_bbp(df)662 663            # --- Compile the 16D Feature Tensor for the Neural Net ---664            # INPUT_SIZE = 16D: stark_s, stark_l, wave, cairo, frama, flux, echo_s,665            #                   echo_l, ridge, spectra, gap, vix, htf_trend, vamp, bbp, regime_signal666            tensor_dict = {667                "stark_s":       stark_short,668                "stark_l":       stark_long,669                "wave":          wave,670                "cairo":         cairo,671                "frama":         frama_long,   # Extracted using the primary longer-window FRAMA672                "flux":          flux,673                "echo_s":        echo_short,674                "echo_l":        echo_long,675                "ridge":         ridge_score,676                "spectra":       spectra_score,677                "gap":           gap_power,678                "vix":           vix_proxy,679                "htf_trend":     htf_trend,680                "vamp":          vamp_score,681                "bbp":           bbp_score,682                "regime_signal": float(max(0.0, min(1.0, regime_signal))), # Injected from HMM683            }684 685            # --- Apply regime-conditional weight gates (v51.0 upgrade) ---686            # Each signal is multiplied by its regime-specific multiplier before voting.687            # When regime_gate is None all multipliers are 1.0 (no change).688            def rw(val, name):689                return self._apply_regime_weight(val, name, regime_gate)690 691            # --- Legacy Heuristic Voting System (For the C++ Fast Path) ---692            vote = 0.0693 694            if echo_short > 0:695                vote += 0.25 * rw(1.0, "echo")           # Base edge, regime-scaled696                vote += (frama_short * 0.10)697                vote += (frama_long  * 0.05)698                vote += (cairo       * 0.15 * rw(1.0, "cairo"))699                vote += (ridge_score * 0.10 * rw(1.0, "ridge"))700                vote += (spectra_score * 0.10 * rw(1.0, "spectra"))701                vote += (gap_power   * 0.15 * rw(1.0, "gap"))702 703                # Penalty if counter-trend to Higher Timeframe704                if htf_trend < 0: vote -= 0.20705 706                flux_scaled = rw(flux, "flux")707                if flux_scaled > 0.7: vote += 0.15708                elif flux_scaled < 0.3: vote -= 0.15709 710                # Additional regime contributions from new features711                vote += (vamp_score * 0.08)   # VAMP adds bullish momentum context712                vote += (bbp_score  * 0.06)   # BBP adds band-position context713 714                # STARK_L regime weight (bear regime)715                vote += (stark_long * 0.05 * rw(1.0, "stark_l"))716 717            elif echo_short < 0:718                vote -= 0.25 * rw(1.0, "echo")           # Base edge, regime-scaled719                vote -= (abs(frama_short) * 0.10)720                vote -= (abs(frama_long)  * 0.05)721                vote -= (abs(cairo)       * 0.15 * rw(1.0, "cairo"))722                vote += (ridge_score      * 0.10 * rw(1.0, "ridge"))723                vote -= (spectra_score    * 0.10 * rw(1.0, "spectra"))724                vote += (gap_power        * 0.15 * rw(1.0, "gap"))   # Gap preserves sign725 726                # Penalty if counter-trend to Higher Timeframe727                if htf_trend > 0: vote += 0.20728 729                flux_scaled = rw(flux, "flux")730                if flux_scaled < 0.3: vote -= 0.15731                elif flux_scaled > 0.7: vote += 0.15732 733                # Additional regime contributions from new features734                vote -= (vamp_score * 0.08)   # Bearish VAMP strengthens short735                vote -= (bbp_score  * 0.06)   # Bearish BBP strengthens short736 737                vote += (stark_long * 0.05 * rw(1.0, "stark_l"))738 739            # Apply VIX Penalty — scaled by regime weight in bear (vix × 1.5)740            vix_effective = vix_proxy * rw(1.0, "vix")741            vix_penalty = 1.0 - (vix_effective * 0.5)742            vote *= vix_penalty743 744            vote *= stark_short745            vote *= noise_weight746 747            final_vote = float(max(-1.0, min(1.0, vote)))748 749            regime_tag = f"RG:{regime_gate}" if regime_gate is not None else "RG:AUTO"750            viz = (751                f"ECHO_S:{echo_short:.1f} | FRAMA:{frama_long:.2f} | "752                f"GAP:{gap_power:.2f} | VIX:{vix_proxy:.2f} | "753                f"VAMP:{vamp_score:.2f} | BBP:{bbp_score:.2f} | {regime_tag}"754            )755 756            return final_vote, str(viz), atr_val, tensor_dict757 758        except Exception as e:759            return 0.0, "ERROR", 0.01, {}760