mgbam commited on
Commit
56eb560
Β·
verified Β·
1 Parent(s): c0d8669

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +46 -67
app/app.py CHANGED
@@ -1,50 +1,32 @@
1
  """
2
  Sentinel TradeFlow Protocol β€” High-performance FastAPI application.
3
-
4
- This is the main entry point that orchestrates the entire application.
5
- - Integrates an asynchronous PriceFetcher for live market data.
6
- - Integrates a sophisticated GeminiAnalyzer for deep text analysis.
7
- - Implements an automated pipeline to fetch, analyze, and stream trading signals.
8
- - Serves the interactive frontend and provides all necessary API endpoints.
9
  """
10
  import asyncio
11
- import json
12
  import os
13
  from contextlib import asynccontextmanager
14
  from typing import Optional, Union
 
15
 
16
  import httpx
17
  from fastapi import FastAPI, Request
18
  from fastapi.responses import HTMLResponse, StreamingResponse
19
  from fastapi.templating import Jinja2Templates
20
 
21
- # Use relative imports because these modules are in the same 'app' package.
22
  from .price_fetcher import PriceFetcher
23
  from .gemini_analyzer import GeminiAnalyzer
24
  from newsapi import NewsApiClient
25
 
26
-
27
- # --- Application Lifespan for Resource Management ---
28
-
29
  @asynccontextmanager
30
  async def lifespan(app: FastAPI):
31
- """
32
- Manages application startup and shutdown events using the modern
33
- lifespan context manager.
34
- """
35
  async with httpx.AsyncClient() as client:
36
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
37
  app.state.gemini_analyzer = GeminiAnalyzer(client=client)
38
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
39
  app.state.signal_queue: asyncio.Queue = asyncio.Queue()
40
 
41
- # Create cancellable background tasks. Let's use a shorter timer for testing.
42
- price_task = asyncio.create_task(
43
- run_periodic_updates(app.state.price_fetcher, interval_seconds=60)
44
- )
45
- news_task = asyncio.create_task(
46
- run_periodic_news_analysis(app, interval_seconds=300) # Check news every 5 minutes for debugging
47
- )
48
 
49
  print("πŸš€ Sentinel TradeFlow Protocol started successfully.")
50
  yield
@@ -59,74 +41,63 @@ async def lifespan(app: FastAPI):
59
  print("βœ… Shutdown complete.")
60
 
61
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
62
- """A robust asyncio background task that periodically updates prices."""
63
  while True:
64
  await fetcher.update_prices_async()
65
  await asyncio.sleep(interval_seconds)
66
 
67
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
68
- """Fetches, analyzes, and queues top crypto news periodically with detailed logging."""
69
  while True:
70
- print("πŸ“° [1/5] Fetching latest crypto news...")
71
  try:
72
  top_headlines = app.state.news_api.get_everything(
73
- q='bitcoin OR ethereum OR "binance coin" OR solana OR ripple OR cardano',
74
- language='en',
75
- sort_by='publishedAt',
76
- page_size=5
77
  )
78
-
79
  articles = top_headlines.get('articles', [])
80
- print(f"πŸ“° [2/5] NewsAPI call successful. Found {len(articles)} articles.")
81
 
82
- if not articles:
83
- print("πŸ“° [SKIP] No new articles found in this cycle.")
84
- await asyncio.sleep(interval_seconds)
85
- continue
86
-
87
  analyzer: GeminiAnalyzer = app.state.gemini_analyzer
88
- for article in articles:
89
  title = article.get('title')
90
- print(f"πŸ“° [3/5] Processing article: '{title}'")
91
-
92
- if not title or "[Removed]" in title:
93
- print(f"πŸ“° [SKIP] Article has no title or was removed.")
94
- continue
95
-
96
- print(f"πŸ“° [4/5] Sending to Gemini for analysis...")
97
- analysis = await analyzer.analyze_text(title)
98
-
99
- if analysis.get("error"):
100
- print(f"❌ [SKIP] Gemini analysis failed for '{title}'. Reason: {analysis.get('reason')}")
101
- continue
102
-
103
- analysis['url'] = article.get('url')
104
- await app.state.signal_queue.put(analysis)
105
- print(f"βœ… [5/5] Signal generated and queued for: '{title}'")
106
-
107
  except Exception as e:
108
- print(f"❌❌❌ CRITICAL ERROR in news analysis loop: {e}")
109
-
110
- print(f"πŸ“° Loop finished. Waiting for {interval_seconds} seconds.")
111
  await asyncio.sleep(interval_seconds)
112
 
113
- # --- FastAPI App Initialization ---
114
-
115
  app = FastAPI(title="Sentinel TradeFlow Protocol", lifespan=lifespan)
116
  templates = Jinja2Templates(directory="templates")
117
 
118
- # --- HTML Rendering Helper ---
119
  def render_signal_card(payload: dict) -> str:
120
- """Renders a dictionary of analysis into a styled HTML card."""
121
  s = payload
122
  url = s.get('url', '#')
123
- summary = s.get('summary', 'Analysis failed or not available.')
124
- text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{summary}</a>'
 
 
 
 
 
125
 
 
126
  impact_class = f"impact-{s.get('impact', 'low').lower()}"
127
  sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
 
 
 
128
 
129
  return f"""
 
130
  <div class="card {impact_class}">
131
  <blockquote>{text_to_show}</blockquote>
132
  <div class="grid">
@@ -140,15 +111,24 @@ def render_signal_card(payload: dict) -> str:
140
  </div>
141
  """
142
 
143
- # --- API Endpoints ---
144
  @app.get("/", response_class=HTMLResponse)
145
  async def serve_dashboard(request: Request):
146
- """Serves the main interactive dashboard from `index.html`."""
147
  return templates.TemplateResponse("index.html", {"request": request})
148
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  @app.get("/api/signals/stream")
150
  async def signal_stream(request: Request):
151
- """SSE stream for the automated Signal Stream."""
152
  queue: asyncio.Queue = request.app.state.signal_queue
153
  async def event_generator():
154
  while True:
@@ -157,5 +137,4 @@ async def signal_stream(request: Request):
157
  data_payload = html.replace('\n', '')
158
  sse_message = f"event: message\ndata: {data_payload}\n\n"
159
  yield sse_message
160
-
161
  return StreamingResponse(event_generator(), media_type="text/event-stream")
 
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
12
  from fastapi import FastAPI, Request
13
  from fastapi.responses import HTMLResponse, StreamingResponse
14
  from fastapi.templating import Jinja2Templates
15
 
 
16
  from .price_fetcher import PriceFetcher
17
  from .gemini_analyzer import GeminiAnalyzer
18
  from newsapi import NewsApiClient
19
 
 
 
 
20
  @asynccontextmanager
21
  async def lifespan(app: FastAPI):
 
 
 
 
22
  async with httpx.AsyncClient() as client:
23
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
24
  app.state.gemini_analyzer = GeminiAnalyzer(client=client)
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
 
41
  print("βœ… Shutdown complete.")
42
 
43
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
 
44
  while True:
45
  await fetcher.update_prices_async()
46
  await asyncio.sleep(interval_seconds)
47
 
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)',
55
+ language='en', sort_by='publishedAt', page_size=10
 
 
56
  )
 
57
  articles = top_headlines.get('articles', [])
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
 
 
 
78
  app = FastAPI(title="Sentinel TradeFlow Protocol", lifespan=lifespan)
79
  templates = Jinja2Templates(directory="templates")
80
 
 
81
  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">
 
111
  </div>
112
  """
113
 
 
114
  @app.get("/", response_class=HTMLResponse)
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:
 
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")