mgbam commited on
Commit
c1c0c08
Β·
verified Β·
1 Parent(s): 3e3365d

Update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +50 -41
app/main.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
- Sentinel Arbitrage Engine - v19.0 FINAL (Correct Mounting Architecture)
3
 
4
- This version uses the official, correct architecture for combining FastAPI and
5
- Socket.IO, resolving the 'mount' attribute error permanently.
6
  """
7
  import asyncio
8
  import os
@@ -10,26 +10,35 @@ import json
10
  import time
11
  from contextlib import asynccontextmanager
12
  from datetime import datetime, timezone
13
-
14
  import httpx
15
- import socketio
16
  from fastapi import FastAPI
17
  from fastapi.staticfiles import StaticFiles
 
18
 
19
- # --- RELATIVE IMPORTS ---
20
- # This assumes your project is structured with an 'app' package.
21
  from .price_fetcher import PriceFetcher
22
  from .arbitrage_analyzer import ArbitrageAnalyzer
23
 
24
  OPPORTUNITY_THRESHOLD = 0.0015
 
25
 
26
- # --- SOCKET.IO SERVER SETUP ---
27
- # Create the server instance that will handle all real-time communication.
28
- sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- # --- BACKGROUND ENGINE ---
31
  async def run_arbitrage_detector(price_fetcher, analyzer):
32
- """The core engine loop; detects opportunities and emits them via Socket.IO."""
33
  while True:
34
  try:
35
  await price_fetcher.update_prices_async()
@@ -41,47 +50,47 @@ async def run_arbitrage_detector(price_fetcher, analyzer):
41
  spread = abs(pyth_price - chainlink_price) / chainlink_price
42
  if spread > OPPORTUNITY_THRESHOLD:
43
  current_time = time.time()
44
- # Simple throttle to avoid spamming Gemini for the same opportunity
45
  if not hasattr(analyzer, 'last_call') or current_time - analyzer.last_call.get(asset, 0) > 60:
46
  analyzer.last_call = getattr(analyzer, 'last_call', {})
47
  analyzer.last_call[asset] = current_time
48
  opportunity = {"asset": asset, "pyth_price": pyth_price, "chainlink_price": chainlink_price, "spread_pct": spread * 100}
49
- print(f"⚑️ Dislocation for {asset}: {opportunity['spread_pct']:.3f}%")
50
  briefing = await analyzer.get_alpha_briefing(asset, opportunity)
51
  if briefing:
52
  signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
53
- await sio.emit('new_signal', signal)
54
- print(f"βœ… Signal Emitted for {asset}: {signal['strategy']}")
 
 
55
  except Exception as e:
56
  print(f"❌ ERROR in engine loop: {e}")
57
  await asyncio.sleep(15)
58
 
59
- # --- FASTAPI APP & LIFESPAN ---
60
- # This is the correct way to manage the background task with Socket.IO
61
- @sio.on('connect')
62
- async def connect(sid, environ):
63
- print(f"βœ… Client connected: {sid}")
64
- # Start the engine only when the first user connects.
65
- if not hasattr(sio, 'background_task') or sio.background_task.done():
66
- print("πŸš€ First client connected. Starting Sentinel Engine...")
67
- # Note: We create clients here as they are not easily shared across lifespan contexts
68
- price_fetcher = PriceFetcher(httpx.AsyncClient())
69
- arbitrage_analyzer = ArbitrageAnalyzer(httpx.AsyncClient())
70
- sio.background_task = sio.start_background_task(run_arbitrage_detector, price_fetcher, arbitrage_analyzer)
71
-
72
- @sio.on('disconnect')
73
- def disconnect(sid):
74
- print(f"πŸ”₯ Client disconnected: {sid}")
75
 
76
- # --- FINAL APP ASSEMBLY ---
77
- # 1. Create the main FastAPI instance.
78
- app = FastAPI()
79
-
80
- # 2. Create the Socket.IO ASGI application.
81
- socket_app = socketio.ASGIApp(sio)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- # 3. Mount the Socket.IO app onto the FastAPI app at the correct path.
84
- app.mount("/socket.io", socket_app)
85
 
86
- # 4. Mount the StaticFiles directory to serve index.html at the root.
87
  app.mount("/", StaticFiles(directory="static", html=True), name="static")
 
1
  """
2
+ Sentinel Arbitrage Engine - v20.0 FINAL (Raw Stream)
3
 
4
+ This version uses a file-based log and a raw, unstoppable streaming
5
+ endpoint to guarantee signal delivery. This is the final architecture.
6
  """
7
  import asyncio
8
  import os
 
10
  import time
11
  from contextlib import asynccontextmanager
12
  from datetime import datetime, timezone
 
13
  import httpx
 
14
  from fastapi import FastAPI
15
  from fastapi.staticfiles import StaticFiles
16
+ from fastapi.responses import StreamingResponse
17
 
 
 
18
  from .price_fetcher import PriceFetcher
19
  from .arbitrage_analyzer import ArbitrageAnalyzer
20
 
21
  OPPORTUNITY_THRESHOLD = 0.0015
22
+ SIGNALS_FILE = "signals.log"
23
 
24
+ @asynccontextmanager
25
+ async def lifespan(app: FastAPI):
26
+ if os.path.exists(SIGNALS_FILE): os.remove(SIGNALS_FILE)
27
+
28
+ async with httpx.AsyncClient() as client:
29
+ price_fetcher = PriceFetcher(client)
30
+ arbitrage_analyzer = ArbitrageAnalyzer(client)
31
+
32
+ app.state.engine_task = asyncio.create_task(
33
+ run_arbitrage_detector(price_fetcher, arbitrage_analyzer)
34
+ )
35
+ print("πŸš€ Sentinel Engine v20.0 (Raw Stream) is ONLINE.")
36
+ yield
37
+ app.state.engine_task.cancel()
38
+ print("βœ… Engine shut down.")
39
 
 
40
  async def run_arbitrage_detector(price_fetcher, analyzer):
41
+ """The core engine loop; writes signals to a log file."""
42
  while True:
43
  try:
44
  await price_fetcher.update_prices_async()
 
50
  spread = abs(pyth_price - chainlink_price) / chainlink_price
51
  if spread > OPPORTUNITY_THRESHOLD:
52
  current_time = time.time()
 
53
  if not hasattr(analyzer, 'last_call') or current_time - analyzer.last_call.get(asset, 0) > 60:
54
  analyzer.last_call = getattr(analyzer, 'last_call', {})
55
  analyzer.last_call[asset] = current_time
56
  opportunity = {"asset": asset, "pyth_price": pyth_price, "chainlink_price": chainlink_price, "spread_pct": spread * 100}
 
57
  briefing = await analyzer.get_alpha_briefing(asset, opportunity)
58
  if briefing:
59
  signal = {**opportunity, **briefing, "timestamp": datetime.now(timezone.utc).isoformat()}
60
+ # --- THE FIX: Append as a single line to a log file ---
61
+ with open(SIGNALS_FILE, "a") as f:
62
+ f.write(json.dumps(signal) + "\n")
63
+ print(f"βœ… Signal LOGGED for {asset}")
64
  except Exception as e:
65
  print(f"❌ ERROR in engine loop: {e}")
66
  await asyncio.sleep(15)
67
 
68
+ # --- FastAPI App Setup ---
69
+ app = FastAPI(lifespan=lifespan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ @app.get("/api/signals/stream")
72
+ async def signal_stream():
73
+ """This endpoint streams the content of the signals.log file line by line."""
74
+ async def event_generator():
75
+ # Use an async-compatible way to tail the file
76
+ try:
77
+ with open(SIGNALS_FILE, 'r') as f:
78
+ f.seek(0, 2) # Go to the end of the file
79
+ while True:
80
+ line = f.readline()
81
+ if not line:
82
+ await asyncio.sleep(0.5) # Wait for new lines
83
+ continue
84
+ # Stream the line to the client
85
+ yield line
86
+ except FileNotFoundError:
87
+ print("signals.log not created yet, client connected early.")
88
+ # Keep the connection open and wait for the file to be created
89
+ while not os.path.exists(SIGNALS_FILE):
90
+ await asyncio.sleep(1)
91
+ # Re-run the generator now that the file exists
92
+ yield from event_generator()
93
 
94
+ return StreamingResponse(event_generator(), media_type="text/plain")
 
95
 
 
96
  app.mount("/", StaticFiles(directory="static", html=True), name="static")