Sistema autónomo de inversión multi-estrategia

FARO,
por dentro.

Cómo está armado el sistema, motor por motor: de dónde salen los datos, qué fórmula se aplica, qué umbral decide, dónde queda registrado y qué pantalla lo muestra. Terminada esta lectura no debería quedar ninguna pregunta sobre qué es FARO ni cómo funciona.

6
motores
1
config = criterio
7
principios duros
F1
fase: solo lectura

Documento generado desde el código fuente del repo (src/faro/, specs/, config/, cockpit/) · v1

M1M2M3 M4M5M6
Guía de lectura

Cómo está organizada esta documentación.

Doce módulos. Se puede leer de corrido (flechas ← →) o saltar por el panel izquierdo.

Módulos 01–03 · el sistema

Qué problema resuelve, los principios que no se negocian, la arquitectura y el núcleo transversal (scoring, máquina de estados, RiskGuard, salud). Esto vale para los seis motores.

Módulos 04–09 · un motor por vez

Cada motor sigue el mismo guion: idea de negocio → fuentes de datos → matemática → umbrales de config → regla de decisión → pantalla. Si entendés uno, entendés los seis.

Módulos 10–12 · usarlo

El cockpit pantalla por pantalla, la operación diaria (Telegram, kill switch, despliegue), el roadmap y cómo agregar un motor nuevo sin romper nada.

Convenciones

  • activo observación apagado bloqueado
  • Los bloques en verde son fórmulas reales del código, no simplificaciones.
  • La tira de arriba de cada slide técnico marca en qué etapa del ciclo vive lo que se explica.
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Así se ve la tira: la etapa iluminada es la que trata el slide.

Módulo 01 · Fundamentos

FARO no adivina el precio. Le cobra peaje al mercado.

La distinción es la tesis entera del sistema: separa lo que FARO hace de lo que hace el 90% de los bots que pierden plata.

Lo que FARO no hace

  • Direccional. Nada de "BTC va a subir". Sin señales, sin momentum, sin predicción.
  • Sin "IA que decide". Toda decisión es una regla determinista contra un umbral configurado.
  • Sin apalancamiento en el sistema base. El looping Pendle+Aave quedó afuera por decisión de diseño.
  • No retira fondos. Nunca. Ni con las keys de producción.

Lo que FARO hace

  • Ineficiencias estructurales. Tasas de financiación, diferencias de precio del mismo activo entre plazas, rendimientos estructurales en USD, tasa en pesos.
  • Vigilancia 24/7. Un scanner por motor, con su propia cadencia, sin cansarse ni distraerse.
  • Registra todo. Cada oportunidad y cada "no operar" queda en el ledger con la regla que lo disparó.
  • Opera dentro de límites que son software, no intenciones.

El ejemplo que lo explica todo (Motor 1)

Comprás 1 BTC spot y vendés 1 BTC en perpetuo al mismo tiempo. Si BTC sube, ganás en el spot y perdés lo mismo en el short: tu resultado por precio es cero (delta-neutral). Pero cada 8 horas los que están comprados apalancados te pagan el funding. Esa tasa es la ganancia. Y no dependió en ningún momento de acertar hacia dónde iba el precio.

Módulo 01 · Fundamentos

Siete principios inquebrantables.

Están en CLAUDE.md y ninguna SPEC los puede contradecir. Si el código viola uno, es un bug — no una decisión de producto.

#PrincipioCómo se hace cumplir en el código
1El criterio es configuración, nunca código.Todo umbral vive en config.yaml, validado con pydantic. Un criterio hardcodeado es un bug.
2Fase 1 es solo lectura.Prohibido crear órdenes o importar credenciales con permiso de trade. Las keys de F1 son read-only.
3Límites duros, no sugerencias.RiskGuard.verificar() corre antes de cada orden. Si excede, la orden no se crea y el rechazo se registra.
4El sistema jamás retira fondos.Las API keys nunca tienen permiso de withdrawal; se verifica con un assert al arrancar.
5Todo es auditable.Cada observación, cada oportunidad y cada "no operar" van al ledger con timestamp, motor, regla, parámetros vigentes y resultado.
6Fallar cerrado.Config inválida, API caída, dato viejo o reloj corrido ⇒ el motor se pausa y alerta. Nunca operar con datos dudosos.
7Estados explícitos.FSM global de 6 estados. SAFE_MODE puede auto-recuperarse; KILLED jamás se reanuda sin un humano.

El corolario que más cuesta aceptar

Un score altísimo no puentea nada. El puntaje de una oportunidad gradúa cuánta automatización se le permite, pero jamás pasa por encima del RiskGuard ni del estado global. La jerarquía es: estado > límite duro > score.

Módulo 01 · Fundamentos

El principio que ordena todo lo demás.

El código implementa mecanismos. La configuración define políticas. Nunca al revés.

La consecuencia práctica

Subir el apetito de riesgo no es un deploy: es cambiar un número y aplicarlo desde el cockpit. Apagar un motor tampoco. Cambiar el umbral de entrada del funding, tampoco.

Y como cada cambio queda versionado y auditado, siempre se puede responder: "¿con qué criterio estaba corriendo el sistema el 3 de marzo a las 14:20?"

  • La oportunidad guarda params_vigentes y config_version_id: la foto del criterio con el que se tomó.
  • Un valor inválido detiene el arranque (pydantic). No hay "arrancó igual, medio roto".
  • Los pesos del scoring deben sumar 1.0: lo valida el config loader.
# config.yaml — el criterio, no el código
motores:
  funding_binance:
    activo: true
    modo: "observacion"   # observacion|simulado|real
    ciclo_segundos: 300
    pares: ["BTC/USDT", "ETH/USDT", …]
    umbral_entrada_apr_pct: 12
    umbral_salida_apr_pct: 4
    persistencia_minima_ciclos: 3
    profundidad_minima_usd: 50000
    fee_taker_spot_pct: 0.1
    fee_taker_futures_pct: 0.04

riesgo:
  perdida_diaria_maxima_pct: 2    # ⇒ SAFE_MODE
  perdida_semanal_maxima_pct: 5   # ⇒ KILLED

Nada de esto está en el código. Todo esto se cambia sin tocar el código.

Módulo 01 · Fundamentos

Los seis motores, de un vistazo.

Cada uno captura una ineficiencia distinta. Se complementan: no dependen del mismo hecho del mundo.

MotorQué cobraFuente de datosCicloEstado F1
M1Funding cripto
cash-and-carry
La tasa que pagan los longs apalancados cada 8 h. Delta-neutral.Binance (ccxt, público)300 sobservación
M2Carry argentino
LECAP / BONCAP
La tasa en pesos cuando supera a la devaluación esperada.ArgentinaDatos + Data912 + CriptoYa900 sobservación
M3Renta USD
sUSDe / PT / T-bill
El rendimiento estructural del capital ocioso en dólares.DefiLlama + Pendle3600 sobservación
M4Stables Argentina
arbitraje local
La diferencia ejecutable del mismo dólar cripto entre plazas.CriptoYa (una request, toda la matriz)60 sobservación
M5Polymarket
predicción
YES + NO por debajo de $1: paga $1 gane quien gane.Polymarket CLOB (público)30 sapagado
M6Wallbit
conector
Soporte a M2/M3/M4: rates ARS→USD y balances. Solo lectura.api.wallbit.io900 sapagado

Por qué seis y no uno

Diversificación real: el funding no depende del riesgo cambiario argentino, y la tasa en pesos no depende del sentimiento cripto. Cuando uno se apaga, los otros siguen.

Por qué todos en "observación"

Fase 1 es solo lectura, por principio. Primero hay que medir cuántas oportunidades reales aparecen por día y de qué tamaño neto de fees. Recién con ≥14 días de datos se abre el roadmap.

Por qué M5 apagado

Polymarket exige latencia que todavía no está validada. Entra solo si la fase de medición demuestra que se llega a tiempo. Evidencia, nunca entusiasmo.

Módulo 01 · Fundamentos

Cuatro fases. Ninguna se saltea.

Cada fase tiene un criterio de avance explícito. No se avanza por entusiasmo; se avanza por evidencia.

FASE 1 · OBSERVAR Scanner + cockpit. Solo lectura. Cuantificar oportunidades reales/día. ← estás acá FASE 2 · SIMULAR Paper trading contra precios reales. Validar slippage, latencia, costos. FASE 3 · CAPITAL INICIAL USD 500–1.000 reales. Solo M1 y M3. FASE 4 · ESCALAR Sumar motores de a uno. M5 último, si supera validación. 14 días de datos ⇒ pasar simulación estable ⇒ pasar 30 días de PnL real ⇒ pasar evidencia, no entusiasmo

Expectativa declarada, sin maquillaje

10–25% anual en USD sobre el capital desplegado, con drawdowns acotados. Es una máquina de rendimiento, no un esquema de enriquecimiento rápido. El capital asignado es ~10% del patrimonio, aislado del resto.

Módulo 02 · Arquitectura

Una EC2 aislada, cero puertos abiertos.

Un solo proceso Python multi-motor. La API escucha únicamente en la interfaz de WireGuard. El cockpit es estático. Nada de esto es accidental.

config.yaml — el criterio validado con pydantic · versionado · auditado gobierna todo EC2 dedicada · sa-east-1 · acceso solo por WireGuard SCANNERS funding_binance · 300 s carry_argentina · 900 s renta_usd · 3600 s stables_criptoya · 60 s polymarket · 30 s (off) wallbit · 900 s (off) apscheduler · 1 job por motor MOTOR DE DECISIÓN funciones puras por motor scoring 0–100 uniforme gates: alerta/manual/auto FSM global (6 estados) persistencia N ciclos determinista · sin LLM LEDGER (Postgres) observaciones oportunidades risk_events system_state_log config_versions / audit la memoria perfecta EJECUTOR — Fase 3+ RiskGuard: límites duros PortfolioManager no cableado en F1 API FastAPI bind: 10.0.0.1:8420 (wg0) Bearer token · scopes Telegram alertas + /pausa + /kill desde el celular Cockpit (Astro estático) gestión y análisis · detrás de la VPN

Un proceso, muchos motores

El scheduler aísla los ciclos: un motor degradado no tumba a los demás. Se separan procesos solo si M5 (latencia) lo exige.

Postgres desde el día 1

Las agregaciones del cockpit y la conciliación de Fase 3 lo ameritan. SQLite queda para los tests.

Dos capas de acceso

API solo en la interfaz WireGuard + Bearer token; el cockpit detrás de Cloudflare Access. Cero puertos de aplicación expuestos a internet.

Módulo 02 · Arquitectura

La anatomía de un ciclo.

Los seis motores hacen exactamente lo mismo, con distinta matemática en el paso 2. Este pipeline es la columna vertebral de FARO.

1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta
EtapaQué pasaQuién lo haceSi falla
0 · GateAntes de tocar la red: ¿el sistema está escaneando? ¿este motor está activo? Se relee la config en cada disparo.core/scheduler.py
tick_con_gate()
No corre el tick. Un /kill o un toggle surten efecto en el ciclo siguiente, sin reiniciar.
1 · FeedI/O contra el exterior: Binance, CriptoYa, DefiLlama, Pendle, Data912. Reintentos con backoff.Un Protocol por fuente (mockeable)Fallo persistente ⇒ el motor se marca degradado y emite risk_event. No inventa datos.
2 · EvaluarLa matemática del motor: APR neto, retorno USD, spread contra el piso, cruce ejecutable. Función pura.evaluar() / evaluar_carry() / evaluar_renta()Dato inválido ⇒ ValueError. Se descarta el instrumento, no el ciclo.
3 · ScoreLas métricas crudas se normalizan a un puntaje 0–100 común, con los pesos de config.core/scoring.pyDimensión sin datos ⇒ neutro 50, documentado. No se infla.
4 · GateEl score cae en un tramo: ninguno / alerta / manual / auto.gate(score, cfg)El gate clasifica, no ejecuta. Nunca puentea RiskGuard ni FSM.
5 · LedgerSe persiste la observación (idempotente por ts_ciclo) y, si corresponde, se abre o cierra la oportunidad.ledger/repos.pyTodo en una transacción. Si falla, no queda una alerta enviada sin registro.
6 · AlertaTelegram, solo si el score supera umbral_alerta y el nivel configurado lo permite.notify/telegram.pyLa señal se registra en la misma transacción que la oportunidad.

El detalle que hace todo esto testeable

La etapa 2 no tiene I/O. Recibe un snapshot y devuelve una evaluación: mismas entradas, misma salida, siempre. Por eso la lógica de negocio de FARO se puede probar sin red, sin base y sin exchange — y por eso los tests corren en milisegundos.

Módulo 02 · Arquitectura

Todo scanner tiene las mismas tres capas.

Aprendido esto, leer cualquiera de los seis motores es cuestión de buscar dónde está cada capa.

1 · Feed (Protocol)

La única capa que toca la red. Es una interfaz: en producción pega contra Binance o CriptoYa; en los tests se le inyecta un stub. Nada de lógica de negocio.

2 · Funciones puras

La matemática. Sin red, sin base, sin reloj. apr_neto_pct(), retorno_usd_neto_pct(), spread_vs_piso_pct(), cruce(). Es donde vive el criterio del negocio.

3 · Scanner (la clase)

El orquestador con estado: pide el snapshot, llama a las puras, arma el score, persiste, notifica y mantiene el historial de persistencia. La única capa "sucia".

# src/faro/scanners/funding_binance.py — las tres capas, en orden

# ── capa 1: el contrato con el mundo exterior (mockeable) ──
class FundingFeed(Protocol):
    async def snapshot(self, par: str) -> FundingSnapshot: ...

# ── capa 2: la matemática. pura. sin I/O. testeable en microsegundos ──
def apr_bruto_pct(funding_rate: float, interval_hours: float) -> float:
    periodos_por_anio = HORAS_ANIO / interval_hours
    return funding_rate * periodos_por_anio * 100.0

def evaluar(snapshot, cfg, apr_neto_previos) -> Evaluacion:
    """Mismas entradas ⇒ misma salida. Sin estado, sin red, sin reloj."""
    ...

# ── capa 3: el orquestador con estado ──
class FundingScanner:
    async def tick(self) -> None:
        cfg = self._config()              # relee config en cada tick (hot reload)
        for par in cfg.pares:
            snapshot = await self._fetch_resiliente(par)   # reintentos + backoff
            if snapshot is None: continue            # degradado, no inventa
            ev = evaluar(snapshot, cfg, self._historial[par])
            await self._persistir(session, par, snapshot, ev, cfg)

Regla de oro: si una decisión de negocio está adentro de la capa 3, está mal ubicada. La capa 3 solo mueve datos.

Módulo 02 · Arquitectura

El ciclo de vida de un cambio de criterio.

Cambiar un umbral no es editar un archivo y rezar. Es un flujo con validación, versión, auditoría y rollback.

1 · Editar cockpit o YAML 2 · Validar POST /config/validar 3 · Aplicar nueva config_version 4 · Auditar campo, de → a, origen 5 · Tomar efecto próximo tick, sin reinicio rollback a versión N

Lo que garantiza cada paso

  • Validar: pydantic. Un valor fuera de rango, o pesos de scoring que no suman 1.0, se rechazan antes de aplicarse.
  • Aplicar: se crea una fila en config_versions con el YAML completo, un checksum y quién lo hizo.
  • Auditar: cada campo que cambió va a config_audit: valor anterior, valor nuevo, origen, versión anterior y nueva.
  • Efecto: el scanner relee la config en cada tick. No hace falta reiniciar el servicio.
  • Rollback: volver a una versión anterior es una llamada, y también queda auditado.

Por qué esto no es burocracia

Dentro de seis meses, mirando una oportunidad rara del ledger, la pregunta va a ser "¿por qué el sistema hizo eso?". La respuesta está en params_vigentes y config_version_id de esa misma fila: el criterio exacto, congelado, con el que se tomó la decisión.

Sin esto, cada análisis post-mortem sería arqueología de git. Con esto, es una consulta SQL.

Módulo 02 · Arquitectura

El ledger: la memoria perfecta.

Ocho tablas. Todo lo que el sistema vio, decidió, ignoró y le dijeron que haga.

1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta
TablaQué guardaCampos que importan
observacionesTodo lo que se vio, señal o no. Una fila por par/instrumento por ciclo. Es la materia prima para medir cuántas oportunidades reales hay por día.ts_ciclo (clave de idempotencia), motor, par, payload (JSON con la evaluación completa)
oportunidadesVentanas abiertas y cerradas. Cuándo apareció, cuánto duró, cuánto valía.ts_apertura/ts_cierre, metrica_principal, score_final, subscores, params_vigentes, config_version_id, ganancia_estimada_usd
risk_eventsTodo lo que salió mal o raro. API caída, dato viejo, cotización fuera de la mediana, depeg, breach de salud.severidad (info/warning/critico), tipo, mensaje, resuelto_ts
system_state_logCada transición de la FSM. Quién pasó a SAFE_MODE, por qué y a instancias de quién.estado_anteriorestado_nuevo, causa (texto legible), origen
config_versionsEl YAML completo de cada versión del criterio, con checksum.nro, estado (draft/activa/archivada), yaml_content, checksum
config_auditCampo por campo: qué cambió, de qué a qué, quién y cuándo.campo, valor_anterior, valor_nuevo, origen
senales_telegramQué se notificó y si salió bien. Ligado a la oportunidad.oportunidad_id, tipo, enviado_ok
venues · order_intentsCatálogo de plazas (con flag solo_lectura) e intenciones de orden — estas últimas vacías en Fase 1.solo_lectura, estado_api, ultimo_healthcheck

Idempotencia por ts_ciclo

El timestamp se trunca al inicio del ciclo. Si el scanner corre dos veces en la misma ventana (reintento, reinicio, jitter del scheduler), no duplica la observación: pisa la misma fila.

El "no operar" también se registra

Es contraintuitivo pero es el principio 5. La observación guarda el decision_reason aunque no haya señal. Sin eso, sería imposible distinguir "no había oportunidad" de "el motor estaba roto".

Módulo 02 · Arquitectura

La API: lectura amplia, escritura estrecha.

FastAPI, escuchando solo en 10.0.0.1:8420 — la interfaz de WireGuard. Dos scopes: lectura y admin. Todo lo que muta pasa por admin y queda auditado.

Lectura — lo que consume el cockpit

GET /v1/estadofase, estado FSM, pausa global, versión de config
GET /v1/motorespor motor: activo, modo, ciclo
GET /v1/saludsalud general del proceso
GET /v1/salud/motoresliveness de trabajo: OK / SILENCIOSO / CAÍDO / APAGADO
GET /v1/oportunidadesventanas abiertas y cerradas
GET /v1/observaciones/serieserie temporal para graficar
GET /v1/metricas/diariasventanas por día y por motor
GET /v1/risk_eventsalertas y anomalías
GET /v1/config · /audit · /versionesel criterio vigente y su historia

Admin — lo que cambia el mundo

POST /v1/pausafreno operativo ⇒ SAFE_MODE
POST /v1/reanudarsale de SAFE_MODE al estado previo
POST /v1/killKILLED. No se reanuda sin humano.
POST /v1/config/validarpydantic dice sí o no. No aplica nada.
POST /v1/config/aplicarnueva versión + audit + efecto en el próximo tick
POST /v1/config/rollback/{nro}volver a una versión anterior (auditado)
PATCH /v1/motores/{motor}prender/apagar un motor en caliente
POST /v1/risk_events/{id}/resolvermarcar una alerta como atendida

El freno tiene prioridad sobre todo

Hay un _verificar_freno() que corre antes de las mutaciones: si el sistema está en KILLED, ninguna llamada lo saca de ahí salvo la acción humana explícita.

Módulo 02 · Arquitectura

Cómo se comporta FARO cuando algo se rompe.

Principio 6: fallar cerrado. Ante la duda, el sistema hace menos, no más.

Qué fallaCómo se detectaQué hace FARO
Una API externa no respondeReintentos con backoff exponencial dentro del tick.El par/instrumento se saltea; el motor se marca degradado y emite risk_event. Los otros motores siguen normales.
Racha de errores de APImax_errores_api_consecutivos: 3SAFE_MODE. Deja de escanear ese motor hasta que sane.
El dato llega, pero viejostale_minutes_* por fuente (CriptoYa 5 min, Data912 20 min, DefiLlama 90 min…)Con fail_closed_on_*_stale: true: no se evalúa. Un precio de hace 3 horas es peor que no tener precio.
Una cotización absurdadesviacion_maxima_mediana_pct: 20 — se compara contra la mediana de todas las plazas.Se descarta esa plaza del ciclo y se loguea como data_quality. Un exchange con la API rota no inventa un arbitraje del 15%.
El reloj se corriómax_clock_drift_segundos: 2SAFE_MODE global. Los horarios de funding dependen del reloj; con drift no se opera.
El motor "vive" pero no trabajaWatchdog de liveness: última observación más vieja que 3 × ciclo_segundos.Se marca SILENCIOSO y aparece en el cockpit y en /estado detalle. El proceso responde 200 pero no produce: eso ahora se ve.
La config quedó corruptaValidación pydantic al arrancar y al aplicar.KILLED. No arranca. Prefiere no correr antes que correr con un criterio inválido.

La regla que resume el módulo

Nunca operar con datos dudosos. Cada una de estas defensas existe porque la alternativa — tomar una decisión con información rota — es más cara que perderse una oportunidad.

Módulo 03 · Núcleo transversal

Scoring: cómo comparar peras con manzanas.

Un funding de 18% anual y un cruce de stables de 1,1% neto no se pueden comparar directamente. El scoring los lleva a la misma escala.

1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

El problema

Cada motor produce métricas crudas y heterogéneas: APR anualizado, spread porcentual, retorno neto en USD, suma de asks. Sin una capa común, no hay forma de decidir a cuál oportunidad darle prioridad cuando el capital es finito.

La solución

Una capa encima de la matemática de cada motor que normaliza toda oportunidad a un puntaje 0–100, ponderando nueve dimensiones. No reemplaza la matemática: la traduce.

El score gradúa, no decide

El puntaje determina cuánta automatización se le permite a una oportunidad: notificar, proponer con aprobación humana, o auto-ejecutar. Jamás pasa por encima del RiskGuard ni del estado global.

La fórmula, completa score = Σ pesoi × subscorei
· los pesos vienen de config.yaml y deben sumar 1.0
· cada subscore está en 0–100
· las dimensiones sin datos usan un neutro = 50, documentado
# core/scoring.py
def calcular_score(subscores, pesos) -> float:
    total = 0.0
    for dim in DIMENSIONES:
        sub = subscores.get(dim, NEUTRO)   # NEUTRO = 50.0
        total += sub * getattr(pesos, dim)
    return _clamp(total)                   # [0, 100]

En Fase 1, cuatro dimensiones (repetibilidad, calidad de ejecución, contraparte, salud) no tienen datos históricos todavía: usan el neutro. Se afinan con datos reales, no con optimismo.

Módulo 03 · Núcleo transversal

Nueve dimensiones. El retorno pesa un cuarto.

El resto es riesgo, liquidez y realismo de ejecución. Que una oportunidad rinda mucho no alcanza — tiene que ser saleable, repetible y limpia.

retorno_neto0.25 liquidez0.15 repetibilidad0.15 calidad_ejecucion0.10 riesgo_operacional0.10 riesgo_contraparte0.10 facilidad_salida0.05 cumplimiento0.05 salud_estrategia0.05 Cuánto rinde, neto de fees, contra el objetivo del motor. Profundidad del libro contra el monto que se operaría. ¿Es una ventana única o pasa todas las semanas? Slippage esperado, impacto en el libro, latencia. Complejidad de la operación, cantidad de patas. Riesgo del exchange/protocolo: ¿quién tiene la plata? Qué tan rápido se puede salir sin regalar nada. Tratamiento fiscal, PSAV, informe a ARCA. StrategyHealthScore: ¿el motor sigue funcionando?

Normalizador de retorno

min(100, métrica / objetivo × 100)

En el objetivo o por encima ⇒ 100. Negativo ⇒ 0. Lineal en el medio.

Normalizador de liquidez

min(100, (prof/monto) / 5 × 100)

Profundidad ≥ 5× el monto a operar ⇒ 100. Menos de eso, castigo lineal.

Los pesos son configurables

Están en scoring.pesos. Si mañana el operador decide que la liquidez importa más que el retorno, cambia dos números. El config loader verifica que sigan sumando 1.0.

Módulo 03 · Núcleo transversal

Tres umbrales, tres niveles de confianza.

El score cae en un tramo y ese tramo define hasta dónde llega la autonomía del sistema.

07080 90100 NINGUNO — ni se notifica ALERTA MANUAL AUTO te avisa por Telegram propone y espera tu OK ejecuta solo solo en AUTO_LIMITED
# config.yaml
scoring:
  umbral_alerta: 70   # score mínimo para notificar
  umbral_manual: 80   # mínimo para proponer con aprobación
  umbral_auto:   90   # mínimo para auto-ejecución

# core/scoring.py — el gate solo CLASIFICA
def gate(score, cfg) -> Gate:
    if score >= cfg.umbral_auto:   return "auto"
    if score >= cfg.umbral_manual: return "manual"
    if score >= cfg.umbral_alerta: return "alerta"
    return "ninguno"

La cadena que no se puede saltear

Un score de 97 no ejecuta nada si:

  • el estado global no es AUTO_LIMITED (en Fase 1 es OBSERVE: no hay órdenes, punto);
  • el RiskGuard rechaza el monto por algún tope duro;
  • el motor está en modo observacion.

Jerarquía: FSM globalmodo del motorRiskGuardgate del score. El score es el último de la fila, no el primero.

Módulo 03 · Núcleo transversal

Seis estados. Uno no tiene vuelta atrás automática.

Es el principio 7. El sistema siempre sabe en qué estado está, y cada transición queda en system_state_log con su causa.

progresión operacional — se avanza y se retrocede DE A UN ESCALÓN OBSERVE solo lectura · Fase 1 PAPER simula contra precios reales MANUAL_APPROVAL propone, vos aprobás AUTO_LIMITED ejecuta dentro de límites estados de protección — se entra DESDE CUALQUIERA SAFE_MODE — el freno recuperable Cancela órdenes abiertas, pasa a solo lectura. Sigue escaneando. Al sanar la causa, vuelve solo al estado en el que estaba. KILLED — el freno definitivo Todo detenido. Y acá está la clave: NO se reanuda sin acción humana explícita. recuperar(humano=True) o TransicionInvalida escala SAFE_MODE recuerda de dónde vino y vuelve ahí
Módulo 03 · Núcleo transversal

Qué te manda a SAFE_MODE, qué te manda a KILLED.

No es criterio del momento. Está escrito en disparador_automatico() y los números vienen de config. KILLED tiene prioridad sobre SAFE_MODE.

⛔ KILLED — requiere un humano para volver

Config corruptano se puede confiar en el criterio
Mismatch de conciliaciónel ledger no coincide con el exchange
perdida_semanal_maxima_pct: 5alcanzado ⇒ frenar y pensar
perdida_mensual_maxima_pct: 10idem
/kill CONFIRMAR por Telegramel freno de mano humano

⚠ SAFE_MODE — puede auto-recuperarse

perdida_diaria_maxima_pct: 2pausa total del día
max_clock_drift_segundos: 2el reloj se corrió: los funding times dependen de él
max_errores_api_consecutivos: 3racha de errores de un venue
Depeg de una stablecoinel dólar dejó de valer un dólar
FX Risk Engine en PANIC (M2)salto cambiario brusco
Riesgo sistémico de Ethena (M3)factor_riesgo_critico: 0.2
# core/fsm.py — el orden importa: primero se pregunta por KILLED
def disparador_automatico(m: MetricasRiesgo, riesgo: RiesgoConfig) -> str | None:
    if (m.config_corrupta or m.mismatch_conciliacion
        or m.perdida_semanal_pct >= riesgo.perdida_semanal_maxima_pct
        or m.perdida_mensual_pct >= riesgo.perdida_mensual_maxima_pct):
        return KILLED
    if (m.perdida_diaria_pct >= riesgo.perdida_diaria_maxima_pct
        or m.clock_drift_seg > riesgo.max_clock_drift_segundos
        or m.errores_api_consecutivos > riesgo.max_errores_api_consecutivos
        or m.depeg):
        return SAFE_MODE
    return None

En Fase 1 casi todas estas métricas valen 0 (no hay PnL ni órdenes). La lógica está presente y testeada desde el día uno, para que el día que haya capital real no haya que escribirla apurado.

Módulo 03 · Núcleo transversal

RiskGuard: el que dice que no.

Stateless. Recibe una orden propuesta, la config de capital y la exposición actual. Devuelve un veredicto. Corre antes de que la orden exista.

El orden de los topes (fail-closed: el primero que se excede, rechaza)

#TopeDefault
0Monto > 0sanidad
1tope_por_operacion_usdUSD 500
2max_desplegado_pct70% del capital
3tope_por_plataforma_pct25% en un solo exchange
4tope_por_activo_pct20%
5tope_por_stablecoin_pct40% en una misma stable
6operacion_sin_confirmar_max_usdUSD 200 ⇒ arriba, pide OK humano

El reserva_cash_pct: 30 es la contracara del max_desplegado_pct: 70: siempre hay un colchón sin desplegar.

# core/riskguard.py
def verificar(orden, cfg, exposicion) -> Veredicto:
    if orden.monto_usd > cfg.tope_por_operacion_usd:
        return Veredicto(False, "excede tope_por_operacion_usd")

    nuevo_total = exposicion.total_desplegado_usd + orden.monto_usd
    if nuevo_total > cfg.total_asignado_usd * cfg.max_desplegado_pct / 100:
        return Veredicto(False, "excede max_desplegado_pct")
    ...
    # pasó los topes duros: ¿necesita confirmación humana?
    requiere = orden.monto_usd > cfg.operacion_sin_confirmar_max_usd
    return Veredicto(True, requiere_confirmacion=requiere)

Si rechaza, se registra

El rechazo no es un log que se pierde: va al ledger. Un patrón de rechazos por el mismo tope es información — significa que la configuración de capital ya no matchea con lo que el mercado ofrece.

Estado en Fase 1

inactivo No hay órdenes. El código está escrito y testeado, sin cablear a ejecución. Se enchufa en Fase 3.

Módulo 03 · Núcleo transversal

El RiskGuard prohíbe. El PortfolioManager elige.

Cuando en un mismo ciclo hay cinco oportunidades elegibles y capital para tres, alguien tiene que decidir. Determinista, sin LLM, sin sorpresas.

Cómo asigna

  1. Ordena todas las candidatas del ciclo por score, de mayor a menor.
  2. Para cada una, la pasa por el RiskGuard con la exposición acumulada de las ya aprobadas.
  3. Si pasa: la aprueba y suma su monto a la exposición por plataforma, por activo y por stablecoin.
  4. Si no pasa: la descarta y sigue con la siguiente.

La consecuencia sutil: una orden que individualmente pasaría puede ser rechazada porque el conjunto ya consumió el cupo de su plataforma. Los topes son agregados, no orden por orden.

Correlación entre motores

Dos motores que pierden juntos no diversifican, aunque parezcan estrategias distintas. FARO calcula la correlación de Pearson entre los retornos de cada par de motores:

config portfolio.correlacion_maxima_motores: 0.5

Los pares con correlación ≥ 0.5 (borde inclusivo, ADR-031) dejan de contar como diversificadores. La anti-correlación (negativa) es buena y no se marca.

inactivo en F1 Sin retornos reales no hay correlación que calcular. Se cablea en F3+.

# core/portfolio.py — el detalle que hace la diferencia
for cand in sorted(candidatas, key=lambda c: c.score, reverse=True):
    acumulada = ExposicionActual(total, por_plataforma, por_activo, por_stablecoin)
    veredicto = verificar(cand.orden, cfg, acumulada)   # ← con lo ya aprobado
    if not veredicto.permitida: continue
    total += cand.orden.monto_usd                       # y se acumula
    aprobadas.append(cand.orden)
Módulo 03 · Núcleo transversal

La disciplina de la estrategia que se muere.

Toda estrategia deja de funcionar en algún momento. La pregunta no es si pasa: es cómo distinguir un drawdown normal de un edge muerto — y decidirlo con números escritos antes, no por feeling.

NivelCondiciónAcciónHealth score
sin breach Todo dentro del cono del backtest. Ninguna. El motor sigue con su tamaño. 85.0
soft breach Rolling Sharpe 90 d por debajo del percentil 5 del backtest,
o drawdown > soft_mult (1.0) × el máximo del backtest.
Cortar el tamaño del motor a la mitad. 40.0
hard breach Drawdown > hard_mult (1.5) × el máximo,
o time-underwater > 1.5 × el más largo del backtest.
SAFE_MODE de ese motor. Se pausa, los demás siguen. 15.0
sin datos Fase 1: no hay PnL ni trades todavía. Ninguna. 50.0 (neutro)

Retroalimenta el scoring

El health score entra como una de las nueve dimensiones (peso 0.05). Un motor enfermo puntúa más bajo sus propias oportunidades, automáticamente.

Y también el capital

pausar_motor_si_salud_menor: 40
sugerir_mas_capital_si_salud_mayor: 85
El sistema sugiere; el humano decide.

Job semanal

Corre a las 08:00 hora local. Reglas escritas antes del launch, no inventadas cuando duele.

Módulo 03 · Núcleo transversal

"Vivo" no es lo mismo que "trabajando".

El healthcheck clásico pregunta si el proceso responde. Eso no alcanza: un motor puede estar activo: true, el proceso respondiendo 200, y el scanner haber dejado de producir observaciones hace seis horas por una excepción silenciada.

OK

Activo y con una observación reciente. Está trabajando.

SILENCIOSO

Activo, pero la última observación es más vieja que tolerancia × ciclo. Dejó de tickear.

CAÍDO

Activo y nunca observó nada. Arrancó pero no produce.

APAGADO

activo: false. No es una anomalía: es una decisión del operador.

la regla edad_segundos > tolerancia_ciclos × ciclo_segundos ⇒ SILENCIOSO
liveness_tolerancia_ciclos: 3.0 — el límite es exclusivo: justo en el límite todavía cuenta como OK, para dar margen al jitter del scheduler.

Cada motor se mide contra su propio ciclo. El de stables (60 s) se considera silencioso a los 3 minutos; el de renta USD (3600 s), recién a las 3 horas. No hay un umbral único que no le sirva a nadie.

Dónde se ve

  • En el cockpit, primera tarjeta de la pantalla de inicio: 4/4 motores OK.
  • En Telegram con /estado detalle.
  • En GET /v1/salud/motores.
  • Lo consume además un watchdog programado en core/runtime.py.

El módulo core/salud_motores.py es puro y sin I/O: los tres consumidores llaman a la misma función. No hay tres definiciones distintas de "sano".

Módulo 04 · Motor 1
M1 Funding cripto · cash-and-carry · el motor central

Cobrarle el alquiler a los apalancados.

Es el motor #1 del ranking: 10–30% anual en USD, delta-neutral, 100% automatizable. Todo lo demás en FARO orbita alrededor de este.

La mecánica, en tres frases

1. En los futuros perpetuos no hay fecha de vencimiento. Para que el precio del perpetuo no se despegue del spot, cada 8 horas se paga un funding: si hay más gente comprada que vendida (el caso normal en un mercado alcista), los longs le pagan a los shorts.

2. FARO compra 1 BTC spot y vende 1 BTC en perpetuo, al mismo tiempo. Si BTC sube, gana en el spot y pierde lo mismo en el short. Si baja, al revés. Su resultado por precio es cero: es delta-neutral.

3. Pero como está short en el perpetuo, cobra el funding cada 8 horas. Esa es la ganancia, y no dependió de acertar la dirección del precio.

El dato que motivó el motor

A comienzos de 2026, el funding de BTC promedió ~0,51% cada 8 horas — más del 70% anualizado en los picos. Firmas profesionales delta-neutral capturaron 19,26% anual en 2025.

SPOT · LONG comprás 1 BTC BTC sube → +$ BTC baja → −$ PERP · SHORT vendés 1 BTC BTC sube → −$ BTC baja → +$ Resultado por precio = 0 (delta-neutral) + funding cada 8 h la única fuente de ganancia — y no depende del precio

Por qué fracasa la gente

  • No contabiliza las fees
  • No monitorea el delta en tiempo real
  • Slippage en libros finos
  • Mueve el capital al momento de operar

La regla operativa

Saldos pre-fondeados en ambos lados. Dimensionar la posición contra la profundidad real del libro, no contra el capital disponible.

El riesgo principal

Funding negativo prolongado: cuando los shorts empiezan a pagarle a los longs, la estrategia se da vuelta. Por eso existe el umbral_salida_apr_pct.

Módulo 04 · Motor 1
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Qué mira el motor cada 300 segundos.

Cinco pares por default, contra los endpoints públicos de Binance vía ccxt. Sin API key, porque en Fase 1 no hace falta ninguna.

# la foto cruda de un par en un ciclo
@dataclass(frozen=True)
class FundingSnapshot:
    par: str                  # "BTC/USDT"
    ts: datetime
    funding_rate: float       # fracción por intervalo
                              # 0.0001 = 0.01%
    interval_hours: float     # cadencia REAL del par:
                              # 8, 4 o 1 h — no se asume
    mark_price: float         # precio del perpetuo
    index_price: float        # precio de referencia (spot)
    profundidad_usd: float    # min(spot, perp)
                              # ← la pata más fina manda

Dos detalles que evitan bugs caros

  • interval_hours viene del feed, no está hardcodeado en 8. Binance tiene pares con funding cada 4 h y cada 1 h. Anualizar todo como si fuera 8 h daría cifras el doble o el óctuple de la realidad.
  • La profundidad es el mínimo entre las dos patas. De nada sirve un libro spot gordísimo si el perpetuo no aguanta el short: la pata más fina define cuánto se puede operar.

Los pares configurados

motores:
  funding_binance:
    activo: true
    modo: "observacion"
    ciclo_segundos: 300
    pares:
      - "BTC/USDT"
      - "ETH/USDT"
      - "SOL/USDT"
      - "BNB/USDT"
      - "XRP/USDT"

Agregar un par es agregar una línea al YAML. No hay deploy.

Resiliencia del feed

El _fetch_resiliente() reintenta 3 veces con backoff exponencial (base 0,5 s). Si un par falla persistentemente: se saltea ese par, el motor queda degradado y emite un risk_event. Los otros cuatro pares se evalúan normal.

El flag degradado se restablece en cada tick. Es un estado del ciclo, no una condena.

Módulo 04 · Motor 1
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

De 0,01% cada 8 horas a un APR neto de fees.

Tres funciones puras. Sin red, sin base, sin estado. Esta es la única parte del motor donde vive el criterio de negocio.

1 · APR bruto — anualizar según la cadencia REAL del par periodos_por_anio = (365 × 24) / interval_hours
apr_bruto_pct = funding_rate × periodos_por_anio × 100

Ejemplo: funding_rate = 0.0001 (0,01%) con interval_hours = 8 ⇒ 1.095 períodos/año ⇒ APR bruto = 10,95%.

2 · APR neto — descontar las fees round-trip, amortizadas fees_roundtrip_pct = 2 × (fee_taker_spot + fee_taker_futures)
costo_anualizado_pct = fees_roundtrip_pct × (365 / horizonte_amortizacion_dias)
apr_neto_pct = apr_bruto_pct − costo_anualizado_pct

Por qué "round-trip" y por qué "amortizadas"

Round-trip = entrar (compra spot + short perp) y salir (venta spot + cierre perp) = 2 × (fee_spot + fee_fut). Con los defaults: 2 × (0,1% + 0,04%) = 0,28%.

Amortizadas: ese 0,28% es un costo de una sola vez. Si la posición se va a sostener horizonte_amortizacion_dias: 30, el costo anualizado equivalente es 0,28% × (365/30) = 3,41%. Eso es lo que se le resta al APR bruto.

Cambiar el horizonte a 90 días bajaría el costo anualizado a 1,14% — y muchas más oportunidades pasarían el umbral. Es una decisión de criterio, y por eso está en config.

3 · Spread mark vs index — la salud del par spread_pct = (mark_price − index_price) / index_price × 100

Un perpetuo muy despegado de su índice es una señal de estrés en el par. Se registra en la observación aunque no bloquee la señal.

La cuenta completa, de punta a punta

funding observado0,015% / 8 h
APR bruto16,43%
− fees round-trip amortizadas (30 d)− 3,41%
APR neto13,02%
umbral de entrada12% ⇒ pasa
Módulo 04 · Motor 1
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Entrar cuesta tres ciclos. Salir, uno solo.

La asimetría es deliberada y está documentada (ADR-009): tardar en entrar cuesta una oportunidad; tardar en salir cuesta plata.

SEÑAL DE ENTRADA

Se necesitan las dos condiciones:

  • El APR neto ≥ umbral_entrada_apr_pct (12%) sostenido persistencia_minima_ciclos = 3 ciclos consecutivos, incluyendo el actual.
  • La profundidad ≥ profundidad_minima_usd (USD 50.000).

La persistencia mata el ruido: un pico de funding de un solo ciclo (5 minutos) no abre una posición. Tiene que sostenerse 15 minutos.

SEÑAL DE SALIDA

Una sola condición, sin persistencia:

  • El APR neto cae por debajo de umbral_salida_apr_pct (4%).

Inmediata. No espera confirmación. Salir rápido de una posición que se deteriora es la interpretación conservadora, y ante ambigüedad FARO siempre elige la que arriesga menos.

La banda entre 4% y 12% es una zona de histéresis: no entra, pero tampoco sale. Evita el ping-pong de abrir y cerrar en el borde.

# scanners/funding_binance.py — la función pura completa
def evaluar(snapshot, cfg, apr_neto_previos) -> Evaluacion:
    bruto = apr_bruto_pct(snapshot.funding_rate, snapshot.interval_hours)
    neto  = apr_neto_pct(snapshot.funding_rate, snapshot.interval_hours,
                         cfg.fee_taker_spot_pct, cfg.fee_taker_futures_pct,
                         cfg.horizonte_amortizacion_dias)
    prof_ok = snapshot.profundidad_usd >= cfg.profundidad_minima_usd

    serie  = [*apr_neto_previos, neto]
    consec = _ciclos_consecutivos_sobre(serie, cfg.umbral_entrada_apr_pct)

    senal = "ninguna"
    if prof_ok and consec >= cfg.persistencia_minima_ciclos:
        senal = "entrada"
    elif neto < cfg.umbral_salida_apr_pct:
        senal = "salida"
    return Evaluacion(bruto, neto, spread, profundidad, prof_ok, consec, senal)

Fijate que la profundidad no bloquea la salida: si hay que salir, se sale, aunque el libro esté fino.

Módulo 04 · Motor 1

Los ocho números que definen el motor.

Cambiar cualquiera de estos cambia el comportamiento del sistema sin tocar una línea de código.

ParámetroDefaultQué controla
ciclo_segundos300Cada cuánto mira. Más corto = más ruido y más requests.
pares5 paresEl universo. Agregar uno es una línea de YAML.
umbral_entrada_apr_pct12El apetito. Bajarlo abre más oportunidades y más riesgo.
umbral_salida_apr_pct4Cuándo se considera que el edge murió.
persistencia_minima_ciclos3El filtro anti-ruido. En 3 ciclos de 300 s = 15 min.
profundidad_minima_usd50.000Descarta pares donde no se puede operar en serio.
fee_taker_spot_pct0,10Fee real de tu cuenta. Ajustar por tier de VIP.
fee_taker_futures_pct0,04Idem, futuros.
horizonte_amortizacion_dias30Cuánto se planea sostener la posición. Afecta el costo anualizado.

Cómo puntúa M1 sus oportunidades

DimensiónCómo la mide M1
retorno_netonormalizar_retorno(apr_neto, umbral_entrada) — en el umbral vale 100.
liquideznormalizar_liquidez(profundidad, monto) — 5× el monto ⇒ 100.
Las otras siete usan el neutro (50) en Fase 1: no hay historial de repetibilidad, ni datos de slippage real, ni health score todavía. Se afinan con datos reales.

La consecuencia de los neutros

Con 7 de 9 dimensiones en 50, un M1 con retorno perfecto (100) y liquidez perfecta (100) da:

0,25×100 + 0,15×100 + 0,60×50 = 70

Justo el umbral_alerta. Es a propósito: en Fase 1 el sistema notifica, pero por diseño ninguna oportunidad llega sola a los umbrales de ejecución. La automatización se gana con datos.

Módulo 05 · Motor 2
M2 Carry argentino · LECAP / BONCAP · táctico, nunca permanente

La tasa en pesos contra el dólar que viene.

El motor más argentino de los seis: gana cuando la tasa en pesos le saca ventaja a la devaluación esperada. Y pierde meses de tasa en un solo salto cambiario.

La mecánica

Poner pesos en LECAP o BONCAP cuando la tasa supera con margen la devaluación esperada. El instrumento paga en pesos; lo que importa es cuánto queda en dólares al vencimiento.

la pregunta que responde el motor ¿Cuánto rinde en USD quedarme en pesos hasta el vencimiento?

La ventaja diferencial de FARO

El dólar cripto opera 24/7 y anticipa al MEP. FARO lo usa como alarma de salida: es una señal en tiempo real de presión cambiaria, antes de que el mercado formal la refleje.

Eso es el FX Risk Engine — el subsistema más interesante de todo M2, y el que lo hace distinto de simplemente mirar una tabla de TIRs.

El riesgo, sin maquillaje

Un salto cambiario brusco licúa meses de tasa. Por eso el motor es táctico, no permanente: entra cuando el retorno en USD es claramente positivo y sale ante la primera señal de estrés.

Dato de contexto relevado: en las primeras ruedas de junio 2026 el dólar minorista saltó >2% en días ($1.430 → $1.460). Ese tipo de movimiento es exactamente lo que el FX Risk Engine tiene que detectar antes, no después.

Los números del contexto

Carry 2026 acumulado~18% en USD (YTD)
LECAP31–37% TNA
Cauciones (muy corto plazo)picos cerca del 50% TNA
Inflación esperada~21% anual
Módulo 05 · Motor 2
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Ninguna fuente sabe todo. El Normalizer las fusiona.

M2 es el motor con la ingesta más complicada: hacen falta tres APIs distintas para armar un solo instrumento evaluable.

ArgentinaDatos metadata: ticker, vencimiento, tipo stale máx: 1440 min Data912 quote: precio, bid/ask, volumen, ops stale máx: 20 min CriptoYa /api/dolar FX: cripto, MEP, CCL, oficial, blue stale máx: 5 min NORMALIZER fusionar_instrumentos() match por ticker InstrumentoCarry ticker · vto · precio · bid/ask volumen · ops · fx del momento evaluar_carry() la matemática función pura Fallback: ManualCsvProvider un CSV en disco con todo ya fusionado — para dev, tests y backtest

Universo filtrado por config

tipos_permitidos: [LECAP, BONCAP], dtm_minimo: 1, dtm_maximo: 365, más whitelist y blacklist. Nada hardcodeado.

Sanity-check opcional: RAVA

Cruza la TIR calculada contra la TIR de mercado de RAVA. Off por default, fail-open, jamás fuente crítica (ADR-049). Divergencia > 10% ⇒ data_quality_warning.

Fail-closed por frescura

fail_closed_on_fx_stale: true
fail_closed_on_market_stale: true
Un FX de hace 6 minutos frena el ciclo. El dólar se mueve rápido.

Módulo 05 · Motor 2
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Cinco pasos: de un precio en pesos a un retorno en dólares.

Todas las funciones reciben y devuelven porcentaje 0–100, nunca fracciones — para evitar el bug clásico de mezclar escalas.

1 · días a vencimiento dtm = fecha_vencimiento − fecha_valuacion
2 · retorno del período — la entrada simula compra al ASK retorno_periodo = (vpv / precio_entrada − 1) × 100
3 · anualizado (dos variantes) simple = retorno_periodo × 365 / dtm
efectivo = ((1 + retorno_periodo)^(365/dtm) − 1) × 100
4 · pasarlo a dólares — acá entra la devaluación esperada retorno_usd = ((1 + retorno_ars) / (1 + deval_esperada) − 1) × 100
5 · el número que decide retorno_usd_neto = retorno_usd − fees − slippage

El parámetro más importante de todo M2

config devaluacion_esperada_pct: 40.0

FARO no predice el dólar. Este número es un supuesto explícito del operador, no una proyección del sistema. Está en la configuración justamente para que sea visible, discutible y auditable.

Subirlo a 60% hace que casi ningún instrumento pase el filtro. Bajarlo a 20% hace que pasen casi todos. Es la perilla de optimismo cambiario, y su valor queda congelado en params_vigentes de cada oportunidad.

Los umbrales de retorno

min_net_usd_return_pct3.0mínimo para que cuente como señal
exigir_retorno_usd_positivotrueno-negociable
fees_pct / slippage_pct0.1 / 0.1costos asumidos
watch_multiplicador_retorno1.5con el FX en WATCH, el umbral sube 50%

Ese último es elegante: cuando el dólar empieza a moverse, FARO no cierra la puerta — sube el precio de entrada. Solo las oportunidades muy buenas justifican el riesgo extra.

Módulo 05 · Motor 2

Un bono con TIR espectacular y sin comprador no sirve.

El liquidity_score_pct combina cuatro señales en un puntaje 0–100. Si da bajo, el instrumento queda afuera del universo aunque el retorno sea el mejor de la tabla.

Volumen

Contra volumen_referencia: 50.000.000 ARS. Igual o más ⇒ score lleno.

Operaciones

Contra operaciones_referencia: 50 por día. Pocas operaciones = precio poco confiable.

Spread bid/ask

Un spread ≥ spread_maximo_pct: 2.0 anula el score de liquidez. Entrar y salir te come el retorno.

Frescura

Cuánto hace que se tomó el snapshot, contra el stale_minutes de la fuente.

spread mid = (bid + ask) / 2
spread_pct = (ask − bid) / mid × 100

El detalle del ask = 0

En datos reales de Data912 aparece ask = 0.0: significa "sin oferta", no un spread válido. La función spread_pct() tira ValueError, y el liquidity_score_pct() lo atrapa sin reventar la evaluación completa del instrumento.

Este es el tipo de detalle que separa un scanner que corre 6 meses de uno que se cae el martes.

El filtro de entrada al universo

instruments:
  tipos_permitidos: ["LECAP", "BONCAP"]
  dtm_minimo: 1
  dtm_maximo: 365
  liquidity_score_minimo_pct: 30.0
  whitelist: []
  blacklist: []

Un instrumento que no pasa estos filtros ni siquiera se evalúa. Y la razón queda en el decision_reason de la observación: "no operar" también se explica.

Facilidad de salida

Es una de las nueve dimensiones del score, y en M2 se deriva del DTM: cuanto más lejos el vencimiento, más difícil salir sin regalar precio. Un instrumento a 15 días es mucho más fácil de abandonar que uno a 300.

Módulo 05 · Motor 2 · el subsistema estrella

El dólar cripto como alarma de incendio.

Cinco señales, cuatro tiers. Se toma el peor tier entre las cinco (any-of, fail-closed): una sola señal en PANIC implica PANIC global, sin importar las otras cuatro.

las 5 señales — cada una se evalúa por su VALOR ABSOLUTO (una caída fuerte alarma tanto como una suba) Cripto 1hwatch ≥ 1,0% Cripto 24hwatch ≥ 3,0% MEP 24hwatch ≥ 2,5% CCL 24hwatch ≥ 2,5% Gap cripto/oficialwatch ≥ 80% ↓ se toma el PEOR tier de las cinco ↓ NORMAL el motor opera con sus umbrales normales WATCH ≥ umbral el umbral de retorno sube ×1,5 solo entran las muy buenas STRESS ≥ umbral × 1,5 bloquear_entradas_en_stress: true no se abre nada nuevo PANIC ≥ umbral × 2,5 SAFE_MODE GLOBAL no solo M2: todo el sistema causa registrada en system_state_log, con la señal exacta que disparó: "FX Risk Engine: PANIC (cripto 1h 2.80% > umbral panic 2.50%)"

Los multiplicadores derivan los tiers

La config define un solo umbral por señal (el de WATCH). STRESS y PANIC se derivan escalándolo: multiplicador_stress: 1.5 y multiplicador_panic: 2.5 (ADR-037). Ajustar la sensibilidad del sistema entero es cambiar dos números.

El límite honesto que está documentado

La ventana histórica para los deltas 1h/24h vive solo en memoria. Tras un reinicio del proceso arranca vacía, y los primeros ticks devuelven 0.0 para las variaciones que todavía no tienen punto de comparación. No crashea, no bloquea, y está escrito en el docstring — no es un gap silencioso.

Módulo 06 · Motor 3
M3 Renta estructural en dólares · el piso de rentabilidad

Que el capital ocioso no esté ocioso.

Mientras M1 espera un funding alto y M4 espera una ventana, hay plata quieta. M3 la pone a rendir 9–12% anual en USD, sin apalancamiento.

M3A · sUSDe variable

El cash-and-carry de Ethena, tokenizado (ERC-4626). Rinde lo que rinde el funding — es M1 en formato pasivo.

APY 7 d: 9,4% · promedio 90 d: 11,8% · mínimo histórico 4,1% · máximo 35,2%.

Costo oculto: cooldown de 7 días para retirar.

M3B · PT-sUSDe fijo

Un Principal Token de Pendle: tasa fija a plazo. Se sabe hoy cuánto se cobra al vencimiento.

Implícita ~11%, típicamente 1–3 puntos por debajo del spot. Se paga certeza.

No tiene concepto de peg: no cotiza 1:1 contra un dólar.

M3C · T-bill tokenizado piso

El benchmark. No es una estrategia: es la vara contra la que se mide todo lo demás.

Un instrumento cripto solo "vale" si su exceso sobre este piso compensa el riesgo extra que asume.

La decisión de diseño más importante de M3: sin apalancamiento

El looping (Pendle PT + Aave) rinde 15–20%+ y quedó explícitamente fuera del sistema base. Chaos Labs advirtió que una caída del 20% en cripto podría disparar ~USD 1.200 millones en liquidaciones por loops apalancados.

La asimetría es mala: se gana un poco más, se puede perder todo. No entra.

Y no es solo una promesa en un documento: hay un invariante duro en el código (valida_sin_capacidades_prohibidas()) que revisa la config y falla si aparece leverage, looping o bridges. AC8 de SPEC-15.

la métrica central de M3 spread_vs_piso = apy_neto_instrumento − apy_piso
min_spread_vs_piso_pct: 1.0 — si un instrumento cripto no le saca al menos 1 punto al T-bill, no justifica el riesgo de smart contract, depeg y protocolo. Y si rinde menos que el piso, se descarta del ranking directamente.

Riesgos propios de este motor

  • Smart contract: el código del protocolo puede tener un bug.
  • Depeg: el dólar sintético deja de valer un dólar.
  • Funding negativo prolongado: es el riesgo de fondo de USDe — si el funding se da vuelta mucho tiempo, el colateral de Ethena sufre.
Módulo 06 · Motor 3
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Cada instrumento tiene su propia cuenta de costos.

El APY que publica el protocolo nunca es el APY que cobrás. M3 descuenta lo que el marketing no descuenta.

sUSDe — el cooldown es un costo de oportunidad real dias_activos = 365 − cooldown_dias
apy_neto = apy_base × dias_activos/365 − fee_mint_redeem
Con susde_cooldown_dias: 7: durante 7 días el capital está inmovilizado sin devengar. Un APY base de 11% se convierte en 10,74% antes de fees. (ADR-039)
PT de Pendle — el implied teórico asume ejecución sin fricción effective_apy = implied_apy − slippage_estimado
slippage_estimado_pct: 0.15. El Hosted SDK de Pendle (que simula el swap con slippage real) llega en F3; en F1 esta resta simple es más honesta que usar el implied a secas.
peg — cuánto se despegó del dólar peg_deviation = |precio − 1| × 100
Un sUSDe a 0,98 da 2,0. Con depeg_umbral_pct: 1.0, eso bloquea la entrada.
precio ≤ 0 no es "depeg total": es un dato roto de la fuenteValueError.

Las cuatro condiciones de bloqueo (no-negociables)

Se evalúan en este orden. Cualquiera alcanza para bloquear; el orden solo decide qué motivo se reporta.

#CondiciónPor qué
1peg staleEl dato de peg no está fresco. No se puede confirmar.
2peg deviation > umbralSe despegó del dólar más de lo tolerado.
3liquidez no medibleNo-negociable de SPEC-15: si no se puede medir cómo salir, no se entra.
4no allowlistedEl mercado no está explícitamente permitido en config.

La excepción del PT

Un PT no tiene concepto de peg — no cotiza 1:1 contra nada. Para esos instrumentos las condiciones 1 y 2 se ignoran por completo, sin importar el valor.

Es el tipo de detalle que, mal resuelto, bloquearía permanentemente un instrumento perfectamente sano.

Fail-closed ante dato ausente

Si el instrumento tiene concepto de peg y el dato no está (peg_dev_pct is None): se bloquea igual. No se puede confirmar el peg ⇒ no se entra.

Módulo 06 · Motor 3

Cuándo el problema no es el instrumento: es el protocolo.

Un depeg de sUSDe bloquea sUSDe. Pero si el colchón de reservas de Ethena se desploma, el problema es sistémico — y escala al sistema entero.

factor_riesgo_ethena — fracción 0..1 combina el reserve ratio del protocolo
y el funding rate que sostiene el colateral
UmbralValorConsecuencia
reserve_ratio_minimo_pct1.0Debajo: el colchón es fino.
funding_flip_umbral_apy_pct0.5El funding se está dando vuelta.
factor_riesgo_critico0.2⇒ SAFE_MODE GLOBAL

Por qué escala a global y no solo bloquea M3

Porque si Ethena está en problemas, el funding cripto probablemente también lo esté — y ese es el Motor 1. Un estrés sistémico del protocolo no es un problema de un instrumento: es una señal sobre el estado del mercado que M1 también está explotando.

Cap de exposición por protocolo

config max_exposicion_por_protocolo_pct: 20.0

Nunca más del 20% del capital en Ethena, ni en Pendle, ni en el emisor del T-bill. Aunque el spread contra el piso sea espectacular. Aunque el score sea 98.

Es la misma lógica del tope_por_plataforma_pct del RiskGuard, aplicada a protocolos DeFi en vez de exchanges.

El ranking

rankear_instrumentos() ordena los candidatos por spread contra el piso, descartando los que rinden menos que el piso (nunca es justificable) y los que tienen la entrada bloqueada.

La liquidez de salida es no-negociable

exige_exit_liquidity_medible: true, con liquidez_referencia_usd: 5.000.000 como monto contra el cual se calcula el score.

Entrar es fácil. La pregunta que M3 hace siempre primero es cómo se sale.

Módulo 07 · Motor 4
M4 Arbitraje dólar cripto local · el cazador de ventanas

La pizarra miente. El cruce ejecutable, no.

Este es el motor donde la ventaja competitiva es puramente geográfica: estar en Argentina, con cuentas en ocho exchanges locales.

La trampa que arruina a los principiantes

Mirás la pizarra: el USDT en un exchange está a $1.514 y en otro a $1.542. 1,8% de spread. Parece plata gratis.

No lo es. Los spreads de pizarra entre extremos van del 1% al 3%, pero el cruce ejecutable — el mejor ask que realmente podés comprar contra el mejor bid que realmente podés vender, con las fees de ambas puntas — suele estar cerrado o negativo en equilibrio.

El día que se relevó el motor (10/06/2026), el cruce ejecutable estaba negativo. La pizarra decía 1–3%.

Cuándo aparecen las ventanas reales (0,5–2% neto)

  • Saltos del dólar — el mercado se desacomoda y tarda en re-arbitrarse.
  • Anuncios económicos — el mismo efecto, con aviso.
  • Cierres de mes — flujos que no responden al precio.
  • Fines de semana — el MEP está cerrado y el cripto no. Nadie está arbitrando.

El diseño seguro: cero exposición al precio

1. Capital pre-posicionado: ARS en el exchange A, USDT en el exchange B.
2. Al detectar el cruce, se ejecutan las dos puntas simultáneamente.
3. El rebalanceo se hace después, sin apuro (transferencia ARS gratis entre CVUs, USDT por TRC20/Polygon a centavos).

Nunca hay un momento en el que FARO esté "esperando que llegue la transferencia" con el precio moviéndose en contra. Mover capital en el momento de operar es una de las causas típicas de fracaso.

Módulo 07 · Motor 4
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Una request. Toda la plaza argentina.

CriptoYa devuelve compra y venta de todas las stables en todos los exchanges argentinos en un solo request. Cada 60 segundos.

La matriz

# matriz[moneda][exchange] -> Cotizacion
class Cotizacion:
    total_ask: float  # ARS que PAGÁS por 1 unidad
    total_bid: float  # ARS que RECIBÍS por 1 unidad
                      # (fees del exchange ya incluidas)

monedas:   ["USDT", "USDC", "DAI"]
exchanges: ["binancep2p", "lemoncash", "ripio",
            "buenbit", "fiwind", "letsbit",
            "belo", "satoshitango"]

3 monedas × 8 exchanges = hasta 24 celdas por ciclo.

Los dos tipos de cruce que se calculan

1 · Inter-plaza (mismo coin, distinto exchange)

Comprar USDT en Lemon, vender USDT en Ripio. El arbitraje clásico.

2 · Stable-vs-stable (mismo exchange, distinto coin)

Comprar USDT y vender DAI en el mismo lugar. DAI suele operar con prima local.

Misma pasada del scanner, mucha menos fricción: no hay transferencia entre plazas. Es la variante más subestimada del motor.

el cruce ejecutable neto — la única cuenta que importa ask_neto = total_ask × (1 + fee_compra/100) ← lo que realmente pagás
bid_neto = total_bid × (1 − fee_venta/100) ← lo que realmente cobrás
spread_neto_pct = (bid_neto / ask_neto − 1) × 100

El umbral

ventana_minima_neta_pct: 0.8
Con fees de 0,1–0,5% por punta, por debajo de ~0,8% no justifica operar.

Las fees, por exchange

default: 0.4 · binancep2p: 0.0 · buenbit: 0.45. Se ajustan con datos reales de tu cuenta.

El filtro de sanidad

desviacion_maxima_mediana_pct: 20
Una plaza que se desvía más del 20% de la mediana de todas se descarta y se loguea como data_quality. Una API rota no inventa un arbitraje del 15%.

Módulo 07 · Motor 4
1 · Feed
2 · Evaluar
3 · Score
4 · Gate
5 · Ledger
6 · Alerta

Se registra la matriz completa, no solo las señales.

Esta es una decisión de diseño deliberada de M4, y es la que convierte a la Fase 1 en algo más que "esperar".

Qué se guarda cada 60 segundos

  • La matriz completa de cotizaciones, haya o no oportunidad.
  • Todos los cruces calculados con su spread neto.
  • Las plazas descartadas por el filtro de sanidad, con el valor rechazado.
  • Una oportunidad por dirección de cruce (se abre cuando aparece, se cierra cuando desaparece).

La dirección del cruce

USDT@lemoncash->USDT@ripio

Cada dirección es una entidad con vida propia: abre, dura, cierra. No es un evento puntual.

Las preguntas que solo se pueden responder con estos datos

PreguntaSe responde con
¿Cuántas ventanas reales hay por día?Contar oportunidades abiertas por día.
¿Cuánto duran?ts_cierre − ts_apertura. Si duran 40 segundos, el ciclo de 60 s no alcanza.
¿De qué tamaño son, neto de fees?metrica_principal de cada oportunidad.
¿En qué momentos aparecen?Distribución horaria y por día de la semana de las aperturas.
¿Qué par de exchanges las produce?Agrupar por dirección de cruce.

Y la más importante

¿Este motor vale la pena? Si tras 14 días de observación las ventanas son tres por semana y de 0,9%, la respuesta honesta puede ser no — y el sistema tiene los datos para decirlo, en vez de que alguien lo decida por intuición.

Eso es exactamente para lo que existe la Fase 1.

Módulo 08 · Motor 5
M5 Mercados de predicción · solo observación

Comprar el SÍ y el NO por menos de un dólar.

La aritmética es trivial. El problema no es la matemática: es la velocidad.

arbitraje intra-mercado suma = (ask_yes + ask_no) × 100
hay_arbitraje = suma ≤ suma_maxima_arb_pct (99,0)

Si comprás YES a $0,48 y NO a $0,49, gastaste $0,97. Al resolverse el mercado, uno de los dos paga exactamente $1, gane quien gane. Ganancia asegurada: 3 centavos por dólar.

Variante multi-outcome ("negative risk"): comprar todos los resultados cuando suman menos de $1.

Que la oportunidad existe, está probado

Un estudio de IMDEA Networks analizó 86 millones de apuestas (abr-2024 a abr-2025) y encontró más de USD 40 millones extraídos en arbitraje solo en Polymarket.

Por qué está apagado

  • Las ventanas se cierran en segundos. La identificación manual es imposible en 2026: el bot no es una ventaja, es la condición mínima de entrada.
  • Fees dinámicas anti-latencia. En enero de 2026 Polymarket introdujo fees de taker dinámicas en los mercados cripto de 15 minutos, específicamente para matar el arbitraje de latencia.
  • Resolución disputada. El oráculo UMA puede fallar o ser disputado.
  • Capital inmovilizado hasta la resolución del mercado.
  • Liquidez fina en los mercados chicos.

Decisión: entra al sistema en modo solo observación. Pasa a producción únicamente si la fase de medición demuestra que se llega a tiempo. La telemetría de latencia es literalmente el criterio de admisión.

Sin wallet, sin keys

El observador solo consulta market data público del CLOB por HTTP. En F1 no existe la capacidad de ejecutar.

Ciclo de 30 s

El más rápido de los seis. Y aun así, probablemente demasiado lento — eso es lo que hay que medir.

Ejecución = FAROPOLY

Si algún día se ejecuta, será un runtime Java separado (ADR-028), no este proceso Python.

Módulo 08 · Motor 5

El observador no busca plata. Busca evidencia.

M5 en Fase 1 tiene un único objetivo: producir el dato que permita responder GO / NO-GO con números en vez de con ganas.

# cada consulta al libro registra su round-trip
@dataclass(frozen=True)
class LibroToken:
    best_ask: float  # precio del token, 0..1
    rtt_ms:   float  # ← LA MÉTRICA QUE DECIDE TODO

El rtt_ms se guarda en cada observación, haya arbitraje o no. Después de semanas, la distribución de esos números responde la pregunta:

la pregunta de admisión ¿El RTT desde una EC2 en sa-east-1 es
lo bastante bajo como para llegar antes
de que la ventana se cierre?

Si la respuesta es no, el motor no entra. Y no habrá sido una pérdida: habrá sido una decisión barata bien tomada.

Taxonomía FAROPOLY

Cada ventana detectada se clasifica contra el registry de estrategias de FAROPOLY, sin cambiar la detección:

CódigoEstrategiaEstado
M-REBRebalancing arb (YES+NO binario)PURSUE
M3Negative-Risk multi-outcome (Σ YES > 1)STUDY
cross-platform vs Kalshi
REJECT — bloqueado por verificación regulatoria del acceso a Kalshi desde Argentina.

El detalle de las fees por categoría

Las categorías de geopolítica y eventos mundiales no pagan taker fee en Polymarket V2. En F1 el umbral sale de suma_maxima_arb_pct a secas; la matemática completa (edge_neto = 100 − Σasks − fees(cat, p)) entra en F1.5.

Está documentado como diferido (ADR-027), no olvidado. La diferencia importa.

Módulo 09 · Motor 6
M6 Wallbit · conector de observación · soporte a M2 / M3 / M4

El motor que no es un motor.

Wallbit no captura una ineficiencia propia: le da a los otros motores datos y capacidades que no tienen. Y pasó la auditoría de seguridad de FARO por diseño.

Por qué Wallbit cumple el principio 4 por construcción

La API tiene scopes granulares (read y trade) y — el punto clave — no existe ningún endpoint de retiro o egreso externo en su API pública. Ni ARS, ni wire, ni cripto saliente.

La única "transferencia" posible es interna: checking ↔ inversión.

Consecuencia: aunque alguien robara la API key, no podría sacar la plata del sistema. Cumple la exigencia "el sistema jamás retira fondos" a nivel de la propia plataforma, no solo por configuración de permisos.

Qué aporta

rates ARS→USDcon rate y updated_at. Alimenta el contexto de M2 y M4.
balances multi-monedachecking + stocks + USD disponible.
transactionscon filtros y paginado — insumo del reporte fiscal.
fees del tierlas fees reales de tu cuenta, no las de la tabla pública.

Configuración en Fase 1

wallbit:
  activo: false        # hasta tener la key read-only
  ciclo_segundos: 900
  frescura_maxima_min: 30  # alerta si el rate envejece

Rate limits de la plataforma: 60 req/min y 10.000/día en el tier Standard. El ciclo de 900 s deja muchísimo margen.

Módulo 10 · Cockpit

Siete pantallas. Cada una responde una pregunta.

Astro estático, servido desde el propio núcleo en http://10.0.0.1:8420/ — la interfaz de WireGuard. Refresca cada 30 segundos contra la API.

PantallaLa pregunta que respondeDe dónde saca los datos
Inicio¿Está todo funcionando? ¿Cuántas oportunidades hubo hoy?/estado · /motores · /salud/motores · /metricas/diarias
Oportunidades¿Qué ventanas se abrieron, cuánto duraron, cuánto valían?/oportunidades
Carry (M2)¿Cómo está el dólar? ¿Qué LECAP conviene y cuál está bloqueada?/observaciones/serie?motor=carry_argentina
Renta (M3)¿Qué instrumento le gana al piso? ¿Ethena está sano?/observaciones/serie?motor=renta_usd
Métricas¿Cuántas ventanas por día y por motor? ¿Va mejorando o peorando?/metricas/diarias
Configuración¿Con qué criterio está corriendo? ¿Qué cambió y quién lo cambió?/config · /config/versiones · /config/audit
Alertas¿Qué salió mal y todavía nadie lo atendió?/risk_events

El botón PAUSA está en todas

Arriba a la derecha, en la barra, siempre visible. Junto al chip de estado. El freno no se busca en un menú.

Nota sobre las capturas que siguen

Las pantallas de las próximas páginas están reconstruidas fielmente desde el código Astro (cockpit/src/pages/) con datos de ejemplo. Sirven para entender qué muestra cada una y por qué. Las cifras son ilustrativas, no lecturas reales del sistema.

Módulo 10 · Cockpit

Inicio: el chequeo de 10 segundos.

Lo primero que se mira todos los días. Si esta pantalla está verde, no hay nada que hacer.

Inicio

Salud de motores4/4 motores OK
MotorEstadoVisto
funding_binanceOKhace 2m
carry_argentinaOKhace 7m
renta_usdOKhace 41m
stables_argentinaOKhace 38s
prediccionAPAGADOnunca
Fase
1
Estado
OBSERVE
Pausa global
no
Config
v14
Motores
funding_binanceactivo
modo: observacion · ciclo: 300s
Oportunidades hoy: 3
carry_argentinaactivo
modo: observacion · ciclo: 900s
Oportunidades hoy: 2
renta_usdactivo
modo: observacion · ciclo: 3600s
Oportunidades hoy: 1
stables_argentinaactivo
modo: observacion · ciclo: 60s
Oportunidades hoy: 0

Cómo leerla: el KPI de salud arriba de todo no dice "el proceso está vivo" — dice "los motores están produciendo observaciones". Un SILENCIOSO ahí es la alarma temprana más valiosa del sistema. renta_usd visto hace 41 minutos es normal (su ciclo es de una hora); stables_argentina hace 38 segundos también (su ciclo es de un minuto). Cada motor se mide contra su propia cadencia.

Módulo 10 · Cockpit

Carry: el panel del dólar.

La pantalla más densa del cockpit, porque el motor más complejo necesita mostrar dos cosas a la vez: el estado del FX y el ranking de instrumentos.

Carry argentino (M2)

Dólar (panel FX)

Estado del FX Risk Engine: WATCH

Mayorista
$ 1.402,50
Oficial venta
$ 1.418,00
Blue venta
$ 1.475,00
Cripto ask/bid
$ 1.489,20 / 1.481,10
MEP
$ 1.466,80
CCL
$ 1.478,40
VariaciónValorQué significa
Cripto 1h+1,24%Movimiento en la última hora — cambios bruscos disparan WATCH/STRESS.
Cripto 24h+2,10%Tendencia de mediano plazo del ciclo.
MEP 24h+1,05%Referencia del dólar financiero regulado.
CCL 24h+1,32%Referencia del dólar financiero para afuera.
Gap cripto vs oficial+5,02%Brechas grandes son señal de estrés cambiario.
Instrumentos (LECAP/BONCAP)

Piso = devaluación esperada. El estado del dólar puede bloquear entradas aunque el retorno sea positivo.

TickerTipoDTMRetorno USD netoLiquidezEstadoMotivo
S30Y6LECAP47+6,20%78WATCHsupera el umbral ×1,5 exigido en WATCH
T15E7BONCAP129+4,10%64bloqueadaretorno 4,10% < umbral WATCH 4,50%
S18J6LECAP12+1,80%91sin señalretorno < min_net_usd_return_pct (3,0%)
T30N6BONCAP218−0,40%22descartadaliquidity_score 22 < mínimo 30

Cómo leerla: con el FX en WATCH, el umbral de retorno subió de 3,0% a 4,5% (×1,5). Por eso T15E7, con un +4,10% que en NORMAL sería señal, aparece bloqueada. Y la columna Motivo nunca está vacía: incluso el "no operar" está explicado. Eso es el principio 5 hecho interfaz.

Módulo 10 · Cockpit

Renta: todo se mide contra el piso.

Renta USD (M3)

Ranking de instrumentos
TickerTipoAPY netoSpread vs pisoLiquidezPegRankingMotivo
sUSDevariable10,74%+6,44%880,04%1sin bloqueos: peg ok, liquidez medible, allowlisted
PT-sUSDEfijo10,85%+6,55%61n/a2sin concepto de peg (PT) · vence 13/08/2026
T-BILL-TOKENIZADObenchmark4,30%— (es el piso)970,01%pisoreferencia, no candidato
Riesgo de protocolo

El factor combina reservas y funding del protocolo; si cruza el umbral crítico, el sistema escala a SAFE_MODE.

ethena
factor de riesgo
0,81
crítico < 0,20
reserve ratio
colchón del protocolo
3,4%
mínimo 1,0%
funding rate
sostén del colateral
+8,2%
flip < 0,5%

Cómo leerla: el PT rinde nominalmente más (10,85% vs 10,74%) pero tiene menos liquidez de salida (61 vs 88) — y esa diferencia entra en el score. El T-bill no compite: es la vara. Y el panel de riesgo de protocolo es el que hay que mirar cuando algo raro pasa en cripto: si ese 0,81 se desploma por debajo de 0,20, todo FARO — no solo M3 — pasa a SAFE_MODE.

Módulo 10 · Cockpit

Configuración: validar antes de aplicar.

La pantalla donde se cambia el criterio del sistema. Con red de contención en cada paso.

Configuración

ValidarAplicarDescartar cambios
motores:
  funding_binance:
    umbral_entrada_apr_pct: 12   # ← editás acá
    persistencia_minima_ciclos: 3

✓ Config válida. 2 campos cambiados respecto de la versión activa.

Versiones
#EstadoOrigenMotivoFecha
14activacockpitbajo umbral de funding a 1212/07 09:14
13archivadacockpitactivo renta_usd11/07 18:02rollback
12archivadaapiajuste de fees por tier08/07 11:47rollback
Auditoría de cambios
FechaCampoDeAOrigen
12/07 09:14motores.funding_binance.umbral_entrada_apr_pct1512cockpit
11/07 18:02motores.renta_usd.activofalsetruecockpit

Cómo leerla: "Validar" corre pydantic sin aplicar nada — si los pesos del scoring no suman 1,0 o un umbral está fuera de rango, te lo dice ahí. "Aplicar" crea una versión nueva y el cambio surte efecto en el próximo tick de cada motor, sin reiniciar el servicio. Y todo cambio deja rastro: la tabla de auditoría responde para siempre la pregunta "¿quién bajó ese umbral y cuándo?".

Módulo 10 · Cockpit

Las tres que miran para atrás.

FAROOportunidadesOBSERVE
MotorParAperturaCierreMétricaGan. est.Estado
funding_binanceSOL/USDT09:02APR 16,4%abierta
stables_argentinaUSDT@lemon→ripio08:4108:471,12%$5,60cerrada
carry_argentinaS30Y608:15USD 6,2%abierta

Oportunidades. La duración (cierre − apertura) es el dato más valioso de la Fase 1: esa ventana de stables duró 6 minutos. Con un ciclo de 60 s, FARO la vio seis veces. Se llegaba.

FAROAlertasSAFE_MODE
FechaSeveridadTipoMensaje
10:22críticofx_panicFX Risk Engine: PANIC (cripto 1h 2,80% > umbral panic 2,50%)resolver
09:58warningdata_qualitybuenbit descartado: total_ask se desvía 24% de la medianaresolver
07:10infoapi_degradadadata912: 2 reintentos, recuperadoresolver

Alertas. Con severidad, tipo y entidad. Se marcan como resueltas y quedan con resuelto_ts. Una alerta crítica sin resolver es deuda operativa visible.

FAROMétricasOBSERVE
DíaMotorVentanas
12/07funding_binance3
12/07stables_argentina0
11/07stables_argentina4

Métricas. Ventanas por día y por motor. Es literalmente el criterio de avance de fase: esta tabla, acumulada 14 días, es lo que dice si vale la pena poner plata.

Módulo 11 · Operación

Telegram: el sistema en el bolsillo.

El cockpit es para gestionar y analizar. Telegram es para enterarse y frenar. El kill switch existe en ambos.

Los comandos

/estadoFase, estado FSM, motores activos, métrica destacada de cada uno.
/estado detalle+ liveness de cada motor, timestamps exactos, anomalías.
/limitesLos topes vigentes del RiskGuard.
/oportunidadesLas ventanas abiertas ahora.
/pausa⇒ SAFE_MODE. Recuperable.
/reanudarSale de SAFE_MODE al estado previo.
/killPide confirmación explícita.

La confirmación de /kill

⚠️ /kill detiene TODO y pasa a KILLED
(no se reanuda automáticamente).
Confirmá con: /kill CONFIRMAR

La confirmación expira a los 60 segundos. No se puede matar el sistema con un pulgar distraído, ni revivir una confirmación vieja.

Whitelist dura

Los comandos se procesan únicamente desde el chat autorizado. Cualquier otro origen se ignora, no se responde.

Rate-limit y agrupación de ráfagas

Si un motor detecta doce oportunidades en un ciclo, no llegan doce mensajes. Se agrupan. Un bot que spamea es un bot que se silencia — y un bot silenciado no avisa cuando importa.

Los niveles de notificación

notificaciones:
  telegram:
    nivel_minimo: "oportunidad"
      # debug | oportunidad | operacion | critico
    resumen_diario_hora_local: "21:00"

El detalle transaccional

La señal de Telegram se registra en senales_telegram dentro de la misma transacción que la oportunidad. No puede existir una alerta enviada sin su registro en el ledger, ni al revés.

Módulo 11 · Operación

Dónde vive, y qué hacer cuando se rompe.

El despliegue

HostEC2 dedicada, Ubuntu 24.04, sa-east-1. Aislada de la infra de producción de la empresa.
Serviciosystemd. Logs a journald en JSON estructurado.
RedAcceso solo por WireGuard. La API bindea a 10.0.0.1 — nunca a 0.0.0.0.
BasePostgreSQL 16. Migraciones con alembic.
Secretos/etc/faro/faro.env con chmod 600. Nunca en el repo, nunca en config.yaml.
HorasTodo se almacena en UTC; se presenta en America/Argentina/Cordoba.

Lo que nunca se loguea

  • Secretos y tokens
  • El balance total del inversor
  • Datos personales

La rutina diaria (5 minutos)

  1. Abrir Inicio: ¿4/4 motores OK?
  2. Mirar Alertas: ¿hay algo crítico sin resolver?
  3. Mirar Métricas: ¿la cuenta de ventanas por día tiene sentido?

Los playbooks de incidente

SíntomaQué significa · qué hacer
Motor SILENCIOSOEl proceso vive pero el scanner no produce. Revisar journald del motor; probable excepción silenciada o API externa colgada.
Racha de errores de APIYa está en SAFE_MODE. Verificar si es el venue o la red. Al sanar, /reanudar.
Clock driftSAFE_MODE global. Revisar NTP. Los funding times dependen del reloj.
DepegSAFE_MODE. No es un bug: es el sistema haciendo exactamente lo que tiene que hacer.
Mismatch de conciliaciónKILLED. El ledger no coincide con el exchange. No reanudar hasta entender por qué.
Módulo 12 · Cierre

Los "no" valen tanto como los "sí".

Se evaluaron 13 oportunidades. Entraron 6. Estas son las que quedaron afuera, y por qué — porque un sistema se define tanto por lo que hace como por lo que se negó a hacer.

OportunidadPor qué NO
Looping apalancado (Pendle PT + Aave)Rinde 15–20%+, pero la asimetría es mala: se gana un poco más y se puede perder todo. Chaos Labs advirtió que una caída del 20% en cripto podría disparar ~USD 1.200M en liquidaciones en cascada. Hay un invariante duro en el código que lo impide.
MEV / arbitraje on-chain en DEXsEs una guerra de microsegundos contra infraestructura de millones de dólares. No hay partido.
Surebets deportivasFunciona… hasta que las casas te cierran la cuenta. Semanas, no meses.
Airdrop farmingLa rentabilidad por hora se desplomó con los filtros anti-sybil. Oportunismo puntual, no estrategia.
Arbitraje triangular intra-exchangeContra HFT no hay partido. Queda como módulo de aprendizaje, nada más.
Grid tradingNeutral. No hay ventaja competitiva propia que lo justifique.
Cross-platform Polymarket vs KalshiBloqueado por verificación regulatoria del acceso a Kalshi desde Argentina.
T-bills / lending simple en Aave (4–6%)No es una estrategia: es estacionamiento de capital. Por eso el T-bill entra en M3 como piso de referencia, no como motor.

El patrón detrás de todos los descartes

Ninguno se descartó por "riesgoso". Se descartaron porque FARO no tenía ventaja competitiva en ellos, o porque la asimetría entre lo que se gana y lo que se puede perder era mala. Perfil arriesgado no significa perfil ingenuo.

Módulo 12 · Cierre

Cómo agregar un motor nuevo.

La prueba final de que la arquitectura es buena: qué tan aburrido es extenderla.

El checklist

#PasoDónde
1Escribir la SPEC: fuentes, matemática, umbrales, criterios de aceptación.specs/SPEC-NN.md
2Agregar la sección al esquema de config y su modelo pydantic.config/config.example.yaml
core/config.py
3Definir el Protocol del feed (capa 1) y su provider real.scanners/nuevo.py
4Escribir las funciones puras (capa 2). Tests primero.scanners/nuevo.py
tests/
5Armar el Scanner (capa 3): tick, persistencia idempotente, apertura/cierre de oportunidades.scanners/nuevo.py
6Mapear las métricas crudas a subscores. Lo que no se mide, va al neutro.usar core/scoring.py
7Registrar el job en el scheduler, con su gate de estado.core/runtime.py
8Sumar la pantalla al cockpit si el motor lo amerita.cockpit/src/pages/
9Anotar las decisiones ambiguas como ADR.docs/DECISIONES.md

Lo que no hay que tocar

  • El scoring (se usa, no se modifica).
  • La FSM (se respeta, no se puentea).
  • El RiskGuard (se pasa por él, siempre).
  • El modelo del ledger (las tablas son genéricas: motor, par, payload JSON).

Si agregar un motor te obliga a modificar el núcleo, o el motor está mal pensado o el núcleo tiene un agujero. Vale la pena parar y averiguar cuál de las dos.

Las reglas que no se negocian al extender

  • No agregar estrategias direccionales, señales ni predicción de precios. Ese alcance lo fija MODEL.md.
  • No integrar exchanges o protocolos nuevos sin SPEC.
  • No meter pandas/numpy en el núcleo sin necesidad demostrada. El scanner tiene que ser liviano.
  • No implementar nada de Fase 2+ mientras la Fase 1 no esté validada con 14 días de datos reales.

Ante la ambigüedad

Elegir siempre la interpretación más conservadora — la que opera menos, la que arriesga menos — y dejar la nota en docs/DECISIONES.md. Un ADR de más nunca hizo daño.

Módulo 12 · Cierre

Glosario de bolsillo.

APR / APYRendimiento anualizado. APR es simple; APY capitaliza.
Delta-neutralPosición cuyo resultado no depende del movimiento del precio.
Funding ratePago periódico entre longs y shorts en un futuro perpetuo, para que su precio no se despegue del spot.
Cash-and-carryComprar spot + vender perpetuo. Cobra el funding sin exposición al precio.
Carry tradeEndeudarse/quedarse en la moneda de tasa baja, invertir en la de tasa alta.
LECAP / BONCAPLetras y bonos capitalizables del Tesoro argentino, en pesos.
DTMDays to maturity: días hasta el vencimiento.
TIRTasa interna de retorno.
MEP / CCLDólares financieros: vía bonos en el mercado local (MEP) o para girar afuera (CCL).
sUSDeDólar sintético de Ethena. Rinde el funding cripto, tokenizado.
PT (Pendle)Principal Token: rendimiento fijo a plazo. No tiene peg.
DepegCuando una stablecoin deja de valer 1 dólar.
Spread ejecutableEl cruce real (mejor ask comprable vs mejor bid vendible), neto de fees. No la pizarra.
SlippageLa diferencia entre el precio que veías y el que conseguiste.
DrawdownCaída desde el pico anterior.
Time underwaterCuánto tiempo se está por debajo del pico anterior.
ADRArchitecture Decision Record: una decisión de diseño, con su contexto y su porqué.
FSMMáquina de estados finitos. Acá: los 6 estados globales de FARO.
CLOBCentral Limit Order Book: el libro de órdenes de Polymarket.
Módulo 12 · Cierre

Si te quedás con cinco cosas.

01 · No predice. Cobra.

FARO captura ineficiencias estructurales — funding, carry, arbitraje de plazas, renta USD. Jamás apuesta a una dirección de precio. Ese es todo el alcance, y está fijado en MODEL.md.

02 · El criterio es configuración.

El código implementa mecanismos; la config define políticas. Cada umbral, cada límite y cada motor activo vive en config.yaml, versionado y auditado. Si un criterio está hardcodeado, es un bug.

03 · Los seis motores son el mismo motor.

Tres capas (feed / puras / scanner), seis etapas por ciclo, un scoring común, una FSM común, un ledger común. Cambia la matemática del paso 2. Nada más.

04 · Falla cerrado, siempre.

Dato viejo, API caída, reloj corrido, cotización absurda, config inválida ⇒ el sistema hace menos, no más. Perderse una oportunidad es barato. Operar con datos rotos, no.

05 · Todo queda escrito.

Cada observación, cada oportunidad, cada "no operar", cada cambio de config, cada transición de estado, cada alerta. Con la regla que lo disparó y los parámetros vigentes en ese instante. Dentro de un año, la pregunta "¿por qué hizo eso?" va a tener respuesta. Esa es, al final, la diferencia entre un sistema y un script.

FARO · documentación técnica v1 · generada desde el repositorio · Este documento no constituye asesoramiento financiero.

1 / 1
← → para navegar · P imprimir