mgbam commited on
Commit
259af0a
Β·
verified Β·
1 Parent(s): 6340f89

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +67 -50
app/app.py CHANGED
@@ -1,54 +1,43 @@
1
  """
2
- Sentinel Arbitrage Engine - v12.0 FINAL (Correct Mount)
3
 
4
- This version uses the robust singleton broker pattern and correctly mounts
5
- the Socket.IO application to the path expected by the client, ensuring
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 socketio
17
- from fastapi import FastAPI
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
- """Manages the application's startup and shutdown procedures."""
39
- print("πŸš€ Initializing Sentinel Arbitrage Engine v12.0...")
 
 
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("βœ… Engine is online and hunting for opportunities.")
51
- yield # Application runs here
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; detects opportunities and puts them in the broker queue."""
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
- await signal_broker.queue.put(signal)
81
- print(f"βœ… Signal Emitted via Broker: {signal['strategy']}")
 
 
 
 
 
 
 
 
 
 
 
82
  except Exception as e:
83
  print(f"❌ ERROR in engine loop: {e}")
84
 
85
  await asyncio.sleep(15)
86
 
87
- # --- FASTAPI APP SETUP ---
88
  app = FastAPI(lifespan=lifespan)
89
 
90
- # --- SOCKET.IO EVENT HANDLERS ---
91
- @sio.event
92
- async def connect(sid, environ):
93
- """Handles new client connections."""
94
- print(f"βœ… Client connected: {sid}")
95
- # We can even send a welcome message here if we want
96
- await sio.emit('welcome', {'message': 'Connection to Sentinel Engine established!'}, to=sid)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- @sio.event
99
- async def disconnect(sid):
100
- """Handles client disconnections."""
101
- print(f"πŸ”₯ Client disconnected: {sid}")
 
 
 
 
 
 
 
 
 
 
102
 
103
- # --- THE CRITICAL FIX ---
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
- # We no longer need the old SSE endpoint, as Socket.IO handles everything.
112
- # The `run_arbitrage_detector` now emits directly to clients,
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")