Files
trumpsignal-backend/app/main.py
T
2026-04-20 23:05:59 +08:00

152 lines
4.6 KiB
Python

import asyncio
import logging
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.database import AsyncSessionLocal, engine
from app.models import Base
from app.scrapers.truth_social import poll_truth_social, backfill_history
from app.services.binance import run_binance_ws
from app.ws.manager import manager
# Import all routers
from app.api.posts import router as posts_router
from app.api.prices import router as prices_router
from app.api.trades import router as trades_router
from app.api.performance import router as performance_router
from app.api.subscribe import router as subscribe_router
from app.api.user import router as user_router
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
from typing import Optional
_binance_task: Optional[asyncio.Task] = None
_scheduler: Optional[AsyncIOScheduler] = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global _binance_task, _scheduler
# 1. Create DB tables (dev convenience; production uses Alembic)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables ensured.")
# 2. Backfill historical posts on startup (fast, no Claude API call)
asyncio.create_task(backfill_history(AsyncSessionLocal, limit=500))
# 3. Start Binance WebSocket background task
_binance_task = asyncio.create_task(run_binance_ws(), name="binance_ws")
logger.info("Binance WebSocket task started.")
# 3. Start Truth Social poller via APScheduler
_scheduler = AsyncIOScheduler()
_scheduler.add_job(
poll_truth_social,
"interval",
seconds=settings.truth_social_poll_seconds,
args=[AsyncSessionLocal],
id="truth_social_poll",
max_instances=1,
coalesce=True,
)
_scheduler.start()
logger.info(
"Truth Social poller scheduled every %ds.", settings.truth_social_poll_seconds
)
yield
# Shutdown
logger.info("Shutting down background tasks...")
if _scheduler:
_scheduler.shutdown(wait=False)
if _binance_task and not _binance_task.done():
_binance_task.cancel()
try:
await _binance_task
except asyncio.CancelledError:
pass
await engine.dispose()
logger.info("Shutdown complete.")
app = FastAPI(
title="TrumpSignal API",
version="1.0.0",
description="Crypto trading signals derived from Trump's Truth Social posts.",
lifespan=lifespan,
)
# CORS
allowed_origins = [
settings.frontend_url,
"http://localhost:3001",
"http://localhost:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# REST routers
app.include_router(posts_router, prefix="/api")
app.include_router(prices_router, prefix="/api")
app.include_router(trades_router, prefix="/api")
app.include_router(performance_router, prefix="/api")
app.include_router(subscribe_router, prefix="/api")
app.include_router(user_router, prefix="/api")
@app.get("/api/health")
async def health():
return {"status": "ok"}
if settings.environment == "development":
from app.api.dev import router as dev_router
app.include_router(dev_router, prefix="/api")
# ── WebSocket endpoints ────────────────────────────────────────────────────
@app.websocket("/ws/prices")
async def ws_prices(websocket: WebSocket):
"""Subscribe to live price ticks (type: 'price')."""
await manager.connect(websocket)
try:
while True:
# Keep connection alive; messages are pushed via manager.broadcast
await websocket.receive_text()
except WebSocketDisconnect:
await manager.disconnect(websocket)
except Exception as exc:
logger.warning("WebSocket /ws/prices error: %s", exc)
await manager.disconnect(websocket)
@app.websocket("/ws/posts")
async def ws_posts(websocket: WebSocket):
"""Subscribe to new post events (type: 'new_post')."""
await manager.connect(websocket)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
await manager.disconnect(websocket)
except Exception as exc:
logger.warning("WebSocket /ws/posts error: %s", exc)
await manager.disconnect(websocket)