Spaces:
Sleeping
Sleeping
""" | |
Sentinel Arbitrage Engine - v13.1 FINAL (UI Fix) | |
This version uses a file-based log for absolute signal persistence and | |
a high-frequency polling mechanism for guaranteed data delivery. | |
It includes the corrected HTML rendering for a professional UI. | |
""" | |
import asyncio | |
import os | |
import json | |
import time | |
from contextlib import asynccontextmanager | |
from datetime import datetime, timezone | |
import httpx | |
from fastapi import FastAPI, Request | |
from fastapi.responses import FileResponse, HTMLResponse | |
from fastapi.staticfiles import StaticFiles | |
from .price_fetcher import PriceFetcher | |
from .arbitrage_analyzer import ArbitrageAnalyzer | |
OPPORTUNITY_THRESHOLD = 0.0015 | |
SIGNALS_FILE = "signals.json" | |
async def lifespan(app: FastAPI): | |
if os.path.exists(SIGNALS_FILE): | |
os.remove(SIGNALS_FILE) | |
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.state.price_fetcher, app.state.arbitrage_analyzer) | |
) | |
print("π Sentinel Arbitrage Engine v13.1 (UI Fix) started.") | |
yield | |
print("β³ Shutting down engine...") | |
arbitrage_task.cancel() | |
try: await arbitrage_task | |
except asyncio.CancelledError: print("Engine shut down gracefully.") | |
async def run_arbitrage_detector(price_fetcher, analyzer): | |
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: | |
opportunity = { | |
"asset": asset, "pyth_price": pyth_price, | |
"chainlink_price": chainlink_price, "spread_pct": spread * 100 | |
} | |
briefing = await analyzer.get_alpha_briefing(asset, opportunity) | |
if briefing: | |
signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()} | |
try: | |
data = [] | |
if os.path.exists(SIGNALS_FILE): | |
with open(SIGNALS_FILE, 'r') as f: | |
data = json.load(f) | |
data.insert(0, signal) | |
with open(SIGNALS_FILE, 'w') as f: | |
json.dump(data, f) | |
except (FileNotFoundError, json.JSONDecodeError): | |
with open(SIGNALS_FILE, 'w') as f: | |
json.dump([signal], f) | |
print(f"β Signal LOGGED for {asset}: {signal['spread_pct']:.3f}%") | |
except Exception as e: | |
print(f"β ERROR in engine loop: {e}") | |
await asyncio.sleep(15) | |
app = FastAPI(lifespan=lifespan) | |
# ==================================================================== | |
# THE CRITICAL FIX IS HERE | |
# ==================================================================== | |
def render_signal_card(signal: dict) -> str: | |
"""Renders a single signal dictionary into a clean HTML table row.""" | |
s = signal | |
time_str = datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S') | |
# Determine which price is higher/lower for coloring | |
is_pyth_cheaper = s['pyth_price'] < s['chainlink_price'] | |
pyth_price_html = f'<span class="{"buy" if is_pyth_cheaper else "sell"}">${s["pyth_price"]:,.2f}</span>' | |
chainlink_price_html = f'<span class="{"sell" if is_pyth_cheaper else "buy"}">${s["chainlink_price"]:,.2f}</span>' | |
# Build the complete table row with 7 cells | |
return f""" | |
<tr> | |
<td>{time_str}</td> | |
<td><strong>{s['asset']}/USD</strong></td> | |
<td>{pyth_price_html}</td> | |
<td>{chainlink_price_html}</td> | |
<td><strong class="buy">{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('strategy', 'N/A')}</td> | |
</tr> | |
""" | |
# ==================================================================== | |
async def get_signals_table(request: Request): | |
"""Reads the signals file and renders the entire table body.""" | |
try: | |
with open(SIGNALS_FILE, 'r') as f: | |
signals = json.load(f) | |
except (FileNotFoundError, json.JSONDecodeError): | |
signals = [] | |
if not signals: | |
return HTMLResponse('<tr><td colspan="7" style="text-align:center;">Monitoring for arbitrage opportunities...</td></tr>') | |
# Generate all table rows | |
table_rows_html = "".join([render_signal_card(s) for s in signals]) | |
# Calculate total simulated profit | |
total_profit = 0 | |
for s in signals: | |
profit = abs(s['chainlink_price'] - s['pyth_price']) | |
total_profit += profit * (1 - 0.002) # Assume 0.2% total fees | |
# Create the P/L ticker with an OOB swap attribute | |
profit_html = f'<span id="pnl-ticker" hx-swap-oob="true">Simulated P/L: <span style="color: #34D399;">${total_profit:,.2f}</span></span>' | |
# Return the P/L ticker and the table rows together | |
return HTMLResponse(profit_html + table_rows_html) | |
# Serve the static files (like index.html) | |
app.mount("/", StaticFiles(directory="static", html=True), name="static") |