File size: 3,910 Bytes
b159555
acb6f17
ea758f6
acb6f17
 
 
b159555
 
c6f94f2
b8d0337
acb6f17
cfa3962
acb6f17
b159555
acb6f17
 
cfa3962
b159555
acb6f17
6a1e668
510ee6f
acb6f17
2ca2f4e
acb6f17
 
 
b349d30
acb6f17
 
 
 
073930c
acb6f17
 
 
073930c
cfa3962
 
acb6f17
1690b33
acb6f17
cfa3962
 
 
acb6f17
 
cfa3962
acb6f17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfa3962
 
510ee6f
1690b33
073930c
acb6f17
 
 
 
 
 
 
 
 
 
 
 
362692f
 
acb6f17
 
 
7ab0ea7
acb6f17
 
 
 
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
"""
Sentinel Arbitrage Engine - v15.0 OMEGA (Socket.IO Perfected)

The definitive, money-spinning engine. This version uses a perfectly
configured Socket.IO server to push real-time, multi-asset arbitrage
signals directly to a dynamic frontend. This is the final architecture.
"""
import asyncio
import os
import json
import time
from datetime import datetime, timezone

import httpx
import socketio
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from .price_fetcher import PriceFetcher
from .arbitrage_analyzer import ArbitrageAnalyzer

OPPORTUNITY_THRESHOLD = 0.0015

# --- Socket.IO Server Setup ---
# We create the server instance that will manage all real-time communication.
sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')

# --- FastAPI App Setup ---
# The FastAPI app is now simpler. It only serves the static files.
app = FastAPI()
app.mount("/", StaticFiles(directory="static", html=True), name="static")

# --- 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)

# --- Socket.IO Lifespan Events ---
# This is the CORRECT way to manage background tasks with python-socketio.
@sio.on('connect')
async def connect(sid, environ):
    print(f"βœ… Client connected: {sid}")
    # Start the engine only when the first user connects.
    if sio.background_task is None:
        print("πŸš€ First client connected. Starting Sentinel Engine...")
        price_fetcher = PriceFetcher(httpx.AsyncClient())
        arbitrage_analyzer = ArbitrageAnalyzer(httpx.AsyncClient())
        sio.background_task = sio.start_background_task(
            run_arbitrage_detector, price_fetcher, arbitrage_analyzer
        )

@sio.on('disconnect')
def disconnect(sid):
    print(f"πŸ”₯ Client disconnected: {sid}")

# --- Final ASGI App ---
# We wrap the FastAPI app (for static files) and the Socket.IO app together.
# The Socket.IO server is the primary application.
combined_app = socketio.ASGIApp(sio, other_asgi_app=app)