File size: 4,567 Bytes
b159555
b8d0337
ea758f6
b8d0337
 
b159555
 
c6f94f2
b159555
56eb560
b8d0337
b159555
b8d0337
 
7ab0ea7
b159555
7ab0ea7
6a1e668
 
 
510ee6f
7ab0ea7
b349d30
0e87c05
 
7ab0ea7
6a1e668
 
 
b8d0337
 
6a1e668
 
 
 
b8d0337
073930c
b8d0337
7ab0ea7
073930c
b8d0337
 
 
 
1690b33
 
 
b8d0337
1690b33
 
b8d0337
 
 
 
 
 
 
 
 
 
510ee6f
1690b33
073930c
7ab0ea7
b8d0337
b159555
7ab0ea7
b8d0337
7ab0ea7
b8d0337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b159555
7ab0ea7
b8d0337
 
7ab0ea7
b8d0337
 
 
 
 
 
7ab0ea7
 
 
 
 
 
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
102
103
"""
Sentinel Arbitrage Engine - v11.0 FINAL (Multi-Asset)

Detects and analyzes price dislocations for multiple assets across
decentralized oracles.
"""
import asyncio
import os
from contextlib import asynccontextmanager
from datetime import datetime, timezone
import json
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles # <-- We use this instead of Jinja2

# Relative imports for package structure
from .price_fetcher import PriceFetcher
from .arbitrage_analyzer import ArbitrageAnalyzer
from .broker import signal_broker

OPPORTUNITY_THRESHOLD = 0.0015  # 0.15% price difference

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Manages application startup and shutdown events."""
    async with httpx.AsyncClient() as client:
        app.state.price_fetcher = PriceFetcher(client)
        app.state.arbitrage_analyzer = ArbitrageAnalyzer(client)
        arbitrage_task = asyncio.create_task(run_arbitrage_detector(app))
        print("🚀 Sentinel Arbitrage Engine v11.0 (Multi-Asset) started.")
        yield
        print("⏳ Shutting down engine...")
        arbitrage_task.cancel()
        try: await arbitrage_task
        except asyncio.CancelledError: print("Engine shut down.")

async def run_arbitrage_detector(app: FastAPI):
    """The core engine loop. Checks for opportunities and queues them."""
    while True:
        await app.state.price_fetcher.update_prices_async()
        all_prices = app.state.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:
                    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 app.state.arbitrage_analyzer.get_alpha_briefing(asset, opportunity)
                    if briefing:
                        signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
                        await signal_broker.queue.put(signal)

        await asyncio.sleep(15)

# --- FastAPI App Initialization ---
app = FastAPI(title="Sentinel Arbitrage Engine", lifespan=lifespan)

# --- HTML Rendering Helper ---
def render_signal_card(payload: dict) -> str:
    """Renders a dictionary of analysis into a styled HTML table row."""
    s = payload
    time_str = datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S UTC')
    pyth_class = "buy" if s['pyth_price'] < s['chainlink_price'] else "sell"
    chainlink_class = "sell" if s['pyth_price'] < s['chainlink_price'] else "buy"
    
    return f"""
    <tr hx-swap-oob="afterbegin:#opportunities-table">
        <td><strong>{s['asset']}/USD</strong></td>
        <td><span class="{pyth_class}">Pyth Network</span><br>${s['pyth_price']:,.2f}</td>
        <td><span class="{chainlink_class}">Chainlink Agg.</span><br>${s['chainlink_price']:,.2f}</td>
        <td><strong>{s['spread_pct']:.3f}%</strong></td>
        <td><span class="risk-{s.get('risk', 'low').lower()}">{s.get('risk', 'N/A')}</span></td>
        <td>{s.get('rationale', 'N/A')}</td>
        <td><button class="trade-btn">{s.get('strategy', 'N/A')}</button></td>
    </tr>
    <div id="last-update-time" hx-swap-oob="true">{time_str}</div>
    """

# --- API Endpoints ---
@app.get("/api/signals/stream")
async def signal_stream(request: Request):
    """SSE stream for the automated Signal Stream."""
    async def event_generator():
        while True:
            payload = await signal_broker.queue.get()
            html_card = render_signal_card(payload)
            data_payload = html_card.replace('\n', ' ').strip()
            yield f"event: message\ndata: {data_payload}\n\n"
    return StreamingResponse(event_generator(), media_type="text/event-stream")

# --- Static File Server ---
# This single mount point serves index.html for the root path "/"
# and any other files like CSS or JS from the "static" directory.
app.mount("/", StaticFiles(directory="static", html=True), name="static")