Spaces:
Sleeping
Sleeping
""" | |
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 | |
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 --- | |
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") |