shaibu01/Titan-Engine
0
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 