"""Advanced backtest example - L2 book simulation, fill models, analytics, walk-forward.

Demonstrates the full backtesting stack:
  1. Synthetic L2 orderbook generation
  2. BookSim exchange with probabilistic fills
  3. Market impact and latency simulation
  4. Calibration analytics and edge decay
  5. Walk-forward optimization
"""

import math

import horizon as hz
from horizon._horizon import Quote, calibration_curve, log_loss, edge_decay
from horizon.walkforward import walk_forward


# ---------------------------------------------------------------------------
# 1. Generate synthetic market data with an L2 orderbook
# ---------------------------------------------------------------------------

def generate_data(n_ticks: int = 500, seed: int = 42):
    """Generate synthetic price + orderbook data for a prediction market.

    Simulates a market that drifts from 0.40 -> 0.65 with mean-reverting noise,
    and builds a realistic L2 book around each price.
    """
    data = []
    book_data = []

    price = 0.40
    rng_state = seed

    for i in range(n_ticks):
        ts = float(i)

        # LCG for deterministic noise
        rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
        noise = (rng_state / 0x7FFFFFFF - 0.5) * 0.02  # +/- 1 cent noise

        # Drift toward 0.65 with mean reversion
        drift = 0.0005 if price < 0.65 else -0.0003
        price = max(0.05, min(0.95, price + drift + noise))

        spread = 0.03 + 0.01 * abs(noise) * 50  # wider spread in volatile ticks
        mid = round(price, 4)
        best_bid = round(mid - spread / 2, 4)
        best_ask = round(mid + spread / 2, 4)

        data.append({"timestamp": ts, "price": mid})

        # Build 5-level book with exponentially decaying depth
        bids = []
        asks = []
        for level in range(5):
            offset = level * 0.005
            depth = max(10.0, 200.0 * math.exp(-0.5 * level))
            bids.append((round(best_bid - offset, 4), round(depth, 1)))
            asks.append((round(best_ask + offset, 4), round(depth, 1)))

        book_data.append({
            "timestamp": ts,
            "bids": bids,
            "asks": asks,
        })

    return data, {"market": book_data}


# ---------------------------------------------------------------------------
# 2. Strategy: adaptive market maker with inventory skew
# ---------------------------------------------------------------------------

_ema_price = 0.0
_ema_alpha = 0.1


def fair_value(ctx: hz.Context) -> float:
    """Compute EMA-smoothed fair value from feed price."""
    global _ema_price
    feed = ctx.feeds.get("default")
    price = feed.price if feed and feed.price > 0 else 0.50

    if _ema_price == 0.0:
        _ema_price = price
    else:
        _ema_price = _ema_alpha * price + (1 - _ema_alpha) * _ema_price

    return _ema_price


def inventory_skew(ctx: hz.Context, fair: float) -> float:
    """Skew the fair value based on current inventory.

    When long, lower the fair value (more eager to sell).
    When short, raise it (more eager to buy).
    """
    net_pos = 0.0
    for pos in ctx.inventory.positions:
        net_pos += pos.size

    skew = -net_pos * 0.001  # 0.1 cent per contract
    return max(0.01, min(0.99, fair + skew))


def adaptive_quoter(ctx: hz.Context, skewed_fair: float) -> Quote:
    """Quote with a spread that widens when inventory is high."""
    net_pos = 0.0
    for pos in ctx.inventory.positions:
        net_pos += pos.size

    base_spread = 0.04
    inventory_penalty = abs(net_pos) * 0.002
    spread = min(base_spread + inventory_penalty, 0.15)

    bid = max(0.01, skewed_fair - spread / 2)
    ask = min(0.99, skewed_fair + spread / 2)

    return Quote(round(bid, 4), round(ask, 4), 5)


# ---------------------------------------------------------------------------
# 3. Run backtest with L2 book simulation
# ---------------------------------------------------------------------------

print("=" * 60)
print("  ADVANCED BACKTEST: L2 Book Simulation")
print("=" * 60)

data, book_data = generate_data(500)

# Probabilistic fills: not every order fills (realistic for thin markets)
result = hz.backtest(
    name="advanced_l2",
    markets=["market"],
    data=data,
    book_data=book_data,
    pipeline=[fair_value, inventory_skew, adaptive_quoter],
    risk=hz.Risk(max_position=100, max_drawdown_pct=20),
    fill_model="probabilistic",
    fill_model_params={"lambda": 2.0, "queue_frac": 0.3},
    impact_temporary_bps=50.0,       # 50 bps temporary impact
    impact_permanent_fraction=0.1,   # 10% of impact persists
    latency_ms=500.0,               # 500ms simulated latency
    rng_seed=42,
    initial_capital=1000.0,
)

print(result.summary())
print(f"\n{len(result.trades)} fills across {len(result.equity_curve)} ticks")
print(f"  Fill rate: {len(result.trades) / max(1, len(result.equity_curve)) * 100:.1f}%")


# ---------------------------------------------------------------------------
# 4. Deterministic comparison (no stochastic fills)
# ---------------------------------------------------------------------------

print("\n" + "=" * 60)
print("  COMPARISON: Deterministic fills (no fill model)")
print("=" * 60)

# Reset EMA state
_ema_price = 0.0

result_det = hz.backtest(
    name="deterministic",
    markets=["market"],
    data=data,
    book_data=book_data,
    pipeline=[fair_value, inventory_skew, adaptive_quoter],
    risk=hz.Risk(max_position=100, max_drawdown_pct=20),
    fill_model="deterministic",
    initial_capital=1000.0,
)

print(f"  Deterministic: {len(result_det.trades)} fills, "
      f"return={result_det.metrics.total_return_pct:+.2f}%")
print(f"  Probabilistic: {len(result.trades)} fills, "
      f"return={result.metrics.total_return_pct:+.2f}%")
print(f"  -> Probabilistic fill model reduces fill count (more realistic)")


# ---------------------------------------------------------------------------
# 5. Calibration analytics
# ---------------------------------------------------------------------------

print("\n" + "=" * 60)
print("  ANALYTICS: Calibration & Edge Decay")
print("=" * 60)

# Simulate predictions vs outcomes for analytics demo
# (in production, these come from resolved markets)
predictions = [0.35, 0.70, 0.45, 0.80, 0.20, 0.90, 0.55, 0.60]
outcomes = [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0]

cal = calibration_curve(predictions, outcomes, 5)
ll = log_loss(predictions, outcomes)

print(f"  Brier Score: {cal.brier_score:.4f} (lower is better, 0 = perfect)")
print(f"  Log-Loss:    {cal.log_loss:.4f}")
print(f"  ECE:         {cal.ece:.4f} (expected calibration error)")
print(f"  Bins:")
for center, freq, count in cal.bins:
    if count > 0:
        print(f"    [{center:.2f}] predicted vs actual={freq:.2f} (n={count})")

# Edge decay: how edge erodes as resolution approaches
entry_prices = [0.35, 0.40, 0.45, 0.50]
entry_outcomes = [1.0, 1.0, 0.0, 0.0]
entry_ts = [0.0, 3600.0, 7200.0, 10800.0]      # 0h, 1h, 2h, 3h before resolution
resolution_ts = [14400.0, 14400.0, 14400.0, 14400.0]  # all resolve at 4h

decay = edge_decay(entry_prices, entry_outcomes, entry_ts, resolution_ts, 4)
print(f"\n  Edge Decay (half-life: {decay.half_life_hours:.1f}h):")
for hours, avg_edge in decay.decay_curve:
    bar = "#" * int(abs(avg_edge) * 40)
    print(f"    {hours:5.1f}h: edge={avg_edge:+.3f} {bar}")


# ---------------------------------------------------------------------------
# 6. Walk-forward optimization
# ---------------------------------------------------------------------------

print("\n" + "=" * 60)
print("  WALK-FORWARD OPTIMIZATION")
print("=" * 60)

data_wf, _ = generate_data(300, seed=99)


def pipeline_factory(params):
    """Create a pipeline with parameterized spread."""
    spread = params.get("spread", 0.04)

    def quoter(ctx):
        feed = ctx.feeds.get("default")
        price = feed.price if feed and feed.price > 0 else 0.50
        bid = max(0.01, price - spread / 2)
        ask = min(0.99, price + spread / 2)
        return Quote(round(bid, 4), round(ask, 4), params.get("size", 5))

    return [quoter]


wf_result = walk_forward(
    data=data_wf,
    pipeline_factory=pipeline_factory,
    param_grid={
        "spread": [0.02, 0.04, 0.06, 0.08],
        "size": [3, 5, 10],
    },
    n_splits=3,
    train_ratio=0.7,
    objective="sharpe_ratio",
    initial_capital=1000.0,
)

print(f"  Windows: {len(wf_result.windows)}")
for i, (window, params) in enumerate(
    zip(wf_result.windows, wf_result.best_params_per_window)
):
    print(
        f"    Window {i + 1}: train=[{window.train_start:.0f}-{window.train_end:.0f}] "
        f"test=[{window.test_start:.0f}-{window.test_end:.0f}] "
        f"best={params}"
    )

print(f"\n  Aggregate OOS metrics:")
print(f"    Sharpe:       {wf_result.aggregate_metrics.sharpe_ratio:.3f}")
print(f"    Total Return: {wf_result.aggregate_metrics.total_return_pct:+.2f}%")
print(f"    Total Trades: {wf_result.aggregate_metrics.total_trades}")

print("\n" + "=" * 60)
print("  DONE")
print("=" * 60)
