fix(strategy): exclude momentum and fear-greed signals from non-price markets (politics/tech/events)
CI/CD / build-and-push (push) Successful in 7s
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:
co-authored by
Claude Fable 5
parent
96f31acf16
commit
4002f03d0c
@@ -419,30 +419,36 @@ class BayesianStrategy:
|
||||
sources: list[str] = [f"Prior=poly({prior:.3f})"]
|
||||
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
|
||||
# is_price_above gives the adjustment a meaningful sign. For
|
||||
# politics/tech/events there is no above/below notion — is_price_above
|
||||
# defaults to False (or flips on accidental wording like "reach"), so
|
||||
# applying these signals just injected sign noise. Skip them entirely;
|
||||
# their contributions stay 0.0 → feat_mom_lo / feat_fg_lo = 0.0.
|
||||
is_non_price = is_politics or is_tech or is_events
|
||||
|
||||
# Signal 1: price momentum (asset-specific; price markets only)
|
||||
_momentum_contribution = 0.0
|
||||
if not is_non_price:
|
||||
if is_btc:
|
||||
momentum = ext.btc_change_24h
|
||||
asset_label = "BTC"
|
||||
elif is_eth:
|
||||
momentum = ext.eth_change_24h
|
||||
asset_label = "ETH"
|
||||
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"
|
||||
|
||||
_momentum_contribution = 0.0
|
||||
if abs(momentum) > 2:
|
||||
momentum_adj = math.tanh(momentum / 20) * 0.15
|
||||
if is_politics or is_tech or is_events:
|
||||
momentum_adj *= 0.5
|
||||
_momentum_contribution = momentum_adj if is_price_above else -momentum_adj
|
||||
adjustments.append(_momentum_contribution)
|
||||
sources.append(f"{asset_label} 24h: {momentum:+.1f}%")
|
||||
|
||||
# Signal 2: Fear & Greed
|
||||
# Signal 2: Fear & Greed (price markets only)
|
||||
_fg_contribution = 0.0
|
||||
if not is_non_price:
|
||||
fg = ext.fear_greed_index
|
||||
if fg > 70:
|
||||
fg_adj = 0.06
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user