"""Unified wallet tracker: per-wallet directional signals, multi-wallet consensus, and auto-discovery.

Three modes of operation:

1. **Track wallet** -- given a wallet address, compute directional signals for
   every market it holds a position in.
2. **Consensus** -- given a market, discover top holders and aggregate their
   signals into a single weighted consensus.
3. **Scan** -- auto-discover high-signal wallets across top markets.

Each per-wallet signal is a 4-component decomposition:

- **Direction** -- net position direction (YES vs NO), size-weighted.
- **Conviction** -- position size relative to the wallet's average position.
- **Recency** -- exponential decay from most recent trade.
- **Momentum** -- recent vs older trade buy-ratio shift.

Pipeline integration follows the ``oracle()`` pattern::

    hz.run(pipeline=[hz.wallet_tracker(wallets=["0x1234..."])])

"""

from __future__ import annotations

import logging
import math
import time
from dataclasses import dataclass, field
from typing import Any, Callable

from horizon._horizon import auth_require_ultra, decay_weight, ema
from horizon.context import Context
from horizon.flow import (
    get_top_holders,
    get_wallet_positions,
    get_wallet_trades,
    Holder,
    Trade,
    WalletPosition,
)
from horizon.wallet_intel import score_wallet, WalletScore

logger = logging.getLogger("horizon.wallet_tracker")


# ---------------------------------------------------------------------------
# Module-level wallet score cache
# ---------------------------------------------------------------------------

_score_cache: dict[str, tuple[float, WalletScore]] = {}
_CACHE_TTL = 300.0  # 5 minutes


def _cached_score(address: str, ttl: float = _CACHE_TTL, limit: int = 100) -> WalletScore | None:
    now = time.time()
    if address in _score_cache:
        ts, cached = _score_cache[address]
        if now - ts < ttl:
            return cached
    try:
        ws = score_wallet(address, limit=limit)
        _score_cache[address] = (now, ws)
        return ws
    except Exception:  # noqa: BLE001
        return None


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------


@dataclass
class TrackerConfig:
    """Tunable knobs for the wallet tracker.

    Attributes:
        decay_half_life_secs: Half-life for recency decay (seconds).
        ema_alpha: Smoothing factor for EMA between refreshes.
        min_signal_strength: Minimum ``|signal - 0.5|`` to be actionable.
        min_confidence: Minimum confidence to be actionable.
        min_edge_bps: Minimum |edge| vs market price to be actionable.
        min_wallets_for_consensus: Minimum wallets for consensus signal.
        min_wallet_quality: Minimum ``|composite_score|`` to include wallet.
        momentum_weight: Maximum momentum contribution to signal.
        regime_change_threshold: Buy-ratio delta threshold for regime flip.
        wallet_score_cache_ttl: Seconds to cache wallet scores.
        poll_interval_cycles: Pipeline cycles between refreshes.
    """

    decay_half_life_secs: float = 21600.0
    ema_alpha: float = 0.3
    min_signal_strength: float = 0.05
    min_confidence: float = 0.3
    min_edge_bps: float = 200.0
    min_wallets_for_consensus: int = 3
    min_wallet_quality: float = 0.1
    momentum_weight: float = 0.2
    regime_change_threshold: float = 0.4
    wallet_score_cache_ttl: float = 300.0
    poll_interval_cycles: int = 20


# ---------------------------------------------------------------------------
# Signal dataclasses
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class WalletMarketSignal:
    """Directional signal for one wallet in one market.

    Attributes:
        wallet: Wallet address.
        market_id: Market condition ID.
        signal: Composite signal in [0.01, 0.99]; 0.5 = neutral.
        direction: Raw position direction D(w,m).
        conviction: Relative sizing C(w,m).
        recency: Decay weight R(w,m).
        momentum: Recent trade direction shift M(w,m).
        wallet_quality: Composite score from ``score_wallet`` [-1, 1].
        confidence: Signal confidence [0, 1].
        confidence_low: Lower bound of confidence interval.
        confidence_high: Upper bound of confidence interval.
        trade_count: Number of trades in the market.
        position_size_usdc: Total position size in USDC.
        regime: Detected regime (bullish, bearish, neutral, flipping).
        edge_bps: Edge vs market price in basis points.
        timestamp: Unix timestamp.
        is_actionable: Whether signal passes all thresholds.
    """

    wallet: str
    market_id: str
    signal: float
    direction: float
    conviction: float
    recency: float
    momentum: float
    wallet_quality: float
    confidence: float
    confidence_low: float
    confidence_high: float
    trade_count: int
    position_size_usdc: float
    regime: str
    edge_bps: float
    timestamp: float
    is_actionable: bool


@dataclass(frozen=True)
class ConsensusSignal:
    """Aggregated signal across multiple wallets for a single market.

    Attributes:
        market_id: Market condition ID.
        signal: Weighted consensus signal in [0.01, 0.99].
        confidence: Consensus confidence [0, 1].
        confidence_low: Lower bound of confidence interval.
        confidence_high: Upper bound of confidence interval.
        agreement: Wallet agreement measure [0, 1].
        n_wallets: Number of contributing wallets.
        edge_bps: Consensus edge vs market price.
        wallet_signals: Individual wallet signals.
        best_wallet: Address of highest-confidence wallet.
        timestamp: Unix timestamp.
        is_actionable: Whether consensus passes thresholds.
    """

    market_id: str
    signal: float
    confidence: float
    confidence_low: float
    confidence_high: float
    agreement: float
    n_wallets: int
    edge_bps: float
    wallet_signals: list[WalletMarketSignal]
    best_wallet: str
    timestamp: float
    is_actionable: bool


@dataclass(frozen=True)
class WalletRegimeChange:
    """Detected regime change for a wallet in a market."""

    wallet: str
    market_id: str
    old_regime: str
    new_regime: str
    confidence: float
    timestamp: float


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
    return max(lo, min(hi, value))


def _safe_std(values: list[float]) -> float:
    n = len(values)
    if n < 2:
        return 0.0
    mean = sum(values) / n
    var = sum((v - mean) ** 2 for v in values) / n
    return math.sqrt(var)


# ---------------------------------------------------------------------------
# Component functions
# ---------------------------------------------------------------------------


def _direction_component(positions: list[WalletPosition], market_id: str) -> float:
    """D(w,m): Net position direction, size-weighted. YES = +1, NO = -1.

    Maps to [0, 1] via ``(net + 1) / 2``.  Returns 0.5 if no positions.
    """
    total_size = 0.0
    weighted_dir = 0.0

    for p in positions:
        if p.condition_id != market_id:
            continue
        if p.size <= 0:
            continue
        direction = 1.0 if p.outcome.upper() in ("YES", "Y") else -1.0
        weighted_dir += p.size * direction
        total_size += p.size

    if total_size <= 0:
        return 0.5

    net = weighted_dir / total_size  # in [-1, 1]
    return (net + 1.0) / 2.0


def _conviction_component(
    positions: list[WalletPosition], market_id: str,
) -> float:
    """C(w,m): Position size in this market vs wallet's average across all markets.

    ``(tanh(ratio - 1) + 1) / 2``.  Returns 0.5 if ratio = 1 or no data.
    """
    market_size = 0.0
    sizes: list[float] = []

    for p in positions:
        size_usdc = p.size * p.current_price if p.current_price > 0 else p.size
        if size_usdc > 0:
            sizes.append(size_usdc)
        if p.condition_id == market_id and size_usdc > 0:
            market_size += size_usdc

    if not sizes or market_size <= 0:
        return 0.5

    avg_size = sum(sizes) / len(sizes)
    if avg_size <= 0:
        return 0.5

    ratio = market_size / avg_size
    return (math.tanh(ratio - 1.0) + 1.0) / 2.0


def _recency_component(
    trades: list[Trade], half_life: float,
) -> float:
    """R(w,m): Decay weight from most recent trade.

    Uses Rust ``decay_weight(age_secs, half_life)``.  Returns 0.0 if no trades.
    """
    if not trades:
        return 0.0

    now = time.time()
    most_recent = max(t.timestamp for t in trades)
    if most_recent <= 0:
        return 0.0

    age_secs = now - most_recent
    if age_secs < 0:
        age_secs = 0.0

    return decay_weight(age_secs, half_life)


def _momentum_component(trades: list[Trade]) -> float:
    """M(w,m): Volume-weighted buy ratio shift between recent half and older half.

    ``0.5 + (recent_ratio - old_ratio)``, clamped to [0, 1].
    Needs 3+ trades; returns 0.5 otherwise.
    """
    if len(trades) < 3:
        return 0.5

    # Sort by timestamp ascending (oldest first)
    sorted_trades = sorted(trades, key=lambda t: t.timestamp)
    mid = len(sorted_trades) // 2
    old_half = sorted_trades[:mid]
    recent_half = sorted_trades[mid:]

    def _buy_ratio(ts: list[Trade]) -> float:
        buy_vol = sum(t.size for t in ts if t.side.upper() == "BUY")
        total_vol = sum(t.size for t in ts)
        if total_vol <= 0:
            return 0.5
        return buy_vol / total_vol

    old_ratio = _buy_ratio(old_half)
    recent_ratio = _buy_ratio(recent_half)

    return _clamp(0.5 + (recent_ratio - old_ratio), 0.0, 1.0)


def _detect_regime(trades: list[Trade], threshold: float) -> str:
    """Detect wallet regime in a market from trade direction patterns.

    Compares buy ratio of recent N trades vs prior N trades.
    """
    if len(trades) < 4:
        return "neutral"

    sorted_trades = sorted(trades, key=lambda t: t.timestamp)
    mid = len(sorted_trades) // 2
    old_half = sorted_trades[:mid]
    recent_half = sorted_trades[mid:]

    def _buy_ratio(ts: list[Trade]) -> float:
        buy_count = sum(1 for t in ts if t.side.upper() == "BUY")
        return buy_count / len(ts) if ts else 0.5

    old_ratio = _buy_ratio(old_half)
    recent_ratio = _buy_ratio(recent_half)

    delta = recent_ratio - old_ratio

    if abs(delta) >= threshold:
        return "flipping"
    if recent_ratio > 0.6:
        return "bullish"
    if recent_ratio < 0.4:
        return "bearish"
    return "neutral"


def _compute_confidence(
    trade_count: int, quality: float, recency: float,
) -> float:
    """Confidence = weighted combination of sample size, quality, and recency."""
    # Sample size logistic: 1 / (1 + exp(-0.3 * (n - 5)))
    try:
        sample_conf = 1.0 / (1.0 + math.exp(-0.3 * (trade_count - 5)))
    except OverflowError:
        sample_conf = 0.0 if trade_count < 5 else 1.0

    quality_conf = min(abs(quality), 1.0)

    return _clamp(
        0.4 * sample_conf + 0.4 * quality_conf + 0.2 * recency,
        0.0,
        1.0,
    )


# ---------------------------------------------------------------------------
# Core signal computation
# ---------------------------------------------------------------------------


def compute_wallet_signal(
    wallet: str,
    market_id: str,
    config: TrackerConfig | None = None,
    market_price: float = 0.0,
    positions: list[WalletPosition] | None = None,
    trades: list[Trade] | None = None,
    wallet_quality: float | None = None,
) -> WalletMarketSignal:
    """Compute a directional signal for one wallet in one market.

    If *positions*, *trades*, or *wallet_quality* are not provided they are
    fetched via the public API.

    Args:
        wallet: Wallet address (0x...).
        market_id: Condition ID of the target market.
        config: Tracker configuration.
        market_price: Current market mid price (for edge calculation).
        positions: Pre-fetched wallet positions (optional).
        trades: Pre-fetched wallet trades in this market (optional).
        wallet_quality: Pre-fetched composite_score (optional).

    Returns:
        :class:`WalletMarketSignal` with signal, components, and metadata.
    """
    auth_require_ultra()

    config = config or TrackerConfig()
    now = time.time()

    # --- Fetch data if not provided ---
    if positions is None:
        try:
            positions = get_wallet_positions(wallet, limit=100)
        except Exception:  # noqa: BLE001
            positions = []

    if trades is None:
        try:
            trades = get_wallet_trades(wallet, limit=500, condition_id=market_id)
        except Exception:  # noqa: BLE001
            trades = []

    if wallet_quality is None:
        ws = _cached_score(wallet, ttl=config.wallet_score_cache_ttl)
        wallet_quality = ws.composite_score if ws is not None else 0.0

    # --- Components ---
    D = _direction_component(positions, market_id)
    C = _conviction_component(positions, market_id)
    R = _recency_component(trades, config.decay_half_life_secs)
    M = _momentum_component(trades)
    Q = wallet_quality

    # --- Composition ---
    deviation = D - 0.5
    amplified = deviation * C * 2.0
    with_momentum = amplified + (M - 0.5) * config.momentum_weight
    decayed = with_momentum * R
    quality_adjusted = decayed * Q
    signal = _clamp(0.5 + quality_adjusted, 0.01, 0.99)

    # --- Position size ---
    position_size_usdc = 0.0
    for p in positions:
        if p.condition_id == market_id:
            position_size_usdc += p.current_value if p.current_value > 0 else p.size

    # --- Regime ---
    market_trades = [t for t in trades if t.condition_id == market_id] if trades else trades
    regime = _detect_regime(market_trades or [], config.regime_change_threshold)

    # --- Confidence ---
    confidence = _compute_confidence(len(market_trades or []), Q, R)

    half_width = (1.0 - confidence) * 0.15
    confidence_low = max(0.01, signal - half_width)
    confidence_high = min(0.99, signal + half_width)

    # --- Edge ---
    edge_bps = round((signal - market_price) * 10000, 4) if market_price > 0 else 0.0

    # --- Actionability ---
    is_actionable = (
        abs(signal - 0.5) >= config.min_signal_strength
        and confidence >= config.min_confidence
        and (abs(edge_bps) >= config.min_edge_bps or market_price <= 0)
    )

    return WalletMarketSignal(
        wallet=wallet,
        market_id=market_id,
        signal=round(signal, 4),
        direction=round(D, 4),
        conviction=round(C, 4),
        recency=round(R, 4),
        momentum=round(M, 4),
        wallet_quality=round(Q, 4),
        confidence=round(confidence, 4),
        confidence_low=round(confidence_low, 4),
        confidence_high=round(confidence_high, 4),
        trade_count=len(market_trades or []),
        position_size_usdc=round(position_size_usdc, 2),
        regime=regime,
        edge_bps=round(edge_bps, 4),
        timestamp=now,
        is_actionable=is_actionable,
    )


# ---------------------------------------------------------------------------
# Track wallet (Mode 1)
# ---------------------------------------------------------------------------


def track_wallet(
    wallet: str,
    config: TrackerConfig | None = None,
    market_price_map: dict[str, float] | None = None,
) -> list[WalletMarketSignal]:
    """Compute signals for every market a wallet holds a position in.

    Returns a list sorted by ``|signal - 0.5|`` descending (strongest first).

    Args:
        wallet: Wallet address (0x...).
        config: Tracker configuration.
        market_price_map: Optional mapping of market_id -> current price.

    Returns:
        List of :class:`WalletMarketSignal`.
    """
    auth_require_ultra()

    config = config or TrackerConfig()
    market_price_map = market_price_map or {}

    try:
        positions = get_wallet_positions(wallet, limit=100)
    except Exception:  # noqa: BLE001
        logger.warning("track_wallet: failed to fetch positions for %s", wallet[:10])
        return []

    if not positions:
        return []

    ws = _cached_score(wallet, ttl=config.wallet_score_cache_ttl)
    quality = ws.composite_score if ws is not None else 0.0

    # Find unique market IDs
    market_ids = list({p.condition_id for p in positions if p.condition_id})

    signals: list[WalletMarketSignal] = []
    for mid in market_ids:
        try:
            trades = get_wallet_trades(wallet, limit=500, condition_id=mid)
        except Exception:  # noqa: BLE001
            trades = []

        mp = market_price_map.get(mid, 0.0)
        sig = compute_wallet_signal(
            wallet, mid,
            config=config,
            market_price=mp,
            positions=positions,
            trades=trades,
            wallet_quality=quality,
        )
        signals.append(sig)

    # Sort by strength descending
    signals.sort(key=lambda s: abs(s.signal - 0.5), reverse=True)
    return signals


# ---------------------------------------------------------------------------
# Consensus (Mode 2)
# ---------------------------------------------------------------------------


def compute_consensus(
    market_id: str,
    config: TrackerConfig | None = None,
    market_price: float = 0.0,
) -> ConsensusSignal:
    """Aggregate wallet signals into a weighted consensus for a market.

    Discovers top holders, scores each, filters by quality, and computes
    a weighted consensus signal.

    Args:
        market_id: Condition ID of the target market.
        config: Tracker configuration.
        market_price: Current market mid price.

    Returns:
        :class:`ConsensusSignal`.
    """
    auth_require_ultra()

    config = config or TrackerConfig()
    now = time.time()

    # --- Discover wallets ---
    try:
        holders = get_top_holders(market_id, limit=20)
    except Exception:  # noqa: BLE001
        holders = []

    if not holders:
        return _neutral_consensus(market_id, now, config)

    # --- Score and filter ---
    wallet_signals: list[WalletMarketSignal] = []
    weights: list[float] = []

    seen_wallets: set[str] = set()
    for h in holders:
        if not h.wallet or h.wallet in seen_wallets:
            continue
        seen_wallets.add(h.wallet)

        ws = _cached_score(h.wallet, ttl=config.wallet_score_cache_ttl)
        if ws is None:
            continue
        if abs(ws.composite_score) < config.min_wallet_quality:
            continue

        sig = compute_wallet_signal(
            h.wallet, market_id,
            config=config,
            market_price=market_price,
            wallet_quality=ws.composite_score,
        )
        wallet_signals.append(sig)

        # Weight: |quality| * sqrt(position_size) * recency
        w = abs(ws.composite_score) * math.sqrt(max(sig.position_size_usdc, 1.0)) * max(sig.recency, 0.01)
        weights.append(w)

    if len(wallet_signals) < config.min_wallets_for_consensus:
        return _neutral_consensus(market_id, now, config, wallet_signals=wallet_signals)

    # --- Weighted average ---
    total_weight = sum(weights)
    if total_weight <= 0:
        total_weight = 1.0

    consensus_signal = sum(
        s.signal * w for s, w in zip(wallet_signals, weights)
    ) / total_weight
    consensus_signal = _clamp(consensus_signal, 0.01, 0.99)

    # --- Agreement: 1 - CV(signals) ---
    sig_values = [s.signal for s in wallet_signals]
    mean_sig = sum(sig_values) / len(sig_values) if sig_values else 0.5
    std_sig = _safe_std(sig_values)
    cv = std_sig / abs(mean_sig) if abs(mean_sig) > 1e-9 else 1.0
    agreement = _clamp(1.0 - cv, 0.0, 1.0)

    # --- Confidence from IQR ---
    sorted_sigs = sorted(sig_values)
    n = len(sorted_sigs)
    q1 = sorted_sigs[n // 4] if n >= 4 else sorted_sigs[0]
    q3 = sorted_sigs[3 * n // 4] if n >= 4 else sorted_sigs[-1]
    iqr = q3 - q1
    confidence_low = max(0.01, consensus_signal - iqr / 2)
    confidence_high = min(0.99, consensus_signal + iqr / 2)

    # Aggregate confidence
    consensus_confidence = _clamp(
        sum(s.confidence * w for s, w in zip(wallet_signals, weights)) / total_weight,
        0.0, 1.0,
    )

    # --- Edge ---
    edge_bps = round((consensus_signal - market_price) * 10000, 4) if market_price > 0 else 0.0

    # --- Best wallet ---
    best = max(wallet_signals, key=lambda s: s.confidence)

    is_actionable = (
        abs(consensus_signal - 0.5) >= config.min_signal_strength
        and consensus_confidence >= config.min_confidence
        and (abs(edge_bps) >= config.min_edge_bps or market_price <= 0)
    )

    return ConsensusSignal(
        market_id=market_id,
        signal=round(consensus_signal, 4),
        confidence=round(consensus_confidence, 4),
        confidence_low=round(confidence_low, 4),
        confidence_high=round(confidence_high, 4),
        agreement=round(agreement, 4),
        n_wallets=len(wallet_signals),
        edge_bps=round(edge_bps, 4),
        wallet_signals=wallet_signals,
        best_wallet=best.wallet,
        timestamp=now,
        is_actionable=is_actionable,
    )


def _neutral_consensus(
    market_id: str,
    timestamp: float,
    config: TrackerConfig,
    wallet_signals: list[WalletMarketSignal] | None = None,
) -> ConsensusSignal:
    """Return a neutral consensus when insufficient data."""
    return ConsensusSignal(
        market_id=market_id,
        signal=0.5,
        confidence=0.0,
        confidence_low=0.35,
        confidence_high=0.65,
        agreement=0.0,
        n_wallets=len(wallet_signals) if wallet_signals else 0,
        edge_bps=0.0,
        wallet_signals=wallet_signals or [],
        best_wallet="",
        timestamp=timestamp,
        is_actionable=False,
    )


# ---------------------------------------------------------------------------
# Scan (Mode 3)
# ---------------------------------------------------------------------------


def scan_wallets(
    market_id: str | None = None,
    config: TrackerConfig | None = None,
    market_price: float = 0.0,
) -> ConsensusSignal:
    """Auto-discover wallets and compute consensus for a market.

    If no *market_id* is given, uses the first top market from discovery.

    Args:
        market_id: Optional market condition ID.
        config: Tracker configuration.
        market_price: Current market mid price.

    Returns:
        :class:`ConsensusSignal`.
    """
    auth_require_ultra()

    config = config or TrackerConfig()

    if not market_id:
        try:
            from horizon.discovery import top_markets
            discovered = top_markets(limit=1)
            if discovered:
                market_id = discovered[0].id
        except Exception:  # noqa: BLE001
            pass

    if not market_id:
        return _neutral_consensus("", time.time(), config)

    return compute_consensus(market_id, config=config, market_price=market_price)


# ---------------------------------------------------------------------------
# Pipeline factory
# ---------------------------------------------------------------------------


def wallet_tracker(
    wallets: list[str] | None = None,
    config: TrackerConfig | None = None,
    poll_interval_cycles: int | None = None,
) -> Callable[[Context], None]:
    """Create a pipeline function that tracks wallets and injects signals.

    The returned callable maintains per-market state and refreshes signals
    every *poll_interval_cycles* cycles.  Between refreshes it serves
    EMA-smoothed cached values.

    Injected context parameters:
        ``wallet_tracker_signal`` -- best :class:`WalletMarketSignal` for current market.
        ``wallet_tracker_consensus`` -- :class:`ConsensusSignal` (if multi-wallet).
        ``wallet_tracker_edge_bps`` -- ``float``, signed edge in basis points.
        ``wallet_tracker_regime_changes`` -- ``list[WalletRegimeChange]``.

    Args:
        wallets: Wallet addresses to track.  If empty/None, auto-discovers
            via top holders (Mode 3).
        config: Tracker configuration.
        poll_interval_cycles: Override for ``config.poll_interval_cycles``.

    Returns:
        Pipeline function with signature ``(Context) -> None``.
    """
    auth_require_ultra()

    config = config or TrackerConfig()
    interval = poll_interval_cycles or config.poll_interval_cycles

    # Closure state
    signals_cache: dict[str, WalletMarketSignal] = {}  # market_id -> best signal
    consensus_cache: dict[str, ConsensusSignal] = {}
    regime_states: dict[str, str] = {}  # "wallet:market_id" -> regime
    smoothed_signals: dict[str, float] = {}  # market_id -> EMA'd signal
    cycle_count = 0

    def _inner(ctx: Context) -> None:
        nonlocal cycle_count
        cycle_count += 1

        market = ctx.market
        if market is None:
            return
        market_id = market.id
        if not market_id:
            return

        price = ctx.feed.price if ctx.feed else 0.5
        regime_changes: list[WalletRegimeChange] = []

        if cycle_count % interval == 0:
            try:
                if wallets:
                    # Mode 1/2: track specific wallets
                    all_signals: list[WalletMarketSignal] = []
                    for w in wallets:
                        sig = compute_wallet_signal(
                            w, market_id, config=config, market_price=price,
                        )
                        all_signals.append(sig)

                        # Regime change detection
                        key = f"{w}:{market_id}"
                        old_regime = regime_states.get(key, "neutral")
                        if sig.regime != old_regime and old_regime != "neutral":
                            regime_changes.append(WalletRegimeChange(
                                wallet=w,
                                market_id=market_id,
                                old_regime=old_regime,
                                new_regime=sig.regime,
                                confidence=sig.confidence,
                                timestamp=sig.timestamp,
                            ))
                        regime_states[key] = sig.regime

                    if all_signals:
                        best = max(all_signals, key=lambda s: abs(s.signal - 0.5))
                        signals_cache[market_id] = best

                    if len(wallets) >= config.min_wallets_for_consensus:
                        consensus = compute_consensus(
                            market_id, config=config, market_price=price,
                        )
                        consensus_cache[market_id] = consensus
                else:
                    # Mode 3: auto-discover
                    consensus = compute_consensus(
                        market_id, config=config, market_price=price,
                    )
                    consensus_cache[market_id] = consensus
                    if consensus.wallet_signals:
                        best = max(
                            consensus.wallet_signals,
                            key=lambda s: abs(s.signal - 0.5),
                        )
                        signals_cache[market_id] = best

            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "wallet_tracker: refresh failed for %s: %s", market_id[:12], exc,
                )

        # --- Inject into context ---
        if market_id in signals_cache:
            sig = signals_cache[market_id]

            # EMA smoothing
            prev = smoothed_signals.get(market_id, sig.signal)
            smoothed = prev + config.ema_alpha * (sig.signal - prev)
            smoothed_signals[market_id] = smoothed

            ctx.params["wallet_tracker_signal"] = sig
            ctx.params["wallet_tracker_edge_bps"] = sig.edge_bps

        if market_id in consensus_cache:
            ctx.params["wallet_tracker_consensus"] = consensus_cache[market_id]

        if regime_changes:
            ctx.params["wallet_tracker_regime_changes"] = regime_changes

    _inner.__name__ = "wallet_tracker"
    return _inner
