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")