feat(manifold): decouple Manifold from edge model (observational-only)
CI/CD / build-and-push (push) Successful in 8s

Per-category coverage audit showed coverage_rate=0.0 across every category in the
bot's current universe, so any edge Manifold produced was false edge. Retire it as
an ACTIVE trading signal while keeping the full audit/coverage/cooldown trail for a
future reactivation decision.

Two module-level flags in bayesian.py (read from env):
- MANIFOLD_SIGNAL_ENABLED (default False): when False, Manifold never touches the
  edge model — manifold_log_adj stays 0.0 (no posterior shift), no confidence bump,
  feat_mfld_lo=0.0 (so it can never be the dominant feature), no trade contribution,
  and mfld_audit_id is not propagated so the audit's used_in_trade stays FALSE.
- MANIFOLD_AUDIT_ENABLED (default True): matcher still runs; audit/coverage rows and
  cooldowns are still written. The matcher is only called when a flag is on.

When signal is disabled, logs and reasoning carry "Manifold: observational_only".
Endpoints /api/metrics/manifold-matches and /api/metrics/manifold-coverage,
cooldowns, audit tables and existing trades are unchanged. No code or tables removed.

Other signals, thresholds, exposure, risk manager and the executor are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-06-08 15:58:02 +00:00
co-authored by Claude Opus 4.8
parent 823914789d
commit 3a353c7e5b
+48 -6
View File
@@ -12,6 +12,7 @@ Polymarket might reflect in a slow-moving order book.
""" """
import logging import logging
import math import math
import os
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone 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. # Weaker than NEWS_LOGODDS_WEIGHT because Manifold can have illiquid/stale markets.
MANIFOLD_LOGODDS_WEIGHT = 0.6 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 # 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. # (politics markets only) and rely on 6 h cache to stay within budget.
MAX_NEWS_QUERIES_PER_CYCLE = 5 MAX_NEWS_QUERIES_PER_CYCLE = 5
@@ -471,7 +493,8 @@ class BayesianStrategy:
manifold_result: Optional[ManifoldMatchResult] = None manifold_result: Optional[ManifoldMatchResult] = None
audit_id: Optional[str] = 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 ──────────────────────────────────────────────── # ── Cooldown gate ────────────────────────────────────────────────
# Skip markets whose Manifold verdict was recently settled to a # Skip markets whose Manifold verdict was recently settled to a
# stable value. A skip is equivalent to a no-signal: the matcher is # stable value. A skip is equivalent to a no-signal: the matcher is
@@ -497,8 +520,10 @@ class BayesianStrategy:
if not in_cooldown: if not in_cooldown:
manifold_result = await self._manifold.get_match(market.question) manifold_result = await self._manifold.get_match(market.question)
# Persist audit record for ALL outcomes (accepted / rejected / no_results) # Persist audit record for ALL outcomes (accepted / rejected / no_results).
if self._db is not None: # 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: if not market.id:
log.error( log.error(
"MANIFOLD_AUDIT: market.id is None/empty — skipping audit save | " "MANIFOLD_AUDIT: market.id is None/empty — skipping audit save | "
@@ -564,7 +589,10 @@ class BayesianStrategy:
"reason": manifold_result.match_reason, "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 manifold_used = True
self._manifold_fetched += 1 self._manifold_fetched += 1
m_clamped = max(0.05, min(0.95, manifold_result.prob_final)) 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)) p_log = math.log(prior / (1 - prior))
manifold_log_adj = (m_log - p_log) * MANIFOLD_LOGODDS_WEIGHT manifold_log_adj = (m_log - p_log) * MANIFOLD_LOGODDS_WEIGHT
sources.append(f"Manifold:{manifold_result.prob_final:.2f}") 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: 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 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_news_lo=feat_news_lo,
feat_mfld_lo=feat_mfld_lo, feat_mfld_lo=feat_mfld_lo,
feat_btc_dom_lo=feat_btc_dom_lo, feat_btc_dom_lo=feat_btc_dom_lo,
# Manifold match audit — propagated through Order → Trade → DB # Manifold match audit — propagated through Order → Trade → DB.
mfld_audit_id=audit_id, # 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_id=manifold_result.market_id if manifold_result else None,
mfld_market_title=manifold_result.market_title 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, mfld_market_url=manifold_result.market_url if manifold_result else None,