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.
Documento generado desde el código fuente del repo (src/faro/, specs/, config/, cockpit/) · v1
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 verdeson 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.
Así se ve la tira: la etapa iluminada es la que trata el slide.
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 sí 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.
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.
| # | Principio | Cómo se hace cumplir en el código |
|---|---|---|
| 1 | El criterio es configuración, nunca código. | Todo umbral vive en config.yaml, validado con pydantic. Un criterio hardcodeado es un bug. |
| 2 | Fase 1 es solo lectura. | Prohibido crear órdenes o importar credenciales con permiso de trade. Las keys de F1 son read-only. |
| 3 | Límites duros, no sugerencias. | RiskGuard.verificar() corre antes de cada orden. Si excede, la orden no se crea y el rechazo se registra. |
| 4 | El sistema jamás retira fondos. | Las API keys nunca tienen permiso de withdrawal; se verifica con un assert al arrancar. |
| 5 | Todo es auditable. | Cada observación, cada oportunidad y cada "no operar" van al ledger con timestamp, motor, regla, parámetros vigentes y resultado. |
| 6 | Fallar cerrado. | Config inválida, API caída, dato viejo o reloj corrido ⇒ el motor se pausa y alerta. Nunca operar con datos dudosos. |
| 7 | Estados 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.
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_vigentesyconfig_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.
Los seis motores, de un vistazo.
Cada uno captura una ineficiencia distinta. Se complementan: no dependen del mismo hecho del mundo.
| Motor | Qué cobra | Fuente de datos | Ciclo | Estado F1 | |
|---|---|---|---|---|---|
| M1 | Funding cripto cash-and-carry | La tasa que pagan los longs apalancados cada 8 h. Delta-neutral. | Binance (ccxt, público) | 300 s | observación |
| M2 | Carry argentino LECAP / BONCAP | La tasa en pesos cuando supera a la devaluación esperada. | ArgentinaDatos + Data912 + CriptoYa | 900 s | observación |
| M3 | Renta USD sUSDe / PT / T-bill | El rendimiento estructural del capital ocioso en dólares. | DefiLlama + Pendle | 3600 s | observación |
| M4 | Stables Argentina arbitraje local | La diferencia ejecutable del mismo dólar cripto entre plazas. | CriptoYa (una request, toda la matriz) | 60 s | observación |
| M5 | Polymarket predicción | YES + NO por debajo de $1: paga $1 gane quien gane. | Polymarket CLOB (público) | 30 s | apagado |
| M6 | Wallbit conector | Soporte a M2/M3/M4: rates ARS→USD y balances. Solo lectura. | api.wallbit.io | 900 s | apagado |
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.
Cuatro fases. Ninguna se saltea.
Cada fase tiene un criterio de avance explícito. No se avanza por entusiasmo; se avanza por evidencia.
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.
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.
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.
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.
| Etapa | Qué pasa | Quién lo hace | Si falla |
|---|---|---|---|
| 0 · Gate | Antes de tocar la red: ¿el sistema está escaneando? ¿este motor está activo? Se relee la config en cada disparo. | core/scheduler.pytick_con_gate() | No corre el tick. Un /kill o un toggle surten efecto en el ciclo siguiente, sin reiniciar. |
| 1 · Feed | I/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 · Evaluar | La 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 · Score | Las métricas crudas se normalizan a un puntaje 0–100 común, con los pesos de config. | core/scoring.py | Dimensión sin datos ⇒ neutro 50, documentado. No se infla. |
| 4 · Gate | El score cae en un tramo: ninguno / alerta / manual / auto. | gate(score, cfg) | El gate clasifica, no ejecuta. Nunca puentea RiskGuard ni FSM. |
| 5 · Ledger | Se persiste la observación (idempotente por ts_ciclo) y, si corresponde, se abre o cierra la oportunidad. | ledger/repos.py | Todo en una transacción. Si falla, no queda una alerta enviada sin registro. |
| 6 · Alerta | Telegram, solo si el score supera umbral_alerta y el nivel configurado lo permite. | notify/telegram.py | La 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.
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.
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.
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_versionscon 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.
El ledger: la memoria perfecta.
Ocho tablas. Todo lo que el sistema vio, decidió, ignoró y le dijeron que haga.
| Tabla | Qué guarda | Campos que importan |
|---|---|---|
observaciones | Todo 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) |
oportunidades | Ventanas 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_events | Todo 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_log | Cada transición de la FSM. Quién pasó a SAFE_MODE, por qué y a instancias de quién. | estado_anterior → estado_nuevo, causa (texto legible), origen |
config_versions | El YAML completo de cada versión del criterio, con checksum. | nro, estado (draft/activa/archivada), yaml_content, checksum |
config_audit | Campo por campo: qué cambió, de qué a qué, quién y cuándo. | campo, valor_anterior, valor_nuevo, origen |
senales_telegram | Qué se notificó y si salió bien. Ligado a la oportunidad. | oportunidad_id, tipo, enviado_ok |
venues · order_intents | Catá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".
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/estado | fase, estado FSM, pausa global, versión de config |
GET /v1/motores | por motor: activo, modo, ciclo |
GET /v1/salud | salud general del proceso |
GET /v1/salud/motores | liveness de trabajo: OK / SILENCIOSO / CAÍDO / APAGADO |
GET /v1/oportunidades | ventanas abiertas y cerradas |
GET /v1/observaciones/serie | serie temporal para graficar |
GET /v1/metricas/diarias | ventanas por día y por motor |
GET /v1/risk_events | alertas y anomalías |
GET /v1/config · /audit · /versiones | el criterio vigente y su historia |
Admin — lo que cambia el mundo
POST /v1/pausa | freno operativo ⇒ SAFE_MODE |
POST /v1/reanudar | sale de SAFE_MODE al estado previo |
POST /v1/kill | KILLED. No se reanuda sin humano. |
POST /v1/config/validar | pydantic dice sí o no. No aplica nada. |
POST /v1/config/aplicar | nueva 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}/resolver | marcar 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.
Cómo se comporta FARO cuando algo se rompe.
Principio 6: fallar cerrado. Ante la duda, el sistema hace menos, no más.
| Qué falla | Cómo se detecta | Qué hace FARO |
|---|---|---|
| Una API externa no responde | Reintentos 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 API | max_errores_api_consecutivos: 3 | ⇒ SAFE_MODE. Deja de escanear ese motor hasta que sane. |
| El dato llega, pero viejo | stale_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 absurda | desviacion_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: 2 | ⇒ SAFE_MODE global. Los horarios de funding dependen del reloj; con drift no se opera. |
| El motor "vive" pero no trabaja | Watchdog 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ó corrupta | Validació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.
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.
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.
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.
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.
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.
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.
# 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 esOBSERVE: no hay órdenes, punto); - el RiskGuard rechaza el monto por algún tope duro;
- el motor está en modo
observacion.
Jerarquía: FSM global → modo del motor →
RiskGuard → gate del score. El score es el último de la fila, no el primero.
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.
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 corrupta | no se puede confiar en el criterio |
| Mismatch de conciliación | el ledger no coincide con el exchange |
perdida_semanal_maxima_pct: 5 | alcanzado ⇒ frenar y pensar |
perdida_mensual_maxima_pct: 10 | idem |
/kill CONFIRMAR por Telegram | el freno de mano humano |
⚠ SAFE_MODE — puede auto-recuperarse
perdida_diaria_maxima_pct: 2 | pausa total del día |
max_clock_drift_segundos: 2 | el reloj se corrió: los funding times dependen de él |
max_errores_api_consecutivos: 3 | racha de errores de un venue |
| Depeg de una stablecoin | el 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.
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)
| # | Tope | Default |
|---|---|---|
| 0 | Monto > 0 | sanidad |
| 1 | tope_por_operacion_usd | USD 500 |
| 2 | max_desplegado_pct | 70% del capital |
| 3 | tope_por_plataforma_pct | 25% en un solo exchange |
| 4 | tope_por_activo_pct | 20% |
| 5 | tope_por_stablecoin_pct | 40% en una misma stable |
| 6 | operacion_sin_confirmar_max_usd | USD 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.
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
- Ordena todas las candidatas del ciclo por score, de mayor a menor.
- Para cada una, la pasa por el RiskGuard con la exposición acumulada de las ya aprobadas.
- Si pasa: la aprueba y suma su monto a la exposición por plataforma, por activo y por stablecoin.
- 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:
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)
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.
| Nivel | Condición | Acción | Health 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: 40sugerir_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.
"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.
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".
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.
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.
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_hoursviene 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.
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.
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%.
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.
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 observado | 0,015% / 8 h |
| APR bruto | 16,43% |
| − fees round-trip amortizadas (30 d) | − 3,41% |
| APR neto | 13,02% |
| umbral de entrada | 12% ⇒ pasa |
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%) sostenidopersistencia_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.
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ámetro | Default | Qué controla |
|---|---|---|
ciclo_segundos | 300 | Cada cuánto mira. Más corto = más ruido y más requests. |
pares | 5 pares | El universo. Agregar uno es una línea de YAML. |
umbral_entrada_apr_pct | 12 | El apetito. Bajarlo abre más oportunidades y más riesgo. |
umbral_salida_apr_pct | 4 | Cuándo se considera que el edge murió. |
persistencia_minima_ciclos | 3 | El filtro anti-ruido. En 3 ciclos de 300 s = 15 min. |
profundidad_minima_usd | 50.000 | Descarta pares donde no se puede operar en serio. |
fee_taker_spot_pct | 0,10 | Fee real de tu cuenta. Ajustar por tier de VIP. |
fee_taker_futures_pct | 0,04 | Idem, futuros. |
horizonte_amortizacion_dias | 30 | Cuánto se planea sostener la posición. Afecta el costo anualizado. |
Cómo puntúa M1 sus oportunidades
| Dimensión | Cómo la mide M1 |
|---|---|
retorno_neto | normalizar_retorno(apr_neto, umbral_entrada) — en el umbral vale 100. |
liquidez | normalizar_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.
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 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) |
| LECAP | 31–37% TNA |
| Cauciones (muy corto plazo) | picos cerca del 50% TNA |
| Inflación esperada | ~21% anual |
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.
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: truefail_closed_on_market_stale: true
Un FX de hace 6 minutos frena el ciclo. El dólar se mueve rápido.
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.
efectivo = ((1 + retorno_periodo)^(365/dtm) − 1) × 100
El parámetro más importante de todo M2
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_pct | 3.0 | mínimo para que cuente como señal |
exigir_retorno_usd_positivo | true | no-negociable |
fees_pct / slippage_pct | 0.1 / 0.1 | costos asumidos |
watch_multiplicador_retorno | 1.5 | con 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.
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_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.
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.
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.
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.
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.
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.
apy_neto = apy_base × dias_activos/365 − fee_mint_redeem
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)
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.
depeg_umbral_pct: 1.0, eso bloquea la entrada.precio ≤ 0 no es "depeg total": es un dato roto de la fuente ⇒ ValueError.
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ón | Por qué |
|---|---|---|
| 1 | peg stale | El dato de peg no está fresco. No se puede confirmar. |
| 2 | peg deviation > umbral | Se despegó del dólar más de lo tolerado. |
| 3 | liquidez no medible | No-negociable de SPEC-15: si no se puede medir cómo salir, no se entra. |
| 4 | no allowlisted | El 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 sí 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.
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.
y el funding rate que sostiene el colateral
| Umbral | Valor | Consecuencia |
|---|---|---|
reserve_ratio_minimo_pct | 1.0 | Debajo: el colchón es fino. |
funding_flip_umbral_apy_pct | 0.5 | El funding se está dando vuelta. |
factor_riesgo_critico | 0.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
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.
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.
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.
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%.
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
| Pregunta | Se 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.
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.
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.
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:
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ódigo | Estrategia | Estado |
|---|---|---|
M-REB | Rebalancing arb (YES+NO binario) | PURSUE |
M3 | Negative-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.
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→USD | con rate y updated_at. Alimenta el contexto de M2 y M4. |
| balances multi-moneda | checking + stocks + USD disponible. |
| transactions | con filtros y paginado — insumo del reporte fiscal. |
| fees del tier | las 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.
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.
| Pantalla | La pregunta que responde | De 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.
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
| Motor | Estado | Visto |
|---|---|---|
| funding_binance | OK | hace 2m |
| carry_argentina | OK | hace 7m |
| renta_usd | OK | hace 41m |
| stables_argentina | OK | hace 38s |
| prediccion | APAGADO | nunca |
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.
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)
Estado del FX Risk Engine: WATCH
| Variación | Valor | Qué 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. |
Piso = devaluación esperada. El estado del dólar puede bloquear entradas aunque el retorno sea positivo.
| Ticker | Tipo | DTM | Retorno USD neto | Liquidez | Estado | Motivo |
|---|---|---|---|---|---|---|
| S30Y6 | LECAP | 47 | +6,20% | 78 | WATCH | supera el umbral ×1,5 exigido en WATCH |
| T15E7 | BONCAP | 129 | +4,10% | 64 | bloqueada | retorno 4,10% < umbral WATCH 4,50% |
| S18J6 | LECAP | 12 | +1,80% | 91 | sin señal | retorno < min_net_usd_return_pct (3,0%) |
| T30N6 | BONCAP | 218 | −0,40% | 22 | descartada | liquidity_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.
Renta: todo se mide contra el piso.
Renta USD (M3)
| Ticker | Tipo | APY neto | Spread vs piso | Liquidez | Peg | Ranking | Motivo |
|---|---|---|---|---|---|---|---|
| sUSDe | variable | 10,74% | +6,44% | 88 | 0,04% | 1 | sin bloqueos: peg ok, liquidez medible, allowlisted |
| PT-sUSDE | fijo | 10,85% | +6,55% | 61 | n/a | 2 | sin concepto de peg (PT) · vence 13/08/2026 |
| T-BILL-TOKENIZADO | benchmark | 4,30% | — (es el piso) | 97 | 0,01% | piso | referencia, no candidato |
El factor combina reservas y funding del protocolo; si cruza el umbral crítico, el sistema escala a SAFE_MODE.
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.
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
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.
| # | Estado | Origen | Motivo | Fecha | |
|---|---|---|---|---|---|
| 14 | activa | cockpit | bajo umbral de funding a 12 | 12/07 09:14 | |
| 13 | archivada | cockpit | activo renta_usd | 11/07 18:02 | rollback |
| 12 | archivada | api | ajuste de fees por tier | 08/07 11:47 | rollback |
| Fecha | Campo | De | A | Origen |
|---|---|---|---|---|
| 12/07 09:14 | motores.funding_binance.umbral_entrada_apr_pct | 15 | 12 | cockpit |
| 11/07 18:02 | motores.renta_usd.activo | false | true | cockpit |
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?".
Las tres que miran para atrás.
| Motor | Par | Apertura | Cierre | Métrica | Gan. est. | Estado |
|---|---|---|---|---|---|---|
| funding_binance | SOL/USDT | 09:02 | — | APR 16,4% | — | abierta |
| stables_argentina | USDT@lemon→ripio | 08:41 | 08:47 | 1,12% | $5,60 | cerrada |
| carry_argentina | S30Y6 | 08:15 | — | USD 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.
| Fecha | Severidad | Tipo | Mensaje | |
|---|---|---|---|---|
| 10:22 | crítico | fx_panic | FX Risk Engine: PANIC (cripto 1h 2,80% > umbral panic 2,50%) | resolver |
| 09:58 | warning | data_quality | buenbit descartado: total_ask se desvía 24% de la mediana | resolver |
| 07:10 | info | api_degradada | data912: 2 reintentos, recuperado | resolver |
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.
| Día | Motor | Ventanas |
|---|---|---|
| 12/07 | funding_binance | 3 |
| 12/07 | stables_argentina | 0 |
| 11/07 | stables_argentina | 4 |
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.
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
/estado | Fase, estado FSM, motores activos, métrica destacada de cada uno. |
/estado detalle | + liveness de cada motor, timestamps exactos, anomalías. |
/limites | Los topes vigentes del RiskGuard. |
/oportunidades | Las ventanas abiertas ahora. |
/pausa | ⇒ SAFE_MODE. Recuperable. |
/reanudar | Sale de SAFE_MODE al estado previo. |
/kill | Pide 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.
Dónde vive, y qué hacer cuando se rompe.
El despliegue
| Host | EC2 dedicada, Ubuntu 24.04, sa-east-1. Aislada de la infra de producción de la empresa. |
| Servicio | systemd. Logs a journald en JSON estructurado. |
| Red | Acceso solo por WireGuard. La API bindea a 10.0.0.1 — nunca a 0.0.0.0. |
| Base | PostgreSQL 16. Migraciones con alembic. |
| Secretos | /etc/faro/faro.env con chmod 600. Nunca en el repo, nunca en config.yaml. |
| Horas | Todo 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)
- Abrir Inicio: ¿4/4 motores OK?
- Mirar Alertas: ¿hay algo crítico sin resolver?
- Mirar Métricas: ¿la cuenta de ventanas por día tiene sentido?
Los playbooks de incidente
| Síntoma | Qué significa · qué hacer |
|---|---|
| Motor SILENCIOSO | El proceso vive pero el scanner no produce. Revisar journald del motor; probable excepción silenciada o API externa colgada. |
| Racha de errores de API | Ya está en SAFE_MODE. Verificar si es el venue o la red. Al sanar, /reanudar. |
| Clock drift | SAFE_MODE global. Revisar NTP. Los funding times dependen del reloj. |
| Depeg | SAFE_MODE. No es un bug: es el sistema haciendo exactamente lo que tiene que hacer. |
| Mismatch de conciliación | KILLED. El ledger no coincide con el exchange. No reanudar hasta entender por qué. |
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.
| Oportunidad | Por 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 DEXs | Es una guerra de microsegundos contra infraestructura de millones de dólares. No hay partido. |
| Surebets deportivas | Funciona… hasta que las casas te cierran la cuenta. Semanas, no meses. |
| Airdrop farming | La rentabilidad por hora se desplomó con los filtros anti-sybil. Oportunismo puntual, no estrategia. |
| Arbitraje triangular intra-exchange | Contra HFT no hay partido. Queda como módulo de aprendizaje, nada más. |
| Grid trading | Neutral. No hay ventaja competitiva propia que lo justifique. |
| Cross-platform Polymarket vs Kalshi | Bloqueado 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.
Cómo agregar un motor nuevo.
La prueba final de que la arquitectura es buena: qué tan aburrido es extenderla.
El checklist
| # | Paso | Dónde |
|---|---|---|
| 1 | Escribir la SPEC: fuentes, matemática, umbrales, criterios de aceptación. | specs/SPEC-NN.md |
| 2 | Agregar la sección al esquema de config y su modelo pydantic. | config/config.example.yamlcore/config.py |
| 3 | Definir el Protocol del feed (capa 1) y su provider real. | scanners/nuevo.py |
| 4 | Escribir las funciones puras (capa 2). Tests primero. | scanners/nuevo.pytests/ |
| 5 | Armar el Scanner (capa 3): tick, persistencia idempotente, apertura/cierre de oportunidades. | scanners/nuevo.py |
| 6 | Mapear las métricas crudas a subscores. Lo que no se mide, va al neutro. | usar core/scoring.py |
| 7 | Registrar el job en el scheduler, con su gate de estado. | core/runtime.py |
| 8 | Sumar la pantalla al cockpit si el motor lo amerita. | cockpit/src/pages/ |
| 9 | Anotar 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,payloadJSON).
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.
Glosario de bolsillo.
| APR / APY | Rendimiento anualizado. APR es simple; APY capitaliza. |
| Delta-neutral | Posición cuyo resultado no depende del movimiento del precio. |
| Funding rate | Pago periódico entre longs y shorts en un futuro perpetuo, para que su precio no se despegue del spot. |
| Cash-and-carry | Comprar spot + vender perpetuo. Cobra el funding sin exposición al precio. |
| Carry trade | Endeudarse/quedarse en la moneda de tasa baja, invertir en la de tasa alta. |
| LECAP / BONCAP | Letras y bonos capitalizables del Tesoro argentino, en pesos. |
| DTM | Days to maturity: días hasta el vencimiento. |
| TIR | Tasa interna de retorno. |
| MEP / CCL | Dólares financieros: vía bonos en el mercado local (MEP) o para girar afuera (CCL). |
| sUSDe | Dólar sintético de Ethena. Rinde el funding cripto, tokenizado. |
| PT (Pendle) | Principal Token: rendimiento fijo a plazo. No tiene peg. |
| Depeg | Cuando una stablecoin deja de valer 1 dólar. |
| Spread ejecutable | El cruce real (mejor ask comprable vs mejor bid vendible), neto de fees. No la pizarra. |
| Slippage | La diferencia entre el precio que veías y el que conseguiste. |
| Drawdown | Caída desde el pico anterior. |
| Time underwater | Cuánto tiempo se está por debajo del pico anterior. |
| ADR | Architecture Decision Record: una decisión de diseño, con su contexto y su porqué. |
| FSM | Máquina de estados finitos. Acá: los 6 estados globales de FARO. |
| CLOB | Central Limit Order Book: el libro de órdenes de Polymarket. |
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.