File size: 4,191 Bytes
b159555
d944095
ea758f6
d944095
 
b159555
 
c6f94f2
b8d0337
acb6f17
3b67fa5
cfa3962
acb6f17
b159555
acb6f17
 
cfa3962
b159555
d944095
acb6f17
6a1e668
510ee6f
acb6f17
2ca2f4e
acb6f17
d944095
acb6f17
b349d30
acb6f17
 
d944095
073930c
cfa3962
 
acb6f17
d944095
acb6f17
d944095
 
 
acb6f17
 
cfa3962
acb6f17
d944095
acb6f17
 
 
d944095
 
 
 
 
acb6f17
 
 
 
 
 
cfa3962
 
d944095
1690b33
073930c
d501c8b
 
 
 
 
d944095
 
 
 
 
 
d501c8b
 
d944095
 
362692f
d944095
 
d501c8b
7ab0ea7
d944095
 
 
3b67fa5
d944095
 
 
d501c8b
 
d944095
d501c8b
 
 
3b67fa5
d501c8b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""
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}")