mgbam commited on
Commit
431c9a5
Β·
verified Β·
1 Parent(s): 5187aa2

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +31 -65
app/app.py CHANGED
@@ -1,10 +1,10 @@
1
  """
2
- CryptoSentinel Pro β€” 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 top crypto news.
8
  - Serves the interactive frontend and provides all necessary API endpoints.
9
  """
10
  import asyncio
@@ -12,25 +12,17 @@ import json
12
  import os
13
  from contextlib import asynccontextmanager
14
  from typing import Optional, Union
15
- from newsapi import NewsApiClient
16
 
17
  import httpx
18
- from fastapi import FastAPI, Request, BackgroundTasks
19
  from fastapi.responses import HTMLResponse, StreamingResponse
20
  from fastapi.templating import Jinja2Templates
21
- from pydantic import BaseModel, constr
22
 
23
- # Correct imports using relative paths
24
- from .price_fetcher import PriceFetcher
25
- from .gemini_analyzer import GeminiAnalyzer
26
  from newsapi import NewsApiClient
27
 
28
- # --- Pydantic Model for API Input Validation ---
29
-
30
- class SentimentRequest(BaseModel):
31
- """Ensures the text for sentiment analysis is a non-empty string."""
32
- text: constr(strip_whitespace=True, min_length=1)
33
-
34
  # --- Application Lifespan for Resource Management ---
35
 
36
  @asynccontextmanager
@@ -40,21 +32,23 @@ async def lifespan(app: FastAPI):
40
  lifespan context manager.
41
  """
42
  async with httpx.AsyncClient() as client:
 
43
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
44
  app.state.gemini_analyzer = GeminiAnalyzer(client=client)
45
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
46
 
47
- app.state.sentiment_queue: asyncio.Queue = asyncio.Queue()
48
- app.state.news_queue: asyncio.Queue = asyncio.Queue()
49
 
 
50
  price_task = asyncio.create_task(
51
- run_periodic_updates(app.state.price_fetcher, interval_seconds=30)
52
  )
53
  news_task = asyncio.create_task(
54
- run_periodic_news_analysis(app, interval_seconds=900)
55
  )
56
 
57
- print("πŸš€ CryptoSentinel Pro started successfully.")
58
  yield
59
 
60
  print("⏳ Shutting down background tasks...")
@@ -67,13 +61,13 @@ async def lifespan(app: FastAPI):
67
  print("βœ… Shutdown complete.")
68
 
69
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
70
- """Periodically updates prices."""
71
  while True:
72
  await fetcher.update_prices_async()
73
  await asyncio.sleep(interval_seconds)
74
 
75
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
76
- """Periodically fetches and analyzes crypto news."""
77
  while True:
78
  print("πŸ“° Fetching latest crypto news for automated analysis...")
79
  try:
@@ -89,7 +83,7 @@ async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
89
  if title and "[Removed]" not in title:
90
  analysis = await analyzer.analyze_text(title)
91
  analysis['url'] = article.get('url')
92
- await app.state.news_queue.put(analysis)
93
  except Exception as e:
94
  print(f"❌ Error during news fetching or analysis: {e}")
95
 
@@ -97,20 +91,21 @@ async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
97
 
98
  # --- FastAPI App Initialization ---
99
 
100
- app = FastAPI(title="CryptoSentinel Pro", lifespan=lifespan)
101
  templates = Jinja2Templates(directory="templates")
102
 
103
  # --- HTML Rendering Helper ---
104
 
105
- def render_analysis_card(payload: dict, is_news: bool = False) -> str:
106
  """Renders a dictionary of analysis into a styled HTML card."""
107
  s = payload
108
- text_to_show = s.get('summary', 'Analysis failed or not available.')
109
- if is_news:
110
- url = s.get('url', '#')
111
- text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{s.get("summary", "N/A")}</a>'
112
  impact_class = f"impact-{s.get('impact', 'low').lower()}"
113
  sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
 
114
  return f"""
115
  <div class="card {impact_class}">
116
  <blockquote>{text_to_show}</blockquote>
@@ -129,48 +124,19 @@ def render_analysis_card(payload: dict, is_news: bool = False) -> str:
129
 
130
  @app.get("/", response_class=HTMLResponse)
131
  async def serve_dashboard(request: Request):
 
132
  return templates.TemplateResponse("index.html", {"request": request})
133
 
134
- @app.get("/api/prices", response_class=HTMLResponse)
135
- async def get_prices_fragment(request: Request):
136
- price_fetcher: PriceFetcher = request.app.state.price_fetcher
137
- prices = price_fetcher.get_current_prices()
138
- html_fragment = "".join(
139
- f"<div><strong>{coin.capitalize()}:</strong> ${price:,.2f}</div>" if isinstance(price, (int, float))
140
- else f"<div><strong>{coin.capitalize()}:</strong> {price}</div>"
141
- for coin, price in prices.items()
142
- )
143
- return HTMLResponse(content=html_fragment)
144
-
145
- @app.post("/api/sentiment")
146
- async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks):
147
- analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer
148
- async def analysis_task_wrapper():
149
- analysis_result = await analyzer.analyze_text(payload.text)
150
- await request.app.state.sentiment_queue.put(analysis_result)
151
- background_tasks.add_task(analysis_task_wrapper)
152
- return HTMLResponse(content="<small>βœ… Queued for deep analysis...</small>")
153
-
154
- @app.get("/api/sentiment/stream")
155
- async def sentiment_stream(request: Request):
156
- queue: asyncio.Queue = request.app.state.sentiment_queue
157
- async def event_generator():
158
- while True:
159
- payload = await queue.get()
160
- html = render_analysis_card(payload)
161
- data_payload = html.replace('\n', '')
162
- sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n"
163
- yield sse_message
164
- return StreamingResponse(event_generator(), media_type="text/event-stream")
165
-
166
- @app.get("/api/news/stream")
167
- async def news_stream(request: Request):
168
- queue: asyncio.Queue = request.app.state.news_queue
169
  async def event_generator():
170
  while True:
171
  payload = await queue.get()
172
- html = render_analysis_card(payload, is_news=True)
173
  data_payload = html.replace('\n', '')
174
- sse_message = f"event: news_update\ndata: {data_payload}\n\n"
 
175
  yield sse_message
176
  return StreamingResponse(event_generator(), media_type="text/event-stream")
 
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
 
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
+ # Import our modular, asynchronous service classes
22
+ from price_fetcher import PriceFetcher
23
+ from gemini_analyzer import GeminiAnalyzer
24
  from newsapi import NewsApiClient
25
 
 
 
 
 
 
 
26
  # --- Application Lifespan for Resource Management ---
27
 
28
  @asynccontextmanager
 
32
  lifespan context manager.
33
  """
34
  async with httpx.AsyncClient() as client:
35
+ # Instantiate and store all services in the application state.
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
 
40
+ # Create a queue for the real-time signal feed
41
+ app.state.signal_queue: asyncio.Queue = asyncio.Queue()
42
 
43
+ # Create cancellable background tasks for periodic updates.
44
  price_task = asyncio.create_task(
45
+ run_periodic_updates(app.state.price_fetcher, interval_seconds=60)
46
  )
47
  news_task = asyncio.create_task(
48
+ run_periodic_news_analysis(app, interval_seconds=900) # Run every 15 minutes
49
  )
50
 
51
+ print("πŸš€ Sentinel TradeFlow Protocol started successfully.")
52
  yield
53
 
54
  print("⏳ Shutting down background tasks...")
 
61
  print("βœ… Shutdown complete.")
62
 
63
  async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
64
+ """A robust asyncio background task that periodically updates prices."""
65
  while True:
66
  await fetcher.update_prices_async()
67
  await asyncio.sleep(interval_seconds)
68
 
69
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
70
+ """Fetches, analyzes, and queues top crypto news periodically."""
71
  while True:
72
  print("πŸ“° Fetching latest crypto news for automated analysis...")
73
  try:
 
83
  if title and "[Removed]" not in title:
84
  analysis = await analyzer.analyze_text(title)
85
  analysis['url'] = article.get('url')
86
+ await app.state.signal_queue.put(analysis)
87
  except Exception as e:
88
  print(f"❌ Error during news fetching or analysis: {e}")
89
 
 
91
 
92
  # --- FastAPI App Initialization ---
93
 
94
+ app = FastAPI(title="Sentinel TradeFlow Protocol", lifespan=lifespan)
95
  templates = Jinja2Templates(directory="templates")
96
 
97
  # --- HTML Rendering Helper ---
98
 
99
+ def render_signal_card(payload: dict) -> str:
100
  """Renders a dictionary of analysis into a styled HTML card."""
101
  s = payload
102
+ url = s.get('url', '#')
103
+ summary = s.get('summary', 'Analysis failed or not available.')
104
+ text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{summary}</a>'
105
+
106
  impact_class = f"impact-{s.get('impact', 'low').lower()}"
107
  sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
108
+
109
  return f"""
110
  <div class="card {impact_class}">
111
  <blockquote>{text_to_show}</blockquote>
 
124
 
125
  @app.get("/", response_class=HTMLResponse)
126
  async def serve_dashboard(request: Request):
127
+ """Serves the main interactive dashboard from `index.html`."""
128
  return templates.TemplateResponse("index.html", {"request": request})
129
 
130
+ @app.get("/api/signals/stream")
131
+ async def signal_stream(request: Request):
132
+ """SSE stream for the automated Signal Stream."""
133
+ queue: asyncio.Queue = request.state.signal_queue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  async def event_generator():
135
  while True:
136
  payload = await queue.get()
137
+ html = render_signal_card(payload)
138
  data_payload = html.replace('\n', '')
139
+ # Use a generic 'message' event, which HTMX listens to by default
140
+ sse_message = f"event: message\ndata: {data_payload}\n\n"
141
  yield sse_message
142
  return StreamingResponse(event_generator(), media_type="text/event-stream")