From f5ac302a86f7d2bce607249b4022ae0d45e4c106 Mon Sep 17 00:00:00 2001 From: chemavx Date: Thu, 11 Jun 2026 16:32:07 +0000 Subject: [PATCH] fix(strategy): use word-boundary token matching for short crypto tickers to prevent false positives Substring matching over question_lower flagged non-crypto markets as crypto: 'dissolved' matched 'sol', 'Canada' matched 'ada', 'Seth' matched 'eth'. Those false flags armed the BTC-dominance signal (btc_dom_lo=+0.06 observed on politics markets in production). Short tickers (btc, eth, sol, xrp, doge, ltc, bnb, ada, avax) now go through has_token(), which requires non-alphanumeric boundaries so 'ETH', '$ETH' and 'ETH/USD' still match. Long unambiguous names (bitcoin, ethereum, solana, cardano, ...) remain substring checks. Co-Authored-By: Claude Fable 5 --- bot/strategy/bayesian.py | 33 ++++- tests/test_bayesian_asset_detection.py | 159 +++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 tests/test_bayesian_asset_detection.py diff --git a/bot/strategy/bayesian.py b/bot/strategy/bayesian.py index c4698c3..ab4b2a6 100644 --- a/bot/strategy/bayesian.py +++ b/bot/strategy/bayesian.py @@ -13,6 +13,7 @@ Polymarket might reflect in a slow-moving order book. import logging import math import os +import re import uuid from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone @@ -159,6 +160,21 @@ def _days_to_resolution(end_date: str) -> int: return 30 +def has_token(text: str, token: str) -> bool: + """ + True if `token` appears in `text` as a standalone word. + + Short crypto tickers (eth, sol, ada, …) must NOT match inside ordinary + words — "Seth", "dissolved", "Canada" — but must still match the usual + market phrasings: "ETH", "$ETH", "ETH/USD", "SOL reach $200". Boundaries + are any non-alphanumeric character (or start/end of string), so "$" and + "/" delimit correctly. + """ + return re.search( + rf"(? 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)