152 lines
4.6 KiB
Python
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)
|