599 lines
22 KiB
Plaintext
599 lines
22 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Matching Engine FIFO\n",
|
|
"\n",
|
|
"Motor de matching de órdenes con prioridad precio-tiempo (FIFO).\n",
|
|
"\n",
|
|
"**Objetivo:** Implementar un order book con matching FIFO que podamos usar después para simular mercados con datos reales de exchanges.\n",
|
|
"\n",
|
|
"**Estructura:**\n",
|
|
"1. Tipos de datos (Order, Trade, OrderBook)\n",
|
|
"2. Order Book con inserción y cancelación\n",
|
|
"3. Matching engine FIFO\n",
|
|
"4. Tests y visualización"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 1. Tipos de datos"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from __future__ import annotations\n",
|
|
"from dataclasses import dataclass, field\n",
|
|
"from enum import Enum\n",
|
|
"from typing import Optional\n",
|
|
"from collections import defaultdict\n",
|
|
"from sortedcontainers import SortedDict\n",
|
|
"import time\n",
|
|
"import uuid\n",
|
|
"\n",
|
|
"\n",
|
|
"class Side(Enum):\n",
|
|
" BUY = \"buy\"\n",
|
|
" SELL = \"sell\"\n",
|
|
"\n",
|
|
"\n",
|
|
"class OrderType(Enum):\n",
|
|
" LIMIT = \"limit\"\n",
|
|
" MARKET = \"market\"\n",
|
|
"\n",
|
|
"\n",
|
|
"class OrderStatus(Enum):\n",
|
|
" NEW = \"new\"\n",
|
|
" PARTIAL = \"partial\"\n",
|
|
" FILLED = \"filled\"\n",
|
|
" CANCELLED = \"cancelled\"\n",
|
|
"\n",
|
|
"\n",
|
|
"@dataclass\n",
|
|
"class Order:\n",
|
|
" \"\"\"Una orden en el libro.\"\"\"\n",
|
|
" side: Side\n",
|
|
" price: float # 0 para market orders\n",
|
|
" qty: float # cantidad original\n",
|
|
" remaining: float = 0 # cantidad pendiente\n",
|
|
" order_type: OrderType = OrderType.LIMIT\n",
|
|
" order_id: str = field(default_factory=lambda: str(uuid.uuid4()))\n",
|
|
" timestamp: float = field(default_factory=time.time)\n",
|
|
" status: OrderStatus = OrderStatus.NEW\n",
|
|
"\n",
|
|
" def __post_init__(self):\n",
|
|
" if self.remaining == 0:\n",
|
|
" self.remaining = self.qty\n",
|
|
"\n",
|
|
"\n",
|
|
"@dataclass\n",
|
|
"class Trade:\n",
|
|
" \"\"\"Un trade ejecutado por el matching engine.\"\"\"\n",
|
|
" price: float\n",
|
|
" qty: float\n",
|
|
" buyer_order_id: str\n",
|
|
" seller_order_id: str\n",
|
|
" timestamp: float = field(default_factory=time.time)\n",
|
|
" trade_id: str = field(default_factory=lambda: str(uuid.uuid4()))\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"Tipos definidos: Side, OrderType, OrderStatus, Order, Trade\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 2. Order Book\n",
|
|
"\n",
|
|
"Estructura del libro de órdenes:\n",
|
|
"- **Bids** (compras): ordenados por precio descendente, FIFO dentro del mismo precio\n",
|
|
"- **Asks** (ventas): ordenados por precio ascendente, FIFO dentro del mismo precio\n",
|
|
"\n",
|
|
"Usamos `SortedDict` para mantener los niveles de precio ordenados y `deque` para la cola FIFO en cada nivel."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from collections import deque\n",
|
|
"\n",
|
|
"\n",
|
|
"class OrderBook:\n",
|
|
" \"\"\"Libro de órdenes con niveles de precio ordenados y colas FIFO por nivel.\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self):\n",
|
|
" # SortedDict: price -> deque[Order]\n",
|
|
" # Bids: negamos el precio para que SortedDict ordene desc\n",
|
|
" self._bids: SortedDict = SortedDict() # key = -price\n",
|
|
" self._asks: SortedDict = SortedDict() # key = price\n",
|
|
" self._orders: dict[str, Order] = {} # order_id -> Order (lookup rápido)\n",
|
|
"\n",
|
|
" def add(self, order: Order) -> None:\n",
|
|
" \"\"\"Añade una orden al libro (sin matching, solo inserción).\"\"\"\n",
|
|
" book = self._bids if order.side == Side.BUY else self._asks\n",
|
|
" key = -order.price if order.side == Side.BUY else order.price\n",
|
|
"\n",
|
|
" if key not in book:\n",
|
|
" book[key] = deque()\n",
|
|
" book[key].append(order)\n",
|
|
" self._orders[order.order_id] = order\n",
|
|
"\n",
|
|
" def cancel(self, order_id: str) -> Optional[Order]:\n",
|
|
" \"\"\"Cancela una orden por ID. Retorna la orden cancelada o None.\"\"\"\n",
|
|
" order = self._orders.pop(order_id, None)\n",
|
|
" if order is None:\n",
|
|
" return None\n",
|
|
"\n",
|
|
" book = self._bids if order.side == Side.BUY else self._asks\n",
|
|
" key = -order.price if order.side == Side.BUY else order.price\n",
|
|
"\n",
|
|
" if key in book:\n",
|
|
" q = book[key]\n",
|
|
" try:\n",
|
|
" q.remove(order)\n",
|
|
" except ValueError:\n",
|
|
" pass\n",
|
|
" if not q:\n",
|
|
" del book[key]\n",
|
|
"\n",
|
|
" order.status = OrderStatus.CANCELLED\n",
|
|
" return order\n",
|
|
"\n",
|
|
" @property\n",
|
|
" def best_bid(self) -> Optional[float]:\n",
|
|
" \"\"\"Mejor precio de compra.\"\"\"\n",
|
|
" if not self._bids:\n",
|
|
" return None\n",
|
|
" return -self._bids.peekitem(0)[0]\n",
|
|
"\n",
|
|
" @property\n",
|
|
" def best_ask(self) -> Optional[float]:\n",
|
|
" \"\"\"Mejor precio de venta.\"\"\"\n",
|
|
" if not self._asks:\n",
|
|
" return None\n",
|
|
" return self._asks.peekitem(0)[0]\n",
|
|
"\n",
|
|
" @property\n",
|
|
" def spread(self) -> Optional[float]:\n",
|
|
" \"\"\"Spread bid-ask.\"\"\"\n",
|
|
" if self.best_bid is None or self.best_ask is None:\n",
|
|
" return None\n",
|
|
" return self.best_ask - self.best_bid\n",
|
|
"\n",
|
|
" @property\n",
|
|
" def midprice(self) -> Optional[float]:\n",
|
|
" \"\"\"Precio medio.\"\"\"\n",
|
|
" if self.best_bid is None or self.best_ask is None:\n",
|
|
" return None\n",
|
|
" return (self.best_bid + self.best_ask) / 2\n",
|
|
"\n",
|
|
" def depth(self, side: Side, levels: int = 5) -> list[tuple[float, float]]:\n",
|
|
" \"\"\"Profundidad del libro: [(price, total_qty), ...] para N niveles.\"\"\"\n",
|
|
" book = self._bids if side == Side.BUY else self._asks\n",
|
|
" result = []\n",
|
|
" for key in book.islice(0, levels):\n",
|
|
" price = -key if side == Side.BUY else key\n",
|
|
" total_qty = sum(o.remaining for o in book[key])\n",
|
|
" result.append((price, total_qty))\n",
|
|
" return result\n",
|
|
"\n",
|
|
" def __repr__(self):\n",
|
|
" bids = self.depth(Side.BUY, 3)\n",
|
|
" asks = self.depth(Side.SELL, 3)\n",
|
|
" return f\"OrderBook(best_bid={self.best_bid}, best_ask={self.best_ask}, spread={self.spread}, bids_top3={bids}, asks_top3={asks})\"\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"OrderBook definido\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 3. Matching Engine FIFO\n",
|
|
"\n",
|
|
"Lógica de matching:\n",
|
|
"1. Orden de **compra** se matchea contra asks (de menor a mayor precio)\n",
|
|
"2. Orden de **venta** se matchea contra bids (de mayor a menor precio)\n",
|
|
"3. Dentro de cada nivel de precio: **FIFO** (primera en llegar, primera en ejecutarse)\n",
|
|
"4. El precio del trade es siempre el de la orden **pasiva** (la que ya estaba en el libro)\n",
|
|
"5. Si la orden agresora no se llena completamente, se inserta en el libro como orden pasiva"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class MatchingEngineFIFO:\n",
|
|
" \"\"\"Motor de matching con prioridad precio-tiempo (FIFO).\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self):\n",
|
|
" self.book = OrderBook()\n",
|
|
" self.trades: list[Trade] = []\n",
|
|
"\n",
|
|
" def submit(self, order: Order) -> list[Trade]:\n",
|
|
" \"\"\"Procesa una orden: matchea lo posible y el resto va al libro.\"\"\"\n",
|
|
" new_trades = self._match(order)\n",
|
|
" self.trades.extend(new_trades)\n",
|
|
"\n",
|
|
" # Si queda cantidad y es limit, insertar en el libro\n",
|
|
" if order.remaining > 0 and order.order_type == OrderType.LIMIT:\n",
|
|
" order.status = OrderStatus.PARTIAL if order.remaining < order.qty else OrderStatus.NEW\n",
|
|
" self.book.add(order)\n",
|
|
"\n",
|
|
" return new_trades\n",
|
|
"\n",
|
|
" def _match(self, aggressor: Order) -> list[Trade]:\n",
|
|
" \"\"\"Matchea la orden agresora contra el lado opuesto del libro.\"\"\"\n",
|
|
" trades = []\n",
|
|
"\n",
|
|
" # Seleccionar el lado opuesto\n",
|
|
" if aggressor.side == Side.BUY:\n",
|
|
" passive_book = self.book._asks # asks ordenados asc\n",
|
|
" price_key_fn = lambda k: k # key es el precio directo\n",
|
|
" can_match = lambda passive_price: (\n",
|
|
" aggressor.order_type == OrderType.MARKET or\n",
|
|
" passive_price <= aggressor.price\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" passive_book = self.book._bids # bids ordenados desc (key negado)\n",
|
|
" price_key_fn = lambda k: -k # desnegar para obtener precio real\n",
|
|
" can_match = lambda passive_price: (\n",
|
|
" aggressor.order_type == OrderType.MARKET or\n",
|
|
" passive_price >= aggressor.price\n",
|
|
" )\n",
|
|
"\n",
|
|
" keys_to_remove = []\n",
|
|
"\n",
|
|
" for key in list(passive_book.keys()):\n",
|
|
" if aggressor.remaining <= 0:\n",
|
|
" break\n",
|
|
"\n",
|
|
" passive_price = price_key_fn(key)\n",
|
|
" if not can_match(passive_price):\n",
|
|
" break # los siguientes niveles son peores\n",
|
|
"\n",
|
|
" queue = passive_book[key]\n",
|
|
"\n",
|
|
" while queue and aggressor.remaining > 0:\n",
|
|
" passive = queue[0] # FIFO: primera de la cola\n",
|
|
" fill_qty = min(aggressor.remaining, passive.remaining)\n",
|
|
"\n",
|
|
" # Ejecutar trade al precio pasivo\n",
|
|
" trade = Trade(\n",
|
|
" price=passive_price,\n",
|
|
" qty=fill_qty,\n",
|
|
" buyer_order_id=aggressor.order_id if aggressor.side == Side.BUY else passive.order_id,\n",
|
|
" seller_order_id=passive.order_id if aggressor.side == Side.BUY else aggressor.order_id,\n",
|
|
" )\n",
|
|
" trades.append(trade)\n",
|
|
"\n",
|
|
" # Actualizar cantidades\n",
|
|
" aggressor.remaining -= fill_qty\n",
|
|
" passive.remaining -= fill_qty\n",
|
|
"\n",
|
|
" if passive.remaining <= 0:\n",
|
|
" passive.status = OrderStatus.FILLED\n",
|
|
" queue.popleft()\n",
|
|
" self.book._orders.pop(passive.order_id, None)\n",
|
|
" else:\n",
|
|
" passive.status = OrderStatus.PARTIAL\n",
|
|
"\n",
|
|
" if not queue:\n",
|
|
" keys_to_remove.append(key)\n",
|
|
"\n",
|
|
" # Limpiar niveles vacíos\n",
|
|
" for key in keys_to_remove:\n",
|
|
" del passive_book[key]\n",
|
|
"\n",
|
|
" if aggressor.remaining <= 0:\n",
|
|
" aggressor.status = OrderStatus.FILLED\n",
|
|
"\n",
|
|
" return trades\n",
|
|
"\n",
|
|
" def cancel(self, order_id: str) -> Optional[Order]:\n",
|
|
" \"\"\"Cancela una orden del libro.\"\"\"\n",
|
|
" return self.book.cancel(order_id)\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"MatchingEngineFIFO definido\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 4. Tests básicos"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def test_basic_match():\n",
|
|
" \"\"\"Dos órdenes opuestas al mismo precio → 1 trade.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" # Sell limit a 100\n",
|
|
" sell = Order(side=Side.SELL, price=100.0, qty=10.0)\n",
|
|
" engine.submit(sell)\n",
|
|
"\n",
|
|
" # Buy limit a 100 → debe matchear\n",
|
|
" buy = Order(side=Side.BUY, price=100.0, qty=10.0)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 1, f\"Expected 1 trade, got {len(trades)}\"\n",
|
|
" assert trades[0].price == 100.0\n",
|
|
" assert trades[0].qty == 10.0\n",
|
|
" assert buy.status == OrderStatus.FILLED\n",
|
|
" assert sell.status == OrderStatus.FILLED\n",
|
|
" print(\"✓ test_basic_match\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_partial_fill():\n",
|
|
" \"\"\"Buy de 15 contra sell de 10 → fill parcial, 5 queda en libro.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell = Order(side=Side.SELL, price=100.0, qty=10.0)\n",
|
|
" engine.submit(sell)\n",
|
|
"\n",
|
|
" buy = Order(side=Side.BUY, price=100.0, qty=15.0)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 1\n",
|
|
" assert trades[0].qty == 10.0\n",
|
|
" assert buy.remaining == 5.0\n",
|
|
" assert buy.status == OrderStatus.PARTIAL\n",
|
|
" assert engine.book.best_bid == 100.0\n",
|
|
" print(\"✓ test_partial_fill\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_fifo_priority():\n",
|
|
" \"\"\"Dos sells al mismo precio → la primera se llena primero (FIFO).\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell1 = Order(side=Side.SELL, price=100.0, qty=5.0)\n",
|
|
" sell2 = Order(side=Side.SELL, price=100.0, qty=5.0)\n",
|
|
" engine.submit(sell1)\n",
|
|
" engine.submit(sell2)\n",
|
|
"\n",
|
|
" buy = Order(side=Side.BUY, price=100.0, qty=7.0)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 2, f\"Expected 2 trades, got {len(trades)}\"\n",
|
|
" assert trades[0].qty == 5.0 # sell1 completamente llena\n",
|
|
" assert trades[1].qty == 2.0 # sell2 parcial\n",
|
|
" assert sell1.status == OrderStatus.FILLED\n",
|
|
" assert sell2.status == OrderStatus.PARTIAL\n",
|
|
" assert sell2.remaining == 3.0\n",
|
|
" print(\"✓ test_fifo_priority\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_price_priority():\n",
|
|
" \"\"\"Sell a 99 antes que sell a 100 → buyer obtiene mejor precio.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell_expensive = Order(side=Side.SELL, price=100.0, qty=5.0)\n",
|
|
" sell_cheap = Order(side=Side.SELL, price=99.0, qty=5.0)\n",
|
|
" engine.submit(sell_expensive)\n",
|
|
" engine.submit(sell_cheap)\n",
|
|
"\n",
|
|
" buy = Order(side=Side.BUY, price=100.0, qty=8.0)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 2\n",
|
|
" assert trades[0].price == 99.0 # primero la más barata\n",
|
|
" assert trades[0].qty == 5.0\n",
|
|
" assert trades[1].price == 100.0 # luego la cara\n",
|
|
" assert trades[1].qty == 3.0\n",
|
|
" print(\"✓ test_price_priority\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_no_match_spread():\n",
|
|
" \"\"\"Buy a 99, sell a 100 → no matchea, ambas en libro.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell = Order(side=Side.SELL, price=100.0, qty=10.0)\n",
|
|
" engine.submit(sell)\n",
|
|
"\n",
|
|
" buy = Order(side=Side.BUY, price=99.0, qty=10.0)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 0\n",
|
|
" assert engine.book.best_bid == 99.0\n",
|
|
" assert engine.book.best_ask == 100.0\n",
|
|
" assert engine.book.spread == 1.0\n",
|
|
" print(\"✓ test_no_match_spread\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_market_order():\n",
|
|
" \"\"\"Market order matchea a cualquier precio disponible.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell = Order(side=Side.SELL, price=105.0, qty=10.0)\n",
|
|
" engine.submit(sell)\n",
|
|
"\n",
|
|
" buy = Order(side=Side.BUY, price=0, qty=5.0, order_type=OrderType.MARKET)\n",
|
|
" trades = engine.submit(buy)\n",
|
|
"\n",
|
|
" assert len(trades) == 1\n",
|
|
" assert trades[0].price == 105.0 # al precio de la pasiva\n",
|
|
" assert trades[0].qty == 5.0\n",
|
|
" print(\"✓ test_market_order\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def test_cancel():\n",
|
|
" \"\"\"Cancelar una orden la remueve del libro.\"\"\"\n",
|
|
" engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
" sell = Order(side=Side.SELL, price=100.0, qty=10.0)\n",
|
|
" engine.submit(sell)\n",
|
|
" assert engine.book.best_ask == 100.0\n",
|
|
"\n",
|
|
" cancelled = engine.cancel(sell.order_id)\n",
|
|
" assert cancelled is not None\n",
|
|
" assert cancelled.status == OrderStatus.CANCELLED\n",
|
|
" assert engine.book.best_ask is None\n",
|
|
" print(\"✓ test_cancel\")\n",
|
|
"\n",
|
|
"\n",
|
|
"# Ejecutar todos\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",
|
|
"print(\"\\n=== Todos los tests pasaron ===\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 5. Visualización del Order Book"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import numpy as np\n",
|
|
"\n",
|
|
"\n",
|
|
"def plot_orderbook(engine: MatchingEngineFIFO, levels: int = 10, title: str = \"Order Book\"):\n",
|
|
" \"\"\"Visualiza la profundidad del order book.\"\"\"\n",
|
|
" bids = engine.book.depth(Side.BUY, levels)\n",
|
|
" asks = engine.book.depth(Side.SELL, levels)\n",
|
|
"\n",
|
|
" fig, ax = plt.subplots(figsize=(10, 5))\n",
|
|
"\n",
|
|
" if bids:\n",
|
|
" bid_prices, bid_qtys = zip(*bids)\n",
|
|
" bid_cum = np.cumsum(bid_qtys)\n",
|
|
" ax.barh(range(len(bids)), bid_qtys, color='#2ecc71', alpha=0.7, label='Bids')\n",
|
|
" for i, (p, q) in enumerate(bids):\n",
|
|
" ax.text(q + 0.1, i, f\"{p:.2f} ({q:.1f})\", va='center', fontsize=9)\n",
|
|
"\n",
|
|
" if asks:\n",
|
|
" ask_prices, ask_qtys = zip(*asks)\n",
|
|
" y_offset = len(bids) + 1 # gap visual\n",
|
|
" ax.barh(range(y_offset, y_offset + len(asks)), ask_qtys, color='#e74c3c', alpha=0.7, label='Asks')\n",
|
|
" for i, (p, q) in enumerate(asks):\n",
|
|
" ax.text(q + 0.1, y_offset + i, f\"{p:.2f} ({q:.1f})\", va='center', fontsize=9)\n",
|
|
"\n",
|
|
" ax.set_yticks([])\n",
|
|
" ax.set_xlabel('Quantity')\n",
|
|
" ax.set_title(f\"{title}\\nSpread: {engine.book.spread:.2f} | Mid: {engine.book.midprice:.2f}\" if engine.book.spread else title)\n",
|
|
" ax.legend()\n",
|
|
" plt.tight_layout()\n",
|
|
" plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Crear un libro con varias órdenes para visualizar\n",
|
|
"import random\n",
|
|
"random.seed(42)\n",
|
|
"\n",
|
|
"engine = MatchingEngineFIFO()\n",
|
|
"\n",
|
|
"# Poblar bids alrededor de 100\n",
|
|
"for i in range(20):\n",
|
|
" price = round(100 - random.uniform(0.1, 2.0), 2)\n",
|
|
" qty = round(random.uniform(1, 20), 1)\n",
|
|
" engine.submit(Order(side=Side.BUY, price=price, qty=qty))\n",
|
|
"\n",
|
|
"# Poblar asks alrededor de 100\n",
|
|
"for i in range(20):\n",
|
|
" price = round(100 + random.uniform(0.1, 2.0), 2)\n",
|
|
" qty = round(random.uniform(1, 20), 1)\n",
|
|
" engine.submit(Order(side=Side.SELL, price=price, qty=qty))\n",
|
|
"\n",
|
|
"print(engine.book)\n",
|
|
"plot_orderbook(engine, levels=8, title=\"Order Book Sintético\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 6. Simulación: impacto de una market order\n",
|
|
"\n",
|
|
"Veamos cómo una market order grande barre niveles del libro y mueve el precio."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Estado antes\n",
|
|
"print(\"=== ANTES ===\")\n",
|
|
"print(f\"Best ask: {engine.book.best_ask}\")\n",
|
|
"print(f\"Best bid: {engine.book.best_bid}\")\n",
|
|
"print(f\"Spread: {engine.book.spread:.4f}\")\n",
|
|
"print(f\"Midprice: {engine.book.midprice:.4f}\")\n",
|
|
"print(f\"\\nAsk depth (5 niveles): {engine.book.depth(Side.SELL, 5)}\")\n",
|
|
"\n",
|
|
"# Market buy grande: comprar 50 unidades\n",
|
|
"big_buy = Order(side=Side.BUY, price=0, qty=50.0, order_type=OrderType.MARKET)\n",
|
|
"trades = engine.submit(big_buy)\n",
|
|
"\n",
|
|
"print(f\"\\n=== MARKET BUY 50 ===\")\n",
|
|
"print(f\"Trades ejecutados: {len(trades)}\")\n",
|
|
"for t in trades:\n",
|
|
" print(f\" {t.qty:.1f} @ {t.price:.2f}\")\n",
|
|
"\n",
|
|
"avg_price = sum(t.price * t.qty for t in trades) / sum(t.qty for t in trades) if trades else 0\n",
|
|
"print(f\"\\nPrecio promedio ponderado: {avg_price:.4f}\")\n",
|
|
"print(f\"Slippage vs best ask: {avg_price - trades[0].price:.4f}\" if trades else \"\")\n",
|
|
"\n",
|
|
"print(f\"\\n=== DESPUÉS ===\")\n",
|
|
"print(f\"Best ask: {engine.book.best_ask}\")\n",
|
|
"print(f\"Best bid: {engine.book.best_bid}\")\n",
|
|
"print(f\"Spread: {engine.book.spread}\")\n",
|
|
"print(engine.book)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python",
|
|
"version": "3.13.0"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|