Dashboard
Estado en tiempo real del sistema, posiciones y actividad—
—
No signals yet
Posiciones Abiertas
| PositionID | Date / Time | Symbol | Side | Entry Price | Current Price | Qty | Unrealized PnL | Stop Loss | Take Profit | Origen | Action |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Sin posiciones abiertas | |||||||||||
Actividad
Cargando...
Bots
Gestiona y monitoriza todos tus bots de tradingControl de Bots
Conectando...
| Bot | Strategy | Market | Status | Performance (7d) | Total P&L | Trades | Win Rate | Last Activity | Actions |
|---|---|---|---|---|---|---|---|---|---|
| Cargando... | |||||||||
Backtest
Resultados y análisis histórico por estrategiaEstrategias
EMA Crossover (VWAP+RSI+ADX)
Backtest agrupado por estrategia. Los paneles siguientes pertenecen únicamente a EMA Crossover (VWAP+RSI+ADX).
Mejores Parámetros por Activo
Cargando...
Historial de Iteraciones
Cargando...
Conclusiones Clave
Configuración del Backtest
Curva de Capital — Retorno (%) por Activo
Trend Pullback (RSI+ATR)
EMA 50/200 + RSI + salidas por ATR. Réplica fiel de pine/ema_rsi_atr_frequent.v1.pine, backtest sobre datos locales (Binance 2024–2026), comparado en 3 timeframes.
Resultados por Timeframe y Activo
| TF | Activo | Trades | Win Rate | PF | Max DD | PnL (USDT) |
|---|---|---|---|---|---|---|
| 15m | BTC | 1121 | 38.5% | 0.75 | 1.5% | −148.63 |
| 15m | ETH | 1054 | 40.9% | 0.87 | 1.2% | −110.85 |
| 15m | SOL | 1155 | 40.6% | 0.89 | 1.4% | −133.78 |
| 15m | BNB | 1146 | 39.6% | 0.81 | 1.4% | −135.91 |
| 15m | TOTAL | 4476 | — | — | — | −529.17 |
| 1h | BTC | 281 | 43.8% | 0.95 | 0.3% | −23.16 |
| 1h | ETH | 283 | 40.3% | 0.99 | 0.4% | −16.71 |
| 1h | SOL | 299 | 39.1% | 0.86 | 1.0% | −66.92 |
| 1h | BNB | 302 | 38.7% | 0.85 | 0.7% | −51.77 |
| 1h | TOTAL | 1165 | — | — | — | −158.56 |
| 4h | BTC | 84 | 38.1% | 0.85 | 0.4% | −21.39 |
| 4h | ETH | 87 | 37.9% | 0.95 | 0.5% | −12.30 |
| 4h | SOL | 83 | 38.6% | 0.81 | 0.7% | −46.49 |
| 4h | BNB | 64 | 45.3% | 1.04 | 0.4% | +0.44 |
| 4h | TOTAL | 318 | — | — | — | −79.74 |
| 1d | BTC | 13 | 30.8% | 0.48 | 0.4% | −31.25 |
| 1d | ETH | 12 | 25.0% | 0.43 | 0.5% | −41.45 |
| 1d | SOL | 12 | 41.7% | 0.83 | 0.7% | −13.39 |
| 1d | BNB | 9 | 55.6% | 1.99 | 0.2% | +25.03 |
| 1d | TOTAL | 46 | — | — | — | −61.06 |
Curva de Capital — Retorno (%) por Activo
Supertrend ATR
Backtest sobre datos locales (Binance 2024–2026), motor event-driven, comparado en 3 timeframes.
Resultados por Timeframe y Activo
| TF | Activo | Trades | Win Rate | PF | Max DD | PnL (USDT) |
|---|---|---|---|---|---|---|
| 15m | BTC | 1087 | 42.7% | 0.86 | 1.7% | −159.62 |
| 15m | ETH | 1412 | 46.0% | 0.99 | 1.3% | −80.36 |
| 15m | SOL | 1707 | 45.5% | 0.97 | 1.4% | −120.62 |
| 15m | BNB | 1242 | 44.8% | 0.94 | 1.1% | −110.20 |
| 15m | TOTAL | 5448 | — | — | — | −470.79 |
| 1h | BTC | 466 | 43.1% | 0.88 | 0.8% | −62.93 |
| 1h | ETH | 503 | 45.3% | 0.96 | 0.5% | −38.27 |
| 1h | SOL | 518 | 43.6% | 0.90 | 0.7% | −63.05 |
| 1h | BNB | 493 | 46.5% | 1.01 | 0.5% | −22.64 |
| 1h | TOTAL | 1980 | — | — | — | −186.90 |
| 4h | BTC | 125 | 50.4% | 1.18 | 0.1% | +7.60 |
| 4h | ETH | 125 | 44.8% | 0.94 | 0.2% | −11.30 |
| 4h | SOL | 123 | 36.6% | 0.67 | 0.4% | −38.34 |
| 4h | BNB | 121 | 40.5% | 0.79 | 0.3% | −24.98 |
| 4h | TOTAL | 494 | — | — | — | −67.02 |
Curva de Capital — Retorno (%) por Activo
RSI Momentum
Backtest sobre datos locales (Binance 2024–2026), motor event-driven, comparado en 3 timeframes.
Resultados por Timeframe y Activo
| TF | Activo | Trades | Win Rate | PF | Max DD | PnL (USDT) |
|---|---|---|---|---|---|---|
| 15m | BTC | 1386 | 45.2% | 0.96 | 1.3% | −111.15 |
| 15m | ETH | 2067 | 45.9% | 0.98 | 1.6% | −124.19 |
| 15m | SOL | 2786 | 45.9% | 0.99 | 1.9% | −164.63 |
| 15m | BNB | 1694 | 44.1% | 0.91 | 2.0% | −182.94 |
| 15m | TOTAL | 7933 | — | — | — | −582.92 |
| 1h | BTC | 737 | 47.1% | 1.03 | 0.4% | −21.30 |
| 1h | ETH | 973 | 43.2% | 0.88 | 1.5% | −130.14 |
| 1h | SOL | 1198 | 45.7% | 0.97 | 1.0% | −80.28 |
| 1h | BNB | 853 | 46.4% | 1.00 | 0.7% | −39.70 |
| 1h | TOTAL | 3761 | — | — | — | −271.42 |
| 4h | BTC | 290 | 41.0% | 0.81 | 0.6% | −55.58 |
| 4h | ETH | 334 | 46.1% | 0.99 | 0.4% | −18.40 |
| 4h | SOL | 372 | 42.2% | 0.85 | 0.6% | −59.55 |
| 4h | BNB | 323 | 48.3% | 1.08 | 0.4% | +1.26 |
| 4h | TOTAL | 1319 | — | — | — | −132.27 |
Curva de Capital — Retorno (%) por Activo
Donchian Breakout
Canal de ruptura de 20 períodos. Backtest sobre datos locales (Binance 2024–2026), motor event-driven, comparado en 3 timeframes.
Resultados por Timeframe y Activo
| TF | Activo | Trades | Win Rate | PF | Max DD | PnL (USDT) |
|---|---|---|---|---|---|---|
| 15m | BTC | 1814 | 43.2% | 0.88 | 2.5% | −242.38 |
| 15m | ETH | 2734 | 45.8% | 0.98 | 2.0% | −172.47 |
| 15m | SOL | 3557 | 43.5% | 0.89 | 4.5% | −435.02 |
| 15m | BNB | 2166 | 45.3% | 0.96 | 1.8% | −165.91 |
| 15m | TOTAL | 10271 | — | — | — | −1015.78 |
| 4h | BTC | 342 | 48.0% | 1.07 | 0.3% | −1.83 |
| 4h | ETH | 353 | 42.8% | 0.87 | 0.5% | −51.12 |
| 4h | SOL | 382 | 35.9% | 0.65 | 1.3% | −125.96 |
| 4h | BNB | 327 | 45.0% | 0.95 | 0.5% | −28.19 |
| 4h | TOTAL | 1404 | — | — | — | −207.10 |
| 1d | BTC | 68 | 41.2% | 0.81 | 0.2% | −12.80 |
| 1d | ETH | 59 | 27.1% | 0.43 | 0.3% | −33.50 |
| 1d | SOL | 64 | 23.4% | 0.35 | 0.4% | −42.62 |
| 1d | BNB | 55 | 30.9% | 0.52 | 0.3% | −25.56 |
| 1d | TOTAL | 246 | — | — | — | −114.48 |
Curva de Capital — Retorno (%) por Activo
Bollinger Bands Mean Reversion
Reversión a la media con bandas de Bollinger 20/2 y salidas por ATR. Primera estrategia de reversión del catálogo. Backtest sobre datos locales (Binance 2024–2026), 3 timeframes.
Resultados por Timeframe y Activo
| TF | Activo | Trades | Win Rate | PF | Max DD | PnL (USDT) |
|---|---|---|---|---|---|---|
| 15m | BTC | 5626 | 42.6% | 0.83 | 5.7% | −566.13 |
| 15m | ETH | 5667 | 41.9% | 0.85 | 6.4% | −637.76 |
| 15m | SOL | 5479 | 42.7% | 0.92 | 5.2% | −515.52 |
| 15m | BNB | 5573 | 42.8% | 0.88 | 5.1% | −505.71 |
| 15m | TOTAL | 22345 | — | — | — | −2225.13 |
| 4h | BTC | 379 | 41.2% | 0.91 | 0.7% | −62.47 |
| 4h | ETH | 368 | 40.8% | 0.99 | 0.7% | −27.30 |
| 4h | SOL | 337 | 40.4% | 0.89 | 1.8% | −106.32 |
| 4h | BNB | 377 | 42.2% | 0.98 | 0.9% | −30.64 |
| 4h | TOTAL | 1461 | — | — | — | −226.74 |
| 1d | BTC | 58 | 43.1% | 1.03 | 0.4% | +3.15 |
| 1d | ETH | 61 | 36.1% | 0.80 | 1.2% | −64.06 |
| 1d | SOL | 58 | 39.7% | 0.93 | 1.0% | −26.93 |
| 1d | BNB | 58 | 39.7% | 1.04 | 0.6% | +5.89 |
| 1d | TOTAL | 235 | — | — | — | −81.94 |
Curva de Capital — Retorno (%) por Activo
Estrategia
Catalogo publico como fuente unica de documentacionEstrategias
Cargando catalogo publico de estrategias...
Documentacion
La documentacion larga de cada estrategia vive en las paginas publicas del catalogo. Esta consola mantiene solo el acceso rapido para evitar duplicar contenido.
Arquitectura
Diseño técnico, componentes y flujo del sistema · Multi-tenantFlujo General
Componentes del Sistema
Orquesta el sistema multi-tenant. Los módulo-globals de la versión anterior
fueron reemplazados por el UserEngineRegistry. Endpoints principales:
POST /webhook/{token} (routing per-user), GET /health,
POST /api/keys (credenciales), GET /api/me,
POST /api/me/email, /api/config, /api/positions/history.
El legacy POST /webhook devuelve HTTP 410 Gone — los operadores deben
apuntar sus alertas de TradingView a /webhook/{token}.
Implementa _place_order_with_retry (2 reintentos con backoff, salvo
errores permanentes 10002/10003/10007). Todos los endpoints de datos están
protegidos por la dependencia get_current_user.
Carga con pydantic-settings las variables de entorno de nivel servidor
(ya no contiene API keys ni config de trading per-user — esas viven en users.db).
Variables actuales: SECRET_KEY (sesiones), MASTER_KEY (Fernet),
ADMIN_USERNAME/ADMIN_PASSWORD (seed), HTTPS_ONLY,
bitunix_base_url y defaults de riesgo globales.
Convención de proyecto: todo el código lee config via get_settings(),
nunca os.environ directamente. Fail-closed: arranca con error si
SECRET_KEY o MASTER_KEY están ausentes.
Cliente async para la API REST de Bitunix Futures. Recibe api_key,
api_secret, base_url y dry_run
por parámetro via engine.exchange_client() — nunca
los lee de un singleton global (refactor "keystone" multi-tenant).
Firma con SHA256 de doble pasada
(digest = SHA256(nonce+ts+key+qs+body), sign = SHA256(digest+secret)).
Gotcha crítico: en modo HEDGE, cualquier query string en GET /position
causa error 10007; get_all_positions() siempre se llama sin parámetros
y se filtra en Python. Cachea el positionId tras cada apertura.
Polling cada 10s al endpoint público de tickers de Bitunix
(GET /api/v1/futures/market/tickers, sin auth) para los símbolos
configurados (BTC, ETH, SOL, BNB). Es el único global que sobrevive al
refactor multi-tenant — los precios son públicos, no per-user.
Usa el markPrice como referencia de SL/TP y gate de slippage:
sin precio fresco (más viejo que price_max_age_sec),
el bot NO opera.
Define WebhookPayload — el modelo Pydantic que valida el JSON de TradingView.
Campos: symbol, side, action, price,
token (ahora opcional e ignorado — el token de la URL
es autoritativo). Propiedades derivadas: is_open, exchange_side,
position_side, price_float.
Calcula el tamaño de posición según riesgo por operación (% del capital del usuario)
y distancia al SL. Cada UserEngine tiene su propia instancia de
RiskManager inicializada con el capital y porcentajes del usuario.
Circuit breaker diario: si las pérdidas acumuladas superan el límite per-user,
bloquea nuevas operaciones hasta el día siguiente. El PnL diario se restaura
desde data/{user_id}/trades.csv al arrancar.
Mantiene en memoria las posiciones abiertas por símbolo. Cada UserEngine
tiene su propio PositionTracker con estado en
data/{user_id}/positions.json. Cada Position incluye
exchange_position_id (requerido por Bitunix HEDGE para cerrar) e
initiator (bot / manual / exchange).
reconcile_from_exchange() sincroniza con el exchange al arrancar.
Escribe cada apertura y cierre en data/{user_id}/trades.csv. Campos:
timestamp, symbol, side, action,
entry_price, exit_price, quantity, pnl,
commission, sl_price, tp_price,
initiator, mode (live/dry), comment.
Incluye migración automática del CSV cuando se añaden campos nuevos.
Además expone setup_logging(), que configura el logging del sistema con
dos handlers: consola (stdout) en formato JSON por línea
(vía JsonFormatter) y archivo logs/bot.log en
texto pipe-delimited. El JSON existe solo en la consola; el archivo nunca es JSON.
Frontend estático (HTML + CSS + JS vanilla) servido en /bot-manager-console/.
Ahora requiere login — un apiFetch wrapper redirige a
/auth/login ante cualquier 401. Menú de cuenta en la esquina superior
derecha con panel de ajustes (username, email, webhook URL, claves Bitunix).
Polling a /health cada 5s para precios, posiciones y fallos recientes.
Cuatro módulos: routes.py — endpoints de login/logout
(GET /auth/login, POST /auth/login,
POST /auth/logout); security.py — hashing con
hashlib.scrypt (stdlib, sin dependencias extra; sal aleatoria de 16 bytes,
parámetros OWASP mínimos, comparación con hmac.compare_digest);
session.py — configura SessionMiddleware de Starlette
con SECRET_KEY y la flag https_only;
deps.py — dependencia FastAPI get_current_user que gatea
todos los endpoints de datos; un request sin sesión válida → 401.
No hay registro abierto: el primer usuario admin se siembra desde
.env en el primer arranque.
Cuatro módulos sobre SQLite: schema.py — DDL de la tabla
users (id, username, password_hash+salt, webhook_token,
enc_api_key/secret, email, capital/risk/sl/tp/leverage/slippage/dry_run,
is_admin) + migración idempotente de columnas nuevas (e.g. email
via PRAGMA table_info); users.py — queries CRUD;
seed.py — siembra idempotente del admin desde .env y
migra la cuenta original (user_id=1) copiando la posición viva si
existía; connection.py — path canónico de users.db y
helper get_conn().
Cifrado simétrico Fernet (biblioteca cryptography) de las API keys
de cada usuario. MASTER_KEY se lee via get_settings()
— nunca os.environ directo. Fail-closed: lanza RuntimeError
si la clave está ausente o vacía. decrypt() devuelve None
ante cualquier InvalidToken — nunca propaga internals. Las credenciales
descifradas solo existen en memoria durante la vida del UserEngine;
ningún endpoint las devuelve (write-only). POST /api/keys hace una
verificación de conectividad en vivo con Bitunix antes de habilitar trading real.
Dos módulos: user_engine.py — UserEngine agrupa el
RiskManager, PositionTracker, estado de bots, credenciales
descifradas, config per-user, locks por símbolo y paths de datos del usuario;
expone engine.exchange_client() que construye un
BitunixClient con las creds del usuario.
registry.py — UserEngineRegistry mantiene el índice
token → engine (O(1) en el hot-path del webhook); carga engines al
arrancar con stagger de 1s entre usuarios para no saturar Bitunix; recarga
atómica bajo lock (swap sin corrupción de requests en vuelo); fail-closed:
NULL o creds no descifrarbles → sin engine.
Bot de Telegram para notificaciones en tiempo real: mensaje al abrir/cerrar una posición
(símbolo, lado, precio, qty, PnL) y alerta cuando un webhook llega pero la orden falla.
Se integrará como llamada asíncrona al final de _execute_open y
_execute_close, sin bloquear el flujo principal.
Ciclo de Vida de una Señal
-
1Detección en TradingView
El indicador Pine Script evalúa en cada vela de 15m: cruce EMA9/EMA21, precio respecto a VWAP y EMA200, RSI en zona neutral (40-60), ADX > umbral y volumen sobre la media. Si todas las condiciones se cumplen, dispara la alerta con acción de webhook.
-
2Recepción del webhook
TradingView envía
POST /webhook/{token}con JSON:{"symbol":"BTCUSDT","side":"buy","action":"open","price":"60000"}. El campotokenen el body es opcional e ignorado — el token en la URL es el autoritativo. La URL debe ser pública; el servidor expone el endpoint en internet. -
3Resolución del usuario y autenticación
FastAPI busca el token en el
UserEngineRegistry. Token completamente desconocido →401. Token válido pero sin engine activo (sin claves configuradas) →403. Ambas respuestas tienen la misma forma para no filtrar si el token existe. A continuación Pydantic valida el schema del payload; si falla →422. -
4Guardia del bot (per-user)
Se comprueba
engine.bot_active[symbol]en elUserEnginedel usuario. Si el bot está pausado para ese usuario, la señal se descarta conrejected: bot is paused. No se toca el exchange. -
5Gestión de riesgo (per-user)
El
RiskManagerdel usuario comprueba su circuit breaker diario. Si el trading está permitido, calcula SL (entry × (1 ± sl_pct%)), TP (entry × (1 ± tp_pct%)) y el tamaño de posición con el capital del usuario:qty = (capital × risk_pct) / (entry − SL). -
6Ejecución con retry
Se envía la orden MARKET a Bitunix via
_place_order_with_retryusando las credenciales descifradas del usuario (engine.exchange_client()). En caso de fallo transitorio, reintenta hasta 2 veces con backoff lineal (2s, luego 4s). Los errores permanentes (10002 parámetro, 10003 saldo, 10007 firma) no se reintentan. Un fallo definitivo se registra enengine.recent_failuresy aparece en el banner rojo del dashboard. -
7Captura del positionId
Tras confirmar la apertura, el sistema espera 0.8s y llama a
get_all_positions()(sin query string — evita error 10007 de Bitunix HEDGE). ElpositionIdse cachea en el objetoPositiony se persiste endata/{user_id}/positions.json. Los cierres posteriores lo usan directamente sin ningún GET adicional. -
8Registro y persistencia
La posición se guarda en el
PositionTrackerdel usuario (memoria +data/{user_id}/positions.json). El trade se loggea endata/{user_id}/trades.csvconinitiator="bot",mode="live"|"dry"y la comisión estimada al 0.05% (round-trip). El dashboard refleja el cambio en el siguiente ciclo de polling (máx. 5s). -
9Notificación Telegram (próximo)
Llamada asíncrona al bot de Telegram con los detalles de la operación (símbolo, lado, precio, qty). En el caso de cierre, incluye el PnL realizado y la comisión. No bloquea el flujo principal — se lanza como tarea en background.
Estado y Persistencia
users: id, username,
password_hash+salt (scrypt), webhook_token, enc_api_key/enc_api_secret (Fernet),
email (nullable), capital_usdt, risk/sl/tp/leverage/slippage/dry_run per-user, is_admin.
Schema idempotente con migración automática de columnas nuevas al arrancar.
La config de trading ya no se escribe en .env en runtime.
type
(bitunix/kucoin),
enc_api_key/enc_api_secret/enc_passphrase (Fernet),
base_url, enabled, is_active. Un índice único parcial
garantiza un solo exchange activo por usuario; el engine carga las credenciales del activo
al arrancar.
{"BTCUSDT": true, "ETHUSDT": false, ...}
symbol, side,
entry_price, quantity, sl_price, tp_price,
opened_at, order_id, initiator
(bot/manual/exchange) y
exchange_position_id. Se reconcilia con el exchange real al arrancar.
pnl, commission (estimada 0.05% round-trip),
initiator (bot/manual/exchange) y mode (live/dry).
Fuente de verdad para stats de bots, PnL diario y circuit breaker.
Incluye migración automática al añadir campos nuevos.
|:
TS | LEVEL | module | msg (una línea por evento, no JSON).
El mismo evento sí se emite como JSON, pero solo en la consola/stdout;
el archivo siempre es pipe-delimited.
El dashboard lee las últimas 50 entradas y las filtra por símbolo en el cliente.
Nivel configurable en caliente desde Configuración (DEBUG / INFO).
SECRET_KEY (firma de sesiones),
MASTER_KEY (Fernet, para cifrar API keys en users.db),
ADMIN_USERNAME/ADMIN_PASSWORD (seed del primer arranque),
HTTPS_ONLY. La config de trading per-user y las API keys
se guardan en users.db — el runtime ya no escribe .env.
Nunca se commitea. Se incluye .env.example como referencia.
Stack Tecnológico
:root como variables CSS. El tema oscuro se aplica globalmente sin preprocesadores.fetch() que redirige a /auth/login ante cualquier respuesta 401. El estado se sincroniza cada 5 segundos con /health y /api/logs. No hay WebSocket — polling simple, predecible y sin estado de conexión que gestionar.polyline. Sin canvas, sin librerías de gráficos — solo vectores calculados en tiempo de render.asyncio nativo para el cliente HTTP concurrente y el feed de precios.StaticFiles. La validación de payloads es automática via Pydantic.uvloop. Ejecuta el event loop async de Python con rendimiento cercano a Go/Node para I/O intensivo.WebhookPayload (señales) y Settings (configuración de servidor desde .env)..env. Acceso via get_settings() — convención de proyecto para no leer os.environ directamente.async with y timeouts configurables, lo que evita bloquear el event loop.typing.Protocol) con el contrato común de un exchange — place_order, set_leverage, get_all_positions, etc. create_exchange_client() instancia BitunixClient o KuCoinClient según el exchange activo del usuario, sin que el engine sepa cuál es.MASTER_KEY en .env; las claves descifradas solo existen en memoria durante la ejecución del engine del usuario.itsdangerous bajo el capó). Firmada con SECRET_KEY; la flag HTTPS_ONLY activa Secure en producción.sqlite3 gestiona users.db sin dependencias extra. hashlib.scrypt (stdlib) hashea passwords con sal aleatoria de 16 bytes y parámetros OWASP mínimos; comparación en tiempo constante con hmac.compare_digest..env en las variables de entorno del proceso antes de que pydantic-settings las lea. Estándar de facto para configuración local en Python.DataFrame y aplica la lógica de señales vectorizada.equity_curve.html) que se embebe en la tab de Backtesting via <iframe>. Exporta HTML standalone sin dependencias externas.Seguridad
digest = SHA256(nonce+ts+key+qs+body) y sign = SHA256(digest+secret) — dos pasadas de SHA256, no HMAC. La firma viaja en el header; Bitunix rechaza cualquier petición que no la lleve.users.db. Solo se descifran en memoria para la llamada al exchange. Ningún endpoint las devuelve — POST /api/keys es write-only.{salt_hex}${hash_hex} con hashlib.scrypt (stdlib). Sin dependencias de terceros para hashing. Comparación en tiempo constante para prevenir timing attacks.get_current_userSECRET_KEY via SessionMiddleware. La dependencia FastAPI get_current_user gatea todos los endpoints de datos — un request sin sesión válida recibe 401 y el frontend redirige a /auth/login.secrets.token_urlsafe(32)) que forma su URL personal: /webhook/{token}. El token en la URL es el único mecanismo de autenticación del webhook — el campo token en el body JSON es ignorado.Infraestructura y Despliegue
El contenedor es la única pieza que ejecuta lógica. Apache (gestionado por Plesk, nunca a mano) es el único proceso expuesto a internet; el puerto 8000 es local. Dentro del contenedor, FastAPI sirve dos sitios estáticos y la API, y habla con las bases de datos montadas como volúmenes y con los exchanges externos.
Base de datos relacional (WAL, sin ORM). Tabla users (cuentas, config de trading,
credenciales legacy) y tabla exchanges (una fila por exchange por usuario, con
credenciales cifradas y un único exchange activo). Montada como volumen para sobrevivir a redeploys.
Estado de runtime por usuario en archivos: positions.json (posiciones abiertas,
escritura atómica), trades.csv (historial, fuente de verdad del PnL) y
bot_state.json (bots activos/pausados). Segundo volumen persistente junto a users.db.
/ → web-public/ (sitio público + catálogo SEO).
/bot-manager-console → web-private/ (esta consola).
/backtest-results → gráficos de backtest. /webhook/{token} → señales.
El mount de / va último porque captura todo.
API REST externa (fapi.bitunix.com) para órdenes, posiciones y leverage. También
alimenta el feed de precios compartido (polling de tickers cada 10s). Exchange por defecto en producción.
Segundo exchange vía la abstracción ExchangePort + factory. Implementado
de punta a punta pero forzado a dry-run hasta aprobación explícita; todavía no
envía órdenes reales.
Las alertas de Pine Script disparan HTTP POST /webhook/{token} al cumplirse las
condiciones de la estrategia: TradingView es el motor de detección de señales, el
bot no calcula indicadores en vivo. El endpoint legacy /webhook sin token devuelve 410 Gone.
Velas históricas descargadas de Binance vía download_data.py, usadas
exclusivamente para backtesting. El bot en vivo opera sobre Bitunix/KuCoin, no sobre estos datos.
Pipeline GGA y ciclo de I+D (tools/) que investigan, backtestean y publican
estrategias. Se disparan bajo demanda (admin) e invocan el CLI de Claude; el SSG regenera el
catálogo en web-public/estrategias/. No intervienen en el camino de trading en vivo.
Entorno de Desarrollo con IA
El modelo de lenguaje que procesa tus mensajes, razona sobre el código, llama herramientas y genera respuestas. Tiene un contexto de conversación activo, pero sin memoria entre sesiones salvo que use Engram o el sistema de archivos de memoria.
La capa que conecta Claude con tu entorno local. Provee las herramientas nativas
(Read, Write, Edit, Bash, Grep, Glob),
gestiona permisos, muestra aprobaciones y carga el contexto del proyecto
(CLAUDE.md, archivos abiertos, selección del IDE).
No es un sistema separado — es un conjunto de reglas en CLAUDE.md que define
cómo Claude se comunica: tono apasionado y directo, Rioplatense con voseo, CAPS para énfasis,
senior architect de 15+ años. Afecta solo el texto del chat, nunca el código generado
ni los artefactos técnicos.
No confundir con GGA (Gentleman Guardian Angel): GGA es el orquestador
del flujo multi-agente — ver más abajo. Mismo apodo, capas distintas.
Proceso externo que expone herramientas de memoria via MCP (Model Context Protocol).
Claude lo llama con mem_save (guardar decisión/bug/convención),
mem_search (búsqueda semántica) y mem_context (sesión reciente).
Persiste en SQLite/vectores — sobrevive a reinicios, compresiones de contexto y nuevas sesiones.
Tipos de observación: bugfix · decision · architecture · discovery · pattern · preference.
Sistema de archivos .md en
~/.claude/projects/…/memory/ que Claude escribe directamente con la herramienta
Write. Complementa a Engram para notas rápidas sobre el usuario, feedback
y preferencias. El índice MEMORY.md se carga en cada sesión automáticamente.
Archivos SKILL.md con instrucciones detalladas para tareas específicas
(SDD phases, code-review, etc.). Claude los lee antes de ejecutar esa tarea.
Se invocan con el Skill tool al escribir /sdd-apply,
/code-review, etc. en el chat.
Instancias independientes de Claude spawneadas con el Agent tool.
Empiezan sin memoria del contexto actual — reciben todo via briefing en el prompt.
Se usan para tareas paralelas, exploración de código sin contaminar el contexto
principal, o revisiones adversariales. Tipos: Explore, sdd-apply,
sdd-verify, general-purpose, Plan, entre otros.
Flujo estructurado para cambios no triviales: explore → propose → spec → design → tasks → apply → verify → archive.
Cada fase es un sub-agente especializado. Los artefactos (specs, diseños, tareas)
se persisten en Engram o en archivos openspec/. Evita que Claude implemente
sin haber entendido primero el problema y acordado el enfoque.
Modelo Multi-Agente — Gentleman Guardian Angel (GGA)
Más allá de la sesión mono-agente de arriba, el proyecto define un equipo de IA con roles y
autoridad acotada, documentado en AGENTS.md (fuente de autoridad) y
DELEGATION.md (qué archivo pertenece a quién). La regla de oro:
ningún agente activa trading real, sube leverage, ni toca credenciales sin aprobación
del Product Owner. Claude es el revisor vinculante en todo lo crítico.
El trabajo sustancial arranca un paso antes, en la etapa de planificación SDD
(ver abajo): define el qué y el porqué antes de que la cadena de agentes ejecute el cómo.
Etapa SDD — Spec-Driven Development
Capa de planificaciónAntes de que un solo agente toque código, SDD captura el cambio como artefactos persistidos (en Engram / OpenSpec, con trazabilidad): proposal → spec → design → tasks. Su salida —el checklist de tareas— es exactamente lo que alimenta al pipeline GGA: una tarea de SDD = una fase de GGA. SDD decide QUÉ construir y POR QUÉ; la cadena de agentes de abajo ejecuta el CÓMO; y al cerrar, verify valida el resultado contra el spec y archive sincroniza las specs y cierra el ciclo.
Coordina a los agentes, rutea tareas, carga skills del proyecto y mantiene la ejecución
del flujo. No override las decisiones de arquitectura de Claude.
Se configura en .gga (provider, patrones de archivos incluidos/excluidos,
RULES_FILE=AGENTS.md y los comandos de cada adaptador de agente).
Orquestador local que ejecuta la cadena de AGENTS.md como una corrida
determinista y auditada: Architect (Claude) → Implementer (Codex) → Reviewer (Copilot)
→ Approver (Claude). No hay canal in-process entre agentes — cada uno es un subproceso de su
CLI headless. El implementer trabaja en un git worktree aislado
(gga/run-<ts>); dev y master nunca se tocan. Todos los artefactos
quedan bajo .gga-runs/<ts>/. Nunca mergea, pushea, deploya,
corre la app ni contacta a Bitunix. Salida final: APPROVED /
CHANGES_REQUESTED / PIPELINE_ABORTED (exit 0/1/2).
Define arquitectura, estándares técnicos, file contracts y criterios de aceptación antes de implementar. Es el revisor vinculante para seguridad, cambios estructurales y cualquier área crítica de trading: webhook, Risk Manager, Bitunix, leverage, SL/TP, persistencia y despliegue. En el pipeline abre la cadena como Architect y la cierra como Approver.
Implementa features, refactoriza, escribe tests y arregla bugs. No puede
cambiar arquitectura, reglas de risk management ni comportamiento de trading en producción
sin revisión de Claude. En el pipeline corre como codex exec --sandbox workspace-write,
con la escritura confinada al worktree de la corrida. Firma sus commits como codex.
Genera implementaciones alternativas, sugiere optimizaciones y revisa cambios. Sus
recomendaciones son advisory: nunca es el único revisor que despeja un
cambio de riesgo/seguridad — la revisión de Claude lo override. Usa el
Copilot CLI agéntico (gh copilot), no la extensión
legacy de suggest/explain. En el rol de reviewer se fija a herramientas read-only
(--available-tools view,grep,glob) para que no pueda editar el árbol.
Define objetivos de negocio y es el único que aprueba la activación de trading real, integraciones de exchange y releases a producción. Ningún agente de IA puede activar dinero real sin su aprobación explícita — es el gate final por encima de todo el flujo.
Desde la raíz del repo, con los CLIs instalados (claude, codex,
gh copilot):
python tools/gga_pipeline.py "<tarea>" --run-tests --allow-dirty
"<tarea>" — descripción de la tarea entre comillas: lo que Claude diseña
(file contract + criterios) y Codex implementa en el worktree aislado.
--run-tests — tras implementar, corre python -m compileall . +
pytest (solo dry-run, nunca coloca órdenes); si fallan, el veredicto es
CHANGES_REQUESTED.
--allow-dirty — arranca aunque el árbol de trabajo tenga cambios sin commitear
(por defecto el pipeline aborta si el árbol está sucio).
La corrida queda auditada en .gga-runs/<ts>/ (diseño, patch, hallazgos,
veredicto y prompts). Termina con APPROVED (exit 0),
CHANGES_REQUESTED (exit 1) o PIPELINE_ABORTED (exit 2). En
APPROVED conserva la rama gga/run-<ts> para que la inspecciones
y mergees a mano — el pipeline nunca mergea ni pushea.
Archivos clave del entorno
.md con el contenido. Se carga en cada sesión para darle a Claude contexto persistente sobre el usuario, el proyecto y el feedback acumulado.PROVIDER, FILE_PATTERNS/EXCLUDE_PATTERNS (excluye .env, state/, logs/, *.db, data/), RULES_FILE=AGENTS.md y los comandos de cada adaptador: CLAUDE_CMD, CODEX_CMD, COPILOT_CMD..gga-runs/<ts>/, semántica de exit codes y decisiones de handoff que el implementer debe respetar. Autoridad: AGENTS.md.Resumen de actores
| Actor | Qué es | Qué NO es |
|---|---|---|
| 🧠 Claude Opus 4.8 | El modelo de IA que razona y genera código | Un agente autónomo — necesita tus instrucciones |
| 🖥️ Claude Code | CLI/extensión que provee herramientas al modelo | El modelo en sí — es la infraestructura que lo rodea |
| 🎩 Gentleman AI | Persona: tono, idioma, filosofía de respuesta | Un sistema separado — es solo configuración en CLAUDE.md |
| 💾 Engram | Memoria persistente entre sesiones (MCP server) | Memoria de trabajo — eso es el contexto de la sesión |
| 📝 File Memory | Archivos .md de notas sobre usuario y proyecto | Sustituto de Engram — son complementarios |
| 🧩 Skills | Instrucciones especializadas por tarea (/sdd-apply…) | Código ejecutable — son Markdown que Claude lee |
| 👥 Sub-agentes | Instancias Claude para tareas paralelas o aisladas | Instancias con memoria — empiezan siempre desde cero |
| 📋 SDD | Metodología de desarrollo spec-first con agentes | Una herramienta — es un flujo de trabajo sobre Claude Code |
| 😇 GGA | Orquestador del equipo multi-agente (config en .gga) | Gentleman AI — ese es la persona; GGA coordina agentes |
| 🛠️ Codex | Implementer: features, refactors, tests, bugfixes | Autoridad sobre arquitectura/riesgo — eso es de Claude |
| 🔍 GitHub Copilot | Reviewer advisory (Copilot CLI agéntico, read-only) | Revisor vinculante — Claude override; ni la extensión legacy |
| ⚙️ gga_pipeline.py | Corrida local auditada de la cadena de delegación | CI/daemon — una tarea, una corrida; no mergea ni deploya |
| 👤 Product Owner | Javier: único que aprueba trading real y producción | Un agente de IA — es el gate humano final |
Endpoints
| Direction | Trigger | Payload | Description | Auth |
|---|---|---|---|---|
| INBOUND | Alert → POST /webhook | symbol · side · action · price · token |
TradingView dispara una alerta HTTP cuando el script Pine detecta un cruce EMA9/EMA21 válido con todos los filtros activos. El bot valida el token y procesa el payload con Pydantic. | WEBHOOK_SECRET |
| Method | Endpoint | Operation | Description | Called by |
|---|---|---|---|---|
| POST | /webhook | Receive signal | Receives TradingView alerts. Validates token secreto, comprueba si el bot está activo para ese activo y ejecuta la orden via Risk Manager. |
TradingView |
| GET | /health | System status | Retorna estado del bot por activo, modo dry-run, precio BTC (markPrice de Bitunix), resumen de posiciones, PnL diario, capital y detalle individual de posiciones (positions_detail). |
Dashboard · 5 s |
| POST | /bot/{symbol}/start | Start bot | Marca el bot como activo para el símbolo indicado y persiste el estado en state/bot_state.json. Los webhooks entrantes vuelven a ejecutarse. |
Dashboard |
| POST | /bot/{symbol}/stop | Stop bot | Marca el bot como pausado para ese símbolo y persiste el estado. Los webhooks para ese activo se rechazan silenciosamente sin tocar el exchange. | Dashboard |
| GET | /api/logs | Activity log | Lee las últimas 50 líneas de logs/bot.log, parsea el formato estructurado TS | LEVEL | module | msg y retorna un array JSON. El filtrado por activo es client-side. |
Dashboard · 5 s |
| GET | /api/backtest/summary | Backtest data | Retorna los mejores parámetros por activo, historial de iteraciones, conclusiones clave y config del backtest. Datos derivados de STRATEGY_LOG.md. |
Backtesting tab |
| GET | /api/bots/stats | Per-bot stats | Agrega logs/trades.csv por activo/iniciador: nº de trades, win rate, PnL total y de 7 días, y última actividad. Acepta ?mode=live|dry|backtest (en backtest sirve _BACKTEST_STATS precalculado). |
Bots tab |
| GET | /api/positions/history | Trade history | Lee logs/trades.csv y retorna todas las entradas reales (excluye mode=dry) en orden cronológico inverso. Fuente del historial de posiciones del dashboard. |
Posiciones tab |
| POST | /api/positions/reconcile | Sync exchange | Consulta Bitunix bajo demanda y sincroniza el tracker con las posiciones reales — detecta cierres por SL/TP o aperturas manuales sin esperar a un reinicio. En dry-run se omite (status: skipped). |
Posiciones tab |
| GET | /api/config | Read config | Retorna la configuración efectiva en runtime: modo dry-run, capital, % de riesgo, SL/TP, leverage, slippage y log level actuales. | Configuración tab |
| POST | /api/config | Update config | Modifica parámetros en caliente (dry-run, capital, riesgo, SL/TP, log level) sin reiniciar. El cambio de dry_run se persiste; valores inválidos se rechazan con 422. |
Configuración tab |
| GET | /api/debug/positions | Raw diagnostics | Devuelve la respuesta cruda de get_all_positions() de Bitunix sin procesar. Solo para diagnóstico — no lo consume el dashboard. |
Manual / debug |
| GET | /api/exchange/ping | Connectivity test | Llama a get_all_positions() en Bitunix para verificar que la API key es válida y hay conectividad real. En dry-run retorna una respuesta simulada. |
Exchange tab |
| POST | /api/exchange/order | Manual order | Abre o cierra una posición manualmente. Acepta symbol, side, action, price (opcional), sl_pct, tp_pct. Respeta el Risk Manager y el modo dry-run. |
Exchange tab |
| GET | /bot-manager-console/ | Dashboard UI | Sirve el dashboard frontend como archivos estáticos via FastAPI StaticFiles. Se monta al arrancar, después de todas las rutas API para que no las tape. |
Browser |
| GET | /backtest-results/ | Chart assets | Sirve el fichero equity_curve.html generado por Plotly, embebido en la pestaña Backtesting via <iframe>. Sin API — solo archivos estáticos. |
Backtesting tab |
| Method | Endpoint | Operation | Description | When |
|---|---|---|---|---|
| GET | /api/v1/futures/market/tickers | Tickers (precios) | Precios públicos (sin auth) de los símbolos configurados. El feed lee markPrice cada 10s y alimenta /health y el gate de slippage. Param: symbols=BTCUSDT,ETHUSDT,…. |
Price feed · 10 s |
| POST | /api/v1/futures/trade/place_order | Place order | Abre una posición MARKET con tradeSide=OPEN, SL y TP opcionales. Campos clave: symbol, side, orderType, qty, tradeSide. |
On signal · Manual |
| POST | /api/v1/futures/trade/place_order CLOSE | Close position | Cierra posición con tradeSide=CLOSE, reduceOnly=true. Mismo endpoint, distinto tradeSide. |
On close · Manual |
| POST | /api/v1/futures/trade/cancel_order | Cancel order | Cancela una orden pendiente por orderId. Payload: {"symbol", "orderId"}. |
On demand |
| POST | /api/v1/futures/trade/cancel_all_orders | Cancel all orders | Cancela todas las órdenes pendientes de un símbolo en una sola llamada. | On demand |
| GET | /api/v1/futures/trade/get_pending_orders | Pending orders | Lista las órdenes activas (limit, stop, etc.) aún no ejecutadas. Acepta ?symbol=. |
On demand |
| GET | /api/v1/futures/trade/get_history_orders | Order history | Historial de órdenes ejecutadas y canceladas. Acepta ?symbol=. |
On demand |
| GET | /api/v1/futures/position/get_pending_positions | Open positions | Posiciones abiertas actualmente. Se llama al arrancar el servidor y en el Ping del Exchange tab. Acepta ?symbol=. |
Startup · Ping |
| GET | /api/v1/futures/position/get_history_positions | Position history | Historial de posiciones cerradas. Acepta ?symbol=. |
On demand |
| GET | /api/v1/futures/account | Account balance | Balance disponible en USDT. No usado en el flujo automático actual. | On demand |
| Method | Endpoint | Operation | Description | When |
|---|---|---|---|---|
| GET | /api/v1/positions | Open positions | Posiciones abiertas. Fuente de get_all_positions, get_position_id y get_position_info (filtran por símbolo en memoria). Se llama al arrancar, en el Ping y tras abrir para capturar el positionId. |
Startup · Ping |
| POST | /api/v1/orders | Place order | Abre una posición MARKET. size en contratos enteros. Campos: symbol (XBTUSDTM), side, type, size, leverage. |
On signal · Manual |
| POST | /api/v1/orders CLOSE | Close position | Cierra con reduceOnly=true. Mismo endpoint que la apertura, distinto flag. |
On close · Manual |
| DELETE | /api/v1/orders/{orderId} | Cancel order | Cancela una orden pendiente por orderId (en el path). |
On demand |
| DELETE | /api/v1/orders | Cancel all orders | Cancela todas las órdenes de un símbolo. Param: ?symbol=XBTUSDTM. |
On demand |
| GET | /api/v1/orders | Orders (pending / history) | get_pending_orders y get_history_orders comparten endpoint con distinto filtro (?status=active / done, ?symbol=). |
On demand |
| GET | /api/v1/stopOrders | Pending TP/SL | Órdenes stop (TP/SL) pendientes. Por verificar contra los docs oficiales de KuCoin Futures — marcado [ASSUMPTION] en el cliente. |
On demand |
| GET | /api/v1/history-positions | Position history | Historial de posiciones cerradas. Por verificar contra los docs oficiales — marcado [ASSUMPTION] en el cliente. |
On demand |
| POST | /api/v2/changeCrossUserLeverage | Set leverage | Fija el leverage cross del símbolo antes de abrir. Payload: {"symbol", "leverage"}. |
Before order |
| GET | /api/v1/account-overview | Account balance | Balance disponible. Param: ?currency=USDT. Usado por el probe de credenciales al guardar claves. |
Ping · Probe |
| Method | Endpoint | Operation | Description | When |
|---|---|---|---|---|
| POST | logonvalidation.net/token | OAuth2 token | Renueva el access_token (vida ~20 min) con el refresh_token. Saxo usa OAuth2, no firma HMAC como Bitunix/KuCoin. |
Refresh · ~20 min |
| GET | /ref/v1/instruments | Instrument lookup | Resuelve el Uic de un par FX (EURUSD → Uic) necesario para construir órdenes. Resultado cacheado. |
Antes de operar |
| POST | /trade/v2/orders | Place order | Orden FxSpot: AccountKey, Uic, AssetType, BuySell, Amount, OrderType (+ órdenes relacionadas/OCO para SL/TP). |
On signal · Manual |
| DELETE | /trade/v2/orders/{orderId} | Cancel order | Cancela una orden pendiente por id (requiere AccountKey). |
On demand |
| GET | /port/v1/positions | Open positions | Posiciones abiertas de la cuenta, normalizadas a la forma que espera el engine. | Startup · Ping |
| GET | /port/v1/balances | Account balance | Balance y margen de la cuenta. Usado por el probe de conexión contra el entorno SIM. | Ping · Probe |
| GET | /trade/v1/infoprices | FX prices | Marks de precios FX para el feed y el gate de slippage del engine cuando el exchange activo es Saxo. | Price feed |
| Method | Endpoint | Operation | Description | Lifecycle |
|---|---|---|---|---|
| GET | api.binance.com /api/v3/ticker/price | Price poll | Polling cada 10s a la API pública de Binance para obtener precios de BTC, ETH, SOL y BNB simultáneamente. Sin autenticación. Alimenta /health → prices y los precios del dashboard. Se usa Binance porque el WebSocket de Bitunix no resuelve DNS correctamente. |
Continuous · 10s |
Cómo Iniciar el Sistema
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # editar con tus credenciales
python main.py
http://localhost:8000/bot-manager-console/
Posiciones
Posiciones abiertas e historial completo de operativasPosiciones Abiertas
| PositionID | Date / Time | Symbol | Side | Entry Price | Current Price | Qty | Unrealized PnL | Stop Loss | Take Profit | Origen | Action |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Sin posiciones abiertas | |||||||||||
Historial de Posiciones
| PositionID | Date / Time | Symbol | Side | Entry Price | Exit Price | Qty | PnL | Commission | Origen |
|---|---|---|---|---|---|---|---|---|---|
| Cargando historial... | |||||||||
Exchange Console
Operativa manual y monitorización directa del exchangeConfigured exchanges
Orders, positions and health data use the active exchange engine.
Loading exchanges...
New Order
DRY RUNOpen Positions
No open positions
Response Log
No requests yet — place an order or run a ping
Exchanges
Análisis y decisiones de integración de exchanges (solo admin)Exchanges crypto
Exchanges crypto integrados en el bot (Bitunix, KuCoin) y otros recomendados a evaluar para futuros perpetuos.
| Broker | Encaje técnico | Pero | Valoración |
|---|---|---|---|
| Bitunix | API REST con firma doble-pass SHA256 (httpx async). Es el cliente nativo base del bot (BitunixClient): request/response que encaja directo con el ExchangePort. Futuros perpetuos en USDT. |
Exchange joven y de menor tamaño (~$5B/día, 2M usuarios); sin licencia MiCA todavía (solo MSB en EE.UU./Canadá y VASP en Filipinas), acceso a retail UE incierto tras julio 2026; liquidez más ligera en algunos pares que los tier-1 y perfil con tolerancia no-KYC. | Operativo ↗ |
| KuCoin | API REST con firma HMAC-SHA256 + passphrase en base64 (símbolo XBTUSDTM). Cliente nativo KuCoinClient añadido en la migración multi-exchange; encaja con el ExchangePort, con cantidad en contratos enteros. |
Settlement con el DOJ de EE.UU. en 2024: 300 M$ de multa y salida obligatoria del mercado estadounidense (operar sin licencia, lavado/AML). Historial de compliance manchado, aunque obtuvo licencia MiCA en Austria (FMA, 2025) para operar en el EEA. | Operativo ↗ |
| Binance | API REST madura con firma HMAC-SHA256 (request/response, encaja con el ExchangePort) más WebSocket para streaming. Soportada por CCXT y wrappers Python maduros; máxima liquidez de futuros (300+ pares perp). | Mayor sanción regulatoria del sector: 4.300 M$ al DOJ de EE.UU. (2023) por fallos AML y violación de sanciones; el fundador CZ se declaró culpable y fue condenado a prisión. Futuros perp para retail UE restringidos (ESMA/MiFID limita apalancamiento); KYC estricto. MiCA (spot) obtenido en 2025. | Restringido |
| Bybit | API v5 unificada (REST + WebSocket) con firma HMAC, construida para derivados sobre una cuenta unificada. Request/response compatible con el ExchangePort; soportada por CCXT. | Sufrió el mayor hack de la historia cripto: 1.500 M$ en ETH robados (feb 2025, atribuido al grupo Lazarus de Corea del Norte, vía una vulnerabilidad en Safe Wallet). Con sede en Dubái; la licencia MiCA (Austria, 2025) cubre spot y la oferta de futuros perp a retail UE es limitada. | Riesgoso |
| OKX | API v5 completa (REST + WebSocket) con firma HMAC + passphrase, como KuCoin. Futuros y opciones bajo margen unificado; request/response compatible con el ExchangePort; soportada por CCXT. | Se declaró culpable ante el DOJ de EE.UU. (2025) y pagó 504 M$ por operar sin licencia y fallos AML (facilitó >5.000 M$ en transacciones sospechosas), con monitor de compliance hasta 2027. Con base en Seychelles; requiere passphrase adicional. Futuros perp a retail UE restringidos; MiCA vía Malta (MFSA). | Cuestionable |
| Kraken | API REST de Kraken Futures (entidad separada) con firma propia, request/response. Bien documentada y soportada por CCXT, aunque con menos pares de futuros que los grandes. | Menor liquidez y cobertura de pares de futuros que Binance/Bybit; los futuros van por una entidad separada (Kraken Futures), no disponible en todas las jurisdicciones. Antecedente regulatorio menor: 30 M$ a la SEC (2023) por su programa de staking no registrado en EE.UU. | Confiable |
Exchanges Forex
Brokers de Forex evaluados para la integración.
| Broker | Encaje técnico | Pero | Valoración |
|---|---|---|---|
| FXMC | Solo plataforma MetaTrader 4, sin API REST propia. La automatización requeriría Expert Advisors en MQL4 o un puente de terceros (p. ej. MetaApi); no encaja con el ExchangePort del bot. | No regulado, perfil de fraude (WikiFX 1.59). | Descartado |
| FXCM | API híbrida REST + Socket.IO (streaming) con token de acceso; parte del stack (namespace O2G2 de ForexConnect) está deprecada. El modelo de socket persistente no encaja con el patrón request/response del ExchangePort. | Expulsado del mercado de EE.UU. (multa CFTC, 2017) y futuro de la API incierto. | Descartado |
| OANDA | API v20 REST limpia (request/response, httpx async) con Bearer token + account_id: el encaje ideal con el ExchangePort… pero sin acceso para clientes de la UE (ver columna Pero). | Sin API REST para clientes de la UE desde la migración a OANDA TMS (Polonia, 2023). | Descartado |
| FOREX.com | API REST + streaming Lightstreamer con sesión (login → session ID). La ejecución va acoplada al feed (necesita Bid/Offer + Audit ID del stream), lo que rompe el patrón request/response del ExchangePort; sin SDK Python oficial. | Documentación cerrada y sin SDK Python oficial. | Descartado |
| Interactive Brokers | API madura vía TWS/IB Gateway (socket) con el wrapper ib_async, o Client Portal Web API (REST). Requiere un proceso Gateway permanente: no es REST stateless puro, no encaja directo con el ExchangePort sin infraestructura extra. |
Requiere un proceso IB Gateway permanente + re-login/2FA: infraestructura adicional. Finalista. | Finalista |
| Saxo Bank | OpenAPI REST/OAuth2 limpia (POST /trade/v2/orders): request/response que encaja con el ExchangePort sin procesos externos. Documentación pública, entorno SIM self-service (mapea al modo paper) y SDK Python (saxo-openapi). |
Comisiones/depósito premium y tokens OAuth2 de vida corta (~40 min). Elegido. | Integrando |
Análisis de broker para Crypto — Bitunix
Bitunix ↗Exchange de futuros perpetuos crypto; cliente base e histórico del bot.
- API REST nativa con firma doble-pass SHA256; es el cliente original sobre el que se construyó el bot.
- 500+ pares de futuros perpetuos y motor de alto rendimiento (hasta 200x de apalancamiento).
- Sin incidentes de hackeo desde su lanzamiento (2021); fondos en cold storage + fondo de garantía (Care Fund de 30 M$).
- Exchange joven y de menor tamaño (~5.000 M$/día, 2M usuarios) frente a los tier-1.
- Sin licencia MiCA todavía (solo MSB en EE.UU./Canadá y VASP en Filipinas); acceso a retail UE incierto tras julio 2026.
- Liquidez más ligera en algunos pares y perfil con tolerancia no-KYC.
Integrado y en producción como exchange base del bot.
Análisis de broker para Crypto — KuCoin
KuCoin ↗Exchange crypto incorporado en la migración multi-exchange.
- API REST HMAC-SHA256 + passphrase; integrado en la fase 2 de la migración multi-exchange.
- Exchange grande y establecido; licencia MiCA (Austria, FMA, 2025) que habilita operar en los 30 países del EEA.
- Amplia cobertura de mercados y buena liquidez.
- Settlement con el DOJ de EE.UU. (2024): 300 M$ de multa y salida obligatoria del mercado estadounidense (operar sin licencia, lavado/AML).
- Historial de compliance manchado (la subsidiaria en Viena se ha visto como arbitraje regulatorio).
- Particularidades de la API: cantidad en contratos enteros (fail-closed en fraccional), símbolo XBTUSDTM y TP/SL nativo limitado.
Integrado; en el bot arranca forzado a dry_run pendiente de aprobación del Product Owner.
Análisis de broker para Forex — Saxo Bank
Saxo Bank 🔧Evaluación de brokers para incorporar operativa Forex, filtrados por dos requisitos: que acepten residencia España/UE y que ofrezcan una API utilizable para el bot.
- FXMC — no regulado, perfil de fraude (WikiFX 1.59). Descartado.
- FXCM — expulsado del mercado de EE.UU. (multa CFTC, 2017) y API en estado incierto (Socket.IO). Descartado.
- OANDA — técnicamente ideal, pero sin API REST v20 para clientes de la UE desde la migración a OANDA TMS (Polonia, 2023). Descartado.
- FOREX.com — documentación cerrada y ejecución de órdenes acoplada al streaming propietario (Lightstreamer), sin SDK Python oficial. Descartado.
- Interactive Brokers — finalista. API completa y comisiones bajas, pero exige un proceso IB Gateway permanente (infraestructura adicional + re-login/2FA).
- Saxo Bank — elegido.
- OpenAPI REST limpia (
POST /trade/v2/orders): encaja con el puerto de exchange existente, sin procesos externos como el Gateway de IBKR. - Documentación pública y entorno de simulación (SIM) self-service: se mapea directamente al modo paper/dry_run del bot.
- Banco europeo regulado (MFSA/MiFID) que da servicio a residentes en España.
- Soporte nativo de stop-loss / take-profit (órdenes relacionadas).
- Comisiones y depósito mínimo más altos (perfil premium frente a IBKR).
- Autenticación OAuth2 con tokens de vida corta (access ~20 min / refresh ~40 min): requiere lógica de renovación y un procedimiento de re-login tras caídas prolongadas. La autenticación por certificado (sin login) es solo para clientes institucionales.
Integración pendiente de diseño. Saxo arrancará en SIM/paper; la activación de trading real requiere aprobación del Product Owner.
Users
Gestion de usuarios administradores y operadoresCrear usuario
Usuarios
| ID | Usuario | Rol | API | Modo | Nuevo password | ||
|---|---|---|---|---|---|---|---|
| Cargando... | |||||||
Trading
Modo de operación y parámetros del sistemaConfiguración en TradingView
Conectá tus señales de TradingView con el bot en 3 pasos. Una vez configurada la alerta, cada señal dispara una orden automáticamente.
- Seleccioná el activo. Abrí en TradingView el gráfico del par que vas a operar (BTCUSDT, ETHUSDT, SOLUSDT o BNBUSDT).
- Cargá el Pine Script. Abrí el Pine Editor, pegá el script de la estrategia y pulsá "Add to chart". El script ya arma el mensaje de la alerta con tu token y el campo
ts(protección anti-replay) — no hace falta escribir el JSON a mano. - Creá la alerta con webhook:
- Condición: la estrategia que cargaste en el gráfico.
- Mensaje:
{{strategy.order.alert_message}}(el Pine lo completa solo). - Notificaciones → Webhook URL: activá la casilla y pegá tu URL personal:
https://bipbiptrading.com/webhook/TU_TOKEN
Tu token de webhook es personal y secreto — no lo compartas. Probá una alerta en modo DRY RUN antes de operar en real.
Modo de Operación
Controla si las órdenes se envían al exchange real o solo se simulan.
Parámetros del Sistema
Los cambios se aplican en runtime y se persisten en la base de datos del usuario.
Roadmap
Tareas pendientes y fases del pipeline GGA acordadas durante el desarrolloBacklog de proyectos
Iniciativas grandes que se ejecutan por fases vía el pipeline GGA. Cada card agrupa las peticiones de su proyecto.
#3. Integración exchange Saxo Bank
Primer broker de Forex (Saxo Bank OpenAPI). A diferencia de Bitunix/KuCoin, autentica con
OAuth2 (no HMAC estático), opera FX spot (no perpetuos), exige resolución de
instrumentos (Uic), su propio feed de precios FX y un entorno SIM para paper.
Una petición por fase para tools/gga_pipeline.py, en orden, mergeando cada fase
antes de la siguiente. En PowerShell: $task = @' … '@ y luego
python tools/gga_pipeline.py $task --run-tests --allow-dirty.
#4. Generación de estrategias guiada (filtros + multi-activo)
Hoy la generación manual de estrategias (tab I+D) es un disparo a ciegas: el prompt elige cualquier
estrategia y el backtest está hardcodeado a crypto (BTC/ETH/SOL/BNB). Este proyecto añade filtros
para guiar el research — tipo de activo (crypto, forex, commodity, acciones), familia
(trend, mean-reversion, breakout, momentum, volatilidad) y timeframe — y abre el backtest a multi-activo.
Una petición por fase para tools/gga_pipeline.py, en orden, mergeando cada fase antes de
la siguiente. En PowerShell: $task = @' … '@ y luego
python tools/gga_pipeline.py $task --run-tests --allow-dirty.
#5. Refactor Backtester v.2
Refactor del motor de backtesting para eliminar los sesgos que invalidan los resultados actuales: la entrada se ejecuta en el cierre de la misma vela que genera la señal (look-ahead), el ratio de Sharpe está mal anualizado fuera de 15m, y las salidas SL/TP asumen un fill perfecto sin slippage. Además la optimización de ADX es in-sample (overfitting) y el loop de simulación no tiene ni un test. El objetivo es que los números del backtester sean confiables ANTES de seguir optimizando estrategias.
#6. Integración Feed Noticias v.1
Agregador de noticias de mercado en español para los activos que opera el bot (BTC, ETH, SOL, BNB y pares FX),
vía feeds RSS nativos en español (Cointelegraph ES, CriptoNoticias, Bit2Me News, FXStreet ES, Investing.com ES) —
sin API key ni traducción. Un loop de refresco en segundo plano parsea con feedparser, normaliza,
deduplica y mapea por activo; un endpoint GET /api/news sirve el caché y un panel en la consola lo muestra por activo.
v.1 enfocada (RSS + panel); sentiment, Finnhub, traducción y feed a I+D quedan para v.2. Una petición por fase para
tools/gga_pipeline.py, en orden, mergeando cada fase antes de la siguiente.
#7. Catálogo de Indicadores v.1
Espejar el sistema de catálogo de Estrategias para una nueva entidad: Indicadores. En vez de clonar el generador SSG (deuda técnica desde el día uno), se generaliza el builder actual a un motor de catálogo parametrizado por entidad que sirve Estrategias E Indicadores desde una sola base de código. Cada indicador (EMA, VWAP, RSI, ADX, ATR, Bollinger, Supertrend, Donchian, Volume MA, SMA) tendrá su ficha pública SEO y una card en la consola que navega a /indicadores/<slug>. Las páginas nuevas nacen con noindex,nofollow para respetar el estado temporal del sitio.
Backlog de tareas
Roadmap de mejoras acordadas durante el desarrollo.
TAREAS RELEASE V.0.4
POST /auth/login (auth/routes.py:44) permite fuerza bruta ilimitada. Sumar slowapi (~5/min por IP) o lockout por usuario tras N intentos fallidos.engine/registry.py reconstruye el PositionTracker vacío al rotar keys/exchange sin transferir estado. Un webhook en esa ventana ve has_position()=False y puede duplicar. Copiar posiciones del engine viejo o recargar de disco tras el swap._slippage_ok (main.py:80,143) valida contra el mark de Bitunix para todos los usuarios, incluido KuCoin. Hoy lo tapa el force_dry_run de KuCoin, pero rompe el día que vaya a live. Feed de precios por tipo de exchange.docker-compose.yml:36 hace que uvicorn confíe X-Forwarded-* de cualquier origen → spoofing de IP/proto si la app queda expuesta directa. Limitarlo a 127.0.0.1 (el proxy corre en el mismo host).max_age (cookie robada = acceso permanente), SameSite=Lax→Strict, y scrypt en N=2^14 (mínimo OWASP) → subir a 2^17 con re-hash on login. Todo en main.py:476-481 y auth/security.py:17.main.py:554) es solo in-process. Con --workers N, dos webhooks abren posición doble. Mínimo: forzar --workers 1 + assert al arranque; ideal: idempotencia en SQLite (INSERT OR IGNORE).kucoin_client.py:256 levanta excepción tras setear leverage → KuCoin queda "abre pero siempre falla". Strippear TP/SL en el path KuCoin o implementar las conditional orders.innerHTML sin escapar (app.js:879, app.js:232). El helper _esc() ya existe y se usa en la tabla de Users — falta aplicarlo acá. Fix de una tarde.TAREAS RELEASE V.0.5
round(qty, 4) arbitrario (risk/manager.py:116). Bitunix tiene step size / min order size por símbolo (puede generar qty bajo el mínimo); KuCoin exige contratos enteros y un float lo hace reventar en runtime → el open falla en silencio. Pasar step_size/min_qty por exchange y snap con floor. Parte restante del sizing (#5).routers/webhook.py, health.py, config.py, keys.py, exchanges.py, positions.py, admin.py) → main.py a ~150 líneas. Mayor impacto / menor riesgo.db/schema.py usa una función ad-hoc por columna, sin versión ni rollback; no escala más allá de 4-5. Tabla schema_migrations + run_migrations() que aplica (version, sql) en orden.requirements.txt usa >= en casi todo y mezcla dev con runtime. Un pip install futuro puede traer un major que rompe. Generar lock con hashes y separar dev/runtime.logs/trade_logger.py lee y reescribe el CSV en cada trade. Moverlo a la init del engine (una vez).UserEngineRegistry() duplicado nunca usado (engine/registry.py:208), alias _get_engine_or_401 = _get_engine_or_403 (main.py:108), imports a mitad de módulo y monkeypatches obsoletos en tests/conftest.py:143-147.app.js:1914 arranca fetchHealth/fetchLogs cada 5s sin guardar los IDs; en re-runs se apilan. Guardar IDs y gatear el arranque.aria-live (los lectores de pantalla pierden señales/trades), y el drawer de cuenta y el modal de Pine no atrapan ni restauran el foco. Jerarquía de headings rota (h2/h2/h2).style.css deja el sidebar fijo en 220px sin colapso ni hamburguesa <768px; en mobile el contenido queda apretado. Varias tablas anchas sin overflow-x: auto.TAREAS RELEASE V.0.6
YFinanceProvider (backtest/data_provider.py) sirve intradía solo ~60d (15m) / ~730d (1h); un backtest forex con since=2024-01-01 recibe un frame truncado pero no vacío, lo escribe y lo backtestea como historia completa → "rentabilidad" sobre 60 días que se publica en el catálogo SEO. El EmptyDataError que el diseño prometía (criterio #7) no se implementó; solo hay un RuntimeError para frame totalmente vacío (download_data.py:63-64). Falta: comparar el rango devuelto vs el since pedido y fallar/flaggear como baja-confianza si es mucho más corto.=X de Yahoo devuelven volumen 0; _flatten_yfinance (data_provider.py:162-164) lo setea a 0.0 en silencio. Cualquier estrategia con VWAP o filtro de volumen (p. ej. la estrella EMA+VWAP) se degrada sin aviso sobre FX. Avisar/bloquear estrategias volume-dependientes para forex.data_provider.py:139 usa auto_adjust=False y Close crudo (descarta Adj Close). En backtests largos de equities los splits/dividendos meten saltos artificiales. Usar auto_adjust=True (ajusta el OHLC) para la clase stock.XAUUSD→GC=F, WTI→CL=F (data_provider.py:30-33) son futuros (front-month, con roll gaps), no el spot que el símbolo implica. Menor (commodities quedan research-only), pero documentar el mismatch de instrumento para no leer mal los resultados.ExecutionModel y mover el fill de close[i] a open[i+1]; actualizar los valores esperados en los tests y documentar el delta de resultados vs. la versión anterior.backtest/metrics.py, corregir BARS_PER_YEAR por timeframe, agregar Sortino, Calmar, máximo de pérdidas consecutivas, duración media de trade y benchmark buy&hold sobre la misma ventana.ExecutionModel: si la vela abre por debajo del SL, el fill es al open (peor precio), no al precio del stop.backtest/validation.py con walk-forward anchored y rolling; reescribir optimize_adx para optimizar en el set de entrenamiento y reportar resultados en el set de test (fuera de muestra).tools/strategy_site/build.py a tools/catalog_site/build.py parametrizado por entidad (entity, data_dir, out_dir, base_path); Estrategias se reescribe como instancia del motor genérico; los tests de strategy_site siguen verdes como red de regresión.content/indicators/_site.json (base_path /indicadores) y 10 fichas JSON (EMA, VWAP, RSI, ADX, ATR, Bollinger, Supertrend, Donchian, Volume MA, SMA) con contenido derivado de backtest/strategy.py y de los bloques indicators[] de cada estrategia.web-public/indicadores/; agregar el middleware en main.py para URLs limpias /indicadores/<slug>; crear la regla Apache; verificar que las páginas generadas incluyen meta robots noindex,nofollow.tab-indicadores y loadIndicatorCatalog() en app.js consumiendo /indicadores/index.json; las cards navegan a /indicadores/<slug>.tests/test_catalog_site_build.py con tests parametrizados del motor para ambas entidades (regresión Estrategias + Indicadores) y tests/test_public_indicator_pages.py para el middleware de URLs limpias; documentar el comando de build de ambos catálogos.Proyectos completados
Proyectos terminados — todas sus fases integradas.
#1. Migración a multi-exchange
Peticiones detalladas para tools/gga_pipeline.py, una por fase.
Ejecutar en orden y mergear cada fase antes de la siguiente
(cada una asume que la anterior ya está en la base). En PowerShell:
$task = @' … '@ y luego
python tools/gga_pipeline.py $task --run-tests --allow-dirty.
#2. Refactorización web SEO estrategias
Catálogo público de estrategias orientado a SEO: cada estrategia es un dato
(content/strategies/*.json) que tools/strategy_site/build.py
convierte en una página estática pre-renderizada en web-public/estrategias/.
Peticiones detalladas para tools/gga_pipeline.py, una por fase.
Ejecutar en orden y mergear cada fase antes de la siguiente
(cada una asume que la anterior ya está en la base). En PowerShell:
$task = @' … '@ y luego
python tools/gga_pipeline.py $task --run-tests --allow-dirty.
Tareas Completadas
Trabajo ya entregado, agrupado por release.
RELEASE NOTES V.0.3
content/strategies/*.json) que tools/strategy_site/build.py convierte en una página estática pre-renderizada en web-public/estrategias/, optimizada para SEO. Se ejecuta por fases vía el pipeline GGA. Escala a 100+ estrategias.POST /api/newsletter/subscribe (email validado, inserción idempotente) + tabla newsletter_subscribers en SQLite + endpoint admin para listar suscriptores + handler fetch en el formulario de las páginas. 251 tests pasan..html con snippet Apache para el operador, y regeneración del catálogo en un stage del Dockerfile (sin meter deps de build en runtime). Primera fase APPROVED por el pipeline; 253 tests pasan./estrategias/index.json (generado por build.py) y enlaza a las páginas públicas /estrategias/<slug>; se eliminó la documentación inline duplicada del index.html monolítico. 256 tests pasan.asyncio.sleep(0.8) fijo se reemplazó por un helper _capture_position_id() que reintenta con backoff 0.5/1/2s en ambos paths (webhook y orden manual). Si tras los reintentos no aparece el positionId, ahora devuelve status=error (HTTP 502) con un motivo que pide reconciliar, en vez del engañoso ok+warning. 280 tests pasan._place_order_with_retry (main.py:117,124) solo trata BitunixAPIError como permanente; un error de auth de KuCoin se reintenta a ciegas dos veces. Agregar rama except KuCoinAPIError con su set de códigos permanentes.main.py:265), no se llama a tracker.close_position ni record_trade_result: el tracker sigue "abierto" y el límite de pérdida diaria no ve la pérdida hasta el próximo reconcile. Hay ventana donde is_trading_allowed() miente.risk/manager.py acumula PnL en floats; deriva error con muchos trades. Migrar a decimal.Decimal o, mínimo, clamp a 2dp antes de comparar con el circuit-breaker.POST /webhook/{token} (main.py:489) no valida timestamp ni nonce: un body capturado se reenvía a voluntad (open → close → open). Exigir un campo ts y rechazar señales fuera de ±30s; opcional, nonce con TTL.#tab-configuracion) usaba un wrapper .cfg-page con max-width: 860px, por lo que sus cards quedaban más angostas que las del resto de páginas. Se quitó el límite para que ocupen todo el ancho del contenido, uniformando el layout con las demás pestañas.RELEASE NOTES V.0.2
exchanges por usuario (credenciales cifradas por exchange + passphrase) con migración idempotente que copia las creds Bitunix existentes. Abstracción ExchangePort que BitunixClient cumple. Aditivo, sin cambiar el trading.ExchangePort, con su firma propia (HMAC-SHA256 + passphrase). Aislado y testeado contra la interfaz.RELEASE NOTES V.0.1
Diseño técnico — Integración exchange Saxo Bank
Documento técnico previo a las 5 fases del pipeline GGA. Define el cómo —arquitectura, componentes, datos, endpoints y pruebas— en la etapa SDD, antes de ejecutar.
Resumen del diseño técnico
Saxo Bank es el primer broker de Forex del bot. A diferencia de Bitunix y KuCoin (firma HMAC estática, perpetuos cripto), Saxo autentica con OAuth2 (access token de vida corta + refresh token), opera FX spot, exige resolver el instrumento a un Uic antes de operar, tiene su propio feed de precios FX y un entorno SIM para paper trading. La integración es ADITIVA y se entrega en 5 fases del pipeline GGA (una tarea SDD = una fase): no cambia el path live de Bitunix/KuCoin. Como con KuCoin, Saxo arranca forzado a SIM/dry-run hasta aprobación explícita del Product Owner.
Diagrama de arquitectura
En paralelo, un feed de precios FX propio (no el de Bitunix) alimenta el slippage y el PnL de los símbolos Forex, y un refresh de token OAuth2 mantiene vivo el access token (~20 min) con el refresh token cifrado.
Componentes implicados
SaxoClient conforme a ExchangePort: OAuth2 (refresh de token), resolución y cache de Uic, órdenes FxSpot (+ OCO para SL/TP), normalización de posiciones al shape del engine y soporte dry-run/SIM.
Agregar la rama "saxo" a create_exchange_client() con sus parámetros OAuth (app key/secret, tokens, environment).
Columnas OAuth en exchanges (migración idempotente) y helpers para leer/guardar tokens cifrados del exchange activo tipo saxo.
El routing al exchange activo ya existe; saxo entra por la factory. Verificar que un usuario saxo cargue creds OAuth en memoria (fail-closed).
Endpoints OAuth (authorize/callback), gestión de exchange saxo en /api/exchanges, feed de precios FX y validación de símbolos Forex en el webhook.
UI del tab Exchange para Saxo: conectar por OAuth, badge SIM, selección de instrumentos FX. Nunca persiste secretos en el cliente.
Modelo de datos afectado
| Campo | Tipo | Descripción |
|---|---|---|
type | text (CHECK) | Añade el valor 'saxo' al CHECK existente (bitunix | kucoin | saxo). |
enc_app_key | text (Fernet) | App key OAuth2 cifrada. |
enc_app_secret | text (Fernet) | App secret OAuth2 cifrado. |
enc_access_token | text (Fernet) | Access token (~20 min) cifrado; se refresca en runtime. |
enc_refresh_token | text (Fernet) | Refresh token cifrado para renovar el access token. |
token_expires_at | integer (epoch) | Expiración del access token; dispara el refresh. |
environment | text | 'sim' | 'live' (default 'sim') — nunca live sin aprobación del PO. |
Las columnas existentes enc_api_key / enc_api_secret / enc_passphrase quedan NULL para filas saxo. Migración idempotente, sin tocar filas bitunix/kucoin.
Endpoints a utilizar
| Método | Ruta | Uso | Descripción |
|---|---|---|---|
| GET | /api/exchanges/saxo/authorize | NUEVO | Inicia el flujo OAuth2: redirige a la pantalla de consentimiento de Saxo. |
| GET | /api/exchanges/saxo/callback | NUEVO | Recibe el code, lo intercambia por access/refresh token y los guarda cifrados. |
| POST | /api/exchanges | Extendido | Alta de exchange type=saxo con app key/secret y environment (write-only). |
| POST | /api/exchanges/active | Existente | Activa Saxo como exchange del usuario. |
| INBOUND | /webhook/{token} | Existente | Señales FX; valida que el símbolo sea un par Forex cuando saxo está activo. |
| Método | Ruta | Uso | Descripción |
|---|---|---|---|
| POST | {auth}/token | OAuth2 | Renueva el access token (~20 min) con el refresh token. |
| GET | /ref/v1/instruments | Uic lookup | Resuelve el Uic de un par FX (EURUSD → Uic); resultado cacheado. |
| POST | /trade/v2/orders | Orden | Orden FxSpot: AccountKey, Uic, BuySell, Amount, OrderType (+ OCO para SL/TP). |
| DELETE | /trade/v2/orders/{orderId} | Cancelar | Cancela una orden pendiente. |
| GET | /port/v1/positions | Posiciones | Posiciones abiertas; se normalizan al shape del engine. |
| GET | /trade/v1/infoprices | Precio FX | Precio del instrumento para el feed FX (slippage / PnL). |
Tests a realizar
isinstance(SaxoClient(...), ExchangePort) y mismas firmas/return shapes que el engine consume.place_order/cancel no pegan al gateway LIVE (mock aserta que no hubo llamada).Prueba proyecto desarrollado
Cómo verificar de punta a punta que la integración Saxo funciona, siempre en SIM, antes de que el Product Owner apruebe el paso a LIVE. Cada paso confirma una pieza del flujo de la sección anterior.
- Preparar el entorno SIM — crear una app OAuth2 en el portal de developers de Saxo (entorno SIM), obtener
app key/app secret, y tener una cuenta SIM con saldo de paper. - Dar de alta el exchange — en el tab Exchange, agregar uno
type=saxocon la app key/secret yenvironment=sim(write-only, nunca se muestran los secretos). - Conectar por OAuth2 — lanzar
/api/exchanges/saxo/authorize, completar el consentimiento en Saxo y volver por/callback; verificar que el exchange queda con badge SIM y tokens cifrados guardados. - Activar Saxo —
POST /api/exchanges/activecontype=saxo; confirmar que/healthrefleja el exchange activo en dry-run/SIM. - Orden manual de prueba — desde la consola de exchange, abrir una orden FxSpot en un par FX (p. ej. EURUSD); verificar en los logs que resuelve el
Uicy envía a/trade/v2/ordersen SIM (sin pegar a LIVE). - Señal por webhook — disparar una alerta de TradingView (o un POST manual) a
/webhook/{token}con un símbolo FX; verificar que el engine la valida, calcula SL/TP y sizing, y rutea porSaxoClient. - Verificar posición, precio y cierre — la posición aparece en Posiciones con precio del feed FX; cerrar la posición y comprobar que se registra el trade y el PnL.
- Refresh de token — dejar pasar >~20 min (o forzar la expiración) y confirmar que el access token se renueva solo y la operativa continúa sin reconectar.
- No regresión — cambiar el exchange activo a Bitunix/KuCoin y comprobar que operan idéntico; los símbolos cripto siguen funcionando.
- Suite automática — correr las pruebas; deben quedar verdes.
python -m compileall . && pytest -q tests/
Criterio de aprobación: todos los pasos OK en SIM + suite verde. Recién entonces el Product
Owner puede aprobar el paso a LIVE (cambiar environment a 'live').
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🟢 Fase 1 — Fundación: modelo de credenciales OAuth2 + tipo "saxo" d4db8ca
Build the FOUNDATION for integrating Saxo Bank as a new exchange type, WITHOUT changing any current live-trading behavior. Bitunix and KuCoin must keep working exactly as today. Context: the multi-exchange foundation is already in master — exchange/port.py (ExchangePort Protocol), an `exchanges` table with per-user rows (type, Fernet-encrypted api_key/api_secret + nullable passphrase, base_url, enabled, one active per user), db/exchanges.py read helpers, exchange/factory.py routing, and per-exchange UI. Both Bitunix (HMAC) and KuCoin (HMAC + passphrase) authenticate with a STATIC api_key/api_secret pair. Saxo is DIFFERENT: it authenticates with OAuth2 — an app key/secret plus a short-lived access token and a refresh token — not a static signing secret. Deliver three ADDITIVE pieces: 1. OAuth2 credential model: extend the exchanges storage so a "saxo" row can hold, Fernet-encrypted: app_key, app_secret, refresh_token and the current access_token, plus a token_expiry timestamp (the expiry itself can be plaintext). Add NULLABLE columns; do not break Bitunix/KuCoin rows. Provide an IDEMPOTENT migration (running it twice is a no-op). 2. Recognize "saxo" as a valid exchange type across the type validators/enums, WITHOUT wiring any client or routing yet. 3. db/ read+write helpers to fetch and UPDATE a user's Saxo OAuth tokens (decrypted only in memory), so a later refresh job can persist rotated tokens. HARD CONSTRAINTS (exchange integration is a Critical Component per AGENTS.md): - Do NOT change order placement, Risk Manager, leverage, SL/TP, daily-loss, or webhook auth. - Do NOT build a SaxoClient or any routing in this phase. - Tokens/secrets stay Fernet-encrypted at rest; never plaintext, never logged, never returned by any endpoint. - Bitunix and KuCoin rows and flows stay byte-for-byte equivalent. Tests: idempotent migration; a "saxo" row round-trips the encrypted OAuth fields; existing bitunix/kucoin rows are unaffected; type validators accept "saxo".
🔴 Fase 2 — SaxoClient aislado (OpenAPI REST + OAuth + SIM)
Implement a Saxo Bank OpenAPI client for Forex trading. This phase is ISOLATED: do NOT wire it into the engine, the registry, the factory, or any live path yet. Context: Phase 1 added OAuth2 credential storage for a "saxo" exchange type (encrypted app_key, app_secret, refresh_token, access_token, token_expiry) and read/write helpers. exchange/port.py defines ExchangePort, which both BitunixClient and KuCoinClient conform to. The port was shaped around crypto PERPETUAL futures (place_order, set_leverage, get_position_id, positionId, etc.). Deliver: 1. A SaxoClient class (exchange/saxo_client.py) implementing ExchangePort with the SAME method names and return shapes the engine consumes. Saxo trades SPOT FX (AssetType FxSpot) via OpenAPI REST, so NORMALIZE Saxo responses into the engine-expected keys (symbol, side, size/qty, entryPrice, positionId, etc.). Document every normalization. 2. OAuth2 token manager: use the refresh_token to obtain/renew the access token (Saxo access tokens are short-lived, ~20 min), refresh PROACTIVELY before expiry, be safe under concurrency, and surface rotated tokens so the caller can persist them via the Phase 1 helpers. Never block a trade on an avoidable token refresh. 3. Instrument (Uic) resolution: map an FX symbol (e.g. EURUSD) to its Saxo Uic via /ref/v1/instruments, with caching. Orders need Uic + AssetType. 4. Map ExchangePort onto OpenAPI: place_order → POST /trade/v2/orders (AccountKey, Uic, AssetType FxSpot, BuySell, Amount, OrderType, plus related/OCO orders for SL/TP); positions → /port/v1/positions; balances → /port/v1/balances. For methods with no FX equivalent (e.g. set_leverage — FX margin is account-level, not per-order) implement a DOCUMENTED no-op or the closest correct behavior and STATE it in the implementer report. 5. dry_run via the Saxo SIM environment (the /sim/openapi gateway), which is real paper trading; live uses the LIVE gateway. Mask app_key in logs; never log secrets, tokens or signing material. CRITICAL: follow Saxo's OFFICIAL OpenAPI docs for exact endpoints, the OAuth2 token endpoint, order payloads, AssetType and response shapes. Do NOT invent endpoints. State any uncertain assumption in the implementer report. CONSTRAINTS: - Do NOT modify BitunixClient, KuCoinClient, the engine, the registry, the factory, Risk Manager, or any live path. This phase only ADDS exchange/saxo_client.py and its tests. - Secrets/tokens never appear in logs or endpoint responses. Tests: unit tests with mocked HTTP for the OAuth2 refresh flow, Uic resolution, a place_order FxSpot payload (live + dry_run/SIM), SL/TP related-order construction, and normalization of a sample Saxo positions/balances response into the engine-expected shape. Assert SaxoClient satisfies ExchangePort.
🔴 Fase 3 — Routing del engine + gestión de credenciales OAuth ⚠️ alto riesgo
Wire Saxo routing into the engine and add per-user Saxo OAuth credential management. This phase touches the LIVE money path: be conservative and keep existing Bitunix/KuCoin behavior identical. Context (Phases 1 and 2 are MERGED): OAuth2 storage for "saxo" exists; exchange/saxo_client.py SaxoClient conforms to ExchangePort with an OAuth2 token manager, Uic resolution and SIM dry_run. exchange/factory.py builds BitunixClient/KuCoinClient from an ActiveExchange; db/exchanges.py get_active_exchange returns decrypted creds in memory; UserEngine.exchange_client() and the registry route by the active exchange. Deliver: 1. Extend the factory to return SaxoClient for type "saxo", owning its OAuth constructor differences (app_key/secret + tokens instead of api_key/secret). A user whose active exchange is Saxo routes to SaxoClient; Bitunix/KuCoin users are completely unaffected. 2. Per-user Saxo onboarding: implement the OAuth2 flow so a user can connect a Saxo account — either the authorization-code redirect flow (redirect to Saxo, capture the code at a callback, exchange it for refresh+access tokens) or accept a manually issued developer token — and store everything encrypted. Reuse the connectivity-probe-before-enable pattern (a real SIM auth check); on failure do not store / force dry_run. 3. A background token-refresh task (or refresh-on-load) that keeps each Saxo engine's access token valid using the stored refresh_token, persisting rotated tokens via the Phase 1 helpers, so sessions do not die after ~20 min. HARD CONSTRAINTS (Critical Component, AGENTS.md, requires Product Owner approval): - Bitunix and KuCoin users MUST keep trading identically. Users with no Saxo row are untouched. - Do NOT change order placement, Risk Manager, leverage, SL/TP, daily-loss, or webhook auth. - Fail-closed: missing/undecryptable/expired-and-unrefreshable Saxo creds means no engine loaded. - Saxo starts in SIM/dry_run; do NOT enable live Saxo trading automatically — live requires explicit Product Owner approval. - Tokens/secrets stay Fernet-encrypted at rest; never plaintext, never logged, never returned. Tests: the factory returns SaxoClient for type saxo; routing picks the active exchange; Bitunix/KuCoin users still route identically; a failing SIM probe is not stored / forces dry_run; the refresh task rotates and persists tokens; secrets never appear in endpoint responses.
🔴 Fase 4 — Feed de precios FX + símbolos/validación de webhooks Forex
Make the engine handle FOREX correctly for Saxo: price feed, symbol mapping and webhook validation. The current price feed and slippage check are crypto/Bitunix-only. Context (Phases 1-3 MERGED): Saxo routing works and SaxoClient resolves FX Uic and trades FxSpot. BUT the engine's shared price cache is fed only by Bitunix tickers, and the webhook/signal model expects crypto-perpetual symbols (e.g. BTCUSDT). _slippage_ok and the open path read a Bitunix mark for every user regardless of the active exchange (a known audit finding). Deliver: 1. A per-exchange-type price source: when the active engine is Saxo, obtain FX marks from Saxo (e.g. /trade/v1/infoprices or the SaxoClient) for the symbols that user trades, instead of the Bitunix ticker cache. The slippage reference and the "no mark price" guard must use the ACTIVE exchange's price, with messages that name the real exchange (not a hardcoded "Bitunix"). 2. FX symbol handling in the webhook/signal layer: accept TradingView FX symbols (e.g. EURUSD / EUR_USD), validate them for a Saxo engine, and pass them to Uic resolution. Keep crypto symbol validation unchanged for Bitunix/KuCoin engines. 3. Ensure PnL/position math uses FX-appropriate fields (Amount in base currency, FxSpot semantics) without breaking the crypto path. CONSTRAINTS: - Do NOT regress the Bitunix/KuCoin price feed, slippage or symbol validation. - Do NOT change Risk Manager math, daily-loss or webhook auth. - No secrets in logs. Tests: a Saxo engine's slippage check uses an FX mark (not a Bitunix one); an FX symbol validates for Saxo and is handled correctly for a crypto engine; the "no mark price" rejection names the active exchange; the Bitunix/KuCoin paths are unchanged.
🔴 Fase 5 — UI del tab Exchange para Saxo (OAuth + Forex)
Add Saxo Bank to the dashboard Exchange tab and account drawer, including the OAuth connect flow. FRONTEND ONLY (web-private/: index.html, app.js, style.css). The Phase 1-4 backend is already in master. Backend already available (do NOT change it): GET /api/exchanges lists configured exchanges (metadata only, including a "saxo" type and whether it is connected / in SIM); the Saxo onboarding endpoint(s) from Phase 3 start the OAuth flow or accept a token and run a SIM probe; POST /api/exchanges/active switches the active exchange; /health drives the status bar for the active engine. Deliver (vanilla HTML/CSS/JS, match existing patterns, no new deps): 1. Let the user add and manage a Saxo exchange: trigger the OAuth2 connect flow (redirect to Saxo and back, or paste a developer token), show connection + token status (connected, SIM/dry_run, expiry/refresh) and surface the probe result. Saxo has NO api_secret/passphrase form like Bitunix/KuCoin — show OAuth-appropriate fields only. 2. Generalize the Exchange tab selector and status bar so Saxo appears alongside Bitunix/KuCoin, shows it is in SIM/dry_run pending live approval, and can be set active via POST /api/exchanges/active. 3. FX-aware hints where the UI assumes crypto symbols (e.g. the symbol/instrument helper), without breaking the crypto experience. 4. Bump the ?v= query string on app.js and style.css so browsers refetch. CONSTRAINTS: - FRONTEND ONLY: do NOT touch Python/backend, webhook, risk or order code. - Never persist tokens/secrets in client storage; forms are write-only; the GET never returns secrets. - HTML-escape all dynamic values. - Keep the experience identical for users who only have Bitunix/KuCoin. - Keep index.html balanced (equal div open/close).
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Fundación OAuth2 | APPROVED | 🧠→🛠️→🔍→✅ | ~17 min | $2.93 | d4db8ca |
| Fase 2 — SaxoClient aislado | Pendiente | — | — | — | — |
| Fase 3 — Routing del engine ⚠️ | Pendiente | — | — | — | — |
| Fase 4 — Feed FX + webhooks Forex | Pendiente | — | — | — | — |
| Fase 5 — UI tab Exchange | Pendiente | — | — | — | — |
Fase 1 corrida hoy por el pipeline; Fases 2-5 pendientes.
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Fundación OAuth2 | 2-3 días | columnas cifradas + migración idempotente + helpers de token |
| Fase 2 — SaxoClient aislado | 5-7 días | OpenAPI REST + flujo OAuth2 + resolución de Uic + entorno SIM + normalización (la más densa) |
| Fase 3 — Routing del engine ⚠️ | 3-5 días | toca el path live + rotación de tokens; conservador |
| Fase 4 — Feed FX + webhooks Forex | 2-3 días | feed de precios propio + validación de símbolos Forex |
| Fase 5 — UI tab Exchange Saxo | 2-3 días | conexión OAuth + Forex en el frontend |
| Total estimado | ~14-21 días (≈3-4 semanas) | jornadas de ~8h, un solo ingeniero |
Diseño técnico — Refactorización web SEO estrategias
Resumen del diseño técnico
El proyecto convierte la documentación de estrategias de trading en un catálogo público SEO
completo, gestionado como un generador de sitio estático (SSG). El generador central
(tools/strategy_site/build.py) lee archivos JSON por estrategia en
content/strategies/, aplica plantillas Jinja2 y emite HTML pre-renderizado en
web-public/estrategias/, incluyendo imágenes Open Graph por estrategia (Pillow),
sitemap, robots.txt e index.json consumido por la consola privada.
La captura de leads se implementa como un endpoint público idempotente
(POST /api/newsletter/subscribe) con validación Pydantic, almacenamiento
en SQLite y endpoint admin protegido. Las URLs limpias (/estrategias/<slug>
sin .html) se resuelven en Apache mediante un snippet de rewrite entregado
al operador —sin tocar los vhosts gestionados por Plesk. La regeneración del catálogo
ocurre en una etapa de build Docker independiente para que el HTML publicado
nunca quede desactualizado respecto a los datos. La consola privada elimina la documentación
duplicada y pasa a consumir /estrategias/index.json como fuente única de verdad.
Diagrama de arquitectura
La regeneración del catálogo ocurre en una etapa Docker independiente
(AS strategy-site-builder) con Pillow y Jinja2 instalados solo en build-time;
el artefacto web-public/ se copia a la imagen runtime final con
COPY --from=strategy-site-builder.
Componentes implicados
Generador SSG central: carga _site.json y los JSON de estrategias, genera imágenes OG con Pillow (render_og_image), renderiza detail.html.j2 y list.html.j2, emite index.json, sitemap y robots.txt. URLs canónicas limpias sin .html.
Plantillas Jinja2: detail.html.j2 genera la página individual de estrategia (OG tags, JSON-LD Article + BreadcrumbList, formulario newsletter con handler fetch); list.html.j2 genera el índice del catálogo con JSON-LD CollectionPage.
Helpers CRUD para suscriptores: insert_subscriber (idempotente via ON CONFLICT DO NOTHING), list_subscribers (ordenado más reciente primero), count_subscribers. Todo SQL parametrizado.
POST /api/newsletter/subscribe público: valida con Pydantic (NewsletterSubscribeRequest), normaliza email, almacena via db/newsletter.py, nunca loguea el email a nivel INFO. GET /api/admin/newsletter/subscribers protegido por get_current_admin.
Función render_og_image: crea imágenes 1200×630 con gradiente de fondo, nombre de la estrategia, tagline, pill de status y pie de página de marca. Una imagen por estrategia en web-public/estrategias/assets/og/<slug>.png más una imagen por defecto. Pillow es dependencia solo de build-time.
Snippet de Apache para "Additional Apache directives for HTTPS" en Plesk: redirige 301 las URLs con .html y sirve internamente /estrategias/<slug> mapeando al archivo .html generado. No toca los vhosts gestionados por Plesk.
Índice de metadatos públicos de todas las estrategias (slug, name, status, tagline, timeframe, assets, pills, url limpia). Consumido por la consola privada para renderizar las cards dinámicamente sin duplicar documentación.
Modelo de datos afectado
| Campo | Tipo | Descripción |
|---|---|---|
id | INTEGER PK AUTOINCREMENT | Identificador interno del suscriptor. |
email | TEXT NOT NULL UNIQUE | Dirección de correo normalizada (lowercase, sin espacios). La constraint UNIQUE garantiza la idempotencia. |
source | TEXT (nullable) | Slug de la estrategia o página desde la que se originó la suscripción (ej. rsi-momentum). |
confirmed | INTEGER DEFAULT 0 | Flag de confirmación de doble opt-in. 0 = pendiente, 1 = confirmado. CHECK (0 o 1). |
created_at | TEXT (ISO-8601 UTC) | Timestamp de alta generado por SQLite: strftime('%Y-%m-%dT%H:%M:%SZ', 'now'). |
La tabla se crea con CREATE TABLE IF NOT EXISTS via db.schema.create_all(), que es idempotente. No hay columnas cifradas: el email es dato de contacto, no secreto.
Endpoints a utilizar
| Método | Ruta | Acceso | Descripción |
|---|---|---|---|
| POST | /api/newsletter/subscribe | Público | Recibe {"email": "...", "source": "..."}. Valida con Pydantic, normaliza, inserta idempotente en newsletter_subscribers. Retorna {"status": "ok", "email": "..."}. Email nunca aparece en logs INFO. |
| GET | /api/admin/newsletter/subscribers | Admin | Lista todos los suscriptores (id, email, source, confirmed, created_at) más el total. Requiere sesión con is_admin=1 (get_current_admin). 401 sin sesión, 403 sin admin. |
| Método | Ruta | Tipo | Descripción |
|---|---|---|---|
| GET | /estrategias | Estático | Índice del catálogo de estrategias (index.html generado). JSON-LD CollectionPage, lista de cards con pills. |
| GET | /estrategias/<slug> | Estático | Página de detalle de estrategia (URL limpia, sin .html). OG tags, canonical, JSON-LD Article + BreadcrumbList, formulario newsletter, sección de backtest si aplica. |
| GET | /estrategias/<slug>.html | Redirect | Redirige 301 permanente a /estrategias/<slug> vía Apache rewrite. Evita que páginas ya indexadas por Google den 404. |
| GET | /sitemap.xml | Estático | Sitemap generado por build.py. URLs canónicas limpias (sin .html) para todas las estrategias y el índice. |
| INDEX | /estrategias/index.json | Estático | Metadatos de todas las estrategias consumidos por la consola privada para renderizar las cards dinámicamente (slug, name, status, tagline, pills, url). |
Tests a realizar
tests/test_strategy_site_build.py — build.main() emite un .png por estrategia en assets/og/, canonical y OG URL sin .html, sitemap sin .html, index.json con url limpia que no contiene /web-public.tests/test_strategy_site_build.py — dos ejecuciones consecutivas de build.main() producen archivos con el mismo SHA-256 (sin timestamps ni entropía).tests/test_newsletter.py — create_all() llamado dos veces no falla y la tabla contiene las columnas email, source, confirmed, created_at.tests/test_newsletter.py — POST /api/newsletter/subscribe con email válido retorna 200, crea exactamente una fila con email normalizado y confirmed=0.tests/test_newsletter.py — suscribir el mismo email dos veces (con variaciones de case/espacios) retorna 200 ambas veces y mantiene exactamente una fila.tests/test_newsletter.py — POST /api/newsletter/subscribe con cadena no-email retorna 422 y no persiste ninguna fila.tests/test_newsletter.py — caplog en nivel INFO no contiene la dirección de email enviada al endpoint.tests/test_newsletter.py — sin sesión: 401; con sesión de operador (no admin): 403; con sesión admin: 200 con lista y count correcto.tests/test_console_catalog_links.py — sección #tab-estrategia contiene #strategy-catalog-list; el JS llama fetch("/estrategias/index.json"); no hay marcadores de documentación inline.tests/test_console_catalog_links.py — todos los href="/estrategias/<slug>" en el HTML de la consola apuntan a slugs que existen en content/strategies/*.json, sin .html ni prefijo /web-public.tests/test_console_catalog_links.py — sección #tab-backtest mantiene best-params-container, findings-list y links a equity curves; elimina la prosa estática de datos hardcodeados.Prueba proyecto desarrollado
Cómo verificar de punta a punta que el catálogo SEO, la captura de newsletter y la integración con la consola funcionan correctamente. Cada paso confirma una pieza del flujo de las secciones anteriores.
- Regenerar el catálogo — ejecutar
python tools/strategy_site/build.pydesde la raíz del proyecto; verificar que termina sin errores y emite un.html+.pngOG por cada estrategia enweb-public/estrategias/, másindex.jsonysitemap.xml. - Verificar una página de estrategia — abrir una página generada en el navegador; confirmar que el tag
<link rel="canonical">y elog:urlapuntan a la URL limpia (sin.html), queog:imagereferencia el.pnggenerado y que el JSON-LD parsea sin errores. - Verificar las URLs limpias en producción — con el snippet
deploy/apache-strategy-catalog-rewrites.confaplicado en Plesk, hacercurl -Ia la URL con.htmly confirmar 301 hacia la URL sin.html; luegocurl -Ia la URL limpia y confirmar 200. - Suscribir un email por el formulario — desde la página pública de una estrategia, completar el formulario de newsletter y enviarlo; verificar que el handler
fetchrecibe 200 y muestra el mensaje de éxito. - Verificar la fila en SQLite —
SELECT * FROM newsletter_subscribers;; confirmar que el email está normalizado (lowercase),sourcecontiene el slug yconfirmed = 0. - Verificar el endpoint admin — con sesión admin,
GET /api/admin/newsletter/subscribersretorna el suscriptor y el count correcto. Con sesión de operador (no admin) debe retornar 403. - Verificar la consola privada — el tab Estrategia renderiza las cards desde
/estrategias/index.json, cada card enlaza a la URL pública limpia y no hay documentación inline duplicada. El tab Backtest conserva tablas y equity curves. - Suite automática — correr las pruebas; deben quedar verdes.
python tools/strategy_site/build.py && pytest -q tests/test_strategy_site_build.py tests/test_newsletter.py tests/test_console_catalog_links.py
Criterio de aprobación: catálogo regenerado sin errores, páginas con OG tags y URLs limpias correctas, suscriptor almacenado una sola vez en SQLite, endpoint admin protegido y consola sin documentación duplicada — suite verde.
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🟢 Fase 1 — Completar el contenido del catálogo 8f61494
Complete the public strategy catalog by migrating the full documentation of the three lighter strategies from the Bot Manager console into the data-driven catalog. ADDITIVE: do not change the generator, the JSON schema, or the already-complete EMA Crossover page. Context: tools/strategy_site/build.py renders one public SEO page per strategy from content/strategies/<slug>.json into web-public/estrategias/. content/strategies/_site.json holds shared config. Four strategies exist: ema-crossover-vwap-rsi-adx.json is fully populated; trend-pullback-rsi-atr.json (status pending), supertrend-atr.json and rsi-momentum.json (status research) only have summary-level data. The full authored documentation for these three (overview, indicators with what/how, entry/exit signals, risk cards, backtest, learnings, sources) already lives as hand-written HTML inside web-private/index.html under the Estrategia tab (#tab-estrategia) and Backtest tab (#tab-backtest). Deliver: 1. For each of the three strategies, extract its real content from web-private/index.html and populate the matching content/strategies/<slug>.json following the schema in tools/strategy_site/README.md (indicators[], signals.long/short[], risk[], backtest.columns/rows, learnings[], sources[]). Preserve the honest "research / not profitable intraday" framing for supertrend-atr and rsi-momentum. 2. Keep each strategy seo.title/seo.description accurate to the now-richer content. 3. Re-run python tools/strategy_site/build.py and commit the regenerated web-public/estrategias/ output alongside the data. Constraints: - Do NOT hand-edit any file under web-public/estrategias/ — it is generated; only edit content/strategies/*.json and regenerate. - Do NOT change the EMA Crossover data or any template/CSS/generator code. - Do NOT remove content from web-private/index.html in this phase (de-duplication is Phase 4). Tests: build.py runs clean; each of the three pages contains its indicators, entry/exit conditions and (where applicable) backtest table; JSON-LD still parses; no unrendered Jinja tags remain.
🟢 Fase 2 — Backend de captura de newsletter e3dfdca
Implement the newsletter subscription backend that the public strategy pages already post to. Each strategy detail page renders a newsletter form whose action is /api/newsletter/subscribe (configured in content/strategies/_site.json), but no such endpoint exists yet. Context: FastAPI app in main.py; SQLite via db/ and users.db; encryption via MASTER_KEY/Fernet; static public pages served at the site root / (no auth). The newsletter form submits an email field by POST. Deliver: 1. A POST /api/newsletter/subscribe endpoint accepting an email (validated with Pydantic), storing it in a new newsletter_subscribers table (email UNIQUE, created_at, optional source page, confirmed flag default false) IDEMPOTENTLY — re-subscribing the same email is a success no-op, not a 500. 2. Public, unauthenticated endpoint: reject malformed emails (422), dedupe, and keep emails out of info-level logs. 3. Make the static form UX work end to end: enhance the generator template with a small fetch handler that posts the email and shows a success/error state (template + regenerate), OR a plain server redirect to a thank-you state. Implement whichever you choose fully. 4. An admin-only endpoint to list/count subscribers, reusing the existing is_admin gate (like /api/admin/users). Constraints: - Do NOT touch trading, risk, webhook, or exchange code. - Validate and escape all input; do not introduce IDOR. - Email is contact data, not a secret, but still avoid logging it at info level. Tests: valid email stored once; duplicate email returns success without a duplicate row; invalid email returns 422; admin list requires an admin session.
🟢 Fase 3 — Infra SEO (imágenes OG, URLs limpias, deploy) 7f940ca
Harden the SEO infrastructure of the strategy catalog: per-page social images, clean URLs, and automated regeneration on deploy. Context: tools/strategy_site/build.py generates web-public/estrategias/<slug>.html + index.html + web-public/sitemap.xml + robots.txt. Each page references an og:image (currently a single default that does not exist as a file). The app is deployed via Docker behind an Apache reverse proxy (Plesk/AlmaLinux) at bipbiptrading.com; web-public/ is served by StaticFiles at the site root /. Page URLs currently end in .html. Deliver: 1. Open Graph images: generate a branded 1200x630 image per strategy at build time (e.g. via Pillow) showing the strategy name, tagline and status, written under web-public/estrategias/assets/og/<slug>.png, and wire seo.og_image per page with a default fallback. Pillow is a BUILD-ONLY dependency — keep it out of requirements.txt, like jinja2. 2. Clean URLs: serve /estrategias/<slug> without the .html extension; update canonical, Open Graph and sitemap URLs accordingly. Do NOT hand-edit Plesk-managed vhosts — emit the Apache rewrite snippet for the operator to apply. Keep old .html URLs working via redirect so nothing already indexed 404s. 3. Deploy integration: regenerate the catalog as part of the build/deploy (a step in the Dockerfile or deploy script) so the published HTML never goes stale relative to content/strategies/. Constraints: - Build-only deps stay out of the runtime image where possible. - Do NOT hand-edit Plesk vhosts; output config for the operator. - Do NOT break existing trading/backend behavior. Tests: build emits one OG png per strategy and matching sitemap entries; canonical/OG URLs are consistent with the chosen URL scheme; the build is reproducible.
🟢 Fase 4 — Unificar consola y catálogo 538d38f
Make the Bot Manager console link to the public strategy pages and stop duplicating strategy documentation inline, so there is a single source of truth. Context: The public catalog (content/strategies/*.json -> web-public/estrategias/) now documents each strategy. web-private/index.html STILL contains the full hand-written documentation for each strategy inline under #tab-estrategia and #tab-backtest, plus strategy cards that scroll to those inline sections. This duplicates the catalog content and is the part that does not scale to 100+ strategies. Deliver: 1. In the console Estrategia tab, make the catalog the single source of truth: each strategy card links out to its public page /web-public/estrategias/<slug>.html (new tab), keeping a short summary in the console but removing the long duplicated documentation panels. 2. Drive the console strategy list from the same data where practical (e.g. a small read-only endpoint or a generated JSON index from build.py) so adding a strategy does not require editing index.html. If a fully data-driven list is too big for this phase, at minimum remove the duplicated long-form panels and link out. 3. Keep the Backtest tab results behavior; only de-duplicate the documentation prose. Constraints: - Do NOT break existing tabs, auth, or the Tareas Pendientes section. - Do NOT delete the public catalog content; the console links TO it. - Frontend-focused; only add a read-only data endpoint if needed, with no secrets. Tests: console strategy cards link to the correct public slugs; strategy documentation is not duplicated between console and catalog; tabs still switch correctly.
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Completar catálogo | Completado | — | — | — | 8f61494 |
| Fase 2 — Backend newsletter | Completado | — | — | — | e3dfdca |
| Fase 3 — Infra SEO | Completado | — | — | — | 7f940ca |
| Fase 4 — Unificar consola | Completado | — | — | — | 538d38f |
Ejecutado antes del logging por fase del pipeline; solo se conserva el commit.
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Completar catálogo | 2-3 días | contenido + JSON por estrategia + SSG |
| Fase 2 — Backend newsletter | 1-2 días | endpoint + tabla SQLite + admin + handler del form |
| Fase 3 — Infra SEO | 2-3 días | imágenes OG (Pillow), URLs limpias, stage Docker |
| Fase 4 — Unificar consola/catálogo | 1-2 días | tab data-driven + limpiar documentación inline |
| Total estimado | ~6-10 días | jornadas de ~8h, un solo ingeniero |
Diseño técnico — Generación de estrategias guiada
Resumen del diseño técnico
Hoy el ciclo de I+D genera estrategias de forma ciega: el agente de investigación elige libremente qué estrategia explorar, el backtest está hardcodeado a cuatro pares crypto en 15m/4h/1d (BTC, ETH, SOL, BNB) y no hay forma de orientar la generación hacia una clase de activo, familia o temporalidad concretos. Este proyecto hace la generación GUIADA: el operador elige filtros (asset_type, family, timeframe, nota libre), el pipeline los inyecta en el prompt de investigación y en la plantilla de backtest, y se añade una capa DataProvider que abstrae la fuente de datos OHLCV para que crypto (ccxt/Binance) y no-crypto (yfinance: forex, commodities, acciones) puedan backtestear por el mismo motor. En paralelo, las estrategias forex generadas podrán promoverse a trading en vivo a través de Saxo Bank (depende del proyecto "Integración exchange Saxo Bank"). El cambio es ADITIVO: el path crypto actual queda byte-for-byte equivalente y la ejecución live no se modifica hasta la Fase 4.
Diagrama de arquitectura
En la Fase 4 (depende del proyecto Saxo), las estrategias forex promovidas a live siguen el flujo normal del engine: webhook → UserEngine → factory → SaxoClient → Saxo OpenAPI. Las estrategias commodity y stock permanecen research-only indefinidamente (sin broker).
Componentes implicados
Protocol DataProvider con fetch_ohlcv(symbol, timeframe, since) → DataFrame y un registro que mapea asset_type a la implementación concreta: BinanceProvider (ccxt, crypto) y YFinanceProvider (forex/commodity/stock). Normaliza columnas y zonas horarias antes de devolver el DataFrame.
Archivo temporal escrito por el endpoint con los filtros de la ejecución en curso (asset_type, family, timeframe, note). Lo lee rnd_autonomous.py para construir el prompt parametrizado; se elimina al finalizar el ciclo.
Convertido de texto estático a plantilla: sustituye {{ASSET_TYPE}}, {{FAMILY}}, {{TIMEFRAME}} y {{NOTE}}. Ajusta las fuentes de referencia recomendadas según la clase de activo (QuantifiedStrategies para crypto/acciones; Babypips/TradingView research para forex; EIA/Bloomberg para commodities).
Lee _run_config.json, inyecta los filtros en el prompt template, pasa --config (o args) a rnd_cycle.py. Sigue siendo el proceso de investigación únicamente — no orquesta el ciclo directamente.
Lee symbols y timeframes del brief en vez de los hardcodeados BTC/ETH/SOL/BNB. Implementa la degradación graceful para clases de activo sin proveedor de datos local (flag "pending_data"). Añade el guard de ejecución en progreso (lock file).
Refactorizado para delegar la descarga al DataProvider según asset_type, en vez de llamar directamente a ccxt.binance. El CSV de salida mantiene el mismo schema (open/high/low/close/volume + timestamp) para que el motor de backtest no necesite cambios.
Acepta body Pydantic RndRunRequest con enums asset_type, family, timeframe y note. Escribe _run_config.json, comprueba el guard in-progress y dispara rnd_autonomous.py en segundo plano. Sigue siendo admin-only.
Añade los controles de filtros (asset-type, family, timeframe, nota) antes del botón de investigación. launchRndRun() envía el body JSON. Muestra un hint ámbar en las clases no-crypto hasta que la Fase 3 desbloquea sus backtests.
Modelo de datos afectado
No se crean tablas de base de datos nuevas. El modelo de persistencia del ciclo I+D es basado
en archivos JSON: el brief en content/strategy_queue/<slug>.json y los
resultados en content/rnd_results.json. El cambio amplía los campos del brief para
transportar los filtros desde el endpoint hasta el pipeline y el DataProvider.
| Campo | Tipo | Descripción |
|---|---|---|
asset_type | string enum | "crypto" | "forex" | "commodity" | "stock". Define el DataProvider y el universo de símbolos por defecto. |
symbols | string[] | Universo de activos para el backtest. rnd_cycle.py lo usa en lugar del hardcode BTC/ETH/SOL/BNB. |
timeframes | string[] | Temporalidades a backtestear, p. ej. ["15m","4h","1d"]. |
family | string enum | trend, mean_reversion, breakout, momentum, volatility o any. |
backtest_status | string | "ok" | "pending_data". Sin proveedor de datos para la clase de activo, rnd_cycle.py marca "pending_data" en vez de fallar. |
| Campo | Tipo | Descripción |
|---|---|---|
asset_type | string | Valor del filtro recibido en el body del endpoint. |
family | string | Familia de estrategia seleccionada. |
timeframe | string | Temporalidad preferida o "any". |
note | string? | Nota libre opcional del operador (se inyecta en el prompt de investigación). |
Endpoints a utilizar
| Método | Ruta | Estado | Descripción |
|---|---|---|---|
| POST | /api/rnd/run | Extendido | Acepta body JSON { asset_type, family, timeframe, note } con enums validados por Pydantic. Guard in-progress. Escribe _run_config.json y dispara rnd_autonomous.py en segundo plano. Sin body: defaults crypto / any / any (retro-compatible). |
| GET | /api/rnd/results | Existente | Devuelve content/rnd_results.json. El dashboard I+D lo poll para la tabla de resultados históricos. |
| GET | /api/rnd/status | NUEVO | Devuelve si hay un ciclo en progreso ({ running, config }), leyendo el lock o _run_config.json. Permite mostrar el estado sin esperar a que el proceso termine. |
| Fuente | Origen | Asset type | Descripción |
|---|---|---|---|
| ccxt | Binance OHLCV | crypto | Proveedor existente; refactorizado detrás de BinanceProvider. Pares USDT en el timeframe solicitado. |
| yfinance | Yahoo Finance | forex / commodity / stock | Proveedor nuevo (YFinanceProvider). Mapea símbolos: EURUSD → EURUSD=X, oro → GC=F, petróleo → CL=F, acciones → ticker directo. Dependencia solo de backtest, nunca del bot live. |
Tests a realizar
asset_type inválido → 422; los cuatro valores válidos aceptados. Defaults crypto / any / any sin body.asset_type=crypto produce el mismo brief, símbolos (BTC/ETH/SOL/BNB) y backtest que antes de la refactorización.asset_type=forex produce un brief con el universo forex y backtest_status="pending_data" antes de la Fase 3.rnd_autonomous.py sustituye {{ASSET_TYPE}}, {{FAMILY}}, {{TIMEFRAME}} y {{NOTE}} antes de llamar al agente.BinanceProvider.fetch_ohlcv() tiene el mismo schema (columnas, tipos, UTC) que el CSV previo de download_data.py.fetch_ohlcv("EURUSD","1d",…) y fetch_ohlcv("GC=F","1d",…) devuelven OHLCV en UTC sin NaNs en las filas de trading activo.YFinanceProvider, una ejecución forex produce backtest real y backtest_status pasa de pending_data a ok.Prueba proyecto desarrollado
Cómo verificar de punta a punta que la generación guiada funciona, en orden de fases. Cada paso confirma una pieza del flujo descrito en el diagrama de arquitectura.
- Ciclo crypto sin filtros (regresión) —
POST /api/rnd/runsin body; verificar que produce un brief crypto, backtesta BTC/ETH/SOL/BNB y añade el resultado arnd_results.jsonigual que antes. - Guard in-progress — con un ciclo en curso, un segundo POST devuelve 409 y no lanza otro proceso.
- Ciclo forex con filtros (Fase 1) —
POST /api/rnd/run {"asset_type":"forex","family":"trend","timeframe":"1d"}; verificar que_run_config.jsonse escribe, el prompt menciona forex/trend/1d, el brief tieneasset_type=forexybacktest_status="pending_data". - UI de filtros (Fase 2) — en el tab I+D aparecen los selectores asset-type / family / timeframe / nota; al elegir opciones no-crypto aparece el hint ámbar "datos en preparación".
- DataProvider yfinance (Fase 3) —
python backtest/download_data.py --asset-type forexgenera los CSVs de EURUSD/GBPUSD/USDJPY con el schema habitual; los CSVs crypto se regeneran idénticos. - Backtest forex real (Fase 3) — relanzar el ciclo forex; ahora se ejecuta el backtest completo (sin flag pending_data) y aparece la curva de capital con veredicto honesto.
- Live forex vía Saxo (Fase 4) — con Saxo activo en SIM, promover una estrategia forex a live; al recibir una señal se rutea por
SaxoClienten SIM sin tocar el path crypto; sin Saxo activo, no es desplegable. - Commodity y stock siempre research-only — verificar que nunca aparecen como desplegables, sea cual sea el exchange activo.
- Suite automática — correr las pruebas; deben quedar verdes.
python -m compileall . && pytest -q tests/
Criterio de aprobación: ciclo crypto sin regresión + filtros funcionales + backtests reales para forex/commodity + suite verde. La Fase 4 (live forex) queda condicionada a la aprobación del Product Owner y a que el proyecto Saxo esté mergeado.
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🟢 Fase 1 — Parametrizar la generación (backend + prompt + pipeline) a376912
Make manual strategy generation TUNABLE by asset class, strategy family and timeframe — backend and pipeline only, NO UI yet. Crypto must keep working end-to-end exactly as today.
Context: POST /api/rnd/run (main.py, admin-only) today takes NO body and spawns tools/rnd_autonomous.py with no args. rnd_autonomous.py passes the STATIC file tools/rnd_autonomous_prompt.txt to a `claude -p` research call, which writes a brief to content/strategy_queue/<slug>.json with keys slug, name, brief. Then tools/rnd_cycle.py runs the GGA pipeline; its Phase 1 task template HARDCODES "backtest BTC/ETH/SOL/BNB on 15m, 4h and 1d". The whole backtest path is crypto-only (backtest/ data are 4 Binance USDT pairs).
Deliver:
1. Accept filters on POST /api/rnd/run: a Pydantic body { asset_type: crypto|forex|commodity|stock, family: any|trend|mean_reversion|breakout|momentum|volatility, timeframe: any|15m|1h|4h|1d, note: optional string }. Validate with enums; defaults asset_type=crypto, family=any, timeframe=any. Keep it admin-only. Add a guard that refuses to launch if an R&D run is already in progress (lock file or flag) — today there is none.
2. Pass the filters to rnd_autonomous.py (e.g. write content/strategy_queue/_run_config.json and call it with --config, or pass CLI args).
3. Turn tools/rnd_autonomous_prompt.txt into a TEMPLATE: rnd_autonomous.py injects the chosen asset class, family, timeframe and free-form note into the research prompt, and adjusts the "reputable sources" hint per asset class. The research agent must write the brief with new keys: asset_type, symbols (the default universe for that class), timeframes.
4. In rnd_cycle.py, READ symbols and timeframes from the brief instead of the hardcoded BTC/ETH/SOL/BNB. Default universes per asset_type: crypto = BTC,ETH,SOL,BNB; forex = EURUSD,GBPUSD,USDJPY,AUDUSD; commodity = XAUUSD,WTI; stock = AAPL,MSFT.
5. Graceful degradation: when the asset class has NO local backtest data yet (everything except crypto for now), the pipeline must still generate the brief, the Pine script and the documentation, but mark the backtest step as "PENDIENTE — sin fuente de datos" and flag the strategy as research-only / not tradeable, instead of failing the run.
Constraints:
- Crypto generation must remain byte-for-byte equivalent end-to-end.
- Do NOT touch live trading, the engine, the Risk Manager or the webhook.
- Do NOT build the non-crypto data providers in this phase (next phase) — only the parameterization and the graceful "pending data" path.
- Keep the run admin-only.
Tests: the endpoint validates the enums and rejects bad values; a crypto run produces the same brief/backtest as before; a forex run produces a brief with asset_type=forex and the forex universe and flags the backtest as pending; the in-progress guard blocks a second concurrent launch.
🟢 Fase 2 — UI de filtros en el tab I+D 266b487
Add the asset-class / family / timeframe filters to the "Generación de estrategia manual" card in the I+D tab. FRONTEND ONLY (web-private/: index.html, app.js, style.css). The Phase 1 backend is already in master.
Backend already available (do NOT change it): POST /api/rnd/run now accepts a JSON body { asset_type, family, timeframe, note } with the Phase 1 enums, admin-only.
Deliver (vanilla HTML/CSS/JS, match existing patterns, no new deps):
1. Before the #rnd-run-btn button, add controls: an asset-type selector with the four options crypto / forex / commodity / stock (ALL selectable; show an amber "datos en preparación" hint on the non-crypto ones), a strategy-family selector (any/trend/mean-reversion/breakout/momentum/volatility), a timeframe selector (any/15m/1h/4h/1d, defaulting to 1d), and an optional free-text note field.
2. In launchRndRun() (app.js), send the selected values as the JSON body to POST /api/rnd/run.
3. Reflect in the status line which configuration was launched.
4. UPDATE the "Qué pasa por detrás" step list in the SAME card to reflect the guided flow: the research step now follows the chosen asset_type / family / timeframe (no longer "any strategy"); the backtest universe and timeframes come from the brief instead of the hardcoded BTC/ETH/SOL/BNB; and non-crypto runs are flagged "PENDIENTE — sin fuente de datos". Keep the list accurate to the new behavior.
5. Bump the ?v= query string on app.js and style.css.
Constraints:
- FRONTEND ONLY: do NOT touch Python/backend, the pipeline or trading code.
- HTML-escape any dynamic value shown back to the user.
- Keep behavior intact if the user just clicks the button with defaults (crypto/any/1d).
Tests: the form sends the chosen filters in the request body; defaults are crypto/any/1d; non-crypto options carry the "datos en preparación" hint.
🟢 Fase 3 — Capa de datos multi-activo (DataProvider + yfinance) 29b40a2
Add a data-provider abstraction so the backtest can fetch OHLCV for forex, commodities and stocks, not just crypto. This unlocks REAL backtests for the non-crypto asset classes selected in the R&D filters. Context: today backtest/download_data.py talks directly to ccxt.binance and only knows 4 crypto USDT pairs; backtest/data/ holds only those crypto CSVs. Phase 1 made the pipeline read symbols/timeframes per asset class from the brief and flag non-crypto backtests as "pending data". Deliver: 1. A DataProvider abstraction (a Protocol with fetch_ohlcv(symbol, timeframe, since) returning a DataFrame) and a registry mapping asset_type to a provider: crypto to the existing ccxt.binance source (refactored behind the interface), forex/commodity/stock to a yfinance-based provider (free; map EURUSD to EURUSD=X, gold to GC=F, oil to CL=F, stocks to their tickers). 2. Refactor download_data.py to request data through the provider for the asset class instead of hardcoding Binance, writing CSVs in the SAME on-disk format the backtest already consumes. 3. Remove the "pending data" flag for asset classes that now have a provider, so their backtests run for real through the existing backtest engine. 4. yfinance is a BACKTEST/BUILD-only dependency — keep it out of the runtime requirements used by the live bot, like the existing backtest deps. 5. UPDATE the "Qué pasa por detrás" step list in the I+D "Generación de estrategia manual" card: the backtest step now runs for real on forex/commodity/stock too (drop the "PENDIENTE — sin fuente de datos" wording for the classes that now have a provider). Constraints: - Crypto data path stays equivalent (same symbols, same CSV shape, same results). - Do NOT touch live trading, the engine, the Risk Manager or the webhook — research/backtest infra only. - Normalize timezones and column names so the backtest code does not care which provider produced the data. Tests: the crypto provider returns the same data as before the refactor; the yfinance provider fetches an FX pair and a commodity into the expected CSV shape; a forex R&D run now produces a real backtest instead of a "pending data" flag.
🔴 Fase 4 — Ejecución forex en vivo (depende del proyecto Saxo) ⚠️ alto riesgo
Allow strategies generated for FOREX to be deployed to LIVE trading via Saxo. This phase DEPENDS on the "Integración exchange Saxo Bank" project being merged. Context: R&D can now generate and backtest forex strategies (Phases 1-3). Live execution still only supports crypto (Bitunix/KuCoin); forex execution requires the Saxo client and routing from the Saxo project. Commodities and stocks have no broker and stay research-only. Deliver: 1. Wire a forex strategy's signals through to the Saxo execution path (webhook to FX symbol to Uic to order), reusing the Saxo client and routing. A forex strategy promoted from R&D becomes deployable only when the user has an active Saxo exchange. 2. In the strategy catalog / bot list, mark forex strategies as tradeable-via-Saxo and keep commodity/stock strategies as research-only (no broker). 3. Respect the same risk, dry-run and Product-Owner-approval gates as any live exchange path. 4. UPDATE the "Qué pasa por detrás" step list in the I+D "Generación de estrategia manual" card so its closing/deployment step reflects that a forex strategy can now be promoted to live via Saxo (and commodity/stock stay research-only). Constraints: - Critical Component (live money): do NOT enable forex live trading automatically; Saxo starts in SIM/dry_run and live requires Product Owner approval. - Do NOT regress crypto execution. - If the Saxo project is not yet merged, this phase is BLOCKED; do not stub a fake execution path. Tests: a forex strategy with an active Saxo exchange routes orders through the Saxo client; without an active Saxo exchange it is not deployable; crypto execution is unchanged; commodity/stock strategies are marked research-only.
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Parametrizar generación | CHANGES_REQUESTED · integrada | 🧠→🛠️ · gate ✗ (tmp Windows) | ~17 min | $0.95 | a376912 |
| Fase 2 — UI de filtros | APPROVED | 🧠→🛠️→🔍→✅ | ~9 min | $1.01 | 266b487 |
| Fase 3 — DataProvider multi-activo | APPROVED | 🧠→🛠️→🔍→✅ | ~24 min | $3.10 | 29b40a2 |
| Fase 4 — Forex live ⚠️ | Pendiente (bloqueada por Saxo) | — | — | — | — |
Fases 1-3 corridas hoy por el pipeline; Fase 4 bloqueada por Saxo.
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Parametrizar generación | 2-3 días | body Pydantic + lock + plantilla de prompt + lectura del brief |
| Fase 2 — UI de filtros | 1-2 días | selectores + wiring de launchRndRun |
| Fase 3 — DataProvider multi-activo | 2-3 días | abstracción + adaptador yfinance + paridad crypto + tests |
| Fase 4 — Forex live ⚠️ | 3-5 días | depende de Saxo; toca el path live |
| Total estimado | ~8-13 días | jornadas de ~8h, un solo ingeniero |
Diseño técnico — Migración a multi-exchange
Resumen del diseño técnico
El bot nació con Bitunix hardcodeado en cada capa: credenciales en la tabla
users, firma SHA-256 propia en el cliente HTTP, y la URL base fija en config.py.
Esta migración lo convierte en un sistema multi-exchange sin romper ningún flujo live
existente. El enfoque es ADITIVO: se añade una tabla exchanges por usuario,
un protocolo ExchangePort como contrato entre el engine y cualquier cliente HTTP, y una
factory que despacha al cliente correcto según el tipo activo. Bitunix entra por la misma ruta
de siempre; KuCoin se integra como segundo exchange y arranca forzado a dry_run hasta
aprobación explícita del Product Owner. Una migración idempotente en run_exchanges_migration()
copia las credenciales cifradas del campo legacy users.enc_api_key a la nueva tabla sin
tocar usuarios existentes.
Diagrama de arquitectura
El registry carga el engine por usuario leyendo la fila activa de exchanges
(vía get_active_exchange) y construye el cliente con credenciales ya descifradas en memoria.
Si no hay fila en exchanges, cae al path legacy users.enc_api_key → Bitunix
(backward-compatible). Un índice único garantiza como máximo un exchange activo por usuario.
Componentes implicados
Define ExchangePort como un Protocol de runtime (@runtime_checkable). Contrato de los métodos que el engine consume: place_order, get_position_info, set_leverage, cancel_order, etc. Todo cliente nuevo debe conformar este protocolo.
create_exchange_client() despacha por exchange.type: "bitunix" → BitunixClient; "kucoin" → KuCoinClient (requiere passphrase). Acepta ActiveExchange o ExchangeCredentials. La factory no toca la DB ni get_settings().
Cliente async REST para KuCoin Futures. Auth: HMAC-SHA256 + passphrase firmado también con HMAC (API key v2). Mapea símbolos USDT → notación KuCoin (BTCUSDT ↔ XBTUSDTM). Normaliza respuestas al shape que consume el engine. Arranca con dry_run=True por defecto.
Sin cambios de lógica. Se declara conforme a ExchangePort (class BitunixClient(ExchangePort)) para habilitar isinstance() checks en tests. Firma SHA-256 doble propia de Bitunix, reintentos con backoff.
Crea la tabla exchanges (idempotente) con CREATE TABLE IF NOT EXISTS. Agrega run_exchanges_migration(): copia credenciales cifradas del campo legacy users.enc_api_key/enc_api_secret a la tabla nueva con INSERT OR IGNORE. Migración segura para correr en cada arranque.
Helpers de lectura y escritura: list_exchanges() (solo metadata, nunca ciphertext), get_active_exchange() (descifra en memoria, fail-closed si Fernet falla), upsert_exchange() y set_active_exchange(). SUPPORTED_EXCHANGE_TYPES = {"bitunix","kucoin"}.
El registry lee la fila activa de exchanges primero; si falla la descifración o no existe, cae al path legacy de Bitunix (fail-closed). UserEngine.exchange_client() invoca la factory con las credenciales ya inyectadas. El campo _exchange_type determina qué rama toma la factory.
Tres endpoints nuevos: GET /api/exchanges (lista metadata sin secretos), POST /api/exchanges (valida conectividad, cifra con Fernet y guarda; KuCoin queda forzado a dry_run), POST /api/exchanges/active (activa un exchange y recarga el engine). El endpoint legacy POST /api/keys sigue operativo para Bitunix.
UI del tab Exchange: selector multi-exchange, formulario de credenciales por tipo (passfrase solo para KuCoin), badge dry_run, botón de activación. Nunca persiste secretos en el cliente ni los recibe en respuestas GET.
Modelo de datos afectado
| Campo | Tipo | Descripción |
|---|---|---|
id | INTEGER PK | Clave primaria autoincremental. |
user_id | INTEGER FK | Referencia a users.id con ON DELETE CASCADE. |
type | TEXT CHECK | Tipo de exchange: 'bitunix' o 'kucoin'. CHECK de integridad en DB. |
enc_api_key | TEXT NOT NULL | API key cifrada con Fernet (MASTER_KEY). Nunca aparece en logs ni en respuestas GET. |
enc_api_secret | TEXT NOT NULL | API secret cifrado con Fernet. Descifrado solo en memoria por get_active_exchange(). |
enc_passphrase | TEXT NULL | Passphrase cifrado. Obligatorio para KuCoin; NULL para Bitunix. |
base_url | TEXT NOT NULL | URL base del exchange (https://fapi.bitunix.com / https://api-futures.kucoin.com). |
enabled | INTEGER (0/1) | Permite deshabilitar un exchange sin eliminarlo. |
is_active | INTEGER (0/1) | Exchange activo del usuario. Un índice único parcial garantiza como máximo uno activo por usuario. |
created_at | TEXT (ISO-8601) | Timestamp de creación; default strftime('%Y-%m-%dT%H:%M:%SZ','now'). |
Índices: UNIQUE(user_id, type) impide duplicados por tipo; UNIQUE(user_id) WHERE is_active=1 hace imposible tener dos activos simultáneos. La migración rellena la tabla desde users.enc_api_key con INSERT OR IGNORE — idempotente y sin alterar filas existentes.
Endpoints a utilizar
| Método | Ruta | Uso | Descripción |
|---|---|---|---|
| GET | /api/exchanges | NUEVO | Lista los exchanges configurados del usuario. Solo metadata (tipo, enabled, is_active, has_passphrase). Nunca devuelve secretos. |
| POST | /api/exchanges | NUEVO | Valida conectividad, cifra con Fernet y guarda. KuCoin queda forzado a dry_run; si la prueba es inalcanzable se guarda igualmente en dry_run. |
| POST | /api/exchanges/active | NUEVO | Activa un exchange ya configurado y recarga el engine del usuario. KuCoin siempre activa en dry_run. |
| POST | /api/keys | Legacy | Alta de credenciales Bitunix (endpoint original). Sigue operativo para retro-compatibilidad. |
| INBOUND | /webhook/{token} | Existente | Señales de TradingView; el engine activo rutea la orden al exchange configurado sin cambios en esta capa. |
| Método | Ruta | Uso | Descripción |
|---|---|---|---|
| POST | /api/v1/futures/trade/place_order | Orden | Coloca una orden de futuros con firma SHA-256 doble propia de Bitunix. |
| GET | /api/v1/futures/position/get_pending_positions | Posiciones | Posiciones abiertas del usuario. |
| GET | /api/v1/futures/market/tickers | Precios | Feed de precios; polling cada 10 s para el cache compartido de marks. |
| Método | Ruta | Uso | Descripción |
|---|---|---|---|
| POST | /api/v1/orders | Orden | Orden de futuros; auth HMAC-SHA256 + passphrase firmado (API key v2). Notación de símbolo KuCoin (XBTUSDTM). |
| GET | /api/v1/positions | Posiciones | Posiciones abiertas; normalizadas al shape del engine. |
| DELETE | /api/v1/orders/{orderId} | Cancelar | Cancela una orden pendiente por ID. |
Tests a realizar
run_exchanges_migration() corrida dos veces produce exactamente una fila bitunix; credenciales cifradas copiadas byte a byte desde users.enc_api_key.isinstance(BitunixClient(...), ExchangePort) y isinstance(KuCoinClient(...), ExchangePort) deben ser True; mismas firmas de métodos.create_exchange_client(ActiveExchange(type="bitunix"), ...) devuelve BitunixClient; con type="kucoin" devuelve KuCoinClient; tipo desconocido lanza ValueError.place_order retorna mock sin HTTP real cuando dry_run=True.UserEngineRegistry.load_engine() construye un engine con _exchange_type="kucoin" y exchange_client() devuelve KuCoinClient.Fernet.decrypt() falla para la fila activa, el registry devuelve None (no carga engine) y logea advertencia.GET /api/exchanges no devuelve enc_api_key, enc_api_secret ni enc_passphrase. repr(ActiveExchange) muestra clave enmascarada.exchanges cae al path legacy (users.enc_api_key) y opera con BitunixClient. Usuario con fila bitunix migrada también resuelve a BitunixClient idéntico.is_active=1 para el mismo usuario viola el índice único parcial y lanza IntegrityError.Prueba proyecto desarrollado
Cómo verificar de punta a punta que la migración multi-exchange funciona, con KuCoin siempre en dry_run, sin afectar la operativa live de Bitunix. Cada paso confirma una pieza del flujo de la sección anterior.
- Arrancar y verificar migración automática — correr
python main.py(o Docker); confirmar en los logs querun_exchanges_migrationejecutó sin error y que los usuarios conusers.enc_api_keyya tienen una fila enexchangescontype=bitunix. - Bitunix sin cambio de comportamiento — con un usuario que ya tenía claves Bitunix, disparar una señal por
/webhook/{token}y confirmar que la orden llega afapi.bitunix.comigual que antes. - Configurar KuCoin — en el tab Exchange, agregar un exchange
type=kucoincon api_key, api_secret y passphrase; verificar que la respuesta esok_dry_runy el badge dry_run aparece. - Activar KuCoin —
POST /api/exchanges/active {"type":"kucoin"}; confirmar queGET /api/exchangesmuestra kucoin conis_active=truey Bitunix en false. - Orden de prueba en dry_run — disparar una señal; verificar en logs que pasa por
KuCoinClient, el símbolo se normaliza (BTCUSDT → XBTUSDTM) y NO hay HTTP real (dry_run). - Aislamiento de credenciales —
GET /api/exchangesno incluyeenc_api_key,enc_api_secretnienc_passphrase. - Volver a Bitunix sin regresión — activar Bitunix y repetir una señal; la orden va por
BitunixClientafapi.bitunix.com. - Path legacy — un usuario sin fila en
exchangespero conusers.enc_api_keyse carga enBitunixClientvía fallback. - Suite automática — correr las pruebas; deben quedar verdes.
python -m compileall . && pytest -q tests/
Criterio de aprobación: todos los pasos OK + suite verde. KuCoin permanece en dry_run hasta que el Product Owner habilite el modo live de forma explícita.
Fases pipeline GGA
Las fases que el pipeline GGA ejecutó, en orden:
🟢 Fase 1 — Fundación (modelo de datos + ExchangePort) c351e5d
Build the FOUNDATION for multi-exchange support, WITHOUT changing any current live-trading behavior. Bitunix must keep working exactly as today.
Context: the system is single-exchange. Per-user Bitunix credentials are a single encrypted pair (enc_api_key, enc_api_secret) in the users table (users.db); the only exchange client is exchange/bitunix_client.py (BitunixClient); config.py holds bitunix_base_url. There is no concept of multiple exchanges.
Deliver three ADDITIVE pieces:
1. Data model: per-user storage for N exchanges (e.g. an `exchanges` table) with, per row: user_id, type ("bitunix"|"kucoin"), Fernet-encrypted api_key and api_secret, a NULLABLE encrypted passphrase column (KuCoin needs it), base_url, enabled flag, and exactly one active exchange per user. Add an IDEMPOTENT migration that, for each existing user with enc_api_key/enc_api_secret set, creates a "bitunix" row by COPYING the existing ciphertext (do NOT decrypt/re-encrypt) and marks it active. Keep the old columns for backward compatibility. Running it twice is a no-op.
2. Exchange abstraction: an ExchangePort interface (typing.Protocol or ABC) capturing the async methods the engine calls on BitunixClient (async context manager, place_order, cancel_order, cancel_all_orders, get_all_positions, get_position, get_position_id, get_position_info, get_pending_orders, get_pending_tp_sl_orders, get_history_orders, get_history_positions, set_leverage, get_balance). Make BitunixClient explicitly conform, with NO signature changes.
3. db/ read helpers: list a user's exchanges and fetch the active one, returning decrypted creds only in memory.
HARD CONSTRAINTS (exchange integration is a Critical Component per AGENTS.md):
- Do NOT change order placement, Risk Manager, leverage, stop-loss, take-profit, daily-loss, or webhook auth.
- Do NOT change how the engine builds its exchange client yet (no routing this phase). The live Bitunix path stays equivalent.
- Credentials stay Fernet-encrypted with MASTER_KEY at rest; never plaintext, never logged, never returned by any endpoint.
- The migration must preserve existing Bitunix access (copy ciphertext) and be idempotent.
- Add tests: idempotent migration; an existing single-key user yields one active "bitunix" exchange with identical ciphertext; BitunixClient satisfies ExchangePort.
Do NOT implement KuCoin or any routing in this phase. Only the data model, the abstraction, and the migration.
🟢 Fase 2 — KuCoinClient (aislado) 0a31f25
Implement a KuCoin Futures client for the multi-exchange foundation. This phase is ISOLATED: do NOT wire it into the engine, the registry, or any live path yet. Context: Phase 1 added an ExchangePort interface that exchange/bitunix_client.py BitunixClient conforms to, and an `exchanges` table that can store a "kucoin" exchange with encrypted api_key, api_secret and passphrase. There is no KuCoin client yet. Deliver: 1. A KuCoinClient class (exchange/kucoin_client.py) implementing ExchangePort with the SAME method names and return shapes the engine already consumes from BitunixClient. Where KuCoin's native response differs, NORMALIZE inside KuCoinClient so callers see the same keys the engine expects (symbol, side, size/qty, avgOpenPrice/entryPrice, positionId, etc.). Document each normalization. 2. KuCoin Futures auth: HMAC-SHA256 signing with the API secret over KuCoin's documented prehash string, base64-encoded, plus the API passphrase (encrypted per KuCoin's API key version rules), with the required KC-API-* headers. Centralize signing in one place. 3. dry_run support like BitunixClient: when dry_run, place_order/cancel/set_leverage log and return a mock without hitting KuCoin. 4. Configurable base_url and timeouts; mask the api key in logs; never log secret, passphrase or signing material. CRITICAL: Follow KuCoin's OFFICIAL Futures API docs for the exact endpoints, signing algorithm, passphrase handling, symbol format (e.g. XBTUSDTM) and response shapes. Do NOT invent endpoints or signing. State any uncertain assumption in the implementer report. CONSTRAINTS: - Do NOT modify BitunixClient, the engine, the registry, the Risk Manager, or any live path. This phase only ADDS exchange/kucoin_client.py and its tests. - Credentials/signing material never appear in logs or endpoint responses. Tests: unit tests with mocked HTTP covering the signing headers for a known input, place_order payload (live + dry_run), and normalization of a sample KuCoin positions response into the engine-expected shape. Assert KuCoinClient satisfies ExchangePort.
🟢 Fase 3 — Routing del engine + credenciales por exchange ⚠️ alto riesgo
Wire multi-exchange routing into the engine and add per-exchange credential management. This phase touches the LIVE money path: be conservative and keep existing Bitunix behavior byte-for-byte. Context (Phases 1 and 2 are already MERGED in master): - exchange/port.py defines ExchangePort (runtime_checkable Protocol); both BitunixClient and KuCoinClient conform. - db/exchanges.py: list_exchanges(db_path, user_id) returns ExchangeSummary metadata (no secrets); get_active_exchange(db_path, user_id) returns an ActiveExchange or None, decrypting creds in memory, fail-closed. ActiveExchange has: type, api_key, api_secret, passphrase, base_url. The `exchanges` table has per-user rows (type bitunix or kucoin, encrypted creds + nullable passphrase, base_url, enabled, exactly one active per user). Existing Bitunix users were migrated to one active "bitunix" row. - exchange/kucoin_client.py: KuCoinClient(api_key, api_secret, passphrase, base_url=..., dry_run=...). exchange/bitunix_client.py: BitunixClient(api_key, api_secret, base_url, dry_run). - TODAY: engine/user_engine.py exchange_client() always returns BitunixClient from the engine's stored _api_key/_api_secret/_base_url; engine/registry.py load_engine builds those from the user row's single enc_api_key/enc_api_secret. No routing yet. Deliver: 1. A client factory (one place, e.g. exchange/factory.py) that, given an ActiveExchange (or type + creds), returns the matching ExchangePort: BitunixClient for "bitunix", KuCoinClient (with passphrase) for "kucoin". It owns the per-type constructor differences. 2. Route by the active exchange: UserEngine.exchange_client() and the registry engine-loading must use db/exchanges.get_active_exchange plus the factory instead of hardcoding Bitunix. A migrated Bitunix user MUST route to BitunixClient with identical behavior (same base_url, dry_run, signing). A user with NO row in `exchanges` keeps the legacy single-key Bitunix path so nothing breaks. 3. Endpoints to manage exchanges per user: add/update an exchange's encrypted creds (api_key, api_secret, and passphrase for KuCoin), set the active exchange, and list configured exchanges (never returning secrets). Reuse the connectivity-check-before-enable pattern from POST /api/keys (live auth probe; on auth rejection do not store; on unreachable, store but force dry_run). Reload/swap the engine atomically after a change, like the current key-upload flow. HARD CONSTRAINTS (Critical Component, AGENTS.md, requires Product Owner approval): - Existing Bitunix users MUST keep trading identically (active "bitunix" uses BitunixClient, no behavior change). Users with no exchanges row keep the legacy path. - Do NOT change order placement, Risk Manager, leverage, stop-loss, take-profit, daily-loss, or webhook auth. - Fail-closed: missing or undecryptable active-exchange creds means no engine loaded (as today). dry_run default preserved. - Credentials stay Fernet-encrypted at rest; never plaintext, never logged, never returned by any endpoint. - Do NOT activate live trading for KuCoin automatically; KuCoin engines start in dry_run until connectivity is confirmed. NOTE: exchange/kucoin_client.py still has two endpoints tagged [ASSUMPTION] (/api/v1/stopOrders and /api/v1/history-positions) that MUST be verified against the official KuCoin Futures docs before any KuCoin live trading. Tests: the factory returns the right client per type; routing picks the active exchange; a migrated Bitunix user AND a no-exchanges-row user both still route to BitunixClient identically; adding a KuCoin exchange with a failing probe is not stored / forces dry_run; secrets never appear in endpoint responses.
🟢 Fase 4 — UI del tab Exchange
Restructure the dashboard "Exchange" tab and the account drawer to manage N exchanges (Bitunix + KuCoin) per user. FRONTEND ONLY (web-private/: index.html, app.js, style.css). The Phase 3 backend is already in master.
Backend already available (Phase 3 — do NOT change it):
- GET /api/exchanges returns {"exchanges": [{id, user_id, type, base_url, enabled, is_active, has_passphrase}]} (metadata only, never secrets).
- POST /api/exchanges body {type: "bitunix"|"kucoin", api_key, api_secret, passphrase (required for kucoin), base_url (optional), make_active}. It runs a live connectivity probe: auth rejection returns HTTP 400 (not stored); unreachable returns status "saved_unvalidated" (stored, forced dry_run); a KuCoin success returns status "ok_dry_run" (KuCoin ALWAYS starts dry_run, never auto-live). Returns {status, message, exchanges}.
- POST /api/exchanges/active body {type}: sets the active exchange, returns {status, active_exchange, exchanges}; 404 if that exchange is not configured/enabled.
- Legacy POST /api/keys still works (Bitunix) and now also writes the exchanges table.
- GET /health (polled every 5s, drives the status bar) returns dry_run, prices, daily_pnl, capital for the ACTIVE exchange engine.
Current UI to generalize (today hardcoded to Bitunix):
- #tab-exchange "Exchange Console": status bar (#exch-conn, #exch-mode-badge, price cells, #exch-pnl-val, #exch-capital-val) and a "Ping Bitunix" button (exchangePing() in app.js). The New Order form, Open Positions and Response Log already target the active engine.
- Account drawer: the "Claves de API Bitunix" form (#keys-inp-api-key etc.) posts to /api/keys.
Deliver (vanilla HTML/CSS/JS, match existing patterns, no new deps):
1. Exchange tab: a selector/list of the configured exchanges from GET /api/exchanges showing which is_active, with an action to switch the active one via POST /api/exchanges/active (then refresh /health). The status bar label and the ping button must reflect the ACTIVE exchange dynamically (not the literal "Bitunix"). If KuCoin is active, surface that it is in dry_run pending live approval.
2. Account drawer: generalize the keys form to per-exchange. Let the user pick the type; show a passphrase field ONLY for KuCoin. Submit to POST /api/exchanges (write-only) and surface the returned status (ok / ok_dry_run / saved_unvalidated / auth error). Keep the existing Bitunix /api/keys path working OR route it through /api/exchanges with type=bitunix.
3. A way to add a new exchange (choose type, enter creds + passphrase for KuCoin) and show the connectivity-check result.
4. Bump the ?v= query string on the app.js and style.css tags in index.html so browsers refetch.
CONSTRAINTS:
- FRONTEND ONLY: do NOT touch Python/backend, the webhook, risk, or order code.
- Never persist API keys/secrets/passphrases in client storage; forms are write-only; the GET never returns secrets so never try to display them.
- HTML-escape all dynamic values (exchange type, status, base_url).
- Keep the experience identical for users who only have Bitunix configured.
- Keep index.html balanced (equal div open/close).
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Fundación (ExchangePort) | Completado | — | — | — | c351e5d |
| Fase 2 — KuCoinClient | Completado | — | — | — | 0a31f25 |
| Fase 3 — Routing del engine ⚠️ | Completado | — | — | — | — |
| Fase 4 — UI tab Exchange | Completado | — | — | — | — |
Ejecutado antes del logging por fase del pipeline; solo se conserva el commit.
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Fundación (ExchangePort) | 2-3 días | protocolo + tabla exchanges + migración idempotente |
| Fase 2 — KuCoinClient | 3-4 días | API nueva, firma HMAC+passphrase, mapeo de símbolos, normalización |
| Fase 3 — Routing + credenciales ⚠️ | 3-4 días | toca el path live; fail-closed; sin regresión Bitunix |
| Fase 4 — UI tab Exchange | 2-3 días | frontend multi-exchange + alta de credenciales |
| Total estimado | ~10-14 días | jornadas de ~8h, un solo ingeniero |
Diseño técnico — Refactor Backtester v.2
Documento técnico previo a las 5 fases del pipeline GGA. Define el cómo —arquitectura, componentes, datos y pruebas— en la etapa SDD, antes de ejecutar.
Resumen del diseño técnico
El motor actual (backtest/run_backtest.py y run_bipbip_v2.py) produce resultados optimistas por tres sesgos concretos. Primero, la señal se detecta y la entrada se ejecuta en el close de la MISMA vela, lo que es look-ahead bias: en la realidad no podés operar al precio de cierre en el instante en que la vela cierra. Segundo, BARS_PER_YEAR está hardcodeado para velas de 15m, así que el Sharpe de cualquier timeframe resampleado (1h/4h/1d) está numéricamente mal. Tercero, las salidas SL/TP asumen fill perfecto al precio del stop con cero slippage, ignorando gaps. A esto se suma que optimize_adx busca el mejor umbral sobre todo el dataset y reporta sobre el mismo dataset (in-sample, overfitting) y que el loop de simulación no tiene ni un test. Este refactor corrige los tres sesgos, agrega validación walk-forward y establece una red de tests, extrayendo el cálculo de métricas y el modelo de ejecución a módulos reutilizables.
Diagrama de arquitectura
Componentes implicados
ExecutionModel: fills a la apertura de la vela siguiente (open[i+1]), slippage configurable y penalización gap-through cuando el precio atraviesa el SL/TP.
Cálculo centralizado de métricas: Sharpe anualizado según el timeframe real, Sortino, Calmar, máximo de pérdidas consecutivas, duración media de trade y benchmark buy&hold.
WalkForwardValidator: split anchored/rolling train-test para que la optimización de parámetros no vea los datos de evaluación.
_run pasa a usar ExecutionModel y metrics; optimize_adx se valida con walk-forward en lugar de in-sample.
_run_atr pasa a usar ExecutionModel y metrics; corrige la anualización del Sharpe por timeframe.
Red de tests del loop con casos conocidos: fees, detección de SL/TP, gap-through, equity y métricas.
Aviso cuando yfinance trunca el histórico intradía y chequeo de staleness del CSV cacheado.
Modelo de datos afectado
| Estructura | Campos | Descripción |
|---|---|---|
| Señales (DataFrame) | timestamp, open, high, low, close, volume, signal_long, signal_short, atr? | Contrato de entrada al loop; sin cambios de columnas salvo atr opcional. |
| Trade (dict, extendido) | entry_ts, exit_ts, side, entry_price, exit_price, qty, fees, slippage, pnl_net, bars_held, exit_reason | Se agregan slippage, bars_held y exit_reason (SL/TP/signal). |
| Stats (dict, extendido) | n, win_rate, profit_factor, max_dd, total_pnl, sharpe, sortino, calmar, max_consec_losses, avg_bars_held, benchmark_pnl, alpha | Se agregan sortino, calmar, max_consec_losses, avg_bars_held, benchmark_pnl y alpha. |
Tests a realizar
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🔴 Fase 1 — Fundación: red de tests del loop de simulación
Construir una red de tests de caracterización del motor de backtesting ACTUAL, sin modificar nada del comportamiento existente. El objetivo de esta fase es tener cobertura antes de tocar el motor. Contexto: backtest/run_backtest.py y backtest/run_bipbip_v2.py contienen los loops _run y _run_atr. No existen tests sobre el loop de simulación; las únicas pruebas actuales son sobre la API y la lógica de trading en vivo. El comportamiento del loop (incluyendo el look-ahead bias) debe quedar documentado como comportamiento esperado en esta fase — los tests cambian en la Fase 2 cuando se corrige el sesgo. Entregables: 1. tests/test_backtest_engine.py con al menos los siguientes casos: (a) trade ganador conocido — precio de entrada, salida y fee configurados producen el equity, fees y pnl_net esperados; (b) detección de SL cuando low <= sl; (c) detección de TP cuando high >= tp; (d) verificar que la entrada ocurre en close[i] (comportamiento actual, aunque incorrecto — se documenta y corrige en Fase 2). 2. Ningún cambio en backtest/run_backtest.py ni run_bipbip_v2.py. Verificación de aceptación: pytest tests/test_backtest_engine.py — todos los tests en verde. pytest tests/ — el baseline de 232+ tests sigue verde.
🔴 Fase 2 — Corrección look-ahead: ExecutionModel y fill en open[i+1]
Introducir ExecutionModel y corregir el look-ahead bias moviendo el fill de close[i] a open[i+1]. Contexto: la Fase 1 (MERGEADA) estableció la red de tests con el comportamiento actual. El sesgo principal es que la entrada se ejecuta en el cierre de la misma vela que genera la señal, lo que es imposible en la realidad. Entregables: 1. backtest/execution.py — clase ExecutionModel con método fill_entry(df, i) que retorna el precio de open[i+1] y fill_exit(df, i, sl, tp) que detecta SL/TP en la vela siguiente. 2. Modificar _run en run_backtest.py y _run_atr en run_bipbip_v2.py para usar ExecutionModel en lugar del fill directo en close[i]. 3. Actualizar los valores esperados en tests/test_backtest_engine.py para el comportamiento corregido (el test de entrada ahora aserta open[i+1]). 4. Incluir en el informe del implementador el delta de PnL y Sharpe antes vs. después: se espera que los números BAJEN (el look-ahead inflaba los resultados). Verificación de aceptación: pytest tests/test_backtest_engine.py y pytest tests/ — todos en verde. El informe documenta el delta de resultados.
🔴 Fase 3 — Métricas correctas: Sharpe por timeframe + Sortino/Calmar + benchmark
Extraer el cálculo de métricas a un módulo independiente y corregir la anualización del Sharpe por timeframe. Contexto: fases 1 y 2 MERGEADAS. BARS_PER_YEAR está hardcodeado para velas de 15m (26280) en ambos archivos. Cualquier backtest en 1h, 4h o 1d usa el factor incorrecto y produce un Sharpe numéricamente erróneo. Entregables: 1. backtest/metrics.py — función compute_stats(trades, equity_curve, timeframe) que recibe la lista de trades y la curva de equity y retorna el dict de estadísticas: n, win_rate, profit_factor, max_dd, total_pnl, sharpe (anualizado por timeframe), sortino, calmar, max_consec_losses, avg_bars_held, benchmark_pnl, alpha. El timeframe determina BARS_PER_YEAR: 15m→26280, 1h→6570, 4h→1642, 1d→365. 2. Modificar run_backtest.py y run_bipbip_v2.py para delegar el cálculo de estadísticas a metrics.compute_stats(). 3. Agregar tests paramétricos en test_backtest_engine.py verificando el BARS_PER_YEAR correcto para cada timeframe y que benchmark_pnl usa la misma ventana que la estrategia. Verificación de aceptación: pytest tests/ — todos en verde. El Sharpe de un backtest en 4h ya no usa el factor de 15m.
🔴 Fase 4 — Realismo de ejecución: slippage configurable + penalización gap-through
Agregar slippage configurable y penalización gap-through en ExecutionModel para que el modelo de ejecución sea realista. Contexto: fases 1, 2 y 3 MERGEADAS. ExecutionModel ya mueve el fill a open[i+1]. Faltan dos casos de realismo: (a) slippage: cada fill (entrada y salida) tiene un coste adicional configurable en puntos básicos; (b) gap-through: si la vela abre por debajo del SL (largo) o por encima del TP (corto), el fill ocurre al open de esa vela —no al precio del stop—, modelando el deslizamiento en gaps overnight. Entregables: 1. Extender ExecutionModel para aceptar slippage_bps (float, default 0) y aplicarlo a cada fill: entry_price *= (1 + slippage_bps/10000) para longs, (1 - ...) para shorts. 2. En fill_exit(), detectar gap-through: si open[i] < sl para un long, el fill de salida es open[i], no sl. 3. Agregar tests en test_backtest_engine.py: (a) fill de entrada con slippage resulta en peor precio; (b) gap-through retorna open como precio de salida en lugar del SL. Verificación de aceptación: pytest tests/ — todos en verde. Los casos de slippage y gap-through cubiertos por tests.
🔴 Fase 5 — Validación robusta: walk-forward y optimización fuera de muestra
Implementar walk-forward validation y reescribir optimize_adx para que la optimización de parámetros sea fuera de muestra. Contexto: fases 1–4 MERGEADAS. optimize_adx en run_backtest.py busca el mejor umbral ADX sobre TODO el dataset y reporta métricas sobre ese mismo dataset (in-sample). Esto es overfitting: los parámetros están ajustados a los mismos datos con los que se evalúa. Walk-forward separa el dataset en ventanas train/test para que la evaluación siempre sea fuera de muestra. Entregables: 1. backtest/validation.py — clase WalkForwardValidator con métodos splits_anchored(n_splits) y splits_rolling(train_size, test_size) que generan índices de ventanas train/test disjuntos sobre el DataFrame de señales. 2. Reescribir optimize_adx para usar WalkForwardValidator: optimizar en el set train de cada split y reportar métricas en el set test (out-of-sample). El resultado final agrega los resultados out-of-sample de todos los splits. 3. Agregar tests en test_backtest_engine.py: (a) los splits de walk-forward son disjuntos (train y test no se solapan); (b) los parámetros óptimos obtenidos en train se evalúan en un set de test nunca visto. Verificación de aceptación: pytest tests/ — todos en verde (232+ tests del baseline incluidos). optimize_adx reporta métricas out-of-sample. El informe documenta la diferencia entre los parámetros in-sample y out-of-sample.
Prueba proyecto desarrollado
- pytest tests/test_backtest_engine.py — toda la red de tests en verde.
- python -m backtest.run_backtest — comparar Sharpe y PnL antes vs. después: se espera que BAJEN (los números viejos eran optimistas; eso es correcto).
- Verificar la anualización por timeframe — correr el backtest en 4h y comprobar que el Sharpe ya no usa el factor de anualización de 15m.
- Correr la optimización con walk-forward — comparar los parámetros in-sample vs. out-of-sample e inspeccionar que las métricas reportadas son las del set de test.
- pytest tests/ — el baseline de 232+ tests sigue verde.
pytest tests/test_backtest_engine.py && python -m backtest.run_backtest && pytest tests/
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Red de tests (fundación) | Pendiente | — | — | — | — |
| Fase 2 — Corrección look-ahead | Pendiente | — | — | — | — |
| Fase 3 — Métricas correctas | Pendiente | — | — | — | — |
| Fase 4 — Slippage + gap-through | Pendiente | — | — | — | — |
| Fase 5 — Walk-forward | Pendiente | — | — | — | — |
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Red de tests (fundación) | ~1 día | Tests sobre código no testeado; caracterizar el comportamiento actual. |
| Fase 2 — Corrección look-ahead | ~0.5 día | Corrección puntual del fill + reajuste de los valores esperados. |
| Fase 3 — Métricas correctas | ~1 día | Métricas nuevas y cálculo del benchmark. |
| Fase 4 — Slippage + gap-through | ~1 día | Modelo de slippage realista y casos de gap. |
| Fase 5 — Walk-forward | ~1.5 días | Walk-forward y reescritura de la optimización. |
| Total estimado | ~5 días | Estimación de desarrollo humano sin pipeline. |
Diseño técnico — Integración Feed Noticias v.1
Documento técnico previo a las 3 fases del pipeline GGA. Define el cómo —arquitectura, componentes, datos, endpoints y pruebas— antes de ejecutar el pipeline.
Resumen del diseño técnico
La integración agrega un feed de noticias en español sobre los activos que opera el bot
(BTC, ETH, SOL, BNB y pares FX) sin requerir API key ni traducción. Las fuentes son RSS nativos en
español: Cointelegraph es, CriptoNoticias y Bit2Me News para cripto; FXStreet español e Investing.com
español para forex/mercados. El cambio es ADITIVO: un módulo news/feeds.py
nuevo implementa la obtención, normalización y caché en memoria; un loop de refresco en segundo plano
(modelado sobre _price_feed_loop) mantiene el caché actualizado; un endpoint autenticado
GET /api/news sirve el caché filtrable; y un panel en web-private lo presenta
por activo. No se crea ninguna tabla de base de datos en v.1 (todo en memoria). El path de trading live,
el webhook y el Risk Manager no se modifican en ninguna fase.
Diagrama de arquitectura
El loop de refresco se registra en el lifespan de FastAPI como
asyncio.create_task, exactamente igual que _price_feed_loop. Un feed que
falla es logueado y omitido; los demás continúan. En v.2 (fuera de alcance) se añadirán
Finnhub, análisis de sentimiento, traducción y alimentación al brief de I+D.
Componentes implicados
Módulo central de noticias. Declara la configuración de fuentes RSS, la función de fetch+parse con feedparser, la normalización a NewsItem (title, summary, url, source, published_at, assets[]), deduplicación por url, mapeo de ítems a activos por keyword, caché en memoria (_news_cache) y el loop de refresco asíncrono (_news_feed_loop).
Registra asyncio.create_task(_news_feed_loop()) en el lifespan (junto a price_task). Agrega el endpoint autenticado GET /api/news que lee el caché y aplica filtros de query. Sin cambios en webhook, Risk Manager ni orden de operativa.
Agrega feedparser>=6.0 como nueva dependencia de runtime. Es la única dependencia nueva: no requiere API key, parse RSS/Atom puro, sin compilación C. httpx ya está presente.
Panel de noticias en la consola privada: sección o tab que hace fetch("/api/news") (con token de sesión), muestra ítems agrupados por activo/clase, cada uno con título, fuente y enlace al artículo original (HTML-escaped). Bump ?v=.
Intervalo de refresco (NEWS_REFRESH_INTERVAL_SEC, default 10 min) y lista de fuentes RSS (RSS_SOURCES) declaradas como constantes en news/feeds.py. Sobreescribibles vía variable de entorno si se desea. No requiere cambios en .env para arrancar.
Modelo de datos afectado
No se crea ninguna tabla nueva en v.1. El almacenamiento es un list[NewsItem]
en memoria, volátil: se pierde al reiniciar el proceso y se reconstruye en el primer ciclo de refresco
(~segundos). Esto es suficiente para el panel informativo — las noticias tienen una vida útil corta.
| Campo | Tipo Python | Descripción |
|---|---|---|
title | str | Título del artículo tal como viene en el feed RSS. |
summary | str | Resumen o descripción breve del artículo (puede estar vacío). |
url | str | URL canónica del artículo; es la clave de deduplicación. |
source | str | Nombre legible de la fuente (ej. "Cointelegraph ES"). |
published_at | str (ISO-8601) | Fecha de publicación normalizada a UTC en formato ISO-8601. |
assets | list[str] | Activos mapeados por keyword (ej. ["BTC", "ETH"]). Lista vacía si el ítem es de mercado general. |
v.2 (fuera de alcance): si se requiere persistencia o historial, se podría añadir una tabla
news_items con los mismos campos más id (INTEGER PK), sentiment
(REAL, nullable) y fetched_at (TEXT ISO-8601). Pero en v.1 la caché volátil es la
decisión correcta para evitar complejidad innecesaria.
Endpoints a utilizar
| Método | Ruta | Estado | Descripción |
|---|---|---|---|
| GET | /api/news | NUEVO | Autenticado (Depends(get_current_user)). Query params: asset (ej. BTC), asset_class (crypto|forex), limit (default 50). Sirve solo el caché; nunca bloquea en red. Retorna {"items": [...], "total": N, "cached_at": "..."}. |
| GET | /api/news/sources | Opcional | Autenticado. Devuelve la lista de fuentes configuradas con su estado (última actualización, número de ítems, error si lo hay). Útil para diagnosticar feeds caídos desde el panel. |
| Tipo | URL del feed | Clase | Descripción |
|---|---|---|---|
| RSS | es.cointelegraph.com/rss | Cripto | Cointelegraph en Español — noticias de Bitcoin, Ethereum y mercado cripto en general. |
| RSS | criptonoticias.com/feed/ | Cripto | CriptoNoticias — cobertura hispana de cripto con énfasis en BTC, ETH, altcoins y regulación. |
| RSS | news.bit2me.com/feed | Cripto | Bit2Me News — análisis y noticias del mercado cripto hispanohablante. |
| RSS | fxstreet.es (feed RSS) | Forex | FXStreet en español — análisis y noticias de pares de divisas (EUR/USD, GBP/USD, etc.). |
| RSS | es.investing.com (feed RSS) | Mercados | Investing.com España — noticias de mercados financieros, divisas y materias primas. |
Tests a realizar
tests/test_news_feeds.py — parsear un XML RSS de muestra (fixture en tests/fixtures/sample_feed.xml) produce la lista de NewsItem con todos los campos correctos (url, source, published_at ISO-8601, assets).tests/test_news_feeds.py — añadir el mismo URL dos veces al caché produce un único ítem; la segunda llamada de merge no duplica.tests/test_news_feeds.py — un ítem con "Bitcoin" en el título se mapea a assets=["BTC"]; uno con "Ethereum" y "Solana" se mapea a ["ETH", "SOL"]; uno genérico queda con assets=[].tests/test_news_feeds.py — mockar un feed para que lance httpx.RequestError; los ítems de los otros feeds siguen presentes en el caché y no se lanza excepción no capturada.tests/test_news_api.py — con caché poblado con ítems de BTC y ETH, GET /api/news?asset=BTC retorna solo ítems donde assets contiene "BTC".tests/test_news_api.py — GET /api/news?asset_class=crypto retorna ítems de fuentes cripto; asset_class=forex retorna ítems de fuentes forex.tests/test_news_api.py — con 100 ítems en caché, GET /api/news?limit=10 retorna exactamente 10 ítems.tests/test_news_api.py — sin token de sesión retorna 401; con sesión válida retorna 200 con la estructura {"items": [...], "total": N, "cached_at": "..."}.tests/test_news_api.py — mockar news/feeds.py:_news_cache directamente; verificar que el endpoint no llama a ninguna URL externa durante la petición.Prueba proyecto desarrollado
Cómo verificar de punta a punta que la integración de noticias funciona, desde el arranque del proceso hasta que los ítems aparecen en el panel del dashboard. Cada paso confirma una pieza del flujo descrito en las secciones anteriores.
- Instalar la dependencia nueva — verificar que
feedparser>=6.0está enrequirements.txty ejecutarpip install -r requirements.txtsin errores. - Arrancar la aplicación —
uvicorn main:app --reload; verificar en los logs que el loop_news_feed_looparranca junto al_price_feed_loopsin trazas de error. - Esperar el primer ciclo de refresco — tras ~10-30 segundos, los logs deben mostrar el número de ítems descargados por fuente; verificar que un feed caído (si lo hay) solo produce un WARNING y no detiene el proceso.
- Verificar el endpoint con curl —
GET /api/newscon una cookie/token de sesión válida retorna 200 con la estructura{"items":[...], "total": N, "cached_at":"..."}yN > 0. - Verificar el filtrado por activo —
GET /api/news?asset=BTC&limit=5retorna como máximo 5 ítems todos con"BTC"en el campoassets. - Verificar el filtrado por clase —
GET /api/news?asset_class=forexretorna solo ítems de las fuentes FXStreet e Investing.com. - Verificar el panel en el dashboard — abrir la consola privada, navegar al panel de noticias; los ítems se renderizan con título, fuente y enlace al artículo; todos los valores dinámicos están HTML-escaped; el enlace abre en nueva pestaña.
- Verificar la autenticación — hacer logout y repetir
GET /api/news; debe retornar 401. - Suite automática — correr las pruebas; deben quedar verdes.
python -m compileall . && pytest -q tests/test_news_feeds.py tests/test_news_api.py
Criterio de aprobación: loop de noticias arranca sin errores, al menos un feed retorna ítems, el endpoint filtra correctamente, el panel los muestra HTML-escaped, la autenticación funciona y la suite queda verde.
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🔴 Fase 1 — Agregador de feeds RSS (backend)
Build the news feed aggregator backend. ADDITIVE: do NOT touch live trading, risk, webhook, order placement, or any exchange client.
Context: FastAPI app in main.py uses an asynccontextmanager lifespan that registers background tasks with asyncio.create_task (see _price_feed_loop for the exact pattern). httpx is already a runtime dependency. feedparser is not yet installed.
Deliver:
1. news/feeds.py — new module containing:
a. RSS_SOURCES: list of source config dicts, one per feed: { name, url, asset_class ("crypto"|"forex"|"markets") }. Sources: Cointelegraph ES (es.cointelegraph.com/rss), CriptoNoticias (criptonoticias.com/feed/), Bit2Me News (news.bit2me.com/feed), FXStreet ES (fxstreet.es RSS), Investing.com ES (es.investing.com RSS).
b. NewsItem: a dataclass or TypedDict with fields: title (str), summary (str), url (str), source (str), published_at (str, ISO-8601 UTC), assets (list[str]).
c. fetch_and_parse(source: dict) -> list[NewsItem]: uses httpx.AsyncClient to GET the feed URL, passes the response body to feedparser.parse(), normalizes each entry to NewsItem (title, summary from entry.summary or entry.description, url from entry.link, source name, published_at as ISO-8601 UTC string). Returns an empty list on any exception, logging a WARNING with the source name and error — never propagates.
d. map_assets(item: NewsItem) -> list[str]: keyword-based mapping of title+summary to asset symbols: BTC/Bitcoin, ETH/Ethereum, SOL/Solana, BNB, and common FX pairs (EURUSD/EUR/USD, GBPUSD/GBP, etc.). Returns [] for general-market items.
e. _news_cache: list[NewsItem] = [] — module-level in-memory cache.
f. _news_feed_loop(): async function that runs forever, calling fetch_and_parse for each source, deduplicating by url against the existing cache, appending new items, and sleeping NEWS_REFRESH_INTERVAL_SEC (default 600) between cycles. A failing source is skipped; the loop never exits on error.
2. Add feedparser>=6.0 to requirements.txt.
3. Wire _news_feed_loop into the FastAPI lifespan in main.py: asyncio.create_task(_news_feed_loop()) alongside price_task, inside the existing lifespan context manager. Import from news.feeds.
Constraints:
- Do NOT touch trading, risk, webhook, engine, exchange, or DB code.
- A failing feed must not break the others or stop the loop.
- No API key, no translation.
- Secrets never logged.
Tests: tests/test_news_feeds.py with a sample RSS XML fixture (tests/fixtures/sample_feed.xml): fetch_and_parse returns correct NewsItem fields; duplicate URLs are not added to the cache after a second merge; map_assets maps "Bitcoin" to ["BTC"] and a generic headline to []; a mocked httpx error on one source does not raise and returns [].
🔴 Fase 2 — Endpoint GET /api/news
Add the authenticated GET /api/news endpoint that serves the in-memory news cache. ADDITIVE: do NOT touch trading, risk, webhook, or exchange code.
Context: Phase 1 is MERGED. news/feeds.py exposes _news_cache (list[NewsItem]) and _news_feed_loop. main.py already uses Depends(get_current_user) for authenticated endpoints (see existing /api/exchanges, /api/positions, etc. for the exact pattern). The cache is populated by the background loop; the endpoint must never make a network call itself.
Deliver:
1. GET /api/news in main.py: authenticated with Depends(get_current_user). Query parameters:
- asset: optional str — filter items where the asset symbol appears in item.assets (case-insensitive).
- asset_class: optional str ("crypto"|"forex"|"markets") — filter items where item.source belongs to sources of that asset_class.
- limit: optional int, default 50, max 200 — truncate the result list.
Response: {"items": [...serialized NewsItem dicts...], "total": int, "cached_at": str|null} where cached_at is the ISO-8601 timestamp of the last cache refresh (add a module-level _last_refresh_at variable to news/feeds.py).
2. The endpoint reads _news_cache directly (or via a thin getter function); it performs NO awaits on network calls. It must return in microseconds if the cache is empty.
3. Pydantic response model (or inline dict) — field names match NewsItem: title, summary, url, source, published_at, assets.
Constraints:
- Authenticated: 401 without a valid session.
- Never blocks on network.
- Do NOT change _news_feed_loop, the lifespan wiring, trading, risk, or webhook.
Tests: tests/test_news_api.py — mock _news_cache with a list of known NewsItem fixtures:
- GET /api/news without auth → 401.
- GET /api/news with valid session → 200, correct structure with "items", "total", "cached_at".
- GET /api/news?asset=BTC → only items with "BTC" in assets.
- GET /api/news?asset_class=crypto → only items from crypto sources.
- GET /api/news?limit=3 → at most 3 items.
- Endpoint does not call any external URL during the request (assert no httpx call).
🔴 Fase 3 — Panel de noticias en el dashboard
Add a news panel/tab to the private dashboard (web-private/) that polls GET /api/news and renders items per asset. FRONTEND ONLY: do NOT touch any Python/backend, risk, webhook, or exchange code.
Context: Phases 1 and 2 are MERGED. GET /api/news is available, authenticated, and returns {"items":[{title,summary,url,source,published_at,assets},...], "total":N, "cached_at":"..."}. The dashboard (web-private/index.html + app.js) uses vanilla HTML/CSS/JS with session-cookie auth; all dynamic values must be HTML-escaped before insertion into the DOM.
Deliver (vanilla HTML/CSS/JS, match existing patterns, no new deps):
1. A news section or tab in web-private/index.html: a panel that shows news items grouped by asset/class. Each item displays: title (linked to source article, target="_blank", rel="noopener", HTML-escaped), source name (HTML-escaped), and published_at formatted as a human-readable relative date or short date.
2. In app.js (or an inline script): a loadNews(asset, assetClass) function that calls fetch("/api/news?limit=50") (or with filters), handles 401 (redirect to login), parses the JSON, and renders items into the panel using DOM methods or safe innerHTML (all dynamic values HTML-escaped). Poll or refresh on tab open.
3. A simple asset/class filter control (e.g. a button group or select) above the list so the user can switch between All, Crypto, Forex, or a specific asset.
4. Empty state message when items=[] or cache not yet populated.
5. Bump the ?v= query string on index.html stylesheet link and any app.js references so browsers refetch.
Constraints:
- FRONTEND ONLY: do NOT modify main.py, news/feeds.py, requirements.txt, or any Python file.
- HTML-escape ALL dynamic values (title, summary, source, url) before DOM insertion — never use raw innerHTML with API data.
- Links to source articles open in a new tab with rel="noopener".
- Keep all existing tabs and auth behavior identical.
- Keep index.html balanced (equal div open/close counts).
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Agregador RSS (backend) | Pendiente | — | — | — | — |
| Fase 2 — Endpoint GET /api/news | Pendiente | — | — | — | — |
| Fase 3 — Panel de noticias (frontend) | Pendiente | — | — | — | — |
Ninguna fase ejecutada aún; el pipeline arrancará una vez aprobado este diseño.
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer (el rol del agente Implementer) implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Agregador RSS (backend) | 2-3 días | news/feeds.py + feedparser + loop de refresco + fixture de tests + wiring en lifespan |
| Fase 2 — Endpoint GET /api/news | 1-2 días | endpoint FastAPI + filtros + modelo Pydantic + tests de auth y filtrado |
| Fase 3 — Panel de noticias (frontend) | 1-2 días | HTML/CSS/JS + filtros de activo + HTML-escape + bump ?v= |
| Total estimado | ~4-7 días | jornadas de ~8h, un solo ingeniero |
Diseño técnico — Catálogo de Indicadores v.1
Documento técnico previo a las 5 fases del pipeline GGA. Define el cómo —arquitectura, componentes, datos y pruebas— en la etapa SDD, antes de ejecutar.
Resumen del diseño técnico
El sistema de Estrategias tiene cuatro capas: datos editoriales en content/strategies/*.json, un generador SSG en tools/strategy_site/build.py (Jinja2 + Pillow), salida estática en web-public/estrategias/ con un index.json legible por máquina, y un tab en la consola que pinta cards consumiendo ese index.json. Para Indicadores se replican las cuatro capas, pero en lugar de clonar el builder (lo que dejaría dos copias casi idénticas de ~400 líneas más templates y tests), se generaliza el motor a un catálogo parametrizado por entidad: una sola base de código que produce tanto /estrategias/ como /indicadores/. El index.json generado sigue siendo la única fuente de las cards de la consola (cero datos hardcodeados), y el ruteo de URLs limpias (/indicadores/<slug>) replica el middleware ya existente para estrategias. Las páginas se generan con noindex,nofollow mientras el sitio está en desarrollo, heredándolo desde los templates.
Diagrama de arquitectura
Componentes implicados
Motor SSG genérico parametrizado por entidad (entity, data_dir, out_dir, base_path); reemplaza a strategy_site/build.py.
Templates list/detail parametrizados por entidad (strings Estrategia/Indicador); nacen con noindex,nofollow.
Estrategias pasa a invocar el motor genérico como una instancia (config de entidad); la interfaz pública no cambia.
10 fichas de indicadores (EMA, VWAP, RSI, ADX, ATR, Bollinger, Supertrend, Donchian, Volume MA, SMA) + _site.json con base_path /indicadores.
Catálogo público generado (index.html, fichas, index.json, sitemap); servido en /indicadores/.
Middleware para servir /indicadores/<slug> con URL limpia; espejo del middleware de estrategias.
Reglas RewriteRule para las URLs limpias de /indicadores/ en producción (Apache/Plesk).
Botón de nav + sección tab-indicadores.
loadIndicatorCatalog() que consume /indicadores/index.json y pinta las cards.
Tests parametrizados del motor para ambas entidades (regresión Estrategias + generación Indicadores).
Tests del middleware de URLs limpias de indicadores (200 HTML, 404 slug inexistente, index.json no interceptado).
Modelo de datos afectado
| Estructura | Campos | Descripción |
|---|---|---|
| Indicador (JSON) | slug, status, name, tagline, updated, seo{title,description,keywords,og_image}, meta{category,default_params,timeframe}, formula, what, how, used_in_strategies[], pills[], overview, sources[] | Ficha editorial de un indicador; espejo del schema de estrategia con campos propios (fórmula, parámetros por defecto, categoría, y enlace cruzado a las estrategias que lo usan). |
| _site.json (Indicadores) | site_name, base_url, base_path=/indicadores, list_page{title,heading,intro}, brand, newsletter | Config del catálogo de indicadores; mismo schema que el de estrategias con base_path propio. |
| index.json (generado) | [{slug, name, status, tagline, category, pills, updated, url}] | Índice legible por máquina que alimenta las cards de la consola; generado por el motor, nunca a mano. |
Tests a realizar
Fases pipeline GGA
Las fases derivadas de este diseño, que el pipeline GGA ejecuta en orden (mergeando cada una antes de la siguiente):
🔴 Fase 1 — Generalizar el motor SSG
Extraer tools/strategy_site/build.py a tools/catalog_site/build.py como motor SSG parametrizado por entidad. La interfaz del motor acepta: entity (str), data_dir (Path), out_dir (Path), base_path (str). Estrategias se reescribe como instancia del motor (mismos argumentos que hoy, misma salida). Los templates Jinja2 y las imágenes OG se mueven a tools/catalog_site/templates/ y se parametrizan para usar el nombre de la entidad en vez de "Estrategia" hardcodeado. Entregables: 1. tools/catalog_site/build.py — motor genérico parametrizado. Produce para una entidad dada: index.html de catálogo, fichas individuales, index.json, sitemap.xml e imágenes OG (Pillow). Los templates nacen con <meta name="robots" content="noindex,nofollow"> por defecto. 2. tools/strategy_site/build.py — reescrito para invocar el motor genérico con la configuración de Estrategias; la interfaz de línea de comandos (python tools/strategy_site/build.py) no cambia. 3. Los tests existentes de strategy_site siguen verdes como red de regresión (ningún artefacto de Estrategias cambia). Verificación de aceptación: pytest tests/ — todos en verde. python tools/strategy_site/build.py produce el mismo output que antes (sitemap, OG, index.json, canonical).
🔴 Fase 2 — Capa de datos de Indicadores
Crear la capa de datos editoriales para el catálogo de Indicadores. Contexto: la Fase 1 (MERGEADA) generalizó el motor SSG. Esta fase entrega los datos de entrada para el catálogo de Indicadores. Entregables: 1. content/indicators/_site.json — config del catálogo de indicadores: site_name, base_url, base_path=/indicadores, list_page (title, heading, intro), brand, newsletter. Mismo schema que content/strategies/_site.json con base_path propio. 2. 10 fichas JSON en content/indicators/: ema.json, vwap.json, rsi.json, adx.json, atr.json, bollinger.json, supertrend.json, donchian.json, volume-ma.json, sma.json. Cada ficha incluye: slug, status=published, name, tagline, updated (hoy), seo (title, description, keywords, og_image), meta (category, default_params, timeframe), formula (expresión matemática breve), what (qué mide), how (cómo interpretarlo), used_in_strategies[] (lista de slugs de estrategias que lo usan, derivada de content/strategies/*.json), pills[], overview, sources[]. Contenido derivado de backtest/strategy.py y de los bloques indicators[] de cada estrategia existente. Verificación de aceptación: python tools/catalog_site/build.py --entity indicadores produce web-public/indicadores/ con 10 fichas HTML, index.json con 10 entradas y sitemap.xml. pytest tests/ — todos en verde.
🔴 Fase 3 — Generar y servir el catálogo
Generar el catálogo estático de Indicadores, agregar el middleware en main.py para URLs limpias y crear la regla Apache. Contexto: fases 1 y 2 MERGEADAS. web-public/indicadores/ existe con las 10 fichas. Entregables: 1. Verificar que web-public/indicadores/ generado contiene meta robots noindex,nofollow en cada ficha. 2. main.py — middleware para /indicadores/<slug>: intercepta rutas sin extensión bajo /indicadores/, busca web-public/indicadores/<slug>/index.html y lo sirve con Content-Type text/html. Archivos con extensión (.json, .css, .js, .png, .xml) se sirven directamente sin pasar por el middleware. Ruta inexistente retorna 404. Espejo exacto del middleware de estrategias. 3. deploy/apache-indicator-catalog-rewrites.conf — reglas RewriteRule para /indicadores/<slug> en producción (Apache/Plesk), análogas a las de estrategias. Verificación de aceptación: GET /indicadores/rsi → 200 HTML con noindex,nofollow. GET /indicadores/index.json → 200 JSON sin ser interceptado. GET /indicadores/inexistente → 404. pytest tests/ — todos en verde.
🔴 Fase 4 — Tab Indicadores en la consola
Agregar el tab de Indicadores en la consola privada (web-private/). FRONTEND ONLY: no tocar ningún archivo Python. Contexto: fases 1–3 MERGEADAS. /indicadores/index.json está disponible y devuelve [{slug, name, status, tagline, category, pills, updated, url}].
Entregables:
1. web-private/index.html — botón de nav para el tab Indicadores (mismo estilo que el tab Estrategia) y sección tab-indicadores con la estructura de cards, estado vacío y contador de indicadores cargados.
2. web-private/app.js — función loadIndicatorCatalog() que hace fetch('/indicadores/index.json'), parsea el JSON y pinta una card por indicador con name, tagline, category, pills y enlace a /indicadores/<slug>. La función se llama al activar el tab. Todos los valores dinámicos HTML-escaped antes de insertar en el DOM.
Verificación de aceptación: abrir la consola, tab Indicadores: las 10 cards cargan correctamente. Al hacer click en una card se navega a /indicadores/<slug> y la ficha pública carga. pytest tests/ — todos en verde.
🔴 Fase 5 — Tests y wiring
Crear los tests parametrizados del motor SSG genérico y los tests del middleware de indicadores. Documentar el comando de build de ambos catálogos. Contexto: fases 1–4 MERGEADAS. Entregables: 1. tests/test_catalog_site_build.py — tests parametrizados sobre el motor tools/catalog_site/build.py cubriendo ambas entidades: (a) regresión de Estrategias: el motor produce el mismo sitemap, OG, index.json y canonical que el builder original; (b) generación de Indicadores: el motor produce web-public/indicadores/ con 10 fichas, index.json con 10 entradas y meta robots noindex,nofollow en cada ficha; (c) fichas con status!=published se excluyen del catálogo. 2. tests/test_public_indicator_pages.py — tests del middleware: GET /indicadores/rsi → 200 HTML; GET /indicadores/index.json → 200 y no interceptado por el middleware; GET /indicadores/slug-inexistente → 404. 3. Actualizar el README o AGENTS.md con el comando de build de ambos catálogos: python tools/catalog_site/build.py --entity estrategias y python tools/catalog_site/build.py --entity indicadores. Verificación de aceptación: pytest tests/test_catalog_site_build.py tests/test_public_indicator_pages.py — todos en verde. pytest tests/ — el baseline completo sigue verde; verificar que Estrategias se sirve idéntico (regresión).
Prueba proyecto desarrollado
- python tools/catalog_site/build.py para ambas entidades — genera
web-public/estrategias/yweb-public/indicadores/sin errores. - pytest tests/test_catalog_site_build.py tests/test_public_indicator_pages.py — todo en verde.
- Levantar el server: GET /indicadores/ y GET /indicadores/rsi — devuelven 200 con
meta robots noindex,nofollowvisible en el HTML. - Abrir la consola, tab Indicadores — las cards cargan desde
index.jsony al hacer click abren la ficha pública en/indicadores/<slug>. - pytest tests/ — el baseline sigue verde; verificar que Estrategias se sirve idéntico (regresión).
python tools/catalog_site/build.py --entity estrategias && python tools/catalog_site/build.py --entity indicadores && pytest tests/
Estadísticas ejecución Pipeline GGA
| Fase | Veredicto | Agentes | Tiempo | Costo | Commit |
|---|---|---|---|---|---|
| Fase 1 — Generalizar el motor SSG | Pendiente | — | — | — | — |
| Fase 2 — Capa de datos de Indicadores | Pendiente | — | — | — | — |
| Fase 3 — Generar y servir el catálogo | Pendiente | — | — | — | — |
| Fase 4 — Tab Indicadores en la consola | Pendiente | — | — | — | — |
| Fase 5 — Tests y wiring | Pendiente | — | — | — | — |
Estimación proyecto desarrollado por un humano
Tiempo orientativo para un Senior Software Engineer implementando cada fase a mano —código + tests + revisión—, sin el pipeline multi-agente.
| Fase | Estimación | Notas |
|---|---|---|
| Fase 1 — Generalizar el motor SSG | ~1.5 días | Refactor del motor manteniendo Estrategias verde (regresión). |
| Fase 2 — Capa de datos de Indicadores | ~1 día | Redactar las 10 fichas JSON de indicadores. |
| Fase 3 — Generar, middleware, Apache y noindex | ~0.5 día | Generar, middleware, Apache y verificar noindex. |
| Fase 4 — Tab de consola y card grid | ~0.5 día | Tab de consola y card grid. |
| Fase 5 — Tests parametrizados y del middleware | ~0.5 día | Tests parametrizados y del middleware. |
| Total estimado | ~4 días | Estimación de desarrollo humano sin pipeline. |
I+D
Investigación y desarrollo de estrategias — fases del pipeline GGAI+D Estrategia & Backtesting
El centro de I+D de estrategias: investigamos estrategias de trading en fuentes
fiables, las programamos en Pine, las backtesteamos sobre datos locales en 15m / 4h / 1d
y las publicamos en las tres superficies web (tab Estrategia, tab Backtest y el catálogo SEO público).
Todo el ciclo lo ejecuta el pipeline multi-agente GGA (Architect → Implementer →
Reviewer → Approver), integrando cada fase a la rama gga/rnd — dev y master nunca
se tocan solos. Abajo: los resultados de cada estrategia, cómo generar una a demanda, la automatización
programada, y las fases del pipeline. Detalle completo en NEW_STRATEGY_WORKFLOW.md.
Resultados I+D
Cada estrategia generada, su veredicto del backtest y los links a su página y su curva. Se actualiza sola cuando el pipeline termina una corrida.
Cargando resultados I+D…
Generación de estrategia manual
Dispará el pipeline GGA a demanda para generar UNA estrategia nueva de punta a punta:
el architect (con WebSearch) investiga una estrategia guiada por los filtros, la implementa,
la backtestea según el brief y la publica, integrando cada fase a gga/rnd (dev y master
intactos). El resultado aparece en Resultados I+D al terminar.
⚠️ Corre en el server: tarda ~20–40 min, cuesta tokens, y necesita los CLIs claude / codex / gh copilot + API keys en el host. Si no están, el botón avisa.
- Disparo. El botón llama a
POST /api/rnd/run(solo admin) y el server lanzatools/rnd_autonomous.pycomo proceso independiente que sobrevive a la sesión web (logs en.gga-runs/). - Research — elegir la estrategia. Un
claude -pcon el prompttools/rnd_autonomous_prompt.txtbusca con WebSearch UNA estrategia nueva alineada con elasset_type, la familia y el timeframe elegidos, evitando duplicados del catálogo y resultados previos. Escribe el brief (indicadores, parámetros, reglas, universo y timeframes) encontent/strategy_queue/<slug>.json. - Arranque del pipeline.
tools/rnd_cycle.pytoma ese brief y crea/usa la ramagga/rnd(forkeada dedev), donde se acumulan las fases aprobadas.devymasterquedan intactos. - Fase 1 — Pine + backtest (código). El pipeline GGA implementa el script Pine v6 (comisión 0.05%, slippage, SL/TP dinámico, 4 alertconditions con payload de webhook), las señales en
backtest/strategy.pyy el runnerbacktest/run_<slug>.pyusando el universo y los timeframes definidos por el brief. Tiene que pasarcompileall+pytest. - Integración. El cambio del implementer se commitea en su rama
gga/run-<ts>y se mergea (--no-ff) agga/rnd. - Backtest real. Como el sandbox no tiene los CSV de datos, el runner se corre en el árbol principal y descarga/lee OHLCV mediante el proveedor del
asset_type: Binance/ccxt para crypto y yfinance para forex, commodity y stock. Generabacktest/results/<slug>_summary.json(mejor activo/TF, profit factor, rentable o no) y la curva de capital en HTML. - Fase 2 — Páginas del dashboard. Agrega la estrategia a la consola con los números reales del summary: ficha en Estrategia (indicadores, paneles LONG/SHORT, salidas), tabla de resultados + curva en Backtest, y la card en ambas listas.
- Fase 3 — Catálogo público. Crea
content/strategies/<slug>.jsony corretools/strategy_site/build.pypara regenerar las páginas SEO deweb-public/estrategias/y el sitemap. - Registro del resultado. Escribe el veredicto (rentable / no rentable, mejor activo, PF) en
content/rnd_results.json, que alimenta la tabla Resultados I+D. - Cierre. Todo queda en
gga/rnd; el ciclo termina avisando que revises y mergeesgga/rnd → devcuando estés conforme. Nunca hace auto-merge.
Automatización programada
Una routine de Claude Code (agente programado en la nube, con WebSearch) corre el ciclo
completo en cada disparo: investiga una estrategia → ejecuta Fases 1-3 vía tools/gga_pipeline.py
→ integra el patch de cada fase a gga/rnd (git apply) y corre el backtest con los datos locales → agrega una fila a
Resultados I+D. La frecuencia (diaria o días concretos) y la hora se configuran al crear o
editar la routine (cron). dev y master nunca se tocan automáticamente — gga/rnd se mergea a mano a dev cuando lo apruebes.
Investigar estrategia - Arquitectura
Una petición por fase para tools/gga_pipeline.py, en orden, cada una asumiendo
la anterior ya integrada. La Fase 0 (investigación) la corre el architect con web. En PowerShell:
$task = @' … '@ y luego python tools/gga_pipeline.py $task --run-tests --allow-dirty.
La investigación automatizada arranca con un agente Claude con WebSearch (tools/rnd_autonomous.py) que investiga una
estrategia y escribe un brief en content/strategy_queue/. Ese brief alimenta a tools/rnd_cycle.py,
que orquesta las fases del pipeline GGA (una tarea = una fase), integra cada fase aprobada a gga/rnd, corre el backtest
donde están los datos y registra el resultado en Resultados I+D. dev y master nunca se tocan automáticamente.
Fase 0 — Investigación (automatizable: architect/routine con web)
El architect del pipeline (claude -p) tiene WebSearch confirmado (2026-06-09), así que la research puede correr dentro del pipeline o en la routine programada (ambos son Claude con web). También se puede hacer a mano. Producir un brief y pegarlo en el $task de la Fase 1: BRIEF: name: <Nombre, p.ej. "Donchian Breakout"> slug: <kebab-slug, p.ej. donchian-breakout> type: <Trend-following | Mean-reversion | Breakout | Momentum> sources: <2+ URLs fiables: QuantifiedStrategies, AlgoTest, research de exchange, papers> indicators: <nombre, params, qué mide, cómo se usa> entry_long / entry_short / exit / params: <reglas exactas y umbrales numéricos> published_metrics: <PF / win rate / timeframe que reporta la fuente, para los caveats honestos> Regla: nunca afirmar que una estrategia es "rentable" desde un blog. El brief registra lo que la fuente AFIRMA; el backtest (Fase 1) decide la verdad.
Fase 1 — Pine script + backtest (15m / 4h / 1d)
Implement the new strategy '<NAME>' (slug <slug>) in the backtesting stack. STRATEGY BRIEF: <PASTE THE PHASE-0 BRIEF>. Deliverables, matching existing conventions exactly:
1) pine/<slug>.v1.pine — Pine v6 strategy() copied from pine/supertrend_atr.v1.pine: overlay, default_qty_type=strategy.percent_of_equity default_qty_value=1, commission_type=strategy.commission.percent commission_value=0.05, slippage=2, dynamic SL/TP, and 4 alertcondition() webhook payloads shaped {"token":"{{strategy.order.alert_message}}","strategy":"<NAME>","symbol":"{{ticker}}","side":"buy|sell","price":"{{close}}","time":"{{time}}","action":"open|close"}.
2) backtest/strategy.py — add calculate_<slug>_signals(df, ...) returning boolean signal_long/signal_short columns (and an 'atr' column if exits are ATR-based), pandas/numpy only (no TA libs), mirroring calculate_supertrend_signals / calculate_rsi_momentum_signals.
3) backtest/run_<slug>.py — backtest BTC/ETH/SOL/BNB across timeframes 15m, 4h and 1d (15m from backtest/data/<ASSET>USDT_15m.csv; 4h and 1d resampled from it: open=first, high=max, low=min, close=last, volume=sum). Reuse run_backtest._run for fixed-% SL/TP, or run_bipbip_v2._run_atr for ATR exits. Print a per-TF/per-asset table (Trades, Win Rate, PF, Max DD, PnL) like run_new_strategies.py, and write the numbers to backtest/results/<slug>_summary.json for the next phase.
4) Extend backtest/make_strategy_charts.py to emit backtest/results/equity_<slug>.html (per-asset 15m equity curve), then run it.
Run run_<slug>.py and make_strategy_charts.py to produce the numbers and chart. Never touch live trading, .env, state/, logs/, data/*.csv, users.db. python -m compileall and pytest must stay green.
Fase 2 — Páginas del dashboard (tabs Estrategia + Backtest)
Add strategy '<NAME>' (slug <slug>) to web-private/index.html using the real numbers in backtest/results/<slug>_summary.json (15m/4h/1d). A) In #tab-estrategia add a <div class='backtest-strategy-section'> matching the existing 'Supertrend ATR' section: a strategy-section-header whose <h3> equals the card name EXACTLY (the scroll handler in app.js matches card name to <h3>); a 'Resumen e Investigación' panel with the research summary + a <p class='strategy-sources'> of source links (target='_blank' rel='noopener noreferrer') + overview-pills; an 'Indicadores' panel (indicator-card grid); LONG/SHORT two-col signal-panels with condition-list; a 'Salidas y nota metodológica' panel; and a 'Backtesting criptomonedas' panel with a 'Ver Backtest' button using openStrategyBacktestFromSection(this) plus a table (Activo/Veredicto/Evidencia) grounded in the backtest per asset. B) In #tab-backtest add the matching section: a 'Resultados por Timeframe y Activo' table (15m/4h/1d, PnL cells using pnl-pos/pnl-neg), a 'Conclusiones' findings-list, a 'Configuración del Backtest' panel, and a 'Curva de Capital — Retorno (%) por Activo' panel embedding <iframe src='/backtest-results/equity_<slug>.html' class='chart-frame' frameborder='0'>. C) Add the strategy card (span.strategy-card-name = '<NAME>') to BOTH tab card lists. D) Bump the ?v= query string on the app.js and style.css tags in index.html so browsers refetch. Keep the HTML balanced (equal <div>/</div>). Do not touch the backend, the webhook, or live trading.
Fase 3 — Catálogo SEO público
Add strategy '<NAME>' (slug <slug>) to the public catalog. Create content/strategies/<slug>.json following the schema in tools/strategy_site/README.md and the existing content/strategies/supertrend-atr.json: slug, status (live|research), name, tagline, updated (ISO date), seo{title,description,keywords}, meta{timeframe,assets,type,sessions}, pills, overview (HTML), indicators[{name,badge,color,what,how}], signals{long,short}, risk[], backtest{intro,columns,rows} filled with the real 15m/4h/1d numbers, learnings[], sources[] with the Phase-0 research URLs. Then run 'python tools/strategy_site/build.py' to regenerate web-public/estrategias/ and web-public/sitemap.xml. Never hand-edit the generated web-public/estrategias/*.html.
FAQ
Preguntas frecuentes sobre el funcionamiento del sistemaHay una premisa que conviene aclarar: TradingView no puede ejecutar órdenes en Bitunix directamente.
- Los webhooks de TradingView solo envían un HTTP POST a una URL. No autentican ni firman peticiones.
- La API de Bitunix exige que cada orden venga firmada con SHA256 (doble pasada) usando tu
api_secret. Una alerta de TradingView no sabe firmar eso (y no querrías exponer tu secret en una alerta). - Bitunix no es un "broker integrado" en el panel de trading de TradingView, así que no existe un botón para ejecutar en Bitunix desde el gráfico.
Por lo tanto, entre la señal de TradingView y la orden en Bitunix siempre tiene que haber algo en el medio que firme la petición. Ese puente es Bipbip Trading Bot Manager — no es opcional, es obligatorio. La alternativa sería un servicio de terceros (3Commas, Alertatron…), al que tendrías que pagar y confiarle tu API key.
- Tu API secret vive en tu servidor, no se lo entregas a una empresa externa.
- Gestión de riesgo propia: tamaño de posición por % de riesgo, circuit breaker diario y validación de margen.
- Lógica de ejecución: cálculo de SL/TP, leverage automático, control de slippage, reintentos ante fallos y reconciliación con el exchange.
- Sin cuota mensual y sin depender de que un tercero esté online.
- Ejecución 24/7 sin tu intervención: aplica tu estrategia con reglas fijas, sin demoras y sin que la emoción mueva tu stop.
La clave: la alternativa real al bot no es "TradingView directo" (eso no existe), sino vos mirando el gráfico y ejecutando a mano en Bitunix. El bot te quita de esa ecuación y ejecuta tu estrategia de forma disciplinada.