diff --git a/bot/strategy/bayesian.py b/bot/strategy/bayesian.py index ab4b2a6..a20d08b 100644 --- a/bot/strategy/bayesian.py +++ b/bot/strategy/bayesian.py @@ -483,16 +483,21 @@ class BayesianStrategy: _fg_contribution = fg_adj if is_price_above else -fg_adj adjustments.append(_fg_contribution) - # Signal 3: BTC dominance — hurts altcoins when high + # Signal 3: BTC dominance — hurts altcoins when high (price markets only) + # Like momentum and Fear & Greed above: no demonstrated causality for + # politics/tech/events, even when they legitimately mention a ticker + # ("Will the ETH ETF be approved?"). For non-price markets the + # contribution stays 0.0 → feat_btc_dom_lo = 0.0. _btc_dom_contribution = 0.0 - if (is_eth or is_altcoin or is_general_crypto) and ext.btc_dominance > 55: - _btc_dom_contribution = -0.03 if is_price_above else 0.03 - adjustments.append(_btc_dom_contribution) - sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (high → alt pressure)") - elif (is_eth or is_altcoin or is_general_crypto) and ext.btc_dominance < 45: - _btc_dom_contribution = 0.03 if is_price_above else -0.03 - adjustments.append(_btc_dom_contribution) - sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (low → alt season)") + if not is_non_price: + if (is_eth or is_altcoin or is_general_crypto) and ext.btc_dominance > 55: + _btc_dom_contribution = -0.03 if is_price_above else 0.03 + adjustments.append(_btc_dom_contribution) + sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (high → alt pressure)") + elif (is_eth or is_altcoin or is_general_crypto) and ext.btc_dominance < 45: + _btc_dom_contribution = 0.03 if is_price_above else -0.03 + adjustments.append(_btc_dom_contribution) + sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (low → alt season)") # Signal 4: GNews sentiment (politics only, budget-gated) # Phase 3: caller has pre-sorted markets by gnews_priority() so the diff --git a/tests/test_bayesian_btc_dom_gate.py b/tests/test_bayesian_btc_dom_gate.py new file mode 100644 index 0000000..32974d3 --- /dev/null +++ b/tests/test_bayesian_btc_dom_gate.py @@ -0,0 +1,130 @@ +""" +Tests for FASE 5 — BTC-dominance signal must not apply to non-price markets. + +FASE 3 gated momentum and Fear & Greed behind is_non_price (politics / tech / +events); FASE 4 fixed ticker detection so non-crypto questions no longer flag +crypto assets by accident. But a non-price market that LEGITIMATELY mentions +a ticker ("Will the ETH ETF be approved?") still armed the BTC-dominance +signal, which has no demonstrated causality for non-price outcomes. FASE 5 +applies the same is_non_price gate to that signal. + +Note: the dominance signal only fires for is_eth / is_altcoin / +is_general_crypto markets — a pure-BTC question never receives it, so the +pro-Bitcoin test below is a regression guard rather than a gate exercise; +the ETH-ETF test is the one that fails without the gate. + +Same caplog technique as test_bayesian_asset_detection.py: btc_dom_lo is +parsed from the structured audit log, with btc_dominance=65 (>55) so the +signal fires whenever it is allowed to. +""" +import asyncio +import logging +import re + +import pytest + +from bot.data.external import ExternalSignals +from bot.data.polymarket import Market +from bot.strategy.bayesian import BayesianStrategy + +BTC_DOM_RE = re.compile(r"btc_dom_lo=([+-]\d+\.\d+)") + + +def _make_market(question: str, category: str) -> Market: + return Market( + id="mkt-test-1", + condition_id="cond-test-1", + question=question, + yes_token_id="yes-tok", + no_token_id="no-tok", + yes_price=0.50, + no_price=0.50, + volume_24h=50_000.0, + end_date="2026-07-15T00:00:00Z", + active=True, + category=category, + ) + + +def _make_signals() -> ExternalSignals: + # btc_dominance=65 (>55) arms the dominance signal wherever it is allowed. + # Momentum kept below the 2% threshold so price-market tests isolate the + # dominance contribution. + return ExternalSignals( + btc_price=100_000.0, + btc_change_24h=1.0, + eth_price=4_000.0, + eth_change_24h=1.0, + btc_dominance=65.0, + fear_greed_index=50, + fear_greed_label="neutral", + total_market_cap_change=1.0, + valid=True, + ) + + +def _evaluate(question: str, category: str, caplog) -> tuple[float, str]: + """Run evaluate() and return (btc_dom_lo, full_log) from the audit log.""" + strategy = BayesianStrategy(news=None, manifold=None, db=None) + market = _make_market(question, category) + with caplog.at_level(logging.INFO, logger="bot.strategy.bayesian"): + asyncio.run( + strategy.evaluate(market, _make_signals(), occupied_families=set()) + ) + full_log = "\n".join(r.getMessage() for r in caplog.records) + for record in caplog.records: + m = BTC_DOM_RE.search(record.getMessage()) + if m: + return float(m.group(1)), full_log + pytest.fail( + "No SKIP_EDGE_NET/TRADE log line with btc_dom_lo found; " + f"got: {[r.getMessage() for r in caplog.records]}" + ) + + +# ── Non-price markets: gate must zero the signal ───────────────────────────── + +def test_politics_market_mentioning_eth_gets_no_btc_dom(caplog): + """Legitimate ETH mention in a politics market → btc_dom_lo == 0.0.""" + btc_dom_lo, full_log = _evaluate( + "Will the ETH ETF be approved?", "politics", caplog + ) + assert btc_dom_lo == 0.0 + assert "BTC dom" not in full_log + +def test_politics_market_mentioning_bitcoin_gets_no_btc_dom(caplog): + """Legitimate Bitcoin mention in a politics market → btc_dom_lo == 0.0.""" + btc_dom_lo, full_log = _evaluate( + "Will a pro-Bitcoin candidate win the election?", "politics", caplog + ) + assert btc_dom_lo == 0.0 + assert "BTC dom" not in full_log + +def test_tech_and_events_markets_get_no_btc_dom(caplog): + for category in ("tech", "events"): + caplog.clear() + btc_dom_lo, full_log = _evaluate( + "Will the ETH foundation launch the product?", category, caplog + ) + assert btc_dom_lo == 0.0, f"BTC dominance applied to {category} market" + assert "BTC dom" not in full_log + + +# ── Price markets: current behavior preserved ─────────────────────────────── + +def test_eth_price_market_keeps_btc_dom(caplog): + """ETH price market with dominance 65 → signal fires as before.""" + btc_dom_lo, full_log = _evaluate( + "Will ETH be above $5000?", "crypto/finance", caplog + ) + # 'above' → is_price_above, dominance 65 > 55 → -0.03 → -0.06 log-odds + assert btc_dom_lo == pytest.approx(-0.06, abs=1e-4) + assert "BTC dom: 65.0% (high → alt pressure)" in full_log + +def test_altcoin_price_market_keeps_btc_dom(caplog): + """SOL price market with dominance 65 → signal fires as before.""" + btc_dom_lo, full_log = _evaluate( + "Will SOL reach $200?", "crypto/finance", caplog + ) + assert btc_dom_lo == pytest.approx(-0.06, abs=1e-4) + assert "BTC dom: 65.0% (high → alt pressure)" in full_log