"""Always-on portfolio risk monitoring for hz.run().

Provides continuous, real-time portfolio surveillance:
- Correlation monitoring via Ledoit-Wolf shrinkage
- Drawdown management with configurable thresholds and auto-deleveraging
- Hedge suggestions from prediction Greeks delta exposure
- Risk budgeting with regime-aware multipliers
- Cornish-Fisher VaR/CVaR for fat-tailed prediction markets

Usage:
    import horizon as hz

    hz.run(
        pipeline=[
            hz.sentinel(config=hz.SentinelConfig(auto_deleverage=True)),
            model,
            quoter,
        ],
        ...
    )

    # Standalone:
    report = hz.sentinel_report(engine)
    hedges = hz.suggest_hedges(engine, max_hedges=3)
"""

from __future__ import annotations

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

from horizon._horizon import (
    Engine,
    Side,
    auth_require_ultra,
    cornish_fisher_var,
    cornish_fisher_cvar,
    ledoit_wolf_shrinkage,
    monte_carlo,
    prediction_greeks,
    estimate_volatility,
    decay_weight,
)
from horizon.context import Context

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


# ---------------------------------------------------------------------------
# Dataclasses
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class CorrelationAlert:
    """Alert for a correlation spike between two markets."""

    market_a: str
    market_b: str
    current_corr: float
    baseline_corr: float
    spike_magnitude: float
    risk_implication: str


@dataclass(frozen=True)
class DrawdownLevel:
    """Drawdown status for a scope (portfolio or single market)."""

    scope: str          # "portfolio" or "market"
    scope_id: str       # market_id or "portfolio"
    current_dd: float   # negative fraction, e.g. -0.12
    peak_value: float
    current_value: float
    threshold: float
    action: str         # "alert", "reduce", "pause", "exit"
    triggered: bool


@dataclass(frozen=True)
class HedgeSuggestion:
    """Actionable hedge recommendation to reduce portfolio risk."""

    market_id: str
    market_title: str
    side: str           # "yes" or "no"
    order_side: str     # "buy" or "sell"
    size: float
    price: float
    risk_reduction_pct: float
    cost_estimate_usdc: float
    rationale: str


@dataclass(frozen=True)
class RiskBudget:
    """Risk budget status with regime-aware adjustments."""

    total_budget: float
    used_budget: float
    usage_pct: float
    regime: str
    regime_multiplier: float
    per_market: dict[str, float] = field(default_factory=dict)


@dataclass(frozen=True)
class SentinelReport:
    """Portfolio risk report."""

    correlation_alerts: list[CorrelationAlert]
    drawdown_levels: list[DrawdownLevel]
    hedges: list[HedgeSuggestion]
    risk_budget: RiskBudget
    var_95: float
    cvar_95: float
    max_position_pct: float
    regime: str
    portfolio_value: float
    timestamp: float


@dataclass
class SentinelConfig:
    """Configuration for the sentinel risk monitor (mutable)."""

    drawdown_thresholds: list[tuple[float, str]] = field(
        default_factory=lambda: [
            (-0.05, "alert"),
            (-0.10, "reduce"),
            (-0.20, "pause"),
            (-0.30, "exit"),
        ]
    )
    correlation_spike_threshold: float = 0.3
    auto_hedge: bool = False
    auto_deleverage: bool = True
    regime_sensitivity: float = 1.0
    var_confidence: float = 0.95
    lookback_returns: int = 100
    risk_budget_total: float = 10000.0


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------


def _portfolio_value(engine: Engine) -> float:
    """Sum position values: size * price (Yes) or size * (1-price) (No)."""
    positions = engine.positions()
    if not positions:
        return 0.0

    feeds = engine.all_feed_snapshots()
    total = 0.0

    for pos in positions:
        snap = feeds.get(pos.market_id)
        price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price
        price = max(0.01, min(0.99, price))

        if pos.side == Side.Yes:
            total += pos.size * price
        else:
            total += pos.size * (1.0 - price)

    return round(total, 2)


def _position_returns(engine: Engine, lookback: int) -> list[float]:
    """Compute per-fill returns as (fill_price - entry) / entry."""
    fills = engine.recent_fills()
    if not fills:
        return []

    entry_prices: dict[str, float] = {}
    for pos in engine.positions():
        entry_prices[pos.market_id] = pos.avg_entry_price

    returns: list[float] = []
    for fill in fills:
        entry = entry_prices.get(fill.market_id, 0.0)
        if entry <= 0.0 or abs(entry) < 1e-10:
            continue
        ret = (fill.price - entry) / entry
        if math.isfinite(ret) and abs(ret) < 10.0:
            returns.append(round(ret, 4))

    return returns[-lookback:]


def _detect_regime(engine: Engine) -> tuple[str, float]:
    """Classify volatility regime from feed spreads.

    Returns (regime_label, budget_multiplier):
      low_vol < 0.05 -> 1.5, normal < 0.15 -> 1.0,
      high_vol < 0.30 -> 0.5, crisis >= 0.30 -> 0.25.
    """
    feeds = engine.all_feed_snapshots()
    if not feeds:
        return ("normal", 1.0)

    vol_estimates: list[float] = []
    for _name, snap in feeds.items():
        if snap.price <= 0:
            continue
        if snap.bid > 0 and snap.ask > 0:
            mid = (snap.bid + snap.ask) / 2.0
            if mid > 0:
                vol_estimates.append((snap.ask - snap.bid) / mid)
        else:
            vol_estimates.append(2.0 * snap.price * (1.0 - snap.price))

    if not vol_estimates:
        return ("normal", 1.0)

    avg_vol = sum(vol_estimates) / len(vol_estimates)
    if avg_vol < 0.05:
        return ("low_vol", 1.5)
    elif avg_vol < 0.15:
        return ("normal", 1.0)
    elif avg_vol < 0.30:
        return ("high_vol", 0.5)
    else:
        return ("crisis", 0.25)


def _check_drawdowns(
    engine: Engine,
    config: SentinelConfig,
    peak_values: dict[str, float],
) -> list[DrawdownLevel]:
    """Check portfolio and per-market drawdowns against thresholds.

    Updates peak_values in place with new high-water marks.
    """
    levels: list[DrawdownLevel] = []
    feeds = engine.all_feed_snapshots()

    # Portfolio-wide drawdown
    portfolio_val = _portfolio_value(engine)
    pk = "portfolio"
    if portfolio_val > peak_values.get(pk, 0.0):
        peak_values[pk] = portfolio_val
    peak_p = peak_values.get(pk, 0.0)
    dd_p = round((portfolio_val - peak_p) / peak_p, 4) if peak_p > 0 else 0.0

    for threshold, action in config.drawdown_thresholds:
        levels.append(DrawdownLevel(
            scope="portfolio", scope_id="portfolio",
            current_dd=dd_p, peak_value=round(peak_p, 2),
            current_value=round(portfolio_val, 2),
            threshold=threshold, action=action, triggered=dd_p <= threshold,
        ))

    # Per-market drawdowns
    for pos in engine.positions():
        snap = feeds.get(pos.market_id)
        price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price
        price = max(0.01, min(0.99, price))
        mval = pos.size * price if pos.side == Side.Yes else pos.size * (1.0 - price)

        mk = pos.market_id
        if mval > peak_values.get(mk, 0.0):
            peak_values[mk] = mval
        peak_m = peak_values.get(mk, 0.0)
        dd_m = round((mval - peak_m) / peak_m, 4) if peak_m > 0 else 0.0

        for threshold, action in config.drawdown_thresholds:
            if dd_m <= threshold:
                levels.append(DrawdownLevel(
                    scope="market", scope_id=pos.market_id,
                    current_dd=dd_m, peak_value=round(peak_m, 2),
                    current_value=round(mval, 2),
                    threshold=threshold, action=action, triggered=True,
                ))

    return levels


def _build_correlation_returns_matrix(
    engine: Engine, market_ids: list[str],
) -> tuple[list[list[float]], list[str]]:
    """Build an observations x assets returns matrix from fills."""
    fills = engine.recent_fills()
    market_fill_prices: dict[str, list[float]] = {m: [] for m in market_ids}

    for fill in fills:
        if fill.market_id in market_fill_prices:
            market_fill_prices[fill.market_id].append(fill.price)

    market_returns: dict[str, list[float]] = {}
    for mid, prices in market_fill_prices.items():
        if len(prices) >= 3:
            rets = []
            for i in range(1, len(prices)):
                rets.append(
                    (prices[i] - prices[i - 1]) / prices[i - 1]
                    if prices[i - 1] > 0 else 0.0
                )
            market_returns[mid] = rets

    valid = [m for m in market_ids if m in market_returns]
    if len(valid) < 2:
        return [], valid

    min_len = min(len(market_returns[m]) for m in valid)
    if min_len < 3:
        return [], valid

    matrix: list[list[float]] = []
    for t in range(min_len):
        matrix.append([market_returns[m][t] for m in valid])
    return matrix, valid


def _compute_correlation_alerts(
    returns_matrix: list[list[float]],
    market_ids: list[str],
    baseline_corr: dict[tuple[str, str], float],
    threshold: float,
) -> list[CorrelationAlert]:
    """Detect correlation spikes using Ledoit-Wolf shrinkage."""
    alerts: list[CorrelationAlert] = []
    n = len(market_ids)
    if n < 2 or len(returns_matrix) < 3:
        return alerts

    valid_rows = [r for r in returns_matrix if len(r) == n]
    if len(valid_rows) < 3:
        return alerts

    try:
        cov_matrix, _shrinkage = ledoit_wolf_shrinkage(valid_rows)
    except (ValueError, RuntimeError) as exc:
        logger.debug("Ledoit-Wolf failed in correlation alerts: %s", exc)
        return alerts

    if len(cov_matrix) != n:
        return alerts

    stds = [math.sqrt(max(cov_matrix[i][i], 1e-15)) for i in range(n)]

    for i in range(n):
        for j in range(i + 1, n):
            if stds[i] < 1e-10 or stds[j] < 1e-10:
                continue

            current = max(-1.0, min(1.0, cov_matrix[i][j] / (stds[i] * stds[j])))
            current = round(current, 4)
            pair = (market_ids[i], market_ids[j])
            rev = (market_ids[j], market_ids[i])

            base = baseline_corr.get(pair) or baseline_corr.get(rev)
            if base is None:
                baseline_corr[pair] = current
                continue

            spike = round(abs(current - base), 4)
            if spike >= threshold:
                if current > base and current > 0.5:
                    impl = (f"Correlation spiked to {current:.2f} from {base:.2f}: "
                            f"diversification diminished, risk concentrated")
                elif current < base and current < -0.5:
                    impl = (f"Negative correlation deepened to {current:.2f} from "
                            f"{base:.2f}: positions more hedged than intended")
                else:
                    impl = (f"Correlation shifted to {current:.2f} from {base:.2f}: "
                            f"regime change detected, review exposure")

                alerts.append(CorrelationAlert(
                    market_a=market_ids[i], market_b=market_ids[j],
                    current_corr=current, baseline_corr=round(base, 4),
                    spike_magnitude=spike, risk_implication=impl,
                ))

            baseline_corr[pair] = round(0.95 * base + 0.05 * current, 4)

    return alerts


def _suggest_hedges_internal(
    engine: Engine, max_hedges: int = 5, min_reduction_pct: float = 5.0,
) -> list[HedgeSuggestion]:
    """Generate hedge suggestions from delta exposure analysis."""
    positions = engine.positions()
    if not positions:
        return []

    feeds = engine.all_feed_snapshots()
    exposures: list[tuple[Any, float, float]] = []

    for pos in positions:
        snap = feeds.get(pos.market_id)
        price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price
        price = max(0.01, min(0.99, price))
        is_yes = pos.side == Side.Yes

        try:
            greeks = prediction_greeks(price, pos.size, is_yes, 24.0, 0.2)
            delta = greeks.delta
        except (ValueError, RuntimeError):
            delta = pos.size if is_yes else -pos.size

        exposures.append((pos, delta, price))

    if not exposures:
        return []

    exposures.sort(key=lambda x: abs(x[1]), reverse=True)
    total_abs = sum(abs(d) for _, d, _ in exposures)
    if total_abs < 1e-10:
        return []

    suggestions: list[HedgeSuggestion] = []
    for pos, delta, price in exposures[:max_hedges]:
        reduction = round((abs(delta) / total_abs) * 100.0, 4)
        if reduction < min_reduction_pct:
            continue

        if delta > 0:
            h_side = "yes" if pos.side == Side.Yes else "no"
            h_order = "sell"
            h_price = max(0.01, price - 0.01)
        else:
            h_side = "no" if pos.side == Side.Yes else "yes"
            h_order = "buy"
            h_price = min(0.99, price + 0.01)

        h_size = round(pos.size * 0.5, 2)
        cost = round(h_size * h_price, 2)

        suggestions.append(HedgeSuggestion(
            market_id=pos.market_id, market_title=pos.market_id,
            side=h_side, order_side=h_order, size=h_size,
            price=round(h_price, 4), risk_reduction_pct=reduction,
            cost_estimate_usdc=cost,
            rationale=(f"Delta {delta:+.2f} on {pos.market_id} = {reduction:.1f}% "
                       f"of risk. Hedge: {h_order} {h_size:.1f} @ {h_price:.2f}."),
        ))

    suggestions.sort(key=lambda s: s.risk_reduction_pct, reverse=True)
    return suggestions[:max_hedges]


def _build_risk_budget(
    engine: Engine, config: SentinelConfig, regime: str, regime_mult: float,
) -> RiskBudget:
    """Compute risk budget usage with regime multiplier."""
    feeds = engine.all_feed_snapshots()
    used = 0.0
    per_market: dict[str, float] = {}

    for pos in engine.positions():
        snap = feeds.get(pos.market_id)
        price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price
        risk = pos.size * price
        per_market[pos.market_id] = round(risk, 2)
        used += risk

    used = round(used, 2)
    effective = config.risk_budget_total * regime_mult
    pct = round((used / effective * 100.0) if effective > 0 else 0.0, 4)

    return RiskBudget(
        total_budget=round(effective, 2), used_budget=used,
        usage_pct=pct, regime=regime, regime_multiplier=regime_mult,
        per_market=per_market,
    )


def _build_report(
    engine: Engine,
    config: SentinelConfig,
    returns: list[float],
    baseline_corr: dict[tuple[str, str], float],
    drawdown_levels: list[DrawdownLevel],
    portfolio_val: float,
) -> SentinelReport:
    """Shared report builder used by sentinel_report() and the pipeline."""
    now = time.time()

    # VaR / CVaR
    var_95 = 0.0
    cvar_95 = 0.0
    if len(returns) >= 5:
        try:
            var_95 = round(cornish_fisher_var(returns, config.var_confidence), 4)
            cvar_95 = round(cornish_fisher_cvar(returns, config.var_confidence), 4)
        except (ValueError, RuntimeError) as exc:
            logger.debug("VaR/CVaR failed: %s", exc)

    # Correlation alerts
    positions = engine.positions()
    market_ids = sorted({p.market_id for p in positions})
    corr_alerts: list[CorrelationAlert] = []

    if len(market_ids) >= 2 and len(returns) >= 5:
        matrix, valid = _build_correlation_returns_matrix(engine, market_ids)
        if matrix and len(valid) >= 2:
            corr_alerts = _compute_correlation_alerts(
                matrix, valid, baseline_corr, config.correlation_spike_threshold,
            )

    # Regime & budget
    regime, regime_mult = _detect_regime(engine)
    budget = _build_risk_budget(engine, config, regime, regime_mult)

    # Max position concentration
    max_pos_pct = 0.0
    if portfolio_val > 0 and budget.per_market:
        max_pos_pct = round(max(budget.per_market.values()) / portfolio_val * 100.0, 4)

    # Hedges
    hedges = _suggest_hedges_internal(engine)

    report = SentinelReport(
        correlation_alerts=corr_alerts, drawdown_levels=drawdown_levels,
        hedges=hedges, risk_budget=budget, var_95=var_95, cvar_95=cvar_95,
        max_position_pct=max_pos_pct, regime=regime,
        portfolio_value=portfolio_val, timestamp=round(now, 4),
    )

    logger.info(
        "Sentinel report: portfolio=$%.2f VaR95=%.4f CVaR95=%.4f "
        "regime=%s budget=%.1f%% alerts=%d hedges=%d",
        portfolio_val, var_95, cvar_95, regime,
        budget.usage_pct, len(corr_alerts), len(hedges),
    )
    return report


# ---------------------------------------------------------------------------
# Public functions
# ---------------------------------------------------------------------------


def sentinel_report(engine: Engine) -> SentinelReport:
    """Generate a portfolio risk report (standalone).

    Combines correlation analysis, drawdown tracking, VaR/CVaR,
    regime detection, risk budgeting, and hedge suggestions.

    Args:
        engine: The Horizon trading engine instance.

    Returns:
        SentinelReport with all risk metrics populated.
    """
    auth_require_ultra()

    config = SentinelConfig()
    portfolio_val = _portfolio_value(engine)
    returns = _position_returns(engine, config.lookback_returns)

    peak_values: dict[str, float] = {}
    drawdown_levels = _check_drawdowns(engine, config, peak_values)

    triggered = [d for d in drawdown_levels if d.triggered]
    if triggered:
        worst = min(triggered, key=lambda d: d.current_dd)
        logger.warning(
            "Sentinel: drawdown %.1f%% in %s (action=%s)",
            worst.current_dd * 100, worst.scope_id, worst.action,
        )

    baseline: dict[tuple[str, str], float] = {}
    return _build_report(engine, config, returns, baseline, drawdown_levels, portfolio_val)


def suggest_hedges(
    engine: Engine, max_hedges: int = 5, min_reduction_pct: float = 5.0,
) -> list[HedgeSuggestion]:
    """Generate hedge suggestions for the current portfolio.

    Analyzes delta exposure and recommends counter-positions ranked
    by risk reduction percentage.

    Args:
        engine: The Horizon trading engine instance.
        max_hedges: Maximum number of suggestions.
        min_reduction_pct: Minimum risk reduction (%) for inclusion.

    Returns:
        List of HedgeSuggestion sorted by risk_reduction_pct descending.
    """
    auth_require_ultra()

    hedges = _suggest_hedges_internal(engine, max_hedges, min_reduction_pct)
    if hedges:
        logger.info(
            "Generated %d hedge suggestions (total reduction: %.1f%%)",
            len(hedges), sum(h.risk_reduction_pct for h in hedges),
        )
    return hedges


def sentinel(
    config: SentinelConfig | None = None,
    report_interval_cycles: int = 10,
) -> Callable[[Context], None]:
    """Create an always-on portfolio sentinel pipeline function.

    Runs every cycle to track drawdowns and returns. Generates a full
    SentinelReport every ``report_interval_cycles`` cycles.

    Injected params:
        ctx.params["sentinel_report"] -> SentinelReport | None

    Auto-deleverage actions (when enabled):
        - "alert": log a warning
        - "reduce": cancel 50% of open orders (largest first)
        - "pause": cancel all open orders
        - "exit": cancel all orders + submit exit orders

    Args:
        config: Sentinel configuration. Defaults to SentinelConfig().
        report_interval_cycles: Cycles between full reports.

    Returns:
        Pipeline function: (Context) -> None
    """
    auth_require_ultra()

    config = config or SentinelConfig()

    # Closure state
    peak_values: dict[str, float] = {}
    return_history: deque[float] = deque(maxlen=config.lookback_returns)
    cycle_count: int = 0
    baseline_corr: dict[tuple[str, str], float] = {}
    last_report: SentinelReport | None = None
    prev_portfolio_value: float = 0.0

    def _inner(ctx: Context) -> None:
        nonlocal cycle_count, last_report, prev_portfolio_value

        engine = ctx.params.get("engine")
        if engine is None:
            logger.debug("Sentinel: no engine in ctx.params, skipping")
            ctx.params["sentinel_report"] = last_report
            return

        cycle_count += 1

        # --- Every cycle: returns, peaks, drawdowns ---
        try:
            current_value = _portfolio_value(engine)
        except Exception as exc:
            logger.warning("Sentinel: portfolio value failed: %s", exc)
            ctx.params["sentinel_report"] = last_report
            return

        # Cycle-over-cycle return
        if prev_portfolio_value > 0 and current_value > 0:
            ret = (current_value - prev_portfolio_value) / prev_portfolio_value
            if math.isfinite(ret) and abs(ret) < 10.0:
                return_history.append(round(ret, 4))
        prev_portfolio_value = current_value

        # Update portfolio peak
        if current_value > peak_values.get("portfolio", 0.0):
            peak_values["portfolio"] = current_value

        # Per-market peaks
        feeds = engine.all_feed_snapshots()
        for pos in engine.positions():
            snap = feeds.get(pos.market_id)
            price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price
            price = max(0.01, min(0.99, price))
            mval = pos.size * price if pos.side == Side.Yes else pos.size * (1.0 - price)
            if mval > peak_values.get(pos.market_id, 0.0):
                peak_values[pos.market_id] = mval

        # Check drawdowns
        try:
            dd_levels = _check_drawdowns(engine, config, peak_values)
        except Exception as exc:
            logger.warning("Sentinel: drawdown check failed: %s", exc)
            dd_levels = []

        # --- Auto-deleverage ---
        if config.auto_deleverage:
            triggered = [d for d in dd_levels if d.triggered]
            if triggered:
                action = _worst_action(triggered)
                if action == "reduce":
                    _execute_reduce(engine)
                elif action == "pause":
                    _execute_pause(engine)
                elif action == "exit":
                    _execute_exit(engine)
                elif action == "alert":
                    w = min(triggered, key=lambda d: d.current_dd)
                    logger.warning(
                        "Sentinel ALERT: drawdown %.1f%% in %s/%s",
                        w.current_dd * 100, w.scope, w.scope_id,
                    )

        # --- Full report every N cycles ---
        if cycle_count % report_interval_cycles == 0:
            try:
                last_report = _build_report(
                    engine, config, list(return_history),
                    baseline_corr, dd_levels, current_value,
                )
            except Exception as exc:
                logger.warning("Sentinel: report generation failed: %s", exc)

        ctx.params["sentinel_report"] = last_report

    _inner.__name__ = "sentinel"
    return _inner


# ---------------------------------------------------------------------------
# Deleverage action helpers
# ---------------------------------------------------------------------------


def _worst_action(triggered: list[DrawdownLevel]) -> str:
    """Return the most severe action: alert < reduce < pause < exit."""
    severity = {"alert": 0, "reduce": 1, "pause": 2, "exit": 3}
    worst = "alert"
    for level in triggered:
        if severity.get(level.action, 0) > severity.get(worst, 0):
            worst = level.action
    return worst


def _execute_reduce(engine: Engine) -> None:
    """Cancel 50% of open orders, largest remaining_size first."""
    try:
        orders = engine.open_orders()
        if not orders:
            return
        ordered = sorted(orders, key=lambda o: o.remaining_size, reverse=True)
        n = max(1, len(ordered) // 2)
        canceled = 0
        for o in ordered[:n]:
            try:
                engine.cancel(o.id)
                canceled += 1
            except Exception:
                pass
        logger.warning("Sentinel REDUCE: canceled %d/%d orders", canceled, len(orders))
    except Exception as exc:
        logger.warning("Sentinel: reduce failed: %s", exc)


def _execute_pause(engine: Engine) -> None:
    """Cancel all open orders."""
    try:
        count = engine.cancel_all()
        logger.warning("Sentinel PAUSE: canceled all %d orders", count)
    except Exception as exc:
        logger.warning("Sentinel: pause failed: %s", exc)


def _execute_exit(engine: Engine) -> None:
    """Cancel all orders and submit aggressive exit orders for all positions."""
    try:
        engine.cancel_all()
    except Exception as exc:
        logger.warning("Sentinel: exit cancel failed: %s", exc)

    feeds = engine.all_feed_snapshots()
    for pos in engine.positions():
        snap = feeds.get(pos.market_id)
        price = snap.price if (snap is not None and snap.price > 0) else pos.avg_entry_price

        try:
            from horizon._horizon import OrderRequest, OrderSide

            if pos.side == Side.Yes:
                ep = max(0.01, price - 0.05)
                req = OrderRequest(
                    market_id=pos.market_id, side=Side.Yes,
                    order_side=OrderSide.Sell, size=pos.size, price=ep,
                )
            else:
                ep = min(0.99, price + 0.05)
                req = OrderRequest(
                    market_id=pos.market_id, side=Side.No,
                    order_side=OrderSide.Sell, size=pos.size, price=ep,
                )

            oid = engine.submit_order(req)
            logger.warning(
                "Sentinel EXIT: %s for %s %s x%.1f @ %.2f",
                oid, pos.market_id,
                "yes" if pos.side == Side.Yes else "no", pos.size, ep,
            )
        except Exception as exc:
            logger.warning("Sentinel: exit order for %s failed: %s", pos.market_id, exc)
