""" Sentinel Arbitrage Engine - v16.0 FINAL (Correct Mount) The definitive, money-spinning engine. This version uses the correct FastAPI and Socket.IO mounting strategy for guaranteed execution. """ import asyncio import os import json import time from contextlib import asynccontextmanager from datetime import datetime, timezone import httpx import socketio from fastapi import FastAPI from fastapi.staticfiles import StaticFiles # Relative imports for our package structure from .price_fetcher import PriceFetcher from .arbitrage_analyzer import ArbitrageAnalyzer OPPORTUNITY_THRESHOLD = 0.0015 # --- Socket.IO Server Setup --- # This creates the server instance that will handle all real-time communication. sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*') # --- Background Engine --- async def run_arbitrage_detector(price_fetcher, analyzer): """The core engine loop; detects opportunities and emits them via Socket.IO.""" while True: try: await price_fetcher.update_prices_async() all_prices = price_fetcher.get_all_prices() for asset, prices in all_prices.items(): pyth_price = prices.get("pyth") chainlink_price = prices.get("chainlink_agg") if pyth_price and chainlink_price and pyth_price > 0: spread = abs(pyth_price - chainlink_price) / chainlink_price if spread > OPPORTUNITY_THRESHOLD: current_time = time.time() # Simple throttle to avoid spamming Gemini for the same opportunity if not hasattr(analyzer, 'last_call') or current_time - analyzer.last_call.get(asset, 0) > 60: analyzer.last_call = getattr(analyzer, 'last_call', {}) analyzer.last_call[asset] = current_time opportunity = { "asset": asset, "pyth_price": pyth_price, "chainlink_price": chainlink_price, "spread_pct": spread * 100 } print(f"⚡️ Dislocation for {asset}: {opportunity['spread_pct']:.3f}%") briefing = await analyzer.get_alpha_briefing(asset, opportunity) if briefing: signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()} await sio.emit('new_signal', signal) print(f"✅ Signal Emitted for {asset}: {signal['strategy']}") except Exception as e: print(f"❌ ERROR in engine loop: {e}") await asyncio.sleep(15) # --- FastAPI Lifespan (for background task) --- @asynccontextmanager async def lifespan(app: FastAPI): print("🚀 Initializing Sentinel Arbitrage Engine v16.0...") async with httpx.AsyncClient() as client: # The background task is started using the Socket.IO server's robust method sio.start_background_task( run_arbitrage_detector, PriceFetcher(client), ArbitrageAnalyzer(client) ) print("✅ Engine is online and hunting for opportunities.") yield # No explicit shutdown needed for the background task here, # as it's tied to the server process. # --- FastAPI App & Final ASGI App --- # We define the FastAPI app instance first, including its lifespan. app = FastAPI(lifespan=lifespan) # THE CRITICAL FIX: We mount the Socket.IO ASGI app ONTO the FastAPI app. # This makes FastAPI the main application. app.mount('/socket.io', socketio.ASGIApp(sio)) # THE SECOND CRITICAL FIX: We mount the StaticFiles ONTO the FastAPI app as well. # This correctly assigns the responsibility of serving files to FastAPI. app.mount("/", StaticFiles(directory="static", html=True), name="static") # --- Socket.IO Event Handlers --- @sio.event async def connect(sid, environ): print(f"✅ Client connected: {sid}") @sio.event async def disconnect(sid): print(f"🔥 Client disconnected: {sid}")