init: estudio_mercados analysis from fn_registry
This commit is contained in:
@@ -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()")
|
||||
@@ -0,0 +1 @@
|
||||
8888
|
||||
@@ -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.
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
3.13
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
@@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
Executable
+45
@@ -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
|
||||
Reference in New Issue
Block a user