Spaces:
Sleeping
Sleeping
| """ | |
| Bayesian Gap Prediction Engine | |
| βββββββββββββββββββββββββββββββ | |
| Applies Bayes' theorem to iteratively update gap direction probabilities | |
| as new evidence arrives throughout the week. | |
| P(Gap_Up | Evidence) β P(Evidence | Gap_Up) Γ P(Gap_Up) | |
| Evidence sources (each contributes a likelihood ratio): | |
| 1. Multi-timeframe momentum (40m, 1D, 3D) | |
| 2. RSI regime (overbought/oversold) | |
| 3. COT institutional positioning | |
| 4. News sentiment flow | |
| 5. Agent consensus (weighted by track record) | |
| 6. Historical gap seasonality (day-of-year patterns) | |
| 7. Volatility regime (ATR expansion/contraction) | |
| 8. Upcoming high-impact events (risk premium adjustment) | |
| Priors are learned from historical trade outcomes stored in the database. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import math | |
| from dataclasses import dataclass, field | |
| from datetime import datetime, timezone | |
| from typing import Any | |
| logger = logging.getLogger("gap_system.analysis.bayesian") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BAYESIAN STATE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class BayesianState: | |
| """ | |
| Tracks the running posterior probability for a single asset. | |
| Updated incrementally as new evidence arrives. | |
| """ | |
| asset: str | |
| # Log-odds representation (numerically stable for repeated updates) | |
| log_odds_bullish: float = 0.0 # 0.0 = 50/50 prior | |
| # Track which evidence has been applied this cycle | |
| evidence_applied: list[str] = field(default_factory=list) | |
| # Historical prior calibration | |
| historical_bull_rate: float = 0.50 # Updated from DB | |
| total_historical_gaps: int = 0 | |
| # Cycle tracking | |
| update_count: int = 0 | |
| last_updated: str = "" | |
| def probability_bullish(self) -> float: | |
| """Convert log-odds to probability.""" | |
| return 1.0 / (1.0 + math.exp(-self.log_odds_bullish)) | |
| def probability_bearish(self) -> float: | |
| return 1.0 - self.probability_bullish | |
| def direction(self) -> str: | |
| p = self.probability_bullish | |
| if p > 0.52: | |
| return "BULLISH" | |
| elif p < 0.48: | |
| return "BEARISH" | |
| return "NEUTRAL" | |
| def confidence(self) -> float: | |
| """Confidence = distance from 50%, scaled to [0, 1].""" | |
| return abs(self.probability_bullish - 0.50) * 2.0 | |
| def apply_evidence(self, name: str, likelihood_ratio: float) -> None: | |
| """ | |
| Update posterior using a likelihood ratio (Bayes factor). | |
| LR > 1.0 β evidence supports BULLISH | |
| LR < 1.0 β evidence supports BEARISH | |
| LR = 1.0 β evidence is uninformative | |
| log_odds += log(LR) is the Bayesian update rule. | |
| """ | |
| if likelihood_ratio <= 0: | |
| logger.warning("Invalid likelihood ratio %.4f for %s β skipping", likelihood_ratio, name) | |
| return | |
| log_lr = math.log(likelihood_ratio) | |
| # Damping: cap the maximum influence of any single evidence source | |
| max_log_shift = 1.2 # ~3.3x odds shift max per evidence | |
| log_lr = max(-max_log_shift, min(max_log_shift, log_lr)) | |
| old_p = self.probability_bullish | |
| self.log_odds_bullish += log_lr | |
| new_p = self.probability_bullish | |
| self.evidence_applied.append(name) | |
| self.update_count += 1 | |
| self.last_updated = datetime.now(timezone.utc).isoformat() | |
| logger.info( | |
| "Bayes [%s] %s: LR=%.3f β P(Bull) %.1f%% β %.1f%% (Ξ%+.1f%%)", | |
| self.asset, name, likelihood_ratio, | |
| old_p * 100, new_p * 100, (new_p - old_p) * 100, | |
| ) | |
| def reset_for_new_cycle(self) -> None: | |
| """Reset to prior for a new weekly cycle.""" | |
| # Use historical prior if available, else 50/50 | |
| if self.total_historical_gaps >= 5: | |
| prior_p = max(0.20, min(0.80, self.historical_bull_rate)) | |
| else: | |
| prior_p = 0.50 | |
| self.log_odds_bullish = math.log(prior_p / (1.0 - prior_p)) | |
| self.evidence_applied.clear() | |
| self.update_count = 0 | |
| logger.info( | |
| "Bayes [%s] reset. Prior: %.1f%% (from %d historical gaps)", | |
| self.asset, prior_p * 100, self.total_historical_gaps, | |
| ) | |
| def to_dict(self) -> dict: | |
| return { | |
| "asset": self.asset, | |
| "probability_bullish": round(self.probability_bullish, 4), | |
| "probability_bearish": round(self.probability_bearish, 4), | |
| "direction": self.direction, | |
| "confidence": round(self.confidence, 4), | |
| "log_odds": round(self.log_odds_bullish, 4), | |
| "evidence_applied": list(self.evidence_applied), | |
| "update_count": self.update_count, | |
| "historical_prior": round(self.historical_bull_rate, 4), | |
| "historical_samples": self.total_historical_gaps, | |
| "last_updated": self.last_updated, | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GLOBAL STATE REGISTRY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _states: dict[str, BayesianState] = {} | |
| def get_state(asset: str) -> BayesianState: | |
| if asset not in _states: | |
| _states[asset] = BayesianState(asset=asset) | |
| return _states[asset] | |
| def get_all_states() -> dict[str, dict]: | |
| return {asset: state.to_dict() for asset, state in _states.items()} | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EVIDENCE FUNCTIONS β Each returns a likelihood ratio | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _lr_momentum(fallback_metrics: dict) -> float: | |
| """ | |
| Multi-timeframe momentum. | |
| Score ranges from -1 (strong bear) to +1 (strong bull). | |
| Convert to likelihood ratio: LR = exp(score * strength). | |
| """ | |
| score = fallback_metrics.get("momentum_score", 0) | |
| # Convert [-1, 1] score to LR | |
| # score=+1.0 β LRβ2.7 (strong bull evidence) | |
| # score=-1.0 β LRβ0.37 (strong bear evidence) | |
| # score=0.0 β LR=1.0 (uninformative) | |
| return math.exp(score * 1.0) | |
| def _lr_rsi(ta_4h: dict) -> float: | |
| """ | |
| RSI regime β mean-reverting signal. | |
| Extreme RSI is contrarian evidence for gaps (weekend reversion). | |
| """ | |
| rsi = ta_4h.get("rsi", 50) | |
| if rsi > 75: | |
| return 0.65 # Overbought β bear evidence for gap | |
| elif rsi > 65: | |
| return 0.85 # Mildly overbought | |
| elif rsi < 25: | |
| return 1.55 # Oversold β bull evidence for gap | |
| elif rsi < 35: | |
| return 1.20 # Mildly oversold | |
| return 1.0 # Neutral | |
| def _lr_cot(cot_data: dict | None) -> float: | |
| """ | |
| COT institutional positioning β leading indicator. | |
| Net long = institutions expect higher = bullish evidence. | |
| Extreme positioning is CONTRARIAN (crowded trades reverse). | |
| """ | |
| if not cot_data: | |
| return 1.0 | |
| bias = cot_data.get("bias", "NEUTRAL") | |
| extreme = cot_data.get("extreme_flag", False) | |
| if extreme: | |
| # CONTRARIAN: extreme long = bearish signal | |
| if bias == "BULLISH": | |
| return 0.70 # Crowded long β expect reversal down | |
| elif bias == "BEARISH": | |
| return 1.40 # Crowded short β expect squeeze up | |
| else: | |
| # Non-extreme: follow the institutions | |
| if bias == "BULLISH": | |
| return 1.20 | |
| elif bias == "BEARISH": | |
| return 0.85 | |
| return 1.0 | |
| def _lr_news_sentiment(news_data: list[dict]) -> float: | |
| """ | |
| News sentiment flow β aggregate positive/negative tilt. | |
| Returns LR based on sentiment balance. | |
| """ | |
| if not news_data: | |
| return 1.0 | |
| positive = sum(1 for n in news_data if str(n.get("sentiment", "")).startswith("positive")) | |
| negative = sum(1 for n in news_data if str(n.get("sentiment", "")).startswith("negative")) | |
| total = positive + negative | |
| if total < 3: | |
| return 1.0 # Not enough data | |
| ratio = positive / total # 0 to 1 | |
| # ratio=0.8 β LRβ1.5 (clear positive sentiment) | |
| # ratio=0.2 β LRβ0.67 (clear negative sentiment) | |
| # ratio=0.5 β LR=1.0 (balanced) | |
| return math.exp((ratio - 0.5) * 1.5) | |
| def _lr_agent_consensus(agents: list, agent_confidence: float, direction: str) -> float: | |
| """ | |
| Agent consensus β the multi-platform debate result. | |
| Strong consensus is strong evidence. Split votes are weak. | |
| """ | |
| if not agents: | |
| return 1.0 | |
| valid = [a for a in agents if getattr(a, 'last_cycle_success', True)] | |
| if not valid: | |
| return 1.0 | |
| bull = sum(1 for a in valid if a.current_stance == "BULLISH") | |
| bear = sum(1 for a in valid if a.current_stance == "BEARISH") | |
| total = bull + bear | |
| if total == 0: | |
| return 1.0 | |
| # Consensus strength: how lopsided is the vote? | |
| consensus_ratio = max(bull, bear) / total | |
| # Agent confidence modulates the strength | |
| strength = (consensus_ratio - 0.5) * 2.0 * agent_confidence # 0 to 1 | |
| # Sign: positive if majority is BULLISH, negative if BEARISH | |
| sign = 1.0 if bull > bear else -1.0 | |
| return math.exp(sign * strength * 0.8) | |
| def _lr_technical_confluence(technical_summary: dict | None) -> float: | |
| """ | |
| Multi-timeframe technical confluence. | |
| All timeframes aligned = strong directional evidence. | |
| """ | |
| if not technical_summary: | |
| return 1.0 | |
| bias = technical_summary.get("weighted_bias", "NEUTRAL") | |
| confluence = technical_summary.get("confluence_score", 0.5) | |
| if bias == "NEUTRAL" or bias == "RANGING": | |
| return 1.0 | |
| # Higher confluence = stronger evidence | |
| strength = (confluence - 0.4) * 2.5 # 0 at 0.4, 1.5 at 1.0 | |
| strength = max(0, min(1.2, strength)) | |
| sign = 1.0 if bias == "BULLISH" else -1.0 | |
| return math.exp(sign * strength * 0.7) | |
| def _lr_volatility_regime(fallback_metrics: dict) -> float: | |
| """ | |
| Volatility regime β modifies gap predictability. | |
| High vol = larger gaps but less directional predictability. | |
| Returns a dampening factor (closer to 1.0 in high vol). | |
| """ | |
| regime = fallback_metrics.get("volatility_regime", "NORMAL") | |
| if regime == "HIGH": | |
| # In high vol, all other evidence is less reliable | |
| # We don't provide directional evidence, just reduce conviction | |
| return 1.0 # Neutral β but other LRs are dampened by the engine | |
| return 1.0 # Normal/Low vol doesn't modify | |
| def _lr_structural_breakout(fallback_metrics: dict) -> float: | |
| """ | |
| Structural breakout/breakdown β strong directional evidence. | |
| Price above prev week high or below prev week low. | |
| """ | |
| if fallback_metrics.get("above_prev_week_high", False): | |
| return 1.6 # Strong bullish breakout | |
| elif fallback_metrics.get("below_prev_week_low", False): | |
| return 0.65 # Strong bearish breakdown | |
| return 1.0 | |
| def _lr_upcoming_events(event_risk_score: float) -> float: | |
| """ | |
| Upcoming high-impact event risk β DAMPENER. | |
| When major events are imminent, directional conviction should decrease. | |
| This pulls all predictions toward 50/50 (LR β 1.0). | |
| event_risk_score: 0.0 (no events) to 1.0 (major event imminent) | |
| """ | |
| if event_risk_score <= 0.1: | |
| return 1.0 # No dampening | |
| # At risk=0.5 β LRβ0.85 (mild dampening) | |
| # At risk=1.0 β LRβ0.6 (strong dampening toward uncertainty) | |
| return 1.0 - (event_risk_score * 0.4) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MAIN UPDATE FUNCTION β Called each debate cycle | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def bayesian_update( | |
| asset: str, | |
| agents: list, | |
| technical_summary: dict | None = None, | |
| cot_data: dict | None = None, | |
| news_data: list[dict] | None = None, | |
| agent_confidence: float = 0.5, | |
| agent_direction: str = "NEUTRAL", | |
| event_risk_score: float = 0.0, | |
| ) -> dict: | |
| """ | |
| Run a full Bayesian update cycle for an asset. | |
| Each evidence source contributes a likelihood ratio. | |
| The posterior accumulates across cycles within the same week, | |
| implementing true Bayesian learning. | |
| Returns the current Bayesian state as a dict. | |
| """ | |
| state = get_state(asset) | |
| # Extract sub-data | |
| fallback = {} | |
| ta_4h = {} | |
| if technical_summary: | |
| fallback = technical_summary.get("fallback_metrics", {}) | |
| ta_4h = technical_summary.get("4H", {}) | |
| # ββ Apply each evidence source ββ | |
| # 1. Multi-timeframe momentum | |
| if fallback: | |
| lr = _lr_momentum(fallback) | |
| state.apply_evidence("momentum", lr) | |
| # 2. RSI regime | |
| if ta_4h: | |
| lr = _lr_rsi(ta_4h) | |
| state.apply_evidence("rsi_regime", lr) | |
| # 3. COT positioning | |
| lr = _lr_cot(cot_data) | |
| if lr != 1.0: | |
| state.apply_evidence("cot_positioning", lr) | |
| # 4. News sentiment | |
| if news_data: | |
| lr = _lr_news_sentiment(news_data) | |
| if abs(lr - 1.0) > 0.05: # Only apply if meaningfully different | |
| state.apply_evidence("news_sentiment", lr) | |
| # 5. Agent consensus (the multi-platform AI debate) | |
| if agents: | |
| lr = _lr_agent_consensus(agents, agent_confidence, agent_direction) | |
| state.apply_evidence("agent_consensus", lr) | |
| # 6. Technical confluence | |
| lr = _lr_technical_confluence(technical_summary) | |
| if lr != 1.0: | |
| state.apply_evidence("technical_confluence", lr) | |
| # 7. Structural breakout | |
| if fallback: | |
| lr = _lr_structural_breakout(fallback) | |
| if lr != 1.0: | |
| state.apply_evidence("structural_breakout", lr) | |
| # 8. Event risk dampening (reduces overall confidence) | |
| if event_risk_score > 0.2: | |
| # Dampen by pulling log-odds toward 0 (50/50) | |
| dampen_factor = 1.0 - (event_risk_score * 0.3) # At max risk, retain 70% | |
| state.log_odds_bullish *= dampen_factor | |
| state.evidence_applied.append(f"event_risk_dampen({event_risk_score:.2f})") | |
| logger.info( | |
| "Bayes [%s] Event risk dampening: %.0f%% β P(Bull) moved toward 50%%", | |
| asset, event_risk_score * 100, | |
| ) | |
| # 9. Volatility regime dampening | |
| if fallback.get("volatility_regime") == "HIGH": | |
| state.log_odds_bullish *= 0.85 | |
| state.evidence_applied.append("high_vol_dampen") | |
| logger.info("Bayes [%s] High volatility dampening applied", asset) | |
| logger.info( | |
| "Bayes [%s] After update #%d: %s %.1f%% confidence (P_bull=%.1f%%, %d evidence sources)", | |
| asset, state.update_count, state.direction, | |
| state.confidence * 100, state.probability_bullish * 100, | |
| len(state.evidence_applied), | |
| ) | |
| return state.to_dict() | |
| def load_historical_priors(trade_history: list[dict]) -> None: | |
| """ | |
| Load historical gap outcomes from the database to calibrate priors. | |
| Called once at startup. | |
| """ | |
| # Group by asset | |
| asset_outcomes: dict[str, list[bool]] = {} | |
| for trade in trade_history: | |
| asset = trade.get("asset", "") | |
| direction = trade.get("direction", "") | |
| profit = trade.get("profit_usd", 0) or 0 | |
| if not asset or not direction: | |
| continue | |
| # A "bullish gap" = gap opened higher than Friday close | |
| # We track whether the BULLISH prediction was profitable | |
| was_bullish = direction == "BULLISH" | |
| was_correct = profit > 0 | |
| # If bullish and correct, gap went up. If bearish and correct, gap went down. | |
| gap_went_up = (was_bullish and was_correct) or (not was_bullish and not was_correct) | |
| asset_outcomes.setdefault(asset, []).append(gap_went_up) | |
| for asset, outcomes in asset_outcomes.items(): | |
| state = get_state(asset) | |
| state.total_historical_gaps = len(outcomes) | |
| state.historical_bull_rate = sum(outcomes) / len(outcomes) | |
| logger.info( | |
| "Historical prior for %s: %.1f%% bullish (%d samples)", | |
| asset, state.historical_bull_rate * 100, len(outcomes), | |
| ) | |
| def reset_all_for_new_week() -> None: | |
| """Reset all Bayesian states for a new trading week.""" | |
| for state in _states.values(): | |
| state.reset_for_new_cycle() | |
| logger.info("All Bayesian states reset for new week (%d assets)", len(_states)) | |