""" Tests for FASE 4 — crypto ticker detection must use word boundaries. Regression: short tickers were detected with substring matching over question_lower, so non-crypto markets triggered crypto flags: "Israeli parliament dissolved" contains "sol" → is_sol / is_altcoin "Will Canada win Group B" contains "ada" → is_altcoin "Will Seth Moulton be the nominee" contains "eth" → is_eth Those flags armed the BTC-dominance signal (btc_dom_lo=+0.06 observed in production on politics markets). The fix routes short tickers (btc, eth, sol, xrp, doge, ltc, bnb, ada, avax) through has_token(), which requires non-alphanumeric boundaries; long unambiguous names (bitcoin, ethereum, solana, cardano, …) remain substrings. The is_* flags are internal to evaluate(), so the integration tests assert on btc_dom_lo parsed from the structured audit log (same technique as test_bayesian_macro_signals.py), with btc_dominance=60 so the signal fires whenever an ETH/altcoin flag is set. """ 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, has_token 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=60 (>55) arms the BTC-dominance signal for any market # flagged as ETH / altcoin / general-crypto. return ExternalSignals( btc_price=100_000.0, btc_change_24h=10.0, eth_price=4_000.0, eth_change_24h=8.0, btc_dominance=60.0, fear_greed_index=80, fear_greed_label="greed", total_market_cap_change=5.0, valid=True, ) def _evaluate_and_parse_btc_dom(question: str, category: str, caplog) -> float: """Run BayesianStrategy.evaluate and return btc_dom_lo 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()) ) for record in caplog.records: m = BTC_DOM_RE.search(record.getMessage()) if m: return float(m.group(1)) pytest.fail( "No SKIP_EDGE_NET/TRADE log line with btc_dom_lo found; " f"got: {[r.getMessage() for r in caplog.records]}" ) # ── has_token unit tests ───────────────────────────────────────────────────── def test_has_token_rejects_substrings_inside_words(): assert has_token("israeli parliament dissolved by june 30?", "sol") is False assert has_token("will canada win group b?", "ada") is False assert has_token("will seth moulton be the nominee?", "eth") is False def test_has_token_matches_common_market_formats(): assert has_token("will eth hit $5000?", "eth") is True assert has_token("$eth above $5000?", "eth") is True assert has_token("eth/usd above 5000?", "eth") is True assert has_token("will sol reach $200?", "sol") is True assert has_token("will ada reach $1?", "ada") is True assert has_token("BTC to $150k?", "btc") is True # case-insensitive # ── Regression: false positives must not arm the BTC-dominance signal ─────── def test_israeli_parliament_market_is_not_sol(caplog): """'dissolved' contains 'sol' — must NOT flag is_sol/is_altcoin.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Israeli parliament dissolved by June 30?", "politics", caplog ) assert btc_dom_lo == 0.0 def test_canada_market_is_not_ada(caplog): """'Canada' contains 'ada' — must NOT flag is_altcoin.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Will Canada win Group B?", "events", caplog ) assert btc_dom_lo == 0.0 def test_seth_moulton_market_is_not_eth(caplog): """'Seth' contains 'eth' — must NOT flag is_eth.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Will Seth Moulton be the nominee?", "politics", caplog ) assert btc_dom_lo == 0.0 # ── Real ticker mentions must keep working ─────────────────────────────────── def test_eth_market_detected(caplog): """Standalone 'ETH' still flags is_eth: BTC-dom fires and momentum uses ETH.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Will ETH hit $5000?", "crypto/finance", caplog ) assert btc_dom_lo != 0.0 # Momentum picks the ETH branch only when is_eth is True. full_log = "\n".join(r.getMessage() for r in caplog.records) assert "ETH 24h: +8.0%" in full_log def test_dollar_eth_market_detected(caplog): """'$ETH' format still flags is_eth.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "$ETH above $5000?", "crypto/finance", caplog ) assert btc_dom_lo != 0.0 full_log = "\n".join(r.getMessage() for r in caplog.records) assert "ETH 24h: +8.0%" in full_log def test_sol_market_detected(caplog): """'SOL reach $200' still flags is_sol → is_altcoin → BTC-dom signal.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Will SOL reach $200?", "crypto/finance", caplog ) # 'reach' → is_price_above, dominance 60 → -0.03 contribution → -0.06 log-odds assert btc_dom_lo == pytest.approx(-0.06, abs=1e-4) def test_ada_market_detected(caplog): """'ADA reach $1' still flags is_altcoin.""" btc_dom_lo = _evaluate_and_parse_btc_dom( "Will ADA reach $1?", "crypto/finance", caplog ) assert btc_dom_lo == pytest.approx(-0.06, abs=1e-4)