DaPex LabsDaPex Labs
AI-Powered Financial Intelligence
English中文Españolالعربية
← Volver al Inicio

Migracion de JSON a SQLite para Almacenamiento OHLC — Engineering

Notas de Engineering — Vol. 1

Migración de JSON a SQLite para Almacenamiento OHLC

2026-06-29 Engineering ~6 min de lectura
TL;DR: Reemplazamos más de 1,200 archivos JSON planos que contenían 42,000 registros OHLC con una única base de datos SQLite utilizando agregación incremental. Los tiempos de consulta se redujeron de 850ms a menos de 50ms. El uso de disco disminuyó en un 67%. Cero pérdida de datos durante la migración. Aquí está exactamente cómo lo hicimos.
42,000
filas migradas
94%
aceleración de consultas
67%
ahorro en disco
0
datos perdidos

El Problema

DaPex Terminal muestra gráficos de velas OHLC en 7 marcos temporales (1m, 5m, 15m, 1h, 4h, 1d, 1w) para más de 30 instrumentos. Detrás de cada gráfico hay datos históricos de precios que deben:

Nuestra primera implementación almacenaba cada combinación de instrumento-marco temporal como un archivo JSON separado:

data/klines/
  XAUUSD_1m.json    (2.1 MB, 28,000 filas)
  XAUUSD_5m.json    (0.5 MB, 5,600 filas)
  XAUUSD_1h.json    (0.1 MB, 720 filas)
  EURUSD_1m.json    (1.8 MB, 24,000 filas)
  ...
  // 30 instrumentos x 7 marcos temporales = 210 archivos

Esto funcionó bien al inicio. Pero para la tercera semana, las grietas comenzaron a aparecer.

Tres Modos de Falla

1. Contención de Escritura

Cuando MT5 enviaba un nuevo tick, un worker de Python abría el archivo JSON, analizaba los 28,000 registros, añadía uno, serializaba todo el array de vuelta al disco y cerraba el archivo. Durante mercados volátiles con más de 6 ticks por segundo en 10 instrumentos activos, la E/S del sistema de archivos se convertía en el cuello de botella. El hilo de solicitud de Flask se bloqueaba esperando el bloqueo del archivo, causando que los tiempos de respuesta de la API aumentaran de 50ms a más de 2 segundos.

2. Fallos de Atomicidad

La escritura de un archivo JSON no es atómica. Si el servidor se bloqueaba a mitad de la escritura (lo que ocurrió dos veces durante una fluctuación de energía en la instancia en la nube), el archivo terminaba truncado, perdiendo todos los datos desde la última copia de seguridad. Tuvimos que reproducir desde el historial de MT5 para recuperarlos, lo que tomó más de 45 minutos por instrumento.

3. Colapso del Rendimiento de Consultas

Construir una vela de 1 hora a partir de 60 velas de 1 minuto implicaba analizar 60 archivos JSON, fusionarlos en memoria y agregarlos. Para un gráfico que mostraba 100 velas horarias a lo largo de 6,000 minutos de datos, el frontend esperaba 850ms en promedio. Para contexto, los Core Web Vitals de Google marcan cualquier valor por encima de 100ms.

Lección aprendida: Los archivos planos JSON son buenos para configuración. No son una base de datos. Si te encuentras escribiendo un mecanismo de bloqueo de archivos para JSON, ya has perdido.

Qué Consideramos

OpciónProsContras
PostgreSQL SQL completo, maduro Más de 200MB de memoria, proceso separado, excesivo para un solo servidor
InfluxDB Construido específicamente para series temporales Configuración compleja, sobrecarga del runtime Go, otro servicio que monitorear
SQLite Sin configuración, un solo archivo, ACID, 600KB de memoria Un solo escritor a la vez (aceptable para nuestra escala)
Archivos Parquet Gran compresión No diseñado para añadidos a nivel de fila, requiere Spark/Pandas

SQLite ganó en tres aspectos: está embebido (sin proceso separado), es compatible con ACID (sin más pérdida de datos) y usa 600KB de memoria en modo WAL, el 0.06% de nuestro servidor de 1GB.

La Migración

Diseño del Esquema

La idea clave fue separar los ticks sin procesar de las velas agregadas. Almacenamos solo las velas de 1 minuto como datos fuente y calculamos todos los marcos temporales superiores mediante agregación SQL:

CREATE TABLE kline_1m (
    symbol    TEXT NOT NULL,    -- XAUUSD, EURUSD
    ts        INTEGER NOT NULL, -- Marca de tiempo Unix de apertura de la vela
    open      REAL NOT NULL,
    high      REAL NOT NULL,
    low       REAL NOT NULL,
    close     REAL NOT NULL,
    volume    INTEGER DEFAULT 0,
    PRIMARY KEY (symbol, ts)
);

CREATE INDEX idx_kline_symbol_ts ON kline_1m(symbol, ts);

Con este esquema, calcular cualquier marco temporal superior es una sola consulta:

-- Velas de 1 hora a partir de datos de 1 minuto
SELECT
    (ts / 3600) * 3600 AS hour_ts,
    symbol,
    FIRST_VALUE(open) OVER w AS open,
    MAX(high) OVER w AS high,
    MIN(low) OVER w AS low,
    LAST_VALUE(close) OVER w AS close,
    SUM(volume) OVER w AS volume
FROM kline_1m
WHERE symbol = ? AND ts BETWEEN ? AND ?
WINDOW w AS (PARTITION BY (ts / 3600) * 3600 ORDER BY ts);

El Script de Migración

Escribimos un script de migración único que:

  1. Lee cada archivo JSON en fragmentos (no todo a la vez, para evitar picos de memoria)
  2. Desduplica por (símbolo, marca de tiempo) — los archivos JSON habían acumulado un 3% de duplicados debido a casos extremos de reinicio
  3. Inserta en lotes de 500 filas usando transacciones BEGIN/COMMIT
  4. Verifica que los recuentos de filas coincidan después de la migración
  5. Mantiene los archivos JSON como copia de seguridad durante 72 horas, luego los elimina
import json, sqlite3, os, glob

conn = sqlite3.connect("klines.db")
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")

batch = []
total = 0

for fpath in sorted(glob.glob("data/klines/*.json")):
    symbol = os.path.basename(fpath).split("_")[0]
    with open(fpath) as f:
        rows = json.load(f)

    for row in rows:
        batch.append((
            symbol, row["ts"], row["o"], row["h"],
            row["l"], row["c"], row.get("v", 0)
        ))

        if len(batch) >= 500:
            conn.executemany(
                "INSERT OR IGNORE INTO kline_1m VALUES (?,?,?,?,?,?,?)",
                batch
            )
            conn.commit()
            total += len(batch)
            batch = []

# Lote final
if batch:
    conn.executemany("INSERT OR IGNORE INTO kline_1m ...", batch)
    conn.commit()

print(f"Se migraron {total} filas")

El script se ejecutó en 12 segundos en el servidor de producción. Verificamos que los recuentos de filas coincidieran entre JSON y SQLite usando una consulta paralela, luego eliminamos el directorio JSON 72 horas después.

Resultados

MétricaAntes (JSON)Después (SQLite)Cambio
Consulta de vela de 1h (100 barras)850ms48ms-94%
Añadido del último tick120ms2ms-98%
Uso de disco (3 meses)~180MB~60MB-67%
Consumo de memoria~80MB (caché de archivos)~6MB (WAL + caché)-92%
Eventos de pérdida de datos2 (corte de energía)0ACID lo previene

Qué Haríamos Diferente

  1. Comenzar con SQLite desde el primer día. Perdimos 3 semanas depurando bloqueos de archivos JSON que SQLite maneja de forma nativa. El costo de memoria de 600KB es insignificante incluso en un servidor de 1GB.
  2. Usar modo WAL inmediatamente. Comenzamos con el modo de diario DELETE, que bloqueaba a los lectores durante las escrituras. Cambiar a WAL (Write-Ahead Log) nos dio lecturas concurrentes durante las escrituras, algo crítico para servir gráficos mientras llegan los ticks.
  3. Inserciones por lotes. Nuestra primera implementación hacía INSERT individual por tick. Agrupar 500 filas por transacción mejoró el rendimiento de escritura en 40x.
  4. Añadir un cron de retención desde el día uno. Olvidamos podar los datos antiguos durante el primer mes. Un simple DELETE FROM kline_1m WHERE ts < strftime('%s','now','-3 months') en crontab ahora maneja esto automáticamente.

¿Por Qué No PostgreSQL?

Recibimos esta pregunta a menudo. PostgreSQL es una base de datos excelente. Pero para un terminal de trading de un solo servidor que necesita ejecutarse en un VPS de $2/mes con 1GB de RAM, es la herramienta equivocada. La huella de memoria mínima viable de PostgreSQL es de ~200MB solo para búferes compartidos. SQLite se ejecuta en proceso con ~600KB. Esa es una diferencia de 333x para nuestro caso de uso.

La compensación es que SQLite no maneja bien escritores concurrentes. Pero nuestro patrón de escritura es de un solo escritor (un proceso de bombeo de datos MT5), que es exactamente para lo que SQLite es excelente. Si alguna vez necesitamos múltiples escritores, evaluaríamos PostgreSQL, pero a nuestra escala actual de 1-2 ticks/segundo en 30 instrumentos, SQLite no es el cuello de botella.

Citar este artículo:
Engineering. (2026). Migración de JSON a SQLite para Almacenamiento OHLC. Notas de Engineering, Vol. 1. https://gfil-lab.com/engineering-json-to-sqlite.html
Ingeniería SQLite Base de Datos Rendimiento Infraestructura
Compartir:TwitterTelegram

Análisis Semanal

Suscribete para analisis exclusivo.

Listo para Trading de Nivel Institucional?

Obten inteligencia de mercado en tiempo real con datos WebSocket.

DaPex Terminal →Gráfico de Oro en Vivo →TelegramDiscord
LiuDecai
LiuDecaiFundador, DaPex Labs

Más de 10 años en la intersección de finanzas cuantitativas y sistemas distribuidos. Pasé una década viendo a las instituciones ganar con mejores herramientas, así que construí una propia. DaPex Terminal es la infraestructura que siempre quise: datos WebSocket en tiempo real (sub-50ms), análisis de order flow institucional e integración de IA multimodal. Sin guardianes, sin concesiones. No uso Bloomberg. Construí el mío.

Leave a Comment

Loading...