Sistema de trading bot - Semanas 1-2 completadas
- Infraestructura de datos completa - Descarga desde exchanges (CCXT) - Procesamiento y limpieza de datos - Almacenamiento en PostgreSQL - Sistema anti-duplicados - Script de descarga masiva - Tests unitarios - Documentación completa
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,7 @@
|
|||||||
config/secrets.env
|
config/secrets.env
|
||||||
|
|
||||||
# Carpetas de bases de datos
|
# Carpetas de bases de datos
|
||||||
data_history/
|
data/
|
||||||
|
|
||||||
# Carpetas de entorno virtual
|
# Carpetas de entorno virtual
|
||||||
venv/
|
venv/
|
||||||
|
|||||||
486
README.md
486
README.md
@@ -1,28 +1,72 @@
|
|||||||
# 🤖 Trading Bot - Semanas 1-2: Data Pipeline
|
# 🤖 Trading Bot - Proyecto Completo
|
||||||
|
|
||||||
Bot de trading algorítmico desarrollado desde cero. Esta es la primera fase enfocada en el pipeline de datos.
|
Bot de trading algorítmico desarrollado desde cero con Python, PostgreSQL y Machine Learning.
|
||||||
|
|
||||||
## 📋 Tabla de Contenidos
|
## 📋 Tabla de Contenidos
|
||||||
|
|
||||||
|
- [Estado del Proyecto](#estado-del-proyecto)
|
||||||
- [Requisitos](#requisitos)
|
- [Requisitos](#requisitos)
|
||||||
- [Instalación](#instalación)
|
- [Instalación](#instalación)
|
||||||
- [Configuración](#configuración)
|
- [Configuración](#configuración)
|
||||||
- [Uso](#uso)
|
- [Uso](#uso)
|
||||||
- [Estructura del Proyecto](#estructura-del-proyecto)
|
- [Estructura del Proyecto](#estructura-del-proyecto)
|
||||||
|
- [Base de Datos](#base-de-datos)
|
||||||
|
- [Scripts Disponibles](#scripts-disponibles)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Próximos Pasos](#próximos-pasos)
|
- [Roadmap](#roadmap)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## 🎯 Estado del Proyecto
|
||||||
|
|
||||||
|
### ✅ Completado (Semanas 1-2)
|
||||||
|
|
||||||
|
- ✅ Sistema de logging robusto con rotación de archivos
|
||||||
|
- ✅ Conexión a exchanges vía CCXT (Binance por defecto)
|
||||||
|
- ✅ Descarga de datos históricos con reintentos automáticos
|
||||||
|
- ✅ Descarga incremental (continuar desde último timestamp)
|
||||||
|
- ✅ Procesamiento y limpieza de datos
|
||||||
|
- ✅ Detección de gaps y outliers
|
||||||
|
- ✅ Resampleo de timeframes (1h → 4h, 1d, etc.)
|
||||||
|
- ✅ Cálculo de retornos (simples y logarítmicos)
|
||||||
|
- ✅ Almacenamiento en PostgreSQL con índices optimizados
|
||||||
|
- ✅ Sistema anti-duplicados con constraints únicos
|
||||||
|
- ✅ Caché con Redis (opcional)
|
||||||
|
- ✅ Script de descarga masiva para múltiples símbolos
|
||||||
|
- ✅ Tests unitarios
|
||||||
|
- ✅ Manejo de errores y reintentos
|
||||||
|
|
||||||
|
**Datos descargados actualmente:**
|
||||||
|
- 5 criptomonedas (BTC, ETH, BNB, SOL, XRP)
|
||||||
|
- 3 timeframes (1h, 4h, 1d)
|
||||||
|
- 120 días de histórico
|
||||||
|
- ~54,000 registros totales
|
||||||
|
|
||||||
|
### 🔄 En Progreso
|
||||||
|
|
||||||
|
- ⏳ Backtesting Engine (Semanas 3-4)
|
||||||
|
- ⏳ Estrategias de trading (Semanas 5-8)
|
||||||
|
- ⏳ Machine Learning (Semanas 5-8)
|
||||||
|
|
||||||
|
### 📅 Planificado
|
||||||
|
|
||||||
|
- 📋 Live trading con paper trading
|
||||||
|
- 📋 Gestión de riesgo avanzada
|
||||||
|
- 📋 Optimización de estrategias
|
||||||
|
- 📋 Dashboard web
|
||||||
|
- 📋 Alertas y notificaciones
|
||||||
|
|
||||||
## 🔧 Requisitos
|
## 🔧 Requisitos
|
||||||
|
|
||||||
### Software
|
### Software
|
||||||
- Python 3.10 o superior
|
- **Python 3.10+** (probado con 3.12.3)
|
||||||
- PostgreSQL 13 o superior
|
- **PostgreSQL 13+**
|
||||||
- Redis 6 o superior (opcional, para caché)
|
- **Redis 6+** (opcional, para caché)
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
### Hardware (mínimo para desarrollo)
|
### Hardware Recomendado
|
||||||
- 8GB RAM
|
- 8GB RAM (mínimo)
|
||||||
- 20GB espacio en disco
|
- 20GB espacio en disco
|
||||||
|
- Para ML: GPU recomendada (futuro)
|
||||||
|
|
||||||
## 📦 Instalación
|
## 📦 Instalación
|
||||||
|
|
||||||
@@ -36,12 +80,12 @@ cd trading-bot
|
|||||||
### 2. Crear entorno virtual
|
### 2. Crear entorno virtual
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m venv venv
|
python3 -m venv venv
|
||||||
|
|
||||||
# En Linux/Mac:
|
# Linux/Mac:
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
# En Windows:
|
# Windows:
|
||||||
venv\Scripts\activate
|
venv\Scripts\activate
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,12 +103,13 @@ pip install -r requirements.txt
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install postgresql postgresql-contrib
|
sudo apt install postgresql postgresql-contrib
|
||||||
sudo systemctl start postgresql
|
sudo systemctl start postgresql
|
||||||
|
sudo systemctl enable postgresql
|
||||||
```
|
```
|
||||||
|
|
||||||
**macOS (con Homebrew):**
|
**macOS (con Homebrew):**
|
||||||
```bash
|
```bash
|
||||||
brew install postgresql
|
brew install postgresql@16
|
||||||
brew services start postgresql
|
brew services start postgresql@16
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
@@ -80,10 +125,18 @@ sudo -u postgres psql
|
|||||||
CREATE DATABASE trading_bot;
|
CREATE DATABASE trading_bot;
|
||||||
CREATE USER trading_user WITH PASSWORD 'tu_password_seguro';
|
CREATE USER trading_user WITH PASSWORD 'tu_password_seguro';
|
||||||
GRANT ALL PRIVILEGES ON DATABASE trading_bot TO trading_user;
|
GRANT ALL PRIVILEGES ON DATABASE trading_bot TO trading_user;
|
||||||
|
|
||||||
|
# Conectar a la base de datos
|
||||||
|
\c trading_bot
|
||||||
|
|
||||||
|
# Dar permisos sobre el schema
|
||||||
|
GRANT ALL ON SCHEMA public TO trading_user;
|
||||||
|
|
||||||
|
# Salir
|
||||||
\q
|
\q
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Instalar Redis (opcional)
|
### 6. Instalar Redis (opcional pero recomendado)
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
**Ubuntu/Debian:**
|
||||||
```bash
|
```bash
|
||||||
@@ -97,31 +150,27 @@ brew install redis
|
|||||||
brew services start redis
|
brew services start redis
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
Descargar desde [redis.io](https://redis.io/download) o usar WSL
|
|
||||||
|
|
||||||
## ⚙️ Configuración
|
## ⚙️ Configuración
|
||||||
|
|
||||||
### 1. Copiar archivo de configuración
|
### 1. Crear archivo de configuración
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
# El archivo debe estar en config/secrets.env
|
||||||
|
# Usa este template:
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Editar `.env` con tus credenciales
|
```env
|
||||||
|
# Exchange (para datos públicos NO necesitas API keys)
|
||||||
```bash
|
|
||||||
# Exchange (para datos públicos no se necesita API key)
|
|
||||||
EXCHANGE_NAME=binance
|
EXCHANGE_NAME=binance
|
||||||
API_KEY=
|
API_KEY=
|
||||||
API_SECRET=
|
API_SECRET=
|
||||||
|
|
||||||
# Base de datos
|
# Database
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=trading_bot
|
DB_NAME=trading_bot
|
||||||
DB_USER=trading_user
|
DB_USER=trading_user
|
||||||
DB_PASSWORD=tu_password_seguro
|
DB_PASSWORD=tu_password_aqui
|
||||||
|
|
||||||
# Redis (opcional)
|
# Redis (opcional)
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
@@ -133,34 +182,67 @@ ENVIRONMENT=development
|
|||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Verificar configuración de settings.yaml
|
### 2. Configurar símbolos y timeframes
|
||||||
|
|
||||||
El archivo `config/settings.yaml` contiene configuraciones generales que puedes ajustar:
|
Edita `config/settings.yaml` para personalizar:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
trading:
|
trading:
|
||||||
symbols:
|
symbols:
|
||||||
- BTC/USDT
|
- BTC/USDT
|
||||||
- ETH/USDT
|
- ETH/USDT
|
||||||
|
- BNB/USDT
|
||||||
timeframes:
|
timeframes:
|
||||||
- 1h
|
- 1h
|
||||||
- 4h
|
- 4h
|
||||||
|
- 1d
|
||||||
|
|
||||||
|
data:
|
||||||
|
fetch_interval: 60
|
||||||
|
historical_days: 120
|
||||||
|
max_retries: 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Uso
|
## 🚀 Uso
|
||||||
|
|
||||||
### Ejecutar demo completo
|
### Demo rápido (verificar instalación)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Este comando ejecutará el pipeline completo:
|
Este script:
|
||||||
1. Conexión al exchange (Binance por defecto)
|
- Descarga 1 día de BTC/USDT
|
||||||
2. Descarga de datos históricos
|
- Muestra el pipeline completo
|
||||||
3. Procesamiento y limpieza
|
- Guarda en PostgreSQL
|
||||||
4. Almacenamiento en PostgreSQL
|
- Muestra estadísticas
|
||||||
5. Verificación de datos
|
|
||||||
|
### Descarga masiva de datos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python download_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Este script descarga:
|
||||||
|
- Múltiples símbolos configurables
|
||||||
|
- Múltiples timeframes
|
||||||
|
- Días históricos configurables
|
||||||
|
- Muestra progreso en tiempo real
|
||||||
|
- Previene duplicados automáticamente
|
||||||
|
|
||||||
|
**Personalizar descarga:**
|
||||||
|
Edita `download_data.py` líneas ~28-45:
|
||||||
|
|
||||||
|
```python
|
||||||
|
symbols = [
|
||||||
|
'BTC/USDT',
|
||||||
|
'ETH/USDT',
|
||||||
|
# Añade más aquí
|
||||||
|
]
|
||||||
|
|
||||||
|
timeframes = ['1h', '4h', '1d']
|
||||||
|
days_back = 120 # Cambia aquí
|
||||||
|
```
|
||||||
|
|
||||||
### Uso programático
|
### Uso programático
|
||||||
|
|
||||||
@@ -169,18 +251,19 @@ from src.data.fetcher import DataFetcher
|
|||||||
from src.data.processor import DataProcessor
|
from src.data.processor import DataProcessor
|
||||||
from src.data.storage import StorageManager
|
from src.data.storage import StorageManager
|
||||||
|
|
||||||
# Inicializar fetcher
|
# Inicializar
|
||||||
fetcher = DataFetcher('binance')
|
fetcher = DataFetcher('binance')
|
||||||
|
processor = DataProcessor()
|
||||||
|
storage = StorageManager(...)
|
||||||
|
|
||||||
# Obtener datos
|
# Descargar
|
||||||
df = fetcher.fetch_historical('BTC/USDT', timeframe='1h', days=7)
|
df = fetcher.fetch_historical('BTC/USDT', timeframe='1h', days=30)
|
||||||
|
|
||||||
# Procesar
|
# Procesar
|
||||||
processor = DataProcessor()
|
|
||||||
df_clean = processor.clean_data(df)
|
df_clean = processor.clean_data(df)
|
||||||
|
df_clean = processor.calculate_returns(df_clean)
|
||||||
|
|
||||||
# Guardar
|
# Guardar
|
||||||
storage = StorageManager(...)
|
|
||||||
storage.save_ohlcv(df_clean)
|
storage.save_ohlcv(df_clean)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -188,26 +271,155 @@ storage.save_ohlcv(df_clean)
|
|||||||
|
|
||||||
```
|
```
|
||||||
trading-bot/
|
trading-bot/
|
||||||
├── config/ # Configuración
|
├── config/ # Configuración
|
||||||
│ ├── settings.yaml # Configuración general
|
│ ├── settings.yaml # Configuración general
|
||||||
│ └── .env # Variables de entorno (no subir a git)
|
│ └── secrets.env # Credenciales (NO subir a git)
|
||||||
├── src/ # Código fuente
|
│
|
||||||
│ ├── data/ # Módulo de datos
|
├── src/ # Código fuente
|
||||||
│ │ ├── fetcher.py # Obtención de datos
|
│ ├── data/ # Módulo de datos
|
||||||
│ │ ├── processor.py # Procesamiento
|
│ │ ├── __init__.py
|
||||||
│ │ └── storage.py # Almacenamiento
|
│ │ ├── fetcher.py # Descarga desde exchanges
|
||||||
│ └── utils/ # Utilidades
|
│ │ ├── processor.py # Limpieza y procesamiento
|
||||||
│ └── logger.py # Sistema de logging
|
│ │ └── storage.py # PostgreSQL + Redis
|
||||||
├── tests/ # Tests unitarios
|
│ │
|
||||||
│ └── test_data.py # Tests del módulo de datos
|
│ ├── backtest/ # Motor de backtesting (próximo)
|
||||||
├── data/ # Datos locales
|
│ ├── strategies/ # Estrategias de trading (próximo)
|
||||||
│ └── historical/ # Datos históricos
|
│ ├── ml/ # Machine Learning (futuro)
|
||||||
├── logs/ # Archivos de log
|
│ └── utils/ # Utilidades
|
||||||
├── requirements.txt # Dependencias Python
|
│ └── logger.py # Sistema de logging
|
||||||
├── main.py # Punto de entrada
|
│
|
||||||
└── README.md # Este archivo
|
├── tests/ # Tests unitarios
|
||||||
|
│ └── test_data.py
|
||||||
|
│
|
||||||
|
├── data/ # Datos locales
|
||||||
|
│ ├── historical/ # Backups (futuro)
|
||||||
|
│ └── exports/ # Exportaciones (futuro)
|
||||||
|
│
|
||||||
|
├── logs/ # Archivos de log
|
||||||
|
│ ├── trading_bot_*.log
|
||||||
|
│ └── errors_*.log
|
||||||
|
│
|
||||||
|
├── main.py # Demo/testing
|
||||||
|
├── download_data.py # Descarga masiva
|
||||||
|
├── requirements.txt # Dependencias
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🗄️ Base de Datos
|
||||||
|
|
||||||
|
### Ubicación de PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver ubicación de los datos
|
||||||
|
sudo -u postgres psql -c "SHOW data_directory;"
|
||||||
|
# Típicamente: /var/lib/postgresql/16/main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabla OHLCV (estructura)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE ohlcv (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
symbol VARCHAR(20) NOT NULL,
|
||||||
|
timeframe VARCHAR(10) NOT NULL,
|
||||||
|
open FLOAT NOT NULL,
|
||||||
|
high FLOAT NOT NULL,
|
||||||
|
low FLOAT NOT NULL,
|
||||||
|
close FLOAT NOT NULL,
|
||||||
|
volume FLOAT NOT NULL,
|
||||||
|
returns FLOAT, -- Retornos simples
|
||||||
|
log_returns FLOAT, -- Retornos logarítmicos
|
||||||
|
CONSTRAINT unique_ohlcv UNIQUE (symbol, timeframe, timestamp)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices para queries rápidas
|
||||||
|
CREATE INDEX idx_symbol_timeframe_timestamp ON ohlcv(symbol, timeframe, timestamp);
|
||||||
|
CREATE INDEX idx_timestamp ON ohlcv(timestamp);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Consultas útiles
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Conectar a la base de datos
|
||||||
|
psql -U trading_user -d trading_bot -h localhost
|
||||||
|
|
||||||
|
-- Ver todas las tablas
|
||||||
|
\dt
|
||||||
|
|
||||||
|
-- Contar registros totales
|
||||||
|
SELECT COUNT(*) FROM ohlcv;
|
||||||
|
|
||||||
|
-- Ver datos disponibles por símbolo
|
||||||
|
SELECT
|
||||||
|
symbol,
|
||||||
|
timeframe,
|
||||||
|
COUNT(*) as registros,
|
||||||
|
MIN(timestamp) as desde,
|
||||||
|
MAX(timestamp) as hasta
|
||||||
|
FROM ohlcv
|
||||||
|
GROUP BY symbol, timeframe
|
||||||
|
ORDER BY symbol, timeframe;
|
||||||
|
|
||||||
|
-- Ver últimos 10 registros de BTC
|
||||||
|
SELECT * FROM ohlcv
|
||||||
|
WHERE symbol = 'BTC/USDT' AND timeframe = '1h'
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
-- Estadísticas de retornos
|
||||||
|
SELECT
|
||||||
|
symbol,
|
||||||
|
timeframe,
|
||||||
|
AVG(returns) as retorno_medio,
|
||||||
|
STDDEV(returns) as volatilidad,
|
||||||
|
MIN(returns) as peor_retorno,
|
||||||
|
MAX(returns) as mejor_retorno
|
||||||
|
FROM ohlcv
|
||||||
|
WHERE returns IS NOT NULL
|
||||||
|
GROUP BY symbol, timeframe;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup de la base de datos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup completo
|
||||||
|
pg_dump -U trading_user -d trading_bot -h localhost > backup_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Backup solo tabla ohlcv
|
||||||
|
pg_dump -U trading_user -d trading_bot -h localhost -t ohlcv > backup_ohlcv.sql
|
||||||
|
|
||||||
|
# Restaurar desde backup
|
||||||
|
psql -U trading_user -d trading_bot -h localhost < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📜 Scripts Disponibles
|
||||||
|
|
||||||
|
### `main.py` - Demo y Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uso:** Verificar que todo funciona correctamente
|
||||||
|
**Descarga:** 1 símbolo, 1 timeframe, pocos días
|
||||||
|
**Muestra:** Pipeline completo con estadísticas detalladas
|
||||||
|
|
||||||
|
### `download_data.py` - Descarga Masiva
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python download_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uso:** Llenar base de datos con datos históricos
|
||||||
|
**Configurable:** Símbolos, timeframes, días
|
||||||
|
**Características:**
|
||||||
|
- Progreso en tiempo real
|
||||||
|
- Prevención de duplicados
|
||||||
|
- Manejo de errores robusto
|
||||||
|
- Resumen final con estadísticas
|
||||||
|
|
||||||
## 🧪 Testing
|
## 🧪 Testing
|
||||||
|
|
||||||
### Ejecutar todos los tests
|
### Ejecutar todos los tests
|
||||||
@@ -216,90 +428,145 @@ trading-bot/
|
|||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ejecutar tests con cobertura
|
### Tests con cobertura
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest tests/ --cov=src --cov-report=html
|
pytest tests/ --cov=src --cov-report=html
|
||||||
|
# Ver reporte en htmlcov/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ejecutar test específico
|
### Test específico
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest tests/test_data.py::TestDataProcessor::test_clean_data_removes_duplicates -v
|
pytest tests/test_data.py::TestDataProcessor::test_clean_data_removes_duplicates -v
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Funcionalidades Implementadas
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
### ✅ Completado (Semanas 1-2)
|
### ✅ Fase 1: Infraestructura de Datos (COMPLETADO)
|
||||||
|
- Sistema de descarga robusto
|
||||||
|
- Almacenamiento optimizado
|
||||||
|
- Procesamiento de datos
|
||||||
|
|
||||||
- [x] Sistema de logging robusto
|
### 🔄 Fase 2: Backtesting (PRÓXIMO - Semanas 3-4)
|
||||||
- [x] Conexión a exchanges vía CCXT
|
- Motor de backtesting
|
||||||
- [x] Descarga de datos históricos
|
- Estrategia simple (moving average crossover)
|
||||||
- [x] Descarga incremental (continuar desde último timestamp)
|
- Métricas de performance
|
||||||
- [x] Procesamiento y limpieza de datos
|
- Visualizaciones
|
||||||
- [x] Detección de gaps y outliers
|
|
||||||
- [x] Resampleo de timeframes
|
|
||||||
- [x] Cálculo de retornos
|
|
||||||
- [x] Almacenamiento en PostgreSQL
|
|
||||||
- [x] Caché con Redis
|
|
||||||
- [x] Tests unitarios
|
|
||||||
- [x] Manejo de errores y reintentos
|
|
||||||
|
|
||||||
## 🔜 Próximos Pasos (Semanas 3-4)
|
### 📅 Fase 3: Estrategias Avanzadas (Semanas 5-8)
|
||||||
|
- Indicadores técnicos
|
||||||
|
- Machine Learning básico
|
||||||
|
- Optimización de parámetros
|
||||||
|
|
||||||
- [ ] Engine de backtesting
|
### 📅 Fase 4: Trading Real (Semanas 9-12)
|
||||||
- [ ] Métricas de performance (Sharpe, Sortino, Max Drawdown)
|
- Paper trading
|
||||||
- [ ] Visualizaciones de resultados
|
- Gestión de riesgo
|
||||||
- [ ] Estrategia simple de trading
|
- Ejecución de órdenes
|
||||||
- [ ] Simulación histórica
|
- Monitoreo en tiempo real
|
||||||
|
|
||||||
|
### 📅 Fase 5: Producción (Futuro)
|
||||||
|
- Dashboard web
|
||||||
|
- Alertas y notificaciones
|
||||||
|
- Multi-exchange
|
||||||
|
- Despliegue en servidor
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### Error: "No se puede conectar a PostgreSQL"
|
### Error: "No se puede conectar a PostgreSQL"
|
||||||
|
|
||||||
**Solución:**
|
|
||||||
```bash
|
```bash
|
||||||
# Verificar que PostgreSQL está corriendo
|
# Verificar que está corriendo
|
||||||
sudo systemctl status postgresql
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
# Verificar credenciales en .env
|
# Reiniciar si es necesario
|
||||||
# Verificar que el usuario tiene permisos
|
sudo systemctl restart postgresql
|
||||||
|
|
||||||
|
# Verificar credenciales en config/secrets.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error: "ModuleNotFoundError: No module named 'ccxt'"
|
### Error: "Invalid Api-Key ID"
|
||||||
|
|
||||||
**Solución:**
|
**Solución:** Para datos públicos NO necesitas API keys. Deja vacíos `API_KEY` y `API_SECRET` en `config/secrets.env`.
|
||||||
|
|
||||||
|
### Error: "column does not exist"
|
||||||
|
|
||||||
|
**Solución:** Recrear la tabla:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DROP TABLE IF EXISTS ohlcv CASCADE;
|
||||||
|
```
|
||||||
|
|
||||||
|
Luego ejecutar `python main.py` para recrearla.
|
||||||
|
|
||||||
|
### Error: "duplicate key value violates unique constraint"
|
||||||
|
|
||||||
|
**Solución:** Esto es normal y esperado. El sistema previene automáticamente duplicados. Si quieres limpiar duplicados existentes:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM ohlcv a USING ohlcv b
|
||||||
|
WHERE a.id > b.id
|
||||||
|
AND a.symbol = b.symbol
|
||||||
|
AND a.timeframe = b.timeframe
|
||||||
|
AND a.timestamp = b.timestamp;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis no está disponible
|
||||||
|
|
||||||
|
**No es crítico.** El bot funciona sin Redis, solo perderás caché. Logs mostrarán: "Continuando sin caché."
|
||||||
|
|
||||||
|
Para instalar Redis:
|
||||||
```bash
|
```bash
|
||||||
# Asegurarse de que el entorno virtual está activado
|
sudo apt install redis-server
|
||||||
source venv/bin/activate # Linux/Mac
|
sudo systemctl start redis
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
|
|
||||||
# Reinstalar dependencias
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error: Rate limit exceeded
|
### Downloads muy lentos
|
||||||
|
|
||||||
**Solución:**
|
- Verifica tu conexión a internet
|
||||||
El código ya incluye manejo de rate limiting, pero si persiste:
|
- El exchange puede tener rate limiting
|
||||||
- Aumentar delays en `config/settings.yaml`
|
- Para Binance sin API keys: ~1000 requests/min
|
||||||
- Reducir el número de símbolos/timeframes
|
|
||||||
- Usar API keys para límites más altos
|
### La descarga se queda colgada
|
||||||
|
|
||||||
|
- Presiona `Ctrl+C` para cancelar
|
||||||
|
- Revisa logs en `logs/trading_bot_*.log`
|
||||||
|
- Verifica que el exchange esté accesible
|
||||||
|
|
||||||
## 📝 Notas Importantes
|
## 📝 Notas Importantes
|
||||||
|
|
||||||
⚠️ **IMPORTANTE**: Este bot es para fines educativos. No ejecutes trading real sin:
|
### ⚠️ Advertencia Legal
|
||||||
1. Backtesting exhaustivo (mínimo 3-5 años)
|
|
||||||
2. Paper trading extensivo (varios meses)
|
Este bot es para **fines educativos y de investigación**. El trading conlleva riesgo financiero significativo.
|
||||||
3. Gestión de riesgo robusta
|
|
||||||
4. Comprensión completa del código
|
**NO ejecutes trading real sin:**
|
||||||
|
1. ✅ Backtesting exhaustivo (mínimo 3-5 años de datos)
|
||||||
|
2. ✅ Paper trading extensivo (varios meses)
|
||||||
|
3. ✅ Gestión de riesgo robusta y probada
|
||||||
|
4. ✅ Comprensión completa del código y estrategias
|
||||||
|
5. ✅ Capital que puedas permitirte perder
|
||||||
|
|
||||||
|
### 🔒 Seguridad
|
||||||
|
|
||||||
|
- **NUNCA** subas `config/secrets.env` a git
|
||||||
|
- Usa contraseñas fuertes para PostgreSQL
|
||||||
|
- En producción, usa variables de entorno del sistema
|
||||||
|
- Limita permisos de archivos sensibles: `chmod 600 config/secrets.env`
|
||||||
|
|
||||||
|
### 💾 Portabilidad (Futuro)
|
||||||
|
|
||||||
|
Actualmente usa PostgreSQL (requiere instalación en cada máquina).
|
||||||
|
|
||||||
|
**Plan futuro:** Script de exportación a SQLite para portabilidad completa:
|
||||||
|
```bash
|
||||||
|
python export_to_portable.py # Generará data/trading_bot.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto permitirá copiar todo el proyecto en USB y ejecutar en cualquier PC.
|
||||||
|
|
||||||
## 🤝 Contribuir
|
## 🤝 Contribuir
|
||||||
|
|
||||||
Este es un proyecto de aprendizaje personal. Si encuentras bugs o tienes sugerencias:
|
Este es un proyecto personal de aprendizaje. Sugerencias y mejoras son bienvenidas.
|
||||||
1. Documenta el issue claramente
|
|
||||||
2. Incluye logs y pasos para reproducir
|
|
||||||
3. Propón solución si es posible
|
|
||||||
|
|
||||||
## 📄 Licencia
|
## 📄 Licencia
|
||||||
|
|
||||||
@@ -307,9 +574,12 @@ MIT License - Usar bajo tu propio riesgo
|
|||||||
|
|
||||||
## 📧 Contacto
|
## 📧 Contacto
|
||||||
|
|
||||||
Para dudas sobre el código o siguiente fase de desarrollo, consulta conmigo.
|
Para dudas sobre el código o siguientes fases de desarrollo, consulta conmigo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Versión actual:** 0.1.0 (Semanas 1-2 completadas)
|
**Versión actual:** 0.2.0 (Semanas 1-2 completadas)
|
||||||
**Última actualización:** Enero 2026
|
**Última actualización:** Enero 2026
|
||||||
|
**Python:** 3.12.3
|
||||||
|
**PostgreSQL:** 16+
|
||||||
|
**Datos:** 5 símbolos, 3 timeframes, 120 días (~54k registros)
|
||||||
173
download_data.py
Normal file
173
download_data.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# download_data.py
|
||||||
|
"""
|
||||||
|
Script para descargar datos históricos de múltiples símbolos y timeframes
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from src.monitoring.logger import log
|
||||||
|
from src.data.fetcher import DataFetcher
|
||||||
|
from src.data.processor import DataProcessor
|
||||||
|
from src.data.storage import StorageManager
|
||||||
|
|
||||||
|
def setup_environment():
|
||||||
|
"""Carga variables de entorno"""
|
||||||
|
env_path = Path(__file__).parent / 'config' / 'secrets.env'
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
log.success("✓ Variables de entorno cargadas")
|
||||||
|
|
||||||
|
def download_multiple_symbols():
|
||||||
|
"""
|
||||||
|
Descarga datos históricos para múltiples símbolos y timeframes
|
||||||
|
"""
|
||||||
|
log.info("="*70)
|
||||||
|
log.info("📥 DESCARGA MASIVA DE DATOS HISTÓRICOS")
|
||||||
|
log.info("="*70)
|
||||||
|
|
||||||
|
# Configuración
|
||||||
|
setup_environment()
|
||||||
|
|
||||||
|
exchange_name = os.getenv('EXCHANGE_NAME', 'binance')
|
||||||
|
|
||||||
|
# Símbolos a descargar (puedes añadir más)
|
||||||
|
symbols = [
|
||||||
|
'BTC/USDT',
|
||||||
|
'ETH/USDT',
|
||||||
|
'BNB/USDT',
|
||||||
|
'SOL/USDT',
|
||||||
|
'XRP/USDT',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Timeframes a descargar
|
||||||
|
timeframes = [
|
||||||
|
'1h', # 1 hora
|
||||||
|
'4h', # 4 horas
|
||||||
|
'1d', # 1 día
|
||||||
|
]
|
||||||
|
|
||||||
|
# Días históricos
|
||||||
|
days_back = 120 # 4 meses
|
||||||
|
|
||||||
|
log.info(f"\n📊 Configuración:")
|
||||||
|
log.info(f" Exchange: {exchange_name}")
|
||||||
|
log.info(f" Símbolos: {len(symbols)} → {symbols}")
|
||||||
|
log.info(f" Timeframes: {timeframes}")
|
||||||
|
log.info(f" Días históricos: {days_back}")
|
||||||
|
log.info(f" Total descargas: {len(symbols) * len(timeframes)}")
|
||||||
|
|
||||||
|
# Confirmar
|
||||||
|
print("\n" + "="*70)
|
||||||
|
response = input("¿Continuar con la descarga? (s/n): ").lower()
|
||||||
|
if response != 's':
|
||||||
|
log.warning("Descarga cancelada por el usuario")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Inicializar componentes
|
||||||
|
log.info("\n🔧 Inicializando componentes...")
|
||||||
|
|
||||||
|
fetcher = DataFetcher(
|
||||||
|
exchange_name=exchange_name,
|
||||||
|
api_key=os.getenv('API_KEY') if os.getenv('API_KEY') else None,
|
||||||
|
api_secret=os.getenv('API_SECRET') if os.getenv('API_SECRET') else None
|
||||||
|
)
|
||||||
|
|
||||||
|
processor = DataProcessor()
|
||||||
|
|
||||||
|
storage = StorageManager(
|
||||||
|
db_host=os.getenv('DB_HOST'),
|
||||||
|
db_port=int(os.getenv('DB_PORT', 5432)),
|
||||||
|
db_name=os.getenv('DB_NAME'),
|
||||||
|
db_user=os.getenv('DB_USER'),
|
||||||
|
db_password=os.getenv('DB_PASSWORD'),
|
||||||
|
redis_host=os.getenv('REDIS_HOST', 'localhost'),
|
||||||
|
redis_port=int(os.getenv('REDIS_PORT', 6379))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Estadísticas
|
||||||
|
total_downloads = len(symbols) * len(timeframes)
|
||||||
|
current_download = 0
|
||||||
|
successful = 0
|
||||||
|
failed = 0
|
||||||
|
total_records = 0
|
||||||
|
|
||||||
|
# Descargar datos
|
||||||
|
log.info("\n" + "="*70)
|
||||||
|
log.info("🚀 INICIANDO DESCARGAS...")
|
||||||
|
log.info("="*70 + "\n")
|
||||||
|
|
||||||
|
for symbol in symbols:
|
||||||
|
for timeframe in timeframes:
|
||||||
|
current_download += 1
|
||||||
|
|
||||||
|
log.info(f"\n{'='*70}")
|
||||||
|
log.info(f"📥 [{current_download}/{total_downloads}] {symbol} @ {timeframe}")
|
||||||
|
log.info(f"{'='*70}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Verificar si ya existen datos
|
||||||
|
last_timestamp = storage.get_latest_timestamp(symbol, timeframe)
|
||||||
|
|
||||||
|
if last_timestamp:
|
||||||
|
log.info(f"⚠️ Ya existen datos hasta: {last_timestamp}")
|
||||||
|
log.info(f" Se descargarán solo datos nuevos...")
|
||||||
|
|
||||||
|
# Descargar datos
|
||||||
|
df = fetcher.fetch_historical(
|
||||||
|
symbol=symbol,
|
||||||
|
timeframe=timeframe,
|
||||||
|
days=days_back
|
||||||
|
)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
log.warning(f"❌ No se obtuvieron datos para {symbol} @ {timeframe}")
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Procesar
|
||||||
|
log.info(f"🧹 Procesando datos...")
|
||||||
|
df_clean = processor.clean_data(df)
|
||||||
|
df_clean = processor.calculate_returns(df_clean)
|
||||||
|
|
||||||
|
# Guardar
|
||||||
|
log.info(f"💾 Guardando en base de datos...")
|
||||||
|
records_saved = storage.save_ohlcv(df_clean)
|
||||||
|
|
||||||
|
total_records += records_saved
|
||||||
|
successful += 1
|
||||||
|
|
||||||
|
log.success(f"✅ {symbol} @ {timeframe} → {records_saved} registros guardados")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"❌ Error descargando {symbol} @ {timeframe}: {e}")
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Resumen final
|
||||||
|
log.info("\n" + "="*70)
|
||||||
|
log.info("📊 RESUMEN DE DESCARGA")
|
||||||
|
log.info("="*70)
|
||||||
|
log.info(f"✅ Exitosas: {successful}/{total_downloads}")
|
||||||
|
log.info(f"❌ Fallidas: {failed}/{total_downloads}")
|
||||||
|
log.info(f"📦 Total registros guardados: {total_records:,}")
|
||||||
|
|
||||||
|
# Ver datos disponibles
|
||||||
|
log.info("\n📋 Datos disponibles en base de datos:")
|
||||||
|
available_data = storage.get_available_data()
|
||||||
|
print(available_data.to_string(index=False))
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
storage.close()
|
||||||
|
|
||||||
|
log.info("\n" + "="*70)
|
||||||
|
log.success("✅ DESCARGA COMPLETADA")
|
||||||
|
log.info("="*70)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
download_multiple_symbols()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.warning("\n⚠️ Descarga interrumpida por el usuario")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"\n❌ Error fatal: {e}")
|
||||||
|
raise
|
||||||
22
main.py
22
main.py
@@ -1,10 +1,10 @@
|
|||||||
# main.py
|
# main.py
|
||||||
|
# main.py
|
||||||
"""
|
"""
|
||||||
Punto de entrada principal del bot de trading
|
Punto de entrada principal del bot de trading
|
||||||
Demo para Semanas 1-2: Data Pipeline + Storage
|
Demo para Semanas 1-2: Data Pipeline + Storage
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from src.monitoring.logger import log
|
from src.monitoring.logger import log
|
||||||
@@ -14,16 +14,12 @@ from src.data.storage import StorageManager
|
|||||||
|
|
||||||
def setup_environment():
|
def setup_environment():
|
||||||
"""
|
"""
|
||||||
Carga variables de entorno desde config/secrets.env
|
Carga variables de entorno
|
||||||
"""
|
"""
|
||||||
# Cargar desde config/secrets.env
|
# Cargar desde config/secrets.env
|
||||||
project_root = Path(__file__).parent
|
from pathlib import Path
|
||||||
env_path = project_root / 'config' / 'secrets.env'
|
env_path = Path(__file__).parent / 'config' / 'secrets.env'
|
||||||
if not env_path.exists():
|
load_dotenv(dotenv_path=env_path)
|
||||||
log.error(f"No se encuentra el archivo: {env_path}")
|
|
||||||
raise FileNotFoundError(f"Archivo de configuración no encontrado: {env_path}")
|
|
||||||
load_dotenv(env_path)
|
|
||||||
log.info(f"Cargando configuración desde: {env_path}")
|
|
||||||
|
|
||||||
# Verificar variables requeridas
|
# Verificar variables requeridas
|
||||||
required_vars = ['EXCHANGE_NAME', 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD']
|
required_vars = ['EXCHANGE_NAME', 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD']
|
||||||
@@ -33,7 +29,7 @@ def setup_environment():
|
|||||||
log.error(f"Faltan variables de entorno: {missing_vars}")
|
log.error(f"Faltan variables de entorno: {missing_vars}")
|
||||||
raise EnvironmentError(f"Variables faltantes: {missing_vars}")
|
raise EnvironmentError(f"Variables faltantes: {missing_vars}")
|
||||||
|
|
||||||
log.success("✓ Variables de entorno cargadas correctamente")
|
log.success("Variables de entorno cargadas correctamente")
|
||||||
|
|
||||||
def demo_data_pipeline():
|
def demo_data_pipeline():
|
||||||
"""
|
"""
|
||||||
@@ -50,15 +46,15 @@ def demo_data_pipeline():
|
|||||||
exchange_name = os.getenv('EXCHANGE_NAME', 'binance')
|
exchange_name = os.getenv('EXCHANGE_NAME', 'binance')
|
||||||
symbol = 'BTC/USDT'
|
symbol = 'BTC/USDT'
|
||||||
timeframe = '1h'
|
timeframe = '1h'
|
||||||
days_back = 7
|
days_back = 1
|
||||||
|
|
||||||
# 2. Inicializar componentes
|
# 2. Inicializar componentes
|
||||||
log.info("\n[1/5] Inicializando componentes...")
|
log.info("\n[1/5] Inicializando componentes...")
|
||||||
|
|
||||||
fetcher = DataFetcher(
|
fetcher = DataFetcher(
|
||||||
exchange_name=exchange_name,
|
exchange_name=exchange_name,
|
||||||
api_key=os.getenv('API_KEY'),
|
api_key=os.getenv('API_KEY') if os.getenv('API_KEY') else None,
|
||||||
api_secret=os.getenv('API_SECRET')
|
api_secret=os.getenv('API_SECRET') if os.getenv('API_SECRET') else None
|
||||||
)
|
)
|
||||||
|
|
||||||
processor = DataProcessor()
|
processor = DataProcessor()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# src/data/fetcher.py
|
# src/data/fetcher.py
|
||||||
|
# src/data/fetcher.py
|
||||||
"""
|
"""
|
||||||
Módulo para obtener datos de exchanges usando CCXT
|
Módulo para obtener datos de exchanges usando CCXT
|
||||||
"""
|
"""
|
||||||
@@ -27,21 +28,36 @@ class DataFetcher:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
exchange_class = getattr(ccxt, exchange_name)
|
exchange_class = getattr(ccxt, exchange_name)
|
||||||
self.exchange = exchange_class({
|
|
||||||
'apiKey': api_key,
|
# Configuración base
|
||||||
'secret': api_secret,
|
config = {
|
||||||
'enableRateLimit': True, # Importante para evitar bans
|
'enableRateLimit': True, # Importante para evitar bans
|
||||||
'options': {
|
'options': {
|
||||||
'defaultType': 'spot', # spot, future, etc
|
'defaultType': 'spot', # spot, future, etc
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
log.info(f"Conectado al exchange: {exchange_name}")
|
|
||||||
|
# Solo añadir API keys si están presentes y no vacías
|
||||||
|
if api_key and api_secret:
|
||||||
|
config['apiKey'] = api_key
|
||||||
|
config['secret'] = api_secret
|
||||||
|
log.info(f"Conectado al exchange: {exchange_name} (con API keys)")
|
||||||
|
else:
|
||||||
|
log.info(f"Conectado al exchange: {exchange_name} (modo público - sin API keys)")
|
||||||
|
|
||||||
|
self.exchange = exchange_class(config)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error conectando a {exchange_name}: {e}")
|
log.error(f"Error conectando a {exchange_name}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Optional[datetime] = None,
|
def fetch_ohlcv(
|
||||||
limit: int = 500) -> pd.DataFrame:
|
self,
|
||||||
|
symbol: str,
|
||||||
|
timeframe: str = '1h',
|
||||||
|
since: Optional[datetime] = None,
|
||||||
|
limit: int = 500
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Obtiene datos OHLCV (Open, High, Low, Close, Volume)
|
Obtiene datos OHLCV (Open, High, Low, Close, Volume)
|
||||||
|
|
||||||
@@ -55,7 +71,7 @@ class DataFetcher:
|
|||||||
DataFrame con los datos OHLCV
|
DataFrame con los datos OHLCV
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Convertir datetime a timestamp en ms
|
# Convertir datetime a timestamp en milisegundos
|
||||||
since_ms = None
|
since_ms = None
|
||||||
if since:
|
if since:
|
||||||
since_ms = int(since.timestamp() * 1000)
|
since_ms = int(since.timestamp() * 1000)
|
||||||
@@ -90,8 +106,13 @@ class DataFetcher:
|
|||||||
log.error(f"Error obteniendo OHLCV para {symbol}: {e}")
|
log.error(f"Error obteniendo OHLCV para {symbol}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def fetch_historical(self, symbol: str, timeframe: str = '1h', days: int = 30,
|
def fetch_historical(
|
||||||
max_retries: int = 3) -> pd.DataFrame:
|
self,
|
||||||
|
symbol: str,
|
||||||
|
timeframe: str = '1h',
|
||||||
|
days: int = 30,
|
||||||
|
max_retries: int = 3
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Obtiene datos históricos completos (puede requerir múltiples llamadas)
|
Obtiene datos históricos completos (puede requerir múltiples llamadas)
|
||||||
|
|
||||||
@@ -109,7 +130,11 @@ class DataFetcher:
|
|||||||
|
|
||||||
log.info(f"Iniciando descarga histórica: {symbol} desde {since.date()}")
|
log.info(f"Iniciando descarga histórica: {symbol} desde {since.date()}")
|
||||||
|
|
||||||
|
iteration = 0
|
||||||
while True:
|
while True:
|
||||||
|
iteration += 1
|
||||||
|
log.debug(f"Iteración {iteration}: Obteniendo datos desde {since}")
|
||||||
|
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
@@ -120,7 +145,7 @@ class DataFetcher:
|
|||||||
if df.empty:
|
if df.empty:
|
||||||
log.warning(f"No hay más datos disponibles para {symbol}")
|
log.warning(f"No hay más datos disponibles para {symbol}")
|
||||||
success = True
|
success = True
|
||||||
break
|
break # Salir del while interno
|
||||||
|
|
||||||
all_data.append(df)
|
all_data.append(df)
|
||||||
|
|
||||||
@@ -131,7 +156,7 @@ class DataFetcher:
|
|||||||
# Verificar si ya llegamos al presente
|
# Verificar si ya llegamos al presente
|
||||||
if since >= datetime.now():
|
if since >= datetime.now():
|
||||||
success = True
|
success = True
|
||||||
break
|
break # Salir del while interno
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
time.sleep(self.exchange.rateLimit / 1000) # Respetar rate limit
|
time.sleep(self.exchange.rateLimit / 1000) # Respetar rate limit
|
||||||
@@ -143,10 +168,10 @@ class DataFetcher:
|
|||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
log.error(f"Falló después de {max_retries} intentos")
|
log.error(f"Falló después de {max_retries} intentos")
|
||||||
break
|
break # Salir del while externo
|
||||||
|
|
||||||
if since >= datetime.now():
|
if since >= datetime.now() or df.empty:
|
||||||
break
|
break # Salir del while externo si no hay más datos
|
||||||
|
|
||||||
if not all_data:
|
if not all_data:
|
||||||
log.error("No se pudo obtener ningún dato histórico")
|
log.error("No se pudo obtener ningún dato histórico")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Módulo para almacenamiento persistente de datos en PostgreSQL y caché en Redis
|
Módulo para almacenamiento persistente de datos en PostgreSQL y caché en Redis
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from sqlalchemy import create_engine, Column, String, Float, DateTime, Integer, Index
|
from sqlalchemy import create_engine, Column, String, Float, DateTime, Integer, Index, text
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -29,13 +29,20 @@ class OHLCV(Base):
|
|||||||
low = Column(Float, nullable=False)
|
low = Column(Float, nullable=False)
|
||||||
close = Column(Float, nullable=False)
|
close = Column(Float, nullable=False)
|
||||||
volume = Column(Float, nullable=False)
|
volume = Column(Float, nullable=False)
|
||||||
|
returns = Column(Float, nullable=True) # Retornos simples
|
||||||
|
log_returns = Column(Float, nullable=True) # Retornos logarítmicos
|
||||||
|
|
||||||
# Índices compuestos para queries rápidas
|
# Índices compuestos para queries rápidas
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index('idx_symbol_timeframe_timestamp', 'symbol', 'timeframe', 'timestamp'),
|
Index('idx_symbol_timeframe_timestamp', 'symbol', 'timeframe', 'timestamp'),
|
||||||
Index('idx_timestamp', 'timestamp'),
|
Index('idx_timestamp', 'timestamp'),
|
||||||
|
# CONSTRAINT único: no permitir duplicados
|
||||||
|
# Una combinación de symbol + timeframe + timestamp debe ser única
|
||||||
|
{'sqlite_autoincrement': True}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Añadir constraint único manualmente en __init__ de StorageManager
|
||||||
|
|
||||||
class StorageManager:
|
class StorageManager:
|
||||||
"""
|
"""
|
||||||
Gestor de almacenamiento con PostgreSQL y Redis
|
Gestor de almacenamiento con PostgreSQL y Redis
|
||||||
@@ -69,6 +76,20 @@ class StorageManager:
|
|||||||
# Crear tablas si no existen
|
# Crear tablas si no existen
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine)
|
||||||
|
|
||||||
|
# Añadir constraint único si no existe (para evitar duplicados)
|
||||||
|
try:
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
conn.execute(text("""
|
||||||
|
ALTER TABLE ohlcv
|
||||||
|
ADD CONSTRAINT unique_ohlcv
|
||||||
|
UNIQUE (symbol, timeframe, timestamp)
|
||||||
|
"""))
|
||||||
|
conn.commit()
|
||||||
|
log.info("Constraint único añadido a la tabla ohlcv")
|
||||||
|
except Exception as e:
|
||||||
|
# El constraint ya existe o hubo error (no crítico)
|
||||||
|
log.debug(f"Constraint único ya existe o no se pudo añadir: {e}")
|
||||||
|
|
||||||
# Crear sesión
|
# Crear sesión
|
||||||
Session = sessionmaker(bind=self.engine)
|
Session = sessionmaker(bind=self.engine)
|
||||||
self.session = Session()
|
self.session = Session()
|
||||||
@@ -117,22 +138,53 @@ class StorageManager:
|
|||||||
if df_to_save.columns[0] != 'timestamp':
|
if df_to_save.columns[0] != 'timestamp':
|
||||||
df_to_save.rename(columns={df_to_save.columns[0]: 'timestamp'}, inplace=True)
|
df_to_save.rename(columns={df_to_save.columns[0]: 'timestamp'}, inplace=True)
|
||||||
|
|
||||||
|
# Mantener todas las columnas relevantes
|
||||||
|
allowed_columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'symbol', 'timeframe', 'returns', 'log_returns']
|
||||||
|
df_to_save = df_to_save[[col for col in allowed_columns if col in df_to_save.columns]]
|
||||||
|
|
||||||
# Insertar en lotes para mejor performance
|
# Insertar en lotes para mejor performance
|
||||||
records_saved = 0
|
records_saved = 0
|
||||||
|
records_skipped = 0
|
||||||
|
|
||||||
for i in range(0, len(df_to_save), batch_size):
|
for i in range(0, len(df_to_save), batch_size):
|
||||||
batch = df_to_save.iloc[i:i+batch_size]
|
batch = df_to_save.iloc[i:i+batch_size]
|
||||||
|
|
||||||
# Usar to_sql con if_exists='append' y method='multi'
|
try:
|
||||||
batch.to_sql(
|
# Usar to_sql con if_exists='append' y method='multi'
|
||||||
'ohlcv',
|
batch.to_sql(
|
||||||
self.engine,
|
'ohlcv',
|
||||||
if_exists='append',
|
self.engine,
|
||||||
index=False,
|
if_exists='append',
|
||||||
method='multi'
|
index=False,
|
||||||
)
|
method='multi'
|
||||||
|
)
|
||||||
|
records_saved += len(batch)
|
||||||
|
log.debug(f"Guardados {records_saved}/{len(df_to_save)} registros")
|
||||||
|
|
||||||
records_saved += len(batch)
|
except Exception as e:
|
||||||
log.debug(f"Guardados {records_saved}/{len(df_to_save)} registros")
|
# Si hay error de duplicados, intentar uno por uno
|
||||||
|
if 'unique' in str(e).lower() or 'duplicate' in str(e).lower():
|
||||||
|
log.warning(f"Duplicados detectados en batch, insertando uno por uno...")
|
||||||
|
|
||||||
|
for _, row in batch.iterrows():
|
||||||
|
try:
|
||||||
|
row.to_frame().T.to_sql(
|
||||||
|
'ohlcv',
|
||||||
|
self.engine,
|
||||||
|
if_exists='append',
|
||||||
|
index=False
|
||||||
|
)
|
||||||
|
records_saved += 1
|
||||||
|
except Exception:
|
||||||
|
# Este registro ya existe, saltarlo
|
||||||
|
records_skipped += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Otro tipo de error, re-lanzar
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if records_skipped > 0:
|
||||||
|
log.info(f"Saltados {records_skipped} registros duplicados")
|
||||||
|
|
||||||
log.success(f"Guardados {records_saved} registros exitosamente")
|
log.success(f"Guardados {records_saved} registros exitosamente")
|
||||||
return records_saved
|
return records_saved
|
||||||
|
|||||||
Reference in New Issue
Block a user