Signal
Quant & research
Sistemas de investigación que no se mienten a sí mismos: ingesta read-only, clasificación de régimen, memoria de mercado y calibración. La columna vertebral de XCAP.
SG-01 Ingesta read-only & edge sin llaves
lección Construir un edge de ingesta read-only que trae datos públicos del mundo a disco sin exponer ni una sola credencial, y que es estructuralmente incapaz de operar o mover dinero.
Ingesta read-only & edge sin llaves
lecciónConstruir un edge de ingesta read-only que trae datos públicos del mundo a disco sin exponer ni una sola credencial, y que es estructuralmente incapaz de operar o mover dinero.
Un sistema de research que toca la red por todas partes es un sistema que puede filtrar llaves, ejecutar trades por accidente y contaminar resultados con datos no reproducibles. El edge keyless es la frontera donde el mundo entra sin poder hacer daño.
LA LECCIÓN
La intuición central de XCAP es brutal y poco intuitiva para quien viene de SaaS: en un sistema de investigación de trading, la red no es una conveniencia, es la superficie de ataque más grande que tienes. Cada endpoint con llave es una credencial que puede filtrarse; cada llamada de escritura es un trade que puede dispararse por un bug. Por eso XCAP colapsa toda su exposición de red a un único punto: un edge de ingesta read-only, keyless y GET-only. El resto del sistema es determinista desde disco. Si entiendes esto, entiendes por qué `binance_public.py` solo pide klines (velas OHLCV históricas) por HTTP GET, sin API key, sin firmar nada, sin tocar jamás un endpoint de cuenta o de orden.
Concretamente, el patrón es: una fuente abstracta (en XCAP, una jerarquía `OHLCSource`) con dos implementaciones. `LocalCSVSource` lee CSVs ya en disco — cero red, el caso por defecto, el que usan TODOS los loops automáticos. Y `HttpOHLCSource` (respaldado por `binance_public.py`) que solo se activa cuando un caller pasa explícitamente `allow_network=True` o invoca el comando `data-fetch`. La asimetría es deliberada: el camino seguro es el default y el camino con red exige una opción explícita en cada llamada. Nunca un flag global ambiental, nunca una variable de entorno que se quede encendida.
El endpoint de Binance que usas es el público de mercado: `GET https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit=1000`. Devuelve un array de arrays: `[openTime, open, high, low, close, volume, closeTime, ...]`. Fíjate en lo que NO hay aquí: no hay header `X-MBX-APIKEY`, no hay query param de firma HMAC, no hay timestamp firmado. Ese endpoint físicamente no puede leer tu balance ni colocar una orden porque la API de cuenta de Binance vive en otra ruta y exige firma. La seguridad no es una promesa tuya de portarte bien: es una propiedad del endpoint que elegiste.
El segundo pilar es la normalización en el borde. Datos del mundo real llegan sucios: timestamps en milisegundos, números como strings, velas duplicadas, huecos. Tu edge debe normalizar a UN formato canónico antes de que nada downstream los toque. En XCAP los CSVs de `data/real` son lowercase (`open,high,low,close,volume`), timestamps parseados a fechas, y `LocalCSVSource` los lee siempre igual. Esta disciplina es lo que hace reproducible al sistema: el clasificador de régimen (SG-02) y la Market Memory (SG-03) leen de disco un formato estable, no de una respuesta HTTP volátil que cambia entre corridas.
La verificación es el cierre. XCAP tiene un comando `check` (corre con `PYTHONPATH=src python3 -m xcap.control check`) que afirma como invariante que el connector es keyless, read-only, no puede operar y no puede mover dinero. No es un test que pasa una vez: es una aserción que corre en cada `check`, de modo que si alguien añade una llave o un método de escritura, el `check` se pone rojo. Así es como conviertes una intención de diseño en una propiedad que el sistema defiende solo. La regla de oro: la seguridad que depende de que recuerdes algo no es seguridad; la seguridad que un test rojo te obliga a respetar, sí.
Un matiz operativo importante: el dashboard de XCAP bindea solo a `127.0.0.1`, nunca a `0.0.0.0`. El edge de ingesta es la única superficie de salida; la de entrada (el dashboard) no escucha en la red. Y FileVault (AES-256) cifra el disco en reposo. La cadena completa — disco cifrado, ingest keyless read-only, dashboard local-only, Capital Gate cerrado — es lo que permite tener un sistema de trading-research en un portátil sin que sea una bomba de relojería.
EJERCICIO
Implementa una fuente de datos con dos backends. Crea `ohlc_source.py` con una clase base `OHLCSource` (método `get(symbol, interval, limit) -> list[Bar]`). Implementa `LocalCSVSource` (lee de `./data/{symbol}.csv`, sin red) y `HttpOHLCSource` (GET a `api.binance.com/api/v3/klines`, sin ninguna API key). El constructor de `HttpOHLCSource` debe recibir `allow_network: bool = False` y lanzar `RuntimeError('network not allowed')` si se llama `get()` con `allow_network=False`. Normaliza ambos a la misma estructura `Bar(time, open, high, low, close, volume)` con floats y fecha parseada. Escribe `check_connector()` que verifique por introspección que la clase HTTP no tiene ningún método ni atributo cuyo nombre contenga 'order', 'trade', 'sign', 'key', 'secret' o 'withdraw', y que falle si lo encuentra.
ENTREGABLE
`ohlc_source.py` con `LocalCSVSource` + `HttpOHLCSource(allow_network=False)` por defecto, normalización a `Bar` canónico, y una función `check_connector()` que afirma keyless/read-only por introspección y devuelve un reporte PASS/FAIL.
INSIGHT CLAVE
El camino seguro debe ser el default y el camino peligroso debe exigir una opción explícita en cada llamada — nunca un flag global. Un `allow_network=True` que se pasa por argumento en cada uso es seguro; una variable de entorno `ALLOW_NETWORK=1` que queda encendida es una fuga esperando ocurrir.
ERRORES A EVITAR
- ×Usar un endpoint que requiere API key 'porque da más datos' — en el momento que firmas una petición, tu edge ya no es estructuralmente incapaz de operar, y toda la garantía se cae.
- ×Poner el control de red en una variable de entorno global en vez de un argumento explícito por llamada; queda encendida entre corridas y los loops automáticos heredan red sin querer.
- ×No normalizar en el borde: dejar que timestamps en ms y números como string se propaguen downstream, rompiendo la reproducibilidad entre la fuente local y la HTTP.
- ×Bindear el dashboard a `0.0.0.0` para 'verlo desde el móvil' — expones a la red toda tu investigación; usa `127.0.0.1` y un túnel SSH si de verdad lo necesitas.
- ×Verificar la propiedad keyless una sola vez a mano en vez de codificarla como aserción en un `check` que corre siempre; la garantía se erosiona en el primer refactor que nadie revisa.
SG-02 Clasificador de régimen de precio
lección Construir un clasificador de régimen de precio puro y determinista que etiqueta el estado del mercado (tendencia, volatilidad, comportamiento, estrés) sin jamás dimensionar ni operar una posición.
Clasificador de régimen de precio
lecciónConstruir un clasificador de régimen de precio puro y determinista que etiqueta el estado del mercado (tendencia, volatilidad, comportamiento, estrés) sin jamás dimensionar ni operar una posición.
Decidir sin saber en qué régimen estás es operar a ciegas: una estrategia que gana en tendencia se desangra en rango. La etiqueta de régimen es la condición previa honesta de cualquier decisión, y separarla de la ejecución evita que el sesgo de 'querer operar' contamine el diagnóstico.
LA LECCIÓN
El error conceptual que XCAP corrigió en su auditoría de junio 2026 es sutil y vale oro: confundir salud operacional con régimen de precio. `market_state.py` mide salud del sistema (¿tengo datos frescos?, ¿están los flags bien?). `regime.py` mide algo totalmente distinto: ¿en qué ESTADO está el mercado mismo? Son ejes ortogonales y mezclarlos produce un clasificador que no sabe qué está diciendo. La primera disciplina es: un clasificador de régimen habla solo del precio, nunca del estado de tu infraestructura.
El diseño de XCAP clasifica a lo largo de cuatro ejes independientes. Trend (¿sube, baja, lateral? — vía pendiente de medias o retorno acumulado sobre una ventana). Volatility (¿calma o agitado? — vía desviación estándar de retornos, normalizada). Behavior (¿persistente/tendencial o mean-reverting? — vía autocorrelación de retornos). Y Stress (¿hay un drawdown desarrollándose?). Cada eje se calcula de forma pura: misma entrada, misma salida, sin estado oculto, sin aleatoriedad. Luego una cascada de prioridad documentada deriva un `Regime` titular único a partir de los cuatro ejes.
El insight más afilado de XCAP está en cómo define Panic, y es donde casi todo el mundo se equivoca. La tentación es definir pánico como 'volatilidad alta' (ratio de stdev elevado). Es incorrecto. Un crash monótono — el precio cayendo en línea casi recta — tiene volatilidad INTERNA baja, porque cada vela se parece a la anterior. Si defines pánico por stdev, te pierdes exactamente el crash más peligroso. La definición correcta de XCAP: pánico es un drawdown grande desarrollándose RÁPIDO, medido por un retorno reciente muy negativo y agudo, no por la dispersión. Esto es lo que distingue a un clasificador pensado por alguien que miró crashes reales de uno que copió una fórmula de un libro.
La pureza no es un capricho estético, es la base de la confianza. `regime.py` es una función pura: `classify(bars) -> Regime`. No lee de disco dentro de sí, no llama a la red, no muta nada global, no tiene rama aleatoria. Esto significa que puedes correrla mil veces sobre la misma ventana y obtener la misma etiqueta, puedes testearla con fixtures deterministas, y puedes auditar exactamente por qué dijo 'Panic' en una fecha concreta. Un clasificador con estado oculto es un clasificador en el que no puedes confiar para construir Market Memory encima.
La regla más importante, y la que conecta con toda la constelación: el clasificador es label-only. JAMÁS dimensiona ni opera. Devuelve una etiqueta y nada más. La razón es de arquitectura de honestidad: si el mismo módulo que diagnostica el régimen también decide el tamaño de la posición, tienes un incentivo para que el diagnóstico justifique la operación que ya querías hacer. Separar el diagnóstico de la acción es lo que permite, en SG-03, medir honestamente si el régimen tiene valor predictivo — porque la etiqueta se generó sin saber qué ibas a hacer con ella.
Para implementarlo bien, documenta la cascada de prioridad como código legible, no como una maraña de ifs. Por ejemplo: si Stress dispara Panic, gana Panic sobre cualquier otra cosa; si no, y Volatility es alta con Trend fuerte, es Trending-Volatile; si Behavior es mean-reverting y Trend es lateral, es Range; etc. Cada rama debe tener un comentario que explique el porqué del mercado, no solo el umbral. Ese comentario es lo que un revisor — o tú mismo en seis meses — necesita para confiar en la etiqueta.
EJERCICIO
Escribe `regime.py` con una función pura `classify(bars: list[Bar]) -> Regime`. Calcula cuatro sub-señales: `trend` (signo y magnitud del retorno sobre la ventana), `vol` (stdev de retornos diarios), `behavior` (autocorrelación lag-1 de retornos), `stress` (drawdown máximo reciente Y rapidez = retorno de los últimos N días). Implementa la cascada de prioridad donde Panic = drawdown grande + retorno reciente fuertemente negativo (NO stdev alto). Devuelve un `Regime` con el label titular y los cuatro ejes expuestos. Prueba con tres fixtures sintéticos: una tendencia alcista limpia, un crash monótono (debe dar Panic pese a vol interna baja), y un rango ruidoso. Verifica que la función es pura corriendo `classify` dos veces sobre la misma entrada y afirmando igualdad.
ENTREGABLE
`regime.py` con `classify(bars) -> Regime` puro y determinista, cuatro ejes (trend/vol/behavior/stress), cascada de prioridad documentada, y tres tests de fixtures donde el crash monótono se clasifica como Panic correctamente.
INSIGHT CLAVE
Pánico no es volatilidad alta — un crash monótono tiene volatilidad interna baja porque cada vela imita a la anterior. Define pánico por la velocidad del drawdown (retorno reciente agudo), no por la dispersión, o te perderás justo el régimen más peligroso.
ERRORES A EVITAR
- ×Mezclar régimen de precio con salud operacional en el mismo módulo; son ejes ortogonales y juntarlos produce etiquetas que no significan nada concreto.
- ×Definir Panic por ratio de stdev alto: pierdes los crashes monótonos, que son justo los que importan.
- ×Meter una rama aleatoria o una lectura de disco dentro del clasificador, rompiendo su pureza y haciendo imposible auditar por qué etiquetó una fecha.
- ×Dejar que el clasificador dimensione o sugiera operaciones; en el momento que diagnóstico y acción comparten módulo, el diagnóstico empieza a justificar la operación que ya querías.
- ×Esconder la cascada de prioridad en ifs sin comentar el porqué de mercado; nadie podrá confiar ni revisar el umbral en seis meses.
SG-03 Market Memory: forecast + calibración
lección Construir Market Memory: un ledger que bloquea una predicción falsable ANTES del desenlace, la resuelve contra el retorno realizado, y puntúa su propia calibración (hit-rate, Brier, calibración por confianza y por régimen).
Market Memory: forecast + calibración
lecciónConstruir Market Memory: un ledger que bloquea una predicción falsable ANTES del desenlace, la resuelve contra el retorno realizado, y puntúa su propia calibración (hit-rate, Brier, calibración por confianza y por régimen).
Sin un registro de predicciones bloqueadas antes del resultado, un sistema de research no tiene forma honesta de saber si sabe algo. Es la diferencia entre 'creo que funciona' y 'aquí está mi historial out-of-sample puntuado'. Es la única fuente real de evidencia de edge.
LA LECCIÓN
El hallazgo central de la auditoría de XCAP fue demoledor: cada módulo previo era o computación stateless o un acumulador pasivo; NADA registraba una predicción falsable antes del desenlace y luego puntuaba su propia calibración. Ese loop ausente es exactamente lo que hace que un sistema de research sea difícil de engañar, y la única fuente honesta de evidencia de edge. Market Memory (`market_memory.py`) es ese loop. Si no construyes este módulo, todo lo demás es teatro: cálculos bonitos que nunca se confrontan con la realidad.
El corazón es `advance_market_memory(universe, state_dir, horizon_days=5, ...)` y su secuencia es sagrada en este orden: observar fechas nuevas → clasificar el régimen (usando el `regime.py` de SG-02) → registrar un forecast BLOQUEADO (un baseline derivado del régimen, con su probabilidad y su horizonte) → resolver los forecasts que ya maduraron contra el retorno forward realizado → puntuar. El bloqueo es la palabra clave: una vez registras 'en la fecha T predigo prob_up=0.62 a 5 días', ese registro es inmutable. No puedes volver a tocarlo cuando ves el resultado. Esa inmutabilidad es lo que convierte el ledger en evidencia y no en una racionalización a posteriori.
Las métricas no son una sola; son un panel honesto. Hit-rate (¿qué fracción de predicciones direccionales acertó?). Brier score (¿qué tan bien calibradas estaban las probabilidades? — penaliza tanto la sobreconfianza como la subconfianza). Calibración por bucket de confianza (cuando dijiste 70%, ¿acertaste el 70% de las veces?). Y la joya: hit-rate POR RÉGIMEN. Esta última es la que de verdad informa: te dice si tu predicción tiene valor en tendencia pero es basura en rango, o si solo aciertas en calma. Un hit-rate global de 55% puede esconder 70% en tendencia y 40% en pánico — y esa descomposición es la que dice dónde, si acaso, hay edge.
La propiedad anti-fabricación es por construcción (la profundizamos en SG-04, pero aquí es estructural): solo se resuelven forecasts cuyo horizonte ya maduró con datos que existían DESPUÉS de la fecha del forecast. Es out-of-sample por diseño. Nunca puedes 'predecir' una fecha cuyo resultado ya conoces, porque el forecast se bloqueó en T y solo se resuelve cuando llega T+horizon con datos reales. El state dir está gitignored — no se versiona el historial para que no haya tentación de editarlo a mano y maquillar resultados.
Las tres propiedades de integridad que XCAP impone, y que debes replicar: acumula (el ledger crece entre corridas, nunca se resetea), idempotente (correr el tick dos veces sobre las mismas fechas no duplica forecasts ni los resuelve dos veces), y Gate-closed (jamás opera). Esta es la misma familia de invariantes que el capital invariant de SG-05: el estado del sistema es la suma de eventos reales, nunca un replay ni un baseline fresco. El CLI `market-memory-tick` orquesta esto y lee CSVs formato `data/real` (lowercase) vía `LocalCSVSource` — cero red, todo determinista desde disco.
Un punto que separa al ingeniero del aficionado: el baseline que registras debe ser honesto y modesto. XCAP registra un 'regime_baseline' — una probabilidad derivada del régimen, no un modelo elaborado que ya optimizaste mirando el pasado. ¿Por qué empezar humilde? Porque el baseline es tu vara de medir. Si un forecaster sofisticado no le gana al baseline de régimen out-of-sample, no tienes edge, tienes overfitting. Market Memory existe para refutar tus ideas, no para confirmarlas. Y un buen registro de forecasts es evidencia, nunca prueba — por eso el Capital Gate sigue cerrado por mucho que el hit-rate se vea bien.
EJERCICIO
Construye `market_memory.py` con un ledger en JSON-lines append-only. Implementa `advance(universe, state_dir, horizon_days=5)` que: (1) lea las fechas nuevas, (2) para cada fecha clasifique régimen con tu `regime.py`, (3) registre un forecast bloqueado `{date, symbol, regime, prob_up, horizon, resolved: false}`, (4) resuelva los forecasts maduros (donde existe la barra a date+horizon) calculando el retorno forward real y marcando `hit`, (5) escriba métricas: hit-rate global, Brier, y un desglose de hit-rate POR régimen. Haz el tick idempotente: si ya existe un forecast para (date, symbol), no lo dupliques; si ya está resuelto, no lo re-resuelvas. Demuestra la acumulación corriendo el tick sobre dos rangos de fechas consecutivos y mostrando que el ledger crece sin resetearse.
ENTREGABLE
`market_memory.py` + ledger append-only en `state/` (gitignored) con forecasts bloqueados antes del desenlace, resolución out-of-sample, métricas hit-rate/Brier/calibración-por-régimen, y un CLI `market-memory-tick` idempotente y acumulativo.
INSIGHT CLAVE
La métrica que de verdad informa es el hit-rate POR RÉGIMEN, no el global. Un 55% global puede esconder 70% en tendencia y 40% en pánico — y esa descomposición es la única que te dice dónde, si acaso, hay edge real que explotar.
ERRORES A EVITAR
- ×Registrar la predicción y resolverla en la misma corrida mirando ya el resultado; eso no es forecast, es ajuste a posteriori, y destruye toda evidencia de edge.
- ×Permitir que el ledger se resetee entre corridas; pierdes el historial que es justamente el activo, y rompes la propiedad de acumulación.
- ×Hacer el tick no idempotente: correrlo dos veces duplica forecasts o re-resuelve, inflando o corrompiendo las métricas.
- ×Reportar solo el hit-rate global y esconder el desglose por régimen, que es donde vive la información accionable.
- ×Versionar el state dir en git, abriendo la puerta a editar el historial a mano y maquillar la calibración; mantenlo gitignored.
SG-04 Anti-fabricación por construcción
lección Diseñar el sistema para que sea estructuralmente incapaz de inventarse datos o edge: null controls, significancia con corrección de comparaciones múltiples, evaluación out-of-sample y un edge_demonstrated que es siempre False por defecto.
Anti-fabricación por construcción
lecciónDiseñar el sistema para que sea estructuralmente incapaz de inventarse datos o edge: null controls, significancia con corrección de comparaciones múltiples, evaluación out-of-sample y un edge_demonstrated que es siempre False por defecto.
La forma más cara de mentirse en research es producir números que parecen edge pero son ruido, deriva o data-snooping. La anti-fabricación no es una revisión final: es una propiedad arquitectónica que hace imposible que un forecaster cante victoria sin haberle ganado al azar Y a la deriva, con significancia.
LA LECCIÓN
La pregunta que define este módulo: ¿cómo construyes un sistema que no PUEDA mentirte sobre si tiene edge? La respuesta de XCAP no es 'ser cuidadoso'. Es introducir adversarios estructurales — null controls — contra los que toda afirmación de edge debe competir. El Hypothesis Lab (`hypothesis_lab.py`) tiene un registro de forecasters reales (regime_baseline, trend_follow, momentum_follow, mean_revert) Y dos controles nulos deliberados: `always_up` (el null de DERIVA — el mercado sube de media, ¿tu forecaster solo está capturando eso?) y `coin_flip` (el null de AZAR — sembrado y determinista, ¿le ganas a tirar una moneda?). Un forecaster que no le gana a AMBOS nulos no tiene edge, tiene una ilusión.
El segundo pilar es la significancia con corrección por comparaciones múltiples. Si pruebas veinte forecasters, alguno parecerá bueno por puro azar — es el problema del data-snooping. XCAP lo neutraliza con un umbral z corregido por Bonferroni que ESCALA con K (el número de forecasters evaluados): cuantas más hipótesis pruebas, más alta la vara que cada una debe superar. `evaluate_forecasters(universe, names, horizon_days, min_samples, alpha)` compara cada forecaster real contra ambos nulos (Δazar y Δderiva) y solo le otorga `candidate_edge` si le gana al azar Y a la deriva Y supera el umbral corregido Y tiene al menos `min_samples` observaciones. Las cuatro condiciones, conjuntas. Quitar cualquiera abre una puerta a la fabricación.
El detalle que demuestra que el sistema es honesto y no está amañado: `edge_demonstrated` es SIEMPRE False. El lab puede otorgar `candidate_edge` — 'esto merece más investigación' — pero nunca declara edge demostrado, porque ningún backtest, por bueno que sea, prueba edge futuro. Esta es una decisión de diseño que protege contra tu propio optimismo: el sistema literalmente no tiene un camino de código que diga 'sí, tienes edge, opera'. El Capital Gate sigue cerrado por construcción, no por tu disciplina.
La verificación de que el scoreboard no está trucado es elegante y la debes replicar: XCAP comprobó que sobre tendencias sintéticas, `trend_follow` gana en grande (detecta la señal real), `mean_revert` se va significativamente NEGATIVO (detecta un anti-edge — apostar contra la tendencia pierde), y `coin_flip` aterriza en ~50% (el azar no se ve premiado). Si tu coin_flip saliera con 60% de hit-rate, tu pipeline está roto o tienes leakage. Los nulls no son solo adversarios: son sondas de calibración de tu propio sistema de medición. Un coin_flip que no da ~50% es un sistema que se está mintiendo, y debes pararlo todo hasta entender por qué.
El fundamento compartido evita el truco de mover la portería: `scoring.py` es la única fuente de verdad para las primitivas (prob_up, is_hit, brier, conf_bucket, hit_rate_z). Tanto Market Memory (SG-03) como el Hypothesis Lab puntúan con EL MISMO código. Si tuvieras dos implementaciones de 'hit', podrías — consciente o no — usar la más generosa para tu forecaster favorito. Una sola primitiva compartida cierra esa puerta. Esto es anti-fabricación por construcción: no puedes elegir la regla que te conviene porque solo hay una regla.
Toda evaluación es out-of-sample por la misma mecánica de SG-03: cada barra se clasifica una sola vez, el forecast se genera sin ver el futuro, y se resuelve contra el retorno forward real. El doc `docs/HYPOTHESIS_LAB.md` y los 4 invariantes del `check` codifican estas garantías. La lección de fondo, que es la tesis de toda la constelación Signal: el binding constraint para operar capital real es edge DEMOSTRADO, y estas herramientas miden edge honestamente (null controls + significancia) en vez de afirmarlo. Un sistema que solo puede emitir `candidate_edge` y nunca `edge_demonstrated` es un sistema que respeta lo que no sabe.
EJERCICIO
Extiende tu Hypothesis Lab con `forecasters.py` (registro con al menos `trend_follow`, `mean_revert` y dos nulls: `always_up` para deriva y `coin_flip` sembrado para azar) y `evaluate_forecasters(names, horizon_days, min_samples, alpha)`. Para cada forecaster real calcula su hit-rate out-of-sample y el delta contra AMBOS nulls. Implementa un umbral z con corrección Bonferroni que escale con K = número de forecasters. Otorga `candidate_edge=True` solo si gana a azar Y a deriva Y supera el umbral Y tiene >= min_samples. Fija `edge_demonstrated=False` siempre. Valida tu pipeline sobre una serie sintética con tendencia: `trend_follow` debe salir significativamente positivo, `mean_revert` significativamente negativo, y `coin_flip` debe aterrizar en ~50% (si no, tu medición tiene leakage).
ENTREGABLE
`forecasters.py` + `hypothesis_lab.py` con dos null controls (azar y deriva), umbral z Bonferroni que escala con K, gate de cuatro condiciones para `candidate_edge`, `edge_demonstrated` siempre False, y una validación donde coin_flip sale ~50% probando que el scoreboard no está trucado.
INSIGHT CLAVE
Los null controls no son solo adversarios contra los que compites — son sondas de calibración de tu propio sistema de medición. Si tu coin_flip no sale ~50%, tienes leakage o un bug, y debes parar TODO hasta entender por qué antes de creerte cualquier otro número.
ERRORES A EVITAR
- ×Comparar el forecaster solo contra el azar y olvidar la deriva; muchos 'edges' son solo el sesgo alcista de fondo del mercado disfrazado.
- ×No corregir por comparaciones múltiples: prueba 20 forecasters y uno parecerá significativo por puro azar (data-snooping).
- ×Permitir un camino de código que declare `edge_demonstrated=True`; ningún backtest prueba edge futuro, y ese flag invita a abrir el Capital Gate prematuramente.
- ×Tener dos implementaciones de 'hit' o 'brier' en vez de una sola primitiva compartida; te deja elegir la regla más generosa para tu forecaster favorito.
- ×Ignorar que el coin_flip salió 60%: eso no es suerte, es leakage en tu pipeline, y cualquier otro resultado del lab es basura hasta arreglarlo.
SG-05 Capital invariant
lección Implementar el capital invariant: un ledger donde el capital acumula entre sesiones, solo se mueve en cierres REALES (P&L realizado), y nunca se resetea — de modo que el número en pantalla siempre refleja resultados cerrados, no replays ni ganancias de papel.
Capital invariant
lecciónImplementar el capital invariant: un ledger donde el capital acumula entre sesiones, solo se mueve en cierres REALES (P&L realizado), y nunca se resetea — de modo que el número en pantalla siempre refleja resultados cerrados, no replays ni ganancias de papel.
Un balance que se infla con P&L no realizado o que se resetea entre corridas es un balance que miente. La integridad del capital es la condición sin la cual ningún otro número del sistema merece confianza: si la contabilidad puede mentir, todo lo demás también.
LA LECCIÓN
Este módulo nace de un fallo real y doloroso en XCAP, y por eso enseña tan bien. Un autorun programado estaba silenciosamente RESETEANDO el ledger al balance inicial de 500 y reproduciendo el mismo fixture idéntico cada 5 minutos. El sistema mostraba actividad, números cambiando, parecía vivo — y era completamente falso. Tres reglas se violaban a la vez, y son las tres que definen el capital invariant. Memoriza estas reglas porque son el núcleo: acumula, realized-only, y re-entry continúa sesiones previas.
Regla 1 — Acumula. El capital NO se resetea entre corridas ni sesiones. Si terminaste la sesión de ayer con 547.30, hoy empiezas con 547.30. Un baseline fresco cada arranque no es contabilidad, es una demo disfrazada. El ledger se carga del disco al iniciar y se persiste al terminar; el estado vive entre ejecuciones.
Regla 2 — Realized-only. El balance solo se mueve cuando una posición se CIERRA/vende. El P&L no realizado (mark-to-market, el valor flotante de una posición abierta) jamás infla el balance. La invariante exacta, que debes poder afirmar en cualquier momento: `current_balance == starting_balance + realized_pnl`. Una posición abierta con +200 de ganancia flotante NO cambia el balance; solo lo hace cuando la cierras y esos +200 se vuelven realizados. Esto evita la trampa psicológica más vieja del trading: contar las ganancias antes de embolsarlas.
Regla 3 — Re-entry continúa. Al reiniciar, el capital es el resultado acumulado de las operaciones de sesiones previas, no una línea base nueva. El sistema debe poder apagarse y encenderse mil veces y el capital ser siempre la suma honesta de todos los cierres reales que ocurrieron. Esto es lo mismo que la propiedad de acumulación de Market Memory (SG-03): el estado es la suma de eventos reales, una sola dirección, sin replay.
La lección de arquitectura es la distinción entre dos comandos que XCAP mantiene deliberadamente separados. El autorun programado corre `sim-autopilot-tick`: carga el ledger existente, rota regímenes sintéticos, y ACUMULA sobre lo anterior. Existe también `sim-run-agent`, que resetea y reproduce un fixture — pero ese se conserva SOLO para demos deterministas, y JAMÁS debe ser lo que corre el autorun. El bug fue precisamente apuntar el scheduler al comando equivocado. La enseñanza: un comando que resetea y un comando que acumula nunca deben ser confundibles; nómbralos de forma que el peligroso grite lo que hace.
Cómo se blinda esto para que no vuelva a romperse: la invariante está LOCKED por `tests/test_sim_autopilot_tick.py`. Si tocas el ledger o el autorun, esos tests deben seguir verdes. No es documentación, es un candado ejecutable. La regla operativa que el usuario impuso enfáticamente: si tocas la contabilidad, corres ese test, y si se pone rojo, has roto la integridad del capital y paras. Esta es la misma filosofía que el Capital Gate y el `check` de SG-01: las invariantes que de verdad importan se codifican como tests que se niegan a dejarte romperlas. La contabilidad de XCAP no es confiable porque el autor sea cuidadoso; es confiable porque un test rojo le impide ser descuidado.
EJERCICIO
Implementa `ledger.py` con un balance persistido en `state/ledger.json`. Funciones: `load()` (lee el balance acumulado, o el starting_balance solo en el primerísimo arranque), `open_position(symbol, entry, size)`, `mark(symbol, price)` (calcula P&L NO realizado pero NO toca el balance), y `close_position(symbol, exit)` (realiza el P&L y SOLO entonces actualiza el balance, luego persiste). Mantén la invariante `current_balance == starting_balance + sum(realized_pnl)` y afírmala tras cada operación. Escribe `test_capital_invariant.py` que: (1) abra una posición ganadora, llame `mark` con precio superior, y verifique que el balance NO cambió; (2) la cierre y verifique que ahora SÍ subió exactamente el P&L realizado; (3) simule un reinicio (load tras persistir) y verifique que el balance continúa, no se resetea.
ENTREGABLE
`ledger.py` con balance acumulativo persistido + `test_capital_invariant.py` que prueba las tres reglas: mark no mueve el balance, close sí lo mueve por el P&L realizado exacto, y un reinicio continúa el acumulado en vez de resetear.
INSIGHT CLAVE
La invariante `current_balance == starting_balance + realized_pnl` es una sola línea que, afirmada tras cada operación, hace estructuralmente imposible que el P&L de papel o un replay inflen tu balance. Cuando la contabilidad es una ecuación verificable y no una creencia, deja de poder mentirte.
ERRORES A EVITAR
- ×Resetear el ledger al starting_balance en cada arranque del autorun — el bug original de XCAP; muestra actividad falsa y borra todo historial real.
- ×Dejar que el P&L no realizado (mark-to-market) toque el balance; cuentas ganancias que aún no embolsaste y el número en pantalla miente.
- ×Confundir el comando que acumula (`sim-autopilot-tick`) con el que resetea-y-reproduce (`sim-run-agent`); nombra el peligroso para que sea imposible apuntarle el scheduler por error.
- ×Tocar el ledger o el autorun sin correr `test_capital_invariant.py` después; la invariante se erosiona en el primer refactor que nadie verificó.
- ×Tratar la invariante como un comentario en el código en vez de una aserción ejecutada tras cada operación; un comentario no impide que la rompas, un assert sí.
SG-06 Autopilot ticks & loops honestos
lección Construir loops de autopilot que acumulan aprendizaje real entre ticks — deterministas desde disco, idempotentes, Gate-cerrados — en lugar de generar ruido o actividad falsa.
Autopilot ticks & loops honestos
lecciónConstruir loops de autopilot que acumulan aprendizaje real entre ticks — deterministas desde disco, idempotentes, Gate-cerrados — en lugar de generar ruido o actividad falsa.
Un loop autónomo mal diseñado es peligroso: corre solo, sin nadie mirando, y puede resetear estado, duplicar registros o disparar acciones reales. La diferencia entre un loop que aprende y uno que solo se mueve es toda la diferencia entre research y teatro automatizado.
LA LECCIÓN
Un autopilot tick es código que corre sin un humano mirando, y esa es exactamente la razón por la que debe ser el código MÁS disciplinado del sistema. El error que XCAP sufrió en carne propia lo enseña todo: un tick programado que cada 5 minutos reseteaba el ledger y replicaba un fixture idéntico. Visto desde fuera, el sistema parecía vivo — números moviéndose, actividad constante. Por dentro era una mentira en bucle. La pregunta que define un buen tick: ¿este loop, corriendo mil veces, ACUMULA algo verdadero, o solo genera la apariencia de actividad?
Propiedad 1 — Determinista desde disco. El tick lee su entrada de disco (CSVs `data/real` vía `LocalCSVSource`), no de la red. Los loops automáticos de XCAP NUNCA habilitan red — `allow_network` queda en False, siempre. Esto es continuidad directa de SG-01: la red es opt-in explícito por llamada humana (el comando `data-fetch`), jamás algo que un loop autónomo active solo. Un tick que toca la red sin supervisión es un tick que puede filtrar, fallar de forma no reproducible o colgarse esperando un socket. Determinista desde disco significa que el mismo estado en disco produce el mismo resultado, audaz y reproducible.
Propiedad 2 — Idempotente. Correr el tick dos veces sobre las mismas fechas no debe duplicar forecasts, ni re-resolver lo ya resuelto, ni contar dos veces un cierre. Esto importa muchísimo en loops porque los schedulers fallan, reintentan, se solapan. Si tu tick no es idempotente, un reintento corrompe el estado en silencio. La forma de lograrlo: cada evento (un forecast, un cierre) tiene una clave natural (fecha+símbolo) y el tick comprueba existencia antes de escribir. `advance_market_memory` y `sim-autopilot-tick` son idempotentes por esta razón.
Propiedad 3 — Acumula, no resetea. El tick carga el estado previo y construye SOBRE él. `sim-autopilot-tick` carga el ledger, rota regímenes sintéticos, y acumula — exactamente la regla del capital invariant de SG-05 y de Market Memory de SG-03. Esta es la misma invariante apareciendo por tercera vez en la constelación, y no es casualidad: la honestidad de un sistema autónomo ES su negativa a resetearse. Un loop que empieza de cero cada vez no aprende nada; un loop que acumula es lo único que puede construir un historial de calibración que valga.
Propiedad 4 — Gate-cerrado por construcción. El tick JAMÁS opera capital real. El Capital Gate (`capital/broker/live_trading/automation` todos en False) está verificado por `python3 -m xcap.control check`. Un autopilot es justo donde un sistema de trading mata a su dueño: corre solo, y si tuviera capacidad de operar en real, un bug a las 3am vacía la cuenta. Por eso en XCAP el autopilot es sim-only, sin excepción, y `check` lo afirma en cada corrida. La automatización y la capacidad de operar real son dos cosas que NUNCA deben coexistir sin una decisión humana deliberadísima.
Cómo se opera y se blinda todo esto: el tick corre vía el scheduler (en XCAP, una tarea programada que invoca `sim-autopilot-tick`, el comando que acumula — NUNCA `sim-run-agent`, el que resetea para demos). El comportamiento queda LOCKED por `tests/test_sim_autopilot_tick.py`: si tocas el autorun, esos tests siguen verdes o has roto la integridad del loop. Y cada corrida termina con un `check` que reafirma los invariantes (keyless, Gate-cerrado, acumulación). El principio final, que sella la constelación entera: un loop honesto es uno cuyas garantías no dependen de que alguien lo vigile, porque están codificadas como tests e invariantes que el propio loop verifica en cada vuelta. Construye loops que se nieguen a mentir incluso cuando nadie mira — esa es la única automatización en la que un sistema de research puede confiar.
EJERCICIO
Construye `autopilot_tick.py` con una función `tick(state_dir)` que en cada llamada: (1) cargue el estado previo de disco (ledger + ledger de forecasts), (2) lea barras nuevas vía `LocalCSVSource` con `allow_network=False`, (3) avance Market Memory (bloquea forecasts nuevos, resuelve los maduros) acumulando, (4) corra `check()` afirmando Gate-cerrado y keyless, (5) persista. Hazlo idempotente: corre `tick` dos veces seguidas sobre el mismo estado y verifica que el segundo no añade ni cambia nada. Escribe `test_autopilot_tick.py` que pruebe: idempotencia (doble tick = estado idéntico), acumulación (dos ticks sobre rangos distintos crecen el estado), y que `check` falla si alguien pone `allow_network=True` o abre el Gate. Programa el tick con una tarea recurrente que invoque SOLO el comando que acumula.
ENTREGABLE
`autopilot_tick.py` (determinista desde disco, idempotente, acumulativo, Gate-cerrado) + `test_autopilot_tick.py` que lockea idempotencia y acumulación, y un scheduler apuntando al comando que acumula — nunca al que resetea.
INSIGHT CLAVE
La invariante de acumulación (no resetear) aparece tres veces en Signal — capital, Market Memory y autopilot — y no es casualidad: la honestidad de un sistema autónomo ES su negativa a resetearse. Un loop que empieza de cero cada vuelta no aprende; solo simula estar vivo.
ERRORES A EVITAR
- ×Apuntar el scheduler al comando que resetea-y-reproduce (`sim-run-agent`) en vez del que acumula (`sim-autopilot-tick`); el bug exacto que falsificaba la actividad de XCAP.
- ×Dejar que el loop habilite red (`allow_network=True`); un tick autónomo tocando la red puede filtrar, colgarse o fallar de forma no reproducible sin nadie mirando.
- ×Hacer el tick no idempotente: cuando el scheduler reintente o se solape, duplicará forecasts y corromperá el estado en silencio.
- ×Dar al autopilot cualquier capacidad de operar real sin una decisión humana deliberada; automatización + capacidad de trade real es como un loop mata una cuenta a las 3am.
- ×Confiar en vigilancia humana en vez de codificar las garantías como tests e invariantes que el propio loop verifica con `check` en cada vuelta.
Siguiente constelación
Brand & Surface
Marca y superficie