{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Simulación de Mercado con Agentes\n",
    "\n",
    "Simulador agent-based donde **makers** colocan bids/asks y **takers** lanzan market orders.\n",
    "El precio emerge de la interacción entre ellos.\n",
    "\n",
    "## Parámetros ajustables\n",
    "\n",
    "| Parámetro | Qué controla |\n",
    "|---|---|\n",
    "| `sigma` | Volatilidad del precio (cuánto se mueve) |\n",
    "| `mu` | Drift/tendencia (positivo = sube, negativo = baja) |\n",
    "| `n_makers` | Cuántos market makers hay poniendo liquidez |\n",
    "| `n_takers_lambda` | Ritmo de llegada de takers (órdenes/tick) |\n",
    "| `maker_spread` | Spread base que los makers quieren capturar |\n",
    "| `gamma` | Aversión al riesgo del maker (alto = ajusta más por inventario) |\n",
    "| `taker_size_alpha` | Exponente power-law para tamaño de órdenes (bajo = más ballenas) |\n",
    "| `hawkes_alpha` | Contagio entre trades (alto = más ráfagas) |\n",
    "| `hawkes_beta` | Decaimiento del contagio (alto = ráfagas más cortas) |\n",
    "| `jump_intensity` | Frecuencia de saltos bruscos de precio |\n",
    "| `jump_size_std` | Tamaño promedio de los saltos |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tipos definidos: Side, OrderType, OrderStatus, Order, Trade\n",
      "OrderBook definido\n",
      "MatchingEngineFIFO definido\n",
      "✓ test_basic_match\n",
      "✓ test_partial_fill\n",
      "✓ test_fifo_priority\n",
      "✓ test_price_priority\n",
      "✓ test_no_match_spread\n",
      "✓ test_market_order\n",
      "✓ test_cancel\n",
      "\n",
      "=== Todos los tests pasaron ===\n",
      "OrderBook(best_bid=99.85, best_ask=100.25, spread=0.4000000000000057, bids_top3=[(99.85, 4.8), (99.72, 2.8), (99.48, 22.799999999999997)], asks_top3=[(100.25, 5.4), (100.29, 6.3), (100.41, 8.2)])\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 51756 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== ANTES ===\n",
      "Best ask: 100.25\n",
      "Best bid: 99.85\n",
      "Spread: 0.4000\n",
      "Midprice: 100.0500\n",
      "\n",
      "Ask depth (5 niveles): [(100.25, 5.4), (100.29, 6.3), (100.41, 8.2), (100.43, 14.9), (100.5, 18.9)]\n",
      "\n",
      "=== MARKET BUY 50 ===\n",
      "Trades ejecutados: 5\n",
      "  5.4 @ 100.25\n",
      "  6.3 @ 100.29\n",
      "  8.2 @ 100.41\n",
      "  14.9 @ 100.43\n",
      "  15.2 @ 100.50\n",
      "\n",
      "Precio promedio ponderado: 100.4109\n",
      "Slippage vs best ask: 0.1609\n",
      "\n",
      "=== DESPUÉS ===\n",
      "Best ask: 100.5\n",
      "Best bid: 99.85\n",
      "Spread: 0.6500000000000057\n",
      "OrderBook(best_bid=99.85, best_ask=100.5, spread=0.6500000000000057, bids_top3=[(99.85, 4.8), (99.72, 2.8), (99.48, 22.799999999999997)], asks_top3=[(100.5, 3.699999999999994), (100.53, 6.5), (100.54, 1.6)])\n",
      "Engine cargado desde notebook 01\n"
     ]
    }
   ],
   "source": [
    "# Cargar el matching engine del notebook 01\n",
    "import os, sys\n",
    "sys.path.insert(0, os.path.join(os.environ.get(\"FN_REGISTRY_ROOT\", os.path.expanduser(\"~/fn_registry\"))))\n",
    "\n",
    "# Re-ejecutar las definiciones necesarias inline\n",
    "from __future__ import annotations\n",
    "from dataclasses import dataclass, field\n",
    "from enum import Enum\n",
    "from typing import Optional\n",
    "from collections import deque\n",
    "from sortedcontainers import SortedDict\n",
    "import time, uuid\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "%run /home/lucas/fn_registry/analysis/estudio_mercados/notebooks/01_matching_engine_fifo.ipynb\n",
    "print(\"Engine cargado desde notebook 01\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Parámetros de simulación"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Parámetros cargados\n",
      "  Precio inicial: 100.0\n",
      "  Volatilidad: 0.02\n",
      "  Makers: 5 (spread=0.5, γ=0.1)\n",
      "  Takers λ: 2.0 (Hawkes α=0.5)\n",
      "  Ticks: 500\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class SimParams:\n",
    "    \"\"\"Todos los parámetros ajustables de la simulación.\"\"\"\n",
    "\n",
    "    # --- Precio fundamental ---\n",
    "    initial_price: float = 100.0       # precio inicial\n",
    "    mu: float = 0.0                    # drift (tendencia): 0 = sin tendencia\n",
    "    sigma: float = 0.02                # volatilidad por tick (2%)\n",
    "\n",
    "    # --- Saltos (jump-diffusion) ---\n",
    "    jump_intensity: float = 0.02       # prob de salto por tick (2%)\n",
    "    jump_size_std: float = 0.05        # std del tamaño del salto (5%)\n",
    "\n",
    "    # --- Makers ---\n",
    "    n_makers: int = 5                  # número de market makers\n",
    "    maker_spread: float = 0.5          # spread base (en unidades de precio)\n",
    "    maker_qty: float = 10.0            # qty base por orden de maker\n",
    "    gamma: float = 0.1                 # aversión al riesgo (Avellaneda-Stoikov)\n",
    "    maker_levels: int = 3              # niveles de profundidad que pone cada maker\n",
    "\n",
    "    # --- Takers ---\n",
    "    n_takers_lambda: float = 2.0       # media de takers por tick (Poisson)\n",
    "    taker_size_alpha: float = 2.0      # exponente power-law para tamaño (mayor = menos ballenas)\n",
    "    taker_size_min: float = 1.0        # tamaño mínimo de orden taker\n",
    "    taker_size_max: float = 100.0      # tamaño máximo de orden taker\n",
    "\n",
    "    # --- Hawkes (clustering de takers) ---\n",
    "    hawkes_alpha: float = 0.5          # excitación por trade (0 = Poisson puro)\n",
    "    hawkes_beta: float = 1.0           # decaimiento de excitación\n",
    "\n",
    "    # --- Simulación ---\n",
    "    n_ticks: int = 500                 # duración de la simulación\n",
    "    seed: int = 42                     # semilla para reproducibilidad\n",
    "\n",
    "\n",
    "params = SimParams()\n",
    "print(\"Parámetros cargados\")\n",
    "print(f\"  Precio inicial: {params.initial_price}\")\n",
    "print(f\"  Volatilidad: {params.sigma}\")\n",
    "print(f\"  Makers: {params.n_makers} (spread={params.maker_spread}, γ={params.gamma})\")\n",
    "print(f\"  Takers λ: {params.n_takers_lambda} (Hawkes α={params.hawkes_alpha})\")\n",
    "print(f\"  Ticks: {params.n_ticks}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Generador de precio fundamental\n",
    "\n",
    "El \"precio verdadero\" que los agentes intentan seguir. Usa **jump-diffusion**:\n",
    "- La mayor parte del tiempo se mueve suavemente (GBM)\n",
    "- De vez en cuando da un salto brusco (jump)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 56404 characters]",
      "text/plain": [
       "<Figure size 1200x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    }
   ],
   "source": [
    "def generate_fundamental_prices(p: SimParams) -> np.ndarray:\n",
    "    \"\"\"Genera serie de precios fundamentales con jump-diffusion.\n",
    "\n",
    "    S(t+1) = S(t) * exp((mu - sigma²/2)*dt + sigma*sqrt(dt)*Z + J*N)\n",
    "    donde Z ~ N(0,1), N ~ Bernoulli(jump_intensity), J ~ N(0, jump_size_std)\n",
    "    \"\"\"\n",
    "    rng = np.random.default_rng(p.seed)\n",
    "    prices = np.zeros(p.n_ticks)\n",
    "    prices[0] = p.initial_price\n",
    "\n",
    "    dt = 1.0  # cada tick es una unidad de tiempo\n",
    "\n",
    "    for t in range(1, p.n_ticks):\n",
    "        # GBM component\n",
    "        z = rng.standard_normal()\n",
    "        gbm = (p.mu - 0.5 * p.sigma**2) * dt + p.sigma * np.sqrt(dt) * z\n",
    "\n",
    "        # Jump component\n",
    "        jump = 0.0\n",
    "        if rng.random() < p.jump_intensity:\n",
    "            jump = rng.normal(0, p.jump_size_std)\n",
    "\n",
    "        prices[t] = prices[t-1] * np.exp(gbm + jump)\n",
    "\n",
    "    return prices\n",
    "\n",
    "\n",
    "# Preview\n",
    "fund_prices = generate_fundamental_prices(params)\n",
    "plt.figure(figsize=(12, 3))\n",
    "plt.plot(fund_prices, linewidth=0.8)\n",
    "plt.title(f'Precio fundamental (σ={params.sigma}, jumps={params.jump_intensity})')\n",
    "plt.xlabel('Tick')\n",
    "plt.ylabel('Precio')\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Hawkes process para arrival de takers\n",
    "\n",
    "Genera cuántos takers llegan en cada tick. Con Hawkes, un trade excita más trades:\n",
    "- `hawkes_alpha = 0` → Poisson puro (sin contagio)\n",
    "- `hawkes_alpha > 0` → trades generan más trades (ráfagas)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Takers por tick: min=0, max=8, mean=2.0\n"
     ]
    }
   ],
   "source": [
    "def generate_hawkes_arrivals(p: SimParams, n_trades_per_tick: list[int]) -> list[int]:\n",
    "    \"\"\"Genera número de takers por tick usando Hawkes process.\n",
    "\n",
    "    λ(t) = λ_base + Σ α * exp(-β * (t - tᵢ))\n",
    "    donde tᵢ son los ticks donde hubo trades.\n",
    "    \"\"\"\n",
    "    rng = np.random.default_rng(p.seed + 1)\n",
    "    arrivals = []\n",
    "    excitation = 0.0  # acumulador de excitación\n",
    "\n",
    "    for t in range(p.n_ticks):\n",
    "        # Intensidad actual\n",
    "        lam = p.n_takers_lambda + excitation\n",
    "        lam = max(0.1, lam)  # piso para evitar λ negativo\n",
    "\n",
    "        # Número de takers este tick\n",
    "        n = rng.poisson(lam)\n",
    "        arrivals.append(n)\n",
    "\n",
    "        # Actualizar excitación: decae + se excita por trades\n",
    "        excitation *= np.exp(-p.hawkes_beta)\n",
    "        if t < len(n_trades_per_tick):\n",
    "            excitation += p.hawkes_alpha * n_trades_per_tick[t]\n",
    "        else:\n",
    "            excitation += p.hawkes_alpha * n\n",
    "\n",
    "    return arrivals\n",
    "\n",
    "\n",
    "# Preview con Poisson puro\n",
    "arrivals_preview = generate_hawkes_arrivals(params, [0] * params.n_ticks)\n",
    "print(f\"Takers por tick: min={min(arrivals_preview)}, max={max(arrivals_preview)}, mean={np.mean(arrivals_preview):.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Agentes\n",
    "\n",
    "### Market Maker (Avellaneda-Stoikov)\n",
    "Calcula su **precio de reserva** según inventario:\n",
    "- Si compró mucho → baja sus precios para vender\n",
    "- Si vendió mucho → sube sus precios para comprar\n",
    "\n",
    "### Taker\n",
    "Lanza market orders con tamaño power-law (muchas chicas, pocas grandes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Agentes definidos: MakerState, maker_quotes (Avellaneda-Stoikov), taker_order (power-law)\n"
     ]
    }
   ],
   "source": [
    "@dataclass\n",
    "class MakerState:\n",
    "    \"\"\"Estado interno de un market maker.\"\"\"\n",
    "    maker_id: str\n",
    "    inventory: float = 0.0        # positivo = largo, negativo = corto\n",
    "    pnl: float = 0.0              # profit & loss acumulado\n",
    "    active_order_ids: list = field(default_factory=list)\n",
    "\n",
    "\n",
    "def maker_quotes(state: MakerState, mid: float, p: SimParams, t: int, rng) -> list[Order]:\n",
    "    \"\"\"Genera las órdenes de un maker usando Avellaneda-Stoikov.\n",
    "\n",
    "    Precio de reserva: r = mid - inventory * gamma * sigma²\n",
    "    Spread óptimo: delta = gamma * sigma² + spread_base\n",
    "    \"\"\"\n",
    "    # Precio de reserva: ajustado por inventario\n",
    "    # Si inventory > 0 (compré mucho), r baja → mis asks bajan para vender\n",
    "    # Si inventory < 0 (vendí mucho), r sube → mis bids suben para comprar\n",
    "    reservation = mid - state.inventory * p.gamma * p.sigma**2\n",
    "\n",
    "    # Spread: base + ajuste por volatilidad\n",
    "    half_spread = p.maker_spread / 2 + p.gamma * p.sigma**2 / 2\n",
    "\n",
    "    orders = []\n",
    "    for level in range(p.maker_levels):\n",
    "        offset = level * half_spread * 0.5  # niveles más profundos\n",
    "        qty = p.maker_qty * (1 + level * 0.5)  # más qty en niveles profundos\n",
    "\n",
    "        # Pequeña variación para que los makers no sean idénticos\n",
    "        noise = rng.uniform(-0.05, 0.05)\n",
    "\n",
    "        bid_price = round(reservation - half_spread - offset + noise, 2)\n",
    "        ask_price = round(reservation + half_spread + offset + noise, 2)\n",
    "\n",
    "        if bid_price > 0:\n",
    "            orders.append(Order(side=Side.BUY, price=bid_price, qty=qty))\n",
    "        if ask_price > 0:\n",
    "            orders.append(Order(side=Side.SELL, price=ask_price, qty=qty))\n",
    "\n",
    "    return orders\n",
    "\n",
    "\n",
    "def taker_order(mid: float, p: SimParams, rng) -> Order:\n",
    "    \"\"\"Genera una market order de taker.\n",
    "\n",
    "    Lado: 50/50 compra/venta\n",
    "    Tamaño: power-law (Pareto) truncada\n",
    "    \"\"\"\n",
    "    side = Side.BUY if rng.random() < 0.5 else Side.SELL\n",
    "\n",
    "    # Power-law: P(size > x) ~ x^(-alpha)\n",
    "    # Pareto genera valores >= 1, escalamos al rango deseado\n",
    "    raw_size = (rng.pareto(p.taker_size_alpha) + 1) * p.taker_size_min\n",
    "    size = min(raw_size, p.taker_size_max)\n",
    "    size = round(size, 1)\n",
    "\n",
    "    return Order(side=side, price=0, qty=size, order_type=OrderType.MARKET)\n",
    "\n",
    "\n",
    "print(\"Agentes definidos: MakerState, maker_quotes (Avellaneda-Stoikov), taker_order (power-law)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Loop de simulación\n",
    "\n",
    "En cada tick:\n",
    "1. El precio fundamental se mueve (GBM + jumps)\n",
    "2. Cada maker cancela sus órdenes anteriores y coloca nuevas\n",
    "3. Llegan N takers (Hawkes) y lanzan market orders\n",
    "4. El engine matchea todo\n",
    "5. Se actualizan inventarios y PnL de los makers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "run_simulation() definida\n"
     ]
    }
   ],
   "source": [
    "@dataclass\n",
    "class SimResult:\n",
    "    \"\"\"Resultados de la simulación para análisis.\"\"\"\n",
    "    fundamental_prices: np.ndarray   # precio \"verdadero\"\n",
    "    trade_prices: list[float]        # precio de cada trade\n",
    "    trade_times: list[int]           # tick de cada trade\n",
    "    trade_sizes: list[float]         # tamaño de cada trade\n",
    "    spreads: list[float]             # spread en cada tick\n",
    "    midprices: list[float]           # midprice del book en cada tick\n",
    "    taker_arrivals: list[int]        # takers por tick\n",
    "    maker_states: list[MakerState]   # estado final de los makers\n",
    "    n_trades_per_tick: list[int]     # trades por tick\n",
    "\n",
    "\n",
    "def run_simulation(p: SimParams) -> SimResult:\n",
    "    \"\"\"Ejecuta la simulación completa.\"\"\"\n",
    "    rng = np.random.default_rng(p.seed)\n",
    "\n",
    "    # Generar precios fundamentales\n",
    "    fund_prices = generate_fundamental_prices(p)\n",
    "\n",
    "    # Inicializar engine y makers\n",
    "    engine = MatchingEngineFIFO()\n",
    "    makers = [MakerState(maker_id=f\"maker_{i}\") for i in range(p.n_makers)]\n",
    "\n",
    "    # Resultados\n",
    "    trade_prices, trade_times, trade_sizes = [], [], []\n",
    "    spreads, midprices = [], []\n",
    "    n_trades_per_tick = []\n",
    "\n",
    "    # Hawkes state\n",
    "    hawkes_excitation = 0.0\n",
    "\n",
    "    for t in range(p.n_ticks):\n",
    "        mid = fund_prices[t]\n",
    "\n",
    "        # --- MAKERS: cancelar y recolocar ---\n",
    "        for maker in makers:\n",
    "            # Cancelar órdenes anteriores\n",
    "            for oid in maker.active_order_ids:\n",
    "                engine.cancel(oid)\n",
    "            maker.active_order_ids = []\n",
    "\n",
    "            # Colocar nuevas\n",
    "            quotes = maker_quotes(maker, mid, p, t, rng)\n",
    "            for q in quotes:\n",
    "                engine.submit(q)\n",
    "                maker.active_order_ids.append(q.order_id)\n",
    "\n",
    "        # --- TAKERS: generar con Hawkes ---\n",
    "        lam = p.n_takers_lambda + hawkes_excitation\n",
    "        lam = max(0.1, lam)\n",
    "        n_takers = rng.poisson(lam)\n",
    "\n",
    "        tick_trades = 0\n",
    "        for _ in range(n_takers):\n",
    "            order = taker_order(mid, p, rng)\n",
    "            trades = engine.submit(order)\n",
    "            tick_trades += len(trades)\n",
    "\n",
    "            for tr in trades:\n",
    "                trade_prices.append(tr.price)\n",
    "                trade_times.append(t)\n",
    "                trade_sizes.append(tr.qty)\n",
    "\n",
    "                # Actualizar inventario de makers\n",
    "                for maker in makers:\n",
    "                    if tr.buyer_order_id in maker.active_order_ids:\n",
    "                        maker.inventory += tr.qty\n",
    "                        maker.pnl -= tr.price * tr.qty\n",
    "                    elif tr.seller_order_id in maker.active_order_ids:\n",
    "                        maker.inventory -= tr.qty\n",
    "                        maker.pnl += tr.price * tr.qty\n",
    "\n",
    "        # Hawkes: actualizar excitación\n",
    "        hawkes_excitation *= np.exp(-p.hawkes_beta)\n",
    "        hawkes_excitation += p.hawkes_alpha * tick_trades\n",
    "\n",
    "        n_trades_per_tick.append(tick_trades)\n",
    "\n",
    "        # Registrar estado del book\n",
    "        sp = engine.book.spread\n",
    "        spreads.append(sp if sp is not None else 0.0)\n",
    "        mp = engine.book.midprice\n",
    "        midprices.append(mp if mp is not None else mid)\n",
    "\n",
    "    # PnL final: mark-to-market\n",
    "    final_price = fund_prices[-1]\n",
    "    for maker in makers:\n",
    "        maker.pnl += maker.inventory * final_price\n",
    "\n",
    "    return SimResult(\n",
    "        fundamental_prices=fund_prices,\n",
    "        trade_prices=trade_prices,\n",
    "        trade_times=trade_times,\n",
    "        trade_sizes=trade_sizes,\n",
    "        spreads=spreads,\n",
    "        midprices=midprices,\n",
    "        taker_arrivals=n_trades_per_tick,\n",
    "        maker_states=makers,\n",
    "        n_trades_per_tick=n_trades_per_tick,\n",
    "    )\n",
    "\n",
    "\n",
    "print(\"run_simulation() definida\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Ejecutar simulación base"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trades: 11740\n",
      "Spread promedio: 0.5285\n",
      "Trades/tick promedio: 23.5\n",
      "\n",
      "Estado final de makers:\n",
      "  maker_0: inventario=153.3, PnL=1807.18\n",
      "  maker_1: inventario=77.3, PnL=1399.79\n",
      "  maker_2: inventario=195.5, PnL=77.56\n",
      "  maker_3: inventario=90.9, PnL=-4273.37\n",
      "  maker_4: inventario=-23.7, PnL=-6305.50\n"
     ]
    }
   ],
   "source": [
    "result = run_simulation(params)\n",
    "\n",
    "print(f\"Total trades: {len(result.trade_prices)}\")\n",
    "print(f\"Spread promedio: {np.mean(result.spreads):.4f}\")\n",
    "print(f\"Trades/tick promedio: {np.mean(result.n_trades_per_tick):.1f}\")\n",
    "print(f\"\\nEstado final de makers:\")\n",
    "for m in result.maker_states:\n",
    "    print(f\"  {m.maker_id}: inventario={m.inventory:.1f}, PnL={m.pnl:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Dashboard de resultados"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 264048 characters]",
      "text/plain": [
       "<Figure size 1400x1200 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    }
   ],
   "source": [
    "def plot_simulation(result: SimResult, params: SimParams):\n",
    "    \"\"\"Dashboard de la simulación.\"\"\"\n",
    "    fig, axes = plt.subplots(4, 1, figsize=(14, 12), gridspec_kw={'height_ratios': [3, 1, 1, 1]})\n",
    "\n",
    "    # --- Panel 1: Precio ---\n",
    "    ax = axes[0]\n",
    "    ax.plot(result.fundamental_prices, color='gray', linewidth=0.8, alpha=0.5, label='Fundamental')\n",
    "    ax.plot(result.midprices, color='#3498db', linewidth=0.8, label='Midprice (book)')\n",
    "    if result.trade_prices:\n",
    "        ax.scatter(result.trade_times, result.trade_prices, s=1, alpha=0.3, color='orange', label='Trades')\n",
    "    ax.set_ylabel('Precio')\n",
    "    ax.set_title(f'Simulación: {params.n_makers} makers, λ_takers={params.n_takers_lambda}, '\n",
    "                 f'σ={params.sigma}, γ={params.gamma}, Hawkes α={params.hawkes_alpha}')\n",
    "    ax.legend(loc='upper left', fontsize=8)\n",
    "    ax.grid(True, alpha=0.3)\n",
    "\n",
    "    # --- Panel 2: Spread ---\n",
    "    ax = axes[1]\n",
    "    ax.fill_between(range(len(result.spreads)), result.spreads, color='#9b59b6', alpha=0.5)\n",
    "    ax.set_ylabel('Spread')\n",
    "    ax.set_ylim(0, np.percentile(result.spreads, 99) * 1.5 if result.spreads else 1)\n",
    "    ax.grid(True, alpha=0.3)\n",
    "\n",
    "    # --- Panel 3: Trades por tick ---\n",
    "    ax = axes[2]\n",
    "    ax.bar(range(len(result.n_trades_per_tick)), result.n_trades_per_tick,\n",
    "           color='#e67e22', alpha=0.6, width=1.0)\n",
    "    ax.set_ylabel('Trades/tick')\n",
    "    ax.grid(True, alpha=0.3)\n",
    "\n",
    "    # --- Panel 4: Volumen por trade ---\n",
    "    ax = axes[3]\n",
    "    if result.trade_sizes:\n",
    "        ax.scatter(result.trade_times, result.trade_sizes, s=2, alpha=0.4, color='#2ecc71')\n",
    "    ax.set_ylabel('Tamaño orden')\n",
    "    ax.set_xlabel('Tick')\n",
    "    ax.set_yscale('log')\n",
    "    ax.grid(True, alpha=0.3)\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_simulation(result, params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Experimentos: comparar escenarios\n",
    "\n",
    "Ajusta los parámetros y observa cómo cambia el mercado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 107672 characters]",
      "text/plain": [
       "<Figure size 1400x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Spread promedio → Tranquilo: 0.4886, Volátil: 0.5116\n"
     ]
    }
   ],
   "source": [
    "# --- Experimento 1: Mercado tranquilo vs volátil ---\n",
    "\n",
    "calm = SimParams(sigma=0.005, jump_intensity=0.0, n_ticks=300, seed=42)\n",
    "volatile = SimParams(sigma=0.05, jump_intensity=0.1, jump_size_std=0.08, n_ticks=300, seed=42)\n",
    "\n",
    "r_calm = run_simulation(calm)\n",
    "r_volatile = run_simulation(volatile)\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
    "\n",
    "axes[0].plot(r_calm.midprices, color='#3498db')\n",
    "axes[0].set_title(f'Tranquilo (σ={calm.sigma}, jumps=0)')\n",
    "axes[0].set_ylabel('Precio')\n",
    "axes[0].grid(True, alpha=0.3)\n",
    "\n",
    "axes[1].plot(r_volatile.midprices, color='#e74c3c')\n",
    "axes[1].set_title(f'Volátil (σ={volatile.sigma}, jumps={volatile.jump_intensity})')\n",
    "axes[1].grid(True, alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "print(f\"Spread promedio → Tranquilo: {np.mean(r_calm.spreads):.4f}, Volátil: {np.mean(r_volatile.spreads):.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 125256 characters]",
      "text/plain": [
       "<Figure size 1400x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    }
   ],
   "source": [
    "# --- Experimento 2: Pocos makers vs muchos makers ---\n",
    "\n",
    "few_makers = SimParams(n_makers=1, n_ticks=300, seed=42)\n",
    "many_makers = SimParams(n_makers=10, n_ticks=300, seed=42)\n",
    "\n",
    "r_few = run_simulation(few_makers)\n",
    "r_many = run_simulation(many_makers)\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
    "\n",
    "axes[0].fill_between(range(len(r_few.spreads)), r_few.spreads, color='#e74c3c', alpha=0.5)\n",
    "axes[0].set_title(f'1 maker → spread promedio: {np.mean(r_few.spreads):.4f}')\n",
    "axes[0].set_ylabel('Spread')\n",
    "axes[0].grid(True, alpha=0.3)\n",
    "\n",
    "axes[1].fill_between(range(len(r_many.spreads)), r_many.spreads, color='#2ecc71', alpha=0.5)\n",
    "axes[1].set_title(f'10 makers → spread promedio: {np.mean(r_many.spreads):.4f}')\n",
    "axes[1].grid(True, alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 108180 characters]",
      "text/plain": [
       "<Figure size 1400x600 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Max trades/tick → Poisson: 8, Hawkes: 295\n"
     ]
    }
   ],
   "source": [
    "# --- Experimento 3: Sin Hawkes vs con Hawkes fuerte ---\n",
    "\n",
    "no_hawkes = SimParams(hawkes_alpha=0.0, n_ticks=300, seed=42)\n",
    "strong_hawkes = SimParams(hawkes_alpha=1.5, hawkes_beta=0.5, n_ticks=300, seed=42)\n",
    "\n",
    "r_no_h = run_simulation(no_hawkes)\n",
    "r_strong_h = run_simulation(strong_hawkes)\n",
    "\n",
    "fig, axes = plt.subplots(2, 2, figsize=(14, 6))\n",
    "\n",
    "axes[0][0].bar(range(len(r_no_h.n_trades_per_tick)), r_no_h.n_trades_per_tick,\n",
    "               color='#3498db', alpha=0.6, width=1.0)\n",
    "axes[0][0].set_title('Poisson puro (hawkes_alpha=0)')\n",
    "axes[0][0].set_ylabel('Trades/tick')\n",
    "\n",
    "axes[0][1].bar(range(len(r_strong_h.n_trades_per_tick)), r_strong_h.n_trades_per_tick,\n",
    "               color='#e74c3c', alpha=0.6, width=1.0)\n",
    "axes[0][1].set_title('Hawkes fuerte (alpha=1.5, beta=0.5)')\n",
    "\n",
    "axes[1][0].plot(r_no_h.midprices, color='#3498db', linewidth=0.8)\n",
    "axes[1][0].set_ylabel('Midprice')\n",
    "\n",
    "axes[1][1].plot(r_strong_h.midprices, color='#e74c3c', linewidth=0.8)\n",
    "\n",
    "for ax in axes.flat:\n",
    "    ax.grid(True, alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "print(f\"Max trades/tick → Poisson: {max(r_no_h.n_trades_per_tick)}, Hawkes: {max(r_strong_h.n_trades_per_tick)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 132704 characters]",
      "text/plain": [
       "<Figure size 1400x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "PnL makers γ=0.01: ['4409', '4499', '1635', '-2600', '-3960']\n",
      "PnL makers γ=1.0: ['300', '-918', '47', '2267', '433']\n"
     ]
    }
   ],
   "source": [
    "# --- Experimento 4: Gamma bajo vs alto (aversión al riesgo del maker) ---\n",
    "\n",
    "low_gamma = SimParams(gamma=0.01, n_ticks=300, seed=42)\n",
    "high_gamma = SimParams(gamma=1.0, n_ticks=300, seed=42)\n",
    "\n",
    "r_low_g = run_simulation(low_gamma)\n",
    "r_high_g = run_simulation(high_gamma)\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
    "\n",
    "axes[0].fill_between(range(len(r_low_g.spreads)), r_low_g.spreads, color='#2ecc71', alpha=0.5)\n",
    "axes[0].set_title(f'γ={low_gamma.gamma} (maker agresivo)\\nspread prom: {np.mean(r_low_g.spreads):.4f}')\n",
    "axes[0].set_ylabel('Spread')\n",
    "axes[0].grid(True, alpha=0.3)\n",
    "\n",
    "axes[1].fill_between(range(len(r_high_g.spreads)), r_high_g.spreads, color='#e74c3c', alpha=0.5)\n",
    "axes[1].set_title(f'γ={high_gamma.gamma} (maker conservador)\\nspread prom: {np.mean(r_high_g.spreads):.4f}')\n",
    "axes[1].grid(True, alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "print(f\"\\nPnL makers γ={low_gamma.gamma}: {[f'{m.pnl:.0f}' for m in r_low_g.maker_states]}\")\n",
    "print(f\"PnL makers γ={high_gamma.gamma}: {[f'{m.pnl:.0f}' for m in r_high_g.maker_states]}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
