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})"]
|
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
|
||||||
|
# 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:
|
if is_btc:
|
||||||
momentum = ext.btc_change_24h
|
momentum = ext.btc_change_24h
|
||||||
asset_label = "BTC"
|
asset_label = "BTC"
|
||||||
elif is_eth:
|
elif is_eth:
|
||||||
momentum = ext.eth_change_24h
|
momentum = ext.eth_change_24h
|
||||||
asset_label = "ETH"
|
asset_label = "ETH"
|
||||||
elif is_politics or is_tech or is_events:
|
|
||||||
momentum = ext.btc_change_24h
|
|
||||||
asset_label = "BTC(sentiment)"
|
|
||||||
else:
|
else:
|
||||||
momentum = ext.total_market_cap_change
|
momentum = ext.total_market_cap_change
|
||||||
asset_label = "total mktcap"
|
asset_label = "total mktcap"
|
||||||
|
|
||||||
_momentum_contribution = 0.0
|
|
||||||
if abs(momentum) > 2:
|
if abs(momentum) > 2:
|
||||||
momentum_adj = math.tanh(momentum / 20) * 0.15
|
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
|
_momentum_contribution = momentum_adj if is_price_above else -momentum_adj
|
||||||
adjustments.append(_momentum_contribution)
|
adjustments.append(_momentum_contribution)
|
||||||
sources.append(f"{asset_label} 24h: {momentum:+.1f}%")
|
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
|
fg = ext.fear_greed_index
|
||||||
if fg > 70:
|
if fg > 70:
|
||||||
fg_adj = 0.06
|
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