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)) # 2b. Rehydrate TP/SL + max-hold backstops for any positions still open in DB. # Critical: without this, a redeploy/crash silently drops protection on live trades. from app.services.recovery import rehydrate_open_trades asyncio.create_task(rehydrate_open_trades()) # 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)