ArmyTrading
Una plataforma de trading algorítmico que opera siempre en simulación
Ingiere el mercado en tiempo real, guarda millones de velas, prueba estrategias sobre todo el histórico y ejecuta señales con límites de riesgo. El modo con dinero real viene apagado de fábrica.
- Python 3.12
- FastAPI
- asyncio + uvloop
- TimescaleDB
- asyncpg
- SQLAlchemy + Alembic
- Redis (pub/sub)
- Docker Compose
- pytest
- structlog + ruff
≈18.500×más rápida la consulta: 44,7 s → 2,4 ms
— ampliar imagenLa demo en 30 segundos
Qué es
Un sistema que ingiere datos de mercado en tiempo real, los almacena por millones de filas, permite probar estrategias sobre todo ese histórico y ejecuta señales con límites de riesgo — siempre en simulación. El modo que tocaría dinero de verdad viene apagado, escribe lo que habría hecho a un fichero y exige activarse a mano.
Las tres piezas son el mismo sistema a propósito: el motor de reglas que evalúa una señal sobre el histórico es exactamente el mismo que la evalúa en vivo, así que un resultado de backtest y una sesión real no pueden divergir por tener dos implementaciones. Los límites de riesgo se comprueban antes de abrir cualquier posición y el tamaño sale del riesgo asumido, no de un número fijo.
Debajo del dominio, esto es un problema de datos: capturar series temporales sin huecos ni duplicados, poder reprocesar un rango sin corromper lo ya guardado y consultar millones de filas lo bastante rápido como para decidir en el momento.
Stack técnico
Python 3.12 con FastAPI y asyncio sobre TimescaleDB —Postgres con extensión de
series temporales—, asyncpg para hablar con la base, Alembic para las
migraciones y Redis pub/sub como bus de avisos en vivo. Las reglas duras —qué es
cada fila, qué no puede duplicarse— viven en el esquema de la base de datos y no
en el código de aplicación: ingestor y descarga histórica escriben con
ON CONFLICT DO NOTHING contra la clave primaria, así que reprocesar un rango no
duplica nada. Se despliega con Docker Compose en mi propio servidor, con
structlog para el rastro.
De ahí sale el número grande de la cabecera. La consulta que busca la última vela antes de
cada descarga filtraba por un campo dentro de la columna JSONB
(raw->>'source'), que no es indexable: el planificador la resolvía fila a fila
y detoastaba el JSONB de cada una para leer un solo campo, descartando ~2,6
millones de filas antes de dar con la primera buena. Lo obvio era un índice
parcial sobre la expresión —una línea de SQL, cero migración—, y lo descarté:
arregla el síntoma y deja la regla enterrada en un campo de texto libre, donde
cualquier escritor nuevo puede volver a mezclar las dos poblaciones en silencio.
Lo que entró fue una columna discriminadora NOT NULL, con CHECK y un índice
parcial encima: la regla pasa a vivir en el único sitio donde no se puede
saltar. Costó una migración y tocar los tres escritores; a cambio, de 44,7 s a
2,4 ms medidos con EXPLAIN (ANALYZE, BUFFERS) sobre la base real, en el peor
caso.