fix(strategy): exclude momentum and fear-greed signals from non-price markets (politics/tech/events)
CI/CD / build-and-push (push) Successful in 7s

For politics/tech/events markets there is no above/below price notion, so
is_price_above defaulted to False (or flipped on accidental wording like
"reach") and sign-inverted the macro adjustments: BTC +5% or high Fear&Greed
subtracted probability from YES on "Will X win the election?" markets.

Skip both signals entirely for non-price markets: contributions stay 0.0,
feat_mom_lo / feat_fg_lo persist as 0.0. Price markets (BTC/ETH/crypto)
keep the exact current behavior, including the below-market sign flip.
Removes the now-dead BTC(sentiment) momentum branch and its 0.5 attenuator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-06-11 15:40:28 +00:00
co-authored by Claude Fable 5
parent 96f31acf16
commit 4002f03d0c
2 changed files with 162 additions and 33 deletions
+39 -33
View File
@@ -419,42 +419,48 @@ class BayesianStrategy:
sources: list[str] = [f"Prior=poly({prior:.3f})"] sources: list[str] = [f"Prior=poly({prior:.3f})"]
adjustments: list[float] = [] adjustments: list[float] = []
# Signal 1: price momentum (asset-specific or BTC as sentiment proxy) # Momentum and Fear & Greed only make sense for price markets, where
if is_btc: # is_price_above gives the adjustment a meaningful sign. For
momentum = ext.btc_change_24h # politics/tech/events there is no above/below notion — is_price_above
asset_label = "BTC" # defaults to False (or flips on accidental wording like "reach"), so
elif is_eth: # applying these signals just injected sign noise. Skip them entirely;
momentum = ext.eth_change_24h # their contributions stay 0.0 → feat_mom_lo / feat_fg_lo = 0.0.
asset_label = "ETH" is_non_price = is_politics or is_tech or is_events
elif is_politics or is_tech or is_events:
momentum = ext.btc_change_24h
asset_label = "BTC(sentiment)"
else:
momentum = ext.total_market_cap_change
asset_label = "total mktcap"
# Signal 1: price momentum (asset-specific; price markets only)
_momentum_contribution = 0.0 _momentum_contribution = 0.0
if abs(momentum) > 2: if not is_non_price:
momentum_adj = math.tanh(momentum / 20) * 0.15 if is_btc:
if is_politics or is_tech or is_events: momentum = ext.btc_change_24h
momentum_adj *= 0.5 asset_label = "BTC"
_momentum_contribution = momentum_adj if is_price_above else -momentum_adj elif is_eth:
adjustments.append(_momentum_contribution) momentum = ext.eth_change_24h
sources.append(f"{asset_label} 24h: {momentum:+.1f}%") asset_label = "ETH"
else:
momentum = ext.total_market_cap_change
asset_label = "total mktcap"
# Signal 2: Fear & Greed if abs(momentum) > 2:
fg = ext.fear_greed_index momentum_adj = math.tanh(momentum / 20) * 0.15
if fg > 70: _momentum_contribution = momentum_adj if is_price_above else -momentum_adj
fg_adj = 0.06 adjustments.append(_momentum_contribution)
sources.append(f"Fear&Greed: {fg} (greed)") sources.append(f"{asset_label} 24h: {momentum:+.1f}%")
elif fg < 30:
fg_adj = -0.06 # Signal 2: Fear & Greed (price markets only)
sources.append(f"Fear&Greed: {fg} (fear)") _fg_contribution = 0.0
else: if not is_non_price:
fg_adj = (fg - 50) / 50 * 0.04 fg = ext.fear_greed_index
sources.append(f"Fear&Greed: {fg} (neutral)") if fg > 70:
_fg_contribution = fg_adj if is_price_above else -fg_adj fg_adj = 0.06
adjustments.append(_fg_contribution) sources.append(f"Fear&Greed: {fg} (greed)")
elif fg < 30:
fg_adj = -0.06
sources.append(f"Fear&Greed: {fg} (fear)")
else:
fg_adj = (fg - 50) / 50 * 0.04
sources.append(f"Fear&Greed: {fg} (neutral)")
_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
_btc_dom_contribution = 0.0 _btc_dom_contribution = 0.0
+123
View File
@@ -0,0 +1,123 @@
"""
Tests for FASE 3 macro signals (momentum, Fear & Greed) must not apply to
non-price markets (politics / tech / events).
Regression: for "Will X win the election?"-style questions, is_price_above is
False, so positive BTC momentum and high Fear & Greed were sign-flipped into
evidence AGAINST the YES outcome. The fix skips both signals entirely for
politics/tech/events, leaving their contributions (and feat_mom_lo /
feat_fg_lo) at 0.0.
evaluate_market only returns a TradingSignal on the TRADE path; on skips it
returns None but always emits a structured log line containing the per-feature
log-odds (fg_lo= mom_lo=). The tests parse that line via caplog.
"""
import asyncio
import logging
import math
import re
import pytest
from bot.data.external import ExternalSignals
from bot.data.polymarket import Market
from bot.strategy.bayesian import BayesianStrategy
FEAT_RE = re.compile(r"fg_lo=([+-]\d+\.\d+) mom_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:
# Strong bullish macro environment: BTC +10%, extreme greed.
return ExternalSignals(
btc_price=100_000.0,
btc_change_24h=10.0,
eth_price=4_000.0,
eth_change_24h=8.0,
btc_dominance=50.0,
fear_greed_index=80,
fear_greed_label="greed",
total_market_cap_change=5.0,
valid=True,
)
def _evaluate_and_parse_feats(question: str, category: str, caplog) -> tuple[float, float]:
"""Run BayesianStrategy.evaluate and return (feat_fg_lo, feat_mom_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 = FEAT_RE.search(record.getMessage())
if m:
return float(m.group(1)), float(m.group(2))
pytest.fail(
"No SKIP_EDGE_NET/TRADE log line with feature contributions found; "
f"got: {[r.getMessage() for r in caplog.records]}"
)
def test_politics_market_ignores_momentum_and_fear_greed(caplog):
"""Political market with BTC +10% and F&G=80 → both contributions 0.0."""
feat_fg_lo, feat_mom_lo = _evaluate_and_parse_feats(
"Will John Smith win the election?", "politics", caplog
)
assert feat_mom_lo == 0.0
assert feat_fg_lo == 0.0
# The signal sources must not mention momentum or Fear & Greed either.
full_log = "\n".join(r.getMessage() for r in caplog.records)
assert "Fear&Greed" not in full_log
assert "24h" not in full_log
def test_tech_and_events_markets_ignore_macro_signals(caplog):
for category in ("tech", "events"):
caplog.clear()
feat_fg_lo, feat_mom_lo = _evaluate_and_parse_feats(
"Will the product launch happen this quarter?", category, caplog
)
assert feat_mom_lo == 0.0, f"momentum applied to {category} market"
assert feat_fg_lo == 0.0, f"Fear&Greed applied to {category} market"
def test_btc_market_keeps_momentum_and_fear_greed(caplog):
"""BTC price market with BTC +10% and F&G=80 → current behavior preserved."""
feat_fg_lo, feat_mom_lo = _evaluate_and_parse_feats(
"Will Bitcoin be above $150,000 on July 1?", "crypto/finance", caplog
)
assert feat_mom_lo > 0
assert feat_fg_lo > 0
# Exact values: is_price_above=True ("above"), so contributions are positive.
# momentum: tanh(10/20) * 0.15, ×2 → log-odds. F&G>70: +0.06, ×2 → log-odds.
assert feat_mom_lo == pytest.approx(math.tanh(10 / 20) * 0.15 * 2, abs=1e-4)
assert feat_fg_lo == pytest.approx(0.06 * 2, abs=1e-4)
full_log = "\n".join(r.getMessage() for r in caplog.records)
assert "Fear&Greed: 80 (greed)" in full_log
assert "BTC 24h: +10.0%" in full_log
def test_btc_below_market_sign_flip_preserved(caplog):
"""'below' market: bullish macro lowers YES probability (sign flip intact)."""
feat_fg_lo, feat_mom_lo = _evaluate_and_parse_feats(
"Will Bitcoin drop below $50,000 by August?", "crypto/finance", caplog
)
assert feat_mom_lo < 0
assert feat_fg_lo < 0