""" Main FastAPI application """ import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, HTMLResponse from app.core.config import settings from app.api.v1.api import api_router from app.core.database import engine, Base from app.middleware.error_logger import ErrorLoggingMiddleware from app.models import error_log, request_log, fred_data, filing, finra_short_volume, alpaca_price # Import to register models from app.models import overlay_registry, overlay_raw_event, overlay_feature # Overlay models # Create database tables @asynccontextmanager async def lifespan(app: FastAPI): # Startup - ensure tables exist async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Start overlay batch scheduler (graceful no-op if apscheduler not installed) try: from app.services.overlay.scheduler import start_scheduler start_scheduler() except Exception: pass yield # Shutdown try: from app.services.overlay.scheduler import stop_scheduler stop_scheduler() except Exception: pass await engine.dispose() # Create FastAPI app app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, openapi_url=f"{settings.API_PREFIX}/openapi.json", docs_url=f"{settings.API_PREFIX}/docs", redoc_url=f"{settings.API_PREFIX}/redoc", lifespan=lifespan ) # Add error logging middleware app.add_middleware(ErrorLoggingMiddleware) # Set up CORS - use configured origins if available, otherwise allow all cors_origins = [str(o) for o in settings.BACKEND_CORS_ORIGINS] if settings.BACKEND_CORS_ORIGINS else ["*"] app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include API router app.include_router(api_router, prefix=settings.API_PREFIX) # Root documentation endpoint โ€” auto-generated from OpenAPI schema @app.get("/", response_class=HTMLResponse, include_in_schema=False) async def root_documentation(request: Request): try: schema = request.app.openapi() paths = schema.get("paths", {}) api_prefix = settings.API_PREFIX # Collect all tags defined in schema (preserves order) tag_order = [t["name"] for t in schema.get("tags", [])] # Group routes by first tag from collections import defaultdict tag_routes: dict = defaultdict(list) untagged: list = [] for path, methods in paths.items(): for method, op in methods.items(): if method.upper() not in ("GET", "POST", "PUT", "DELETE", "PATCH"): continue tags = op.get("tags", []) entry = { "method": method.upper(), "path": path, "summary": op.get("summary", ""), "deprecated": op.get("deprecated", False), } if tags: tag_routes[tags[0]].append(entry) else: untagged.append(entry) # Build tag sections โ€” respect schema tag order, then alphabetical remainder all_tags = tag_order + sorted(t for t in tag_routes if t not in tag_order) if untagged: all_tags.append("other") tag_routes["other"] = untagged method_colors = { "GET": "#61affe", "POST": "#49cc90", "PUT": "#fca130", "DELETE": "#f93e3e", "PATCH": "#50e3c2", } sections_html = "" for tag in all_tags: routes = tag_routes.get(tag, []) if not routes: continue rows = "" for r in routes: color = method_colors.get(r["method"], "#999") deprecated = " style='opacity:0.5;text-decoration:line-through'" if r["deprecated"] else "" display_path = r["path"].removeprefix(api_prefix) rows += ( f"" f"{r['method']}" f"{display_path}" f"{r['summary']}" f"" ) sections_html += f"

{tag}

{rows}
" title = schema.get("info", {}).get("title", "API") version = schema.get("info", {}).get("version", "") total = sum(len(v) for v in tag_routes.values()) html = f""" {title}

๐Ÿ”ฎ {title}

v{version}  ยท  Base URL: {api_prefix}  ยท  {total} endpoints

{sections_html} """ return HTMLResponse(content=html) except Exception: return RedirectResponse(url=f"{settings.API_PREFIX}/docs") # Additional metadata for OpenAPI app.openapi_tags = [ { "name": "health", "description": "Health check endpoints" }, { "name": "financial", "description": "Financial data retrieval endpoints" }, { "name": "price", "description": "Price data endpoints (OHLCV)" }, { "name": "news", "description": "News and social media endpoints" }, { "name": "metadata", "description": "Data catalog and metadata endpoints" }, { "name": "filings", "description": "SEC filings search, document listing, and exhibit extraction (8-K, 6-K, 20-F, 40-F)" }, { "name": "etf", "description": "ETF holdings endpoints" }, { "name": "alpaca", "description": "Alpaca Market Data endpoints (OHLCV bars, connection status)" }, { "name": "finra", "description": "FINRA RegSHO short sale volume data (ingest, query, ratio history)" }, { "name": "admin", "description": "Administrative endpoints (migration, etc.)" }, { "name": "overlay", "description": "Attention Overlay - retail investor interest, media diffusion, crowding signals" }, { "name": "overlay-admin", "description": "Overlay administrative endpoints (pipeline trigger, health, job log)" }, { "name": "screener", "description": "Stock screener โ€” condition-based filtering by market cap, volume, price, P/E, sector, exchange" }, { "name": "stocks", "description": "Stock market data โ€” most active, 52-week gainers, trending, and index constituents (S&P 500 / Nasdaq 100)" } ] if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host="0.0.0.0", port=settings.API_PORT, reload=settings.DEBUG )