diff --git a/bot/strategy/bayesian.py b/bot/strategy/bayesian.py index a7e14ae..832120e 100644 --- a/bot/strategy/bayesian.py +++ b/bot/strategy/bayesian.py @@ -12,6 +12,7 @@ Polymarket might reflect in a slow-moving order book. """ import logging import math +import os import uuid from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone @@ -61,6 +62,27 @@ NEWS_LOGODDS_WEIGHT = 1.5 # Weaker than NEWS_LOGODDS_WEIGHT because Manifold can have illiquid/stale markets. MANIFOLD_LOGODDS_WEIGHT = 0.6 + +def _env_bool(name: str, default: bool) -> bool: + return os.getenv(name, str(default)).strip().lower() in ("1", "true", "yes", "on") + + +# ── Manifold activation flags ────────────────────────────────────────────────── +# Manifold has been retired as an ACTIVE trading signal: a per-category coverage +# audit (see /api/metrics/manifold-coverage) showed coverage_rate=0.0 across every +# category in the bot's current universe, so any edge it produced was false edge. +# +# MANIFOLD_SIGNAL_ENABLED (default False): when False, Manifold is observational +# only — its probability never touches the edge model: no manifold_log_adj, no +# confidence bump, feat_mfld_lo stays 0.0 (so it can never be the dominant +# feature), and it never contributes to a trade. +# MANIFOLD_AUDIT_ENABLED (default True): when True the matcher still runs and +# audit/coverage rows + cooldowns are written, preserving the trail so we can +# decide later whether to reactivate Manifold in a universe with real coverage. +# The matcher is only called when at least one flag is on. +MANIFOLD_SIGNAL_ENABLED = _env_bool("MANIFOLD_SIGNAL_ENABLED", False) +MANIFOLD_AUDIT_ENABLED = _env_bool("MANIFOLD_AUDIT_ENABLED", True) + # GNews free tier: 100 req/day. We limit to 5 queries per trading cycle # (politics markets only) and rely on 6 h cache to stay within budget. MAX_NEWS_QUERIES_PER_CYCLE = 5 @@ -471,7 +493,8 @@ class BayesianStrategy: manifold_result: Optional[ManifoldMatchResult] = None audit_id: Optional[str] = None - if (is_politics or is_tech) and self._manifold is not None: + if ((is_politics or is_tech) and self._manifold is not None + and (MANIFOLD_AUDIT_ENABLED or MANIFOLD_SIGNAL_ENABLED)): # ── Cooldown gate ──────────────────────────────────────────────── # Skip markets whose Manifold verdict was recently settled to a # stable value. A skip is equivalent to a no-signal: the matcher is @@ -497,8 +520,10 @@ class BayesianStrategy: if not in_cooldown: manifold_result = await self._manifold.get_match(market.question) - # Persist audit record for ALL outcomes (accepted / rejected / no_results) - if self._db is not None: + # Persist audit record for ALL outcomes (accepted / rejected / no_results). + # Gated by MANIFOLD_AUDIT_ENABLED so the audit/coverage trail and + # cooldowns can be kept even while Manifold is observational-only. + if MANIFOLD_AUDIT_ENABLED and self._db is not None: if not market.id: log.error( "MANIFOLD_AUDIT: market.id is None/empty — skipping audit save | " @@ -564,7 +589,10 @@ class BayesianStrategy: "reason": manifold_result.match_reason, }) - if manifold_result.status == "accepted" and manifold_result.prob_final is not None: + if (MANIFOLD_SIGNAL_ENABLED + and manifold_result.status == "accepted" + and manifold_result.prob_final is not None): + # ACTIVE signal path — only when explicitly enabled. manifold_used = True self._manifold_fetched += 1 m_clamped = max(0.05, min(0.95, manifold_result.prob_final)) @@ -572,6 +600,16 @@ class BayesianStrategy: p_log = math.log(prior / (1 - prior)) manifold_log_adj = (m_log - p_log) * MANIFOLD_LOGODDS_WEIGHT sources.append(f"Manifold:{manifold_result.prob_final:.2f}") + elif not MANIFOLD_SIGNAL_ENABLED: + # Observational-only: matched/audited but NEVER fed to the edge + # model. manifold_log_adj stays 0.0 → no confidence bump, + # feat_mfld_lo=0.0 (cannot be dominant), no trade contribution. + log.info( + "Manifold: observational_only — signal disabled " + "(MANIFOLD_SIGNAL_ENABLED=false) | market=%s status=%s", + market.id, manifold_result.status, + ) + sources.append("Manifold: observational_only") # Confidence cap: macro/politics/tech signals are weaker proxies confidence_cap = 0.65 if (is_macro or is_politics or is_tech or is_events) else 0.90 @@ -701,8 +739,12 @@ class BayesianStrategy: feat_news_lo=feat_news_lo, feat_mfld_lo=feat_mfld_lo, feat_btc_dom_lo=feat_btc_dom_lo, - # Manifold match audit — propagated through Order → Trade → DB - mfld_audit_id=audit_id, + # Manifold match audit — propagated through Order → Trade → DB. + # mfld_audit_id is the hook main.py uses to flip the audit row's + # used_in_trade=TRUE; suppress it when observational so the trail + # truthfully shows Manifold drove no trades. The mfld_* fields below + # stay as observational record (feat_mfld_lo is already 0.0). + mfld_audit_id=(audit_id if MANIFOLD_SIGNAL_ENABLED else None), mfld_market_id=manifold_result.market_id if manifold_result else None, mfld_market_title=manifold_result.market_title if manifold_result else None, mfld_market_url=manifold_result.market_url if manifold_result else None,