2. La estructura mínima de un proyecto que se puede operar
Nivel 7: Del cuaderno al proceso
Un cuaderno mezcla todo en una columna vertical. Un sistema separa las responsabilidades en piezas que se pueden mirar, probar y sustituir de una en una. No es burocracia de arquitecto: es lo que decide qué partes de tu código puedes ejecutar mil veces sin consecuencias y qué partes tocan dinero.
Cinco responsabilidades, cinco módulos
La estructura mínima cabe en cinco ficheros, y conviene que sean cinco de verdad:
- datos. Descarga, cachea y valida precios y fundamentales. Devuelve tablas. No sabe qué es una señal.
- senal. Recibe tablas, devuelve una decisión abstracta: entrar, salir, no hacer nada. No sabe que existe un bróker.
- dimension. Convierte la decisión en un número de acciones, dado el capital y los límites de riesgo. No sabe de dónde salió la decisión.
- ejecucion. Traduce ese número en llamadas concretas a la interfaz del bróker y devuelve lo que el bróker responde.
- registro. Escribe qué se decidió, con qué entradas y qué respondió el bróker.
Y por encima, un sexto fichero que no calcula nada: el punto de entrada, que los conecta en orden.
El criterio de corte no es el número de líneas, es la razón para cambiar cada pieza. El módulo de datos cambia cuando cambias de proveedor; el de señal, cuando cambias de idea; el de ejecución, cuando el bróker actualiza su interfaz. Son tres motivos independientes, con tres calendarios independientes, y por eso viven en tres ficheros. Cuando dos piezas que cambian por motivos distintos comparten fichero, tocar una obliga a revalidar la otra.
Por qué la estrategia no llama al bróker
Es la regla que más resistencia genera y la más rentable de todas. Si la función de señal consulta la posición actual por la interfaz del bróker, ocurren tres cosas a la vez: no puedes ejecutarla en una prueba sin red, su resultado depende de un estado externo que cambia solo, y ya no puedes recorrer diez años de histórico pasándole datos. La función deja de ser una función y pasa a ser un procedimiento con efectos.
# senal.py -- logica pura: mismas entradas, misma salida, siempre
def calcular_senal(precios, ventana_corta=20, ventana_larga=100):
media_corta = precios['cierre'].rolling(ventana_corta).mean()
media_larga = precios['cierre'].rolling(ventana_larga).mean()
if media_corta.iloc[-1] > media_larga.iloc[-1]:
return 'largo'
return 'plano'Esa función no descarga nada, no escribe nada y no manda nada. Por eso se puede probar en microsegundos, con datos inventados, y por eso el mismo código que corre en el backtest puede correr en producción sin tocar una línea. Lo que no tiene efectos secundarios se puede repetir; lo que tiene efectos hay que aislarlo.
Un único punto de entrada, ejecutable desde la línea de comandos
Si en el equipo hay tres formas de lanzar la estrategia, hay tres estrategias. El punto de entrada es un fichero que se ejecuta con una orden en la terminal, acepta argumentos explícitos y no contiene lógica de negocio: solo orquesta.
# ejecutar.py
import argparse
from datos import cargar_precios
from senal import calcular_senal
from dimension import acciones_objetivo
from ejecucion import enviar_orden
from registro import anotar
def main():
p = argparse.ArgumentParser()
p.add_argument('--simbolo', required=True)
p.add_argument('--capital', type=float, required=True)
p.add_argument('--simular', action='store_true')
args = p.parse_args()
precios = cargar_precios(args.simbolo)
decision = calcular_senal(precios)
cantidad = acciones_objetivo(decision, args.capital, precios)
anotar('decision', simbolo=args.simbolo, decision=decision, cantidad=cantidad)
if args.simular:
anotar('simulado', cantidad=cantidad)
return
respuesta = enviar_orden(args.simbolo, cantidad)
anotar('respuesta', datos=respuesta)
if __name__ == '__main__':
main()Fíjate en tres detalles. Los argumentos son obligatorios, así que nadie hereda por descuido el capital de ayer. El modo de simulación recorre exactamente el mismo camino y se detiene justo antes del único paso que toca el mundo. Y la condición final permite importar el fichero desde una prueba sin que se ejecute nada.
Fíjate también en lo que no hay: ni un cálculo, ni un umbral, ni una condición de negocio. El punto de entrada solo lee argumentos, llama en orden y anota. Cuando empiezan a aparecer ahí condiciones del tipo si el símbolo es este entonces el límite es aquel, la lógica se está escapando al único fichero que nadie prueba, y ese fichero acaba siendo el más largo del proyecto.
La prueba que solo es posible si separas
Con esta estructura, comprobar la estrategia no requiere ni red, ni claves, ni mercado abierto:
# test_senal.py
import pandas as pd
from senal import calcular_senal
def test_cruce_al_alza_da_largo():
cierres = list(range(100, 130)) + list(range(130, 100, -1))
precios = pd.DataFrame({'cierre': cierres})
assert calcular_senal(precios, 3, 10) == 'plano'Puedes lanzarla mil veces seguidas en una máquina sin internet. Ese es el criterio operativo de si has separado bien: si para probar una parte necesitas credenciales, no la has separado.
Caso real: Knight Capital, 1 de agosto de 2012
Knight Capital era uno de los mayores creadores de mercado de Estados Unidos. Aquella mañana desplegó código nuevo en sus servidores de enrutamiento de órdenes. El despliegue se hizo a mano, servidor por servidor, y en uno de los ocho no llegó a copiarse la versión nueva. Peor: el código nuevo reutilizaba una bandera de configuración que en la versión antigua activaba una funcionalidad retirada años atrás y que seguía viviendo en el mismo ejecutable.
En ese servidor rezagado, la bandera despertó el código viejo. El sistema empezó a mandar órdenes al mercado a un ritmo enorme en cuanto abrió la sesión. En unos cuarenta y cinco minutos la firma acumuló una pérdida aproximada de 440 millones de dólares, más de lo que valía la propia empresa; tuvo que ser rescatada en días y acabó absorbida por un competidor. La SEC impuso además una multa de 12 millones de dólares en 2013.
Hay muchas lecciones ahí y varias son de niveles posteriores. La de este módulo es concreta: código muerto que sigue empaquetado con el vivo, y una bandera que significa dos cosas distintas según qué versión la lea. Cuando las responsabilidades no están separadas, ningún componente sabe lo suficiente para negarse.