Golden SkyGolden SkyArquitectura

Arquitectura · A3 · Dominio y servicio

El dominio calcula, el servicio orquesta

La regla que mantiene el cálculo reutilizable: cada función de dominio es una unidad aislada y pura —recibe datos, devuelve datos— sin saber de la API, el disco ni el run. El servicio es el que coordina, lee y escribe.

§1Dos responsabilidades, dos capas

Nunca se mezclan. Si algo se puede sacar a una función pura, va al dominio; si coordina o toca el mundo exterior, es del servicio.

domain/ — calcula
  • Transformaciones de datos
  • Tests estadísticos, métricas
  • Reglas de dominio
  • Puro: solo entra y sale data
services/ — orquesta
  • Lee/escribe artefactos (storage)
  • Valida el estado del run
  • Serializa el filelock por (run, paso)
  • Compone llamadas al dominio

El servicio traduce entre los schemas Pydantic del contrato y las entradas primitivas (arrays, DataFrames) que el dominio espera. La concurrencia (un filelock por (run_id, paso)) y el marcador is_stale —que avisa, sin borrar, cuando re-ejecutás un paso anterior— son responsabilidad del servicio, nunca del dominio.

§2Las seis reglas de una función de dominio

Toda función en domain/ las cumple. Juntas garantizan que se pueda invocar desde cualquier lado sin arrastrar el resto del sistema.

Entrada tipada primitiva. Recibe np.ndarray, DataFrame, int, dict — no objetos Pydantic del schema, no run_id, no requests.
Salida simple. Devuelve dict, dataclass, DataFrame o primitivo — no una respuesta HTTP.
Sin side-effects. No imprime, no logea a archivo, no persiste, no lee del disco, no llama a la red. Solo calcula.
Determinista. Misma entrada → misma salida. Lo aleatorio usa un seed parametrizable.
Testeable sin bootstrap. Los tests usan entrada sintética; no requieren HTTP, base de datos ni levantar el servicio.
Un archivo por función lógica. domain/{modulo}/{funcion}.py en vez de un utils.py gigante.

§3El mismo endpoint, dos formas

La tentación es meter el cálculo en el servicio "porque es más rápido escribir todo junto". El resultado es lógica imposible de reusar o testear sin la API.

mal — cálculo en el serviciodef analyze_filter(run_id, config): trades = storage.load_trades(run_id, config.combo_id) excluded = np.isin(trades.regime, config.excluded_regimes) # ... 40 líneas de métricas + tests estadísticos ... return AnalyzeResponse(metrics=..., jk=..., losers=...)
bien — servicio orquesta, dominio calculadef analyze_filter(run_id, config): trades = storage.load_trades(run_id, config.combo_id) filtered = _apply_excluded_regimes(trades, config.excluded_regimes) return AnalyzeResponse( metrics=compute_metrics_delta(trades, filtered), jk=compute_jk_test(trades.returns, filtered.returns), losers=compute_losers_excluded_ratio(...), )

Cada compute_* vive en su propio archivo del dominio, con su test. El servicio solo lee, arma la entrada y compone la salida.

§4Por qué importa

La pureza del dominio no es purismo: compra cuatro cosas concretas.

BeneficioEn la práctica
Reusabilidad cross-stepLa misma función (ej. losers_ratio) sirve al refinamiento y al transfer sin duplicar.
Análisis ad-hocEl trader abre un notebook, importa la función y la corre sobre datos exploratorios sin levantar la API.
Tests rápidosValidar un cálculo no necesita FastAPI ni disco: genera arrays, corre, chequea.
Refactor localCambiar cómo se calcula algo toca solo su archivo del dominio y su test — no el servicio ni las rutas.

§5La forma protege el resultado

El webapp es la capa visible de un pipeline de descubrimiento cuantitativo. Si introdujera uno de los cinco antipatrones clásicos, invalidaría todo lo que está arriba. La arquitectura los frena activamente, en el código.

AntipatrónDónde lo frena la arquitectura
Data LeakageUn checker escanea filtraciones; un CRITICAL fuerza REJECT — el endpoint devuelve 422 y NO persiste el resultado.
Look-Aheadshift(1) en las señales rolling; el HTF se desplaza para estar disponible solo desde su cierre.
Data SnoopingWalk-forward: test de permutación por combo + corrección FDR-BH sobre los p-values.
OverfittingSplits IS/OOS estrictamente temporales; el webapp no permite optimizar hiperparámetros sobre OOS.
Selection BiasSe reporta el conjunto evaluado completo — el grid entero de calibración, la lista completa de instrumentos (incluidos los sin datos).

Que estos guards vivan en el dominio (puro y testeable) es lo que permite probar cada uno de forma aislada: un test verifica que un CRITICAL fuerza REJECT sin levantar el servidor. La integridad no es una promesa — es un test.

Viene de A2 (fuente de verdad). Sigue en A4: el walkthrough de 9 pasos para agregar un módulo nuevo de punta a punta.

Fuente: webapp/ARCHITECTURE.md §1.5 (antipatrones), §5.5 (dominio vs servicio).