Spaces:
Running
Running
Update app/app.py
Browse files- app/app.py +67 -50
app/app.py
CHANGED
@@ -1,54 +1,43 @@
|
|
1 |
"""
|
2 |
-
Sentinel Arbitrage Engine -
|
3 |
|
4 |
-
This version uses
|
5 |
-
|
6 |
-
a successful real-time connection.
|
7 |
"""
|
8 |
import asyncio
|
9 |
import os
|
10 |
-
from contextlib import asynccontextmanager
|
11 |
import json
|
12 |
import time
|
|
|
13 |
from datetime import datetime, timezone
|
14 |
-
|
15 |
import httpx
|
16 |
-
import
|
17 |
-
from fastapi import
|
18 |
from fastapi.staticfiles import StaticFiles
|
19 |
|
20 |
-
# --- IMPORTS ---
|
21 |
-
# Correctly use relative imports for our local package structure
|
22 |
from .price_fetcher import PriceFetcher
|
23 |
from .arbitrage_analyzer import ArbitrageAnalyzer
|
24 |
-
from .broker import signal_broker # Using the robust singleton broker
|
25 |
|
26 |
OPPORTUNITY_THRESHOLD = 0.0015
|
|
|
27 |
|
28 |
-
# --- SOCKET.IO SERVER SETUP ---
|
29 |
-
# This creates the server instance that will handle WebSocket connections
|
30 |
-
sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
|
31 |
-
# This wraps the Socket.IO server in an ASGI application
|
32 |
-
socket_app = socketio.ASGIApp(sio)
|
33 |
-
|
34 |
-
|
35 |
-
# --- APPLICATION LIFESPAN ---
|
36 |
@asynccontextmanager
|
37 |
async def lifespan(app: FastAPI):
|
38 |
-
|
39 |
-
|
|
|
|
|
40 |
async with httpx.AsyncClient() as client:
|
41 |
-
# We can store these on the app state, but the broker handles the critical part
|
42 |
app.state.price_fetcher = PriceFetcher(client)
|
43 |
app.state.arbitrage_analyzer = ArbitrageAnalyzer(client)
|
44 |
|
45 |
-
# Launch the engine as a background task
|
46 |
arbitrage_task = asyncio.create_task(
|
47 |
run_arbitrage_detector(app.state.price_fetcher, app.state.arbitrage_analyzer)
|
48 |
)
|
49 |
|
50 |
-
print("
|
51 |
-
yield
|
52 |
|
53 |
print("β³ Shutting down engine...")
|
54 |
arbitrage_task.cancel()
|
@@ -56,7 +45,7 @@ async def lifespan(app: FastAPI):
|
|
56 |
except asyncio.CancelledError: print("Engine shut down gracefully.")
|
57 |
|
58 |
async def run_arbitrage_detector(price_fetcher, analyzer):
|
59 |
-
"""The core engine loop
|
60 |
while True:
|
61 |
try:
|
62 |
await price_fetcher.update_prices_async()
|
@@ -73,41 +62,69 @@ async def run_arbitrage_detector(price_fetcher, analyzer):
|
|
73 |
"asset": asset, "pyth_price": pyth_price,
|
74 |
"chainlink_price": chainlink_price, "spread_pct": spread * 100
|
75 |
}
|
76 |
-
print(f"β‘οΈ Dislocation for {asset}: {opportunity['spread_pct']:.3f}%")
|
77 |
briefing = await analyzer.get_alpha_briefing(asset, opportunity)
|
78 |
if briefing:
|
79 |
signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
except Exception as e:
|
83 |
print(f"β ERROR in engine loop: {e}")
|
84 |
|
85 |
await asyncio.sleep(15)
|
86 |
|
87 |
-
# ---
|
88 |
app = FastAPI(lifespan=lifespan)
|
89 |
|
90 |
-
# ---
|
91 |
-
@
|
92 |
-
async def
|
93 |
-
"""
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
-
|
104 |
-
# Mount the Socket.IO application at the path the client expects ('/socket.io/').
|
105 |
-
# This MUST come before the root static file mount.
|
106 |
-
app.mount('/socket.io', socket_app)
|
107 |
|
108 |
-
# Mount the static directory to serve index.html at the root path "/".
|
109 |
-
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
110 |
|
111 |
-
#
|
112 |
-
|
113 |
-
# and the `static/index.html`'s JavaScript listens for these events.
|
|
|
1 |
"""
|
2 |
+
Sentinel Arbitrage Engine - v13.0 OMEGA (File-Based & Unbreakable)
|
3 |
|
4 |
+
This version uses a file-based log for absolute signal persistence and
|
5 |
+
a high-frequency polling mechanism for guaranteed data delivery.
|
|
|
6 |
"""
|
7 |
import asyncio
|
8 |
import os
|
|
|
9 |
import json
|
10 |
import time
|
11 |
+
from contextlib import asynccontextmanager
|
12 |
from datetime import datetime, timezone
|
|
|
13 |
import httpx
|
14 |
+
from fastapi import FastAPI, Request
|
15 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
16 |
from fastapi.staticfiles import StaticFiles
|
17 |
|
18 |
+
# --- RELATIVE IMPORTS ---
|
|
|
19 |
from .price_fetcher import PriceFetcher
|
20 |
from .arbitrage_analyzer import ArbitrageAnalyzer
|
|
|
21 |
|
22 |
OPPORTUNITY_THRESHOLD = 0.0015
|
23 |
+
SIGNALS_FILE = "signals.json"
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
@asynccontextmanager
|
26 |
async def lifespan(app: FastAPI):
|
27 |
+
# Clear the log on startup
|
28 |
+
if os.path.exists(SIGNALS_FILE):
|
29 |
+
os.remove(SIGNALS_FILE)
|
30 |
+
|
31 |
async with httpx.AsyncClient() as client:
|
|
|
32 |
app.state.price_fetcher = PriceFetcher(client)
|
33 |
app.state.arbitrage_analyzer = ArbitrageAnalyzer(client)
|
34 |
|
|
|
35 |
arbitrage_task = asyncio.create_task(
|
36 |
run_arbitrage_detector(app.state.price_fetcher, app.state.arbitrage_analyzer)
|
37 |
)
|
38 |
|
39 |
+
print("π Sentinel Arbitrage Engine v13.0 (File-Based) started.")
|
40 |
+
yield
|
41 |
|
42 |
print("β³ Shutting down engine...")
|
43 |
arbitrage_task.cancel()
|
|
|
45 |
except asyncio.CancelledError: print("Engine shut down gracefully.")
|
46 |
|
47 |
async def run_arbitrage_detector(price_fetcher, analyzer):
|
48 |
+
"""The core engine loop. Detects opportunities and writes them to a file."""
|
49 |
while True:
|
50 |
try:
|
51 |
await price_fetcher.update_prices_async()
|
|
|
62 |
"asset": asset, "pyth_price": pyth_price,
|
63 |
"chainlink_price": chainlink_price, "spread_pct": spread * 100
|
64 |
}
|
|
|
65 |
briefing = await analyzer.get_alpha_briefing(asset, opportunity)
|
66 |
if briefing:
|
67 |
signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
|
68 |
+
|
69 |
+
# --- THE FIX: Write signal directly to the JSON file ---
|
70 |
+
try:
|
71 |
+
with open(SIGNALS_FILE, 'r+') as f:
|
72 |
+
data = json.load(f)
|
73 |
+
data.insert(0, signal) # Prepend new signal
|
74 |
+
f.seek(0)
|
75 |
+
json.dump(data, f, indent=4)
|
76 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
77 |
+
with open(SIGNALS_FILE, 'w') as f:
|
78 |
+
json.dump([signal], f, indent=4)
|
79 |
+
|
80 |
+
print(f"β
Signal LOGGED for {asset}: {signal['spread_pct']:.3f}%")
|
81 |
except Exception as e:
|
82 |
print(f"β ERROR in engine loop: {e}")
|
83 |
|
84 |
await asyncio.sleep(15)
|
85 |
|
86 |
+
# --- FastAPI App Setup ---
|
87 |
app = FastAPI(lifespan=lifespan)
|
88 |
|
89 |
+
# --- API Endpoints ---
|
90 |
+
@app.get("/api/signals", response_class=HTMLResponse)
|
91 |
+
async def get_signals_table(request: Request):
|
92 |
+
"""Reads the signals file and renders the table body."""
|
93 |
+
try:
|
94 |
+
with open(SIGNALS_FILE, 'r') as f:
|
95 |
+
signals = json.load(f)
|
96 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
97 |
+
signals = []
|
98 |
+
|
99 |
+
if not signals:
|
100 |
+
return HTMLResponse('<tr id="placeholder-row"><td colspan="7" style="text-align:center;">Monitoring for arbitrage opportunities...</td></tr>')
|
101 |
+
|
102 |
+
table_rows = []
|
103 |
+
total_profit = 0
|
104 |
+
for s in signals:
|
105 |
+
pyth_class = "buy" if s['pyth_price'] < s['chainlink_price'] else "sell"
|
106 |
+
chainlink_class = "sell" if s['pyth_price'] < s['chainlink_price'] else "buy"
|
107 |
+
profit = (s['chainlink_price'] - s['pyth_price']) if pyth_class == 'buy' else (s['pyth_price'] - s['chainlink_price'])
|
108 |
+
profit_after_fees = profit * (1 - 0.002) # Assume 0.2% total fees
|
109 |
+
total_profit += profit_after_fees
|
110 |
|
111 |
+
table_rows.append(f"""
|
112 |
+
<tr>
|
113 |
+
<td>{datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S')}</td>
|
114 |
+
<td><strong>{s['asset']}/USD</strong></td>
|
115 |
+
<td><span class="{pyth_class}">Pyth</span><br>${s['pyth_price']:,.2f}</td>
|
116 |
+
<td><span class="{chainlink_class}">Agg.</span><br>${s['chainlink_price']:,.2f}</td>
|
117 |
+
<td><strong>{s['spread_pct']:.3f}%</strong></td>
|
118 |
+
<td><span class="risk-{s.get('risk', 'low').lower()}">{s.get('risk', 'N/A')}</span></td>
|
119 |
+
<td>{s.get('strategy', 'N/A')}</td>
|
120 |
+
</tr>
|
121 |
+
""")
|
122 |
+
|
123 |
+
# Use OOB swap to update the P/L ticker
|
124 |
+
profit_html = f'<span id="pnl-ticker" hx-swap-oob="true">Simulated P/L: <span style="color: #34D399;">${total_profit:,.2f}</span></span>'
|
125 |
|
126 |
+
return HTMLResponse(profit_html + "".join(table_rows))
|
|
|
|
|
|
|
127 |
|
|
|
|
|
128 |
|
129 |
+
# Serve the static files (like index.html) from the 'static' directory
|
130 |
+
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
|