mgbam commited on
Commit
ea758f6
Β·
verified Β·
1 Parent(s): c3c2e5d

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +37 -50
app/app.py CHANGED
@@ -1,11 +1,12 @@
1
  """
2
- Sentinel TradeFlow Protocol β€” High-performance FastAPI application.
3
- This version includes a live price ticker and robust, structured analysis.
 
 
4
  """
5
  import asyncio
6
  import os
7
  from contextlib import asynccontextmanager
8
- from typing import Optional, Union
9
  from datetime import datetime, timezone
10
 
11
  import httpx
@@ -25,10 +26,10 @@ async def lifespan(app: FastAPI):
25
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
26
  app.state.signal_queue: asyncio.Queue = asyncio.Queue()
27
 
28
- price_task = asyncio.create_task(run_periodic_updates(app.state.price_fetcher, 60))
29
  news_task = asyncio.create_task(run_periodic_news_analysis(app, 300))
30
 
31
- print("πŸš€ Sentinel TradeFlow Protocol started successfully.")
32
  yield
33
 
34
  print("⏳ Shutting down background tasks...")
@@ -37,8 +38,7 @@ async def lifespan(app: FastAPI):
37
  try:
38
  await asyncio.gather(price_task, news_task)
39
  except asyncio.CancelledError:
40
- print("Background tasks cancelled successfully.")
41
- print("βœ… Shutdown complete.")
42
 
43
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
44
  while True:
@@ -48,7 +48,7 @@ async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
48
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
49
  processed_urls = set()
50
  while True:
51
- print(f"πŸ“° Fetching news... (have processed {len(processed_urls)} articles so far)")
52
  try:
53
  top_headlines = app.state.news_api.get_everything(
54
  q='(crypto OR bitcoin OR ethereum) AND (regulation OR partnership OR hack OR update OR adoption)',
@@ -58,20 +58,20 @@ async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
58
  print(f"πŸ“° Found {len(articles)} articles.")
59
 
60
  analyzer: GeminiAnalyzer = app.state.gemini_analyzer
61
- for article in reversed(articles): # Process oldest first
62
  title = article.get('title')
63
  url = article.get('url')
64
  if title and "[Removed]" not in title and url not in processed_urls:
65
- print(f"✨ New article found: '{title}'. Analyzing...")
66
  analysis = await analyzer.analyze_text(title)
67
  if not analysis.get("error"):
68
  analysis['url'] = url
69
  analysis['timestamp'] = datetime.now(timezone.utc).isoformat()
70
  await app.state.signal_queue.put(analysis)
71
- processed_urls.add(url) # Mark as processed
72
- print(f"βœ… Signal generated and queued for: '{title}'")
73
  except Exception as e:
74
- print(f"❌ CRITICAL ERROR in news analysis loop: {e}")
75
 
76
  await asyncio.sleep(interval_seconds)
77
 
@@ -82,32 +82,18 @@ def render_signal_card(payload: dict) -> str:
82
  s = payload
83
  url = s.get('url', '#')
84
  summary = s.get('summary', 'N/A')
85
- timestamp_str = s.get('timestamp')
86
- if timestamp_str:
87
- dt_obj = datetime.fromisoformat(timestamp_str)
88
- time_ago = f"{dt_obj.strftime('%H:%M:%S UTC')}"
89
- else:
90
- time_ago = "Just now"
91
-
92
- text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{summary}</a>'
93
- impact_class = f"impact-{s.get('impact', 'low').lower()}"
94
- sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
95
 
96
- # Use Out-of-Band swaps to update the timestamp without re-rendering the whole stream
97
- oob_swap = f'<span id="last-signal-time" hx-swap-oob="true">{time_ago}</span>'
98
-
99
  return f"""
100
- {oob_swap}
101
- <div class="card {impact_class}">
102
- <blockquote>{text_to_show}</blockquote>
103
- <div class="grid">
104
- <div><strong>Sentiment:</strong> <span class="{sentiment_class}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span></div>
105
- <div><strong>Impact:</strong> {s.get('impact')}</div>
106
- </div>
107
- <div class="grid">
108
- <div><strong>Topic:</strong> {s.get('topic')}</div>
109
- <div><strong>Entities:</strong> {', '.join(s.get('entities', []))}</div>
110
- </div>
111
  </div>
112
  """
113
 
@@ -115,26 +101,27 @@ def render_signal_card(payload: dict) -> str:
115
  async def serve_dashboard(request: Request):
116
  return templates.TemplateResponse("index.html", {"request": request})
117
 
118
- # NEW ENDPOINT FOR LIVE PRICES
119
  @app.get("/api/prices", response_class=HTMLResponse)
120
  async def get_prices_fragment(request: Request):
121
- price_fetcher: PriceFetcher = request.app.state.price_fetcher
122
- prices = price_fetcher.get_current_prices()
123
- html_fragment = "".join(
124
- f"<span>{coin.capitalize()}: <strong>${price:,.2f}</strong></span>" if isinstance(price, (int, float))
125
- else f"<span>{coin.capitalize()}: <strong>{price}</strong></span>"
126
- for coin, price in prices.items()
127
- )
128
- return HTMLResponse(content=html_fragment)
129
 
130
  @app.get("/api/signals/stream")
131
  async def signal_stream(request: Request):
132
  queue: asyncio.Queue = request.app.state.signal_queue
133
  async def event_generator():
 
 
 
 
134
  while True:
135
  payload = await queue.get()
136
- html = render_signal_card(payload)
137
- data_payload = html.replace('\n', '')
138
- sse_message = f"event: message\ndata: {data_payload}\n\n"
139
- yield sse_message
140
  return StreamingResponse(event_generator(), media_type="text/event-stream")
 
1
  """
2
+ Sentinel TradeFlow Protocol β€” v3.0 FINAL
3
+
4
+ This is the definitive, robust version of the application, designed for
5
+ guaranteed functionality and a superior user experience.
6
  """
7
  import asyncio
8
  import os
9
  from contextlib import asynccontextmanager
 
10
  from datetime import datetime, timezone
11
 
12
  import httpx
 
26
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
27
  app.state.signal_queue: asyncio.Queue = asyncio.Queue()
28
 
29
+ price_task = asyncio.create_task(run_periodic_updates(app.state.price_fetcher, 30))
30
  news_task = asyncio.create_task(run_periodic_news_analysis(app, 300))
31
 
32
+ print("πŸš€ Sentinel TradeFlow Protocol v3.0 FINAL started successfully.")
33
  yield
34
 
35
  print("⏳ Shutting down background tasks...")
 
38
  try:
39
  await asyncio.gather(price_task, news_task)
40
  except asyncio.CancelledError:
41
+ print("Background tasks cancelled.")
 
42
 
43
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
44
  while True:
 
48
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
49
  processed_urls = set()
50
  while True:
51
+ print("πŸ“° Fetching news...")
52
  try:
53
  top_headlines = app.state.news_api.get_everything(
54
  q='(crypto OR bitcoin OR ethereum) AND (regulation OR partnership OR hack OR update OR adoption)',
 
58
  print(f"πŸ“° Found {len(articles)} articles.")
59
 
60
  analyzer: GeminiAnalyzer = app.state.gemini_analyzer
61
+ for article in reversed(articles):
62
  title = article.get('title')
63
  url = article.get('url')
64
  if title and "[Removed]" not in title and url not in processed_urls:
65
+ print(f"✨ Analyzing: '{title}'")
66
  analysis = await analyzer.analyze_text(title)
67
  if not analysis.get("error"):
68
  analysis['url'] = url
69
  analysis['timestamp'] = datetime.now(timezone.utc).isoformat()
70
  await app.state.signal_queue.put(analysis)
71
+ processed_urls.add(url)
72
+ print(f"βœ… Queued: '{title}'")
73
  except Exception as e:
74
+ print(f"❌ CRITICAL ERROR in news loop: {e}")
75
 
76
  await asyncio.sleep(interval_seconds)
77
 
 
82
  s = payload
83
  url = s.get('url', '#')
84
  summary = s.get('summary', 'N/A')
85
+ time_str = datetime.fromisoformat(s['timestamp']).strftime('%H:%M:%S UTC')
 
 
 
 
 
 
 
 
 
86
 
87
+ # This entire block will be sent as a single data payload
 
 
88
  return f"""
89
+ <div id="last-signal-time" hx-swap-oob="true">{time_str}</div>
90
+ <div class="card impact-{s.get('impact', 'low').lower()}" hx-swap-oob="afterbegin:#signal-container">
91
+ <header class="card-header">
92
+ <span>{s.get('topic', 'General News')}</span>
93
+ <span>{', '.join(s.get('entities', []))}</span>
94
+ </header>
95
+ <blockquote><a href="{url}" target="_blank" rel="noopener noreferrer">{summary}</a></blockquote>
96
+ <footer><strong>Sentiment:</strong> <span class="sentiment-{s.get('sentiment', 'neutral').lower()}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span> β€’ <strong>Impact:</strong> {s.get('impact')}</footer>
 
 
 
97
  </div>
98
  """
99
 
 
101
  async def serve_dashboard(request: Request):
102
  return templates.TemplateResponse("index.html", {"request": request})
103
 
 
104
  @app.get("/api/prices", response_class=HTMLResponse)
105
  async def get_prices_fragment(request: Request):
106
+ prices = request.app.state.price_fetcher.get_current_prices()
107
+ return HTMLResponse("".join(
108
+ f"<span>{c.capitalize()}: <strong>${p:,.2f}</strong></span>" if isinstance(p, (int, float))
109
+ else f"<span>{c.capitalize()}: <strong>{p}</strong></span>"
110
+ for c, p in prices.items()
111
+ ))
 
 
112
 
113
  @app.get("/api/signals/stream")
114
  async def signal_stream(request: Request):
115
  queue: asyncio.Queue = request.app.state.signal_queue
116
  async def event_generator():
117
+ # --- THE GUARANTEED "HELLO" MESSAGE ---
118
+ welcome_html = "<div id='signal-container' hx-swap-oob='innerHTML'></div>"
119
+ yield f"event: message\ndata: {welcome_html}\n\n"
120
+
121
  while True:
122
  payload = await queue.get()
123
+ html_card = render_signal_card(payload)
124
+ data_payload = html_card.replace('\n', ' ').strip()
125
+ yield f"event: message\ndata: {data_payload}\n\n"
126
+
127
  return StreamingResponse(event_generator(), media_type="text/event-stream")