signal-agent · monitor de mercado · Python + SQLite + GitHub Actions

Arquitectura de signal-agent

Un monitor de indicadores cortado en dos mitades. La primera, determinista y probada, recolecta cada semana y arma un briefing cada mes en GitHub Actions, sin ninguna llave. La segunda es el juicio: una sesión de Claude Code lee el briefing contra un criterio versionado y emite un JSON que el código valida y guarda como predicciones falsables en un ledger SQLite. Cinco vistas: contenedores, datos, flujos, despliegue y decisiones.

este repo sesión o persona fuera del repo servicio externo tráfico
Detalle

Haz clic en un contenedor

Cada caja es una pieza que corre por separado: un runner de GitHub Actions, el CLI, el ledger, una sesión de juicio. Las líneas son llamadas reales: protocolo y qué viaja.

Por qué dos mitades

La mitad automática no emite ningún juicio, así que cada pieza se prueba sin modelo en el camino: dedup, ventanas, selección, migraciones. La mitad de juicio tiene como entrada un archivo, el briefing, que queda congelado en disco y en la base. La costura entre las dos es ese archivo.

Por qué el ledger es el producto

Cada señal que afirma un movimiento deja una predicción binaria con fecha de vencimiento. El sistema existe para producir una tasa de acierto honesta: un indicador con diez o más predicciones resueltas que no supera el azar queda marcado como candidato a eliminar.

Por qué sin llaves

El código no llama a ninguna API de modelos: el juicio lo aporta la sesión de Claude Code y el código solo valida y persiste. Los workflows no necesitan secretos, solo el token del repositorio. Clonar el repo y correr los comandos funciona sin configurar nada.

Esquema signal.db · modelo entidad-relación (tablas del ledger) · pasa el cursor por una relación

Decisiones de datos, con su razón

dedup
Restricción UNIQUE (domain, content_hash) sobre el contenido normalizado. Nunca por URL: las URL de un agregador de noticias son redirecciones distintas para el mismo artículo.
juicio firmado
signals.judged_by y signals.analyze_version dicen quién juzgó y bajo qué versión del criterio. Sin la versión, el histórico de precisión mezclaría criterios distintos.
confianza
CHECK: no_change exige confidence NULL; trigger y movement la exigen no nula. La confianza es una apuesta sobre una predicción; sin predicción no hay nada que calibrar.
ventana
briefings.window_start/end es un dato, no una inferencia desde la etiqueta YYYY-MM: si se salta una corrida, el briefing siguiente cubre más de un período y se ve.
reconciliación
briefing_indicators guarda found (antes del tope) y rendered, con CHECK (rendered <= found): el juicio tiene que dar cuenta de cada observación que el briefing listó.
frescura
collection_stats registra cuántas entradas trajo una fuente y cuántas cayeron dentro de la ventana: "trajo 100 y ninguna sirve" no deja rastro en observations.
migraciones
Las tablas usan CREATE TABLE IF NOT EXISTS; todo cambio va en core/migrations/NNN_*.sql, transaccional, con respaldo previo. Las nueve vistas se borran y se recrean siempre.
outcomes
La tabla existe y las vistas de puntuación ya leen de ella, pero ningún comando del código la escribe todavía: resolve está declarado en el CLI y falla con un mensaje explícito.

Nueve vistas SQL (v_prediction_scoring, v_indicator_accuracy, v_confidence_calibration, v_overdue_predictions, v_indicator_noise, v_indicator_collection y otras) concentran la política de métricas en SQL, no dispersa en el código.

Secuencias · una por flujo · los mismos pasos que reproduce el C4
Estados · una predicción · clic en un estado

Las transiciones hacia resolved y unresolvable están en el esquema y en las vistas, pero el código que las ejecuta no existe: el CLI declara resolve y expire y falla diciéndolo.

Despliegue · lo que hay y lo que cambiaría

Hoy dos workflows de GitHub Actions que commitean la base. Mañana el mismo binario en un VPS con cron.

No hay servidor ni hosting: el sistema es un repositorio. La base signal.db se versiona a propósito, porque es el histórico de precisión y los workflows dependen de ella. Según el README, los dos workflows están escritos pero todavía no se han ejecutado en GitHub.

Hoy · GitHub Actions

collect.yml: domingos 12:00 UTC, una tarea por dominiocron semanal
brief.yml: día 1 de cada mes, 14:00 UTCcron mensual
Python 3.12, pip install -r requirements.txtubuntu-latest
permisos: contenido en escritura; issues solo en briefGITHUB_TOKEN
un solo grupo de concurrencia para las dos escriturassignal-agent-write
el juicio: sesión de Claude Code fuera del runnermanual, mensual

Calidad: pruebas con pytest, ruff y mypy en modo estricto, también sobre los tests.

→ mismo código →

Al escalar · VPS con cron

mismo binario, otra planificación (el módulo de configuración lo prevé)sin cambios de código
rutas de base, briefings y dominios por variables de entornoSIGNAL_*
reporters adicionales por configuraciónconfig.reporters
base compartida en lugar de SQLite versionadapor confirmar

Qué gana: horarios libres y estado fuera del historial de git. Qué cuesta: un servidor que mantener, justo lo que hoy no existe.

Decisiones · con la alternativa descartada

Cada decisión dice qué se eligió, qué se dejó y por qué

DecisiónPor quéDescartado
Dos mitades unidas por un archivo .mdLa mitad automática se prueba entera porque no hay modelo en el camino; el briefing congelado permite re-juzgar el mismo insumo con otro criterioUn pipeline único que recolecta y juzga: no testeable y no repetible
Sin llamadas a la API de un modeloCosto variable cero, sin acoplamiento a un proveedor y sin secretos: el juicio entra por una sesión de Claude Code y el código solo valida y persisteUn paso del workflow que llame a un modelo: llaves, gasto por corrida y una dependencia más
Colecta semanal, juicio mensualLos feeds RSS tienen profundidad limitada y lo que sale del feed se pierde sin dejar hueco visible; las predicciones tienen horizonte de 180 díasTodo mensual: pérdida silenciosa. Juicio semanal: desajuste de escalas
Los dominios son datos, no códigoUn área nueva es un domain.yaml con indicadores, condición de disparo y collectors; el id de un indicador nunca se renombra porque es la llave del históricoUn módulo Python por dominio
El tope de observaciones está en el renderEl collector devuelve todo lo de la ventana; el briefing corta y reporta la pérdida. La resolución necesita la evidencia completa del períodoCortar en el collector: el ledger resolvería contra una vista parcial del mundo
Diversidad de fuente antes que recenciaPrimero entra una observación por fuente distinta; si no, una nota de agencia replicada por doce sitios consume todo el presupuesto del indicadorOrdenar solo por fecha
Dedup por contenido, nunca por URLLas URL de un agregador son redirecciones distintas para el mismo artículoRestricción única sobre la URL
Anti-anclaje por tipoLa estructura de las predicciones vencidas no tiene campos de confianza ni de razonamiento, así que el renderer no puede imprimirlos ni por errorOmitirlos en la plantilla: depende de la disciplina de quien la edite
judge nunca lee la tabla de señalesRe-juzgar el mismo briefing no se contamina con el juicio anterior; tres pruebas defienden la propiedadMostrar el juicio previo como contexto
Superseder conserva lo resueltoRe-juzgar cancela las predicciones abiertas pero lo ya resuelto sigue contando bajo su versión de criterio; lo que pasó en el mundo no cambiaBorrar el juicio viejo: un botón de limpiar historial
Pre-registro del experimentoPredicciones, umbrales y reintentos escritos antes de correr; un criterio escrito después del resultado no significa nadaAjustar el criterio mirando el mes concreto