Ir al contenido

Pipeline de Normalización de Ingesta

Cada transacción que entra en Criptofolio —ya sea mediante el asistente visual de CSV/XLSX en el navegador o a través de endpoints API— se procesa mediante una función pura, centralizada y determinista:

prepareIngestionRows() (packages/core-domain/src/domain/services/normalizer/ingestionPipeline.ts).


flowchart LR
    A["1. classifyCustodyMovement\n(normaliza dirección y signo)"] --> B["2. applyProfileToRow\n(por lado, según formato origen)"]
    B --> C["3. aggregateRows\n(fusiona lados de una operación)"]
    C --> D["4. resolveTradeDirection\n(resolución de BUY / SELL / SWAP)"]

El orden de ejecución es crítico tanto matemática como arquitectónicamente:

Paso 1: Clasificar la Dirección Primero (classifyCustodyMovement)

Sección titulada «Paso 1: Clasificar la Dirección Primero (classifyCustodyMovement)»

classifyCustodyMovement examina el signo numérico del importe y el activo transferido para determinar su naturaleza fiscal antes de que la agregación destruya ese signo al repartirlo en amount_in y amount_out.

[!IMPORTANT] Ejecutar la agregación antes de la clasificación era el origen de un error histórico muy común: el clasificador recibía registros sin signo identificable, respondía UNCLASSIFIED y acababa registrando cada venta como si fuera una compra.

Paso 2: Aplicar el Perfil de Origen por Fila (applyProfileToRow)

Sección titulada «Paso 2: Aplicar el Perfil de Origen por Fila (applyProfileToRow)»

El significado de una comisión sin divisa especificada o el volcado de una cantidad en ambas columnas direccionales son particularidades del exchange de origen. Aplicar el perfil mientras cada activo sigue en contexto garantiza la correcta asignación de gastos y comisiones.

Paso 3: Fusionar Registros de una Misma Operación (aggregateRows)

Sección titulada «Paso 3: Fusionar Registros de una Misma Operación (aggregateRows)»

Muchos exchanges (como Kraken) anotan dos filas separadas para una única operación de compraventa: una fila para el dinero fiat y otra para la criptomoneda. El paso 3 fusiona estos registros complementarios compartiendo el identificador de orden.

Paso 4: Resolver la Dirección Real de la Operación (resolveTradeDirection)

Sección titulada «Paso 4: Resolver la Dirección Real de la Operación (resolveTradeDirection)»

Kraken utiliza la etiqueta genérica trade tanto para compras como para ventas. La dirección verdadera solo puede determinarse cuando ambas partes de la operación están presentes:

  • paidInMoney → BUY (Compra)
  • receivedMoney → SELL (Venta)
  • Ninguna es fiat (cripto por cripto) → SWAP (Permuta)

Si un registro no se puede resolver inequívocamente, se rechaza con un error descriptivo en lugar de asumir un valor por defecto, previniendo alteraciones silenciosas del historial.


⏰ Normalización de Zona Horaria (Exactamente Una Vez)

Sección titulada «⏰ Normalización de Zona Horaria (Exactamente Una Vez)»

Un fallo habitual en aplicaciones web es el desfase doble de zona horaria:

  1. El navegador convierte la hora local a UTC.
  2. El backend reinterpreta la cadena UTC aplicando el desfase local una segunda vez.

Criptofolio garantiza que la conversión horaria se produce exactamente una única vez mediante dateNormalizer.ts:

normalizeToUtcIso(dateStr, timeStr, timezone);

Todas las marcas de tiempo en SQLite y DuckDB quedan normalizadas en formato canónico UTC ISO-8601 (YYYY-MM-DDTHH:mm:ss.sssZ).


🔐 Hash Criptográfico de Idempotencia (id_hash)

Sección titulada «🔐 Hash Criptográfico de Idempotencia (id_hash)»

Para evitar transacciones duplicadas al importar extractos con periodos solapados, cada fila recibe un hash SHA-256 determinista:

$$\text{id_hash} = \text{SHA-256}(\text{account_id} + \text{source_id} + \text{timestamp} + \text{asset} + \text{amount})$$

La base de datos SQLite impone un índice UNIQUE sobre id_hash. Reimportar un archivo existente ignora las filas ya registradas sin duplicar balances ni recalcular erróneamente los lotes fiscales.