Trading bot
DRY_RUN
v0.1.0 · d3b61f2 arrancado 2026-09-11 03:16 UTC último ciclo nunca último sync nunca sqlite:///./trading.db auto-update OFF

Plan: Bot de trading automatizado de acciones US (Alpaca, paper trading) + web de información

Contexto

El usuario quiere automatizar la compra y venta de acciones y saber si conviene crear una página web para guardar información relevante. Decisiones ya tomadas con el usuario:

  • Proyecto independiente con repositorio propio. Nació como carpeta dentro del repo HabitOS (app de hábitos del mismo autor) y se movió aquí con su historial.
  • Bróker: Alpaca, cuenta paper (dinero simulado, sin fondeo). Sin dinero real hasta cumplir criterios objetivos.
  • Stack: Python 3.11+, FastAPI para la web, SQLite al inicio.
  • Perfil: desarrollador único, estudiante, presupuesto ≈ 0 USD.

Ubicación: repositorio keudhhdnfustedeyfbgbf-ctrl/trading-bot, rama principal main. Cada fase se desarrolla en una rama propia y entra por PR.

Veredicto sobre la web: sí conviene, pero mínima y después de que el bot funcione en paper (Fase 3). Sin datos reales de decisiones no tiene nada que mostrar. Opción recomendada: FastAPI + Jinja2 + HTMX en el mismo proceso que el bot (ver sección 7).

No-objetivos: HFT, opciones/cripto, machine learning, multiusuario, dinero real.


1. Arquitectura

Componente Responsabilidad
Data ingestion Descargar barras diarias ajustadas por splits de Alpaca Market Data; caché CSV
Strategy engine Función pura: DataFrame → Signal(BUY/SELL/HOLD, reason). Sin red ni BD
Risk manager Valida señal contra reglas; devuelve OrderIntent o Rejection con motivo
Order executor Traduce a órdenes Alpaca (bracket con stop-loss/take-profit). Idempotente por client_order_id
Scheduler APScheduler: ejecución 9:35 ET, sincronización de órdenes cada hora, snapshot 16:10, resumen 16:15
Persistence SQLModel/SQLite: bars_cache, signals, orders, trades, equity_snapshots, decision_log, journal_notes, watchlist, settings_runtime (kill switch)
Web dashboard FastAPI + Jinja2 + HTMX: estado, posiciones, bitácora, journal, watchlist, métricas, kill switch
Notifications Telegram (POST con httpx): orden ejecutada, rechazo, kill switch, error, resumen diario

Principio: strategy y risk son funciones puras testeables sin red. Todo contacto con Alpaca vive en broker/ con una interfaz BrokerClient y un FakeBroker para tests.

Flujo del ciclo:

scheduler → market_open? → fetch_bars(watchlist) → strategy.evaluate() → risk.validate()
         → executor.submit() → persist(decision_log, orders) → notify()

Si un paso falla: se registra status=error, se notifica y no se reintentan órdenes (evita duplicados).

Estructura de directorios

trading-bot/
├── README.md
├── pyproject.toml              # deps, ruff, pytest
├── .env.example                # nombres de variables, sin valores
├── .gitignore                  # .env, *.db, data/
├── data/                       # cache + trading.db (ignorado)
├── src/trading_bot/
│   ├── config.py               # pydantic-settings: keys, paper/live, símbolos, parámetros
│   ├── logging_setup.py
│   ├── models.py               # SQLModel tablas
│   ├── db.py
│   ├── broker/                 # base.py (Protocol), alpaca_client.py, fake_client.py
│   ├── data/                   # ingest.py, indicators.py (SMA, EMA, ATR, RSI en pandas)
│   ├── strategy/               # base.py (Signal), sma_cross.py
│   ├── risk/                   # rules.py (funciones puras), manager.py
│   ├── execution/executor.py
│   ├── engine/                 # cycle.py, scheduler.py
│   ├── backtest/               # engine.py (motor propio), portfolio.py, report.py
│   ├── notify/telegram.py
│   ├── web/                    # app.py, routes/, templates/, static/ (htmx vendored)
│   └── cli.py                  # bot status | backtest | run | web | kill
├── scripts/download_history.py
└── tests/                      # conftest.py (barras sintéticas, FakeBroker, BD en memoria),
                                # test_indicators, test_strategy_sma, test_risk_rules,
                                # test_executor, test_cycle, test_web_smoke

Archivos críticos: config.py, strategy/sma_cross.py, risk/manager.py, engine/cycle.py, broker/alpaca_client.py, web/app.py.


2. Fases

Fase 0 — Setup (1–2 días)

  • Cuenta Alpaca, activar paper, generar keys paper.
  • pyproject.toml con deps + ruff + pytest; venv (uv o venv).
  • config.py (pydantic-settings): ALPACA_API_KEY, ALPACA_SECRET_KEY, ALPACA_PAPER=true, SYMBOLS, parámetros. .env.example, .gitignore.
  • alpaca_client.py mínimo: get_account(), get_clock(). Comando bot status.
  • logging_setup.py, db.py + init_db(). CI opcional (GitHub Actions: ruff + pytest).

Done: bot status muestra equity paper y si el mercado está abierto; pytest y ruff verdes; .env no está en git.

Fase 1 — Datos + backtesting (1–2 semanas)

  • ingest.py: 2–5 años de barras diarias para watchlist (SPY, QQQ + 5–10 large caps), cache incremental.
  • indicators.py: SMA, EMA, ATR, RSI, retorno N días (sin TA-Lib).
  • sma_cross.py (sección 3), con reason textual en cada señal.
  • backtest/engine.py: motor propio barra a barra que reutiliza la estrategia y el sizing reales; comisión 0, slippage 0.05 %, capital 100 000. (Se descartó backtesting.py porque no modela cartera.)
  • report.py: CAGR, max drawdown, Sharpe, win rate, nº trades, vs. buy & hold; markdown + PNG.
  • Walk-forward: optimizar 2019–2022, validar 2023–2025. Máx. 2–3 parámetros.

Done: bot backtest --symbol SPY --from 2019-01-01 genera reporte; tests de indicadores/estrategia pasan; README documenta el resultado honesto.

Fase 1b — Backtest de cartera y ciclo de vida de posiciones (2 semanas, antes del paper)

Añadida tras la revisión del 10-sep-2026. Detalle en docs/CHANGELOG.md y en el plan de correcciones.

  • backtest/portfolio.py: capital compartido, MAX_OPEN_POSITIONS, mismo position_size, bracket simulado con mínimo/máximo del día y "stop primero", selector por distancia sobre SMA200. Compara EXIT_MODE=take_profit vs trailing y contra SPY. Sin pérdida diaria ni drawdown en esta versión.
  • Tabla positions con máquina de estados (SIGNALED → SUBMITTED → OPEN → EXITING → CLOSED, más REJECTED/CANCELLED); executor que cancela las patas del bracket antes de vender; sincronización de fills en cada ciclo; recálculo de stop/TP/cantidad en ejecución con MAX_GAP_PCT; kill switch diario con kill_until; conteo real de day trades.

Done: reporte docs/backtests/PORTFOLIO_*.md commiteado (cierra la versión 1.1 de la estrategia); tests de cada transición con FakeBroker.

Fase 2 — Paper trading en vivo (2–3 semanas)

  • Completar alpaca_client.py: get_positions, submit_order (bracket), cancel_all, get_orders. fake_client.py.
  • risk/rules.py + manager.py (sección 3).
  • executor.py: client_order_id = f"{symbol}-{date}-{signal_hash}"; nunca reenviar si existe.
  • cycle.py completo; scheduler.py: ejecución 9:35 ET lun–vie, sincronización de órdenes cada hora en mercado, snapshot 16:10 ET, resumen 16:15 ET; chequeo de kill switch al arrancar.
  • notify/telegram.py. ✅ Hecho: bot run, bot sync, bot notify-test, jobs envueltos para que un fallo notifique sin matar el proceso.
  • Pendiente: DRY_RUN=true 2–3 días (todo igual pero sin órdenes), luego paper.

Done: 10 días hábiles seguidos en paper sin intervención; cada orden en Alpaca tiene fila en orders y decision_log; cero duplicados; notificación diaria; tests cubren cada regla con casos límite.

Fase 3 — Web: panel operativo + documentación viva (2–3 semanas)

La web tiene dos mitades que comparten proceso, autenticación y estilo:

A. Panel operativo (datos de la BD)

  • FastAPI + Jinja2 + HTMX (vendored, sin CDN en prod).
  • Páginas: Dashboard (equity, P&L día/semana/total, posiciones, últimas 20 decisiones, kill switch), Bitácora de decisiones (filtros por símbolo/fecha/estado), Journal manual (markdown + fecha + etiqueta), Watchlist CRUD, Métricas (drawdown, win rate, expectancy, backtest vs. paper), Logs (tail).
  • POST /kill con confirmación y token; GET /api/* JSON.

B. Documentación viva (archivos markdown del repo, renderizados)

  • Plan: docs/PLAN.md tal cual, con índice lateral.
  • Historial de cambios: docs/CHANGELOG.md (una entrada por mejora, fecha, qué cambió y por qué) + últimos commits de git (git log) para trazabilidad.
  • Estrategia: docs/STRATEGY.md con reglas de entrada/salida, parámetros vigentes leídos de la configuración real, y tabla de versiones de la estrategia con su backtest asociado.
  • Estructura: docs/ARCHITECTURE.md con el diagrama de componentes y el flujo del ciclo; la página además lista el árbol real de src/ para que nunca quede desactualizado.
  • Backtests: índice de docs/backtests/*.md con sus gráficos, ordenado por fecha, con comparación entre corridas.
  • Mejoras propuestas: entradas del journal etiquetadas mejora, con estado (idea / probada en backtest / desplegada / descartada) y enlace al backtest que la justificó.

Reglas para que la documentación no envejezca: cada cambio de estrategia o de reglas de riesgo obliga a (1) una entrada en CHANGELOG.md, (2) un backtest guardado en docs/backtests/, (3) actualizar STRATEGY.md. Un test verifica que los cuatro archivos existen y que STRATEGY.md menciona los parámetros actuales de config.py.

  • Auth: HTTP Basic con credenciales en .env; servir en LAN/Tailscale o tras HTTPS.
  • Estética: CSS propio ~200 líneas estilo Notion/Bento (estilo Notion / Bento). Markdown renderizado con markdown-it-py o mistune.

Done: bot web sirve en :8000; test_web_smoke.py con TestClient (200 en todas las rutas, /kill requiere auth y cambia el flag); dashboard refleja el paper run; las seis páginas de documentación renderizan los archivos de docs/.

Fase 4 — Universo ampliado: S&P 500 (2–3 semanas, tras ≥2 semanas de paper con 10 símbolos)

Pasar de 10 símbolos a ~500 cambia cuatro cosas: obtención de la lista, volumen de datos, selección entre muchas señales y forma de backtestear.

4.1 Lista de constituyentes

  • No hay API oficial gratuita. Fuente: tabla pública de Wikipedia leída con pandas.read_html, guardada como data/universe/sp500.csv (símbolo, nombre, sector, fecha de inclusión) y commiteada al repo para reproducibilidad.
  • Comando bot universe refresh que actualiza el CSV y registra en el CHANGELOG los símbolos añadidos/eliminados. Refresco manual, una vez al mes.
  • Normalizar tickers con punto (BRK.B → BRK-B según lo que espere Alpaca).
  • SYMBOLS en .env pasa a admitir @sp500 como alias del archivo, más exclusiones (SYMBOLS_EXCLUDE).

4.2 Datos

  • Alpaca acepta varios símbolos por petición: descargar en lotes de 100 → 5 peticiones por ciclo, muy por debajo del límite de ~200/min.
  • Caché incremental: por la mañana solo se pide la barra faltante de cada símbolo.
  • Barras diarias para todos; una evaluación al cierre y una ejecución a las 9:35 ET.

4.3 Selección de candidatos (nuevo componente portfolio/selector.py)

  • Con 500 símbolos habrá muchas señales BUY el mismo día y solo 5 huecos. Regla de ranking explicable: ordenar por fuerza de tendencia (retorno de 6 meses o distancia sobre SMA200) y tomar las mejores hasta llenar max_open_positions.
  • Filtros previos: precio > 10 USD y ≤ MAX_POSITION_PCT × equity (acciones enteras), volumen medio 20 días > 1 M acciones. (El filtro de earnings se retiró: no hay fuente gratuita fiable.)
  • Diversificación: máximo 2 posiciones por sector (columna del CSV).
  • El ranking y el motivo del descarte quedan en decision_log.

4.4 Backtest de cartera (backtest/portfolio.py)

  • El backtester actual es por símbolo. Se añade uno de cartera: capital compartido, max_open_positions, selector y reglas de riesgo diarias, para medir la estrategia como se ejecutará de verdad.
  • Sesgo de supervivencia: usar la lista actual del S&P 500 sobre datos de 2019 sobreestima resultados (las empresas que quebraron o salieron no están). Mitigación: documentarlo en el reporte y, si es posible, guardar listas históricas del CSV en cada refresco para usarlas en el futuro.
  • Comparar contra SPY como benchmark de cartera.

4.5 Riesgo y operación

  • Sube el riesgo de errores de datos (splits, símbolos deslistados). Validar barras (precio > 0, sin huecos > 5 días) antes de evaluar.
  • Los límites de la cartera no cambian: 5 posiciones, 10 % por posición, 1 % de riesgo por trade.

Done: bot universe refresh genera el CSV; un ciclo completo sobre ~500 símbolos termina en < 2 min; el backtest de cartera 2019–hoy corre y su reporte declara el sesgo de supervivencia; el selector tiene tests con señales simultáneas; 2 semanas de paper con el universo ampliado sin errores de datos.

Fase 4b — Más horas de mercado: módulo cripto 24/7 (condicionado)

Añadida el 11-sep-2026 a petición del usuario (quiere operar ~14 h/día o más). Decisiones: cripto en Alpaca con la misma API; stop como orden stop-limit separada tras el fill (Alpaca no admite bracket en cripto); capital separado por CRYPTO_CAPITAL_PCT; barras de 4 h (seis evaluaciones al día); comisiones por lado en el backtest.

  • Hecho: bot backtest-portfolio --market crypto, catch-up al arrancar, HEALTHCHECK_URL, deploy/SERVER.md.
  • Pendiente (solo si el backtest cripto supera a mantener BTC en Sharpe o drawdown con ≥ 30 operaciones OOS): MarketModule por mercado, órdenes cripto GTC fraccionarias, colocación idempotente del stop-limit en el sync, riesgo por módulo, jobs crypto_execute cada 4 h UTC, panel con filtro por módulo, DRY_RUN 3 días y paper 2 semanas con ambos módulos.
  • Advertencias: más horas no son más rentabilidad; cripto tiene más volatilidad y comisiones; el stop-limit puede no ejecutarse en una caída vertical (CRYPTO_STOP_LIMIT_GAP lo mitiga).

Fase 5 — Hardening / monitoring (1 semana + continuo)

  • Reconciliación al arrancar: posiciones/órdenes Alpaca vs. BD.
  • Reintentos con backoff solo en lecturas, nunca en envío de órdenes; timeout por ciclo.
  • GET /health + heartbeat; alerta si no hay ciclo en 45 min en horario de mercado (healthchecks.io / UptimeRobot gratis).
  • ✅ Backup diario de trading.db (job backup 16:30 ET, API de backup de SQLite, 14 copias).
  • systemd + deploy/install.sh + guía Oracle Cloud Free con Tailscale. PostgreSQL solo si >1 proceso escribe o BD >1 GB.
  • ✅ Despliegue sin tarjeta: GitHub Actions (bot job auto, cron UTC) + panel estático en Cloudflare Pages (rama site) + comandos por Telegram. Estado persistente en la rama state (STATE_SYNC). (Hugging Face Spaces quedó descartado: exige PRO para Docker.)
  • ✅ Auto-update (AUTO_UPDATE=true): bot run trae origin/main cada hora, reinstala si hace falta y se reinicia bajo un supervisor (deploy/run-forever.cmd o systemd). Operativa: los PR se fusionan en main con CI verde y el bot instalado los recibe sin intervención.

Done: kill -9 a mitad de sesión + reinicio no duplica órdenes ni pierde estado; alerta de bot caído comprobada; backup restaurable.

Fase 6 — Criterios para dinero real (decisión, no código)

Solo si se cumplen todos: ≥3 meses (ideal 6) de paper con el mismo código; resultado ≥ 0 tras slippage y drawdown < 10 %; diferencia backtest/paper explicable; cero incidentes en el último mes; capital que se puede perder por completo (mínimo ~1 500–2 000 USD: con 10 % por posición son 150–200 USD, suficiente para una acción entera de casi todo el universo; los símbolos más caros se descartan con el filtro de precio; las órdenes bracket no admiten fraccionadas); keys live separadas + I_UNDERSTAND_LIVE_TRADING=true + confirmación interactiva; obligaciones fiscales entendidas.


3. Estrategia inicial y reglas de riesgo

Estrategia: cruce de medias con filtro de tendencia (barras diarias, solo largos).

  • Universo inicial: 10 símbolos líquidos (SPY, QQQ, AAPL, MSFT, NVDA, AMZN, GOOGL, META, JPM, XOM). Ampliación al S&P 500 en la Fase 4.
  • Entrada permitida hasta 5 barras después del cruce (entry_window), para no perder señales cuando el filtro de tendencia se cumple unos días más tarde.
  • BUY: SMA20 cruza sobre SMA50 y precio > SMA200 y volumen > media 20 días × 0.8.
  • SELL: SMA20 cruza bajo SMA50, o stop-loss 2×ATR14, o take-profit 3×ATR14, o 40 días hábiles en posición.
  • Evaluación al cierre; ejecución al día siguiente en el primer ciclo tras 9:35 ET.
  • Cada señal guarda reason legible (se muestra en el dashboard).
  • Elegida por ser explicable y con pocos parámetros; no se espera que sea rentable, sirve para validar la tubería.

Reglas de riesgo (risk/rules.py, una función y un test por regla):

Regla Valor inicial
Tamaño máx. por posición 10 % del equity
Riesgo por trade 1 % del equity (según distancia al stop)
Máx. posiciones abiertas 5
Pérdida diaria −2 % → kill switch hasta el día siguiente
Drawdown total −10 % desde máximo → kill switch permanente hasta revisión manual
Kill switch flag en settings_runtime + archivo KILL en disco
Horario usar get_clock() de Alpaca, nunca cálculo local
Cash mínimo 10 %
Anti-PDT máx. 3 day-trades en 5 días hábiles (contar en BD)

4. Librerías

alpaca-py (SDK oficial, flag paper/live), pandas, backtesting.py (simple; vectorbt después si hace falta), APScheduler 3.x (sin Redis/Celery), SQLModel, FastAPI + jinja2 + htmx + uvicorn, pydantic-settings, httpx (Telegram), pytest + pytest-cov, ruff, typer (opcional), matplotlib (PNG de backtest).

Evitar por ahora: Celery, Redis, Airflow, React, TA-Lib, ML.


5. Seguridad

  • Secrets solo en .env (gitignored desde el primer commit); .env.example sin valores. Si una key se commitea: rotarla en Alpaca de inmediato.
  • Variables separadas paper/live; ALPACA_PAPER=true por defecto; el cliente imprime el endpoint al arrancar; live exige flag extra + confirmación por teclado.
  • Keys de solo lectura para el proceso web si se separa.
  • Dashboard con HTTP Basic, nunca /kill sin auth; exponer solo en LAN/Tailscale o tras HTTPS (Caddy).
  • Filtro de logging que nunca imprime keys; pre-commit con ruff + gitleaks.

6. Riesgos y advertencias

  • No es asesoría financiera. La mayoría de estrategias simples no superan al índice tras costos.
  • Regla PDT: cuentas con margen < 25 000 USD y ≥ 4 day-trades en 5 días quedan restringidas en live. Paper no lo bloquea; el bot debe contar.
  • Impuestos: W-8BEN; retención 30 % sobre dividendos (Perú sin tratado); ganancias de capital declarables en Perú (SUNAT). Consultar contador antes de dinero real.
  • Alpaca para residentes de Perú: acepta cuentas internacionales en muchos países, pero verificar en alpaca.markets antes de la Fase 6. Paper solo requiere registro.
  • Datos gratuitos: IEX no consolidado, ~200 req/min; suficiente para diario/15 min de ~10 símbolos.
  • Riesgo psicológico: cambios de parámetros solo documentados en el journal y con nuevo backtest.
  • Perú es UTC−5; difiere 1 h de ET en horario de verano de EE.UU. Siempre usar el reloj de Alpaca.

7. Evaluación de la página web

Decisión tomada y ejecutada en la Fase 3 (panel local FastAPI). El análisis de alternativas se archivó en docs/CHANGELOG.md (entrada 2026-09-10, revisión pre-paper).

8. Costos mensuales

Concepto Paper Live
Alpaca (cuenta, API, datos IEX) 0 0 (SIP opcional ~9 USD)
Telegram, GitHub Actions 0 0
Hosting: laptop / Raspberry Pi / Oracle Cloud Free / Fly.io–Railway 0 / ~1–2 / 0 / 0–5 USD igual
Total ≈ 0 USD 0–15 USD + capital de riesgo

Empezar en la laptop con DRY_RUN; pasar a Oracle Free o Raspberry Pi cuando deba correr a diario.


9. Verificación

Fase Cómo
0 bot status imprime equity y market_is_open; pytest/ruff verdes; git ls-files | grep .env vacío
1 test_indicators.py vs. cálculo manual en series sintéticas; test_strategy_sma.py fuerza un cruce y verifica señal + reason; reporte de backtest en docs/; walk-forward sin colapso fuera de muestra
2 Un test por regla y por umbral; test_executor.py con FakeBroker: reenviar la misma señal no crea 2 órdenes; test_cycle.py en memoria; 3 días DRY_RUN → 10 días paper; comparar Alpaca UI vs. tabla orders cada mañana
3 test_web_smoke.py con TestClient; revisión manual con datos del paper run
5 kill -9 en horario de mercado y reinicio; FakeBroker lanzando excepción → notificación; restaurar backup
6 Checklist de Fase 6 firmado en el journal con métricas reales de 3–6 meses

Orden de ejecución

Fase 0 ✅ → Fase 1 ✅ → Fase 3 web operativa + documentación ✅ (adelantada a petición del usuario) → Fase 2 scheduler + Telegram, dry-run y paper (sem. 1–3) → Fase 4 universo S&P 500 (sem. 7–9) → Fase 5 hardening (sem. 10) → observar 3–6 meses → decidir Fase 6.

Siguiente paso concreto: Fase 2, engine/scheduler.py + notify/telegram.py, y bot run como proceso permanente (trabajo parcial en la rama feat/phase-2-scheduler).

Fuente: docs/PLAN.md · modificado hace 0 min. Se lee del disco en cada carga: git pull lo actualiza sin reiniciar.

trading-bot v0.1.0 · 2026-09-11 03:16 UTC