##El problema y la restricción de diseño
Quería un sistema que analizara mi portafolio una vez al mes y me entregara razonamiento auditable — no predicciones. Esa restricción, escrita en la spec antes que ninguna línea de código, definió toda la arquitectura: el sistema evalúa condiciones actuales y está estructuralmente incapacitado para prometer futuros. Sin precios objetivo, sin "esta acción subirá", sin conexión a brokers.
La unidad central es la tesis razonada, con un pipeline obligatorio:
datos → señales → argumentos a favor Y en contra → qué invalidaría la tesis → postura
La postura sale de un menú cerrado (AUMENTAR|MANTENER|REDUCIR|SALIR|VIGILAR
para posiciones; ENTRAR|ESPERAR para candidatos). Y la regla
de oro: sin argumentos en contra, la tesis no se emite. Es un
control de sesgo de confirmación convertido en requisito estructural.
##Stack: aburrido a propósito
Python 3.12, SQLite, yfinance, la API de Anthropic (Claude Sonnet) y
rich para la CLI. Cero frameworks. Cuatro tablas. La decisión menos
glamorosa y más rentable: una capa de proveedor de datos con
interfaz común (ProveedorDatos), de modo que yfinance es reemplazable
por EODHD (ya integrado para histórico) sin tocar señales ni tesis, y
una caché diaria en SQLite con patrón decorador: la clave incluye la
fecha (AAPL:historico:2026-07-16), así que expira sola a medianoche.
##El motor de señales: matemática que no se negocia
RSI(14) con suavizado de Wilder, SMA50/200, MACD(12,26,9), volatilidad anualizada, distancia a extremos de 52 semanas. Cada indicador devuelve valor numérico y lectura en lenguaje humano ("RSI 74 → sobrecompra"), porque el sistema es también una herramienta de aprendizaje.
La verificación fue doble: el dataset clásico de Wilder (el RSI debe dar exactamente los valores publicados: 70.53, 57.97) y una implementación independiente con pandas como referencia — coincidencia a 4 decimales sobre datos reales. Los bordes importan más de lo que parece: un solo cierre en 0 (dato sucio real de Yahoo) rompía el logaritmo de la volatilidad; una serie plana producía "RSI 100 → sobrecompra" sin que nada hubiera subido. Cada borde terminó como test.
##La lección central: el prompt propone, el validador dispone
El generador arma un contexto JSON por ticker (señales calculadas, fundamentales, titulares del mes, catalizadores, la posición y la tesis del mes anterior) y llama al modelo con un prompt versionado en el repo que solo cambia con aprobación humana explícita. Hasta ahí, lo fácil.
Lo difícil: la respuesta no se acepta — se valida estructuralmente. Mi primera versión buscaba subcadenas ("¿aparece 'argumentos en contra'?") y un panel de agentes revisores la destrozó con contraejemplos ejecutables: una tesis que enumera las secciones en un párrafo introductorio validaba sin tenerlas; "no hay razones para SALIR; lo sensato es MANTENER" registraba la postura opuesta al texto. La versión final parsea las secciones reales del documento (encabezados), exige contenido mínimo en cada una y una postura única e inequívoca dentro de su sección. Si el modelo menciona dos opciones del menú, se rechaza y se reintenta explicándole el motivo exacto. Tras N intentos, el sistema prefiere fallar ruidosamente antes que archivar una tesis coja.
max_tokens y el validador la rechazó tres veces por "faltar" justo las
secciones finales. La corrección no fue solo subir el límite: fue
detectar stop_reason == "max_tokens" y tratarlo como problema
distinto, porque reintentarle a un modelo con el mismo techo es tirar
dinero.
##El sistema que se audita: scorecard y sesgo retrospectivo
Cada revisión mensual abre auditando las tesis del mes anterior. Un
segundo prompt (también versionado, también con gate humano) dictamina
SOSTENIDA|INVALIDADA con una regla que me importa más que cualquier
métrica: si una condición de invalidación declarada ocurrió, la tesis
está invalidada aunque el precio haya acompañado. Se audita el
razonamiento, no la suerte.
Y la decisión de diseño más contraintuitiva: me negué a generar tesis retroactivas para fabricar historial. Podía reconstruir los precios de mayo, pero no los fundamentales point-in-time ni las noticias de entonces — y el modelo ya sabe el final de la película. Eso tiene nombre: sesgo retrospectivo (look-ahead bias). Un historial corto y limpio vale más que uno largo y contaminado.
Entre revisiones, un comando de vigilancia compara las señales
archivadas en el contexto_json de cada tesis (auditoría por diseño:
toda tesis guarda los datos con los que se razonó) contra las de hoy, y
reporta cambios de régimen objetivos — cruce de medias invertido,
pérdida de la SMA50, RSI en zona extrema — junto al texto de
invalidación que la tesis declaró. Deliberadamente no interpreta ese
texto con IA: detecta hechos y deja el juicio al humano.
##El resto del sistema, en rápido
- Dashboard: un HTML autocontenido generado por Python — sparklines SVG dibujados a mano (sin librerías), CSS inline, cero peticiones salientes. El markdown de cada tesis se re-renderiza escapando todo primero (el texto viene de un LLM: nada de HTML crudo en mi página) y rebajando su jerarquía de encabezados para no romper el documento.
-
Explorador: screener sobre un universo curado (~90 tickers por
sectores) filtrado por un
perfil.jsoneditable. Reusa el motor de señales verificado y produce un puntaje con motivos legibles — nada de cajas negras. Su primer hallazgo real: una large-cap en sobreventa profunda tras un desplome por earnings, detectada por pura matemática. - Operación: LaunchAgent mensual (día 1, 9:00) que corre revisión + dashboard + respaldo. Los datos personales (base SQLite, reportes, dashboard) viven fuera de git; el código, en un repo privado.
##Números y moraleja
3 días, 12 commits, 111 tests, ~15 comandos de CLI. Construido en pareja con Claude Code, hito por hito, con revisión humana en cada gate y paneles de agentes revisores que encontraron bugs reales ejecutando el código (el mejor: la caché que "envenenaba" el día entero con un fallo transitorio).