You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
8.8 KiB
Python
247 lines
8.8 KiB
Python
"""
|
|
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"<tr{deprecated}>"
|
|
f"<td><span class='method' style='background:{color}'>{r['method']}</span></td>"
|
|
f"<td><code>{display_path}</code></td>"
|
|
f"<td>{r['summary']}</td>"
|
|
f"</tr>"
|
|
)
|
|
sections_html += f"<h3>{tag}</h3><table>{rows}</table>"
|
|
|
|
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"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{title}</title>
|
|
<style>
|
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6; max-width: 1100px; margin: 0 auto; padding: 24px; color: #333; }}
|
|
h1 {{ color: #1a1a2e; border-bottom: 3px solid #007acc; padding-bottom: 10px; }}
|
|
h3 {{ color: #2c3e50; margin-top: 2em; text-transform: capitalize; border-left: 4px solid #007acc; padding-left: 10px; }}
|
|
.nav {{ margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; }}
|
|
.nav a {{ padding: 8px 18px; background: #007acc; color: white; text-decoration: none;
|
|
border-radius: 6px; font-size: 0.9em; }}
|
|
.nav a:hover {{ background: #005fa3; }}
|
|
.meta {{ color: #666; font-size: 0.9em; margin-bottom: 1.5em; }}
|
|
table {{ width: 100%; border-collapse: collapse; margin-bottom: 1em; font-size: 0.9em; }}
|
|
tr:hover {{ background: #f8f9fa; }}
|
|
td {{ padding: 7px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
|
|
td:first-child {{ width: 72px; }}
|
|
td:nth-child(2) {{ width: 38%; }}
|
|
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; word-break: break-all; }}
|
|
.method {{ display: inline-block; color: white; font-weight: bold; font-size: 0.75em;
|
|
padding: 3px 8px; border-radius: 4px; letter-spacing: 0.5px; }}
|
|
footer {{ margin-top: 3em; padding-top: 1.5em; border-top: 1px solid #eee;
|
|
text-align: center; color: #888; font-size: 0.85em; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🔮 {title}</h1>
|
|
<div class="nav">
|
|
<a href="{api_prefix}/docs">📋 Swagger UI</a>
|
|
<a href="{api_prefix}/redoc">📖 ReDoc</a>
|
|
<a href="{api_prefix}/health">💚 Health</a>
|
|
<a href="{api_prefix}/openapi.json">⚙️ OpenAPI JSON</a>
|
|
</div>
|
|
<p class="meta">v{version} · Base URL: <code>{api_prefix}</code> · {total} endpoints</p>
|
|
{sections_html}
|
|
<footer>Auto-generated from OpenAPI schema · <a href="{api_prefix}/docs">Full interactive docs</a></footer>
|
|
</body>
|
|
</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
|
|
) |