4. Reconciliación: cuadrar tus libros con los del bróker
Nivel 10: Saber qué está pasando
Ya sabes que el bróker es la fuente de verdad. Eso resuelve quién gana una discusión, pero no resuelve el problema de verdad: enterarte de que hay discusión. La reconciliación es el proceso diario que compara tus libros con los suyos, y es el control que más dinero salva de todo el nivel, porque es el único que detecta los fallos que no producen ni errores ni alertas.
Reconciliar es comparar tres cosas, no una
Cada día, después del cierre y antes de que el sistema pueda operar de nuevo, se comparan tres bloques contra lo que declara el bróker:
- Posiciones: acciones por símbolo, con signo. Aquí la tolerancia es cero.
- Efectivo: el saldo disponible. Aquí hay una tolerancia mínima por redondeos.
- Operaciones del día: la lista de ejecuciones, cotejada por identificador. Deben aparecer las mismas a ambos lados.
Comparar solo una de las tres da una falsa sensación de control. El efectivo puede cuadrar mientras las acciones no cuadran, por ejemplo tras un desdoblamiento que el bróker ajustó y tu base de datos no. Y las posiciones pueden cuadrar mientras el efectivo no lo hace, porque cobraste un dividendo que tu contabilidad no modela.
from decimal import Decimal
TOLERANCIA_ACCIONES = Decimal("0")
TOLERANCIA_EFECTIVO = Decimal("0.05")
def dec(x):
return Decimal(str(x))
def reconciliar_posiciones(locales, del_broker):
simbolos = set(locales) | set(del_broker)
diferencias = []
for s in sorted(simbolos):
mia = dec(locales.get(s, 0))
suya = dec(del_broker.get(s, 0))
if abs(mia - suya) > TOLERANCIA_ACCIONES:
diferencias.append({"simbolo": s, "local": mia,
"broker": suya, "delta": mia - suya})
return diferenciasUsa Decimal, no coma flotante: un descuadre de céntimos que aparece y desaparece según el día es indistinguible de un descuadre real y acaba enseñándote a ignorar el proceso.
De dónde salen las diferencias
Casi todas las diferencias que verás caen en seis familias, y conviene reconocerlas de un vistazo:
- Ejecuciones parciales que tu código contó como completas.
- Comisiones, tasas e impuestos que el bróker descuenta y tu contabilidad no modela.
- Dividendos y cupones abonados en efectivo, que mueven el saldo sin ninguna operación tuya.
- Desdoblamientos y operaciones societarias, que cambian el número de acciones sin que tú hagas nada.
- Operaciones manuales del usuario desde la aplicación del bróker: la causa más frecuente y la más difícil de imaginar desde el código.
- Órdenes que creías canceladas y que se ejecutaron entre tu petición de cancelación y su procesamiento.
Fíjate en que solo las dos últimas son fallos tuyos; las cuatro primeras son hechos legítimos del mundo real que tu contabilidad no conocía. Por eso la reconciliación no sirve únicamente para cazar errores de software: sirve para descubrir todo lo que le pasa a tu dinero y tu código todavía no modela. Cada diferencia explicada acaba convertida en una regla nueva, y con los meses el proceso se vuelve silencioso.
Tolerancias: una sola cifra no vale para todo
La tolerancia de acciones es cero y no se negocia: media acción de diferencia no existe en la naturaleza salvo en cuentas fraccionadas, y si tu bróker las permite, la tolerancia sigue siendo el mínimo que él admite. La del efectivo es distinta, porque el redondeo de comisiones sí produce céntimos legítimos, pero hay que ponerla lo bastante baja como para que un error real no quepa dentro. Una tolerancia generosa es la forma más elegante de no enterarte nunca de nada, y tiene la desagradable propiedad de esconder mejor los errores cuanto más pequeña es la cuenta con la que empiezas a operar.
La comprobación completa
def reconciliar_dia(estado_local, cliente, fecha):
dif_pos = reconciliar_posiciones(estado_local.posiciones, cliente.posiciones())
dif_efectivo = dec(estado_local.efectivo) - dec(cliente.efectivo())
mias = {o["id_cliente"] for o in estado_local.operaciones_de(fecha)}
suyas = {o["id_cliente"] for o in cliente.operaciones(fecha)}
solo_mias = mias - suyas
solo_suyas = suyas - mias
cuadra = (not dif_pos
and abs(dif_efectivo) <= TOLERANCIA_EFECTIVO
and not solo_mias and not solo_suyas)
registrar(logging.INFO if cuadra else logging.ERROR, "reconciliacion",
fecha=fecha, cuadra=cuadra, posiciones_dif=len(dif_pos),
delta_efectivo=str(dif_efectivo),
solo_mias=sorted(solo_mias), solo_suyas=sorted(solo_suyas))
return cuadra, dif_pos, dif_efectivo, solo_mias, solo_suyas
def cerrar_dia(estado_local, cliente, fecha):
cuadra, dif_pos, dif_efectivo, solo_mias, solo_suyas = reconciliar_dia(
estado_local, cliente, fecha)
if cuadra:
M.contar("reconciliaciones_ok")
return True
estado_local.bloquear("descuadre:" + fecha)
alertar("critico", "descuadre",
"reconciliacion de %s no cuadra: %d posiciones, efectivo %s"
% (fecha, len(dif_pos), dif_efectivo))
return FalseAl encontrar una diferencia: parar y avisar, nunca corregir en silencio
La tentación es enorme: el bróker dice 40 y tú tienes 26, el bróker manda, escribo 40 y mañana arrancamos cuadrados. Nunca hagas eso. Copiar el número tapa el hecho que produjo la diferencia, y ese hecho casi siempre sigue activo: si una orden se duplicó por un reintento mal hecho, mañana se duplicará otra vez, con más dinero y sin que nadie lo note, porque el proceso que debía avisar ha aprendido a ajustar solo.
El comportamiento correcto tiene tres pasos, en este orden: bloquear la operativa para el día siguiente, emitir una alerta crítica, y usar el registro del módulo uno para reconstruir la cadena completa de la orden implicada hasta encontrar la causa. El ajuste se hace después, a mano, con la explicación escrita al lado.
Y ojo con una trampa de diseño frecuente: escribir solo la rama feliz. Un proceso que marca la reconciliación como correcta cuando cuadra y no hace nada cuando no cuadra parece funcionar durante meses. Si además nadie vigila la ausencia de esa marca, con lo que aprendiste en el módulo tres, el descuadre es invisible.
Caso trabajado: dos ejecuciones donde debía haber una
Escenario inventado con fines didácticos. Un jueves la reconciliación devuelve: posiciones con una diferencia de 26 acciones en un valor, efectivo local 4.938 euros por encima del real, y un identificador de ejecución que aparece en el bróker y no en tus libros. Las tres pistas apuntan al mismo sitio: se compró dos veces. El registro confirma que el segundo envío llevaba un identificador de cliente nuevo, así que el bróker lo trató como una orden distinta. Coste del fallo: la posición no deseada más el deslizamiento de deshacerla. Coste de no reconciliar: el mismo error repitiéndose cada semana sin que nadie lo vea.