mgbam commited on
Commit
cfa3962
Β·
verified Β·
1 Parent(s): 7ab0ea7

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +83 -73
app/app.py CHANGED
@@ -1,103 +1,113 @@
1
  """
2
- Sentinel Arbitrage Engine - v11.0 FINAL (Multi-Asset)
3
 
4
- Detects and analyzes price dislocations for multiple assets across
5
- decentralized oracles.
 
6
  """
7
  import asyncio
8
  import os
9
  from contextlib import asynccontextmanager
10
- from datetime import datetime, timezone
11
  import json
 
 
 
12
  import httpx
13
- from fastapi import FastAPI, Request
14
- from fastapi.responses import HTMLResponse, StreamingResponse
15
- from fastapi.staticfiles import StaticFiles # <-- We use this instead of Jinja2
16
 
17
- # Relative imports for package structure
 
18
  from .price_fetcher import PriceFetcher
19
  from .arbitrage_analyzer import ArbitrageAnalyzer
20
- from .broker import signal_broker
21
 
22
- OPPORTUNITY_THRESHOLD = 0.0015 # 0.15% price difference
23
 
 
 
 
 
 
 
 
 
24
  @asynccontextmanager
25
  async def lifespan(app: FastAPI):
26
- """Manages application startup and shutdown events."""
 
27
  async with httpx.AsyncClient() as client:
 
28
  app.state.price_fetcher = PriceFetcher(client)
29
  app.state.arbitrage_analyzer = ArbitrageAnalyzer(client)
30
- arbitrage_task = asyncio.create_task(run_arbitrage_detector(app))
31
- print("πŸš€ Sentinel Arbitrage Engine v11.0 (Multi-Asset) started.")
32
- yield
 
 
 
 
 
 
33
  print("⏳ Shutting down engine...")
34
  arbitrage_task.cancel()
35
  try: await arbitrage_task
36
- except asyncio.CancelledError: print("Engine shut down.")
37
 
38
- async def run_arbitrage_detector(app: FastAPI):
39
- """The core engine loop. Checks for opportunities and queues them."""
40
  while True:
41
- await app.state.price_fetcher.update_prices_async()
42
- all_prices = app.state.price_fetcher.get_all_prices()
43
-
44
- for asset, prices in all_prices.items():
45
- pyth_price = prices.get("pyth")
46
- chainlink_price = prices.get("chainlink_agg")
47
 
48
- if pyth_price and chainlink_price and pyth_price > 0:
49
- spread = abs(pyth_price - chainlink_price) / chainlink_price
50
- if spread > OPPORTUNITY_THRESHOLD:
51
- opportunity = {
52
- "asset": asset,
53
- "pyth_price": pyth_price, "chainlink_price": chainlink_price,
54
- "spread_pct": spread * 100
55
- }
56
- print(f"⚑️ Dislocation for {asset}: {opportunity['spread_pct']:.3f}%")
57
- briefing = await app.state.arbitrage_analyzer.get_alpha_briefing(asset, opportunity)
58
- if briefing:
59
- signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
60
- await signal_broker.queue.put(signal)
 
 
 
 
 
 
61
 
62
  await asyncio.sleep(15)
63
 
64
- # --- FastAPI App Initialization ---
65
- app = FastAPI(title="Sentinel Arbitrage Engine", lifespan=lifespan)
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- # --- HTML Rendering Helper ---
68
- def render_signal_card(payload: dict) -> str:
69
- """Renders a dictionary of analysis into a styled HTML table row."""
70
- s = payload
71
- time_str = datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S UTC')
72
- pyth_class = "buy" if s['pyth_price'] < s['chainlink_price'] else "sell"
73
- chainlink_class = "sell" if s['pyth_price'] < s['chainlink_price'] else "buy"
74
-
75
- return f"""
76
- <tr hx-swap-oob="afterbegin:#opportunities-table">
77
- <td><strong>{s['asset']}/USD</strong></td>
78
- <td><span class="{pyth_class}">Pyth Network</span><br>${s['pyth_price']:,.2f}</td>
79
- <td><span class="{chainlink_class}">Chainlink Agg.</span><br>${s['chainlink_price']:,.2f}</td>
80
- <td><strong>{s['spread_pct']:.3f}%</strong></td>
81
- <td><span class="risk-{s.get('risk', 'low').lower()}">{s.get('risk', 'N/A')}</span></td>
82
- <td>{s.get('rationale', 'N/A')}</td>
83
- <td><button class="trade-btn">{s.get('strategy', 'N/A')}</button></td>
84
- </tr>
85
- <div id="last-update-time" hx-swap-oob="true">{time_str}</div>
86
- """
87
 
88
- # --- API Endpoints ---
89
- @app.get("/api/signals/stream")
90
- async def signal_stream(request: Request):
91
- """SSE stream for the automated Signal Stream."""
92
- async def event_generator():
93
- while True:
94
- payload = await signal_broker.queue.get()
95
- html_card = render_signal_card(payload)
96
- data_payload = html_card.replace('\n', ' ').strip()
97
- yield f"event: message\ndata: {data_payload}\n\n"
98
- return StreamingResponse(event_generator(), media_type="text/event-stream")
99
 
100
- # --- Static File Server ---
101
- # This single mount point serves index.html for the root path "/"
102
- # and any other files like CSS or JS from the "static" directory.
103
- app.mount("/", StaticFiles(directory="static", html=True), name="static")
 
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()
55
  try: await arbitrage_task
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()
63
+ all_prices = price_fetcher.get_all_prices()
 
 
 
64
 
65
+ for asset, prices in all_prices.items():
66
+ pyth_price = prices.get("pyth")
67
+ chainlink_price = prices.get("chainlink_agg")
68
+
69
+ if pyth_price and chainlink_price and pyth_price > 0:
70
+ spread = abs(pyth_price - chainlink_price) / chainlink_price
71
+ if spread > OPPORTUNITY_THRESHOLD:
72
+ opportunity = {
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.