File size: 4,497 Bytes
b159555
c3b443a
ea758f6
c3b443a
 
b159555
 
c6f94f2
b159555
56eb560
510ee6f
b159555
431c9a5
b159555
 
 
01e217d
510ee6f
 
c3b443a
b349d30
0e87c05
 
 
510ee6f
 
431c9a5
c3b443a
6ca5793
a17b947
c3b443a
c6f94f2
0e87c05
6ca5793
510ee6f
6ca5793
 
073930c
510ee6f
c3b443a
073930c
510ee6f
 
 
6ca5793
 
510ee6f
6ca5793
 
 
 
 
 
 
 
 
 
510ee6f
 
 
 
 
073930c
 
510ee6f
b159555
 
c3b443a
 
431c9a5
073930c
ea758f6
6ca5793
 
510ee6f
a17b947
6ca5793
 
 
 
510ee6f
 
6ca5793
 
510ee6f
 
a17b947
b159555
 
073930c
b159555
 
431c9a5
 
24a8706
c6f94f2
 
 
ea758f6
 
 
a17b947
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
"""
Sentinel Arbitrage Engine - v6.0 FINAL (Rate-Limit Proof)

Detects on-chain vs. off-chain price discrepancies using a fault-tolerant,
multi-source data aggregator.
"""
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.templating import Jinja2Templates

from .price_fetcher import PriceFetcher
from .arbitrage_analyzer import ArbitrageAnalyzer

OPPORTUNITY_THRESHOLD = 0.001  # 0.1% price difference

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with httpx.AsyncClient() as client:
        app.state.price_fetcher = PriceFetcher(client=client)
        app.state.arbitrage_analyzer = ArbitrageAnalyzer(client=client)
        app.state.signal_queue: asyncio.Queue = asyncio.Queue()
        # Slowing down the loop slightly to be respectful to APIs
        arbitrage_task = asyncio.create_task(run_arbitrage_detector(app, 10))
        
        print("🚀 Sentinel Arbitrage Engine v6.0 started successfully.")
        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, interval_seconds: int):
    # This loop logic remains the same, but it's now fed by a more reliable PriceFetcher
    while True:
        await app.state.price_fetcher.update_prices_async()
        prices = app.state.price_fetcher.get_current_prices()
        
        on_chain = prices.get("on_chain_pyth")
        off_chain = prices.get("off_chain_agg")
        
        if on_chain and off_chain:
            spread = abs(on_chain - off_chain) / off_chain
            if spread > OPPORTUNITY_THRESHOLD:
                opportunity = {
                    "id": f"{int(datetime.now().timestamp())}",
                    "on_chain_price": on_chain,
                    "off_chain_price": off_chain,
                    "spread_pct": spread * 100
                }
                print(f"⚡️ Discrepancy Detected: {opportunity['spread_pct']:.3f}%")
                briefing = await app.state.arbitrage_analyzer.get_alpha_briefing(opportunity)
                if briefing:
                    signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
                    await app.state.signal_queue.put(signal)

        await asyncio.sleep(interval_seconds)

app = FastAPI(title="Sentinel Arbitrage Engine", lifespan=lifespan)
templates = Jinja2Templates(directory="templates")

# No changes needed to render_signal_card, GET /, or the SSE stream endpoint.
# The code below this line is the same as the previous version.
def render_signal_card(payload: dict) -> str:
    s = payload
    time_str = datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S UTC')
    on_chain_class = "buy" if s['on_chain_price'] < s['off_chain_price'] else "sell"
    off_chain_class = "sell" if s['on_chain_price'] < s['off_chain_price'] else "buy"

    return f"""
    <tr id="trade-row-{s['id']}" hx-swap-oob="afterbegin:#opportunities-table">
        <td><strong>BTC/USD</strong></td>
        <td><span class="{on_chain_class}">On-Chain (Pyth)</span><br>${s['on_chain_price']:,.2f}</td>
        <td><span class="{off_chain_class}">Off-Chain (Agg)</span><br>${s['off_chain_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>
    """

@app.get("/", response_class=HTMLResponse)
async def serve_dashboard(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.get("/api/signals/stream")
async def signal_stream(request: Request):
    queue: asyncio.Queue = request.app.state.signal_queue
    async def event_generator():
        while True:
            payload = await 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")