init: estudio_mercados analysis from fn_registry

This commit is contained in:
2026-04-06 00:57:05 +02:00
commit 65460ca942
48 changed files with 1888583 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# JUPYTER HABILITADO EN ESTE ANALISIS
## Reglas OBLIGATORIAS para Claude
### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
- **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
- **SIEMPRE** anadir celdas NUEVAS al final del notebook
- Si hay un error en una celda, crear celda nueva con la correccion
- El historial de trabajo debe quedar intacto para trazabilidad
### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
- **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
- **Inmutabilidad**: nunca mutar datos, crear copias transformadas
- **Composicion**: funciones pequenas que se combinan
- Preferir: `map`, `filter`, `reduce`, list comprehensions
- Evitar: loops con mutacion, `global`, modificar argumentos in-place
### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
- Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
- Compartimos variables y estado del kernel
- **NUNCA usar bash para ejecutar Python en este analisis**
### 4. Verificar Jupyter activo ANTES de ejecutar
- Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`
### 5. Gestion de notebooks
- Notebooks en la carpeta `notebooks/` o subcarpetas
- Si un notebook tiene >50 celdas, crear uno nuevo
- Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`
### 6. Gestion de Python
- **SIEMPRE usar `uv`** para gestionar dependencias
- Anadir paquetes con `uv add nombre_paquete`
### 7. Acceso al fn_registry
- `FN_REGISTRY_ROOT` apunta a la raiz del registry
- Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
- Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`
@@ -0,0 +1,83 @@
"""
fn_registry kernel startup
Autoconfigura acceso al registry en cada notebook.
Generado por write_jupyter_registry_kernel (fn_registry).
"""
import os
import sys
import sqlite3
from pathlib import Path
# ── FN_REGISTRY_ROOT ────────────────────────────────────────
FN_REGISTRY_ROOT = Path("/home/lucas/fn_registry")
os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)
# ── sys.path: importar funciones Python del registry ────────
_python_functions = FN_REGISTRY_ROOT / "python" / "functions"
for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
if _domain.is_dir() and not _domain.name.startswith("_"):
_path = str(_domain)
if _path not in sys.path:
sys.path.insert(0, _path)
# Tambien el directorio padre para imports por dominio: from core import filter_list
_pf = str(_python_functions)
if _pf not in sys.path:
sys.path.insert(0, _pf)
# ── fn_query: consultar registry.db desde el notebook ───────
_REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"
def fn_query(sql, params=()):
"""Ejecuta una consulta SQL sobre registry.db y retorna las filas.
Ejemplos:
fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
"""
if not _REGISTRY_DB.exists():
raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
con = sqlite3.connect(str(_REGISTRY_DB))
con.row_factory = sqlite3.Row
try:
rows = con.execute(sql, params).fetchall()
return [dict(r) for r in rows]
finally:
con.close()
def fn_search(term):
"""Busca funciones y tipos en el registry por nombre o descripcion.
Ejemplo:
fn_search("slice")
fn_search("finance")
"""
fts_term = f"name:{term}* OR description:{term}*"
functions = fn_query(
"SELECT id, kind, purity, lang, description FROM functions "
"WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
"ORDER BY name", (fts_term,)
)
types = fn_query(
"SELECT id, algebraic, lang, description FROM types "
"WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
"ORDER BY name", (fts_term,)
)
return {"functions": functions, "types": types}
def fn_code(function_id):
"""Retorna el codigo fuente de una funcion del registry.
Ejemplo:
print(fn_code("filter_list_py_core"))
"""
rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
if not rows:
raise KeyError(f"Funcion no encontrada: {function_id}")
return rows[0]["code"]
# ── Mensaje de bienvenida ───────────────────────────────────
print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
print(f" registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
print(f" Python functions: {_pf}")
print(f" Helpers: fn_query(), fn_search(), fn_code()")
+1
View File
@@ -0,0 +1 @@
8888
+7
View File
@@ -0,0 +1,7 @@
{
"d7268c60-f6df-4777-b24a-912ed608664f": {
"version": "2.3.0",
"created_at": "2026-04-03T19:29:29.221413+00:00",
"document_version": "2.0.0"
}
}
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
{
"mcpServers": {
"jupyter": {
"command": "/home/lucas/fn_registry/analysis/estudio_mercados/.venv/bin/python",
"args": ["-m", "jupyter_mcp_server.server"],
"env": {
"SERVER_URL": "http://localhost:8888",
"TOKEN": ""
}
}
}
}
+1
View File
@@ -0,0 +1 @@
3.13
View File
File diff suppressed because it is too large Load Diff
+501
View File
@@ -0,0 +1,501 @@
timestamp,open,high,low,close,volume,datetime
1775195160000,66754.01,66774.55,66744.38,66771.25,5.36355,2026-04-03T05:46:00.000000
1775195220000,66771.25,66771.25,66729.05,66736.01,22.33537,2026-04-03T05:47:00.000000
1775195280000,66736.0,66736.01,66720.33,66720.34,28.46124,2026-04-03T05:48:00.000000
1775195340000,66720.34,66721.78,66696.83,66696.84,32.96766,2026-04-03T05:49:00.000000
1775195400000,66696.84,66696.84,66623.44,66625.84,48.91239,2026-04-03T05:50:00.000000
1775195460000,66625.84,66630.48,66576.09,66586.94,76.08788,2026-04-03T05:51:00.000000
1775195520000,66586.94,66586.94,66566.47,66576.66,48.91854,2026-04-03T05:52:00.000000
1775195580000,66576.67,66607.52,66553.31,66571.23,54.06214,2026-04-03T05:53:00.000000
1775195640000,66571.24,66571.24,66548.0,66549.08,41.66811,2026-04-03T05:54:00.000000
1775195700000,66549.08,66587.0,66518.58,66567.93,72.68151,2026-04-03T05:55:00.000000
1775195760000,66567.93,66598.97,66560.42,66579.03,45.79834,2026-04-03T05:56:00.000000
1775195820000,66579.02,66579.02,66555.89,66561.59,43.93982,2026-04-03T05:57:00.000000
1775195880000,66561.58,66579.93,66539.0,66579.93,39.79082,2026-04-03T05:58:00.000000
1775195940000,66579.93,66600.35,66579.92,66600.35,2.93569,2026-04-03T05:59:00.000000
1775196000000,66600.35,66609.02,66600.0,66600.0,2.01119,2026-04-03T06:00:00.000000
1775196060000,66600.01,66610.25,66584.22,66610.25,7.9318,2026-04-03T06:01:00.000000
1775196120000,66610.25,66626.0,66602.65,66626.0,4.44872,2026-04-03T06:02:00.000000
1775196180000,66626.0,66646.37,66625.99,66629.99,7.10476,2026-04-03T06:03:00.000000
1775196240000,66629.99,66629.99,66570.35,66570.36,24.48685,2026-04-03T06:04:00.000000
1775196300000,66570.36,66597.34,66563.0,66597.34,4.50543,2026-04-03T06:05:00.000000
1775196360000,66597.33,66610.0,66588.0,66609.99,1.8859,2026-04-03T06:06:00.000000
1775196420000,66609.99,66616.48,66589.91,66616.48,12.37846,2026-04-03T06:07:00.000000
1775196480000,66616.49,66636.0,66600.36,66600.36,13.47984,2026-04-03T06:08:00.000000
1775196540000,66600.37,66600.37,66558.76,66591.88,9.77952,2026-04-03T06:09:00.000000
1775196600000,66591.88,66629.98,66591.88,66629.98,2.08453,2026-04-03T06:10:00.000000
1775196660000,66629.98,66658.05,66629.98,66657.31,4.29341,2026-04-03T06:11:00.000000
1775196720000,66657.3,66658.05,66650.73,66650.74,1.62141,2026-04-03T06:12:00.000000
1775196780000,66650.74,66650.74,66629.96,66629.97,1.53868,2026-04-03T06:13:00.000000
1775196840000,66629.97,66660.33,66629.96,66660.32,7.05165,2026-04-03T06:14:00.000000
1775196900000,66660.33,66667.15,66620.36,66657.3,4.11344,2026-04-03T06:15:00.000000
1775196960000,66657.31,66657.31,66636.94,66653.66,4.84025,2026-04-03T06:16:00.000000
1775197020000,66653.67,66668.47,66639.71,66657.02,2.35065,2026-04-03T06:17:00.000000
1775197080000,66657.02,66657.02,66620.35,66649.19,4.31623,2026-04-03T06:18:00.000000
1775197140000,66649.18,66668.47,66646.36,66656.37,2.64156,2026-04-03T06:19:00.000000
1775197200000,66656.37,66656.37,66608.45,66608.45,2.38354,2026-04-03T06:20:00.000000
1775197260000,66608.45,66628.0,66601.0,66613.13,4.15268,2026-04-03T06:21:00.000000
1775197320000,66613.12,66640.22,66613.12,66640.22,2.67113,2026-04-03T06:22:00.000000
1775197380000,66640.22,66674.64,66640.21,66674.64,2.28324,2026-04-03T06:23:00.000000
1775197440000,66674.64,66694.22,66633.15,66633.15,16.24791,2026-04-03T06:24:00.000000
1775197500000,66633.15,66670.63,66633.15,66670.62,2.688,2026-04-03T06:25:00.000000
1775197560000,66670.62,66670.63,66646.36,66652.62,3.45023,2026-04-03T06:26:00.000000
1775197620000,66652.62,66652.63,66624.57,66635.4,5.27228,2026-04-03T06:27:00.000000
1775197680000,66635.39,66635.4,66615.16,66629.99,4.09926,2026-04-03T06:28:00.000000
1775197740000,66630.0,66637.16,66619.54,66624.98,2.11746,2026-04-03T06:29:00.000000
1775197800000,66624.98,66629.21,66601.01,66613.9,2.468,2026-04-03T06:30:00.000000
1775197860000,66613.9,66626.55,66579.32,66626.55,11.74893,2026-04-03T06:31:00.000000
1775197920000,66626.54,66657.98,66626.54,66632.56,1.78324,2026-04-03T06:32:00.000000
1775197980000,66632.56,66640.22,66608.32,66633.99,3.13569,2026-04-03T06:33:00.000000
1775198040000,66633.99,66633.99,66631.82,66631.82,1.6266,2026-04-03T06:34:00.000000
1775198100000,66631.83,66718.33,66631.82,66718.33,3.17047,2026-04-03T06:35:00.000000
1775198160000,66718.32,66744.66,66703.99,66738.33,7.71878,2026-04-03T06:36:00.000000
1775198220000,66738.33,66772.0,66717.26,66772.0,8.95205,2026-04-03T06:37:00.000000
1775198280000,66771.99,66772.0,66718.98,66718.98,3.23504,2026-04-03T06:38:00.000000
1775198340000,66718.97,66731.29,66712.88,66719.84,3.02625,2026-04-03T06:39:00.000000
1775198400000,66719.84,66752.48,66719.83,66752.48,7.18074,2026-04-03T06:40:00.000000
1775198460000,66752.47,66752.48,66738.91,66748.02,2.17916,2026-04-03T06:41:00.000000
1775198520000,66748.03,66748.03,66732.13,66739.26,2.45104,2026-04-03T06:42:00.000000
1775198580000,66739.26,66754.37,66739.25,66754.37,2.203,2026-04-03T06:43:00.000000
1775198640000,66754.37,66770.49,66740.63,66740.63,29.66449,2026-04-03T06:44:00.000000
1775198700000,66740.64,66780.0,66740.63,66779.99,14.95583,2026-04-03T06:45:00.000000
1775198760000,66780.0,66800.0,66779.99,66782.42,3.64919,2026-04-03T06:46:00.000000
1775198820000,66782.42,66782.42,66756.15,66756.15,3.1932,2026-04-03T06:47:00.000000
1775198880000,66756.15,66756.15,66715.97,66715.97,9.52683,2026-04-03T06:48:00.000000
1775198940000,66715.98,66764.0,66715.97,66764.0,15.06678,2026-04-03T06:49:00.000000
1775199000000,66764.0,66797.09,66763.99,66787.5,47.66688,2026-04-03T06:50:00.000000
1775199060000,66787.51,66792.0,66765.7,66765.7,21.64904,2026-04-03T06:51:00.000000
1775199120000,66765.7,66765.71,66744.0,66764.02,6.99187,2026-04-03T06:52:00.000000
1775199180000,66764.03,66764.03,66752.31,66758.05,7.96139,2026-04-03T06:53:00.000000
1775199240000,66758.06,66780.44,66732.6,66735.14,29.15909,2026-04-03T06:54:00.000000
1775199300000,66735.14,66764.05,66735.14,66764.04,5.23016,2026-04-03T06:55:00.000000
1775199360000,66764.05,66768.76,66759.48,66768.75,3.64087,2026-04-03T06:56:00.000000
1775199420000,66768.76,66771.22,66748.02,66771.22,4.00973,2026-04-03T06:57:00.000000
1775199480000,66771.22,66788.65,66771.22,66788.65,1.33814,2026-04-03T06:58:00.000000
1775199540000,66788.65,66796.8,66788.64,66796.79,1.48421,2026-04-03T06:59:00.000000
1775199600000,66796.8,66797.61,66788.95,66797.61,5.47762,2026-04-03T07:00:00.000000
1775199660000,66797.61,66813.68,66797.6,66799.22,4.90028,2026-04-03T07:01:00.000000
1775199720000,66799.23,66800.76,66791.77,66800.75,3.42236,2026-04-03T07:02:00.000000
1775199780000,66800.76,66834.49,66800.76,66830.03,6.01416,2026-04-03T07:03:00.000000
1775199840000,66830.03,66830.03,66800.5,66800.5,3.65694,2026-04-03T07:04:00.000000
1775199900000,66800.5,66800.5,66768.77,66768.77,8.73741,2026-04-03T07:05:00.000000
1775199960000,66768.77,66811.46,66758.0,66811.46,6.71524,2026-04-03T07:06:00.000000
1775200020000,66811.47,66850.19,66811.47,66850.19,21.8258,2026-04-03T07:07:00.000000
1775200080000,66850.19,66872.93,66850.19,66864.47,27.69143,2026-04-03T07:08:00.000000
1775200140000,66864.47,66895.13,66864.47,66884.35,22.4525,2026-04-03T07:09:00.000000
1775200200000,66884.35,66911.98,66877.44,66877.45,22.73372,2026-04-03T07:10:00.000000
1775200260000,66877.45,66877.45,66828.0,66828.0,9.21428,2026-04-03T07:11:00.000000
1775200320000,66827.31,66830.34,66801.29,66801.29,25.18619,2026-04-03T07:12:00.000000
1775200380000,66801.28,66819.0,66787.95,66806.89,68.86024,2026-04-03T07:13:00.000000
1775200440000,66806.89,66825.35,66796.97,66818.36,42.69469,2026-04-03T07:14:00.000000
1775200500000,66818.36,66849.42,66810.64,66834.41,13.12959,2026-04-03T07:15:00.000000
1775200560000,66834.41,66836.36,66813.61,66829.66,3.19703,2026-04-03T07:16:00.000000
1775200620000,66829.66,66829.66,66800.5,66800.51,1.88882,2026-04-03T07:17:00.000000
1775200680000,66800.51,66830.95,66800.51,66830.24,3.27785,2026-04-03T07:18:00.000000
1775200740000,66830.23,66844.85,66824.61,66844.84,2.66024,2026-04-03T07:19:00.000000
1775200800000,66844.85,66878.58,66844.85,66858.49,3.05023,2026-04-03T07:20:00.000000
1775200860000,66858.49,66867.36,66847.24,66867.35,3.90897,2026-04-03T07:21:00.000000
1775200920000,66867.36,66872.58,66853.74,66868.99,5.07939,2026-04-03T07:22:00.000000
1775200980000,66869.0,66869.0,66862.03,66862.39,2.57586,2026-04-03T07:23:00.000000
1775201040000,66862.4,66870.0,66839.01,66839.65,13.98753,2026-04-03T07:24:00.000000
1775201100000,66839.66,66859.77,66839.65,66847.7,6.03865,2026-04-03T07:25:00.000000
1775201160000,66847.69,66887.28,66847.69,66887.28,6.91063,2026-04-03T07:26:00.000000
1775201220000,66887.28,66893.68,66880.78,66880.78,3.76091,2026-04-03T07:27:00.000000
1775201280000,66880.78,66898.38,66880.78,66898.37,1.67036,2026-04-03T07:28:00.000000
1775201340000,66898.37,66945.58,66898.37,66942.95,5.67733,2026-04-03T07:29:00.000000
1775201400000,66942.96,66960.18,66937.5,66950.86,8.34496,2026-04-03T07:30:00.000000
1775201460000,66950.86,66950.86,66922.49,66922.5,4.01133,2026-04-03T07:31:00.000000
1775201520000,66922.5,66947.44,66910.0,66947.43,12.82896,2026-04-03T07:32:00.000000
1775201580000,66947.44,67027.75,66947.43,67027.75,20.27123,2026-04-03T07:33:00.000000
1775201640000,67027.75,67091.17,67027.74,67069.96,25.6555,2026-04-03T07:34:00.000000
1775201700000,67069.95,67247.38,67069.95,67164.17,75.31708,2026-04-03T07:35:00.000000
1775201760000,67164.17,67164.17,67070.09,67075.89,11.21993,2026-04-03T07:36:00.000000
1775201820000,67075.88,67148.99,67072.32,67135.92,14.08032,2026-04-03T07:37:00.000000
1775201880000,67135.91,67139.13,67096.87,67129.42,9.48167,2026-04-03T07:38:00.000000
1775201940000,67129.41,67129.42,67076.22,67081.58,13.65929,2026-04-03T07:39:00.000000
1775202000000,67081.57,67118.78,67076.0,67115.77,4.75168,2026-04-03T07:40:00.000000
1775202060000,67115.76,67115.77,67073.02,67100.49,8.02978,2026-04-03T07:41:00.000000
1775202120000,67100.49,67126.86,67085.23,67117.43,13.0991,2026-04-03T07:42:00.000000
1775202180000,67117.42,67117.43,67071.44,67071.44,4.94972,2026-04-03T07:43:00.000000
1775202240000,67071.44,67075.73,67057.41,67075.72,11.02419,2026-04-03T07:44:00.000000
1775202300000,67075.72,67075.73,67049.99,67058.59,11.22612,2026-04-03T07:45:00.000000
1775202360000,67058.59,67066.04,67032.67,67052.4,8.7103,2026-04-03T07:46:00.000000
1775202420000,67052.39,67064.43,67046.59,67048.8,5.07304,2026-04-03T07:47:00.000000
1775202480000,67048.51,67048.52,67032.42,67032.43,4.37636,2026-04-03T07:48:00.000000
1775202540000,67032.42,67057.02,67032.42,67035.77,5.1997,2026-04-03T07:49:00.000000
1775202600000,67035.76,67035.77,67017.0,67017.01,7.60439,2026-04-03T07:50:00.000000
1775202660000,67017.0,67035.01,67010.14,67033.3,23.03664,2026-04-03T07:51:00.000000
1775202720000,67033.3,67070.58,67030.32,67070.58,27.87481,2026-04-03T07:52:00.000000
1775202780000,67071.3,67134.0,67071.3,67133.03,47.24015,2026-04-03T07:53:00.000000
1775202840000,67133.03,67133.04,67037.77,67063.28,117.32921,2026-04-03T07:54:00.000000
1775202900000,67063.28,67075.95,67050.58,67050.59,5.49778,2026-04-03T07:55:00.000000
1775202960000,67050.58,67050.59,67029.0,67049.06,6.61401,2026-04-03T07:56:00.000000
1775203020000,67049.05,67049.06,67037.27,67045.43,7.23732,2026-04-03T07:57:00.000000
1775203080000,67045.43,67045.44,67025.8,67042.07,4.98094,2026-04-03T07:58:00.000000
1775203140000,67042.07,67042.08,67032.77,67042.08,8.25555,2026-04-03T07:59:00.000000
1775203200000,67042.08,67050.02,67033.08,67047.11,5.63711,2026-04-03T08:00:00.000000
1775203260000,67047.11,67079.67,67040.1,67059.61,1.82372,2026-04-03T08:01:00.000000
1775203320000,67059.61,67072.25,67054.37,67068.96,5.25613,2026-04-03T08:02:00.000000
1775203380000,67068.95,67085.72,67046.11,67083.82,5.92799,2026-04-03T08:03:00.000000
1775203440000,67083.82,67118.0,67069.01,67110.02,6.07318,2026-04-03T08:04:00.000000
1775203500000,67110.01,67110.02,67084.11,67088.0,3.24727,2026-04-03T08:05:00.000000
1775203560000,67088.01,67181.21,67088.0,67181.21,22.43708,2026-04-03T08:06:00.000000
1775203620000,67181.21,67222.61,67181.0,67222.6,26.12492,2026-04-03T08:07:00.000000
1775203680000,67222.6,67224.27,67185.0,67185.01,52.35244,2026-04-03T08:08:00.000000
1775203740000,67185.01,67288.0,67185.0,67277.28,27.52814,2026-04-03T08:09:00.000000
1775203800000,67277.52,67284.51,67240.66,67253.51,21.46807,2026-04-03T08:10:00.000000
1775203860000,67252.99,67263.07,67247.83,67247.84,6.5342,2026-04-03T08:11:00.000000
1775203920000,67247.83,67255.63,67211.8,67211.8,43.63354,2026-04-03T08:12:00.000000
1775203980000,67211.79,67211.79,67192.86,67192.99,41.46455,2026-04-03T08:13:00.000000
1775204040000,67192.99,67210.0,67163.11,67169.74,80.66772,2026-04-03T08:14:00.000000
1775204100000,67169.75,67186.01,67166.64,67186.01,3.1101,2026-04-03T08:15:00.000000
1775204160000,67186.01,67222.6,67167.6,67167.6,6.42734,2026-04-03T08:16:00.000000
1775204220000,67167.6,67181.14,67121.92,67121.93,9.15108,2026-04-03T08:17:00.000000
1775204280000,67121.92,67154.65,67121.92,67152.8,3.88994,2026-04-03T08:18:00.000000
1775204340000,67152.81,67159.54,67152.8,67154.69,2.55071,2026-04-03T08:19:00.000000
1775204400000,67154.68,67154.68,67124.0,67135.66,4.33226,2026-04-03T08:20:00.000000
1775204460000,67135.66,67175.97,67135.65,67173.67,11.30521,2026-04-03T08:21:00.000000
1775204520000,67173.67,67173.67,67162.67,67162.67,3.22862,2026-04-03T08:22:00.000000
1775204580000,67162.67,67178.37,67162.67,67178.37,2.38612,2026-04-03T08:23:00.000000
1775204640000,67178.36,67178.37,67164.6,67164.61,1.22166,2026-04-03T08:24:00.000000
1775204700000,67164.6,67171.27,67125.83,67125.84,12.6242,2026-04-03T08:25:00.000000
1775204760000,67125.84,67125.84,67102.0,67102.0,5.40965,2026-04-03T08:26:00.000000
1775204820000,67102.0,67102.01,67066.13,67066.14,7.18303,2026-04-03T08:27:00.000000
1775204880000,67066.13,67068.87,67057.96,67057.96,2.77629,2026-04-03T08:28:00.000000
1775204940000,67057.96,67069.67,67056.24,67056.25,5.57167,2026-04-03T08:29:00.000000
1775205000000,67056.25,67074.9,67056.24,67074.88,27.84393,2026-04-03T08:30:00.000000
1775205060000,67074.89,67112.84,67072.65,67112.84,8.32787,2026-04-03T08:31:00.000000
1775205120000,67112.83,67119.97,67096.13,67111.74,6.10437,2026-04-03T08:32:00.000000
1775205180000,67111.74,67115.56,67090.01,67094.17,2.01094,2026-04-03T08:33:00.000000
1775205240000,67094.18,67123.87,67094.17,67105.85,1.76641,2026-04-03T08:34:00.000000
1775205300000,67105.84,67105.84,67088.78,67088.78,2.57572,2026-04-03T08:35:00.000000
1775205360000,67088.79,67088.79,67073.24,67073.25,19.79366,2026-04-03T08:36:00.000000
1775205420000,67073.25,67073.25,67056.0,67056.0,1.65705,2026-04-03T08:37:00.000000
1775205480000,67056.0,67056.01,67022.57,67033.03,7.94188,2026-04-03T08:38:00.000000
1775205540000,67033.04,67033.04,66996.83,66998.63,21.65561,2026-04-03T08:39:00.000000
1775205600000,66998.64,67015.38,66971.3,66982.66,16.15066,2026-04-03T08:40:00.000000
1775205660000,66982.66,66990.0,66936.0,66954.53,6.03326,2026-04-03T08:41:00.000000
1775205720000,66954.53,66961.43,66926.43,66926.43,3.90457,2026-04-03T08:42:00.000000
1775205780000,66926.44,66936.15,66903.01,66919.98,21.32797,2026-04-03T08:43:00.000000
1775205840000,66919.98,66940.93,66913.28,66934.65,5.28545,2026-04-03T08:44:00.000000
1775205900000,66934.65,66951.55,66915.69,66937.08,19.49201,2026-04-03T08:45:00.000000
1775205960000,66937.08,66937.09,66900.56,66917.61,5.28333,2026-04-03T08:46:00.000000
1775206020000,66917.62,66960.01,66917.62,66951.72,2.491,2026-04-03T08:47:00.000000
1775206080000,66951.73,66991.27,66951.73,66991.27,3.86657,2026-04-03T08:48:00.000000
1775206140000,66991.27,67011.94,66983.75,66989.15,6.97903,2026-04-03T08:49:00.000000
1775206200000,66989.15,66991.07,66964.53,66980.99,4.04158,2026-04-03T08:50:00.000000
1775206260000,66981.0,66999.99,66980.99,66994.97,3.05455,2026-04-03T08:51:00.000000
1775206320000,66994.97,66994.97,66960.4,66979.9,5.02731,2026-04-03T08:52:00.000000
1775206380000,66979.9,66979.9,66940.97,66964.19,5.91942,2026-04-03T08:53:00.000000
1775206440000,66964.18,66964.19,66938.44,66943.18,2.20749,2026-04-03T08:54:00.000000
1775206500000,66943.19,66943.19,66924.27,66924.27,5.36976,2026-04-03T08:55:00.000000
1775206560000,66924.28,66945.9,66917.56,66944.94,3.31572,2026-04-03T08:56:00.000000
1775206620000,66944.94,66987.07,66940.56,66981.59,60.78508,2026-04-03T08:57:00.000000
1775206680000,66981.59,66981.6,66972.75,66972.75,2.65695,2026-04-03T08:58:00.000000
1775206740000,66972.76,66986.28,66961.29,66965.39,2.95655,2026-04-03T08:59:00.000000
1775206800000,66965.39,66970.5,66951.33,66970.49,5.33614,2026-04-03T09:00:00.000000
1775206860000,66970.49,66970.5,66960.17,66970.48,2.83529,2026-04-03T09:01:00.000000
1775206920000,66970.48,66993.98,66970.48,66991.29,7.16337,2026-04-03T09:02:00.000000
1775206980000,66991.28,67007.93,66984.36,66991.18,6.74547,2026-04-03T09:03:00.000000
1775207040000,66991.19,66991.19,66956.93,66956.93,3.02077,2026-04-03T09:04:00.000000
1775207100000,66956.93,66956.93,66887.56,66908.53,18.1528,2026-04-03T09:05:00.000000
1775207160000,66908.52,66932.21,66906.0,66921.93,2.79027,2026-04-03T09:06:00.000000
1775207220000,66921.92,66923.46,66917.58,66917.59,3.76255,2026-04-03T09:07:00.000000
1775207280000,66917.59,66917.59,66884.0,66886.35,5.9789,2026-04-03T09:08:00.000000
1775207340000,66886.36,66892.78,66883.0,66883.01,2.82806,2026-04-03T09:09:00.000000
1775207400000,66883.01,66892.16,66883.01,66885.9,1.44746,2026-04-03T09:10:00.000000
1775207460000,66885.89,66885.9,66883.2,66883.2,1.93231,2026-04-03T09:11:00.000000
1775207520000,66883.2,66909.47,66883.19,66894.01,10.17379,2026-04-03T09:12:00.000000
1775207580000,66894.0,66897.59,66886.06,66897.59,3.87371,2026-04-03T09:13:00.000000
1775207640000,66897.58,66909.31,66888.53,66889.47,2.31462,2026-04-03T09:14:00.000000
1775207700000,66889.46,66905.39,66889.46,66898.0,1.43148,2026-04-03T09:15:00.000000
1775207760000,66898.0,66914.0,66897.0,66914.0,1.54201,2026-04-03T09:16:00.000000
1775207820000,66914.0,66916.36,66913.99,66914.0,6.89283,2026-04-03T09:17:00.000000
1775207880000,66914.0,66914.0,66902.45,66902.45,1.2915,2026-04-03T09:18:00.000000
1775207940000,66902.44,66902.45,66872.68,66887.09,45.41341,2026-04-03T09:19:00.000000
1775208000000,66887.1,66887.1,66786.06,66786.06,20.36982,2026-04-03T09:20:00.000000
1775208060000,66786.06,66808.24,66757.67,66805.7,13.47486,2026-04-03T09:21:00.000000
1775208120000,66805.7,66805.7,66749.35,66765.7,9.5535,2026-04-03T09:22:00.000000
1775208180000,66765.7,66783.1,66764.0,66778.92,10.61732,2026-04-03T09:23:00.000000
1775208240000,66778.92,66778.92,66778.4,66778.41,3.10006,2026-04-03T09:24:00.000000
1775208300000,66778.4,66795.36,66752.28,66795.36,7.68228,2026-04-03T09:25:00.000000
1775208360000,66795.35,66816.0,66795.35,66816.0,4.21059,2026-04-03T09:26:00.000000
1775208420000,66816.0,66816.0,66805.67,66805.67,1.36698,2026-04-03T09:27:00.000000
1775208480000,66805.68,66828.8,66805.66,66828.79,1.5496,2026-04-03T09:28:00.000000
1775208540000,66828.79,66828.8,66819.28,66819.29,0.8349,2026-04-03T09:29:00.000000
1775208600000,66819.29,66819.29,66790.1,66807.9,2.28725,2026-04-03T09:30:00.000000
1775208660000,66807.9,66819.01,66807.9,66813.28,3.58395,2026-04-03T09:31:00.000000
1775208720000,66813.28,66849.8,66813.27,66840.08,4.10115,2026-04-03T09:32:00.000000
1775208780000,66840.08,66840.08,66835.27,66840.06,1.698,2026-04-03T09:33:00.000000
1775208840000,66840.07,66844.0,66840.06,66842.22,1.88223,2026-04-03T09:34:00.000000
1775208900000,66842.23,66842.23,66805.45,66805.46,5.9966,2026-04-03T09:35:00.000000
1775208960000,66805.46,66812.76,66801.7,66812.76,15.84137,2026-04-03T09:36:00.000000
1775209020000,66812.76,66832.85,66812.76,66828.65,3.3294,2026-04-03T09:37:00.000000
1775209080000,66828.66,66839.83,66828.65,66839.83,2.79458,2026-04-03T09:38:00.000000
1775209140000,66839.82,66844.6,66838.97,66838.98,3.09882,2026-04-03T09:39:00.000000
1775209200000,66838.97,66875.1,66838.97,66875.09,4.43434,2026-04-03T09:40:00.000000
1775209260000,66875.1,66892.89,66875.09,66890.9,1.53262,2026-04-03T09:41:00.000000
1775209320000,66890.9,66912.96,66887.34,66912.96,2.43478,2026-04-03T09:42:00.000000
1775209380000,66912.96,66917.0,66912.95,66913.03,3.65842,2026-04-03T09:43:00.000000
1775209440000,66913.03,66913.03,66902.2,66902.2,2.39452,2026-04-03T09:44:00.000000
1775209500000,66902.2,66902.21,66875.09,66875.09,1.27985,2026-04-03T09:45:00.000000
1775209560000,66875.09,66881.99,66875.09,66881.98,0.87081,2026-04-03T09:46:00.000000
1775209620000,66881.99,66881.99,66804.81,66817.86,7.17945,2026-04-03T09:47:00.000000
1775209680000,66817.87,66837.64,66817.87,66837.64,1.60673,2026-04-03T09:48:00.000000
1775209740000,66837.63,66846.85,66837.63,66846.85,1.27394,2026-04-03T09:49:00.000000
1775209800000,66846.84,66850.56,66846.84,66850.55,0.7962,2026-04-03T09:50:00.000000
1775209860000,66850.56,66866.53,66850.56,66851.32,2.10445,2026-04-03T09:51:00.000000
1775209920000,66851.31,66851.32,66839.58,66843.48,1.55757,2026-04-03T09:52:00.000000
1775209980000,66843.49,66843.49,66821.2,66821.21,3.10013,2026-04-03T09:53:00.000000
1775210040000,66821.21,66824.99,66821.2,66821.49,0.86405,2026-04-03T09:54:00.000000
1775210100000,66821.49,66831.3,66821.48,66831.3,1.04173,2026-04-03T09:55:00.000000
1775210160000,66831.29,66851.32,66831.29,66851.32,0.756,2026-04-03T09:56:00.000000
1775210220000,66851.31,66859.57,66851.31,66859.57,0.74449,2026-04-03T09:57:00.000000
1775210280000,66859.56,66859.58,66859.56,66859.58,0.81269,2026-04-03T09:58:00.000000
1775210340000,66859.58,66859.58,66859.57,66859.58,0.6576,2026-04-03T09:59:00.000000
1775210400000,66859.58,66859.58,66823.85,66823.86,3.0008,2026-04-03T10:00:00.000000
1775210460000,66823.86,66828.33,66820.99,66824.22,1.10177,2026-04-03T10:01:00.000000
1775210520000,66824.22,66824.23,66793.74,66804.0,4.14844,2026-04-03T10:02:00.000000
1775210580000,66804.0,66804.33,66784.6,66804.33,1.36397,2026-04-03T10:03:00.000000
1775210640000,66804.32,66804.33,66790.03,66790.04,0.46164,2026-04-03T10:04:00.000000
1775210700000,66790.04,66829.29,66790.03,66829.29,0.98244,2026-04-03T10:05:00.000000
1775210760000,66829.28,66829.29,66819.59,66819.59,1.71069,2026-04-03T10:06:00.000000
1775210820000,66819.6,66819.6,66786.83,66786.84,1.49118,2026-04-03T10:07:00.000000
1775210880000,66786.83,66786.84,66778.73,66778.73,1.50321,2026-04-03T10:08:00.000000
1775210940000,66778.74,66796.58,66778.73,66795.11,4.40356,2026-04-03T10:09:00.000000
1775211000000,66795.12,66795.12,66786.83,66786.83,1.24206,2026-04-03T10:10:00.000000
1775211060000,66786.83,66813.58,66778.4,66813.58,4.52514,2026-04-03T10:11:00.000000
1775211120000,66813.58,66813.58,66811.4,66811.4,6.44744,2026-04-03T10:12:00.000000
1775211180000,66811.41,66825.1,66807.52,66825.09,1.22162,2026-04-03T10:13:00.000000
1775211240000,66825.1,66834.39,66825.08,66828.1,3.77548,2026-04-03T10:14:00.000000
1775211300000,66828.09,66830.4,66823.09,66823.1,0.96001,2026-04-03T10:15:00.000000
1775211360000,66823.09,66823.1,66775.06,66807.58,11.86481,2026-04-03T10:16:00.000000
1775211420000,66807.59,66835.88,66807.58,66835.87,1.01941,2026-04-03T10:17:00.000000
1775211480000,66835.87,66866.0,66835.87,66841.96,3.52657,2026-04-03T10:18:00.000000
1775211540000,66841.96,66841.96,66841.94,66841.95,0.48651,2026-04-03T10:19:00.000000
1775211600000,66841.95,66841.95,66825.1,66841.93,2.61026,2026-04-03T10:20:00.000000
1775211660000,66841.93,66847.2,66831.01,66831.41,7.74092,2026-04-03T10:21:00.000000
1775211720000,66831.4,66831.41,66806.37,66806.37,2.75372,2026-04-03T10:22:00.000000
1775211780000,66806.37,66806.38,66783.04,66783.05,2.78614,2026-04-03T10:23:00.000000
1775211840000,66783.05,66792.59,66773.06,66792.44,1.85436,2026-04-03T10:24:00.000000
1775211900000,66792.45,66818.7,66792.45,66818.7,11.62913,2026-04-03T10:25:00.000000
1775211960000,66818.71,66818.71,66814.55,66814.55,1.36972,2026-04-03T10:26:00.000000
1775212020000,66814.55,66818.33,66814.55,66815.38,1.70847,2026-04-03T10:27:00.000000
1775212080000,66815.38,66815.38,66801.0,66801.01,1.28946,2026-04-03T10:28:00.000000
1775212140000,66801.0,66801.01,66765.7,66765.71,22.34875,2026-04-03T10:29:00.000000
1775212200000,66765.71,66765.71,66705.46,66705.47,5.92362,2026-04-03T10:30:00.000000
1775212260000,66705.47,66705.47,66674.72,66698.82,7.62942,2026-04-03T10:31:00.000000
1775212320000,66698.81,66741.82,66698.81,66741.81,2.33127,2026-04-03T10:32:00.000000
1775212380000,66741.81,66748.5,66741.72,66744.89,2.56594,2026-04-03T10:33:00.000000
1775212440000,66744.89,66747.57,66731.05,66731.05,1.27721,2026-04-03T10:34:00.000000
1775212500000,66731.06,66731.06,66711.32,66723.64,3.69269,2026-04-03T10:35:00.000000
1775212560000,66723.65,66756.69,66723.64,66756.69,1.39776,2026-04-03T10:36:00.000000
1775212620000,66756.68,66773.15,66756.68,66756.7,2.40255,2026-04-03T10:37:00.000000
1775212680000,66756.69,66756.7,66728.47,66728.48,1.4785,2026-04-03T10:38:00.000000
1775212740000,66728.47,66742.48,66728.47,66742.48,1.73632,2026-04-03T10:39:00.000000
1775212800000,66742.47,66772.0,66742.47,66772.0,1.29848,2026-04-03T10:40:00.000000
1775212860000,66771.99,66781.91,66771.99,66781.9,2.63191,2026-04-03T10:41:00.000000
1775212920000,66781.91,66786.96,66781.9,66786.87,2.16233,2026-04-03T10:42:00.000000
1775212980000,66786.87,66797.73,66786.87,66793.74,1.22847,2026-04-03T10:43:00.000000
1775213040000,66793.73,66800.93,66781.9,66781.91,1.18021,2026-04-03T10:44:00.000000
1775213100000,66781.91,66800.81,66781.91,66800.81,1.59567,2026-04-03T10:45:00.000000
1775213160000,66800.81,66823.39,66800.8,66816.37,2.45361,2026-04-03T10:46:00.000000
1775213220000,66816.38,66824.45,66816.37,66824.45,1.43341,2026-04-03T10:47:00.000000
1775213280000,66824.45,66875.1,66824.45,66868.83,6.10687,2026-04-03T10:48:00.000000
1775213340000,66868.82,66868.83,66845.0,66845.0,2.05195,2026-04-03T10:49:00.000000
1775213400000,66845.01,66852.63,66836.02,66852.63,2.43097,2026-04-03T10:50:00.000000
1775213460000,66852.64,66875.1,66852.64,66872.47,1.81713,2026-04-03T10:51:00.000000
1775213520000,66872.47,66895.5,66864.7,66864.7,1.80454,2026-04-03T10:52:00.000000
1775213580000,66864.71,66864.71,66840.91,66840.91,1.06368,2026-04-03T10:53:00.000000
1775213640000,66840.92,66869.66,66840.91,66869.66,1.95358,2026-04-03T10:54:00.000000
1775213700000,66869.66,66885.84,66869.66,66874.3,10.80021,2026-04-03T10:55:00.000000
1775213760000,66874.3,66880.8,66874.29,66880.79,6.58341,2026-04-03T10:56:00.000000
1775213820000,66880.8,66888.96,66867.35,66880.0,9.17356,2026-04-03T10:57:00.000000
1775213880000,66880.0,66896.86,66877.5,66896.85,10.20135,2026-04-03T10:58:00.000000
1775213940000,66896.86,66898.97,66876.75,66883.08,5.59709,2026-04-03T10:59:00.000000
1775214000000,66883.09,66914.28,66875.09,66875.1,8.44422,2026-04-03T11:00:00.000000
1775214060000,66875.1,66875.1,66862.93,66865.54,1.97967,2026-04-03T11:01:00.000000
1775214120000,66865.55,66880.29,66863.05,66880.29,1.8254,2026-04-03T11:02:00.000000
1775214180000,66880.29,66905.16,66868.76,66875.08,2.58732,2026-04-03T11:03:00.000000
1775214240000,66875.09,66890.47,66875.08,66890.46,2.44594,2026-04-03T11:04:00.000000
1775214300000,66890.47,66890.47,66858.95,66858.95,1.18245,2026-04-03T11:05:00.000000
1775214360000,66858.96,66872.28,66847.4,66872.28,1.57732,2026-04-03T11:06:00.000000
1775214420000,66872.28,66878.0,66862.93,66862.94,3.22395,2026-04-03T11:07:00.000000
1775214480000,66862.94,66868.64,66855.15,66855.15,1.10567,2026-04-03T11:08:00.000000
1775214540000,66855.16,66855.16,66832.97,66842.63,1.61658,2026-04-03T11:09:00.000000
1775214600000,66842.63,66871.73,66842.63,66864.67,6.31907,2026-04-03T11:10:00.000000
1775214660000,66864.67,66864.68,66854.01,66854.02,2.04249,2026-04-03T11:11:00.000000
1775214720000,66854.02,66854.02,66849.67,66849.67,0.82933,2026-04-03T11:12:00.000000
1775214780000,66849.68,66849.68,66833.68,66833.68,1.12189,2026-04-03T11:13:00.000000
1775214840000,66833.68,66852.07,66823.18,66852.07,1.855,2026-04-03T11:14:00.000000
1775214900000,66852.07,66860.63,66852.06,66852.07,1.46006,2026-04-03T11:15:00.000000
1775214960000,66852.08,66878.0,66852.07,66873.88,2.88564,2026-04-03T11:16:00.000000
1775215020000,66873.88,66875.5,66849.67,66849.67,21.41347,2026-04-03T11:17:00.000000
1775215080000,66849.68,66862.0,66845.49,66859.05,2.35166,2026-04-03T11:18:00.000000
1775215140000,66859.05,66875.38,66859.04,66871.38,1.33744,2026-04-03T11:19:00.000000
1775215200000,66871.38,66905.04,66871.35,66905.04,2.34313,2026-04-03T11:20:00.000000
1775215260000,66905.03,66911.69,66905.03,66905.3,0.76227,2026-04-03T11:21:00.000000
1775215320000,66905.29,66936.58,66902.48,66931.19,5.0555,2026-04-03T11:22:00.000000
1775215380000,66931.19,66951.34,66930.13,66935.67,7.69158,2026-04-03T11:23:00.000000
1775215440000,66935.66,66935.66,66897.16,66899.83,3.27201,2026-04-03T11:24:00.000000
1775215500000,66899.82,66927.22,66899.82,66927.21,2.77702,2026-04-03T11:25:00.000000
1775215560000,66927.22,66940.93,66927.21,66940.93,1.5526,2026-04-03T11:26:00.000000
1775215620000,66940.93,66950.6,66928.02,66928.03,2.31882,2026-04-03T11:27:00.000000
1775215680000,66928.02,66960.18,66928.02,66960.17,2.48304,2026-04-03T11:28:00.000000
1775215740000,66960.17,66960.18,66937.62,66937.63,5.75833,2026-04-03T11:29:00.000000
1775215800000,66937.62,66953.94,66928.03,66928.03,2.25762,2026-04-03T11:30:00.000000
1775215860000,66928.04,66928.04,66922.66,66922.67,1.17744,2026-04-03T11:31:00.000000
1775215920000,66922.66,66992.99,66920.86,66989.68,16.2136,2026-04-03T11:32:00.000000
1775215980000,66989.68,66989.78,66960.17,66960.18,2.99091,2026-04-03T11:33:00.000000
1775216040000,66960.17,66960.18,66951.33,66951.34,1.22073,2026-04-03T11:34:00.000000
1775216100000,66951.34,66993.76,66951.33,66991.68,3.02096,2026-04-03T11:35:00.000000
1775216160000,66991.68,66991.68,66980.0,66980.0,3.08128,2026-04-03T11:36:00.000000
1775216220000,66980.01,67000.0,66980.01,66990.0,8.76096,2026-04-03T11:37:00.000000
1775216280000,66990.0,67004.28,66984.75,66984.75,9.06093,2026-04-03T11:38:00.000000
1775216340000,66984.69,66996.72,66980.0,66996.71,1.62359,2026-04-03T11:39:00.000000
1775216400000,66996.71,66997.25,66980.0,66980.01,1.2659,2026-04-03T11:40:00.000000
1775216460000,66980.01,66992.55,66980.0,66987.08,1.93114,2026-04-03T11:41:00.000000
1775216520000,66987.07,66995.71,66980.39,66995.7,1.73307,2026-04-03T11:42:00.000000
1775216580000,66995.71,67057.42,66995.71,67030.41,13.45846,2026-04-03T11:43:00.000000
1775216640000,67030.41,67030.41,67001.23,67001.23,2.14208,2026-04-03T11:44:00.000000
1775216700000,67001.23,67008.46,66980.01,67000.03,3.55673,2026-04-03T11:45:00.000000
1775216760000,67000.03,67042.56,67000.02,67038.62,5.39622,2026-04-03T11:46:00.000000
1775216820000,67038.62,67038.63,67017.61,67026.39,3.38088,2026-04-03T11:47:00.000000
1775216880000,67026.4,67026.4,67011.64,67011.64,1.28481,2026-04-03T11:48:00.000000
1775216940000,67011.64,67015.0,66968.27,66968.27,2.39094,2026-04-03T11:49:00.000000
1775217000000,66968.27,66974.7,66960.0,66974.7,8.30368,2026-04-03T11:50:00.000000
1775217060000,66974.7,66978.43,66968.27,66978.0,1.78751,2026-04-03T11:51:00.000000
1775217120000,66978.0,66997.32,66977.99,66995.05,1.33557,2026-04-03T11:52:00.000000
1775217180000,66995.04,67017.79,66995.04,67017.79,1.62975,2026-04-03T11:53:00.000000
1775217240000,67017.79,67026.66,67007.24,67007.25,1.07717,2026-04-03T11:54:00.000000
1775217300000,67007.25,67007.25,66987.23,67002.33,2.68066,2026-04-03T11:55:00.000000
1775217360000,67002.33,67014.0,67002.32,67014.0,2.94748,2026-04-03T11:56:00.000000
1775217420000,67014.0,67014.0,66992.65,67006.55,3.15317,2026-04-03T11:57:00.000000
1775217480000,67006.56,67006.56,66999.46,66999.47,2.70078,2026-04-03T11:58:00.000000
1775217540000,66999.47,67017.44,66999.46,67017.44,2.73166,2026-04-03T11:59:00.000000
1775217600000,67017.43,67030.0,67014.41,67019.72,2.58999,2026-04-03T12:00:00.000000
1775217660000,67019.72,67032.72,67019.72,67032.0,2.63058,2026-04-03T12:01:00.000000
1775217720000,67032.0,67051.12,67022.0,67022.0,2.13202,2026-04-03T12:02:00.000000
1775217780000,67022.01,67026.28,67020.03,67026.27,1.53715,2026-04-03T12:03:00.000000
1775217840000,67026.28,67026.28,67006.9,67013.62,0.9561,2026-04-03T12:04:00.000000
1775217900000,67013.61,67013.62,67006.9,67006.9,1.29353,2026-04-03T12:05:00.000000
1775217960000,67006.91,67033.47,67006.9,67033.47,2.50337,2026-04-03T12:06:00.000000
1775218020000,67033.47,67050.23,67006.9,67006.91,4.18078,2026-04-03T12:07:00.000000
1775218080000,67006.9,67025.52,67006.9,67025.52,2.11836,2026-04-03T12:08:00.000000
1775218140000,67025.52,67025.52,67024.44,67024.44,0.86738,2026-04-03T12:09:00.000000
1775218200000,67024.44,67024.44,66941.5,66948.01,6.86816,2026-04-03T12:10:00.000000
1775218260000,66948.01,66960.01,66948.0,66960.01,2.9015,2026-04-03T12:11:00.000000
1775218320000,66960.01,66978.43,66960.0,66969.8,9.58986,2026-04-03T12:12:00.000000
1775218380000,66969.81,66982.99,66969.8,66982.99,2.41067,2026-04-03T12:13:00.000000
1775218440000,66982.99,67008.0,66982.98,67008.0,2.52133,2026-04-03T12:14:00.000000
1775218500000,67008.0,67059.59,67007.99,67032.05,6.24381,2026-04-03T12:15:00.000000
1775218560000,67032.05,67032.05,66965.11,66965.11,2.16879,2026-04-03T12:16:00.000000
1775218620000,66965.11,66965.12,66965.11,66965.11,1.55971,2026-04-03T12:17:00.000000
1775218680000,66965.11,66965.12,66951.33,66957.97,1.2984,2026-04-03T12:18:00.000000
1775218740000,66957.97,66989.01,66957.96,66988.79,2.64916,2026-04-03T12:19:00.000000
1775218800000,66988.79,67020.0,66988.79,67019.99,1.2246,2026-04-03T12:20:00.000000
1775218860000,67019.99,67019.99,66998.17,67003.06,3.32236,2026-04-03T12:21:00.000000
1775218920000,67003.06,67020.0,66984.0,66984.01,3.09956,2026-04-03T12:22:00.000000
1775218980000,66984.0,66984.01,66946.62,66952.98,35.37469,2026-04-03T12:23:00.000000
1775219040000,66952.99,66956.77,66942.35,66956.77,2.09136,2026-04-03T12:24:00.000000
1775219100000,66956.77,67000.0,66956.77,66999.99,3.38862,2026-04-03T12:25:00.000000
1775219160000,67000.0,67019.58,66999.99,67005.01,1.06405,2026-04-03T12:26:00.000000
1775219220000,67005.01,67034.48,67005.01,67034.48,8.1581,2026-04-03T12:27:00.000000
1775219280000,67034.48,67144.09,67034.48,67094.01,14.33131,2026-04-03T12:28:00.000000
1775219340000,67094.01,67116.0,67088.52,67115.99,3.48036,2026-04-03T12:29:00.000000
1775219400000,67115.99,67370.42,67001.9,67014.71,289.34002,2026-04-03T12:30:00.000000
1775219460000,67014.71,67021.17,66878.57,66967.49,32.91348,2026-04-03T12:31:00.000000
1775219520000,66967.49,66973.0,66894.38,66920.01,16.29626,2026-04-03T12:32:00.000000
1775219580000,66920.01,66961.03,66893.8,66940.01,10.352,2026-04-03T12:33:00.000000
1775219640000,66940.01,66940.02,66900.79,66926.67,3.07772,2026-04-03T12:34:00.000000
1775219700000,66926.66,66961.03,66849.67,66849.68,6.04789,2026-04-03T12:35:00.000000
1775219760000,66849.68,66892.92,66847.69,66849.68,10.56621,2026-04-03T12:36:00.000000
1775219820000,66849.68,66853.67,66809.17,66836.34,12.50796,2026-04-03T12:37:00.000000
1775219880000,66836.34,66845.13,66805.2,66837.22,7.26132,2026-04-03T12:38:00.000000
1775219940000,66837.21,66845.0,66812.0,66834.73,4.37124,2026-04-03T12:39:00.000000
1775220000000,66834.73,66889.2,66832.09,66833.14,10.31768,2026-04-03T12:40:00.000000
1775220060000,66833.14,66840.58,66796.43,66840.58,4.27452,2026-04-03T12:41:00.000000
1775220120000,66840.58,66878.4,66828.3,66878.39,3.41869,2026-04-03T12:42:00.000000
1775220180000,66878.4,66891.99,66868.0,66871.09,3.11944,2026-04-03T12:43:00.000000
1775220240000,66871.09,66874.85,66841.05,66867.31,6.45471,2026-04-03T12:44:00.000000
1775220300000,66867.31,66899.99,66862.93,66883.01,2.25666,2026-04-03T12:45:00.000000
1775220360000,66883.01,66883.02,66868.62,66868.62,11.60254,2026-04-03T12:46:00.000000
1775220420000,66868.62,66874.43,66821.84,66821.85,2.34366,2026-04-03T12:47:00.000000
1775220480000,66821.85,66860.64,66805.27,66805.27,2.8112,2026-04-03T12:48:00.000000
1775220540000,66805.27,66805.27,66750.0,66750.01,8.30559,2026-04-03T12:49:00.000000
1775220600000,66750.01,66750.01,66646.36,66737.15,59.9241,2026-04-03T12:50:00.000000
1775220660000,66737.15,66753.45,66716.9,66716.91,9.88561,2026-04-03T12:51:00.000000
1775220720000,66716.91,66756.0,66716.91,66736.32,33.90108,2026-04-03T12:52:00.000000
1775220780000,66736.33,66787.83,66736.32,66760.94,64.3983,2026-04-03T12:53:00.000000
1775220840000,66760.94,66777.48,66758.57,66771.65,8.23481,2026-04-03T12:54:00.000000
1775220900000,66771.65,66777.47,66759.47,66777.46,15.33841,2026-04-03T12:55:00.000000
1775220960000,66777.46,66810.61,66753.4,66800.11,5.7686,2026-04-03T12:56:00.000000
1775221020000,66800.12,66809.33,66765.7,66768.9,12.6327,2026-04-03T12:57:00.000000
1775221080000,66768.89,66768.9,66742.14,66742.14,3.98556,2026-04-03T12:58:00.000000
1775221140000,66742.14,66753.67,66703.98,66722.16,11.74689,2026-04-03T12:59:00.000000
1775221200000,66722.16,66729.46,66683.16,66698.42,8.97083,2026-04-03T13:00:00.000000
1775221260000,66698.42,66717.85,66653.38,66717.85,14.38555,2026-04-03T13:01:00.000000
1775221320000,66717.86,66735.62,66680.34,66708.0,10.08679,2026-04-03T13:02:00.000000
1775221380000,66708.0,66710.0,66705.08,66707.34,9.48014,2026-04-03T13:03:00.000000
1775221440000,66707.34,66707.34,66626.77,66627.95,10.44664,2026-04-03T13:04:00.000000
1775221500000,66627.95,66680.05,66627.94,66676.1,4.55243,2026-04-03T13:05:00.000000
1775221560000,66676.11,66710.0,66670.33,66670.33,3.3808,2026-04-03T13:06:00.000000
1775221620000,66670.34,66670.34,66641.14,66646.37,3.28778,2026-04-03T13:07:00.000000
1775221680000,66646.36,66654.34,66577.03,66598.56,15.67526,2026-04-03T13:08:00.000000
1775221740000,66598.56,66630.48,66595.19,66603.59,6.69989,2026-04-03T13:09:00.000000
1775221800000,66603.59,66607.99,66563.06,66587.73,7.37278,2026-04-03T13:10:00.000000
1775221860000,66587.72,66596.44,66562.72,66562.72,6.45342,2026-04-03T13:11:00.000000
1775221920000,66562.71,66637.69,66561.05,66637.69,9.44229,2026-04-03T13:12:00.000000
1775221980000,66637.69,66649.13,66619.94,66643.06,6.62657,2026-04-03T13:13:00.000000
1775222040000,66643.06,66672.07,66638.0,66672.07,12.93657,2026-04-03T13:14:00.000000
1775222100000,66672.07,66693.22,66646.75,66680.02,12.72778,2026-04-03T13:15:00.000000
1775222160000,66680.02,66689.08,66624.64,66655.41,9.09104,2026-04-03T13:16:00.000000
1775222220000,66655.41,66655.41,66596.66,66624.44,10.79523,2026-04-03T13:17:00.000000
1775222280000,66624.45,66658.03,66624.44,66658.03,4.59921,2026-04-03T13:18:00.000000
1775222340000,66658.03,66680.63,66658.02,66668.46,4.60014,2026-04-03T13:19:00.000000
1775222400000,66668.46,66696.29,66658.0,66695.58,7.91823,2026-04-03T13:20:00.000000
1775222460000,66695.57,66726.67,66685.43,66695.99,12.20486,2026-04-03T13:21:00.000000
1775222520000,66695.99,66717.69,66692.04,66710.33,8.24302,2026-04-03T13:22:00.000000
1775222580000,66710.33,66710.33,66702.04,66703.36,3.06914,2026-04-03T13:23:00.000000
1775222640000,66703.36,66703.36,66699.91,66700.59,7.81359,2026-04-03T13:24:00.000000
1775222700000,66700.58,66717.04,66680.39,66717.03,22.12502,2026-04-03T13:25:00.000000
1775222760000,66717.04,66724.52,66682.43,66682.44,5.02702,2026-04-03T13:26:00.000000
1775222820000,66682.43,66684.46,66672.44,66682.15,5.05727,2026-04-03T13:27:00.000000
1775222880000,66682.16,66697.68,66660.34,66660.35,5.17874,2026-04-03T13:28:00.000000
1775222940000,66660.35,66693.29,66660.34,66688.73,39.28154,2026-04-03T13:29:00.000000
1775223000000,66688.72,66688.73,66649.07,66649.07,5.38932,2026-04-03T13:30:00.000000
1775223060000,66649.07,66674.06,66616.79,66669.01,9.18955,2026-04-03T13:31:00.000000
1775223120000,66669.01,66669.01,66650.51,66659.94,15.13676,2026-04-03T13:32:00.000000
1775223180000,66659.94,66669.91,66580.44,66606.0,68.31757,2026-04-03T13:33:00.000000
1775223240000,66606.0,66626.84,66589.05,66626.84,8.3131,2026-04-03T13:34:00.000000
1775223300000,66626.83,66669.91,66624.21,66661.41,5.12626,2026-04-03T13:35:00.000000
1775223360000,66661.4,66666.23,66632.3,66637.25,5.13057,2026-04-03T13:36:00.000000
1775223420000,66637.25,66648.18,66627.71,66636.37,2.95846,2026-04-03T13:37:00.000000
1775223480000,66636.36,66636.36,66594.04,66610.68,4.16015,2026-04-03T13:38:00.000000
1775223540000,66610.67,66617.77,66601.19,66609.67,6.37921,2026-04-03T13:39:00.000000
1775223600000,66609.66,66610.03,66601.18,66608.59,4.01657,2026-04-03T13:40:00.000000
1775223660000,66608.59,66608.59,66587.33,66605.23,3.93411,2026-04-03T13:41:00.000000
1775223720000,66605.22,66627.24,66605.22,66616.19,4.08756,2026-04-03T13:42:00.000000
1775223780000,66616.2,66622.82,66595.53,66595.54,2.45207,2026-04-03T13:43:00.000000
1775223840000,66595.53,66602.66,66595.53,66602.66,3.0955,2026-04-03T13:44:00.000000
1775223900000,66602.66,66609.68,66587.14,66587.14,4.33359,2026-04-03T13:45:00.000000
1775223960000,66587.15,66621.93,66587.15,66607.57,4.68552,2026-04-03T13:46:00.000000
1775224020000,66607.58,66613.31,66568.24,66568.24,5.58519,2026-04-03T13:47:00.000000
1775224080000,66568.24,66568.24,66533.35,66539.58,8.48778,2026-04-03T13:48:00.000000
1775224140000,66539.58,66547.0,66508.1,66508.1,13.3492,2026-04-03T13:49:00.000000
1775224200000,66508.1,66532.66,66508.1,66522.96,4.53322,2026-04-03T13:50:00.000000
1775224260000,66522.96,66547.99,66522.96,66547.99,8.68209,2026-04-03T13:51:00.000000
1775224320000,66547.98,66582.29,66539.79,66582.28,11.1393,2026-04-03T13:52:00.000000
1775224380000,66582.29,66635.39,66582.29,66635.39,34.68692,2026-04-03T13:53:00.000000
1775224440000,66635.38,66680.33,66634.86,66680.32,7.8464,2026-04-03T13:54:00.000000
1775224500000,66680.33,66680.33,66644.47,66649.2,7.23775,2026-04-03T13:55:00.000000
1775224560000,66649.2,66649.21,66632.22,66632.23,1.18501,2026-04-03T13:56:00.000000
1775224620000,66632.23,66658.08,66622.0,66658.08,4.47329,2026-04-03T13:57:00.000000
1775224680000,66658.07,66673.38,66658.07,66673.38,1.05921,2026-04-03T13:58:00.000000
1775224740000,66673.38,66673.38,66667.09,66672.0,2.43319,2026-04-03T13:59:00.000000
1775224800000,66671.99,66675.8,66648.06,66648.06,5.05901,2026-04-03T14:00:00.000000
1775224860000,66648.07,66652.3,66648.07,66652.22,2.21796,2026-04-03T14:01:00.000000
1775224920000,66652.22,66683.69,66634.0,66661.3,4.88277,2026-04-03T14:02:00.000000
1775224980000,66661.31,66748.03,66655.0,66736.76,9.91011,2026-04-03T14:03:00.000000
1775225040000,66736.32,66765.71,66710.66,66717.91,16.2169,2026-04-03T14:04:00.000000
1775225100000,66717.91,66730.54,66717.91,66722.04,1.51069,2026-04-03T14:05:00.000000
1 timestamp open high low close volume datetime
2 1775195160000 66754.01 66774.55 66744.38 66771.25 5.36355 2026-04-03T05:46:00.000000
3 1775195220000 66771.25 66771.25 66729.05 66736.01 22.33537 2026-04-03T05:47:00.000000
4 1775195280000 66736.0 66736.01 66720.33 66720.34 28.46124 2026-04-03T05:48:00.000000
5 1775195340000 66720.34 66721.78 66696.83 66696.84 32.96766 2026-04-03T05:49:00.000000
6 1775195400000 66696.84 66696.84 66623.44 66625.84 48.91239 2026-04-03T05:50:00.000000
7 1775195460000 66625.84 66630.48 66576.09 66586.94 76.08788 2026-04-03T05:51:00.000000
8 1775195520000 66586.94 66586.94 66566.47 66576.66 48.91854 2026-04-03T05:52:00.000000
9 1775195580000 66576.67 66607.52 66553.31 66571.23 54.06214 2026-04-03T05:53:00.000000
10 1775195640000 66571.24 66571.24 66548.0 66549.08 41.66811 2026-04-03T05:54:00.000000
11 1775195700000 66549.08 66587.0 66518.58 66567.93 72.68151 2026-04-03T05:55:00.000000
12 1775195760000 66567.93 66598.97 66560.42 66579.03 45.79834 2026-04-03T05:56:00.000000
13 1775195820000 66579.02 66579.02 66555.89 66561.59 43.93982 2026-04-03T05:57:00.000000
14 1775195880000 66561.58 66579.93 66539.0 66579.93 39.79082 2026-04-03T05:58:00.000000
15 1775195940000 66579.93 66600.35 66579.92 66600.35 2.93569 2026-04-03T05:59:00.000000
16 1775196000000 66600.35 66609.02 66600.0 66600.0 2.01119 2026-04-03T06:00:00.000000
17 1775196060000 66600.01 66610.25 66584.22 66610.25 7.9318 2026-04-03T06:01:00.000000
18 1775196120000 66610.25 66626.0 66602.65 66626.0 4.44872 2026-04-03T06:02:00.000000
19 1775196180000 66626.0 66646.37 66625.99 66629.99 7.10476 2026-04-03T06:03:00.000000
20 1775196240000 66629.99 66629.99 66570.35 66570.36 24.48685 2026-04-03T06:04:00.000000
21 1775196300000 66570.36 66597.34 66563.0 66597.34 4.50543 2026-04-03T06:05:00.000000
22 1775196360000 66597.33 66610.0 66588.0 66609.99 1.8859 2026-04-03T06:06:00.000000
23 1775196420000 66609.99 66616.48 66589.91 66616.48 12.37846 2026-04-03T06:07:00.000000
24 1775196480000 66616.49 66636.0 66600.36 66600.36 13.47984 2026-04-03T06:08:00.000000
25 1775196540000 66600.37 66600.37 66558.76 66591.88 9.77952 2026-04-03T06:09:00.000000
26 1775196600000 66591.88 66629.98 66591.88 66629.98 2.08453 2026-04-03T06:10:00.000000
27 1775196660000 66629.98 66658.05 66629.98 66657.31 4.29341 2026-04-03T06:11:00.000000
28 1775196720000 66657.3 66658.05 66650.73 66650.74 1.62141 2026-04-03T06:12:00.000000
29 1775196780000 66650.74 66650.74 66629.96 66629.97 1.53868 2026-04-03T06:13:00.000000
30 1775196840000 66629.97 66660.33 66629.96 66660.32 7.05165 2026-04-03T06:14:00.000000
31 1775196900000 66660.33 66667.15 66620.36 66657.3 4.11344 2026-04-03T06:15:00.000000
32 1775196960000 66657.31 66657.31 66636.94 66653.66 4.84025 2026-04-03T06:16:00.000000
33 1775197020000 66653.67 66668.47 66639.71 66657.02 2.35065 2026-04-03T06:17:00.000000
34 1775197080000 66657.02 66657.02 66620.35 66649.19 4.31623 2026-04-03T06:18:00.000000
35 1775197140000 66649.18 66668.47 66646.36 66656.37 2.64156 2026-04-03T06:19:00.000000
36 1775197200000 66656.37 66656.37 66608.45 66608.45 2.38354 2026-04-03T06:20:00.000000
37 1775197260000 66608.45 66628.0 66601.0 66613.13 4.15268 2026-04-03T06:21:00.000000
38 1775197320000 66613.12 66640.22 66613.12 66640.22 2.67113 2026-04-03T06:22:00.000000
39 1775197380000 66640.22 66674.64 66640.21 66674.64 2.28324 2026-04-03T06:23:00.000000
40 1775197440000 66674.64 66694.22 66633.15 66633.15 16.24791 2026-04-03T06:24:00.000000
41 1775197500000 66633.15 66670.63 66633.15 66670.62 2.688 2026-04-03T06:25:00.000000
42 1775197560000 66670.62 66670.63 66646.36 66652.62 3.45023 2026-04-03T06:26:00.000000
43 1775197620000 66652.62 66652.63 66624.57 66635.4 5.27228 2026-04-03T06:27:00.000000
44 1775197680000 66635.39 66635.4 66615.16 66629.99 4.09926 2026-04-03T06:28:00.000000
45 1775197740000 66630.0 66637.16 66619.54 66624.98 2.11746 2026-04-03T06:29:00.000000
46 1775197800000 66624.98 66629.21 66601.01 66613.9 2.468 2026-04-03T06:30:00.000000
47 1775197860000 66613.9 66626.55 66579.32 66626.55 11.74893 2026-04-03T06:31:00.000000
48 1775197920000 66626.54 66657.98 66626.54 66632.56 1.78324 2026-04-03T06:32:00.000000
49 1775197980000 66632.56 66640.22 66608.32 66633.99 3.13569 2026-04-03T06:33:00.000000
50 1775198040000 66633.99 66633.99 66631.82 66631.82 1.6266 2026-04-03T06:34:00.000000
51 1775198100000 66631.83 66718.33 66631.82 66718.33 3.17047 2026-04-03T06:35:00.000000
52 1775198160000 66718.32 66744.66 66703.99 66738.33 7.71878 2026-04-03T06:36:00.000000
53 1775198220000 66738.33 66772.0 66717.26 66772.0 8.95205 2026-04-03T06:37:00.000000
54 1775198280000 66771.99 66772.0 66718.98 66718.98 3.23504 2026-04-03T06:38:00.000000
55 1775198340000 66718.97 66731.29 66712.88 66719.84 3.02625 2026-04-03T06:39:00.000000
56 1775198400000 66719.84 66752.48 66719.83 66752.48 7.18074 2026-04-03T06:40:00.000000
57 1775198460000 66752.47 66752.48 66738.91 66748.02 2.17916 2026-04-03T06:41:00.000000
58 1775198520000 66748.03 66748.03 66732.13 66739.26 2.45104 2026-04-03T06:42:00.000000
59 1775198580000 66739.26 66754.37 66739.25 66754.37 2.203 2026-04-03T06:43:00.000000
60 1775198640000 66754.37 66770.49 66740.63 66740.63 29.66449 2026-04-03T06:44:00.000000
61 1775198700000 66740.64 66780.0 66740.63 66779.99 14.95583 2026-04-03T06:45:00.000000
62 1775198760000 66780.0 66800.0 66779.99 66782.42 3.64919 2026-04-03T06:46:00.000000
63 1775198820000 66782.42 66782.42 66756.15 66756.15 3.1932 2026-04-03T06:47:00.000000
64 1775198880000 66756.15 66756.15 66715.97 66715.97 9.52683 2026-04-03T06:48:00.000000
65 1775198940000 66715.98 66764.0 66715.97 66764.0 15.06678 2026-04-03T06:49:00.000000
66 1775199000000 66764.0 66797.09 66763.99 66787.5 47.66688 2026-04-03T06:50:00.000000
67 1775199060000 66787.51 66792.0 66765.7 66765.7 21.64904 2026-04-03T06:51:00.000000
68 1775199120000 66765.7 66765.71 66744.0 66764.02 6.99187 2026-04-03T06:52:00.000000
69 1775199180000 66764.03 66764.03 66752.31 66758.05 7.96139 2026-04-03T06:53:00.000000
70 1775199240000 66758.06 66780.44 66732.6 66735.14 29.15909 2026-04-03T06:54:00.000000
71 1775199300000 66735.14 66764.05 66735.14 66764.04 5.23016 2026-04-03T06:55:00.000000
72 1775199360000 66764.05 66768.76 66759.48 66768.75 3.64087 2026-04-03T06:56:00.000000
73 1775199420000 66768.76 66771.22 66748.02 66771.22 4.00973 2026-04-03T06:57:00.000000
74 1775199480000 66771.22 66788.65 66771.22 66788.65 1.33814 2026-04-03T06:58:00.000000
75 1775199540000 66788.65 66796.8 66788.64 66796.79 1.48421 2026-04-03T06:59:00.000000
76 1775199600000 66796.8 66797.61 66788.95 66797.61 5.47762 2026-04-03T07:00:00.000000
77 1775199660000 66797.61 66813.68 66797.6 66799.22 4.90028 2026-04-03T07:01:00.000000
78 1775199720000 66799.23 66800.76 66791.77 66800.75 3.42236 2026-04-03T07:02:00.000000
79 1775199780000 66800.76 66834.49 66800.76 66830.03 6.01416 2026-04-03T07:03:00.000000
80 1775199840000 66830.03 66830.03 66800.5 66800.5 3.65694 2026-04-03T07:04:00.000000
81 1775199900000 66800.5 66800.5 66768.77 66768.77 8.73741 2026-04-03T07:05:00.000000
82 1775199960000 66768.77 66811.46 66758.0 66811.46 6.71524 2026-04-03T07:06:00.000000
83 1775200020000 66811.47 66850.19 66811.47 66850.19 21.8258 2026-04-03T07:07:00.000000
84 1775200080000 66850.19 66872.93 66850.19 66864.47 27.69143 2026-04-03T07:08:00.000000
85 1775200140000 66864.47 66895.13 66864.47 66884.35 22.4525 2026-04-03T07:09:00.000000
86 1775200200000 66884.35 66911.98 66877.44 66877.45 22.73372 2026-04-03T07:10:00.000000
87 1775200260000 66877.45 66877.45 66828.0 66828.0 9.21428 2026-04-03T07:11:00.000000
88 1775200320000 66827.31 66830.34 66801.29 66801.29 25.18619 2026-04-03T07:12:00.000000
89 1775200380000 66801.28 66819.0 66787.95 66806.89 68.86024 2026-04-03T07:13:00.000000
90 1775200440000 66806.89 66825.35 66796.97 66818.36 42.69469 2026-04-03T07:14:00.000000
91 1775200500000 66818.36 66849.42 66810.64 66834.41 13.12959 2026-04-03T07:15:00.000000
92 1775200560000 66834.41 66836.36 66813.61 66829.66 3.19703 2026-04-03T07:16:00.000000
93 1775200620000 66829.66 66829.66 66800.5 66800.51 1.88882 2026-04-03T07:17:00.000000
94 1775200680000 66800.51 66830.95 66800.51 66830.24 3.27785 2026-04-03T07:18:00.000000
95 1775200740000 66830.23 66844.85 66824.61 66844.84 2.66024 2026-04-03T07:19:00.000000
96 1775200800000 66844.85 66878.58 66844.85 66858.49 3.05023 2026-04-03T07:20:00.000000
97 1775200860000 66858.49 66867.36 66847.24 66867.35 3.90897 2026-04-03T07:21:00.000000
98 1775200920000 66867.36 66872.58 66853.74 66868.99 5.07939 2026-04-03T07:22:00.000000
99 1775200980000 66869.0 66869.0 66862.03 66862.39 2.57586 2026-04-03T07:23:00.000000
100 1775201040000 66862.4 66870.0 66839.01 66839.65 13.98753 2026-04-03T07:24:00.000000
101 1775201100000 66839.66 66859.77 66839.65 66847.7 6.03865 2026-04-03T07:25:00.000000
102 1775201160000 66847.69 66887.28 66847.69 66887.28 6.91063 2026-04-03T07:26:00.000000
103 1775201220000 66887.28 66893.68 66880.78 66880.78 3.76091 2026-04-03T07:27:00.000000
104 1775201280000 66880.78 66898.38 66880.78 66898.37 1.67036 2026-04-03T07:28:00.000000
105 1775201340000 66898.37 66945.58 66898.37 66942.95 5.67733 2026-04-03T07:29:00.000000
106 1775201400000 66942.96 66960.18 66937.5 66950.86 8.34496 2026-04-03T07:30:00.000000
107 1775201460000 66950.86 66950.86 66922.49 66922.5 4.01133 2026-04-03T07:31:00.000000
108 1775201520000 66922.5 66947.44 66910.0 66947.43 12.82896 2026-04-03T07:32:00.000000
109 1775201580000 66947.44 67027.75 66947.43 67027.75 20.27123 2026-04-03T07:33:00.000000
110 1775201640000 67027.75 67091.17 67027.74 67069.96 25.6555 2026-04-03T07:34:00.000000
111 1775201700000 67069.95 67247.38 67069.95 67164.17 75.31708 2026-04-03T07:35:00.000000
112 1775201760000 67164.17 67164.17 67070.09 67075.89 11.21993 2026-04-03T07:36:00.000000
113 1775201820000 67075.88 67148.99 67072.32 67135.92 14.08032 2026-04-03T07:37:00.000000
114 1775201880000 67135.91 67139.13 67096.87 67129.42 9.48167 2026-04-03T07:38:00.000000
115 1775201940000 67129.41 67129.42 67076.22 67081.58 13.65929 2026-04-03T07:39:00.000000
116 1775202000000 67081.57 67118.78 67076.0 67115.77 4.75168 2026-04-03T07:40:00.000000
117 1775202060000 67115.76 67115.77 67073.02 67100.49 8.02978 2026-04-03T07:41:00.000000
118 1775202120000 67100.49 67126.86 67085.23 67117.43 13.0991 2026-04-03T07:42:00.000000
119 1775202180000 67117.42 67117.43 67071.44 67071.44 4.94972 2026-04-03T07:43:00.000000
120 1775202240000 67071.44 67075.73 67057.41 67075.72 11.02419 2026-04-03T07:44:00.000000
121 1775202300000 67075.72 67075.73 67049.99 67058.59 11.22612 2026-04-03T07:45:00.000000
122 1775202360000 67058.59 67066.04 67032.67 67052.4 8.7103 2026-04-03T07:46:00.000000
123 1775202420000 67052.39 67064.43 67046.59 67048.8 5.07304 2026-04-03T07:47:00.000000
124 1775202480000 67048.51 67048.52 67032.42 67032.43 4.37636 2026-04-03T07:48:00.000000
125 1775202540000 67032.42 67057.02 67032.42 67035.77 5.1997 2026-04-03T07:49:00.000000
126 1775202600000 67035.76 67035.77 67017.0 67017.01 7.60439 2026-04-03T07:50:00.000000
127 1775202660000 67017.0 67035.01 67010.14 67033.3 23.03664 2026-04-03T07:51:00.000000
128 1775202720000 67033.3 67070.58 67030.32 67070.58 27.87481 2026-04-03T07:52:00.000000
129 1775202780000 67071.3 67134.0 67071.3 67133.03 47.24015 2026-04-03T07:53:00.000000
130 1775202840000 67133.03 67133.04 67037.77 67063.28 117.32921 2026-04-03T07:54:00.000000
131 1775202900000 67063.28 67075.95 67050.58 67050.59 5.49778 2026-04-03T07:55:00.000000
132 1775202960000 67050.58 67050.59 67029.0 67049.06 6.61401 2026-04-03T07:56:00.000000
133 1775203020000 67049.05 67049.06 67037.27 67045.43 7.23732 2026-04-03T07:57:00.000000
134 1775203080000 67045.43 67045.44 67025.8 67042.07 4.98094 2026-04-03T07:58:00.000000
135 1775203140000 67042.07 67042.08 67032.77 67042.08 8.25555 2026-04-03T07:59:00.000000
136 1775203200000 67042.08 67050.02 67033.08 67047.11 5.63711 2026-04-03T08:00:00.000000
137 1775203260000 67047.11 67079.67 67040.1 67059.61 1.82372 2026-04-03T08:01:00.000000
138 1775203320000 67059.61 67072.25 67054.37 67068.96 5.25613 2026-04-03T08:02:00.000000
139 1775203380000 67068.95 67085.72 67046.11 67083.82 5.92799 2026-04-03T08:03:00.000000
140 1775203440000 67083.82 67118.0 67069.01 67110.02 6.07318 2026-04-03T08:04:00.000000
141 1775203500000 67110.01 67110.02 67084.11 67088.0 3.24727 2026-04-03T08:05:00.000000
142 1775203560000 67088.01 67181.21 67088.0 67181.21 22.43708 2026-04-03T08:06:00.000000
143 1775203620000 67181.21 67222.61 67181.0 67222.6 26.12492 2026-04-03T08:07:00.000000
144 1775203680000 67222.6 67224.27 67185.0 67185.01 52.35244 2026-04-03T08:08:00.000000
145 1775203740000 67185.01 67288.0 67185.0 67277.28 27.52814 2026-04-03T08:09:00.000000
146 1775203800000 67277.52 67284.51 67240.66 67253.51 21.46807 2026-04-03T08:10:00.000000
147 1775203860000 67252.99 67263.07 67247.83 67247.84 6.5342 2026-04-03T08:11:00.000000
148 1775203920000 67247.83 67255.63 67211.8 67211.8 43.63354 2026-04-03T08:12:00.000000
149 1775203980000 67211.79 67211.79 67192.86 67192.99 41.46455 2026-04-03T08:13:00.000000
150 1775204040000 67192.99 67210.0 67163.11 67169.74 80.66772 2026-04-03T08:14:00.000000
151 1775204100000 67169.75 67186.01 67166.64 67186.01 3.1101 2026-04-03T08:15:00.000000
152 1775204160000 67186.01 67222.6 67167.6 67167.6 6.42734 2026-04-03T08:16:00.000000
153 1775204220000 67167.6 67181.14 67121.92 67121.93 9.15108 2026-04-03T08:17:00.000000
154 1775204280000 67121.92 67154.65 67121.92 67152.8 3.88994 2026-04-03T08:18:00.000000
155 1775204340000 67152.81 67159.54 67152.8 67154.69 2.55071 2026-04-03T08:19:00.000000
156 1775204400000 67154.68 67154.68 67124.0 67135.66 4.33226 2026-04-03T08:20:00.000000
157 1775204460000 67135.66 67175.97 67135.65 67173.67 11.30521 2026-04-03T08:21:00.000000
158 1775204520000 67173.67 67173.67 67162.67 67162.67 3.22862 2026-04-03T08:22:00.000000
159 1775204580000 67162.67 67178.37 67162.67 67178.37 2.38612 2026-04-03T08:23:00.000000
160 1775204640000 67178.36 67178.37 67164.6 67164.61 1.22166 2026-04-03T08:24:00.000000
161 1775204700000 67164.6 67171.27 67125.83 67125.84 12.6242 2026-04-03T08:25:00.000000
162 1775204760000 67125.84 67125.84 67102.0 67102.0 5.40965 2026-04-03T08:26:00.000000
163 1775204820000 67102.0 67102.01 67066.13 67066.14 7.18303 2026-04-03T08:27:00.000000
164 1775204880000 67066.13 67068.87 67057.96 67057.96 2.77629 2026-04-03T08:28:00.000000
165 1775204940000 67057.96 67069.67 67056.24 67056.25 5.57167 2026-04-03T08:29:00.000000
166 1775205000000 67056.25 67074.9 67056.24 67074.88 27.84393 2026-04-03T08:30:00.000000
167 1775205060000 67074.89 67112.84 67072.65 67112.84 8.32787 2026-04-03T08:31:00.000000
168 1775205120000 67112.83 67119.97 67096.13 67111.74 6.10437 2026-04-03T08:32:00.000000
169 1775205180000 67111.74 67115.56 67090.01 67094.17 2.01094 2026-04-03T08:33:00.000000
170 1775205240000 67094.18 67123.87 67094.17 67105.85 1.76641 2026-04-03T08:34:00.000000
171 1775205300000 67105.84 67105.84 67088.78 67088.78 2.57572 2026-04-03T08:35:00.000000
172 1775205360000 67088.79 67088.79 67073.24 67073.25 19.79366 2026-04-03T08:36:00.000000
173 1775205420000 67073.25 67073.25 67056.0 67056.0 1.65705 2026-04-03T08:37:00.000000
174 1775205480000 67056.0 67056.01 67022.57 67033.03 7.94188 2026-04-03T08:38:00.000000
175 1775205540000 67033.04 67033.04 66996.83 66998.63 21.65561 2026-04-03T08:39:00.000000
176 1775205600000 66998.64 67015.38 66971.3 66982.66 16.15066 2026-04-03T08:40:00.000000
177 1775205660000 66982.66 66990.0 66936.0 66954.53 6.03326 2026-04-03T08:41:00.000000
178 1775205720000 66954.53 66961.43 66926.43 66926.43 3.90457 2026-04-03T08:42:00.000000
179 1775205780000 66926.44 66936.15 66903.01 66919.98 21.32797 2026-04-03T08:43:00.000000
180 1775205840000 66919.98 66940.93 66913.28 66934.65 5.28545 2026-04-03T08:44:00.000000
181 1775205900000 66934.65 66951.55 66915.69 66937.08 19.49201 2026-04-03T08:45:00.000000
182 1775205960000 66937.08 66937.09 66900.56 66917.61 5.28333 2026-04-03T08:46:00.000000
183 1775206020000 66917.62 66960.01 66917.62 66951.72 2.491 2026-04-03T08:47:00.000000
184 1775206080000 66951.73 66991.27 66951.73 66991.27 3.86657 2026-04-03T08:48:00.000000
185 1775206140000 66991.27 67011.94 66983.75 66989.15 6.97903 2026-04-03T08:49:00.000000
186 1775206200000 66989.15 66991.07 66964.53 66980.99 4.04158 2026-04-03T08:50:00.000000
187 1775206260000 66981.0 66999.99 66980.99 66994.97 3.05455 2026-04-03T08:51:00.000000
188 1775206320000 66994.97 66994.97 66960.4 66979.9 5.02731 2026-04-03T08:52:00.000000
189 1775206380000 66979.9 66979.9 66940.97 66964.19 5.91942 2026-04-03T08:53:00.000000
190 1775206440000 66964.18 66964.19 66938.44 66943.18 2.20749 2026-04-03T08:54:00.000000
191 1775206500000 66943.19 66943.19 66924.27 66924.27 5.36976 2026-04-03T08:55:00.000000
192 1775206560000 66924.28 66945.9 66917.56 66944.94 3.31572 2026-04-03T08:56:00.000000
193 1775206620000 66944.94 66987.07 66940.56 66981.59 60.78508 2026-04-03T08:57:00.000000
194 1775206680000 66981.59 66981.6 66972.75 66972.75 2.65695 2026-04-03T08:58:00.000000
195 1775206740000 66972.76 66986.28 66961.29 66965.39 2.95655 2026-04-03T08:59:00.000000
196 1775206800000 66965.39 66970.5 66951.33 66970.49 5.33614 2026-04-03T09:00:00.000000
197 1775206860000 66970.49 66970.5 66960.17 66970.48 2.83529 2026-04-03T09:01:00.000000
198 1775206920000 66970.48 66993.98 66970.48 66991.29 7.16337 2026-04-03T09:02:00.000000
199 1775206980000 66991.28 67007.93 66984.36 66991.18 6.74547 2026-04-03T09:03:00.000000
200 1775207040000 66991.19 66991.19 66956.93 66956.93 3.02077 2026-04-03T09:04:00.000000
201 1775207100000 66956.93 66956.93 66887.56 66908.53 18.1528 2026-04-03T09:05:00.000000
202 1775207160000 66908.52 66932.21 66906.0 66921.93 2.79027 2026-04-03T09:06:00.000000
203 1775207220000 66921.92 66923.46 66917.58 66917.59 3.76255 2026-04-03T09:07:00.000000
204 1775207280000 66917.59 66917.59 66884.0 66886.35 5.9789 2026-04-03T09:08:00.000000
205 1775207340000 66886.36 66892.78 66883.0 66883.01 2.82806 2026-04-03T09:09:00.000000
206 1775207400000 66883.01 66892.16 66883.01 66885.9 1.44746 2026-04-03T09:10:00.000000
207 1775207460000 66885.89 66885.9 66883.2 66883.2 1.93231 2026-04-03T09:11:00.000000
208 1775207520000 66883.2 66909.47 66883.19 66894.01 10.17379 2026-04-03T09:12:00.000000
209 1775207580000 66894.0 66897.59 66886.06 66897.59 3.87371 2026-04-03T09:13:00.000000
210 1775207640000 66897.58 66909.31 66888.53 66889.47 2.31462 2026-04-03T09:14:00.000000
211 1775207700000 66889.46 66905.39 66889.46 66898.0 1.43148 2026-04-03T09:15:00.000000
212 1775207760000 66898.0 66914.0 66897.0 66914.0 1.54201 2026-04-03T09:16:00.000000
213 1775207820000 66914.0 66916.36 66913.99 66914.0 6.89283 2026-04-03T09:17:00.000000
214 1775207880000 66914.0 66914.0 66902.45 66902.45 1.2915 2026-04-03T09:18:00.000000
215 1775207940000 66902.44 66902.45 66872.68 66887.09 45.41341 2026-04-03T09:19:00.000000
216 1775208000000 66887.1 66887.1 66786.06 66786.06 20.36982 2026-04-03T09:20:00.000000
217 1775208060000 66786.06 66808.24 66757.67 66805.7 13.47486 2026-04-03T09:21:00.000000
218 1775208120000 66805.7 66805.7 66749.35 66765.7 9.5535 2026-04-03T09:22:00.000000
219 1775208180000 66765.7 66783.1 66764.0 66778.92 10.61732 2026-04-03T09:23:00.000000
220 1775208240000 66778.92 66778.92 66778.4 66778.41 3.10006 2026-04-03T09:24:00.000000
221 1775208300000 66778.4 66795.36 66752.28 66795.36 7.68228 2026-04-03T09:25:00.000000
222 1775208360000 66795.35 66816.0 66795.35 66816.0 4.21059 2026-04-03T09:26:00.000000
223 1775208420000 66816.0 66816.0 66805.67 66805.67 1.36698 2026-04-03T09:27:00.000000
224 1775208480000 66805.68 66828.8 66805.66 66828.79 1.5496 2026-04-03T09:28:00.000000
225 1775208540000 66828.79 66828.8 66819.28 66819.29 0.8349 2026-04-03T09:29:00.000000
226 1775208600000 66819.29 66819.29 66790.1 66807.9 2.28725 2026-04-03T09:30:00.000000
227 1775208660000 66807.9 66819.01 66807.9 66813.28 3.58395 2026-04-03T09:31:00.000000
228 1775208720000 66813.28 66849.8 66813.27 66840.08 4.10115 2026-04-03T09:32:00.000000
229 1775208780000 66840.08 66840.08 66835.27 66840.06 1.698 2026-04-03T09:33:00.000000
230 1775208840000 66840.07 66844.0 66840.06 66842.22 1.88223 2026-04-03T09:34:00.000000
231 1775208900000 66842.23 66842.23 66805.45 66805.46 5.9966 2026-04-03T09:35:00.000000
232 1775208960000 66805.46 66812.76 66801.7 66812.76 15.84137 2026-04-03T09:36:00.000000
233 1775209020000 66812.76 66832.85 66812.76 66828.65 3.3294 2026-04-03T09:37:00.000000
234 1775209080000 66828.66 66839.83 66828.65 66839.83 2.79458 2026-04-03T09:38:00.000000
235 1775209140000 66839.82 66844.6 66838.97 66838.98 3.09882 2026-04-03T09:39:00.000000
236 1775209200000 66838.97 66875.1 66838.97 66875.09 4.43434 2026-04-03T09:40:00.000000
237 1775209260000 66875.1 66892.89 66875.09 66890.9 1.53262 2026-04-03T09:41:00.000000
238 1775209320000 66890.9 66912.96 66887.34 66912.96 2.43478 2026-04-03T09:42:00.000000
239 1775209380000 66912.96 66917.0 66912.95 66913.03 3.65842 2026-04-03T09:43:00.000000
240 1775209440000 66913.03 66913.03 66902.2 66902.2 2.39452 2026-04-03T09:44:00.000000
241 1775209500000 66902.2 66902.21 66875.09 66875.09 1.27985 2026-04-03T09:45:00.000000
242 1775209560000 66875.09 66881.99 66875.09 66881.98 0.87081 2026-04-03T09:46:00.000000
243 1775209620000 66881.99 66881.99 66804.81 66817.86 7.17945 2026-04-03T09:47:00.000000
244 1775209680000 66817.87 66837.64 66817.87 66837.64 1.60673 2026-04-03T09:48:00.000000
245 1775209740000 66837.63 66846.85 66837.63 66846.85 1.27394 2026-04-03T09:49:00.000000
246 1775209800000 66846.84 66850.56 66846.84 66850.55 0.7962 2026-04-03T09:50:00.000000
247 1775209860000 66850.56 66866.53 66850.56 66851.32 2.10445 2026-04-03T09:51:00.000000
248 1775209920000 66851.31 66851.32 66839.58 66843.48 1.55757 2026-04-03T09:52:00.000000
249 1775209980000 66843.49 66843.49 66821.2 66821.21 3.10013 2026-04-03T09:53:00.000000
250 1775210040000 66821.21 66824.99 66821.2 66821.49 0.86405 2026-04-03T09:54:00.000000
251 1775210100000 66821.49 66831.3 66821.48 66831.3 1.04173 2026-04-03T09:55:00.000000
252 1775210160000 66831.29 66851.32 66831.29 66851.32 0.756 2026-04-03T09:56:00.000000
253 1775210220000 66851.31 66859.57 66851.31 66859.57 0.74449 2026-04-03T09:57:00.000000
254 1775210280000 66859.56 66859.58 66859.56 66859.58 0.81269 2026-04-03T09:58:00.000000
255 1775210340000 66859.58 66859.58 66859.57 66859.58 0.6576 2026-04-03T09:59:00.000000
256 1775210400000 66859.58 66859.58 66823.85 66823.86 3.0008 2026-04-03T10:00:00.000000
257 1775210460000 66823.86 66828.33 66820.99 66824.22 1.10177 2026-04-03T10:01:00.000000
258 1775210520000 66824.22 66824.23 66793.74 66804.0 4.14844 2026-04-03T10:02:00.000000
259 1775210580000 66804.0 66804.33 66784.6 66804.33 1.36397 2026-04-03T10:03:00.000000
260 1775210640000 66804.32 66804.33 66790.03 66790.04 0.46164 2026-04-03T10:04:00.000000
261 1775210700000 66790.04 66829.29 66790.03 66829.29 0.98244 2026-04-03T10:05:00.000000
262 1775210760000 66829.28 66829.29 66819.59 66819.59 1.71069 2026-04-03T10:06:00.000000
263 1775210820000 66819.6 66819.6 66786.83 66786.84 1.49118 2026-04-03T10:07:00.000000
264 1775210880000 66786.83 66786.84 66778.73 66778.73 1.50321 2026-04-03T10:08:00.000000
265 1775210940000 66778.74 66796.58 66778.73 66795.11 4.40356 2026-04-03T10:09:00.000000
266 1775211000000 66795.12 66795.12 66786.83 66786.83 1.24206 2026-04-03T10:10:00.000000
267 1775211060000 66786.83 66813.58 66778.4 66813.58 4.52514 2026-04-03T10:11:00.000000
268 1775211120000 66813.58 66813.58 66811.4 66811.4 6.44744 2026-04-03T10:12:00.000000
269 1775211180000 66811.41 66825.1 66807.52 66825.09 1.22162 2026-04-03T10:13:00.000000
270 1775211240000 66825.1 66834.39 66825.08 66828.1 3.77548 2026-04-03T10:14:00.000000
271 1775211300000 66828.09 66830.4 66823.09 66823.1 0.96001 2026-04-03T10:15:00.000000
272 1775211360000 66823.09 66823.1 66775.06 66807.58 11.86481 2026-04-03T10:16:00.000000
273 1775211420000 66807.59 66835.88 66807.58 66835.87 1.01941 2026-04-03T10:17:00.000000
274 1775211480000 66835.87 66866.0 66835.87 66841.96 3.52657 2026-04-03T10:18:00.000000
275 1775211540000 66841.96 66841.96 66841.94 66841.95 0.48651 2026-04-03T10:19:00.000000
276 1775211600000 66841.95 66841.95 66825.1 66841.93 2.61026 2026-04-03T10:20:00.000000
277 1775211660000 66841.93 66847.2 66831.01 66831.41 7.74092 2026-04-03T10:21:00.000000
278 1775211720000 66831.4 66831.41 66806.37 66806.37 2.75372 2026-04-03T10:22:00.000000
279 1775211780000 66806.37 66806.38 66783.04 66783.05 2.78614 2026-04-03T10:23:00.000000
280 1775211840000 66783.05 66792.59 66773.06 66792.44 1.85436 2026-04-03T10:24:00.000000
281 1775211900000 66792.45 66818.7 66792.45 66818.7 11.62913 2026-04-03T10:25:00.000000
282 1775211960000 66818.71 66818.71 66814.55 66814.55 1.36972 2026-04-03T10:26:00.000000
283 1775212020000 66814.55 66818.33 66814.55 66815.38 1.70847 2026-04-03T10:27:00.000000
284 1775212080000 66815.38 66815.38 66801.0 66801.01 1.28946 2026-04-03T10:28:00.000000
285 1775212140000 66801.0 66801.01 66765.7 66765.71 22.34875 2026-04-03T10:29:00.000000
286 1775212200000 66765.71 66765.71 66705.46 66705.47 5.92362 2026-04-03T10:30:00.000000
287 1775212260000 66705.47 66705.47 66674.72 66698.82 7.62942 2026-04-03T10:31:00.000000
288 1775212320000 66698.81 66741.82 66698.81 66741.81 2.33127 2026-04-03T10:32:00.000000
289 1775212380000 66741.81 66748.5 66741.72 66744.89 2.56594 2026-04-03T10:33:00.000000
290 1775212440000 66744.89 66747.57 66731.05 66731.05 1.27721 2026-04-03T10:34:00.000000
291 1775212500000 66731.06 66731.06 66711.32 66723.64 3.69269 2026-04-03T10:35:00.000000
292 1775212560000 66723.65 66756.69 66723.64 66756.69 1.39776 2026-04-03T10:36:00.000000
293 1775212620000 66756.68 66773.15 66756.68 66756.7 2.40255 2026-04-03T10:37:00.000000
294 1775212680000 66756.69 66756.7 66728.47 66728.48 1.4785 2026-04-03T10:38:00.000000
295 1775212740000 66728.47 66742.48 66728.47 66742.48 1.73632 2026-04-03T10:39:00.000000
296 1775212800000 66742.47 66772.0 66742.47 66772.0 1.29848 2026-04-03T10:40:00.000000
297 1775212860000 66771.99 66781.91 66771.99 66781.9 2.63191 2026-04-03T10:41:00.000000
298 1775212920000 66781.91 66786.96 66781.9 66786.87 2.16233 2026-04-03T10:42:00.000000
299 1775212980000 66786.87 66797.73 66786.87 66793.74 1.22847 2026-04-03T10:43:00.000000
300 1775213040000 66793.73 66800.93 66781.9 66781.91 1.18021 2026-04-03T10:44:00.000000
301 1775213100000 66781.91 66800.81 66781.91 66800.81 1.59567 2026-04-03T10:45:00.000000
302 1775213160000 66800.81 66823.39 66800.8 66816.37 2.45361 2026-04-03T10:46:00.000000
303 1775213220000 66816.38 66824.45 66816.37 66824.45 1.43341 2026-04-03T10:47:00.000000
304 1775213280000 66824.45 66875.1 66824.45 66868.83 6.10687 2026-04-03T10:48:00.000000
305 1775213340000 66868.82 66868.83 66845.0 66845.0 2.05195 2026-04-03T10:49:00.000000
306 1775213400000 66845.01 66852.63 66836.02 66852.63 2.43097 2026-04-03T10:50:00.000000
307 1775213460000 66852.64 66875.1 66852.64 66872.47 1.81713 2026-04-03T10:51:00.000000
308 1775213520000 66872.47 66895.5 66864.7 66864.7 1.80454 2026-04-03T10:52:00.000000
309 1775213580000 66864.71 66864.71 66840.91 66840.91 1.06368 2026-04-03T10:53:00.000000
310 1775213640000 66840.92 66869.66 66840.91 66869.66 1.95358 2026-04-03T10:54:00.000000
311 1775213700000 66869.66 66885.84 66869.66 66874.3 10.80021 2026-04-03T10:55:00.000000
312 1775213760000 66874.3 66880.8 66874.29 66880.79 6.58341 2026-04-03T10:56:00.000000
313 1775213820000 66880.8 66888.96 66867.35 66880.0 9.17356 2026-04-03T10:57:00.000000
314 1775213880000 66880.0 66896.86 66877.5 66896.85 10.20135 2026-04-03T10:58:00.000000
315 1775213940000 66896.86 66898.97 66876.75 66883.08 5.59709 2026-04-03T10:59:00.000000
316 1775214000000 66883.09 66914.28 66875.09 66875.1 8.44422 2026-04-03T11:00:00.000000
317 1775214060000 66875.1 66875.1 66862.93 66865.54 1.97967 2026-04-03T11:01:00.000000
318 1775214120000 66865.55 66880.29 66863.05 66880.29 1.8254 2026-04-03T11:02:00.000000
319 1775214180000 66880.29 66905.16 66868.76 66875.08 2.58732 2026-04-03T11:03:00.000000
320 1775214240000 66875.09 66890.47 66875.08 66890.46 2.44594 2026-04-03T11:04:00.000000
321 1775214300000 66890.47 66890.47 66858.95 66858.95 1.18245 2026-04-03T11:05:00.000000
322 1775214360000 66858.96 66872.28 66847.4 66872.28 1.57732 2026-04-03T11:06:00.000000
323 1775214420000 66872.28 66878.0 66862.93 66862.94 3.22395 2026-04-03T11:07:00.000000
324 1775214480000 66862.94 66868.64 66855.15 66855.15 1.10567 2026-04-03T11:08:00.000000
325 1775214540000 66855.16 66855.16 66832.97 66842.63 1.61658 2026-04-03T11:09:00.000000
326 1775214600000 66842.63 66871.73 66842.63 66864.67 6.31907 2026-04-03T11:10:00.000000
327 1775214660000 66864.67 66864.68 66854.01 66854.02 2.04249 2026-04-03T11:11:00.000000
328 1775214720000 66854.02 66854.02 66849.67 66849.67 0.82933 2026-04-03T11:12:00.000000
329 1775214780000 66849.68 66849.68 66833.68 66833.68 1.12189 2026-04-03T11:13:00.000000
330 1775214840000 66833.68 66852.07 66823.18 66852.07 1.855 2026-04-03T11:14:00.000000
331 1775214900000 66852.07 66860.63 66852.06 66852.07 1.46006 2026-04-03T11:15:00.000000
332 1775214960000 66852.08 66878.0 66852.07 66873.88 2.88564 2026-04-03T11:16:00.000000
333 1775215020000 66873.88 66875.5 66849.67 66849.67 21.41347 2026-04-03T11:17:00.000000
334 1775215080000 66849.68 66862.0 66845.49 66859.05 2.35166 2026-04-03T11:18:00.000000
335 1775215140000 66859.05 66875.38 66859.04 66871.38 1.33744 2026-04-03T11:19:00.000000
336 1775215200000 66871.38 66905.04 66871.35 66905.04 2.34313 2026-04-03T11:20:00.000000
337 1775215260000 66905.03 66911.69 66905.03 66905.3 0.76227 2026-04-03T11:21:00.000000
338 1775215320000 66905.29 66936.58 66902.48 66931.19 5.0555 2026-04-03T11:22:00.000000
339 1775215380000 66931.19 66951.34 66930.13 66935.67 7.69158 2026-04-03T11:23:00.000000
340 1775215440000 66935.66 66935.66 66897.16 66899.83 3.27201 2026-04-03T11:24:00.000000
341 1775215500000 66899.82 66927.22 66899.82 66927.21 2.77702 2026-04-03T11:25:00.000000
342 1775215560000 66927.22 66940.93 66927.21 66940.93 1.5526 2026-04-03T11:26:00.000000
343 1775215620000 66940.93 66950.6 66928.02 66928.03 2.31882 2026-04-03T11:27:00.000000
344 1775215680000 66928.02 66960.18 66928.02 66960.17 2.48304 2026-04-03T11:28:00.000000
345 1775215740000 66960.17 66960.18 66937.62 66937.63 5.75833 2026-04-03T11:29:00.000000
346 1775215800000 66937.62 66953.94 66928.03 66928.03 2.25762 2026-04-03T11:30:00.000000
347 1775215860000 66928.04 66928.04 66922.66 66922.67 1.17744 2026-04-03T11:31:00.000000
348 1775215920000 66922.66 66992.99 66920.86 66989.68 16.2136 2026-04-03T11:32:00.000000
349 1775215980000 66989.68 66989.78 66960.17 66960.18 2.99091 2026-04-03T11:33:00.000000
350 1775216040000 66960.17 66960.18 66951.33 66951.34 1.22073 2026-04-03T11:34:00.000000
351 1775216100000 66951.34 66993.76 66951.33 66991.68 3.02096 2026-04-03T11:35:00.000000
352 1775216160000 66991.68 66991.68 66980.0 66980.0 3.08128 2026-04-03T11:36:00.000000
353 1775216220000 66980.01 67000.0 66980.01 66990.0 8.76096 2026-04-03T11:37:00.000000
354 1775216280000 66990.0 67004.28 66984.75 66984.75 9.06093 2026-04-03T11:38:00.000000
355 1775216340000 66984.69 66996.72 66980.0 66996.71 1.62359 2026-04-03T11:39:00.000000
356 1775216400000 66996.71 66997.25 66980.0 66980.01 1.2659 2026-04-03T11:40:00.000000
357 1775216460000 66980.01 66992.55 66980.0 66987.08 1.93114 2026-04-03T11:41:00.000000
358 1775216520000 66987.07 66995.71 66980.39 66995.7 1.73307 2026-04-03T11:42:00.000000
359 1775216580000 66995.71 67057.42 66995.71 67030.41 13.45846 2026-04-03T11:43:00.000000
360 1775216640000 67030.41 67030.41 67001.23 67001.23 2.14208 2026-04-03T11:44:00.000000
361 1775216700000 67001.23 67008.46 66980.01 67000.03 3.55673 2026-04-03T11:45:00.000000
362 1775216760000 67000.03 67042.56 67000.02 67038.62 5.39622 2026-04-03T11:46:00.000000
363 1775216820000 67038.62 67038.63 67017.61 67026.39 3.38088 2026-04-03T11:47:00.000000
364 1775216880000 67026.4 67026.4 67011.64 67011.64 1.28481 2026-04-03T11:48:00.000000
365 1775216940000 67011.64 67015.0 66968.27 66968.27 2.39094 2026-04-03T11:49:00.000000
366 1775217000000 66968.27 66974.7 66960.0 66974.7 8.30368 2026-04-03T11:50:00.000000
367 1775217060000 66974.7 66978.43 66968.27 66978.0 1.78751 2026-04-03T11:51:00.000000
368 1775217120000 66978.0 66997.32 66977.99 66995.05 1.33557 2026-04-03T11:52:00.000000
369 1775217180000 66995.04 67017.79 66995.04 67017.79 1.62975 2026-04-03T11:53:00.000000
370 1775217240000 67017.79 67026.66 67007.24 67007.25 1.07717 2026-04-03T11:54:00.000000
371 1775217300000 67007.25 67007.25 66987.23 67002.33 2.68066 2026-04-03T11:55:00.000000
372 1775217360000 67002.33 67014.0 67002.32 67014.0 2.94748 2026-04-03T11:56:00.000000
373 1775217420000 67014.0 67014.0 66992.65 67006.55 3.15317 2026-04-03T11:57:00.000000
374 1775217480000 67006.56 67006.56 66999.46 66999.47 2.70078 2026-04-03T11:58:00.000000
375 1775217540000 66999.47 67017.44 66999.46 67017.44 2.73166 2026-04-03T11:59:00.000000
376 1775217600000 67017.43 67030.0 67014.41 67019.72 2.58999 2026-04-03T12:00:00.000000
377 1775217660000 67019.72 67032.72 67019.72 67032.0 2.63058 2026-04-03T12:01:00.000000
378 1775217720000 67032.0 67051.12 67022.0 67022.0 2.13202 2026-04-03T12:02:00.000000
379 1775217780000 67022.01 67026.28 67020.03 67026.27 1.53715 2026-04-03T12:03:00.000000
380 1775217840000 67026.28 67026.28 67006.9 67013.62 0.9561 2026-04-03T12:04:00.000000
381 1775217900000 67013.61 67013.62 67006.9 67006.9 1.29353 2026-04-03T12:05:00.000000
382 1775217960000 67006.91 67033.47 67006.9 67033.47 2.50337 2026-04-03T12:06:00.000000
383 1775218020000 67033.47 67050.23 67006.9 67006.91 4.18078 2026-04-03T12:07:00.000000
384 1775218080000 67006.9 67025.52 67006.9 67025.52 2.11836 2026-04-03T12:08:00.000000
385 1775218140000 67025.52 67025.52 67024.44 67024.44 0.86738 2026-04-03T12:09:00.000000
386 1775218200000 67024.44 67024.44 66941.5 66948.01 6.86816 2026-04-03T12:10:00.000000
387 1775218260000 66948.01 66960.01 66948.0 66960.01 2.9015 2026-04-03T12:11:00.000000
388 1775218320000 66960.01 66978.43 66960.0 66969.8 9.58986 2026-04-03T12:12:00.000000
389 1775218380000 66969.81 66982.99 66969.8 66982.99 2.41067 2026-04-03T12:13:00.000000
390 1775218440000 66982.99 67008.0 66982.98 67008.0 2.52133 2026-04-03T12:14:00.000000
391 1775218500000 67008.0 67059.59 67007.99 67032.05 6.24381 2026-04-03T12:15:00.000000
392 1775218560000 67032.05 67032.05 66965.11 66965.11 2.16879 2026-04-03T12:16:00.000000
393 1775218620000 66965.11 66965.12 66965.11 66965.11 1.55971 2026-04-03T12:17:00.000000
394 1775218680000 66965.11 66965.12 66951.33 66957.97 1.2984 2026-04-03T12:18:00.000000
395 1775218740000 66957.97 66989.01 66957.96 66988.79 2.64916 2026-04-03T12:19:00.000000
396 1775218800000 66988.79 67020.0 66988.79 67019.99 1.2246 2026-04-03T12:20:00.000000
397 1775218860000 67019.99 67019.99 66998.17 67003.06 3.32236 2026-04-03T12:21:00.000000
398 1775218920000 67003.06 67020.0 66984.0 66984.01 3.09956 2026-04-03T12:22:00.000000
399 1775218980000 66984.0 66984.01 66946.62 66952.98 35.37469 2026-04-03T12:23:00.000000
400 1775219040000 66952.99 66956.77 66942.35 66956.77 2.09136 2026-04-03T12:24:00.000000
401 1775219100000 66956.77 67000.0 66956.77 66999.99 3.38862 2026-04-03T12:25:00.000000
402 1775219160000 67000.0 67019.58 66999.99 67005.01 1.06405 2026-04-03T12:26:00.000000
403 1775219220000 67005.01 67034.48 67005.01 67034.48 8.1581 2026-04-03T12:27:00.000000
404 1775219280000 67034.48 67144.09 67034.48 67094.01 14.33131 2026-04-03T12:28:00.000000
405 1775219340000 67094.01 67116.0 67088.52 67115.99 3.48036 2026-04-03T12:29:00.000000
406 1775219400000 67115.99 67370.42 67001.9 67014.71 289.34002 2026-04-03T12:30:00.000000
407 1775219460000 67014.71 67021.17 66878.57 66967.49 32.91348 2026-04-03T12:31:00.000000
408 1775219520000 66967.49 66973.0 66894.38 66920.01 16.29626 2026-04-03T12:32:00.000000
409 1775219580000 66920.01 66961.03 66893.8 66940.01 10.352 2026-04-03T12:33:00.000000
410 1775219640000 66940.01 66940.02 66900.79 66926.67 3.07772 2026-04-03T12:34:00.000000
411 1775219700000 66926.66 66961.03 66849.67 66849.68 6.04789 2026-04-03T12:35:00.000000
412 1775219760000 66849.68 66892.92 66847.69 66849.68 10.56621 2026-04-03T12:36:00.000000
413 1775219820000 66849.68 66853.67 66809.17 66836.34 12.50796 2026-04-03T12:37:00.000000
414 1775219880000 66836.34 66845.13 66805.2 66837.22 7.26132 2026-04-03T12:38:00.000000
415 1775219940000 66837.21 66845.0 66812.0 66834.73 4.37124 2026-04-03T12:39:00.000000
416 1775220000000 66834.73 66889.2 66832.09 66833.14 10.31768 2026-04-03T12:40:00.000000
417 1775220060000 66833.14 66840.58 66796.43 66840.58 4.27452 2026-04-03T12:41:00.000000
418 1775220120000 66840.58 66878.4 66828.3 66878.39 3.41869 2026-04-03T12:42:00.000000
419 1775220180000 66878.4 66891.99 66868.0 66871.09 3.11944 2026-04-03T12:43:00.000000
420 1775220240000 66871.09 66874.85 66841.05 66867.31 6.45471 2026-04-03T12:44:00.000000
421 1775220300000 66867.31 66899.99 66862.93 66883.01 2.25666 2026-04-03T12:45:00.000000
422 1775220360000 66883.01 66883.02 66868.62 66868.62 11.60254 2026-04-03T12:46:00.000000
423 1775220420000 66868.62 66874.43 66821.84 66821.85 2.34366 2026-04-03T12:47:00.000000
424 1775220480000 66821.85 66860.64 66805.27 66805.27 2.8112 2026-04-03T12:48:00.000000
425 1775220540000 66805.27 66805.27 66750.0 66750.01 8.30559 2026-04-03T12:49:00.000000
426 1775220600000 66750.01 66750.01 66646.36 66737.15 59.9241 2026-04-03T12:50:00.000000
427 1775220660000 66737.15 66753.45 66716.9 66716.91 9.88561 2026-04-03T12:51:00.000000
428 1775220720000 66716.91 66756.0 66716.91 66736.32 33.90108 2026-04-03T12:52:00.000000
429 1775220780000 66736.33 66787.83 66736.32 66760.94 64.3983 2026-04-03T12:53:00.000000
430 1775220840000 66760.94 66777.48 66758.57 66771.65 8.23481 2026-04-03T12:54:00.000000
431 1775220900000 66771.65 66777.47 66759.47 66777.46 15.33841 2026-04-03T12:55:00.000000
432 1775220960000 66777.46 66810.61 66753.4 66800.11 5.7686 2026-04-03T12:56:00.000000
433 1775221020000 66800.12 66809.33 66765.7 66768.9 12.6327 2026-04-03T12:57:00.000000
434 1775221080000 66768.89 66768.9 66742.14 66742.14 3.98556 2026-04-03T12:58:00.000000
435 1775221140000 66742.14 66753.67 66703.98 66722.16 11.74689 2026-04-03T12:59:00.000000
436 1775221200000 66722.16 66729.46 66683.16 66698.42 8.97083 2026-04-03T13:00:00.000000
437 1775221260000 66698.42 66717.85 66653.38 66717.85 14.38555 2026-04-03T13:01:00.000000
438 1775221320000 66717.86 66735.62 66680.34 66708.0 10.08679 2026-04-03T13:02:00.000000
439 1775221380000 66708.0 66710.0 66705.08 66707.34 9.48014 2026-04-03T13:03:00.000000
440 1775221440000 66707.34 66707.34 66626.77 66627.95 10.44664 2026-04-03T13:04:00.000000
441 1775221500000 66627.95 66680.05 66627.94 66676.1 4.55243 2026-04-03T13:05:00.000000
442 1775221560000 66676.11 66710.0 66670.33 66670.33 3.3808 2026-04-03T13:06:00.000000
443 1775221620000 66670.34 66670.34 66641.14 66646.37 3.28778 2026-04-03T13:07:00.000000
444 1775221680000 66646.36 66654.34 66577.03 66598.56 15.67526 2026-04-03T13:08:00.000000
445 1775221740000 66598.56 66630.48 66595.19 66603.59 6.69989 2026-04-03T13:09:00.000000
446 1775221800000 66603.59 66607.99 66563.06 66587.73 7.37278 2026-04-03T13:10:00.000000
447 1775221860000 66587.72 66596.44 66562.72 66562.72 6.45342 2026-04-03T13:11:00.000000
448 1775221920000 66562.71 66637.69 66561.05 66637.69 9.44229 2026-04-03T13:12:00.000000
449 1775221980000 66637.69 66649.13 66619.94 66643.06 6.62657 2026-04-03T13:13:00.000000
450 1775222040000 66643.06 66672.07 66638.0 66672.07 12.93657 2026-04-03T13:14:00.000000
451 1775222100000 66672.07 66693.22 66646.75 66680.02 12.72778 2026-04-03T13:15:00.000000
452 1775222160000 66680.02 66689.08 66624.64 66655.41 9.09104 2026-04-03T13:16:00.000000
453 1775222220000 66655.41 66655.41 66596.66 66624.44 10.79523 2026-04-03T13:17:00.000000
454 1775222280000 66624.45 66658.03 66624.44 66658.03 4.59921 2026-04-03T13:18:00.000000
455 1775222340000 66658.03 66680.63 66658.02 66668.46 4.60014 2026-04-03T13:19:00.000000
456 1775222400000 66668.46 66696.29 66658.0 66695.58 7.91823 2026-04-03T13:20:00.000000
457 1775222460000 66695.57 66726.67 66685.43 66695.99 12.20486 2026-04-03T13:21:00.000000
458 1775222520000 66695.99 66717.69 66692.04 66710.33 8.24302 2026-04-03T13:22:00.000000
459 1775222580000 66710.33 66710.33 66702.04 66703.36 3.06914 2026-04-03T13:23:00.000000
460 1775222640000 66703.36 66703.36 66699.91 66700.59 7.81359 2026-04-03T13:24:00.000000
461 1775222700000 66700.58 66717.04 66680.39 66717.03 22.12502 2026-04-03T13:25:00.000000
462 1775222760000 66717.04 66724.52 66682.43 66682.44 5.02702 2026-04-03T13:26:00.000000
463 1775222820000 66682.43 66684.46 66672.44 66682.15 5.05727 2026-04-03T13:27:00.000000
464 1775222880000 66682.16 66697.68 66660.34 66660.35 5.17874 2026-04-03T13:28:00.000000
465 1775222940000 66660.35 66693.29 66660.34 66688.73 39.28154 2026-04-03T13:29:00.000000
466 1775223000000 66688.72 66688.73 66649.07 66649.07 5.38932 2026-04-03T13:30:00.000000
467 1775223060000 66649.07 66674.06 66616.79 66669.01 9.18955 2026-04-03T13:31:00.000000
468 1775223120000 66669.01 66669.01 66650.51 66659.94 15.13676 2026-04-03T13:32:00.000000
469 1775223180000 66659.94 66669.91 66580.44 66606.0 68.31757 2026-04-03T13:33:00.000000
470 1775223240000 66606.0 66626.84 66589.05 66626.84 8.3131 2026-04-03T13:34:00.000000
471 1775223300000 66626.83 66669.91 66624.21 66661.41 5.12626 2026-04-03T13:35:00.000000
472 1775223360000 66661.4 66666.23 66632.3 66637.25 5.13057 2026-04-03T13:36:00.000000
473 1775223420000 66637.25 66648.18 66627.71 66636.37 2.95846 2026-04-03T13:37:00.000000
474 1775223480000 66636.36 66636.36 66594.04 66610.68 4.16015 2026-04-03T13:38:00.000000
475 1775223540000 66610.67 66617.77 66601.19 66609.67 6.37921 2026-04-03T13:39:00.000000
476 1775223600000 66609.66 66610.03 66601.18 66608.59 4.01657 2026-04-03T13:40:00.000000
477 1775223660000 66608.59 66608.59 66587.33 66605.23 3.93411 2026-04-03T13:41:00.000000
478 1775223720000 66605.22 66627.24 66605.22 66616.19 4.08756 2026-04-03T13:42:00.000000
479 1775223780000 66616.2 66622.82 66595.53 66595.54 2.45207 2026-04-03T13:43:00.000000
480 1775223840000 66595.53 66602.66 66595.53 66602.66 3.0955 2026-04-03T13:44:00.000000
481 1775223900000 66602.66 66609.68 66587.14 66587.14 4.33359 2026-04-03T13:45:00.000000
482 1775223960000 66587.15 66621.93 66587.15 66607.57 4.68552 2026-04-03T13:46:00.000000
483 1775224020000 66607.58 66613.31 66568.24 66568.24 5.58519 2026-04-03T13:47:00.000000
484 1775224080000 66568.24 66568.24 66533.35 66539.58 8.48778 2026-04-03T13:48:00.000000
485 1775224140000 66539.58 66547.0 66508.1 66508.1 13.3492 2026-04-03T13:49:00.000000
486 1775224200000 66508.1 66532.66 66508.1 66522.96 4.53322 2026-04-03T13:50:00.000000
487 1775224260000 66522.96 66547.99 66522.96 66547.99 8.68209 2026-04-03T13:51:00.000000
488 1775224320000 66547.98 66582.29 66539.79 66582.28 11.1393 2026-04-03T13:52:00.000000
489 1775224380000 66582.29 66635.39 66582.29 66635.39 34.68692 2026-04-03T13:53:00.000000
490 1775224440000 66635.38 66680.33 66634.86 66680.32 7.8464 2026-04-03T13:54:00.000000
491 1775224500000 66680.33 66680.33 66644.47 66649.2 7.23775 2026-04-03T13:55:00.000000
492 1775224560000 66649.2 66649.21 66632.22 66632.23 1.18501 2026-04-03T13:56:00.000000
493 1775224620000 66632.23 66658.08 66622.0 66658.08 4.47329 2026-04-03T13:57:00.000000
494 1775224680000 66658.07 66673.38 66658.07 66673.38 1.05921 2026-04-03T13:58:00.000000
495 1775224740000 66673.38 66673.38 66667.09 66672.0 2.43319 2026-04-03T13:59:00.000000
496 1775224800000 66671.99 66675.8 66648.06 66648.06 5.05901 2026-04-03T14:00:00.000000
497 1775224860000 66648.07 66652.3 66648.07 66652.22 2.21796 2026-04-03T14:01:00.000000
498 1775224920000 66652.22 66683.69 66634.0 66661.3 4.88277 2026-04-03T14:02:00.000000
499 1775224980000 66661.31 66748.03 66655.0 66736.76 9.91011 2026-04-03T14:03:00.000000
500 1775225040000 66736.32 66765.71 66710.66 66717.91 16.2169 2026-04-03T14:04:00.000000
501 1775225100000 66717.91 66730.54 66717.91 66722.04 1.51069 2026-04-03T14:05:00.000000
+41
View File
@@ -0,0 +1,41 @@
price,qty,side
66793.89,0.08639,bid
66793.88,0.00072,bid
66793.87,0.00008,bid
66793.86,0.00024,bid
66793.85,0.05024,bid
66793.84,0.00016,bid
66793.81,0.00024,bid
66793.8,0.05032,bid
66793.44,0.00008,bid
66793.07,0.00024,bid
66793.06,0.01505,bid
66792.88,0.00008,bid
66792.46,0.00831,bid
66792.4,0.00029,bid
66792.38,0.00008,bid
66792.21,0.00008,bid
66792.0,0.00008,bid
66791.83,0.0039,bid
66791.67,0.00016,bid
66791.66,0.06358,bid
66793.9,1.94694,ask
66793.91,0.00016,ask
66794.0,0.00129,ask
66794.6,0.00532,ask
66794.67,0.00008,ask
66794.99,0.003,ask
66795.12,0.00009,ask
66795.41,0.00755,ask
66795.71,0.00016,ask
66795.72,0.04151,ask
66795.73,0.13112,ask
66795.74,0.00008,ask
66796.0,0.0012,ask
66796.57,0.00262,ask
66796.66,0.003,ask
66796.8,0.00008,ask
66797.06,0.31234,ask
66797.08,0.11709,ask
66797.09,0.00029,ask
66797.3,0.00222,ask
1 price qty side
2 66793.89 0.08639 bid
3 66793.88 0.00072 bid
4 66793.87 0.00008 bid
5 66793.86 0.00024 bid
6 66793.85 0.05024 bid
7 66793.84 0.00016 bid
8 66793.81 0.00024 bid
9 66793.8 0.05032 bid
10 66793.44 0.00008 bid
11 66793.07 0.00024 bid
12 66793.06 0.01505 bid
13 66792.88 0.00008 bid
14 66792.46 0.00831 bid
15 66792.4 0.00029 bid
16 66792.38 0.00008 bid
17 66792.21 0.00008 bid
18 66792.0 0.00008 bid
19 66791.83 0.0039 bid
20 66791.67 0.00016 bid
21 66791.66 0.06358 bid
22 66793.9 1.94694 ask
23 66793.91 0.00016 ask
24 66794.0 0.00129 ask
25 66794.6 0.00532 ask
26 66794.67 0.00008 ask
27 66794.99 0.003 ask
28 66795.12 0.00009 ask
29 66795.41 0.00755 ask
30 66795.71 0.00016 ask
31 66795.72 0.04151 ask
32 66795.73 0.13112 ask
33 66795.74 0.00008 ask
34 66796.0 0.0012 ask
35 66796.57 0.00262 ask
36 66796.66 0.003 ask
37 66796.8 0.00008 ask
38 66797.06 0.31234 ask
39 66797.08 0.11709 ask
40 66797.09 0.00029 ask
41 66797.3 0.00222 ask
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
trade_id,side,price,amount,buy_order_id,sell_order_id,timestamp,microtimestamp
555923544,buy,66879.0,0.01327193,1992262225506305,1992262052610055,1775227119,1775227119556000
555923545,buy,66879.0,0.00155663,1992262225506305,1992262225227776,1775227119,1775227119556000
555923566,buy,66898.0,0.00007677,1992262268186625,1992262234124290,1775227129,1775227129976000
555923579,buy,66903.0,0.00029642,1992262292611072,1992262286491650,1775227135,1775227135939000
555923581,sell,66902.0,0.075,1992262298943491,1992262301265924,1775227138,1775227138052000
555923601,sell,66889.0,0.00095353,1992262302203907,1992262350704652,1775227150,1775227150122000
555923627,buy,66879.0,0.00148263,1992262424711168,1992262409236482,1775227168,1775227168190000
555923645,buy,66886.0,0.02242337,1992262478069761,1992262431969290,1775227181,1775227181218000
555923646,buy,66886.0,0.02590868,1992262478069761,1992262465957888,1775227181,1775227181218000
555923651,buy,66879.0,0.02242337,1992262513623043,1992262508392449,1775227189,1775227189897000
555923652,buy,66879.0,0.02757663,1992262513623043,1992262508584961,1775227189,1775227189897000
555923666,sell,66889.0,0.02757663,1992262543187977,1992262568288264,1775227203,1775227203243000
555923667,sell,66889.0,0.10929673,1992262545416193,1992262568288264,1775227203,1775227203243000
555923668,sell,66888.0,0.03189722,1992262545412096,1992262569066496,1775227203,1775227203433000
555923723,buy,66860.0,0.00821891,1992262693527553,1992262680788993,1775227233,1775227233819000
555923724,buy,66860.0,0.00106,1992262693859328,1992262680788993,1775227233,1775227233900000
555923736,buy,66860.0,0.0052,1992262724993024,1992262680788993,1775227241,1775227241501000
555923737,buy,66860.0,0.00386139,1992262728818690,1992262680788993,1775227242,1775227242435000
555923739,buy,66860.0,0.0000148,1992262731710464,1992262680788993,1775227243,1775227243141000
555923742,buy,66860.0,0.00123,1992262753402880,1992262680788993,1775227248,1775227248437000
555923746,buy,66860.0,0.00109746,1992262791847936,1992262680788993,1775227257,1775227257823000
555923762,buy,66859.0,0.00092391,1992262816784385,1992262794743824,1775227263,1775227263912000
555923763,buy,66859.0,0.0047,1992262816976897,1992262794743824,1775227263,1775227263958000
555923768,buy,66859.0,0.00410159,1992262827986944,1992262794858496,1775227266,1775227266646000
555923769,buy,66859.0,0.00375,1992262828830721,1992262794858496,1775227266,1775227266852000
555923770,buy,66859.0,0.00022443,1992262829400064,1992262794858496,1775227266,1775227266991000
555923771,buy,66859.0,0.00375,1992262829977600,1992262794858496,1775227267,1775227267132000
555923772,buy,66859.0,0.00375,1992262831710208,1992262794858496,1775227267,1775227267555000
555923773,buy,66859.0,0.05625,1992262832484356,1992262794858496,1775227267,1775227267745000
555923774,buy,66859.0,0.00375,1992262832762884,1992262794858496,1775227267,1775227267812000
555923775,buy,66859.0,0.00375,1992262833315840,1992262794858496,1775227267,1775227267947000
555923776,buy,66859.0,0.04667398,1992262833442819,1992262794858496,1775227267,1775227267979000
555923777,buy,66859.0,0.00957602,1992262833442819,1992262818598917,1775227267,1775227267979000
555923778,buy,66859.0,0.00375,1992262834438145,1992262818598917,1775227268,1775227268221000
555923779,buy,66859.0,0.00909735,1992262834606084,1992262818598917,1775227268,1775227268262000
555923780,buy,66860.0,0.06590265,1992262834606084,1992262810214401,1775227268,1775227268262000
555923783,buy,66859.0,0.02242337,1992262835220480,1992262834733056,1775227268,1775227268413000
555923784,buy,66860.0,0.00888108,1992262835220480,1992262810214401,1775227268,1775227268413000
555923785,buy,66860.0,0.00375,1992262835945472,1992262835494913,1775227268,1775227268589000
555923786,buy,66860.0,0.01867337,1992262836375552,1992262835494913,1775227268,1775227268694000
555923787,buy,66860.0,0.03757663,1992262836375552,1992262835920901,1775227268,1775227268694000
555923788,buy,66860.0,0.00375,1992262836535297,1992262835920901,1775227268,1775227268733000
555923790,buy,66860.0,0.00375,1992262837018627,1992262835920901,1775227268,1775227268852000
555923791,buy,66860.0,0.0297071,1992262837039105,1992262835920901,1775227268,1775227268856000
555923792,buy,66860.0,0.02242337,1992262837039105,1992262836502528,1775227268,1775227268856000
555923793,buy,66860.0,0.02242337,1992262837743616,1992262837211136,1775227269,1775227269028000
555923794,buy,66861.0,0.00419616,1992262837743616,1992262811152386,1775227269,1775227269028000
555923795,buy,66860.0,0.00375,1992262838194176,1992262837866498,1775227269,1775227269138000
555923796,buy,66860.0,0.01867337,1992262838312960,1992262837866498,1775227269,1775227269168000
555923797,buy,66860.0,0.05632663,1992262838312960,1992262837927937,1775227269,1775227269168000
555923798,buy,66860.0,0.00375,1992262838730752,1992262837927937,1775227269,1775227269269000
555923799,buy,66860.0,0.06592337,1992262838894593,1992262837927937,1775227269,1775227269309000
555923800,buy,66860.0,0.00907663,1992262838894593,1992262837968896,1775227269,1775227269309000
555923801,buy,66860.0,0.00375,1992262839214080,1992262837968896,1775227269,1775227269387000
555923803,buy,66860.0,0.05625,1992262839549952,1992262837968896,1775227269,1775227269469000
555923805,buy,66860.0,0.00375,1992262839705601,1992262837968896,1775227269,1775227269508000
555923806,buy,66860.0,0.0019571,1992262840037377,1992262837968896,1775227269,1775227269588000
555923808,buy,66860.0,0.02242337,1992262840635396,1992262838435840,1775227269,1775227269735000
555923809,buy,66860.0,0.05236036,1992262840635396,1992262840475650,1775227269,1775227269735000
555923810,buy,66860.0,0.00375,1992262840877057,1992262840475650,1775227269,1775227269793000
555923811,buy,66860.0,0.01867337,1992262841241600,1992262840475650,1775227269,1775227269882000
555923812,buy,66860.0,0.02242337,1992262841241600,1992262840807424,1775227269,1775227269882000
555923813,buy,66860.0,0.02242337,1992262841892865,1992262841413632,1775227270,1775227270041000
555923814,buy,66861.0,0.04816271,1992262841892865,1992262811152386,1775227270,1775227270041000
555923816,buy,66861.0,0.00375,1992262842421250,1992262811152386,1775227270,1775227270171000
555923817,buy,66861.0,0.01867337,1992262842490880,1992262811152386,1775227270,1775227270187000
555923818,buy,66861.0,0.02242337,1992262842490880,1992262842216450,1775227270,1775227270187000
555923819,buy,66862.0,0.03390326,1992262842490880,1992262811111431,1775227270,1775227270187000
555923822,buy,66860.0,0.0014807,1992262853017600,1992262843183104,1775227272,1775227272757000
555923839,buy,66867.0,0.02173126,1992262895083541,1992262890827780,1775227283,1775227283027000
555923863,buy,66870.0,9.03e-6,1992262967214081,1992262916182017,1775227300,1775227300637000
555923874,sell,66869.0,0.09328224,1992262896930816,1992263032487936,1775227316,1775227316573000
555923875,sell,66869.0,0.10671776,1992262897733633,1992263032487936,1775227316,1775227316573000
555923907,sell,66863.0,0.00111,1992263102058496,1992263104647169,1775227334,1775227334190000
555923942,sell,66871.0,0.00057763,1992263185809411,1992263219957761,1775227362,1775227362342000
555923946,sell,66857.0,0.01495673,1992263219818499,1992263221239868,1775227362,1775227362656000
555923966,buy,66840.0,0.00001481,1992263296352257,1992263294287876,1775227380,1775227380993000
555923967,buy,66840.0,0.00082214,1992263310286848,1992263294287876,1775227384,1775227384395000
555923974,buy,66828.0,0.00086049,1992263321845761,1992263320252428,1775227387,1775227387217000
555923980,buy,66827.0,0.00328919,1992263340531713,1992263328579584,1775227391,1775227391779000
555923992,buy,66822.0,0.00047598,1992263353720832,1992263342510085,1775227394,1775227394999000
555924016,buy,66821.0,0.00079,1992263395639298,1992263365795840,1775227405,1775227405233000
1 trade_id side price amount buy_order_id sell_order_id timestamp microtimestamp
2 555923544 buy 66879.0 0.01327193 1992262225506305 1992262052610055 1775227119 1775227119556000
3 555923545 buy 66879.0 0.00155663 1992262225506305 1992262225227776 1775227119 1775227119556000
4 555923566 buy 66898.0 0.00007677 1992262268186625 1992262234124290 1775227129 1775227129976000
5 555923579 buy 66903.0 0.00029642 1992262292611072 1992262286491650 1775227135 1775227135939000
6 555923581 sell 66902.0 0.075 1992262298943491 1992262301265924 1775227138 1775227138052000
7 555923601 sell 66889.0 0.00095353 1992262302203907 1992262350704652 1775227150 1775227150122000
8 555923627 buy 66879.0 0.00148263 1992262424711168 1992262409236482 1775227168 1775227168190000
9 555923645 buy 66886.0 0.02242337 1992262478069761 1992262431969290 1775227181 1775227181218000
10 555923646 buy 66886.0 0.02590868 1992262478069761 1992262465957888 1775227181 1775227181218000
11 555923651 buy 66879.0 0.02242337 1992262513623043 1992262508392449 1775227189 1775227189897000
12 555923652 buy 66879.0 0.02757663 1992262513623043 1992262508584961 1775227189 1775227189897000
13 555923666 sell 66889.0 0.02757663 1992262543187977 1992262568288264 1775227203 1775227203243000
14 555923667 sell 66889.0 0.10929673 1992262545416193 1992262568288264 1775227203 1775227203243000
15 555923668 sell 66888.0 0.03189722 1992262545412096 1992262569066496 1775227203 1775227203433000
16 555923723 buy 66860.0 0.00821891 1992262693527553 1992262680788993 1775227233 1775227233819000
17 555923724 buy 66860.0 0.00106 1992262693859328 1992262680788993 1775227233 1775227233900000
18 555923736 buy 66860.0 0.0052 1992262724993024 1992262680788993 1775227241 1775227241501000
19 555923737 buy 66860.0 0.00386139 1992262728818690 1992262680788993 1775227242 1775227242435000
20 555923739 buy 66860.0 0.0000148 1992262731710464 1992262680788993 1775227243 1775227243141000
21 555923742 buy 66860.0 0.00123 1992262753402880 1992262680788993 1775227248 1775227248437000
22 555923746 buy 66860.0 0.00109746 1992262791847936 1992262680788993 1775227257 1775227257823000
23 555923762 buy 66859.0 0.00092391 1992262816784385 1992262794743824 1775227263 1775227263912000
24 555923763 buy 66859.0 0.0047 1992262816976897 1992262794743824 1775227263 1775227263958000
25 555923768 buy 66859.0 0.00410159 1992262827986944 1992262794858496 1775227266 1775227266646000
26 555923769 buy 66859.0 0.00375 1992262828830721 1992262794858496 1775227266 1775227266852000
27 555923770 buy 66859.0 0.00022443 1992262829400064 1992262794858496 1775227266 1775227266991000
28 555923771 buy 66859.0 0.00375 1992262829977600 1992262794858496 1775227267 1775227267132000
29 555923772 buy 66859.0 0.00375 1992262831710208 1992262794858496 1775227267 1775227267555000
30 555923773 buy 66859.0 0.05625 1992262832484356 1992262794858496 1775227267 1775227267745000
31 555923774 buy 66859.0 0.00375 1992262832762884 1992262794858496 1775227267 1775227267812000
32 555923775 buy 66859.0 0.00375 1992262833315840 1992262794858496 1775227267 1775227267947000
33 555923776 buy 66859.0 0.04667398 1992262833442819 1992262794858496 1775227267 1775227267979000
34 555923777 buy 66859.0 0.00957602 1992262833442819 1992262818598917 1775227267 1775227267979000
35 555923778 buy 66859.0 0.00375 1992262834438145 1992262818598917 1775227268 1775227268221000
36 555923779 buy 66859.0 0.00909735 1992262834606084 1992262818598917 1775227268 1775227268262000
37 555923780 buy 66860.0 0.06590265 1992262834606084 1992262810214401 1775227268 1775227268262000
38 555923783 buy 66859.0 0.02242337 1992262835220480 1992262834733056 1775227268 1775227268413000
39 555923784 buy 66860.0 0.00888108 1992262835220480 1992262810214401 1775227268 1775227268413000
40 555923785 buy 66860.0 0.00375 1992262835945472 1992262835494913 1775227268 1775227268589000
41 555923786 buy 66860.0 0.01867337 1992262836375552 1992262835494913 1775227268 1775227268694000
42 555923787 buy 66860.0 0.03757663 1992262836375552 1992262835920901 1775227268 1775227268694000
43 555923788 buy 66860.0 0.00375 1992262836535297 1992262835920901 1775227268 1775227268733000
44 555923790 buy 66860.0 0.00375 1992262837018627 1992262835920901 1775227268 1775227268852000
45 555923791 buy 66860.0 0.0297071 1992262837039105 1992262835920901 1775227268 1775227268856000
46 555923792 buy 66860.0 0.02242337 1992262837039105 1992262836502528 1775227268 1775227268856000
47 555923793 buy 66860.0 0.02242337 1992262837743616 1992262837211136 1775227269 1775227269028000
48 555923794 buy 66861.0 0.00419616 1992262837743616 1992262811152386 1775227269 1775227269028000
49 555923795 buy 66860.0 0.00375 1992262838194176 1992262837866498 1775227269 1775227269138000
50 555923796 buy 66860.0 0.01867337 1992262838312960 1992262837866498 1775227269 1775227269168000
51 555923797 buy 66860.0 0.05632663 1992262838312960 1992262837927937 1775227269 1775227269168000
52 555923798 buy 66860.0 0.00375 1992262838730752 1992262837927937 1775227269 1775227269269000
53 555923799 buy 66860.0 0.06592337 1992262838894593 1992262837927937 1775227269 1775227269309000
54 555923800 buy 66860.0 0.00907663 1992262838894593 1992262837968896 1775227269 1775227269309000
55 555923801 buy 66860.0 0.00375 1992262839214080 1992262837968896 1775227269 1775227269387000
56 555923803 buy 66860.0 0.05625 1992262839549952 1992262837968896 1775227269 1775227269469000
57 555923805 buy 66860.0 0.00375 1992262839705601 1992262837968896 1775227269 1775227269508000
58 555923806 buy 66860.0 0.0019571 1992262840037377 1992262837968896 1775227269 1775227269588000
59 555923808 buy 66860.0 0.02242337 1992262840635396 1992262838435840 1775227269 1775227269735000
60 555923809 buy 66860.0 0.05236036 1992262840635396 1992262840475650 1775227269 1775227269735000
61 555923810 buy 66860.0 0.00375 1992262840877057 1992262840475650 1775227269 1775227269793000
62 555923811 buy 66860.0 0.01867337 1992262841241600 1992262840475650 1775227269 1775227269882000
63 555923812 buy 66860.0 0.02242337 1992262841241600 1992262840807424 1775227269 1775227269882000
64 555923813 buy 66860.0 0.02242337 1992262841892865 1992262841413632 1775227270 1775227270041000
65 555923814 buy 66861.0 0.04816271 1992262841892865 1992262811152386 1775227270 1775227270041000
66 555923816 buy 66861.0 0.00375 1992262842421250 1992262811152386 1775227270 1775227270171000
67 555923817 buy 66861.0 0.01867337 1992262842490880 1992262811152386 1775227270 1775227270187000
68 555923818 buy 66861.0 0.02242337 1992262842490880 1992262842216450 1775227270 1775227270187000
69 555923819 buy 66862.0 0.03390326 1992262842490880 1992262811111431 1775227270 1775227270187000
70 555923822 buy 66860.0 0.0014807 1992262853017600 1992262843183104 1775227272 1775227272757000
71 555923839 buy 66867.0 0.02173126 1992262895083541 1992262890827780 1775227283 1775227283027000
72 555923863 buy 66870.0 9.03e-6 1992262967214081 1992262916182017 1775227300 1775227300637000
73 555923874 sell 66869.0 0.09328224 1992262896930816 1992263032487936 1775227316 1775227316573000
74 555923875 sell 66869.0 0.10671776 1992262897733633 1992263032487936 1775227316 1775227316573000
75 555923907 sell 66863.0 0.00111 1992263102058496 1992263104647169 1775227334 1775227334190000
76 555923942 sell 66871.0 0.00057763 1992263185809411 1992263219957761 1775227362 1775227362342000
77 555923946 sell 66857.0 0.01495673 1992263219818499 1992263221239868 1775227362 1775227362656000
78 555923966 buy 66840.0 0.00001481 1992263296352257 1992263294287876 1775227380 1775227380993000
79 555923967 buy 66840.0 0.00082214 1992263310286848 1992263294287876 1775227384 1775227384395000
80 555923974 buy 66828.0 0.00086049 1992263321845761 1992263320252428 1775227387 1775227387217000
81 555923980 buy 66827.0 0.00328919 1992263340531713 1992263328579584 1775227391 1775227391779000
82 555923992 buy 66822.0 0.00047598 1992263353720832 1992263342510085 1775227394 1775227394999000
83 555924016 buy 66821.0 0.00079 1992263395639298 1992263365795840 1775227405 1775227405233000
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+235942
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from estudio-mercados!")
if __name__ == "__main__":
main()
@@ -0,0 +1,598 @@
{
"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
}
@@ -0,0 +1,666 @@
{
"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": 1,
"metadata": {},
"outputs": [
{
"ename": "Exception",
"evalue": "File `'01_matching_engine_fifo.ipynb'` not found.",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mOSError\u001b[39m Traceback (most recent call last)",
"\u001b[31mOSError\u001b[39m: File `'01_matching_engine_fifo.ipynb'` not found.",
"\nThe above exception was the direct cause of the following exception:\n",
"\u001b[31mException\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Importar todo del notebook 01\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m get_ipython().run_line_magic(\u001b[33m'run'\u001b[39m, \u001b[33m'01_matching_engine_fifo.ipynb'\u001b[39m)\n",
"\u001b[31mException\u001b[39m: File `'01_matching_engine_fifo.ipynb'` not found."
]
}
],
"source": [
"# Importar todo del notebook 01\n",
"%run 01_matching_engine_fifo.ipynb"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Parámetros de simulación"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,592 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Datos reales de Binance\n",
"\n",
"Usamos la API pública de Binance (gratis, sin API key) para obtener:\n",
"1. **Order book** (L2) — profundidad del libro en tiempo real\n",
"2. **Trades recientes** — los últimos fills ejecutados\n",
"3. **OHLCV** — velas históricas\n",
"\n",
"Después aplicamos las mismas técnicas de estimación del notebook 03 sobre datos reales."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Exchange: Binance\n",
"Rate limit: 50ms\n"
]
}
],
"source": [
"import ccxt\n",
"import polars as pl\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from datetime import datetime, timedelta\n",
"import time\n",
"\n",
"exchange = ccxt.binance({'enableRateLimit': True})\n",
"print(f\"Exchange: {exchange.name}\")\n",
"print(f\"Rate limit: {exchange.rateLimit}ms\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Elegir par y explorar qué hay disponible"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"SYMBOL = 'BTC/USDT'\n",
"\n",
"ticker = exchange.fetch_ticker(SYMBOL)\n",
"print(f\"Par: {SYMBOL}\")\n",
"print(f\"Último precio: {ticker['last']}\")\n",
"print(f\"Bid: {ticker['bid']} Ask: {ticker['ask']}\")\n",
"print(f\"Spread: {ticker['ask'] - ticker['bid']:.2f} ({(ticker['ask'] - ticker['bid']) / ticker['last'] * 100:.4f}%)\")\n",
"print(f\"Volumen 24h: {ticker['baseVolume']:,.0f} BTC\")\n",
"print(f\"Volumen 24h: ${ticker['quoteVolume']:,.0f} USDT\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Order Book (L2)\n",
"\n",
"El order book de Binance te da los **niveles de precio agregados** — no ves órdenes individuales (no es L3).\n",
"Cada nivel muestra: precio y cantidad total a ese precio."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def fetch_orderbook(symbol: str, limit: int = 50) -> pl.DataFrame:\n",
" \"\"\"Obtiene el order book y lo devuelve como DataFrame.\"\"\"\n",
" ob = exchange.fetch_order_book(symbol, limit=limit)\n",
"\n",
" bids = pl.DataFrame(ob['bids'], schema=['price', 'qty'], orient='row')\n",
" bids = bids.with_columns(pl.lit('bid').alias('side'))\n",
"\n",
" asks = pl.DataFrame(ob['asks'], schema=['price', 'qty'], orient='row')\n",
" asks = asks.with_columns(pl.lit('ask').alias('side'))\n",
"\n",
" df = pl.concat([bids, asks])\n",
" return df, ob['timestamp']\n",
"\n",
"\n",
"ob_df, ob_ts = fetch_orderbook(SYMBOL, limit=20)\n",
"print(f\"Timestamp: {datetime.fromtimestamp(ob_ts/1000)}\")\n",
"print(f\"\\nTop 5 bids:\")\n",
"print(ob_df.filter(pl.col('side') == 'bid').head(5))\n",
"print(f\"\\nTop 5 asks:\")\n",
"print(ob_df.filter(pl.col('side') == 'ask').head(5))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_real_orderbook(ob_df: pl.DataFrame, symbol: str):\n",
" \"\"\"Visualiza el order book real.\"\"\"\n",
" bids = ob_df.filter(pl.col('side') == 'bid').sort('price', descending=True)\n",
" asks = ob_df.filter(pl.col('side') == 'ask').sort('price')\n",
"\n",
" bid_prices = bids['price'].to_numpy()\n",
" bid_cum = np.cumsum(bids['qty'].to_numpy())\n",
" ask_prices = asks['price'].to_numpy()\n",
" ask_cum = np.cumsum(asks['qty'].to_numpy())\n",
"\n",
" fig, ax = plt.subplots(figsize=(12, 5))\n",
" ax.fill_between(bid_prices, bid_cum, step='post', color='#2ecc71', alpha=0.5, label='Bids')\n",
" ax.fill_between(ask_prices, ask_cum, step='pre', color='#e74c3c', alpha=0.5, label='Asks')\n",
" ax.set_xlabel('Precio (USDT)')\n",
" ax.set_ylabel('Cantidad acumulada (BTC)')\n",
"\n",
" best_bid = bid_prices[0]\n",
" best_ask = ask_prices[0]\n",
" spread = best_ask - best_bid\n",
" mid = (best_bid + best_ask) / 2\n",
"\n",
" ax.axvline(x=mid, color='gray', linestyle='--', linewidth=0.8)\n",
" ax.set_title(f'{symbol} Order Book — Spread: ${spread:.2f} ({spread/mid*100:.4f}%) — Mid: ${mid:,.2f}')\n",
" ax.legend()\n",
" ax.grid(True, alpha=0.3)\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"plot_real_orderbook(ob_df, SYMBOL)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Trades recientes (fills)\n",
"\n",
"Esto es lo que ves en el tape público. Cada trade es un **fill** — no sabes si vienen de la misma orden."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def fetch_trades(symbol: str, limit: int = 1000) -> pl.DataFrame:\n",
" \"\"\"Obtiene trades recientes.\"\"\"\n",
" raw = exchange.fetch_trades(symbol, limit=limit)\n",
" records = [{\n",
" 'timestamp': t['timestamp'],\n",
" 'datetime': t['datetime'],\n",
" 'price': t['price'],\n",
" 'qty': t['amount'],\n",
" 'side': t['side'], # taker side\n",
" 'cost': t['cost'], # price * qty en quote currency\n",
" } for t in raw]\n",
" return pl.DataFrame(records)\n",
"\n",
"\n",
"trades = fetch_trades(SYMBOL, limit=1000)\n",
"print(f\"Trades obtenidos: {trades.shape[0]}\")\n",
"print(f\"Rango: {trades['datetime'].min()} → {trades['datetime'].max()}\")\n",
"print(f\"\\nÚltimos 5 trades:\")\n",
"print(trades.tail(5))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Estadísticas básicas de los trades\n",
"buys = trades.filter(pl.col('side') == 'buy')\n",
"sells = trades.filter(pl.col('side') == 'sell')\n",
"\n",
"print(f\"Buy trades: {buys.shape[0]} ({buys.shape[0]/trades.shape[0]*100:.1f}%)\")\n",
"print(f\"Sell trades: {sells.shape[0]} ({sells.shape[0]/trades.shape[0]*100:.1f}%)\")\n",
"print(f\"\\nTamaño promedio: {trades['qty'].mean():.6f} BTC\")\n",
"print(f\"Tamaño mediano: {trades['qty'].median():.6f} BTC\")\n",
"print(f\"Tamaño máximo: {trades['qty'].max():.6f} BTC\")\n",
"print(f\"\\nPrecio min: ${trades['price'].min():,.2f}\")\n",
"print(f\"Precio max: ${trades['price'].max():,.2f}\")\n",
"print(f\"Rango: ${trades['price'].max() - trades['price'].min():,.2f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Velas históricas (OHLCV)\n",
"\n",
"Las velas agregan trades en intervalos. Útiles para estimar σ en distintos timeframes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def fetch_ohlcv(symbol: str, timeframe: str = '1m', limit: int = 500) -> pl.DataFrame:\n",
" \"\"\"Obtiene velas OHLCV.\"\"\"\n",
" raw = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit)\n",
" df = pl.DataFrame(raw, schema=['timestamp', 'open', 'high', 'low', 'close', 'volume'], orient='row')\n",
" df = df.with_columns(\n",
" (pl.col('timestamp').cast(pl.Int64) * 1000).cast(pl.Datetime('us')).alias('datetime')\n",
" )\n",
" return df\n",
"\n",
"\n",
"# 1-minute candles, últimas 500\n",
"ohlcv_1m = fetch_ohlcv(SYMBOL, '1m', 500)\n",
"print(f\"Velas 1m: {ohlcv_1m.shape[0]}\")\n",
"print(f\"Rango: {ohlcv_1m['datetime'].min()} → {ohlcv_1m['datetime'].max()}\")\n",
"print(ohlcv_1m.tail(3))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_candles_and_volume(ohlcv: pl.DataFrame, symbol: str, timeframe: str):\n",
" \"\"\"Gráfico de velas con volumen.\"\"\"\n",
" fig, axes = plt.subplots(2, 1, figsize=(14, 7), gridspec_kw={'height_ratios': [3, 1]}, sharex=True)\n",
"\n",
" dt = ohlcv['datetime'].to_numpy()\n",
" opens = ohlcv['open'].to_numpy()\n",
" closes = ohlcv['close'].to_numpy()\n",
" highs = ohlcv['high'].to_numpy()\n",
" lows = ohlcv['low'].to_numpy()\n",
" volumes = ohlcv['volume'].to_numpy()\n",
"\n",
" colors = ['#2ecc71' if c >= o else '#e74c3c' for o, c in zip(opens, closes)]\n",
"\n",
" # Velas\n",
" ax = axes[0]\n",
" for i in range(len(dt)):\n",
" ax.plot([i, i], [lows[i], highs[i]], color=colors[i], linewidth=0.5)\n",
" ax.plot([i, i], [opens[i], closes[i]], color=colors[i], linewidth=2)\n",
" ax.set_ylabel('Precio (USDT)')\n",
" ax.set_title(f'{symbol} — {timeframe} candles')\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
" # Volumen\n",
" ax = axes[1]\n",
" ax.bar(range(len(dt)), volumes, color=colors, alpha=0.6, width=0.8)\n",
" ax.set_ylabel('Volumen (BTC)')\n",
" ax.set_xlabel('Vela')\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"# Últimas 100 velas para que se vea claro\n",
"plot_candles_and_volume(ohlcv_1m.tail(100), SYMBOL, '1m')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 5. Estimación de parámetros sobre datos reales\n",
"\n",
"Aplicamos las mismas técnicas del notebook 03 pero sobre BTC/USDT real."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.1 Volatilidad (σ) desde velas"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Retornos logarítmicos close-to-close\n",
"closes = ohlcv_1m['close'].to_numpy()\n",
"log_returns = np.diff(np.log(closes))\n",
"\n",
"sigma_1m = np.std(log_returns)\n",
"sigma_1h = sigma_1m * np.sqrt(60) # escalar a 1 hora\n",
"sigma_1d = sigma_1m * np.sqrt(60 * 24) # escalar a 1 día\n",
"sigma_annual = sigma_1d * np.sqrt(365) # anualizada\n",
"\n",
"print(f\"σ por minuto: {sigma_1m:.6f}\")\n",
"print(f\"σ por hora: {sigma_1h:.6f}\")\n",
"print(f\"σ por día: {sigma_1d:.4f} ({sigma_1d*100:.2f}%)\")\n",
"print(f\"σ anualizada: {sigma_annual:.4f} ({sigma_annual*100:.1f}%)\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.2 Arrival rate (λ) de trades"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Inter-arrival times entre trades consecutivos\n",
"timestamps = trades['timestamp'].to_numpy()\n",
"inter_arrivals_ms = np.diff(timestamps)\n",
"inter_arrivals_s = inter_arrivals_ms / 1000.0\n",
"\n",
"# Filtrar zeros (trades en el mismo milisegundo = probablemente mismo matching event)\n",
"inter_arrivals_s = inter_arrivals_s[inter_arrivals_s > 0]\n",
"\n",
"lambda_per_sec = 1.0 / np.mean(inter_arrivals_s)\n",
"lambda_per_min = lambda_per_sec * 60\n",
"\n",
"print(f\"Tiempo medio entre trades: {np.mean(inter_arrivals_s)*1000:.1f} ms\")\n",
"print(f\"Tiempo mediano entre trades: {np.median(inter_arrivals_s)*1000:.1f} ms\")\n",
"print(f\"λ (trades/segundo): {lambda_per_sec:.1f}\")\n",
"print(f\"λ (trades/minuto): {lambda_per_min:.0f}\")\n",
"print(f\"\\nRecuerda: esto son FILLS, no órdenes originales\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.3 Clustering (Hawkes) — ¿los trades generan más trades?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Agrupar trades por segundo y calcular autocorrelación\n",
"trades_per_sec = trades.with_columns(\n",
" (pl.col('timestamp') // 1000).alias('second')\n",
").group_by('second').agg(pl.len().alias('n_trades')).sort('second')\n",
"\n",
"arrivals = trades_per_sec['n_trades'].to_numpy()\n",
"\n",
"# Autocorrelación\n",
"max_lag = 30\n",
"mean_arr = np.mean(arrivals)\n",
"var_arr = np.var(arrivals)\n",
"acf = np.array([\n",
" np.mean((arrivals[lag:] - mean_arr) * (arrivals[:-lag] - mean_arr)) / var_arr\n",
" if lag > 0 else 1.0\n",
" for lag in range(max_lag)\n",
"])\n",
"\n",
"# Var/Mean ratio (dispersion index)\n",
"dispersion = var_arr / mean_arr\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
"\n",
"axes[0].bar(range(max_lag), acf, color='#e67e22', alpha=0.6)\n",
"axes[0].axhline(y=0, color='black', linewidth=0.5)\n",
"axes[0].axhline(y=1.96/np.sqrt(len(arrivals)), color='blue', linestyle='--', linewidth=0.8, label='95% CI')\n",
"axes[0].axhline(y=-1.96/np.sqrt(len(arrivals)), color='blue', linestyle='--', linewidth=0.8)\n",
"axes[0].set_title('Autocorrelación de trades/segundo')\n",
"axes[0].set_xlabel('Lag (segundos)')\n",
"axes[0].legend(fontsize=8)\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"axes[1].hist(arrivals, bins=50, color='#3498db', alpha=0.6, density=True)\n",
"axes[1].set_title(f'Distribución de trades/segundo\\nMedia={mean_arr:.1f}, Var/Mean={dispersion:.1f}')\n",
"axes[1].set_xlabel('Trades por segundo')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Var/Mean ratio: {dispersion:.2f}\")\n",
"if dispersion > 1.5:\n",
" print(\" → Hay clustering significativo (Hawkes). Los trades generan más trades.\")\n",
"else:\n",
" print(\" → Cercano a Poisson. Poco clustering.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.4 Distribución de tamaños — ¿hay ballenas?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sizes = trades['qty'].to_numpy()\n",
"sizes = sizes[sizes > 0]\n",
"\n",
"# Estimar exponente Pareto (MLE)\n",
"x_min = np.percentile(sizes, 90) # usar percentil 90 como x_min (zona de cola)\n",
"tail = sizes[sizes >= x_min]\n",
"alpha_est = len(tail) / np.sum(np.log(tail / x_min))\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
"\n",
"# Histograma\n",
"axes[0].hist(sizes, bins=100, color='#2ecc71', alpha=0.6, density=True)\n",
"axes[0].set_title('Distribución de tamaños de trades')\n",
"axes[0].set_xlabel('Tamaño (BTC)')\n",
"axes[0].set_yscale('log')\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# CCDF log-log (survival function)\n",
"sizes_sorted = np.sort(sizes)[::-1]\n",
"ranks = np.arange(1, len(sizes_sorted) + 1) / len(sizes_sorted)\n",
"axes[1].loglog(sizes_sorted, ranks, '.', markersize=1, alpha=0.4, color='#2ecc71')\n",
"# Fit Pareto\n",
"x_fit = np.logspace(np.log10(x_min), np.log10(sizes.max()), 50)\n",
"axes[1].loglog(x_fit, (x_fit / x_min) ** (-alpha_est) * (len(tail)/len(sizes)),\n",
" 'r-', linewidth=2, label=f'Pareto α={alpha_est:.2f}')\n",
"axes[1].set_title('CCDF (complementary CDF) — cola pesada')\n",
"axes[1].set_xlabel('Tamaño (BTC)')\n",
"axes[1].set_ylabel('P(X > x)')\n",
"axes[1].legend()\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Tamaño mediano: {np.median(sizes):.6f} BTC (${np.median(sizes) * ticker['last']:,.2f})\")\n",
"print(f\"Tamaño p99: {np.percentile(sizes, 99):.6f} BTC (${np.percentile(sizes, 99) * ticker['last']:,.2f})\")\n",
"print(f\"Tamaño max: {sizes.max():.6f} BTC (${sizes.max() * ticker['last']:,.2f})\")\n",
"print(f\"Pareto α (cola): {alpha_est:.2f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.5 Detección de jumps en retornos reales"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Retornos de 1 minuto\n",
"threshold = 3 * sigma_1m\n",
"jump_mask = np.abs(log_returns) > threshold\n",
"n_jumps = np.sum(jump_mask)\n",
"jump_intensity = n_jumps / len(log_returns)\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
"\n",
"# Retornos con jumps marcados\n",
"ax = axes[0]\n",
"ax.plot(log_returns, linewidth=0.5, color='#3498db', alpha=0.6)\n",
"jump_indices = np.where(jump_mask)[0]\n",
"ax.scatter(jump_indices, log_returns[jump_indices], color='red', s=20, zorder=5, label=f'Jumps ({n_jumps})')\n",
"ax.axhline(y=threshold, color='red', linestyle='--', linewidth=0.8, alpha=0.5)\n",
"ax.axhline(y=-threshold, color='red', linestyle='--', linewidth=0.8, alpha=0.5)\n",
"ax.set_title('Retornos 1m con jumps detectados (> 3σ)')\n",
"ax.set_ylabel('Log-return')\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# QQ plot\n",
"from scipy.stats import probplot\n",
"probplot(log_returns, dist=\"norm\", plot=axes[1])\n",
"axes[1].set_title('QQ-Plot: retornos vs Normal\\n(colas pesadas = desviación en extremos)')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Jumps detectados: {n_jumps} de {len(log_returns)} velas ({jump_intensity*100:.1f}%)\")\n",
"print(f\"Kurtosis: {float(np.mean((log_returns - np.mean(log_returns))**4) / np.std(log_returns)**4):.1f} (Normal=3, >3 = colas pesadas)\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 6. Resumen: perfil del mercado BTC/USDT"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"best_bid = ob_df.filter(pl.col('side') == 'bid')['price'].max()\n",
"best_ask = ob_df.filter(pl.col('side') == 'ask')['price'].min()\n",
"spread = best_ask - best_bid\n",
"\n",
"print(\"=\" * 60)\n",
"print(f\" PERFIL DE MERCADO: {SYMBOL}\")\n",
"print(f\" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n",
"print(\"=\" * 60)\n",
"print(f\"\")\n",
"print(f\" Precio: ${ticker['last']:,.2f}\")\n",
"print(f\" Spread: ${spread:.2f} ({spread/ticker['last']*100:.4f}%)\")\n",
"print(f\" Vol 24h: {ticker['baseVolume']:,.0f} BTC\")\n",
"print(f\"\")\n",
"print(f\" σ (1 min): {sigma_1m:.6f}\")\n",
"print(f\" σ (diaria): {sigma_1d:.4f} ({sigma_1d*100:.2f}%)\")\n",
"print(f\" σ (anual): {sigma_annual:.2f} ({sigma_annual*100:.0f}%)\")\n",
"print(f\"\")\n",
"print(f\" λ (fills/seg): {lambda_per_sec:.1f}\")\n",
"print(f\" Clustering: Var/Mean = {dispersion:.1f} {'(Hawkes)' if dispersion > 1.5 else '(~Poisson)'}\")\n",
"print(f\"\")\n",
"print(f\" Tamaño mediano: {np.median(sizes):.6f} BTC\")\n",
"print(f\" Pareto α: {alpha_est:.2f}\")\n",
"print(f\" Kurtosis: {float(np.mean((log_returns - np.mean(log_returns))**4) / np.std(log_returns)**4):.1f}\")\n",
"print(f\" Jumps (>3σ): {jump_intensity*100:.1f}%\")\n",
"print(f\"\")\n",
"print(\"=\" * 60)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. Guardar datos para análisis offline"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Guardar todo en data/\n",
"trades.write_csv('../data/binance_btcusdt_trades.csv')\n",
"ohlcv_1m.write_csv('../data/binance_btcusdt_ohlcv_1m.csv')\n",
"ob_df.write_csv('../data/binance_btcusdt_orderbook.csv')\n",
"\n",
"print(f\"Guardados en data/:\")\n",
"print(f\" binance_btcusdt_trades.csv ({trades.shape[0]} trades)\")\n",
"print(f\" binance_btcusdt_ohlcv_1m.csv ({ohlcv_1m.shape[0]} velas)\")\n",
"print(f\" binance_btcusdt_orderbook.csv ({ob_df.shape[0]} niveles)\")"
]
}
],
"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
}
@@ -0,0 +1,387 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Recolección de datos: Binance + Bitstamp L3\n",
"\n",
"**Objetivo:** Dataset de 1M+ filas guardado en `data/`\n",
"\n",
"| Fuente | Tipo | Método | Qué obtenemos |\n",
"|---|---|---|---|\n",
"| Binance | aggTrades (fills agrupados por taker) | REST paginado | 1M+ trades históricos |\n",
"| Binance | Order book L2 | REST snapshots | Profundidad del libro |\n",
"| Bitstamp | L3 live_orders | WebSocket | Cada orden individual con ID |"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Data dir: /home/lucas/fn_registry/analysis/estudio_mercados/data\n"
]
}
],
"source": [
"import aiohttp\n",
"import asyncio\n",
"import websockets\n",
"import json\n",
"import time\n",
"import polars as pl\n",
"import numpy as np\n",
"from datetime import datetime, timedelta\n",
"from pathlib import Path\n",
"\n",
"DATA_DIR = Path('../data')\n",
"DATA_DIR.mkdir(exist_ok=True)\n",
"\n",
"BINANCE_BASE = 'https://api.binance.com'\n",
"BITSTAMP_WS = 'wss://ws.bitstamp.net'\n",
"\n",
"print(f'Data dir: {DATA_DIR.resolve()}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 1. Binance aggTrades — 1M+ filas\n",
"\n",
"Los `aggTrades` agrupan fills de la misma taker order:\n",
"- Cada fila = 1 taker order (o parte si cruzó muchos niveles)\n",
"- Campo `a` = aggregate trade ID\n",
"- Campo `m` = true si el maker es buyer (taker es seller)\n",
"- Paginamos con `fromId` para ir hacia atrás en el tiempo"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def fetch_binance_agg_trades(\n",
" symbol: str = 'BTCUSDT',\n",
" target_rows: int = 1_000_000,\n",
" batch_size: int = 1000,\n",
") -> pl.DataFrame:\n",
" \"\"\"Descarga aggTrades de Binance paginando hacia atrás.\n",
"\n",
" Cada aggTrade agrupa fills de la misma taker order:\n",
" - a: aggregate trade id\n",
" - p: price\n",
" - q: quantity\n",
" - f: first trade id\n",
" - l: last trade id\n",
" - T: timestamp\n",
" - m: was the buyer the maker? (true = taker sold, false = taker bought)\n",
" \"\"\"\n",
" all_records = []\n",
" from_id = None\n",
" total = 0\n",
" start_time = time.time()\n",
"\n",
" async with aiohttp.ClientSession() as session:\n",
" while total < target_rows:\n",
" params = {'symbol': symbol, 'limit': batch_size}\n",
" if from_id is not None:\n",
" params['fromId'] = from_id\n",
"\n",
" async with session.get(f'{BINANCE_BASE}/api/v3/aggTrades', params=params) as resp:\n",
" if resp.status != 200:\n",
" text = await resp.text()\n",
" print(f'Error {resp.status}: {text}')\n",
" break\n",
" data = await resp.json()\n",
"\n",
" if not data:\n",
" break\n",
"\n",
" for row in data:\n",
" all_records.append({\n",
" 'agg_trade_id': row['a'],\n",
" 'price': float(row['p']),\n",
" 'qty': float(row['q']),\n",
" 'first_trade_id': row['f'],\n",
" 'last_trade_id': row['l'],\n",
" 'timestamp': row['T'],\n",
" 'is_buyer_maker': row['m'], # True = taker vendió\n",
" 'side': 'sell' if row['m'] else 'buy', # taker side\n",
" 'n_fills': row['l'] - row['f'] + 1, # fills en esta agg\n",
" })\n",
"\n",
" # Avanzar: siguiente página desde el último ID + 1\n",
" from_id = data[-1]['a'] + 1\n",
" total += len(data)\n",
"\n",
" if total % 50_000 == 0:\n",
" elapsed = time.time() - start_time\n",
" rate = total / elapsed\n",
" eta = (target_rows - total) / rate if rate > 0 else 0\n",
" ts = datetime.fromtimestamp(data[-1]['T'] / 1000)\n",
" print(f' {total:>8,} rows | {rate:,.0f} rows/s | ETA {eta:.0f}s | hasta {ts}')\n",
"\n",
" # Rate limit: Binance permite 1200 req/min en aggTrades\n",
" await asyncio.sleep(0.05)\n",
"\n",
" elapsed = time.time() - start_time\n",
" print(f'\\nDescargados {total:,} aggTrades en {elapsed:.1f}s ({total/elapsed:,.0f} rows/s)')\n",
"\n",
" return pl.DataFrame(all_records)\n",
"\n",
"\n",
"print('fetch_binance_agg_trades() definida')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Descargar 1M+ aggTrades de BTC/USDT\n",
"binance_trades = await fetch_binance_agg_trades('BTCUSDT', target_rows=1_000_000)\n",
"\n",
"print(f'\\nShape: {binance_trades.shape}')\n",
"print(f'Columnas: {binance_trades.columns}')\n",
"print(binance_trades.head(5))\n",
"print(f'\\nRango temporal:')\n",
"t_min = datetime.fromtimestamp(binance_trades['timestamp'].min() / 1000)\n",
"t_max = datetime.fromtimestamp(binance_trades['timestamp'].max() / 1000)\n",
"print(f' {t_min} → {t_max}')\n",
"print(f' Duración: {t_max - t_min}')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Guardar Binance aggTrades\n",
"out_path = DATA_DIR / 'binance_btcusdt_aggtrades.csv'\n",
"binance_trades.write_csv(str(out_path))\n",
"size_mb = out_path.stat().st_size / 1024 / 1024\n",
"print(f'Guardado: {out_path}')\n",
"print(f' {binance_trades.shape[0]:,} filas, {size_mb:.1f} MB')\n",
"\n",
"# Estadísticas rápidas\n",
"print(f'\\nEstadísticas:')\n",
"print(f' Buys (taker): {binance_trades.filter(pl.col(\"side\") == \"buy\").shape[0]:,}')\n",
"print(f' Sells (taker): {binance_trades.filter(pl.col(\"side\") == \"sell\").shape[0]:,}')\n",
"print(f' Precio min: ${binance_trades[\"price\"].min():,.2f}')\n",
"print(f' Precio max: ${binance_trades[\"price\"].max():,.2f}')\n",
"print(f' Qty mediana: {binance_trades[\"qty\"].median():.6f} BTC')\n",
"print(f' Qty max: {binance_trades[\"qty\"].max():.4f} BTC')\n",
"print(f' Fills/aggTrade mediana: {binance_trades[\"n_fills\"].median():.0f}')\n",
"print(f' Fills/aggTrade max: {binance_trades[\"n_fills\"].max()}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 2. Bitstamp L3 — órdenes individuales via WebSocket\n",
"\n",
"Cada mensaje tiene:\n",
"- `id`: ID único de la orden\n",
"- `order_type`: 0 = buy, 1 = sell\n",
"- `price`, `amount`\n",
"- `datetime`, `microtimestamp`\n",
"\n",
"Los canales:\n",
"- `live_orders_btcusd`: cada orden creada\n",
"- `live_trades_btcusd`: cada ejecución con IDs de maker y taker"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def record_bitstamp_l3(\n",
" pair: str = 'btcusd',\n",
" duration_seconds: int = 300,\n",
") -> tuple[pl.DataFrame, pl.DataFrame]:\n",
" \"\"\"Graba datos L3 de Bitstamp via WebSocket.\n",
"\n",
" Retorna (orders_df, trades_df) con todas las órdenes y trades capturados.\n",
" \"\"\"\n",
" orders = []\n",
" trades = []\n",
" start = time.time()\n",
" msg_count = 0\n",
"\n",
" async with websockets.connect(BITSTAMP_WS) as ws:\n",
" # Suscribirse a órdenes individuales + trades\n",
" for channel in [f'live_orders_{pair}', f'live_trades_{pair}']:\n",
" await ws.send(json.dumps({\n",
" 'event': 'bts:subscribe',\n",
" 'data': {'channel': channel}\n",
" }))\n",
"\n",
" print(f'Grabando Bitstamp L3 ({pair}) por {duration_seconds}s...')\n",
"\n",
" while time.time() - start < duration_seconds:\n",
" try:\n",
" raw = await asyncio.wait_for(ws.recv(), timeout=5.0)\n",
" msg = json.loads(raw)\n",
" msg_count += 1\n",
"\n",
" event = msg.get('event', '')\n",
" channel = msg.get('channel', '')\n",
" data = msg.get('data', {})\n",
"\n",
" if isinstance(data, str):\n",
" try:\n",
" data = json.loads(data)\n",
" except:\n",
" continue\n",
"\n",
" # Órdenes (L3)\n",
" if 'live_orders' in channel and event in ('order_created', 'order_changed', 'order_deleted'):\n",
" orders.append({\n",
" 'event': event,\n",
" 'order_id': data.get('id', ''),\n",
" 'side': 'buy' if data.get('order_type') == 0 else 'sell',\n",
" 'price': float(data.get('price', 0)),\n",
" 'amount': float(data.get('amount', 0)),\n",
" 'datetime': data.get('datetime', ''),\n",
" 'microtimestamp': data.get('microtimestamp', ''),\n",
" })\n",
"\n",
" # Trades\n",
" elif 'live_trades' in channel and event == 'trade':\n",
" trades.append({\n",
" 'trade_id': data.get('id', ''),\n",
" 'side': 'buy' if data.get('type') == 0 else 'sell',\n",
" 'price': float(data.get('price', 0)),\n",
" 'amount': float(data.get('amount', 0)),\n",
" 'buy_order_id': data.get('buy_order_id', ''),\n",
" 'sell_order_id': data.get('sell_order_id', ''),\n",
" 'timestamp': data.get('timestamp', ''),\n",
" 'microtimestamp': data.get('microtimestamp', ''),\n",
" })\n",
"\n",
" if msg_count % 5000 == 0:\n",
" elapsed = time.time() - start\n",
" print(f' {elapsed:.0f}s: {len(orders):,} orders, {len(trades):,} trades ({msg_count:,} msgs)')\n",
"\n",
" except asyncio.TimeoutError:\n",
" continue\n",
"\n",
" elapsed = time.time() - start\n",
" print(f'\\nGrabación terminada: {elapsed:.0f}s')\n",
" print(f' Órdenes L3: {len(orders):,}')\n",
" print(f' Trades: {len(trades):,}')\n",
" print(f' Msgs total: {msg_count:,}')\n",
"\n",
" orders_df = pl.DataFrame(orders) if orders else pl.DataFrame()\n",
" trades_df = pl.DataFrame(trades) if trades else pl.DataFrame()\n",
"\n",
" return orders_df, trades_df\n",
"\n",
"\n",
"print('record_bitstamp_l3() definida')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Grabar 5 minutos de L3 de Bitstamp\n",
"bs_orders, bs_trades = await record_bitstamp_l3('btcusd', duration_seconds=300)\n",
"\n",
"if bs_orders.shape[0] > 0:\n",
" print(f'\\n=== Órdenes L3 ===')\n",
" print(f'Shape: {bs_orders.shape}')\n",
" print(bs_orders.head(5))\n",
" print(f'\\nEventos:')\n",
" print(bs_orders.group_by('event').agg(pl.len().alias('count')).sort('count', descending=True))\n",
"\n",
"if bs_trades.shape[0] > 0:\n",
" print(f'\\n=== Trades ===')\n",
" print(f'Shape: {bs_trades.shape}')\n",
" print(bs_trades.head(5))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Guardar Bitstamp L3\n",
"if bs_orders.shape[0] > 0:\n",
" path = DATA_DIR / 'bitstamp_btcusd_l3_orders.csv'\n",
" bs_orders.write_csv(str(path))\n",
" print(f'Guardado: {path} ({bs_orders.shape[0]:,} filas, {path.stat().st_size/1024/1024:.1f} MB)')\n",
"\n",
"if bs_trades.shape[0] > 0:\n",
" path = DATA_DIR / 'bitstamp_btcusd_l3_trades.csv'\n",
" bs_trades.write_csv(str(path))\n",
" print(f'Guardado: {path} ({bs_trades.shape[0]:,} filas, {path.stat().st_size/1024/1024:.1f} MB)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 3. Resumen del dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"print('=' * 70)\n",
"print(' DATASET RECOLECTADO')\n",
"print('=' * 70)\n",
"\n",
"total_rows = 0\n",
"for f in sorted(DATA_DIR.glob('*.csv')):\n",
" size_mb = f.stat().st_size / 1024 / 1024\n",
" # Contar filas rápido\n",
" try:\n",
" nrows = pl.scan_csv(str(f)).select(pl.len()).collect().item()\n",
" except:\n",
" nrows = '?'\n",
" total_rows += nrows if isinstance(nrows, int) else 0\n",
" print(f' {f.name:<45} {nrows:>10,} filas {size_mb:>7.1f} MB')\n",
"\n",
"print(f'\\n TOTAL: {total_rows:>10,} filas')\n",
"print('=' * 70)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,573 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Análisis del dataset real: 1M aggTrades + Bitstamp L3\n",
"\n",
"Tenemos:\n",
"- **Binance**: 1M aggTrades de BTC/USDT (~26h de mercado)\n",
"- **Bitstamp**: L3 orders + trades (5 min de captura)\n",
"\n",
"## Objetivos\n",
"1. Estimar parámetros de microestructura sobre datos reales\n",
"2. Ver cómo cambian con ventanas deslizantes\n",
"3. Comparar Binance (aggTrades = órdenes agrupadas) vs Bitstamp (L3 = cada orden)\n",
"4. Calibrar nuestra simulación para que genere datos similares"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Binance aggTrades: 1,000,000 filas\n",
"Columnas: ['agg_trade_id', 'price', 'qty', 'first_trade_id', 'last_trade_id', 'timestamp', 'is_buyer_maker', 'side', 'n_fills']\n",
"Rango: 2026-04-02 14:26:02.324000 → 2026-04-03 16:32:41.139000 (1 day, 2:06:38.815000)\n",
"\n",
"Bitstamp L3 aún no disponible (grabando...)\n"
]
}
],
"source": [
"import polars as pl\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import curve_fit\n",
"from scipy.stats import probplot\n",
"from datetime import datetime\n",
"from pathlib import Path\n",
"\n",
"DATA = Path('../data')\n",
"\n",
"# Cargar Binance aggTrades\n",
"trades = pl.read_csv(str(DATA / 'binance_btcusdt_aggtrades_1M.csv'))\n",
"print(f'Binance aggTrades: {trades.shape[0]:,} filas')\n",
"print(f'Columnas: {trades.columns}')\n",
"\n",
"t_min = datetime.fromtimestamp(trades['timestamp'].min() / 1000)\n",
"t_max = datetime.fromtimestamp(trades['timestamp'].max() / 1000)\n",
"print(f'Rango: {t_min} → {t_max} ({t_max - t_min})')\n",
"\n",
"# Intentar cargar Bitstamp si existe\n",
"bs_path = DATA / 'bitstamp_btcusd_l3_orders.csv'\n",
"if bs_path.exists():\n",
" bs_orders = pl.read_csv(str(bs_path))\n",
" print(f'\\nBitstamp L3 orders: {bs_orders.shape[0]:,} filas')\n",
"else:\n",
" bs_orders = None\n",
" print('\\nBitstamp L3 aún no disponible (grabando...)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Visión general del dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Añadir columna datetime y agrupar por minuto\n",
"trades_dt = trades.with_columns(\n",
" (pl.col('timestamp') * 1000).cast(pl.Datetime('us')).alias('datetime'),\n",
" (pl.col('timestamp') // 60000).alias('minute'),\n",
")\n",
"\n",
"# Por minuto\n",
"per_min = trades_dt.group_by('minute').agg(\n",
" pl.len().alias('n_trades'),\n",
" pl.col('price').last().alias('close'),\n",
" pl.col('price').min().alias('low'),\n",
" pl.col('price').max().alias('high'),\n",
" pl.col('qty').sum().alias('volume'),\n",
" (pl.col('qty') * pl.col('price')).sum().alias('turnover'),\n",
" pl.col('timestamp').min().alias('ts'),\n",
").sort('minute')\n",
"\n",
"# Log returns\n",
"per_min = per_min.with_columns(\n",
" (pl.col('close').log() - pl.col('close').shift(1).log()).alias('log_return')\n",
")\n",
"\n",
"print(f'Minutos: {per_min.shape[0]}')\n",
"print(f'Trades/minuto: media={per_min[\"n_trades\"].mean():.0f}, mediana={per_min[\"n_trades\"].median():.0f}')\n",
"print(f'Volumen/minuto: media={per_min[\"volume\"].mean():.2f} BTC')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Overview: precio, volumen, trades/min\n",
"fig, axes = plt.subplots(3, 1, figsize=(16, 10), gridspec_kw={'height_ratios': [3, 1, 1]}, sharex=True)\n",
"\n",
"minutes = np.arange(per_min.shape[0])\n",
"\n",
"ax = axes[0]\n",
"ax.plot(minutes, per_min['close'].to_numpy(), linewidth=0.5, color='#3498db')\n",
"ax.set_ylabel('Precio (USDT)')\n",
"ax.set_title(f'BTC/USDT — 1M aggTrades ({t_min.strftime(\"%Y-%m-%d %H:%M\")} → {t_max.strftime(\"%Y-%m-%d %H:%M\")})')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"ax = axes[1]\n",
"ax.bar(minutes, per_min['volume'].to_numpy(), width=1.0, color='#e67e22', alpha=0.6)\n",
"ax.set_ylabel('Volumen (BTC)')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"ax = axes[2]\n",
"ax.bar(minutes, per_min['n_trades'].to_numpy(), width=1.0, color='#9b59b6', alpha=0.6)\n",
"ax.set_ylabel('Trades/min')\n",
"ax.set_xlabel('Minuto')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Estimación de parámetros\n",
"\n",
"### 2.1 Volatilidad (σ)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"returns = per_min.drop_nulls('log_return')['log_return'].to_numpy()\n",
"\n",
"sigma_1m = np.std(returns)\n",
"sigma_1h = sigma_1m * np.sqrt(60)\n",
"sigma_1d = sigma_1m * np.sqrt(60 * 24)\n",
"sigma_ann = sigma_1d * np.sqrt(365)\n",
"\n",
"print(f'σ por minuto: {sigma_1m:.6f}')\n",
"print(f'σ por hora: {sigma_1h:.5f}')\n",
"print(f'σ diaria: {sigma_1d:.4f} ({sigma_1d*100:.2f}%)')\n",
"print(f'σ anualizada: {sigma_ann:.2f} ({sigma_ann*100:.0f}%)')\n",
"\n",
"# Rolling sigma (ventana de 60 minutos)\n",
"window = 60\n",
"rolling_sigma = np.array([np.std(returns[max(0,i-window):i]) for i in range(window, len(returns))])\n",
"\n",
"fig, axes = plt.subplots(2, 2, figsize=(14, 8))\n",
"\n",
"# Histograma de retornos\n",
"ax = axes[0][0]\n",
"ax.hist(returns, bins=100, density=True, color='#3498db', alpha=0.6)\n",
"x = np.linspace(returns.min(), returns.max(), 200)\n",
"from scipy.stats import norm\n",
"ax.plot(x, norm.pdf(x, 0, sigma_1m), 'r-', linewidth=1.5, label=f'Normal σ={sigma_1m:.5f}')\n",
"ax.set_title('Distribución de retornos 1m')\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# QQ plot\n",
"probplot(returns, dist='norm', plot=axes[0][1])\n",
"axes[0][1].set_title('QQ-Plot vs Normal')\n",
"axes[0][1].grid(True, alpha=0.3)\n",
"\n",
"# Rolling sigma\n",
"ax = axes[1][0]\n",
"ax.fill_between(range(len(rolling_sigma)), rolling_sigma, color='#e74c3c', alpha=0.5)\n",
"ax.axhline(y=sigma_1m, color='black', linestyle='--', linewidth=0.8, label=f'σ global={sigma_1m:.5f}')\n",
"ax.set_title(f'σ rolling (ventana {window}m)')\n",
"ax.set_ylabel('σ por minuto')\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Retornos absolutos (clustering de volatilidad)\n",
"ax = axes[1][1]\n",
"ax.plot(np.abs(returns), linewidth=0.3, color='#e74c3c', alpha=0.6)\n",
"ax.set_title('|Retornos| — clustering de volatilidad')\n",
"ax.set_ylabel('|log-return|')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"kurtosis = float(np.mean((returns - np.mean(returns))**4) / sigma_1m**4)\n",
"skew = float(np.mean((returns - np.mean(returns))**3) / sigma_1m**3)\n",
"print(f'\\nKurtosis: {kurtosis:.1f} (Normal=3)')\n",
"print(f'Skewness: {skew:.3f} (Normal=0)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.2 Arrival rate (λ) y Hawkes clustering"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Trades por segundo\n",
"trades_per_sec = trades.with_columns(\n",
" (pl.col('timestamp') // 1000).alias('second')\n",
").group_by('second').agg(\n",
" pl.len().alias('n_trades'),\n",
" pl.col('qty').sum().alias('volume'),\n",
").sort('second')\n",
"\n",
"arrivals = trades_per_sec['n_trades'].to_numpy()\n",
"\n",
"lambda_mean = np.mean(arrivals)\n",
"var_mean = np.var(arrivals) / np.mean(arrivals)\n",
"\n",
"print(f'Trades/segundo: media={lambda_mean:.1f}, mediana={np.median(arrivals):.0f}')\n",
"print(f'Var/Mean ratio: {var_mean:.1f} (=1 si Poisson, >1 = clustering)')\n",
"\n",
"# Autocorrelación\n",
"max_lag = 60\n",
"mean_a = np.mean(arrivals)\n",
"var_a = np.var(arrivals)\n",
"acf = np.array([\n",
" np.mean((arrivals[lag:] - mean_a) * (arrivals[:-lag] - mean_a)) / var_a\n",
" if lag > 0 else 1.0\n",
" for lag in range(max_lag)\n",
"])\n",
"\n",
"# Ajustar exponencial para estimar Hawkes\n",
"lags = np.arange(1, max_lag)\n",
"acf_vals = acf[1:]\n",
"positive_mask = acf_vals > 0\n",
"if np.sum(positive_mask) > 5:\n",
" try:\n",
" exp_fn = lambda x, a, b: a * np.exp(-b * x)\n",
" popt, _ = curve_fit(exp_fn, lags[positive_mask], acf_vals[positive_mask], p0=[0.3, 0.1], maxfev=5000)\n",
" hawkes_a, hawkes_b = abs(popt[0]), abs(popt[1])\n",
" branching = hawkes_a / hawkes_b\n",
" except:\n",
" hawkes_a, hawkes_b, branching = 0, 1, 0\n",
"else:\n",
" hawkes_a, hawkes_b, branching = 0, 1, 0\n",
"\n",
"print(f'\\nHawkes (ajuste exp a ACF):')\n",
"print(f' α ≈ {hawkes_a:.4f}')\n",
"print(f' β ≈ {hawkes_b:.4f}')\n",
"print(f' Branching ratio η = α/β = {branching:.3f} (< 1 = estacionario)')\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(16, 4))\n",
"\n",
"# ACF\n",
"ax = axes[0]\n",
"ax.bar(range(max_lag), acf, color='#e67e22', alpha=0.6)\n",
"if hawkes_a > 0:\n",
" ax.plot(lags, exp_fn(lags, hawkes_a, hawkes_b), 'r-', linewidth=2, label=f'Exp fit: α={hawkes_a:.3f}, β={hawkes_b:.3f}')\n",
"ax.axhline(y=0, color='black', linewidth=0.5)\n",
"ci = 1.96 / np.sqrt(len(arrivals))\n",
"ax.axhline(y=ci, color='blue', linestyle='--', linewidth=0.8, alpha=0.5)\n",
"ax.axhline(y=-ci, color='blue', linestyle='--', linewidth=0.8, alpha=0.5)\n",
"ax.set_title('ACF trades/segundo')\n",
"ax.set_xlabel('Lag (s)')\n",
"ax.legend(fontsize=7)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Distribución de arrivals\n",
"ax = axes[1]\n",
"ax.hist(arrivals, bins=50, density=True, color='#3498db', alpha=0.6)\n",
"ax.set_title(f'Trades/segundo (media={lambda_mean:.1f}, V/M={var_mean:.1f})')\n",
"ax.set_xlabel('Trades/s')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Rolling lambda\n",
"w = 300 # ventana 5 min\n",
"rolling_lambda = np.convolve(arrivals, np.ones(w)/w, mode='valid')\n",
"ax = axes[2]\n",
"ax.plot(rolling_lambda, linewidth=0.5, color='#9b59b6')\n",
"ax.axhline(y=lambda_mean, color='black', linestyle='--', linewidth=0.8)\n",
"ax.set_title(f'λ rolling (ventana {w}s = 5min)')\n",
"ax.set_ylabel('Trades/s')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.3 Distribución de tamaños (Pareto)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sizes = trades['qty'].to_numpy()\n",
"sizes = sizes[sizes > 0]\n",
"costs = (trades['qty'] * trades['price']).to_numpy()\n",
"costs = costs[costs > 0]\n",
"\n",
"# Pareto MLE sobre la cola (p90+)\n",
"x_min_qty = np.percentile(sizes, 90)\n",
"tail_qty = sizes[sizes >= x_min_qty]\n",
"alpha_qty = len(tail_qty) / np.sum(np.log(tail_qty / x_min_qty))\n",
"\n",
"x_min_cost = np.percentile(costs, 90)\n",
"tail_cost = costs[costs >= x_min_cost]\n",
"alpha_cost = len(tail_cost) / np.sum(np.log(tail_cost / x_min_cost))\n",
"\n",
"print(f'Tamaños (BTC):')\n",
"print(f' Mediana: {np.median(sizes):.6f} BTC')\n",
"print(f' p99: {np.percentile(sizes, 99):.4f} BTC')\n",
"print(f' Max: {sizes.max():.2f} BTC')\n",
"print(f' Pareto α (cola p90+): {alpha_qty:.2f}')\n",
"\n",
"print(f'\\nTurnover (USDT):')\n",
"print(f' Mediana: ${np.median(costs):,.0f}')\n",
"print(f' p99: ${np.percentile(costs, 99):,.0f}')\n",
"print(f' Max: ${costs.max():,.0f}')\n",
"print(f' Pareto α (cola p90+): {alpha_cost:.2f}')\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
"\n",
"# CCDF log-log de tamaños\n",
"for ax, data, alpha, label, xmin in [\n",
" (axes[0], sizes, alpha_qty, 'BTC', x_min_qty),\n",
" (axes[1], costs, alpha_cost, 'USDT', x_min_cost),\n",
"]:\n",
" sorted_d = np.sort(data)[::-1]\n",
" ranks = np.arange(1, len(sorted_d) + 1) / len(sorted_d)\n",
" ax.loglog(sorted_d, ranks, '.', markersize=0.5, alpha=0.3, color='#2ecc71')\n",
" x_fit = np.logspace(np.log10(xmin), np.log10(data.max()), 50)\n",
" ax.loglog(x_fit, (x_fit/xmin)**(-alpha) * (len(data[data>=xmin])/len(data)),\n",
" 'r-', linewidth=2, label=f'Pareto α={alpha:.2f}')\n",
" ax.set_title(f'CCDF tamaños ({label})')\n",
" ax.set_xlabel(label)\n",
" ax.set_ylabel('P(X > x)')\n",
" ax.legend()\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.4 Jumps y colas pesadas"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Detectar jumps (retornos > 3σ)\n",
"threshold = 3 * sigma_1m\n",
"jump_mask = np.abs(returns) > threshold\n",
"n_jumps = np.sum(jump_mask)\n",
"jump_intensity = n_jumps / len(returns)\n",
"jump_sizes = np.abs(returns[jump_mask])\n",
"jump_size_std = np.std(jump_sizes) if len(jump_sizes) > 1 else 0\n",
"\n",
"print(f'Jumps detectados (>3σ): {n_jumps} de {len(returns)} ({jump_intensity*100:.1f}%)')\n",
"print(f'Jump size std: {jump_size_std:.6f}')\n",
"print(f'Kurtosis: {kurtosis:.1f} (Normal=3, >3 = colas pesadas)')\n",
"\n",
"# Retornos con jumps marcados\n",
"fig, ax = plt.subplots(figsize=(16, 4))\n",
"ax.plot(returns, linewidth=0.3, color='#3498db', alpha=0.6)\n",
"idx = np.where(jump_mask)[0]\n",
"ax.scatter(idx, returns[idx], color='red', s=10, zorder=5, label=f'Jumps ({n_jumps})')\n",
"ax.axhline(y=threshold, color='red', linestyle='--', linewidth=0.5, alpha=0.5)\n",
"ax.axhline(y=-threshold, color='red', linestyle='--', linewidth=0.5, alpha=0.5)\n",
"ax.set_title('Retornos 1m — jumps marcados en rojo')\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2.5 Fills por aggTrade — estructura de las órdenes"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# n_fills nos dice cuántos niveles del book barrió cada taker order\n",
"fills = trades['n_fills'].to_numpy()\n",
"\n",
"print(f'Fills por aggTrade:')\n",
"print(f' 1 fill (no cruzó niveles): {np.sum(fills == 1):,} ({np.mean(fills == 1)*100:.1f}%)')\n",
"print(f' 2-5 fills: {np.sum((fills >= 2) & (fills <= 5)):,} ({np.mean((fills >= 2) & (fills <= 5))*100:.1f}%)')\n",
"print(f' 6-20 fills: {np.sum((fills >= 6) & (fills <= 20)):,} ({np.mean((fills >= 6) & (fills <= 20))*100:.1f}%)')\n",
"print(f' >20 fills (ballenas): {np.sum(fills > 20):,} ({np.mean(fills > 20)*100:.1f}%)')\n",
"print(f' Max fills: {fills.max()}')\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
"\n",
"ax = axes[0]\n",
"ax.hist(fills[fills <= 20], bins=range(1, 22), color='#3498db', alpha=0.6, edgecolor='white')\n",
"ax.set_title('Fills por aggTrade (≤20)')\n",
"ax.set_xlabel('Número de fills')\n",
"ax.set_ylabel('Frecuencia')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Qty vs n_fills — las ballenas barren más niveles\n",
"ax = axes[1]\n",
"sample = trades.sample(min(50000, trades.shape[0]), seed=42)\n",
"ax.scatter(sample['n_fills'].to_numpy(), sample['qty'].to_numpy(), s=0.5, alpha=0.2, color='#e67e22')\n",
"ax.set_xlabel('Fills por aggTrade')\n",
"ax.set_ylabel('Qty (BTC)')\n",
"ax.set_title('Tamaño de orden vs fills (más grande = barre más niveles)')\n",
"ax.set_yscale('log')\n",
"ax.set_xscale('log')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 3. Bitstamp L3: comparar con Binance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Cargar Bitstamp si ya existe\n",
"bs_orders_path = DATA / 'bitstamp_btcusd_l3_orders.csv'\n",
"bs_trades_path = DATA / 'bitstamp_btcusd_l3_trades.csv'\n",
"\n",
"if bs_orders_path.exists():\n",
" bs_orders = pl.read_csv(str(bs_orders_path))\n",
" print(f'Bitstamp L3 orders: {bs_orders.shape[0]:,}')\n",
" print(bs_orders.group_by('event').agg(pl.len().alias('count')).sort('count', descending=True))\n",
" print()\n",
" \n",
" # Ratio create/delete — vida media de las órdenes\n",
" creates = bs_orders.filter(pl.col('event') == 'order_created').shape[0]\n",
" deletes = bs_orders.filter(pl.col('event') == 'order_deleted').shape[0]\n",
" changes = bs_orders.filter(pl.col('event') == 'order_changed').shape[0]\n",
" print(f'Creadas: {creates:,} Borradas: {deletes:,} Cambiadas: {changes:,}')\n",
" print(f'Ratio delete/create: {deletes/creates:.2f} (cercano a 1 = la mayoría se cancela sin ejecutar)')\n",
" \n",
" # Cuántas se cancelan vs se ejecutan\n",
" print(f'\\nEsto revela algo fundamental: la mayoría de órdenes se CANCELAN, no se ejecutan.')\n",
" print(f'Los makers constantemente ponen y quitan órdenes para ajustar sus quotes.')\n",
"\n",
"if bs_trades_path.exists():\n",
" bs_trades = pl.read_csv(str(bs_trades_path))\n",
" print(f'\\nBitstamp L3 trades: {bs_trades.shape[0]:,}')\n",
" print(bs_trades.head(3))\n",
" \n",
" # En L3 podemos ver maker y taker order IDs\n",
" print(f'\\nCon L3 vemos los IDs del buyer y seller de cada trade:')\n",
" print(f' Unique buy_order_ids: {bs_trades[\"buy_order_id\"].n_unique():,}')\n",
" print(f' Unique sell_order_ids: {bs_trades[\"sell_order_id\"].n_unique():,}')\n",
"\n",
"if not bs_orders_path.exists():\n",
" print('Bitstamp L3 aún no disponible. Ejecutar notebook 05 primero.')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 4. Resumen: parámetros calibrados desde datos reales\n",
"\n",
"Estos son los valores que usaríamos para que nuestra simulación genere datos similares a BTC/USDT."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Recopilar todo\n",
"print('=' * 65)\n",
"print(' PARÁMETROS CALIBRADOS DESDE BTC/USDT REAL')\n",
"print(' Dataset: 1M aggTrades, ~26 horas')\n",
"print('=' * 65)\n",
"print(f'')\n",
"print(f' # Precio fundamental')\n",
"print(f' sigma = {sigma_1m:.6f} # por minuto')\n",
"print(f' mu = {np.mean(returns):.8f} # drift (cercano a 0)')\n",
"print(f'')\n",
"print(f' # Jumps')\n",
"print(f' jump_intensity = {jump_intensity:.4f} # {jump_intensity*100:.1f}% de velas tienen jump')\n",
"print(f' jump_size_std = {jump_size_std:.6f}')\n",
"print(f'')\n",
"print(f' # Arrival rate')\n",
"print(f' n_takers_lambda = {lambda_mean:.1f} # aggTrades/segundo')\n",
"print(f'')\n",
"print(f' # Hawkes clustering')\n",
"print(f' hawkes_alpha = {hawkes_a:.4f}')\n",
"print(f' hawkes_beta = {hawkes_b:.4f}')\n",
"print(f' branching_ratio = {branching:.3f}')\n",
"print(f'')\n",
"print(f' # Distribución de tamaños')\n",
"print(f' taker_size_alpha = {alpha_qty:.2f} # Pareto exponent (cola p90+)')\n",
"print(f' taker_size_min = {np.percentile(sizes, 5):.6f} # BTC (p5)')\n",
"print(f' taker_size_max = {np.percentile(sizes, 99.9):.4f} # BTC (p99.9)')\n",
"print(f'')\n",
"print(f' # Estructura de fills')\n",
"print(f' median_fills_per_order = {np.median(fills):.0f}')\n",
"print(f' pct_single_fill = {np.mean(fills==1)*100:.1f}%')\n",
"print(f'')\n",
"print(f' # Resumen estadístico')\n",
"print(f' kurtosis = {kurtosis:.1f}')\n",
"print(f' skewness = {skew:.3f}')\n",
"print(f' var_mean_ratio = {var_mean:.1f}')\n",
"print('=' * 65)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,517 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Monte Carlo: análisis de sensibilidad por parámetro\n",
"\n",
"Usamos las funciones del registry para correr cientos de simulaciones variando **un parámetro a la vez**.\n",
"Esto nos dice:\n",
"- Qué parámetros importan más\n",
"- Cómo responde el mercado simulado a cada cambio\n",
"- Qué rangos producen mercados realistas\n",
"\n",
"## Parámetros calibrados desde BTC/USDT real (notebook 06)\n",
"\n",
"| Parámetro | Valor calibrado | Confianza | Fuente |\n",
"|---|---|---|---|\n",
"| sigma | 0.000514 | Alta | Std retornos 1m |\n",
"| mu | ~0 | Alta | Media retornos |\n",
"| jump_intensity | 0.013 | Media | % retornos > 3σ |\n",
"| jump_size_std | 0.000356 | Media | Std de los jumps |\n",
"| n_takers_lambda | 12.0 | Media | aggTrades/segundo |\n",
"| taker_size_alpha | 0.78 | Media | Pareto MLE cola p90+ |\n",
"| hawkes_alpha | 0.17 | Baja | Fit exp sobre ACF |\n",
"| hawkes_beta | 0.015 | Baja | Fit exp sobre ACF |\n",
"| gamma | ? | No observable | Relación spread~vol |\n",
"| n_makers | ? | No observable | Capas de liquidez L2 |\n",
"| maker_spread | 0.01 | Alta | Spread real del book |"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Base params cargados\n",
"Test: 270 trades, spread=-0.057967\n"
]
}
],
"source": [
"import sys, os\n",
"sys.path.insert(0, os.path.join(os.environ.get('FN_REGISTRY_ROOT', os.path.expanduser('~/fn_registry')), 'python', 'functions'))\n",
"sys.path.insert(0, os.path.join(os.environ.get('FN_REGISTRY_ROOT', os.path.expanduser('~/fn_registry')), 'python', 'functions', 'pipelines'))\n",
"\n",
"from run_market_sim import run_market_sim\n",
"import numpy as np\n",
"import polars as pl\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# Parámetros base calibrados desde datos reales\n",
"BASE = dict(\n",
" initial_price=100.0,\n",
" n_ticks=300,\n",
" sigma=0.000514,\n",
" mu=0.0,\n",
" jump_intensity=0.013,\n",
" jump_size_std=0.000356,\n",
" n_makers=5,\n",
" maker_spread=0.01,\n",
" gamma=0.1,\n",
" maker_levels=3,\n",
" maker_qty=10.0,\n",
" n_takers_lambda=12.0,\n",
" taker_size_alpha=0.78,\n",
" taker_size_min=0.001,\n",
" taker_size_max=5.0,\n",
" hawkes_alpha=0.17,\n",
" hawkes_beta=0.015,\n",
")\n",
"\n",
"print('Base params cargados')\n",
"# Quick test\n",
"r = run_market_sim(**BASE, seed=0)\n",
"print(f'Test: {r[\"total_trades\"]} trades, spread={np.mean(r[\"spreads\"]):.6f}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Herramientas de análisis"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sweep_param(param_name: str, values: list, base_params: dict, n_seeds: int = 10) -> pl.DataFrame:\n",
" \"\"\"Corre simulaciones variando un parámetro. N seeds por valor para tener distribución.\"\"\"\n",
" records = []\n",
" total = len(values) * n_seeds\n",
" done = 0\n",
" for val in values:\n",
" for seed in range(n_seeds):\n",
" params = dict(base_params)\n",
" params[param_name] = val\n",
" params['seed'] = seed * 1000 + hash(str(val)) % 1000\n",
" sim = run_market_sim(**params)\n",
" \n",
" spreads = sim['spreads']\n",
" npt = sim['n_trades_per_tick']\n",
" tp = np.array(sim['trade_prices']) if sim['trade_prices'] else np.array([0.0])\n",
" fp = np.array(sim['fundamental_prices'])\n",
" \n",
" # Realized vol de trades\n",
" tp_pos = tp[tp > 0]\n",
" if len(tp_pos) > 2:\n",
" log_ret = np.diff(np.log(tp_pos))\n",
" rvol = float(np.std(log_ret))\n",
" else:\n",
" rvol = 0.0\n",
" \n",
" records.append({\n",
" 'param_value': float(val),\n",
" 'seed': seed,\n",
" 'total_trades': sim['total_trades'],\n",
" 'mean_spread': float(np.mean(spreads)),\n",
" 'std_spread': float(np.std(spreads)),\n",
" 'mean_trades_tick': float(np.mean(npt)),\n",
" 'max_trades_tick': int(np.max(npt)),\n",
" 'realized_vol': rvol,\n",
" 'price_return_pct': float((fp[-1] / fp[0] - 1) * 100),\n",
" 'maker_total_pnl': float(sum(sim['maker_pnls'])),\n",
" })\n",
" done += 1\n",
" \n",
" print(f'{param_name}: {done} simulaciones')\n",
" return pl.DataFrame(records)\n",
"\n",
"\n",
"def plot_sweep(df: pl.DataFrame, param_name: str, metrics: list[tuple[str, str]], title: str = ''):\n",
" \"\"\"Grafica métricas vs parámetro con bandas de confianza.\"\"\"\n",
" n = len(metrics)\n",
" fig, axes = plt.subplots(1, n, figsize=(5 * n, 4))\n",
" if n == 1:\n",
" axes = [axes]\n",
" \n",
" agg = df.group_by('param_value').agg(\n",
" *[pl.col(m).mean().alias(f'{m}_mean') for m, _ in metrics],\n",
" *[pl.col(m).std().alias(f'{m}_std') for m, _ in metrics],\n",
" ).sort('param_value')\n",
" \n",
" x = agg['param_value'].to_numpy()\n",
" \n",
" for i, (metric, label) in enumerate(metrics):\n",
" ax = axes[i]\n",
" y = agg[f'{metric}_mean'].to_numpy()\n",
" yerr = agg[f'{metric}_std'].to_numpy()\n",
" yerr = np.nan_to_num(yerr, nan=0.0)\n",
" \n",
" ax.fill_between(x, y - yerr, y + yerr, alpha=0.2, color='#3498db')\n",
" ax.plot(x, y, 'o-', color='#3498db', markersize=4, linewidth=1.5)\n",
" ax.set_xlabel(param_name)\n",
" ax.set_ylabel(label)\n",
" ax.grid(True, alpha=0.3)\n",
" \n",
" fig.suptitle(title or f'Sensibilidad a {param_name}', fontsize=12, fontweight='bold')\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"METRICS = [\n",
" ('mean_spread', 'Spread medio'),\n",
" ('total_trades', 'Total trades'),\n",
" ('realized_vol', 'Vol realizada'),\n",
" ('maker_total_pnl', 'PnL makers'),\n",
"]\n",
"\n",
"print('sweep_param() y plot_sweep() definidas')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 1. SIGMA (volatilidad)\n",
"\n",
"**Qué es:** cuánto se mueve el precio fundamental por tick. \n",
"**Calibrado:** 0.000514 (desde retornos 1m de BTC) \n",
"**Confianza:** ALTA — medición directa \n",
"**Hipótesis:** más σ → más oportunidades para takers → más trades, spread más ancho (makers se protegen)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sigma_vals = [0.0001, 0.0003, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05]\n",
"df_sigma = sweep_param('sigma', sigma_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_sigma, 'sigma', METRICS, 'SIGMA — volatilidad del precio fundamental')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 2. GAMMA (aversión al riesgo del maker)\n",
"\n",
"**Qué es:** cuánto ajusta el maker sus precios por inventario acumulado. \n",
"**Calibrado:** NO directamente — se infiere de spread vs volatilidad \n",
"**Confianza:** BAJA \n",
"**Hipótesis:** más γ → spread más ancho → menos ejecuciones → makers más seguros pero mercado menos líquido"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gamma_vals = [0.001, 0.005, 0.01, 0.05, 0.1, 0.3, 0.5, 1.0, 2.0, 5.0]\n",
"df_gamma = sweep_param('gamma', gamma_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_gamma, 'gamma', METRICS, 'GAMMA — aversión al riesgo del maker (Avellaneda-Stoikov)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 3. N_TAKERS_LAMBDA (arrival rate de takers)\n",
"\n",
"**Qué es:** cuántos takers llegan por tick en promedio (base Poisson, amplificado por Hawkes). \n",
"**Calibrado:** 12.0 aggTrades/segundo \n",
"**Confianza:** MEDIA — medimos aggTrades, no órdenes originales \n",
"**Hipótesis:** más λ → más presión sobre el book → más trades, spreads más volátiles"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"lambda_vals = [0.5, 1, 2, 5, 10, 15, 20, 30, 50]\n",
"df_lambda = sweep_param('n_takers_lambda', lambda_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_lambda, 'n_takers_lambda', METRICS, 'LAMBDA — arrival rate de takers')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 4. HAWKES_ALPHA (contagio entre trades)\n",
"\n",
"**Qué es:** cuánto excita un trade la llegada de más trades (clustering). \n",
"**Calibrado:** 0.17 (fit exponencial sobre ACF) \n",
"**Confianza:** BAJA — el branching ratio salió >1, modelo simple no captura bien \n",
"**Hipótesis:** más α → ráfagas más intensas → max trades/tick explota, spread se estresa"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hawkes_a_vals = [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.9]\n",
"df_hawkes_a = sweep_param('hawkes_alpha', hawkes_a_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_hawkes_a, 'hawkes_alpha', METRICS, 'HAWKES_ALPHA — contagio entre trades')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 5. TAKER_SIZE_ALPHA (cola de tamaños — ballenas)\n",
"\n",
"**Qué es:** exponente Pareto de los tamaños de órdenes. Bajo = más ballenas. \n",
"**Calibrado:** 0.78 (MLE sobre cola p90+) \n",
"**Confianza:** MEDIA — medimos fills agrupados, no órdenes originales \n",
"**Hipótesis:** α bajo → más órdenes grandes → más slippage, spread se abre más, más impacto en precio"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"size_a_vals = [0.3, 0.5, 0.78, 1.0, 1.5, 2.0, 3.0, 5.0]\n",
"df_size_a = sweep_param('taker_size_alpha', size_a_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_size_a, 'taker_size_alpha', METRICS, 'TAKER_SIZE_ALPHA — cola de tamaños (bajo = más ballenas)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 6. N_MAKERS (número de market makers)\n",
"\n",
"**Qué es:** cuántos makers compiten poniendo liquidez. \n",
"**Calibrado:** NO directamente observable — se infiere de capas de liquidez en L2 \n",
"**Confianza:** BAJA \n",
"**Hipótesis:** más makers → más competencia → spread más tight, más liquidez, pero PnL por maker baja"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"nmakers_vals = [1, 2, 3, 5, 7, 10, 15, 20]\n",
"df_nmakers = sweep_param('n_makers', nmakers_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_nmakers, 'n_makers', METRICS, 'N_MAKERS — número de market makers')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7. MAKER_SPREAD (spread base)\n",
"\n",
"**Qué es:** el spread mínimo que los makers intentan capturar. \n",
"**Calibrado:** $0.01 (spread real de BTC/USDT en Binance) \n",
"**Confianza:** ALTA — medición directa del book \n",
"**Hipótesis:** spread más ancho → menos ejecuciones → makers más rentables pero mercado menos eficiente"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spread_vals = [0.001, 0.005, 0.01, 0.05, 0.1, 0.3, 0.5, 1.0, 2.0]\n",
"df_spread = sweep_param('maker_spread', spread_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_spread, 'maker_spread', METRICS, 'MAKER_SPREAD — spread base deseado por makers')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 8. JUMP_INTENSITY (frecuencia de saltos)\n",
"\n",
"**Qué es:** probabilidad de un movimiento brusco en cada tick. \n",
"**Calibrado:** 1.3% (retornos > 3σ) \n",
"**Confianza:** MEDIA — depende del threshold elegido \n",
"**Hipótesis:** más jumps → más volatilidad realizada, kurtosis sube, makers sufren más"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"jump_vals = [0.0, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2]\n",
"df_jump = sweep_param('jump_intensity', jump_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_jump, 'jump_intensity', METRICS, 'JUMP_INTENSITY — frecuencia de saltos bruscos')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 9. HAWKES_BETA (decaimiento del contagio)\n",
"\n",
"**Qué es:** qué tan rápido se calma la excitación después de una ráfaga. \n",
"**Calibrado:** 0.015 \n",
"**Confianza:** BAJA \n",
"**Hipótesis:** β bajo → ráfagas más largas → mercado más caótico. β alto → ráfagas cortas → más Poisson"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hawkes_b_vals = [0.005, 0.01, 0.02, 0.05, 0.1, 0.3, 0.5, 1.0, 2.0]\n",
"df_hawkes_b = sweep_param('hawkes_beta', hawkes_b_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_hawkes_b, 'hawkes_beta', METRICS, 'HAWKES_BETA — decaimiento del contagio (alto = se calma rápido)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 10. MAKER_LEVELS (profundidad del maker)\n",
"\n",
"**Qué es:** cuántos niveles de precio pone cada maker a cada lado. \n",
"**Calibrado:** se estima contando niveles con liquidez significativa en L2 \n",
"**Confianza:** BAJA \n",
"**Hipótesis:** más niveles → más profundidad → menos slippage para órdenes grandes"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"levels_vals = [1, 2, 3, 5, 7, 10, 15]\n",
"df_levels = sweep_param('maker_levels', levels_vals, BASE, n_seeds=10)\n",
"plot_sweep(df_levels, 'maker_levels', METRICS, 'MAKER_LEVELS — niveles de profundidad por maker')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 11. Resumen: sensibilidad relativa\n",
"\n",
"¿Qué parámetro afecta más a cada métrica?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Calcular coeficiente de variación de cada métrica respecto a cada parámetro\n",
"all_sweeps = {\n",
" 'sigma': df_sigma,\n",
" 'gamma': df_gamma,\n",
" 'n_takers_lambda': df_lambda,\n",
" 'hawkes_alpha': df_hawkes_a,\n",
" 'taker_size_alpha': df_size_a,\n",
" 'n_makers': df_nmakers,\n",
" 'maker_spread': df_spread,\n",
" 'jump_intensity': df_jump,\n",
" 'hawkes_beta': df_hawkes_b,\n",
" 'maker_levels': df_levels,\n",
"}\n",
"\n",
"sensitivity = []\n",
"for pname, df in all_sweeps.items():\n",
" agg = df.group_by('param_value').agg(\n",
" pl.col('mean_spread').mean(),\n",
" pl.col('total_trades').mean(),\n",
" pl.col('realized_vol').mean(),\n",
" pl.col('maker_total_pnl').mean(),\n",
" )\n",
" for metric in ['mean_spread', 'total_trades', 'realized_vol', 'maker_total_pnl']:\n",
" vals = agg[metric].to_numpy()\n",
" vals = vals[~np.isnan(vals)]\n",
" if len(vals) > 1 and np.mean(np.abs(vals)) > 0:\n",
" cv = np.std(vals) / np.mean(np.abs(vals))\n",
" else:\n",
" cv = 0.0\n",
" sensitivity.append({'param': pname, 'metric': metric, 'cv': round(cv, 3)})\n",
"\n",
"sens_df = pl.DataFrame(sensitivity)\n",
"\n",
"# Heatmap\n",
"params_order = list(all_sweeps.keys())\n",
"metrics_order = ['mean_spread', 'total_trades', 'realized_vol', 'maker_total_pnl']\n",
"metrics_labels = ['Spread', 'Trades', 'Vol realizada', 'PnL makers']\n",
"\n",
"matrix = np.zeros((len(params_order), len(metrics_order)))\n",
"for row in sens_df.iter_rows(named=True):\n",
" i = params_order.index(row['param'])\n",
" j = metrics_order.index(row['metric'])\n",
" matrix[i, j] = row['cv']\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 8))\n",
"im = ax.imshow(matrix, cmap='YlOrRd', aspect='auto')\n",
"ax.set_xticks(range(len(metrics_labels)))\n",
"ax.set_xticklabels(metrics_labels, fontsize=10)\n",
"ax.set_yticks(range(len(params_order)))\n",
"ax.set_yticklabels(params_order, fontsize=10)\n",
"\n",
"for i in range(len(params_order)):\n",
" for j in range(len(metrics_order)):\n",
" ax.text(j, i, f'{matrix[i,j]:.2f}', ha='center', va='center', fontsize=9,\n",
" color='white' if matrix[i,j] > 0.5 else 'black')\n",
"\n",
"ax.set_title('Sensibilidad: coeficiente de variación por parámetro × métrica\\n(más alto = más impacto)', fontsize=12)\n",
"plt.colorbar(im, label='CV')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Top sensibilidades\n",
"print('\\nTop 10 combinaciones param × métrica más sensibles:')\n",
"top = sens_df.sort('cv', descending=True).head(10)\n",
"print(top)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,504 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Estimación de precios futuros con Monte Carlo\n",
"\n",
"Usamos los parámetros calibrados de BTC/USDT real para generar **miles de caminos de precio posibles** y estimar:\n",
"- Distribución del precio a distintos horizontes\n",
"- Intervalos de confianza (fan chart)\n",
"- Probabilidad de subir/bajar X%\n",
"- Value at Risk (VaR) y Expected Shortfall\n",
"\n",
"**Importante:** Esto NO es una predicción. Es un modelo probabilístico que dice \"dado cómo se ha comportado el mercado, estos son los escenarios posibles\". La distribución real tiene colas más pesadas que nuestro modelo."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Listo\n"
]
}
],
"source": [
"import sys, os\n",
"sys.path.insert(0, os.path.join(os.environ.get('FN_REGISTRY_ROOT', os.path.expanduser('~/fn_registry')), 'python', 'functions'))\n",
"sys.path.insert(0, os.path.join(os.environ.get('FN_REGISTRY_ROOT', os.path.expanduser('~/fn_registry')), 'python', 'functions', 'pipelines'))\n",
"\n",
"from finance.finance import generate_gbm_prices\n",
"from run_market_sim import run_market_sim\n",
"import numpy as np\n",
"import polars as pl\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.colors import LinearSegmentedColormap\n",
"\n",
"print('Listo')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Parámetros calibrados y escenarios"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Precio actual de BTC\n",
"CURRENT_PRICE = 66760.0\n",
"\n",
"# Parámetros calibrados de notebook 06 (datos reales 1M trades)\n",
"CALIBRATED = dict(\n",
" sigma=0.000514, # por minuto\n",
" mu=0.0, # sin drift (conservador)\n",
" jump_intensity=0.013, # 1.3% de velas con jump\n",
" jump_size_std=0.000356, # tamaño de los jumps\n",
")\n",
"\n",
"# Horizontes de simulación\n",
"HORIZONS = {\n",
" '1 hora': 60,\n",
" '4 horas': 240,\n",
" '1 día': 1440,\n",
" '1 semana': 10080,\n",
"}\n",
"\n",
"N_SIMS = 5000 # simulaciones por escenario\n",
"\n",
"print(f'Precio actual: ${CURRENT_PRICE:,.0f}')\n",
"print(f'σ minuto: {CALIBRATED[\"sigma\"]:.6f}')\n",
"print(f'σ diaria: {CALIBRATED[\"sigma\"] * np.sqrt(1440):.4f} ({CALIBRATED[\"sigma\"] * np.sqrt(1440) * 100:.2f}%)')\n",
"print(f'σ anual: {CALIBRATED[\"sigma\"] * np.sqrt(1440 * 365):.2f} ({CALIBRATED[\"sigma\"] * np.sqrt(1440 * 365) * 100:.0f}%)')\n",
"print(f'Simulaciones: {N_SIMS:,}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Generar caminos de precio Monte Carlo\n",
"\n",
"Para cada simulación generamos un camino completo de precios usando GBM + jumps.\n",
"El horizonte más largo (1 semana = 10,080 minutos) incluye a todos los demás."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"max_ticks = max(HORIZONS.values())\n",
"\n",
"# Generar todos los caminos (matrix: N_SIMS x max_ticks)\n",
"all_paths = np.zeros((N_SIMS, max_ticks))\n",
"\n",
"for i in range(N_SIMS):\n",
" path = generate_gbm_prices(\n",
" initial_price=CURRENT_PRICE,\n",
" n_ticks=max_ticks,\n",
" seed=i,\n",
" **CALIBRATED,\n",
" )\n",
" all_paths[i] = path\n",
"\n",
"print(f'Generados {N_SIMS:,} caminos de {max_ticks:,} ticks ({max_ticks/1440:.0f} días)')\n",
"print(f'Shape: {all_paths.shape}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Fan chart — todos los caminos posibles\n",
"\n",
"El fan chart muestra la distribución del precio en cada momento.\n",
"Las bandas representan percentiles: cuanto más oscuro, más probable."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_fan_chart(paths, horizon_ticks, horizon_name, n_sample_paths=50):\n",
" \"\"\"Fan chart con bandas de percentiles.\"\"\"\n",
" data = paths[:, :horizon_ticks]\n",
" ticks = np.arange(horizon_ticks)\n",
" \n",
" # Percentiles\n",
" bands = [\n",
" (1, 99, '#3498db', 0.08),\n",
" (5, 95, '#3498db', 0.12),\n",
" (10, 90, '#3498db', 0.18),\n",
" (25, 75, '#3498db', 0.25),\n",
" (40, 60, '#3498db', 0.35),\n",
" ]\n",
" \n",
" fig, ax = plt.subplots(figsize=(16, 7))\n",
" \n",
" for plo, phi, color, alpha in bands:\n",
" lo = np.percentile(data, plo, axis=0)\n",
" hi = np.percentile(data, phi, axis=0)\n",
" ax.fill_between(ticks, lo, hi, color=color, alpha=alpha, label=f'p{plo}-p{phi}')\n",
" \n",
" # Mediana\n",
" median = np.median(data, axis=0)\n",
" ax.plot(ticks, median, color='#2c3e50', linewidth=1.5, label='Mediana')\n",
" \n",
" # Sample paths\n",
" rng = np.random.default_rng(0)\n",
" idx = rng.choice(N_SIMS, n_sample_paths, replace=False)\n",
" for j in idx:\n",
" ax.plot(ticks, data[j], linewidth=0.15, alpha=0.3, color='#7f8c8d')\n",
" \n",
" ax.axhline(y=CURRENT_PRICE, color='red', linestyle='--', linewidth=0.8, alpha=0.5, label=f'Precio actual ${CURRENT_PRICE:,.0f}')\n",
" \n",
" # Formatear eje x\n",
" if horizon_ticks <= 240:\n",
" ax.set_xlabel('Minutos')\n",
" elif horizon_ticks <= 1440:\n",
" xticks = np.arange(0, horizon_ticks + 1, 60)\n",
" ax.set_xticks(xticks)\n",
" ax.set_xticklabels([f'{int(x/60)}h' for x in xticks])\n",
" ax.set_xlabel('Horas')\n",
" else:\n",
" xticks = np.arange(0, horizon_ticks + 1, 1440)\n",
" ax.set_xticks(xticks)\n",
" ax.set_xticklabels([f'{int(x/1440)}d' for x in xticks])\n",
" ax.set_xlabel('Días')\n",
" \n",
" ax.set_ylabel('Precio (USDT)')\n",
" ax.set_title(f'BTC/USDT — Monte Carlo {N_SIMS:,} simulaciones — Horizonte {horizon_name}', fontsize=13)\n",
" ax.legend(loc='upper left', fontsize=8)\n",
" ax.grid(True, alpha=0.3)\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"# Fan charts para cada horizonte\n",
"for name, ticks in HORIZONS.items():\n",
" plot_fan_chart(all_paths, ticks, name)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Distribución del precio final por horizonte"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n",
"\n",
"for ax, (name, ticks) in zip(axes.flat, HORIZONS.items()):\n",
" final_prices = all_paths[:, ticks - 1]\n",
" returns_pct = (final_prices / CURRENT_PRICE - 1) * 100\n",
" \n",
" ax.hist(returns_pct, bins=80, density=True, color='#3498db', alpha=0.6, edgecolor='white')\n",
" \n",
" # Percentiles\n",
" p5 = np.percentile(returns_pct, 5)\n",
" p50 = np.percentile(returns_pct, 50)\n",
" p95 = np.percentile(returns_pct, 95)\n",
" \n",
" ax.axvline(x=p5, color='red', linewidth=1.5, linestyle='--', label=f'p5: {p5:+.2f}%')\n",
" ax.axvline(x=p50, color='#2c3e50', linewidth=1.5, label=f'Mediana: {p50:+.2f}%')\n",
" ax.axvline(x=p95, color='green', linewidth=1.5, linestyle='--', label=f'p95: {p95:+.2f}%')\n",
" ax.axvline(x=0, color='gray', linewidth=0.8, alpha=0.5)\n",
" \n",
" ax.set_title(f'{name}', fontsize=12, fontweight='bold')\n",
" ax.set_xlabel('Retorno (%)')\n",
" ax.legend(fontsize=8)\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"fig.suptitle(f'Distribución de retornos por horizonte — {N_SIMS:,} simulaciones', fontsize=14)\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Tabla de estimaciones por horizonte"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(f'Precio actual: ${CURRENT_PRICE:,.0f}')\n",
"print(f'Modelo: GBM + Jump-diffusion (σ={CALIBRATED[\"sigma\"]}, jumps={CALIBRATED[\"jump_intensity\"]})')\n",
"print(f'Simulaciones: {N_SIMS:,}')\n",
"print()\n",
"print(f'{\"Horizonte\":<12} {\"P5\":>10} {\"P25\":>10} {\"Mediana\":>10} {\"P75\":>10} {\"P95\":>10} {\"σ rango\":>10}')\n",
"print('-' * 75)\n",
"\n",
"for name, ticks in HORIZONS.items():\n",
" fp = all_paths[:, ticks - 1]\n",
" p5, p25, p50, p75, p95 = np.percentile(fp, [5, 25, 50, 75, 95])\n",
" sigma_range = (p95 - p5) / CURRENT_PRICE * 100\n",
" print(f'{name:<12} ${p5:>9,.0f} ${p25:>9,.0f} ${p50:>9,.0f} ${p75:>9,.0f} ${p95:>9,.0f} ±{sigma_range/2:.1f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. Probabilidades de escenarios"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"scenarios = [-10, -5, -2, -1, 0, 1, 2, 5, 10] # % de cambio\n",
"\n",
"print(f'{\"Horizonte\":<12}', end='')\n",
"for s in scenarios:\n",
" label = f'{s:+d}%' if s != 0 else ' =0%'\n",
" print(f'{label:>8}', end='')\n",
"print()\n",
"print('-' * (12 + 8 * len(scenarios)))\n",
"\n",
"for name, ticks in HORIZONS.items():\n",
" fp = all_paths[:, ticks - 1]\n",
" returns = (fp / CURRENT_PRICE - 1) * 100\n",
" \n",
" print(f'{name:<12}', end='')\n",
" for s in scenarios:\n",
" if s < 0:\n",
" prob = np.mean(returns <= s) * 100\n",
" elif s > 0:\n",
" prob = np.mean(returns >= s) * 100\n",
" else:\n",
" prob = np.mean(returns >= 0) * 100\n",
" print(f'{prob:>7.1f}%', end='')\n",
" print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. Value at Risk (VaR) y Expected Shortfall (CVaR)\n",
"\n",
"- **VaR(95%):** pérdida máxima que no se supera el 95% del tiempo\n",
"- **CVaR(95%):** pérdida promedio en el peor 5% de los casos (más conservador)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"confidence_levels = [0.90, 0.95, 0.99]\n",
"\n",
"print(f'{\"Horizonte\":<12}', end='')\n",
"for cl in confidence_levels:\n",
" print(f'{\"VaR \" + str(int(cl*100)) + \"%\":>10} {\"CVaR \" + str(int(cl*100)) + \"%\":>10}', end='')\n",
"print()\n",
"print('-' * (12 + 20 * len(confidence_levels)))\n",
"\n",
"for name, ticks in HORIZONS.items():\n",
" fp = all_paths[:, ticks - 1]\n",
" pnl = fp - CURRENT_PRICE # P&L en dólares\n",
" pnl_pct = (fp / CURRENT_PRICE - 1) * 100\n",
" \n",
" print(f'{name:<12}', end='')\n",
" for cl in confidence_levels:\n",
" var_pct = np.percentile(pnl_pct, (1 - cl) * 100)\n",
" # CVaR = promedio de las pérdidas peores que VaR\n",
" cvar_pct = np.mean(pnl_pct[pnl_pct <= var_pct])\n",
" print(f'{var_pct:>+9.2f}% {cvar_pct:>+9.2f}%', end='')\n",
" print()\n",
"\n",
"print()\n",
"print('En dólares (por 1 BTC):')\n",
"print(f'{\"Horizonte\":<12}', end='')\n",
"for cl in confidence_levels:\n",
" print(f'{\"VaR \" + str(int(cl*100)) + \"%\":>12} {\"CVaR \" + str(int(cl*100)) + \"%\":>12}', end='')\n",
"print()\n",
"print('-' * (12 + 24 * len(confidence_levels)))\n",
"\n",
"for name, ticks in HORIZONS.items():\n",
" fp = all_paths[:, ticks - 1]\n",
" pnl = fp - CURRENT_PRICE\n",
" \n",
" print(f'{name:<12}', end='')\n",
" for cl in confidence_levels:\n",
" var_usd = np.percentile(pnl, (1 - cl) * 100)\n",
" cvar_usd = np.mean(pnl[pnl <= var_usd])\n",
" print(f' ${var_usd:>+9,.0f} ${cvar_usd:>+9,.0f}', end='')\n",
" print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. Impacto del matching engine: simulación completa vs GBM puro\n",
"\n",
"¿Cambian las estimaciones cuando incluimos el matching engine (makers + takers) en vez de solo GBM?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"N_ENGINE_SIMS = 200 # menos porque el engine es más lento\n",
"HORIZON_ENGINE = 300 # ticks\n",
"\n",
"# Con matching engine\n",
"engine_finals = []\n",
"for i in range(N_ENGINE_SIMS):\n",
" sim = run_market_sim(\n",
" initial_price=CURRENT_PRICE,\n",
" n_ticks=HORIZON_ENGINE,\n",
" sigma=CALIBRATED['sigma'],\n",
" mu=CALIBRATED['mu'],\n",
" jump_intensity=CALIBRATED['jump_intensity'],\n",
" jump_size_std=CALIBRATED['jump_size_std'],\n",
" n_makers=5,\n",
" maker_spread=0.01,\n",
" gamma=0.1,\n",
" n_takers_lambda=12.0,\n",
" taker_size_alpha=0.78,\n",
" hawkes_alpha=0.17,\n",
" hawkes_beta=0.015,\n",
" seed=i,\n",
" )\n",
" # Último midprice como precio final\n",
" engine_finals.append(sim['midprices'][-1] if sim['midprices'] else CURRENT_PRICE)\n",
"\n",
"engine_finals = np.array(engine_finals)\n",
"\n",
"# GBM puro (mismos parámetros, mismo horizonte)\n",
"gbm_finals = all_paths[:N_ENGINE_SIMS, HORIZON_ENGINE - 1]\n",
"\n",
"print(f'Simulaciones: {N_ENGINE_SIMS}')\n",
"print(f'Horizonte: {HORIZON_ENGINE} minutos ({HORIZON_ENGINE/60:.0f}h)')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
"\n",
"# Distribuciones comparadas\n",
"ax = axes[0]\n",
"gbm_ret = (gbm_finals / CURRENT_PRICE - 1) * 100\n",
"eng_ret = (engine_finals / CURRENT_PRICE - 1) * 100\n",
"\n",
"ax.hist(gbm_ret, bins=40, density=True, alpha=0.5, color='#3498db', label=f'GBM puro (σ={np.std(gbm_ret):.3f}%)')\n",
"ax.hist(eng_ret, bins=40, density=True, alpha=0.5, color='#e74c3c', label=f'Con engine (σ={np.std(eng_ret):.3f}%)')\n",
"ax.set_xlabel('Retorno (%)')\n",
"ax.set_title(f'Distribución a {HORIZON_ENGINE/60:.0f}h: GBM vs Engine')\n",
"ax.legend(fontsize=9)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# QQ plot\n",
"ax = axes[1]\n",
"gbm_sorted = np.sort(gbm_ret)\n",
"eng_sorted = np.sort(eng_ret)\n",
"min_len = min(len(gbm_sorted), len(eng_sorted))\n",
"ax.scatter(gbm_sorted[:min_len], eng_sorted[:min_len], s=5, alpha=0.5, color='#9b59b6')\n",
"lims = [min(gbm_sorted.min(), eng_sorted.min()), max(gbm_sorted.max(), eng_sorted.max())]\n",
"ax.plot(lims, lims, 'k--', linewidth=0.8)\n",
"ax.set_xlabel('GBM puro (%)')\n",
"ax.set_ylabel('Con engine (%)')\n",
"ax.set_title('QQ-Plot: GBM vs Engine\\n(en la diagonal = idénticos)')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f'GBM puro: media={np.mean(gbm_ret):+.3f}%, std={np.std(gbm_ret):.3f}%, kurtosis={float(np.mean((gbm_ret-np.mean(gbm_ret))**4)/np.std(gbm_ret)**4):.1f}')\n",
"print(f'Con engine: media={np.mean(eng_ret):+.3f}%, std={np.std(eng_ret):.3f}%, kurtosis={float(np.mean((eng_ret-np.mean(eng_ret))**4)/np.std(eng_ret)**4):.1f}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 9. Resumen"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('=' * 70)\n",
"print(f' ESTIMACIÓN MONTE CARLO — BTC/USDT')\n",
"print(f' Precio actual: ${CURRENT_PRICE:,.0f}')\n",
"print(f' Modelo: GBM + Jump-diffusion calibrado con 1M trades reales')\n",
"print(f' Simulaciones: {N_SIMS:,}')\n",
"print('=' * 70)\n",
"print()\n",
"\n",
"for name, ticks in HORIZONS.items():\n",
" fp = all_paths[:, ticks - 1]\n",
" ret = (fp / CURRENT_PRICE - 1) * 100\n",
" p5, p50, p95 = np.percentile(fp, [5, 50, 95])\n",
" prob_up = np.mean(fp > CURRENT_PRICE) * 100\n",
" var95 = np.percentile(ret, 5)\n",
" \n",
" print(f' {name:}')\n",
" print(f' Rango p5-p95: ${p5:,.0f} — ${p95:,.0f}')\n",
" print(f' Mediana: ${p50:,.0f} ({(p50/CURRENT_PRICE - 1)*100:+.2f}%)')\n",
" print(f' P(sube): {prob_up:.1f}%')\n",
" print(f' VaR 95%: {var95:+.2f}% (${var95/100 * CURRENT_PRICE:+,.0f})')\n",
" print()\n",
"\n",
"print(' NOTA: Estas estimaciones asumen que la volatilidad y la estructura')\n",
"print(' del mercado se mantienen constantes. En la realidad cambian.')\n",
"print(' Esto es un modelo probabilístico, NO una predicción.')\n",
"print('=' * 70)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,512 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Alpha Research: señales de microestructura\n",
"\n",
"Exploramos señales que podrían predecir movimientos de precio a corto plazo.\n",
"\n",
"Para cada señal:\n",
"1. La calculamos sobre los datos reales\n",
"2. Medimos su correlación con retornos futuros a distintos horizontes\n",
"3. Visualizamos si tiene poder predictivo\n",
"\n",
"**Datos:** 1M aggTrades BTC/USDT (~26h)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Trades: 1,000,000\n",
"Columnas: ['agg_trade_id', 'price', 'qty', 'first_trade_id', 'last_trade_id', 'timestamp', 'is_buyer_maker', 'side', 'n_fills']\n"
]
}
],
"source": [
"import polars as pl\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from pathlib import Path\n",
"from scipy.stats import spearmanr\n",
"\n",
"DATA = Path('../data')\n",
"trades = pl.read_csv(str(DATA / 'binance_btcusdt_aggtrades_1M.csv'))\n",
"print(f'Trades: {trades.shape[0]:,}')\n",
"print(f'Columnas: {trades.columns}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Preparación: agrupar en barras de tiempo\n",
"\n",
"Las señales se calculan sobre ventanas de tiempo, no sobre trades individuales.\n",
"Creamos barras de 1 segundo con todas las métricas que necesitamos."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Barras de 1 segundo\n",
"bars = trades.with_columns(\n",
" (pl.col('timestamp') // 1000).alias('second'),\n",
" (pl.col('price') * pl.col('qty')).alias('turnover'),\n",
" pl.when(pl.col('side') == 'buy').then(pl.col('qty')).otherwise(0.0).alias('buy_qty'),\n",
" pl.when(pl.col('side') == 'sell').then(pl.col('qty')).otherwise(0.0).alias('sell_qty'),\n",
" pl.when(pl.col('side') == 'buy').then(1).otherwise(0).alias('is_buy'),\n",
").group_by('second').agg(\n",
" pl.col('price').last().alias('close'),\n",
" pl.col('price').first().alias('open'),\n",
" pl.col('price').max().alias('high'),\n",
" pl.col('price').min().alias('low'),\n",
" pl.col('qty').sum().alias('volume'),\n",
" pl.col('turnover').sum().alias('turnover'),\n",
" pl.len().alias('n_trades'),\n",
" pl.col('buy_qty').sum().alias('buy_volume'),\n",
" pl.col('sell_qty').sum().alias('sell_volume'),\n",
" pl.col('is_buy').sum().alias('n_buys'),\n",
" (pl.len() - pl.col('is_buy').sum()).alias('n_sells'),\n",
" pl.col('n_fills').max().alias('max_fills'), # biggest order this second\n",
" pl.col('qty').max().alias('max_qty'),\n",
").sort('second')\n",
"\n",
"# VWAP por segundo\n",
"bars = bars.with_columns(\n",
" (pl.col('turnover') / pl.col('volume')).alias('vwap'),\n",
")\n",
"\n",
"# Log returns futuros a distintos horizontes (para evaluar señales)\n",
"for horizon in [1, 5, 10, 30, 60]:\n",
" bars = bars.with_columns(\n",
" (pl.col('close').shift(-horizon).log() - pl.col('close').log()).alias(f'fwd_ret_{horizon}s')\n",
" )\n",
"\n",
"print(f'Barras de 1s: {bars.shape[0]:,}')\n",
"print(f'Columnas: {bars.columns}')\n",
"print(bars.head(3))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def evaluate_signal(bars: pl.DataFrame, signal_col: str, name: str, horizons=[1, 5, 10, 30, 60]):\n",
" \"\"\"Evalúa una señal: correlación con retornos futuros + gráficos.\"\"\"\n",
" fig, axes = plt.subplots(1, len(horizons) + 1, figsize=(4 * (len(horizons) + 1), 4))\n",
" \n",
" # Panel 1: la señal en el tiempo\n",
" ax = axes[0]\n",
" sig = bars[signal_col].to_numpy()\n",
" ax.plot(sig[:2000], linewidth=0.3, color='#3498db', alpha=0.7)\n",
" ax.set_title(f'{name}\\n(primeros 2000s)', fontsize=9)\n",
" ax.set_xlabel('Segundo')\n",
" ax.grid(True, alpha=0.3)\n",
" \n",
" # Paneles 2+: scatter señal vs retorno futuro por horizonte\n",
" corrs = []\n",
" for i, h in enumerate(horizons):\n",
" ax = axes[i + 1]\n",
" ret_col = f'fwd_ret_{h}s'\n",
" \n",
" clean = bars.select([signal_col, ret_col]).drop_nulls()\n",
" if clean.shape[0] < 100:\n",
" corrs.append((h, 0, 1))\n",
" continue\n",
" \n",
" x = clean[signal_col].to_numpy()\n",
" y = clean[ret_col].to_numpy()\n",
" \n",
" # Spearman (rank correlation, más robusto a outliers)\n",
" rho, pval = spearmanr(x, y)\n",
" corrs.append((h, rho, pval))\n",
" \n",
" # Binned scatter: dividir señal en 20 bins, plotear media de retorno\n",
" n_bins = 20\n",
" try:\n",
" bins = np.percentile(x[~np.isnan(x)], np.linspace(0, 100, n_bins + 1))\n",
" bins = np.unique(bins)\n",
" if len(bins) < 3:\n",
" raise ValueError\n",
" bin_idx = np.digitize(x, bins) - 1\n",
" bin_idx = np.clip(bin_idx, 0, len(bins) - 2)\n",
" bin_means_x = [np.mean(x[bin_idx == b]) for b in range(len(bins) - 1) if np.sum(bin_idx == b) > 0]\n",
" bin_means_y = [np.mean(y[bin_idx == b]) * 10000 for b in range(len(bins) - 1) if np.sum(bin_idx == b) > 0] # in bps\n",
" ax.bar(range(len(bin_means_y)), bin_means_y, color='#2ecc71' if rho > 0 else '#e74c3c', alpha=0.6)\n",
" except:\n",
" pass\n",
" \n",
" color = 'green' if abs(rho) > 0.02 and pval < 0.01 else 'gray'\n",
" ax.set_title(f'{h}s: ρ={rho:.4f}\\np={pval:.2e}', fontsize=9, color=color)\n",
" ax.set_xlabel(f'Bin de {name}')\n",
" if i == 0:\n",
" ax.set_ylabel('Ret futuro (bps)')\n",
" ax.axhline(y=0, color='black', linewidth=0.5)\n",
" ax.grid(True, alpha=0.3)\n",
" \n",
" fig.suptitle(f'Señal: {name}', fontsize=12, fontweight='bold')\n",
" plt.tight_layout()\n",
" plt.show()\n",
" \n",
" # Resumen\n",
" for h, rho, pval in corrs:\n",
" sig_marker = '***' if pval < 0.001 else '**' if pval < 0.01 else '*' if pval < 0.05 else ''\n",
" print(f' {h:>3}s: ρ={rho:+.4f} (p={pval:.2e}) {sig_marker}')\n",
" \n",
" return corrs\n",
"\n",
"print('evaluate_signal() definida')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 1: Order Flow Imbalance (OFI)\n",
"\n",
"**Qué mide:** La diferencia entre volumen de compras y ventas en los últimos N segundos. \n",
"**Intuición:** Si llegan más market buys que sells, hay presión compradora → el precio debería subir. \n",
"**Fórmula:** `OFI = (buy_volume - sell_volume) / (buy_volume + sell_volume)` \n",
"Normalizado entre -1 (todo sells) y +1 (todo buys)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# OFI en ventanas de 5, 10, 30 segundos\n",
"for w in [5, 10, 30]:\n",
" buy_sum = bars['buy_volume'].rolling_sum(window_size=w)\n",
" sell_sum = bars['sell_volume'].rolling_sum(window_size=w)\n",
" total = buy_sum + sell_sum\n",
" ofi = (buy_sum - sell_sum) / total\n",
" bars = bars.with_columns(ofi.alias(f'ofi_{w}s'))\n",
"\n",
"print('OFI 5s:')\n",
"corrs_ofi5 = evaluate_signal(bars, 'ofi_5s', 'OFI 5s')\n",
"print('\\nOFI 10s:')\n",
"corrs_ofi10 = evaluate_signal(bars, 'ofi_10s', 'OFI 10s')\n",
"print('\\nOFI 30s:')\n",
"corrs_ofi30 = evaluate_signal(bars, 'ofi_30s', 'OFI 30s')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 2: Trade Count Imbalance\n",
"\n",
"**Qué mide:** Diferencia entre número de buys y sells (no volumen, sino conteo). \n",
"**Intuición:** Muchos trades pequeños de compra pueden ser más informativos que un solo trade grande. \n",
"**Fórmula:** `TCI = (n_buys - n_sells) / (n_buys + n_sells)`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for w in [5, 10, 30]:\n",
" nb = bars['n_buys'].rolling_sum(window_size=w)\n",
" ns = bars['n_sells'].rolling_sum(window_size=w)\n",
" bars = bars.with_columns(\n",
" ((nb - ns) / (nb + ns)).alias(f'tci_{w}s')\n",
" )\n",
"\n",
"print('Trade Count Imbalance 10s:')\n",
"corrs_tci = evaluate_signal(bars, 'tci_10s', 'TCI 10s')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 3: Trade Intensity (aceleración de actividad)\n",
"\n",
"**Qué mide:** ¿Están llegando trades más rápido que lo normal? \n",
"**Intuición:** Aceleraciones predicen movimientos — los informados tradean antes del movimiento. \n",
"**Fórmula:** `intensity = trades_last_5s / trades_last_60s_avg`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"short_window = bars['n_trades'].rolling_sum(window_size=5)\n",
"long_window = bars['n_trades'].rolling_mean(window_size=60)\n",
"bars = bars.with_columns(\n",
" (short_window / 5 / long_window).alias('trade_intensity')\n",
")\n",
"\n",
"print('Trade Intensity (5s / 60s avg):')\n",
"corrs_intensity = evaluate_signal(bars, 'trade_intensity', 'Trade Intensity')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 4: Volume-Weighted Imbalance\n",
"\n",
"**Qué mide:** OFI pero ponderando más los trades grandes (ballenas). \n",
"**Intuición:** Un trade de 1 BTC tiene más información que 100 trades de 0.001 BTC. \n",
"**Fórmula:** Separar trades grandes (>p90) y calcular su OFI"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Señal basada en los trades más grandes de cada segundo\n",
"# max_qty ya captura el trade más grande, pero necesitamos su lado\n",
"# Usamos n_fills como proxy: más fills = orden más grande que barrió más niveles\n",
"\n",
"# Proxy: volumen de los trades con >5 fills (ballenas)\n",
"whale_trades = trades.filter(pl.col('n_fills') > 5).with_columns(\n",
" (pl.col('timestamp') // 1000).alias('second'),\n",
" pl.when(pl.col('side') == 'buy').then(pl.col('qty')).otherwise(-pl.col('qty')).alias('signed_qty'),\n",
")\n",
"\n",
"whale_flow = whale_trades.group_by('second').agg(\n",
" pl.col('signed_qty').sum().alias('whale_flow'),\n",
" pl.len().alias('whale_count'),\n",
").sort('second')\n",
"\n",
"# Unir con bars\n",
"bars = bars.join(whale_flow, on='second', how='left').with_columns(\n",
" pl.col('whale_flow').fill_null(0.0),\n",
" pl.col('whale_count').fill_null(0),\n",
")\n",
"\n",
"# Whale flow rolling\n",
"bars = bars.with_columns(\n",
" pl.col('whale_flow').rolling_sum(window_size=10).alias('whale_flow_10s'),\n",
")\n",
"\n",
"print('Whale Flow 10s (trades con >5 fills):')\n",
"print(f'Trades clasificados como ballena: {whale_trades.shape[0]:,} ({whale_trades.shape[0]/trades.shape[0]*100:.1f}%)')\n",
"corrs_whale = evaluate_signal(bars, 'whale_flow_10s', 'Whale Flow 10s')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 5: VWAP Deviation\n",
"\n",
"**Qué mide:** ¿El precio actual está por encima o debajo del VWAP reciente? \n",
"**Intuición:** El precio tiende a revertir al VWAP (mean reversion). \n",
"**Fórmula:** `deviation = (close - vwap_rolling) / close`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for w in [30, 60, 300]:\n",
" rolling_turnover = bars['turnover'].rolling_sum(window_size=w)\n",
" rolling_volume = bars['volume'].rolling_sum(window_size=w)\n",
" rolling_vwap = rolling_turnover / rolling_volume\n",
" deviation = (bars['close'] - rolling_vwap) / bars['close']\n",
" bars = bars.with_columns(deviation.alias(f'vwap_dev_{w}s'))\n",
"\n",
"print('VWAP Deviation 30s:')\n",
"corrs_vwap30 = evaluate_signal(bars, 'vwap_dev_30s', 'VWAP Dev 30s')\n",
"print('\\nVWAP Deviation 60s:')\n",
"corrs_vwap60 = evaluate_signal(bars, 'vwap_dev_60s', 'VWAP Dev 60s')\n",
"print('\\nVWAP Deviation 300s (5min):')\n",
"corrs_vwap300 = evaluate_signal(bars, 'vwap_dev_300s', 'VWAP Dev 5min')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 6: Volatility Breakout\n",
"\n",
"**Qué mide:** ¿La volatilidad actual es anormalmente alta? \n",
"**Intuición:** Picos de volatilidad preceden movimientos direccionales (momentum post-breakout). \n",
"**Fórmula:** `breakout = vol_5s / vol_60s`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Volatilidad realizada como rango (high - low) / close\n",
"bars = bars.with_columns(\n",
" ((pl.col('high') - pl.col('low')) / pl.col('close')).alias('range_pct')\n",
")\n",
"\n",
"short_vol = bars['range_pct'].rolling_mean(window_size=5)\n",
"long_vol = bars['range_pct'].rolling_mean(window_size=60)\n",
"bars = bars.with_columns(\n",
" (short_vol / long_vol).alias('vol_breakout')\n",
")\n",
"\n",
"print('Volatility Breakout (5s / 60s):')\n",
"corrs_volbreak = evaluate_signal(bars, 'vol_breakout', 'Vol Breakout')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Señal 7: Retorno reciente (momentum/reversal)\n",
"\n",
"**Qué mide:** ¿El precio acaba de subir o bajar? \n",
"**Intuición:** A muy corto plazo puede haber momentum (inercia) o reversal (rebote). \n",
"**Fórmula:** `ret_Ns = log(close) - log(close_N_ago)`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for w in [1, 5, 10, 30, 60]:\n",
" bars = bars.with_columns(\n",
" (pl.col('close').log() - pl.col('close').shift(w).log()).alias(f'past_ret_{w}s')\n",
" )\n",
"\n",
"print('Past Return 1s (ultra corto):')\n",
"corrs_ret1 = evaluate_signal(bars, 'past_ret_1s', 'Past Ret 1s')\n",
"print('\\nPast Return 5s:')\n",
"corrs_ret5 = evaluate_signal(bars, 'past_ret_5s', 'Past Ret 5s')\n",
"print('\\nPast Return 30s:')\n",
"corrs_ret30 = evaluate_signal(bars, 'past_ret_30s', 'Past Ret 30s')\n",
"print('\\nPast Return 60s:')\n",
"corrs_ret60 = evaluate_signal(bars, 'past_ret_60s', 'Past Ret 60s')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Resumen: ranking de señales"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Recopilar todas las correlaciones\n",
"all_signals = [\n",
" ('OFI 5s', corrs_ofi5),\n",
" ('OFI 10s', corrs_ofi10),\n",
" ('OFI 30s', corrs_ofi30),\n",
" ('TCI 10s', corrs_tci),\n",
" ('Trade Intensity', corrs_intensity),\n",
" ('Whale Flow 10s', corrs_whale),\n",
" ('VWAP Dev 30s', corrs_vwap30),\n",
" ('VWAP Dev 60s', corrs_vwap60),\n",
" ('VWAP Dev 5min', corrs_vwap300),\n",
" ('Vol Breakout', corrs_volbreak),\n",
" ('Past Ret 1s', corrs_ret1),\n",
" ('Past Ret 5s', corrs_ret5),\n",
" ('Past Ret 30s', corrs_ret30),\n",
" ('Past Ret 60s', corrs_ret60),\n",
"]\n",
"\n",
"records = []\n",
"for name, corrs in all_signals:\n",
" for h, rho, pval in corrs:\n",
" records.append({'signal': name, 'horizon_s': h, 'spearman_rho': round(rho, 5), 'p_value': pval})\n",
"\n",
"results = pl.DataFrame(records)\n",
"\n",
"# Heatmap de correlaciones\n",
"signal_names = [s[0] for s in all_signals]\n",
"horizons = [1, 5, 10, 30, 60]\n",
"\n",
"matrix = np.zeros((len(signal_names), len(horizons)))\n",
"for row in results.iter_rows(named=True):\n",
" i = signal_names.index(row['signal'])\n",
" j = horizons.index(row['horizon_s'])\n",
" matrix[i, j] = row['spearman_rho']\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 10))\n",
"vmax = max(0.01, np.max(np.abs(matrix)))\n",
"im = ax.imshow(matrix, cmap='RdBu_r', aspect='auto', vmin=-vmax, vmax=vmax)\n",
"ax.set_xticks(range(len(horizons)))\n",
"ax.set_xticklabels([f'{h}s' for h in horizons], fontsize=10)\n",
"ax.set_yticks(range(len(signal_names)))\n",
"ax.set_yticklabels(signal_names, fontsize=10)\n",
"\n",
"for i in range(len(signal_names)):\n",
" for j in range(len(horizons)):\n",
" val = matrix[i, j]\n",
" # Marcar significativos\n",
" r = results.filter((pl.col('signal') == signal_names[i]) & (pl.col('horizon_s') == horizons[j]))\n",
" if r.shape[0] > 0:\n",
" pv = r['p_value'][0]\n",
" star = '***' if pv < 0.001 else '**' if pv < 0.01 else '*' if pv < 0.05 else ''\n",
" else:\n",
" star = ''\n",
" color = 'white' if abs(val) > vmax * 0.6 else 'black'\n",
" ax.text(j, i, f'{val:.4f}\\n{star}', ha='center', va='center', fontsize=8, color=color)\n",
"\n",
"ax.set_title('Spearman ρ: señal vs retorno futuro\\n(rojo = predice subida, azul = predice bajada, *** = p<0.001)', fontsize=12)\n",
"ax.set_xlabel('Horizonte futuro')\n",
"plt.colorbar(im, label='ρ')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Top señales\n",
"print('\\nTop 15 señales por |ρ| (significativas p<0.01):')\n",
"top = results.filter(pl.col('p_value') < 0.01).with_columns(\n",
" pl.col('spearman_rho').abs().alias('abs_rho')\n",
").sort('abs_rho', descending=True).head(15)\n",
"print(top.select(['signal', 'horizon_s', 'spearman_rho', 'p_value']))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8b2334dd",
"metadata": {},
"source": [
"# 01 — Descarga Masiva de Datos Historicos (Binance)\n",
"\n",
"Dos metodos para obtener datos historicos:\n",
"\n",
"1. **REST API** (`/api/v3/klines`) — Paginado, max 1000 velas por request. Ideal para 7 dias.\n",
"2. **Data Vision** (`data.binance.vision`) — CSVs comprimidos diarios/mensuales. Ideal para meses/anos.\n",
"\n",
"**Rate limits REST:** 6000 weight/min, klines cuesta 2 weight.\n",
"\n",
"| Endpoint | Max/req | Weight | Uso |\n",
"|---|---|---|---|\n",
"| `/api/v3/klines` | 1000 velas | 2 | Candlesticks OHLCV |\n",
"| `/api/v3/aggTrades` | 1000 trades | 2 | Trades agregados (max 1h window) |\n",
"| `/api/v3/historicalTrades` | 1000 trades | 25 | Trades individuales (requiere API key) |\n",
"| `data.binance.vision` | Sin limite | 0 | CSVs bulk diarios/mensuales |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3fe45a0a",
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"import time\n",
"import datetime\n",
"import io\n",
"import zipfile\n",
"import pandas as pd\n",
"\n",
"BASE = \"https://api.binance.com\"\n",
"DATA_VISION = \"https://data.binance.vision\""
]
},
{
"cell_type": "markdown",
"id": "721f68a0",
"metadata": {},
"source": [
"## Metodo 1: REST API — Klines con paginacion automatica\n",
"\n",
"`GET /api/v3/klines` devuelve max 1000 velas. Paginamos con `startTime`/`endTime`.\n",
"\n",
"Para 7 dias de velas 1m: ceil(7*24*60/1000) = **11 requests** (22 weight total, trivial)."
]
}
],
"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": 5
}
@@ -0,0 +1,113 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "45f026b5",
"metadata": {},
"source": [
"# 02 — Streaming de Datos en Tiempo Real (Binance WebSocket)\n",
"\n",
"Binance ofrece WebSocket streams push-based para datos de mercado en tiempo real.\n",
"\n",
"**Base URLs:**\n",
"- Produccion: `wss://stream.binance.com:9443/ws/<stream>`\n",
"- Testnet: `wss://testnet.binance.vision/ws/<stream>`\n",
"- Multi-stream: `wss://stream.binance.com:9443/stream?streams=<s1>/<s2>`\n",
"\n",
"**Streams principales:**\n",
"| Stream | Nombre | Frecuencia |\n",
"|---|---|---|\n",
"| Trades individuales | `<symbol>@trade` | Cada trade |\n",
"| Klines en vivo | `<symbol>@kline_<interval>` | Cada cambio en vela |\n",
"| Mini ticker 24h | `<symbol>@miniTicker` | ~1s |\n",
"| Book ticker (best bid/ask) | `<symbol>@bookTicker` | Cada cambio |\n",
"| Todos los tickers | `!miniTicker@arr` | ~1s |\n",
"\n",
"**Reglas de conexion:**\n",
"- Ping cada 3 min desde Binance, pong requerido\n",
"- Desconexion automatica a las 24h — reconectar periodicamente\n",
"- Se puede suscribir/desuscribir dinamicamente via JSON"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6bb48c2e",
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"import json\n",
"import websockets\n",
"import pandas as pd\n",
"from datetime import datetime, timezone\n",
"from collections import deque\n",
"\n",
"WS_BASE = \"wss://stream.binance.com:9443/ws\""
]
},
{
"cell_type": "markdown",
"id": "2b2d9b2d",
"metadata": {},
"source": [
"## Stream de Trades individuales\n",
"\n",
"`<symbol>@trade` — recibe cada trade ejecutado en tiempo real.\n",
"\n",
"Campos clave:\n",
"- `p` = precio, `q` = cantidad\n",
"- `m` = true si el buyer es maker (es decir, fue un sell market order que impacto un bid)\n",
"- `t` = trade ID, `T` = timestamp"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aaed9f77",
"metadata": {},
"outputs": [],
"source": [
"async def stream_trades(symbol: str, max_trades: int = 100) -> list[dict]:\n",
" \"\"\"Captura N trades en tiempo real y retorna como lista.\"\"\"\n",
" url = f\"{WS_BASE}/{symbol.lower()}@trade\"\n",
" trades = []\n",
"\n",
" async with websockets.connect(url) as ws:\n",
" while len(trades) < max_trades:\n",
" msg = json.loads(await ws.recv())\n",
" trades.append({\n",
" \"trade_id\": msg[\"t\"],\n",
" \"time\": datetime.fromtimestamp(msg[\"T\"] / 1000, tz=timezone.utc),\n",
" \"price\": float(msg[\"p\"]),\n",
" \"qty\": float(msg[\"q\"]),\n",
" \"is_buyer_maker\": msg[\"m\"],\n",
" \"side\": \"SELL\" if msg[\"m\"] else \"BUY\",\n",
" })\n",
"\n",
" return trades"
]
}
],
"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": 5
}
@@ -0,0 +1,119 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c723d607",
"metadata": {},
"source": [
"# 03 — Libro de Ordenes en Tiempo Real (Binance)\n",
"\n",
"Dos enfoques para mantener un order book local:\n",
"\n",
"### Enfoque A: Partial Book Depth (simple)\n",
"Stream `<symbol>@depth<levels>@100ms` con levels = 5, 10, 20.\n",
"Envia snapshot completo del top N en cada update. Sin logica de sync.\n",
"\n",
"### Enfoque B: Diff Depth + REST Snapshot (completo)\n",
"1. Abrir stream `<symbol>@depth@100ms` (diffs incrementales)\n",
"2. Buffear eventos iniciales\n",
"3. Pedir snapshot REST: `GET /api/v3/depth?symbol=X&limit=1000`\n",
"4. Descartar eventos con `u <= lastUpdateId` del snapshot\n",
"5. Primer evento procesado debe tener `U <= lastUpdateId+1` AND `u >= lastUpdateId+1`\n",
"6. Aplicar: qty > 0 = update nivel, qty = 0 = eliminar nivel\n",
"7. Validar continuidad: cada evento `U` == anterior `u + 1`, si no, re-sync\n",
"\n",
"### Campos del depth update\n",
"```json\n",
"{\n",
" \"U\": 157, \"u\": 160,\n",
" \"b\": [[\"price\", \"qty\"], ...], \n",
" \"a\": [[\"price\", \"qty\"], ...] \n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23b19294",
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"import json\n",
"import requests\n",
"import websockets\n",
"from decimal import Decimal\n",
"from collections import deque\n",
"import pandas as pd\n",
"\n",
"BASE = \"https://api.binance.com\"\n",
"WS_BASE = \"wss://stream.binance.com:9443/ws\""
]
},
{
"cell_type": "markdown",
"id": "cff459c9",
"metadata": {},
"source": [
"## Enfoque A: Partial Book Depth (simple, sin sync)\n",
"\n",
"Stream `<symbol>@depth<levels>@100ms` — recibe snapshot completo del top N cada 100ms.\n",
"\n",
"Ideal para monitoreo rapido sin necesidad de mantener estado."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bf70712b",
"metadata": {},
"outputs": [],
"source": [
"async def stream_top_book(symbol: str, levels: int = 10, snapshots: int = 50) -> list[dict]:\n",
" \"\"\"Captura N snapshots del top del order book.\"\"\"\n",
" url = f\"{WS_BASE}/{symbol.lower()}@depth{levels}@100ms\"\n",
" results = []\n",
"\n",
" async with websockets.connect(url) as ws:\n",
" while len(results) < snapshots:\n",
" data = json.loads(await ws.recv())\n",
" best_bid = (float(data[\"bids\"][0][0]), float(data[\"bids\"][0][1]))\n",
" best_ask = (float(data[\"asks\"][0][0]), float(data[\"asks\"][0][1]))\n",
" spread = best_ask[0] - best_bid[0]\n",
" mid = (best_bid[0] + best_ask[0]) / 2\n",
" results.append({\n",
" \"time\": pd.Timestamp.now(tz=\"UTC\"),\n",
" \"best_bid\": best_bid[0], \"bid_qty\": best_bid[1],\n",
" \"best_ask\": best_ask[0], \"ask_qty\": best_ask[1],\n",
" \"spread\": spread, \"spread_bps\": (spread / mid) * 10000,\n",
" \"bids\": [(float(p), float(q)) for p, q in data[\"bids\"]],\n",
" \"asks\": [(float(p), float(q)) for p, q in data[\"asks\"]],\n",
" })\n",
"\n",
" return results"
]
}
],
"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": 5
}
@@ -0,0 +1,137 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c4c1bfe6",
"metadata": {},
"source": [
"# 04 — Trading Programatico (Binance API)\n",
"\n",
"Operaciones de trading via REST API con autenticacion HMAC-SHA256.\n",
"\n",
"**Autenticacion:** Cada request firmado necesita:\n",
"1. Header `X-MBX-APIKEY` con tu API key\n",
"2. Parametro `timestamp` (unix ms, dentro de 5000ms del server)\n",
"3. Parametro `signature` = HMAC-SHA256(query_string, secret_key)\n",
"\n",
"**Endpoints de trading (Spot):**\n",
"| Accion | Metodo | Endpoint | Weight |\n",
"|---|---|---|---|\n",
"| Crear orden | POST | `/api/v3/order` | 1 |\n",
"| Test orden | POST | `/api/v3/order/test` | 1 |\n",
"| Cancelar orden | DELETE | `/api/v3/order` | 1 |\n",
"| Cancelar todas | DELETE | `/api/v3/openOrders` | 1 |\n",
"| Ver orden | GET | `/api/v3/order` | 4 |\n",
"| Ordenes abiertas | GET | `/api/v3/openOrders` | 6 |\n",
"| Cuenta/balances | GET | `/api/v3/account` | 20 |\n",
"| Mis trades | GET | `/api/v3/myTrades` | 20 |\n",
"\n",
"**Tipos de orden:** MARKET, LIMIT (GTC/IOC/FOK), STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER\n",
"\n",
"**TESTNET:** `https://testnet.binance.vision` — mismo API, balances gratis, keys via GitHub login"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "599a54e7",
"metadata": {},
"outputs": [],
"source": [
"import hashlib\n",
"import hmac\n",
"import time\n",
"import math\n",
"import requests\n",
"import pandas as pd\n",
"\n",
"# --- CONFIGURACION ---\n",
"# Para testnet (seguro para pruebas):\n",
"BASE = \"https://testnet.binance.vision\"\n",
"# Para produccion (dinero real):\n",
"# BASE = \"https://api.binance.com\"\n",
"\n",
"# Crea tus keys en https://testnet.binance.vision (login con GitHub)\n",
"API_KEY = \"\" # <-- tu API key aqui\n",
"API_SECRET = \"\" # <-- tu secret aqui"
]
},
{
"cell_type": "markdown",
"id": "acc0b354",
"metadata": {},
"source": [
"## Firma HMAC-SHA256\n",
"\n",
"Toda request autenticada requiere `timestamp` + `signature`. La firma es HMAC del query string completo."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3fa09567",
"metadata": {},
"outputs": [],
"source": [
"def signed_request(method: str, endpoint: str, params: dict | None = None) -> dict:\n",
" \"\"\"Request firmado a Binance API (funciona con testnet y produccion).\"\"\"\n",
" if params is None:\n",
" params = {}\n",
"\n",
" params[\"timestamp\"] = int(time.time() * 1000)\n",
" params[\"recvWindow\"] = 5000\n",
"\n",
" query_string = \"&\".join(f\"{k}={v}\" for k, v in params.items())\n",
" signature = hmac.new(\n",
" API_SECRET.encode(), query_string.encode(), hashlib.sha256\n",
" ).hexdigest()\n",
" params[\"signature\"] = signature\n",
"\n",
" headers = {\"X-MBX-APIKEY\": API_KEY}\n",
"\n",
" if method == \"GET\":\n",
" resp = requests.get(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" elif method == \"POST\":\n",
" resp = requests.post(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" elif method == \"DELETE\":\n",
" resp = requests.delete(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" else:\n",
" raise ValueError(f\"Metodo no soportado: {method}\")\n",
"\n",
" resp.raise_for_status()\n",
" return resp.json()"
]
},
{
"cell_type": "markdown",
"id": "725c3d14",
"metadata": {},
"source": [
"## Consultar informacion del simbolo (filtros de ordenes)\n",
"\n",
"Antes de operar, hay que conocer los filtros: `LOT_SIZE` (min/max qty, step), `PRICE_FILTER` (tick size), `NOTIONAL` (min valor en quote)."
]
}
],
"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": 5
}
@@ -0,0 +1,127 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8b2334dd",
"metadata": {},
"source": [
"# 01 — Descarga Masiva de Datos Historicos (Binance)\n",
"\n",
"Dos metodos para obtener datos historicos:\n",
"\n",
"1. **REST API** (`/api/v3/klines`) — Paginado, max 1000 velas por request. Ideal para 7 dias.\n",
"2. **Data Vision** (`data.binance.vision`) — CSVs comprimidos diarios/mensuales. Ideal para meses/anos.\n",
"\n",
"**Rate limits REST:** 6000 weight/min, klines cuesta 2 weight.\n",
"\n",
"| Endpoint | Max/req | Weight | Uso |\n",
"|---|---|---|---|\n",
"| `/api/v3/klines` | 1000 velas | 2 | Candlesticks OHLCV |\n",
"| `/api/v3/aggTrades` | 1000 trades | 2 | Trades agregados (max 1h window) |\n",
"| `/api/v3/historicalTrades` | 1000 trades | 25 | Trades individuales (requiere API key) |\n",
"| `data.binance.vision` | Sin limite | 0 | CSVs bulk diarios/mensuales |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3fe45a0a",
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"import time\n",
"import datetime\n",
"import io\n",
"import zipfile\n",
"import pandas as pd\n",
"\n",
"BASE = \"https://api.binance.com\"\n",
"DATA_VISION = \"https://data.binance.vision\""
]
},
{
"cell_type": "markdown",
"id": "721f68a0",
"metadata": {},
"source": [
"## Metodo 1: REST API — Klines con paginacion automatica\n",
"\n",
"`GET /api/v3/klines` devuelve max 1000 velas. Paginamos con `startTime`/`endTime`.\n",
"\n",
"Para 7 dias de velas 1m: ceil(7*24*60/1000) = **11 requests** (22 weight total, trivial)."
]
},
{
"cell_type": "code",
"id": "24c183b7",
"source": "def parse_kline(k: list) -> dict:\n \"\"\"Parsea una vela raw de Binance a dict con tipos correctos.\"\"\"\n return {\n \"open_time\": pd.Timestamp(k[0], unit=\"ms\", tz=\"UTC\"),\n \"open\": float(k[1]),\n \"high\": float(k[2]),\n \"low\": float(k[3]),\n \"close\": float(k[4]),\n \"volume\": float(k[5]),\n \"close_time\": pd.Timestamp(k[6], unit=\"ms\", tz=\"UTC\"),\n \"quote_volume\": float(k[7]),\n \"trades\": int(k[8]),\n \"taker_buy_base_vol\": float(k[9]),\n \"taker_buy_quote_vol\": float(k[10]),\n }\n\n\ndef fetch_klines(symbol: str, interval: str, start_ms: int, end_ms: int, limit: int = 1000) -> list[dict]:\n \"\"\"Descarga klines con paginacion automatica.\"\"\"\n all_klines = []\n current = start_ms\n\n while current < end_ms:\n resp = requests.get(f\"{BASE}/api/v3/klines\", params={\n \"symbol\": symbol, \"interval\": interval,\n \"startTime\": current, \"endTime\": end_ms, \"limit\": limit,\n })\n resp.raise_for_status()\n data = resp.json()\n if not data:\n break\n\n all_klines.extend(parse_kline(k) for k in data)\n current = data[-1][6] + 1 # close_time + 1ms\n\n if len(data) < limit:\n break\n time.sleep(0.1)\n\n return all_klines",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "e3280dc5",
"source": "### Ejemplo: 7 dias de velas 1m para BTCUSDT",
"metadata": {}
},
{
"cell_type": "code",
"id": "b0b389b8",
"source": "now_ms = int(datetime.datetime.now(datetime.timezone.utc).timestamp() * 1000)\nseven_days_ago = now_ms - 7 * 24 * 60 * 60 * 1000\n\nklines = fetch_klines(\"BTCUSDT\", \"1m\", seven_days_ago, now_ms)\ndf_klines = pd.DataFrame(klines)\nprint(f\"Descargadas {len(df_klines)} velas de 1m ({len(df_klines)/1440:.1f} dias)\")\ndf_klines.head()",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "e52d66d1",
"source": "## Metodo 2: Data Vision — CSVs bulk (meses/anos de datos)\n\n`data.binance.vision` publica CSVs comprimidos diarios y mensuales. Sin API key, sin rate limits.\n\nIdeal para backtesting con historicos largos. Disponible para spot y futures.",
"metadata": {}
},
{
"cell_type": "code",
"id": "a42f986c",
"source": "def download_data_vision(symbol: str, data_type: str, date_str: str,\n interval: str = \"1m\", market: str = \"spot\") -> str | None:\n \"\"\"\n Descarga CSV diario/mensual desde data.binance.vision.\n date_str: \"2024-01-15\" (diario) o \"2024-01\" (mensual)\n data_type: \"klines\", \"trades\", \"aggTrades\"\n \"\"\"\n granularity = \"daily\" if len(date_str) > 7 else \"monthly\"\n\n if data_type == \"klines\":\n path = f\"data/{market}/{granularity}/{data_type}/{symbol}/{interval}/{symbol}-{interval}-{date_str}.zip\"\n else:\n path = f\"data/{market}/{granularity}/{data_type}/{symbol}/{symbol}-{data_type}-{date_str}.zip\"\n\n resp = requests.get(f\"{DATA_VISION}/{path}\")\n if resp.status_code == 404:\n return None\n resp.raise_for_status()\n\n with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:\n return zf.read(zf.namelist()[0]).decode(\"utf-8\")\n\n\ndef download_klines_bulk(symbol: str, days: int = 7, interval: str = \"1m\") -> pd.DataFrame:\n \"\"\"Descarga N dias de klines desde Data Vision y retorna DataFrame.\"\"\"\n from datetime import date, timedelta\n\n cols = [\"open_time\", \"open\", \"high\", \"low\", \"close\", \"volume\",\n \"close_time\", \"quote_volume\", \"trades\", \"taker_buy_base_vol\",\n \"taker_buy_quote_vol\", \"ignore\"]\n all_rows = []\n today = date.today()\n\n for i in range(2, days + 2): # empezar 2 dias atras (hoy puede no estar disponible)\n d = today - timedelta(days=i)\n csv_text = download_data_vision(symbol, \"klines\", d.isoformat(), interval)\n if csv_text:\n for line in csv_text.strip().split(\"\\n\"):\n all_rows.append(line.split(\",\"))\n\n df = pd.DataFrame(all_rows, columns=cols)\n for col in [\"open\", \"high\", \"low\", \"close\", \"volume\", \"quote_volume\",\n \"taker_buy_base_vol\", \"taker_buy_quote_vol\"]:\n df[col] = df[col].astype(float)\n df[\"open_time\"] = pd.to_datetime(df[\"open_time\"].astype(int), unit=\"ms\", utc=True)\n df[\"close_time\"] = pd.to_datetime(df[\"close_time\"].astype(int), unit=\"ms\", utc=True)\n df[\"trades\"] = df[\"trades\"].astype(int)\n return df.drop(columns=[\"ignore\"]).sort_values(\"open_time\").reset_index(drop=True)",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "89438f51",
"source": "### Ejemplo: 7 dias de BTCUSDT via Data Vision (mas rapido, sin rate limits)",
"metadata": {}
},
{
"cell_type": "code",
"id": "5368c4b7",
"source": "df_bulk = download_klines_bulk(\"BTCUSDT\", days=7, interval=\"1m\")\nprint(f\"Data Vision: {len(df_bulk)} velas ({len(df_bulk)/1440:.1f} dias)\")\nprint(f\"Rango: {df_bulk['open_time'].min()} -> {df_bulk['open_time'].max()}\")\nprint(f\"Precio: {df_bulk['close'].min():.2f} - {df_bulk['close'].max():.2f}\")\ndf_bulk.describe()",
"metadata": {},
"execution_count": null,
"outputs": []
}
],
"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": 5
}
@@ -0,0 +1,169 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "45f026b5",
"metadata": {},
"source": [
"# 02 — Streaming de Datos en Tiempo Real (Binance WebSocket)\n",
"\n",
"Binance ofrece WebSocket streams push-based para datos de mercado en tiempo real.\n",
"\n",
"**Base URLs:**\n",
"- Produccion: `wss://stream.binance.com:9443/ws/<stream>`\n",
"- Testnet: `wss://testnet.binance.vision/ws/<stream>`\n",
"- Multi-stream: `wss://stream.binance.com:9443/stream?streams=<s1>/<s2>`\n",
"\n",
"**Streams principales:**\n",
"| Stream | Nombre | Frecuencia |\n",
"|---|---|---|\n",
"| Trades individuales | `<symbol>@trade` | Cada trade |\n",
"| Klines en vivo | `<symbol>@kline_<interval>` | Cada cambio en vela |\n",
"| Mini ticker 24h | `<symbol>@miniTicker` | ~1s |\n",
"| Book ticker (best bid/ask) | `<symbol>@bookTicker` | Cada cambio |\n",
"| Todos los tickers | `!miniTicker@arr` | ~1s |\n",
"\n",
"**Reglas de conexion:**\n",
"- Ping cada 3 min desde Binance, pong requerido\n",
"- Desconexion automatica a las 24h — reconectar periodicamente\n",
"- Se puede suscribir/desuscribir dinamicamente via JSON"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6bb48c2e",
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"import json\n",
"import websockets\n",
"import pandas as pd\n",
"from datetime import datetime, timezone\n",
"from collections import deque\n",
"\n",
"WS_BASE = \"wss://stream.binance.com:9443/ws\""
]
},
{
"cell_type": "markdown",
"id": "2b2d9b2d",
"metadata": {},
"source": [
"## Stream de Trades individuales\n",
"\n",
"`<symbol>@trade` — recibe cada trade ejecutado en tiempo real.\n",
"\n",
"Campos clave:\n",
"- `p` = precio, `q` = cantidad\n",
"- `m` = true si el buyer es maker (es decir, fue un sell market order que impacto un bid)\n",
"- `t` = trade ID, `T` = timestamp"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aaed9f77",
"metadata": {},
"outputs": [],
"source": [
"async def stream_trades(symbol: str, max_trades: int = 100) -> list[dict]:\n",
" \"\"\"Captura N trades en tiempo real y retorna como lista.\"\"\"\n",
" url = f\"{WS_BASE}/{symbol.lower()}@trade\"\n",
" trades = []\n",
"\n",
" async with websockets.connect(url) as ws:\n",
" while len(trades) < max_trades:\n",
" msg = json.loads(await ws.recv())\n",
" trades.append({\n",
" \"trade_id\": msg[\"t\"],\n",
" \"time\": datetime.fromtimestamp(msg[\"T\"] / 1000, tz=timezone.utc),\n",
" \"price\": float(msg[\"p\"]),\n",
" \"qty\": float(msg[\"q\"]),\n",
" \"is_buyer_maker\": msg[\"m\"],\n",
" \"side\": \"SELL\" if msg[\"m\"] else \"BUY\",\n",
" })\n",
"\n",
" return trades"
]
},
{
"cell_type": "markdown",
"id": "5dc19bb4",
"source": "### Ejemplo: Capturar 100 trades de BTCUSDT",
"metadata": {}
},
{
"cell_type": "code",
"id": "2ad6bc1c",
"source": "trades = await stream_trades(\"BTCUSDT\", max_trades=100)\ndf_trades = pd.DataFrame(trades)\nprint(f\"Capturados {len(df_trades)} trades\")\nprint(f\"Rango de tiempo: {df_trades['time'].min()} -> {df_trades['time'].max()}\")\nprint(f\"Precio: {df_trades['price'].min():.2f} - {df_trades['price'].max():.2f}\")\nprint(f\"BUY: {(df_trades['side'] == 'BUY').sum()}, SELL: {(df_trades['side'] == 'SELL').sum()}\")\ndf_trades.head(10)",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "65a65f5b",
"source": "## Stream de Klines en vivo\n\n`<symbol>@kline_<interval>` — recibe actualizaciones de la vela actual y velas cerradas.\n\nCampo clave: `k.x` = true cuando la vela esta cerrada (final). Mientras `x=false`, los valores OHLCV son parciales.",
"metadata": {}
},
{
"cell_type": "code",
"id": "99c11390",
"source": "async def stream_klines(symbol: str, interval: str = \"1m\", max_closed: int = 5) -> list[dict]:\n \"\"\"Captura N velas CERRADAS en tiempo real (espera a que x=true).\"\"\"\n url = f\"{WS_BASE}/{symbol.lower()}@kline_{interval}\"\n closed_candles = []\n current = None\n\n async with websockets.connect(url) as ws:\n while len(closed_candles) < max_closed:\n msg = json.loads(await ws.recv())\n k = msg[\"k\"]\n current = {\n \"open_time\": pd.Timestamp(k[\"t\"], unit=\"ms\", tz=\"UTC\"),\n \"open\": float(k[\"o\"]),\n \"high\": float(k[\"h\"]),\n \"low\": float(k[\"l\"]),\n \"close\": float(k[\"c\"]),\n \"volume\": float(k[\"v\"]),\n \"trades\": k[\"n\"],\n \"is_closed\": k[\"x\"],\n }\n if k[\"x\"]: # vela cerrada\n closed_candles.append(current)\n print(f\"Vela cerrada #{len(closed_candles)}: {current['close']:.2f} | vol={current['volume']:.4f} | trades={current['trades']}\")\n\n return closed_candles",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "ddab999b",
"source": "### Ejemplo: Capturar 3 velas cerradas de 1m de BTCUSDT\n\n**Nota:** Esto tarda hasta 3 minutos esperando que se cierren las velas.",
"metadata": {}
},
{
"cell_type": "code",
"id": "5e4b017f",
"source": "candles = await stream_klines(\"BTCUSDT\", interval=\"1m\", max_closed=3)\ndf_candles = pd.DataFrame(candles)\ndf_candles",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "b067572a",
"source": "## Multi-stream: multiples feeds en una conexion\n\nCombinar trades + klines + book ticker de varios pares en un solo WebSocket.",
"metadata": {}
},
{
"cell_type": "code",
"id": "b4763056",
"source": "async def stream_multiple(streams: list[str], max_messages: int = 100) -> list[dict]:\n \"\"\"Captura N mensajes de multiples streams combinados.\"\"\"\n combined = \"/\".join(streams)\n url = f\"wss://stream.binance.com:9443/stream?streams={combined}\"\n messages = []\n\n async with websockets.connect(url) as ws:\n while len(messages) < max_messages:\n raw = json.loads(await ws.recv())\n messages.append({\n \"stream\": raw[\"stream\"],\n \"data\": raw[\"data\"],\n })\n\n return messages\n\n\n# Ejemplo: trades de BTC + ETH + book ticker de BTC\nmulti = await stream_multiple([\n \"btcusdt@trade\",\n \"ethusdt@trade\",\n \"btcusdt@bookTicker\",\n], max_messages=50)\n\n# Contar mensajes por stream\nfrom collections import Counter\ncounts = Counter(m[\"stream\"] for m in multi)\nprint(\"Mensajes por stream:\")\nfor stream, count in counts.most_common():\n print(f\" {stream}: {count}\")",
"metadata": {},
"execution_count": null,
"outputs": []
}
],
"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": 5
}
+175
View File
@@ -0,0 +1,175 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c723d607",
"metadata": {},
"source": [
"# 03 — Libro de Ordenes en Tiempo Real (Binance)\n",
"\n",
"Dos enfoques para mantener un order book local:\n",
"\n",
"### Enfoque A: Partial Book Depth (simple)\n",
"Stream `<symbol>@depth<levels>@100ms` con levels = 5, 10, 20.\n",
"Envia snapshot completo del top N en cada update. Sin logica de sync.\n",
"\n",
"### Enfoque B: Diff Depth + REST Snapshot (completo)\n",
"1. Abrir stream `<symbol>@depth@100ms` (diffs incrementales)\n",
"2. Buffear eventos iniciales\n",
"3. Pedir snapshot REST: `GET /api/v3/depth?symbol=X&limit=1000`\n",
"4. Descartar eventos con `u <= lastUpdateId` del snapshot\n",
"5. Primer evento procesado debe tener `U <= lastUpdateId+1` AND `u >= lastUpdateId+1`\n",
"6. Aplicar: qty > 0 = update nivel, qty = 0 = eliminar nivel\n",
"7. Validar continuidad: cada evento `U` == anterior `u + 1`, si no, re-sync\n",
"\n",
"### Campos del depth update\n",
"```json\n",
"{\n",
" \"U\": 157, \"u\": 160,\n",
" \"b\": [[\"price\", \"qty\"], ...], \n",
" \"a\": [[\"price\", \"qty\"], ...] \n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23b19294",
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"import json\n",
"import requests\n",
"import websockets\n",
"from decimal import Decimal\n",
"from collections import deque\n",
"import pandas as pd\n",
"\n",
"BASE = \"https://api.binance.com\"\n",
"WS_BASE = \"wss://stream.binance.com:9443/ws\""
]
},
{
"cell_type": "markdown",
"id": "cff459c9",
"metadata": {},
"source": [
"## Enfoque A: Partial Book Depth (simple, sin sync)\n",
"\n",
"Stream `<symbol>@depth<levels>@100ms` — recibe snapshot completo del top N cada 100ms.\n",
"\n",
"Ideal para monitoreo rapido sin necesidad de mantener estado."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bf70712b",
"metadata": {},
"outputs": [],
"source": [
"async def stream_top_book(symbol: str, levels: int = 10, snapshots: int = 50) -> list[dict]:\n",
" \"\"\"Captura N snapshots del top del order book.\"\"\"\n",
" url = f\"{WS_BASE}/{symbol.lower()}@depth{levels}@100ms\"\n",
" results = []\n",
"\n",
" async with websockets.connect(url) as ws:\n",
" while len(results) < snapshots:\n",
" data = json.loads(await ws.recv())\n",
" best_bid = (float(data[\"bids\"][0][0]), float(data[\"bids\"][0][1]))\n",
" best_ask = (float(data[\"asks\"][0][0]), float(data[\"asks\"][0][1]))\n",
" spread = best_ask[0] - best_bid[0]\n",
" mid = (best_bid[0] + best_ask[0]) / 2\n",
" results.append({\n",
" \"time\": pd.Timestamp.now(tz=\"UTC\"),\n",
" \"best_bid\": best_bid[0], \"bid_qty\": best_bid[1],\n",
" \"best_ask\": best_ask[0], \"ask_qty\": best_ask[1],\n",
" \"spread\": spread, \"spread_bps\": (spread / mid) * 10000,\n",
" \"bids\": [(float(p), float(q)) for p, q in data[\"bids\"]],\n",
" \"asks\": [(float(p), float(q)) for p, q in data[\"asks\"]],\n",
" })\n",
"\n",
" return results"
]
},
{
"cell_type": "markdown",
"id": "99c8d974",
"source": "### Ejemplo: Capturar 50 snapshots del top-10 del book de BTCUSDT",
"metadata": {}
},
{
"cell_type": "code",
"id": "855fc378",
"source": "snapshots = await stream_top_book(\"BTCUSDT\", levels=10, snapshots=50)\ndf_book = pd.DataFrame([{k: v for k, v in s.items() if k not in (\"bids\", \"asks\")} for s in snapshots])\nprint(f\"Capturados {len(df_book)} snapshots del order book\")\nprint(f\"Spread medio: {df_book['spread'].mean():.2f} USD ({df_book['spread_bps'].mean():.2f} bps)\")\nprint(f\"Spread min: {df_book['spread'].min():.2f}, max: {df_book['spread'].max():.2f}\")\ndf_book.head(10)",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "271ea442",
"source": "## Enfoque B: Order Book completo con diff depth + REST sync\n\nMantiene un order book local completo sincronizado via WebSocket diffs.\nMas complejo pero da acceso a todos los niveles de precio.",
"metadata": {}
},
{
"cell_type": "code",
"id": "79701eff",
"source": "class LocalOrderBook:\n \"\"\"Order book local sincronizado con Binance via WebSocket diffs.\"\"\"\n\n def __init__(self, symbol: str, depth_limit: int = 1000):\n self.symbol = symbol.upper()\n self.depth_limit = depth_limit\n self.bids: dict[Decimal, Decimal] = {} # price -> qty\n self.asks: dict[Decimal, Decimal] = {}\n self.last_update_id: int = 0\n self._synced = False\n\n def _get_snapshot(self) -> dict:\n resp = requests.get(f\"{BASE}/api/v3/depth\",\n params={\"symbol\": self.symbol, \"limit\": self.depth_limit})\n resp.raise_for_status()\n return resp.json()\n\n def _apply_update(self, sides: list[tuple[str, dict]]):\n for price_str, qty_str in sides:\n price, qty = Decimal(price_str), Decimal(qty_str)\n return price, qty\n\n def apply_event(self, event: dict):\n for price_str, qty_str in event.get(\"b\", []):\n p, q = Decimal(price_str), Decimal(qty_str)\n if q == 0:\n self.bids.pop(p, None)\n else:\n self.bids[p] = q\n for price_str, qty_str in event.get(\"a\", []):\n p, q = Decimal(price_str), Decimal(qty_str)\n if q == 0:\n self.asks.pop(p, None)\n else:\n self.asks[p] = q\n self.last_update_id = event[\"u\"]\n\n def best_bid(self) -> tuple[float, float] | None:\n if not self.bids:\n return None\n p = max(self.bids)\n return (float(p), float(self.bids[p]))\n\n def best_ask(self) -> tuple[float, float] | None:\n if not self.asks:\n return None\n p = min(self.asks)\n return (float(p), float(self.asks[p]))\n\n def spread(self) -> float | None:\n b, a = self.best_bid(), self.best_ask()\n return a[0] - b[0] if b and a else None\n\n def top_n(self, n: int = 5) -> dict:\n return {\n \"bids\": [(float(p), float(q)) for p, q in sorted(self.bids.items(), reverse=True)[:n]],\n \"asks\": [(float(p), float(q)) for p, q in sorted(self.asks.items())[:n]],\n }\n\n async def run(self, duration_seconds: int = 30):\n \"\"\"Sincroniza y mantiene el book durante N segundos.\"\"\"\n import time as _time\n url = f\"{WS_BASE}/{self.symbol.lower()}@depth@100ms\"\n buffer = []\n start = _time.time()\n\n async with websockets.connect(url) as ws:\n while _time.time() - start < duration_seconds:\n msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=5))\n\n if not self._synced:\n buffer.append(msg)\n if len(buffer) >= 3:\n snap = self._get_snapshot()\n # Inicializar desde snapshot\n self.bids = {Decimal(p): Decimal(q) for p, q in snap[\"bids\"]}\n self.asks = {Decimal(p): Decimal(q) for p, q in snap[\"asks\"]}\n self.last_update_id = snap[\"lastUpdateId\"]\n\n # Procesar buffer\n for evt in buffer:\n if evt[\"u\"] <= self.last_update_id:\n continue\n if not self._synced and evt[\"U\"] <= self.last_update_id + 1:\n self._synced = True\n if self._synced:\n self.apply_event(evt)\n buffer.clear()\n if self._synced:\n print(f\"Book sincronizado! Levels: {len(self.bids)} bids, {len(self.asks)} asks\")\n else:\n if msg[\"U\"] != self.last_update_id + 1:\n print(\"Gap detectado, re-sync...\")\n self._synced = False\n buffer = [msg]\n continue\n self.apply_event(msg)",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "7370d164",
"source": "### Ejemplo: Correr el order book 30 segundos y ver el estado",
"metadata": {}
},
{
"cell_type": "code",
"id": "ae407462",
"source": "book = LocalOrderBook(\"BTCUSDT\")\nawait book.run(duration_seconds=30)\n\nprint(f\"\\nBest bid: {book.best_bid()}\")\nprint(f\"Best ask: {book.best_ask()}\")\nprint(f\"Spread: {book.spread():.2f} USD\")\nprint(f\"\\nTop 5:\")\ntop = book.top_n(5)\nprint(\"BIDS:\", top[\"bids\"])\nprint(\"ASKS:\", top[\"asks\"])",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "c11ab0fb",
"source": "## Snapshot REST directo (alternativa simple)\n\nPara consultas puntuales sin mantener estado, un GET directo al depth endpoint.",
"metadata": {}
},
{
"cell_type": "code",
"id": "9a31a0ba",
"source": "def get_depth_snapshot(symbol: str, limit: int = 20) -> dict:\n \"\"\"Snapshot directo del order book via REST.\"\"\"\n resp = requests.get(f\"{BASE}/api/v3/depth\", params={\"symbol\": symbol, \"limit\": limit})\n resp.raise_for_status()\n data = resp.json()\n return {\n \"bids\": [(float(p), float(q)) for p, q in data[\"bids\"]],\n \"asks\": [(float(p), float(q)) for p, q in data[\"asks\"]],\n \"last_update_id\": data[\"lastUpdateId\"],\n }\n\nsnap = get_depth_snapshot(\"BTCUSDT\", limit=10)\nprint(f\"Top 10 bids y asks (update_id={snap['last_update_id']}):\")\nprint(\"\\nBIDS (compra):\")\nfor p, q in snap[\"bids\"]:\n print(f\" {p:>12.2f} | {q:.6f}\")\nprint(\"\\nASKS (venta):\")\nfor p, q in snap[\"asks\"]:\n print(f\" {p:>12.2f} | {q:.6f}\")",
"metadata": {},
"execution_count": null,
"outputs": []
}
],
"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": 5
}
@@ -0,0 +1,205 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c4c1bfe6",
"metadata": {},
"source": [
"# 04 — Trading Programatico (Binance API)\n",
"\n",
"Operaciones de trading via REST API con autenticacion HMAC-SHA256.\n",
"\n",
"**Autenticacion:** Cada request firmado necesita:\n",
"1. Header `X-MBX-APIKEY` con tu API key\n",
"2. Parametro `timestamp` (unix ms, dentro de 5000ms del server)\n",
"3. Parametro `signature` = HMAC-SHA256(query_string, secret_key)\n",
"\n",
"**Endpoints de trading (Spot):**\n",
"| Accion | Metodo | Endpoint | Weight |\n",
"|---|---|---|---|\n",
"| Crear orden | POST | `/api/v3/order` | 1 |\n",
"| Test orden | POST | `/api/v3/order/test` | 1 |\n",
"| Cancelar orden | DELETE | `/api/v3/order` | 1 |\n",
"| Cancelar todas | DELETE | `/api/v3/openOrders` | 1 |\n",
"| Ver orden | GET | `/api/v3/order` | 4 |\n",
"| Ordenes abiertas | GET | `/api/v3/openOrders` | 6 |\n",
"| Cuenta/balances | GET | `/api/v3/account` | 20 |\n",
"| Mis trades | GET | `/api/v3/myTrades` | 20 |\n",
"\n",
"**Tipos de orden:** MARKET, LIMIT (GTC/IOC/FOK), STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER\n",
"\n",
"**TESTNET:** `https://testnet.binance.vision` — mismo API, balances gratis, keys via GitHub login"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "599a54e7",
"metadata": {},
"outputs": [],
"source": [
"import hashlib\n",
"import hmac\n",
"import time\n",
"import math\n",
"import requests\n",
"import pandas as pd\n",
"\n",
"# --- CONFIGURACION ---\n",
"# Para testnet (seguro para pruebas):\n",
"BASE = \"https://testnet.binance.vision\"\n",
"# Para produccion (dinero real):\n",
"# BASE = \"https://api.binance.com\"\n",
"\n",
"# Crea tus keys en https://testnet.binance.vision (login con GitHub)\n",
"API_KEY = \"\" # <-- tu API key aqui\n",
"API_SECRET = \"\" # <-- tu secret aqui"
]
},
{
"cell_type": "markdown",
"id": "acc0b354",
"metadata": {},
"source": [
"## Firma HMAC-SHA256\n",
"\n",
"Toda request autenticada requiere `timestamp` + `signature`. La firma es HMAC del query string completo."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3fa09567",
"metadata": {},
"outputs": [],
"source": [
"def signed_request(method: str, endpoint: str, params: dict | None = None) -> dict:\n",
" \"\"\"Request firmado a Binance API (funciona con testnet y produccion).\"\"\"\n",
" if params is None:\n",
" params = {}\n",
"\n",
" params[\"timestamp\"] = int(time.time() * 1000)\n",
" params[\"recvWindow\"] = 5000\n",
"\n",
" query_string = \"&\".join(f\"{k}={v}\" for k, v in params.items())\n",
" signature = hmac.new(\n",
" API_SECRET.encode(), query_string.encode(), hashlib.sha256\n",
" ).hexdigest()\n",
" params[\"signature\"] = signature\n",
"\n",
" headers = {\"X-MBX-APIKEY\": API_KEY}\n",
"\n",
" if method == \"GET\":\n",
" resp = requests.get(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" elif method == \"POST\":\n",
" resp = requests.post(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" elif method == \"DELETE\":\n",
" resp = requests.delete(f\"{BASE}{endpoint}\", params=params, headers=headers)\n",
" else:\n",
" raise ValueError(f\"Metodo no soportado: {method}\")\n",
"\n",
" resp.raise_for_status()\n",
" return resp.json()"
]
},
{
"cell_type": "markdown",
"id": "725c3d14",
"metadata": {},
"source": [
"## Consultar informacion del simbolo (filtros de ordenes)\n",
"\n",
"Antes de operar, hay que conocer los filtros: `LOT_SIZE` (min/max qty, step), `PRICE_FILTER` (tick size), `NOTIONAL` (min valor en quote)."
]
},
{
"cell_type": "code",
"id": "f7dd53ba",
"source": "def get_symbol_filters(symbol: str) -> dict:\n \"\"\"Obtiene filtros de trading para un simbolo (no requiere auth).\"\"\"\n resp = requests.get(f\"{BASE}/api/v3/exchangeInfo\", params={\"symbol\": symbol})\n resp.raise_for_status()\n info = resp.json()[\"symbols\"][0]\n filters = {f[\"filterType\"]: f for f in info[\"filters\"]}\n return {\n \"status\": info[\"status\"],\n \"base\": info[\"baseAsset\"],\n \"quote\": info[\"quoteAsset\"],\n \"lot_size\": {\n \"min_qty\": float(filters[\"LOT_SIZE\"][\"minQty\"]),\n \"max_qty\": float(filters[\"LOT_SIZE\"][\"maxQty\"]),\n \"step\": float(filters[\"LOT_SIZE\"][\"stepSize\"]),\n },\n \"price_filter\": {\n \"min_price\": float(filters[\"PRICE_FILTER\"][\"minPrice\"]),\n \"max_price\": float(filters[\"PRICE_FILTER\"][\"maxPrice\"]),\n \"tick_size\": float(filters[\"PRICE_FILTER\"][\"tickSize\"]),\n },\n \"notional\": {\n \"min_notional\": float(filters[\"NOTIONAL\"][\"minNotional\"]),\n },\n }\n\n\ndef round_step(value: float, step: float) -> float:\n \"\"\"Redondea hacia abajo al step size mas cercano.\"\"\"\n precision = len(str(step).rstrip('0').split('.')[-1]) if '.' in str(step) else 0\n return math.floor(value / step) * step\n\n\n# Ejemplo: ver filtros de BTCUSDT\nfilters = get_symbol_filters(\"BTCUSDT\")\nfilters",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "53578416",
"source": "## Operaciones de Trading\n\nFunciones para market buy/sell, limit orders, stop-loss, y gestion de ordenes.\n\n**IMPORTANTE:** Estas funciones operan contra el endpoint configurado en `BASE`. Asegurate de usar testnet mientras pruebas.",
"metadata": {}
},
{
"cell_type": "code",
"id": "d4d33fb0",
"source": "def get_account() -> dict:\n \"\"\"Obtiene info de cuenta y balances no-cero.\"\"\"\n account = signed_request(\"GET\", \"/api/v3/account\")\n balances = {b[\"asset\"]: {\"free\": float(b[\"free\"]), \"locked\": float(b[\"locked\"])}\n for b in account[\"balances\"] if float(b[\"free\"]) > 0 or float(b[\"locked\"]) > 0}\n return balances\n\n\ndef market_buy(symbol: str, quote_qty: float) -> dict:\n \"\"\"Market buy: gasta X cantidad de quote asset (ej: 100 USDT).\"\"\"\n return signed_request(\"POST\", \"/api/v3/order\", {\n \"symbol\": symbol, \"side\": \"BUY\", \"type\": \"MARKET\",\n \"quoteOrderQty\": quote_qty, \"newOrderRespType\": \"FULL\",\n })\n\n\ndef market_sell(symbol: str, quantity: float) -> dict:\n \"\"\"Market sell: vende X cantidad de base asset (ej: 0.001 BTC).\"\"\"\n return signed_request(\"POST\", \"/api/v3/order\", {\n \"symbol\": symbol, \"side\": \"SELL\", \"type\": \"MARKET\",\n \"quantity\": quantity, \"newOrderRespType\": \"FULL\",\n })\n\n\ndef limit_buy(symbol: str, quantity: float, price: float) -> dict:\n \"\"\"Limit buy: compra X a precio Y o mejor.\"\"\"\n return signed_request(\"POST\", \"/api/v3/order\", {\n \"symbol\": symbol, \"side\": \"BUY\", \"type\": \"LIMIT\",\n \"timeInForce\": \"GTC\", \"quantity\": quantity, \"price\": price,\n \"newOrderRespType\": \"FULL\",\n })\n\n\ndef limit_sell(symbol: str, quantity: float, price: float) -> dict:\n \"\"\"Limit sell: vende X a precio Y o mejor.\"\"\"\n return signed_request(\"POST\", \"/api/v3/order\", {\n \"symbol\": symbol, \"side\": \"SELL\", \"type\": \"LIMIT\",\n \"timeInForce\": \"GTC\", \"quantity\": quantity, \"price\": price,\n \"newOrderRespType\": \"FULL\",\n })\n\n\ndef stop_loss_sell(symbol: str, quantity: float, stop_price: float, limit_price: float) -> dict:\n \"\"\"Stop-loss limit: se activa en stop_price, coloca limit en limit_price.\"\"\"\n return signed_request(\"POST\", \"/api/v3/order\", {\n \"symbol\": symbol, \"side\": \"SELL\", \"type\": \"STOP_LOSS_LIMIT\",\n \"timeInForce\": \"GTC\", \"quantity\": quantity,\n \"stopPrice\": stop_price, \"price\": limit_price,\n \"newOrderRespType\": \"FULL\",\n })\n\n\ndef cancel_order(symbol: str, order_id: int) -> dict:\n \"\"\"Cancela una orden especifica.\"\"\"\n return signed_request(\"DELETE\", \"/api/v3/order\", {\n \"symbol\": symbol, \"orderId\": order_id,\n })\n\n\ndef cancel_all_orders(symbol: str) -> dict:\n \"\"\"Cancela todas las ordenes abiertas de un simbolo.\"\"\"\n return signed_request(\"DELETE\", \"/api/v3/openOrders\", {\"symbol\": symbol})\n\n\ndef get_open_orders(symbol: str) -> list[dict]:\n \"\"\"Lista ordenes abiertas.\"\"\"\n return signed_request(\"GET\", \"/api/v3/openOrders\", {\"symbol\": symbol})\n\n\ndef get_my_trades(symbol: str, limit: int = 50) -> list[dict]:\n \"\"\"Historial de mis trades.\"\"\"\n return signed_request(\"GET\", \"/api/v3/myTrades\", {\"symbol\": symbol, \"limit\": limit})",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "9adf7577",
"source": "## Ejemplos de uso (TESTNET)\n\nConfigurar `API_KEY` y `API_SECRET` arriba antes de ejecutar estas celdas.\nKeys de testnet se crean en `testnet.binance.vision` con login de GitHub.",
"metadata": {}
},
{
"cell_type": "code",
"id": "ce167a84",
"source": "# 1. Ver balances de la cuenta testnet\nbalances = get_account()\nprint(\"Balances:\")\nfor asset, vals in sorted(balances.items()):\n print(f\" {asset}: free={vals['free']}, locked={vals['locked']}\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"id": "b4bc87f5",
"source": "# 2. Market buy — comprar $10 de BTC en testnet\norder = market_buy(\"BTCUSDT\", 10)\nprint(f\"Order ID: {order['orderId']}\")\nprint(f\"Status: {order['status']}\")\nprint(f\"Fills: {order.get('fills', [])}\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"id": "9ae35b32",
"source": "# 3. Limit buy — comprar 0.001 BTC a un precio bajo\n# Primero obtener precio actual para poner una limit por debajo\ncurrent_price = float(requests.get(f\"{BASE}/api/v3/ticker/price\",\n params={\"symbol\": \"BTCUSDT\"}).json()[\"price\"])\nlimit_price = round_step(current_price * 0.95, filters[\"price_filter\"][\"tick_size\"]) # 5% debajo\nqty = round_step(0.001, filters[\"lot_size\"][\"step\"])\n\nprint(f\"Precio actual: {current_price:.2f}\")\nprint(f\"Limit buy a: {limit_price:.2f} ({qty} BTC)\")\norder = limit_buy(\"BTCUSDT\", qty, limit_price)\nprint(f\"Order ID: {order['orderId']}, Status: {order['status']}\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"id": "f3b60aa0",
"source": "# 4. Ver ordenes abiertas y cancelar\nopen_orders = get_open_orders(\"BTCUSDT\")\nprint(f\"Ordenes abiertas: {len(open_orders)}\")\nfor o in open_orders:\n print(f\" ID={o['orderId']} {o['side']} {o['type']} qty={o['origQty']} price={o['price']} status={o['status']}\")\n\n# Cancelar todas\nif open_orders:\n cancel_all_orders(\"BTCUSDT\")\n print(\"Todas las ordenes canceladas\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"id": "a61bf0e9",
"source": "# 5. Historial de trades ejecutados\nmy_trades = get_my_trades(\"BTCUSDT\", limit=10)\nif my_trades:\n df_my = pd.DataFrame(my_trades)\n df_my[\"time\"] = pd.to_datetime(df_my[\"time\"], unit=\"ms\", utc=True)\n df_my[\"price\"] = df_my[\"price\"].astype(float)\n df_my[\"qty\"] = df_my[\"qty\"].astype(float)\n df_my[\"quoteQty\"] = df_my[\"quoteQty\"].astype(float)\n print(f\"Ultimos {len(df_my)} trades:\")\n display(df_my[[\"time\", \"price\", \"qty\", \"quoteQty\", \"isBuyer\", \"isMaker\"]])\nelse:\n print(\"Sin trades ejecutados aun\")",
"metadata": {},
"execution_count": null,
"outputs": []
}
],
"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": 5
}
+22
View File
@@ -0,0 +1,22 @@
[project]
name = "estudio-mercados"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"aiohttp>=3.13.5",
"ccxt>=4.5.46",
"jupyter>=1.1.1",
"jupyter-collaboration>=4.3.0",
"jupyter-mcp-server>=0.4.0",
"jupyterlab>=4.5.6",
"matplotlib>=3.10.8",
"numpy>=2.4.4",
"pandas>=3.0.2",
"polars>=1.39.3",
"requests>=2.33.1",
"scipy>=1.17.1",
"sortedcontainers>=2.4.0",
"websockets>=16.0",
]
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Jupyter Lab — modo colaborativo con autodeteccion de puerto
# Generado por write_jupyter_launcher (fn_registry)
find_free_port() {
for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
! lsof -i:"$port" >/dev/null 2>&1; then
echo $port
return
fi
done
echo 8888
}
PORT=${1:-$(find_free_port)}
cd "$(dirname "$0")"
echo $PORT > .jupyter-port
source .venv/bin/activate 2>/dev/null || true
if ! python -c "import jupyter_collaboration" 2>/dev/null; then
echo "ERROR: jupyter-collaboration no esta instalado"
echo "Instala con: uv add jupyter-collaboration"
exit 1
fi
echo "════════════════════════════════════════════════"
echo " Jupyter Lab + Colaboracion en puerto $PORT"
echo "════════════════════════════════════════════════"
echo ""
echo " Abre: http://localhost:$PORT"
echo " Ctrl+C para detener"
echo ""
jupyter lab \
--port=$PORT \
--no-browser \
--ServerApp.token='' \
--ServerApp.password='' \
--ServerApp.disable_check_xsrf=True \
--ServerApp.allow_origin='*' \
--ServerApp.root_dir="$(pwd)" \
--collaborative
Generated
+3146
View File
File diff suppressed because it is too large Load Diff