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.tomlcon deps + ruff + pytest; venv (uvovenv).config.py(pydantic-settings):ALPACA_API_KEY,ALPACA_SECRET_KEY,ALPACA_PAPER=true,SYMBOLS, parámetros..env.example,.gitignore.alpaca_client.pymínimo:get_account(),get_clock(). Comandobot 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), conreasontextual 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.pyporque 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, mismoposition_size, bracket simulado con mínimo/máximo del día y "stop primero", selector por distancia sobre SMA200. ComparaEXIT_MODE=take_profitvstrailingy contra SPY. Sin pérdida diaria ni drawdown en esta versión.- Tabla
positionscon 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 conMAX_GAP_PCT; kill switch diario conkill_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.pycompleto;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=true2–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 /killcon confirmación y token;GET /api/*JSON.
B. Documentación viva (archivos markdown del repo, renderizados)
- Plan:
docs/PLAN.mdtal 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.mdcon 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.mdcon el diagrama de componentes y el flujo del ciclo; la página además lista el árbol real desrc/para que nunca quede desactualizado. - Backtests: índice de
docs/backtests/*.mdcon 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-pyomistune.
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 comodata/universe/sp500.csv(símbolo, nombre, sector, fecha de inclusión) y commiteada al repo para reproducibilidad. - Comando
bot universe refreshque 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).
SYMBOLSen.envpasa a admitir@sp500como 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):
MarketModulepor mercado, órdenes cripto GTC fraccionarias, colocación idempotente del stop-limit en el sync, riesgo por módulo, jobscrypto_executecada 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_GAPlo 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(jobbackup16: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 (ramasite) + comandos por Telegram. Estado persistente en la ramastate(STATE_SYNC). (Hugging Face Spaces quedó descartado: exige PRO para Docker.) - ✅ Auto-update (
AUTO_UPDATE=true):bot runtraeorigin/maincada hora, reinstala si hace falta y se reinicia bajo un supervisor (deploy/run-forever.cmdo systemd). Operativa: los PR se fusionan enmaincon 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
reasonlegible (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.examplesin valores. Si una key se commitea: rotarla en Alpaca de inmediato. - Variables separadas paper/live;
ALPACA_PAPER=truepor 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
/killsin auth; exponer solo en LAN/Tailscale o tras HTTPS (Caddy). - Filtro de logging que nunca imprime keys;
pre-commitconruff+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.