diegobeyl/backtesting
2
1"""
2FastAPI Main Application V2 - Enhanced with rate limiting, structured logging and caching
3"""
4import sys
5import logging
6from pathlib import Path
7from fastapi import FastAPI, Request
8from fastapi.staticfiles import StaticFiles
9from fastapi.responses import FileResponse
10from fastapi.middleware.cors import CORSMiddleware
11from slowapi import Limiter, _rate_limit_exceeded_handler
12from slowapi.util import get_remote_address
13from slowapi.errors import RateLimitExceeded
14
15# Desactivar TODOS los loggers de terceros ANTES de cualquier import
16for logger_name in ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'asyncio', 'click']:
17 logger_obj = logging.getLogger(logger_name)
18 logger_obj.handlers = []
19 logger_obj.propagate = False
20 logger_obj.setLevel(logging.CRITICAL)
21
22# Limpiar el logger raíz
23root_logger = logging.getLogger()
24for handler in root_logger.handlers[:]:
25 handler.close()
26 root_logger.removeHandler(handler)
27root_logger.propagate = False
28
29# Add parent directory for imports
30sys.path.insert(0, str(Path(__file__).parent.parent))
31
32from api.routes import router as api_router
33from api.config_manager import get_config_manager
34from config import config
35from utils.logger import setup_logger
36
37import pandas as pd
38import functools
39
40# Setup logger
41logger = setup_logger(__name__)
42
43# --- MONKEYPATCH FOR PANDAS FREQUENCY COMPATIBILITY ---
44def patch_pandas_resample():
45 original_series_resample = pd.Series.resample
46 original_df_resample = pd.DataFrame.resample
47
48 @functools.wraps(original_series_resample)
49 def patched_series_resample(self, rule, *args, **kwargs):
50 if rule == 'ME':
51 rule = 'M'
52 return original_series_resample(self, rule, *args, **kwargs)
53
54 @functools.wraps(original_df_resample)
55 def patched_df_resample(self, rule, *args, **kwargs):
56 if rule == 'ME':
57 rule = 'M'
58 return original_df_resample(self, rule, *args, **kwargs)
59
60 pd.Series.resample = patched_series_resample
61 pd.DataFrame.resample = patched_df_resample
62 logger.info("Pandas resample patched for 'ME' frequency compatibility")
63
64# Apply patch immediately
65patch_pandas_resample()
66# ------------------------------------------------------
67
68# Initialize rate limiter
69limiter = Limiter(
70 key_func=get_remote_address,
71 default_limits=[f"{config.RATE_LIMIT_PER_MINUTE}/minute", f"{config.RATE_LIMIT_PER_HOUR}/hour"]
72)
73
74# Create FastAPI app
75app = FastAPI(
76 title="Donchian Backtesting API V2",
77 description="Enhanced backtesting system with rate limiting, caching, and structured logging",
78 version="2.0.0",
79 debug=config.DEBUG
80)
81
82# Add rate limiter state
83app.state.limiter = limiter
84app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
85
86# Flags para evitar eventos duplicados
87_startup_done = False
88_shutdown_done = False
89
90# Configure CORS
91app.add_middleware(
92 CORSMiddleware,
93 allow_origins=config.CORS_ORIGINS,
94 allow_credentials=True,
95 allow_methods=["*"],
96 allow_headers=["*"],
97)
98
99# Request logging middleware
100@app.middleware("http")
101async def log_requests(request: Request, call_next):
102 """Log all incoming requests."""
103 logger.info(
104 f"Request: {request.method} {request.url.path}",
105 extra={
106 "method": request.method,
107 "path": request.url.path,
108 "client": request.client.host if request.client else "unknown"
109 }
110 )
111 response = await call_next(request)
112 logger.info(
113 f"Response: {request.method} {request.url.path} - Status {response.status_code}",
114 extra={
115 "method": request.method,
116 "path": request.url.path,
117 "status_code": response.status_code
118 }
119 )
120 return response
121
122# Startup event
123@app.on_event("startup")
124async def startup_event():
125 """Initialize application on startup."""
126 global _startup_done
127 if _startup_done:
128 return
129 _startup_done = True
130 logger.info(f"Starting Backtesting API V2 in {config.ENVIRONMENT} mode")
131 logger.info(f"Rate limiting: {config.RATE_LIMIT_PER_MINUTE}/min, {config.RATE_LIMIT_PER_HOUR}/hour")
132 logger.info(f"Cache enabled: {config.ENABLE_CACHE}")
133 logger.info(f"CORS origins: {config.CORS_ORIGINS}")
134
135# Shutdown event
136@app.on_event("shutdown")
137async def shutdown_event():
138 """Cleanup on shutdown."""
139 global _shutdown_done
140 if _shutdown_done:
141 return
142 _shutdown_done = True
143 logger.info("Shutting down Backtesting API V2")
144
145# Include API routes
146app.include_router(api_router, prefix="/api")
147
148# Mount static files
149static_path = Path(__file__).parent.parent / "static"
150if static_path.exists():
151 app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
152
153# Root endpoint
154@app.get("/")
155@limiter.limit(f"{config.RATE_LIMIT_PER_MINUTE * 2}/minute") # More permissive for root
156async def read_root(request: Request):
157 """API root endpoint with documentation links."""
158 return {
159 "message": "Donchian Backtesting API V2",
160 "version": "2.0.0",
161 "description": "Enhanced backtesting system with rate limiting, caching, and structured logging",
162 "documentation": "/docs",
163 "openapi": "/openapi.json",
164 "health_check": "/health",
165 "api_endpoints": "/docs#/default",
166 "streamlit_ui": "http://localhost:8501"
167 }
168
169# Health check endpoint
170@app.get("/health")
171async def health_check():
172 """Health check endpoint for monitoring."""
173 from utils.cache import get_cache_stats
174
175 return {
176 "status": "healthy",
177 "version": "2.0.0",
178 "environment": config.ENVIRONMENT,
179 "cache": get_cache_stats() if config.ENABLE_CACHE else {"enabled": False}
180 }
181
182# Cache management endpoint (admin only in production)
183@app.post("/api/cache/clear")
184@limiter.limit("5/minute")
185async def clear_cache_endpoint(request: Request):
186 """Clear application cache (use with caution)."""
187 from utils.cache import clear_cache
188
189 if config.ENVIRONMENT == "production":
190 return {"error": "Cache clearing disabled in production"}
191
192 clear_cache()
193 logger.warning("Cache cleared via API endpoint")
194 return {"message": "Cache cleared successfully"}
195
196if __name__ == "__main__":
197 import uvicorn
198
199 # Desactivar COMPLETAMENTE los loggers de uvicorn antes de ejecutar
200 for logger_name in ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'asyncio']:
201 logger_obj = logging.getLogger(logger_name)
202 logger_obj.handlers = []
203 logger_obj.propagate = False
204 logger_obj.setLevel(logging.CRITICAL) # Silenciar completamente
205
206 # Configuración de uvicorn sin logging
207 uvicorn_config = {
208 "app": "api.main:app",
209 "host": config.API_HOST,
210 "port": config.API_PORT,
211 "reload": False,
212 "log_level": "critical", # Solo mostrar críticos
213 "access_log": False,
214 "use_colors": False
215 }
216
217 uvicorn.run(**uvicorn_config)
218 