mgbam's picture
Update app/app.py
243f098 verified
raw
history blame
5.95 kB
"""
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"
@asynccontextmanager
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>
"""
# ====================================================================
@app.get("/api/signals", response_class=HTMLResponse)
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")