Spaces:
Sleeping
Sleeping
File size: 5,949 Bytes
b159555 243f098 ea758f6 259af0a 243f098 b159555 c6f94f2 b8d0337 cfa3962 259af0a cfa3962 b159555 259af0a cfa3962 b159555 6a1e668 510ee6f cfa3962 259af0a b349d30 0e87c05 259af0a 6a1e668 cfa3962 243f098 259af0a cfa3962 6a1e668 cfa3962 073930c cfa3962 073930c cfa3962 1690b33 cfa3962 259af0a 243f098 259af0a 243f098 259af0a cfa3962 510ee6f 1690b33 073930c cfa3962 243f098 259af0a 243f098 259af0a 243f098 259af0a 243f098 259af0a 243f098 cfa3962 243f098 259af0a b159555 243f098 7ab0ea7 243f098 259af0a |
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
"""
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") |