🛡️ sandbox-labs GitHub ↗

CM-03 · Custodia y segregación de activos#

En una frase, para cualquiera: cuando una empresa que guarda dinero de sus clientes quiebra, la pregunta no es cuánto dinero había. Es de quién era. Y si se mezcló con el dinero de la empresa, esa pregunta ya no tiene respuesta.

Estado real: 🟢 functional — se ejecuta y hay prueba que lo demuestra · Carpeta: domains/capital-markets/cases/03-asset-custody/

Cuentas, saldos e instrumentos simulados. No es una autorización regulatoria ni una recomendación de inversión.

Por qué se realiza este caso#

Segregar los activos de los clientes suena a papeleo. No lo es: es lo que decide si un cliente recupera lo suyo cuando la empresa cae.

Si el dinero de los clientes está en una cuenta separada, identificado como suyo, no forma parte de la masa de la quiebra: es de ellos y se les devuelve. Si se mezcló con el de la casa, se convierte en un crédito más contra una empresa insolvente, y se cobra lo que quede, si queda algo.

El invariante que lo gobierna cabe en una línea:

Activos de clientes registrados = Activos custodiados + Operaciones pendientes justificadas

Cuando esa igualdad se rompe, algo pasó. Y lo que pasó importa:

RupturaQué significa
FaltanteHay menos custodiado que registrado. Alguien usó activos de clientes
SobranteHay más de lo que debería. Suena bien y no lo es: indica registro incompleto
Posición negativa de clienteSe entregó algo que el cliente no tenía
Cuenta mezcladaActivos de clientes y de la casa en el mismo sitio
Pendiente sin justificarUn descuadre que se explica como «está en tránsito» sin operación que lo respalde

Ese último es el favorito de los fraudes: un descuadre permanente disfrazado de operación en curso.

La idea que enseña, y que ningún otro caso enseña#

La conciliación como control ejecutable. No es un informe mensual que alguien revisa: es una función que se ejecuta y devuelve hallazgos con nombre. Y su utilidad se mide de una forma concreta: por cada tipo de descuadre existe un escenario que lo produce, y la conciliación tiene que detectarlo. Si deja de detectarlo, la integración continua se pone roja.

Casos de uso reales#

mismo.

Cómo funciona#

flowchart LR
  R["📒 Registro:<br/>lo que dicen los libros<br/>que tiene cada cliente"] --> C
  H["🏦 Custodia:<br/>lo que hay de verdad<br/>en las cuentas"] --> C
  P["⏳ Pendientes:<br/>operaciones en curso"] --> C
  C{"⚖️ Conciliación"}
  C -->|"cuadra"| OK["✅ Sin hallazgos"]
  C -->|"no cuadra"| F["🚨 Hallazgos con nombre"]
  F --> F1["Faltante"] & F2["Sobrante"] & F3["Posición negativa"] & F4["Cuenta mezclada"] & F5["Pendiente sin justificar"]
flowchart TB
  A["Por cada cliente e instrumento"] --> B{"registrado ==<br/>custodiado + pendientes justificados?"}
  B -- sí --> C{"¿La posición<br/>es negativa?"}
  C -- sí --> C1["🚨 NegativeClientPosition"]
  C -- no --> D{"¿La cuenta mezcla<br/>cliente y casa?"}
  D -- sí --> D1["🚨 CommingledAccount"]
  D -- no --> OK["✅"]
  B -- "falta" --> E1["🚨 Shortfall"]
  B -- "sobra" --> E2["🚨 Surplus"]

Esquemas#

Libro de custodia#

{
  "positions": [
    { "owner": { "client": "cliente-1" }, "instrument": "CLP", "registered": 1500000, "custodied": 1500000 },
    { "owner": { "house": true },        "instrument": "CLP", "registered": 200000,  "custodied": 200000 }
  ],
  "pending": [
    { "owner": { "client": "cliente-1" }, "instrument": "CLP", "amount": -50000, "reason": "retiro en curso", "operationId": "op-77" }
  ]
}

Cada pendiente lleva operationId. Un pendiente sin operación que lo respalde es en sí mismo un hallazgo, y esa es la trampa que este caso enseña a detectar.

Hallazgos#

{
  "findings": [
    { "kind": "Shortfall", "owner": "cliente-1", "instrument": "CLP", "missing": 50000 },
    { "kind": "CommingledAccount", "account": "cuenta-unica", "detail": "cliente-2 y casa comparten cuenta" }
  ],
  "balanced": false
}

Software necesario#

ComponenteVersiónPara qué¿Obligatorio?
Rust1.75+El motor de custodia, Money y Ledger
Node.js 20+ / pnpm 9+Panel (opcional)No
bubblewrap, LinuxNo hacen faltaNo

Este caso corre en cualquier sistema operativo: no ejecuta código ajeno, así que no necesita jaula. Es lógica determinista con enteros.

Instalación#

git clone https://github.com/vladimiracunadev-create/sandbox-labs
cd sandbox-labs
cargo build --release

Cómo se ejecuta y cómo se comprueba#

cargo run -p sandboxctl -- markets reconcile

Ese comando ejecuta seis escenarios, y cada uno declara de antemano el hallazgo que debe producir. Corre en cada commit: si la conciliación deja de detectar lo que declara, la integración continua se pone roja.

EscenarioHallazgo esperado
Todo cuadraninguno
Faltan activos custodiadosShortfall
Hay más de lo registradoSurplus
Un cliente queda en negativoNegativeClientPosition
Cliente y casa en la misma cuentaCommingledAccount
Un pendiente sin operación que lo justifiqueUnexplainedPending

Procesos que se crean#

sandboxctl markets reconcile
  │
  └─ un proceso determinista
      ├─ sin red
      ├─ sin reloj
      ├─ enteros en unidades mínimas: nunca coma flotante
      └─ mismo resultado en cualquier máquina y en cualquier momento

Por qué enteros y no decimales. Un f64 no puede representar 0,10 de forma exacta. Sumar diez veces 0,10 no da 1,00. En un libro contable eso es un descuadre que aparece tarde, en producción, y que nadie sabe explicar. Aquí el dinero son enteros en unidades mínimas —pesos, centavos— con la moneda pegada al importe: pesos y dólares no se suman porque no compila.

Tiempo de carga#

OperaciónCoste medido
cargo run -p sandboxctl -- markets reconcile< 1 s, incluida la carga del binario
Conciliar un libro de miles de posicionesmilisegundos
Una operación en el libro de partida doblemicrosegundos

Estado real y qué falta#

Construido y verificado: el invariante de custodia, los cinco tipos de hallazgo, los seis escenarios, el libro de partida doble con reversas e idempotencia, y la comprobación automática en cada commit.

Falta: dividendos, bloqueos, garantías, transferencias entre custodios, y el escenario de insolvencia del custodio, que es donde el invariante deja de ser un ejercicio y se convierte en la única pregunta que importa.

Si algo falla#

SituaciónCausaCómo se resuelve
ShortfallHay menos custodiado que registradoAlguien usó activos de clientes. Se localiza el movimiento que lo produjo en el libro append-only y se repone. Es el hallazgo más grave del caso
SurplusHay más de lo registradoSuena bien y no lo es: significa registro incompleto. Buscar la operación que no se anotó
UnexplainedPendingUn pendiente sin operationId que lo respaldeEs el disfraz favorito de un descuadre permanente. Todo pendiente tiene que apuntar a una operación real, con fecha
CommingledAccountActivos de clientes y de la casa en la misma cuentaSeparar las cuentas. No hay arreglo contable posible: la segregación es física o no es
NegativeClientPositionSe entregó algo que el cliente no teníaRevisar el orden de las operaciones: casi siempre es una entrega liquidada antes de que llegara el activo
markets reconcile falla en CI tras tocar el motorLa conciliación dejó de detectar lo que declaraEl escenario que falla dice qué hallazgo esperaba. Arreglar la conciliación, no el escenario
Los importes no cuadran por céntimosSe usó coma flotante en algún puntoMoney son enteros en unidades mínimas y la moneda va pegada al importe. Si aparece un f64 manejando dinero, eso es el fallo

Los fallos que afectan a cualquier caso —la compilación, el catálogo, la evidencia— están resueltos uno a uno en Cuando algo falla.

Esta familia no necesita aislamiento del sistema: no ejecuta código ajeno, sino reglas de negocio deterministas. Por eso casi ningún fallo suyo viene del entorno, y casi todos vienen de los datos.


Ver también: Catálogo completo · CM-10 · liquidación · CM-13 · salida ordenada · Estado del proyecto