mgbam commited on
Commit
073930c
Β·
verified Β·
1 Parent(s): 347f446

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +132 -128
app/app.py CHANGED
@@ -1,171 +1,175 @@
1
  """
2
- The Sentinel TradeFlow Protocol - Main Application
3
-
4
- Orchestrates the 3-tier intelligence funnel:
5
- 1. Ingests a (mock) real-time data stream.
6
- 2. Filters events with the local Tier 1 SentimentEngine.
7
- 3. Escalates high-conviction events to the Tier 2 GeminiAnalyzer.
8
- 4. Generates actionable trade hypotheses with the Tier 3 Strategy Engine.
9
- 5. Pushes final signals to the Command Center UI.
10
  """
11
  import asyncio
12
  import json
13
  import os
14
  from contextlib import asynccontextmanager
15
- from typing import Optional, Dict
16
 
17
  import httpx
18
- from fastapi import FastAPI, Request, BackgroundTasks, Form
19
  from fastapi.responses import HTMLResponse, StreamingResponse
20
  from fastapi.templating import Jinja2Templates
 
21
 
22
- # Import our intelligence modules
 
23
  from .gemini_analyzer import GeminiAnalyzer
24
- from .sentiment_engine import LocalSentimentFilter
25
-
26
- # --- Tier 3: The Strategist ---
27
- def generate_trade_hypothesis(analysis: dict) -> Optional[Dict]:
28
- """A simple rules-based engine to generate an actionable signal."""
29
- sentiment = analysis.get("sentiment")
30
- impact = analysis.get("impact")
31
- score = analysis.get("sentiment_score", 0.0)
32
-
33
- # High-conviction rules
34
- if impact == "HIGH" and sentiment == "NEGATIVE" and score < -0.7:
35
- return {"type": "HYPOTHETICAL SHORT", "confidence": "HIGH", "reason": "High impact, strongly negative news detected."}
36
- if impact == "HIGH" and sentiment == "POSITIVE" and score > 0.7:
37
- return {"type": "HYPOTHETICAL LONG", "confidence": "HIGH", "reason": "High impact, strongly positive news detected."}
38
-
39
- # Medium-conviction rules
40
- if impact == "MEDIUM" and sentiment == "NEGATIVE" and score < -0.5:
41
- return {"type": "HYPOTHETICAL SHORT", "confidence": "MEDIUM", "reason": "Medium impact, negative news."}
42
- if impact == "MEDIUM" and sentiment == "POSITIVE" and score > 0.5:
43
- return {"type": "HYPOTHETICAL LONG", "confidence": "MEDIUM", "reason": "Medium impact, positive news."}
44
-
45
- return None
46
-
47
- # --- Mock Real-Time Data Feed & Pipeline Orchestration ---
48
- async def real_time_intelligence_pipeline(app: FastAPI):
49
- """Mocks a high-frequency WebSocket news feed and runs it through the 3-tier funnel."""
50
- await asyncio.sleep(5) # Initial delay to let UI connect
51
- print("πŸš€ [Pipeline] Real-time intelligence pipeline is active.")
52
-
53
- # A more realistic stream of headlines
54
- mock_headlines = [
55
- ("Coinbase reports minor outage, services restored.", 5),
56
- ("New memecoin 'ShibaCat' gains 20% on low volume.", 3),
57
- ("BREAKING: US Federal Reserve signals potential for surprise interest rate hike next month.", 8),
58
- ("Ethereum developer announces successful testnet merge for upcoming 'Prague' upgrade.", 6),
59
- ("CEO of major crypto fund says market is 'overheated'.", 4),
60
- ("MASSIVE EXPLOIT: Cross-chain bridge 'Wormhole' drained of $150M in ETH and SOL.", 7),
61
- ("BlackRock files updated S-1 form for its spot Bitcoin ETF.", 5),
62
- ("Polygon announces major partnership with a leading gaming studio.", 4),
63
- ]
64
-
65
- for headline, delay in mock_headlines:
66
- print(f"πŸ”₯ [Tier 1] Ingested: '{headline}'")
67
-
68
- # Tier 1 Analysis: Fast, local filtering
69
- local_analysis = LocalSentimentFilter.analyze(headline)
70
-
71
- # Trigger Condition for Tier 2: Is sentiment strong enough?
72
- if abs(local_analysis['score']) > 0.65 or local_analysis['label'].lower() != 'neutral':
73
- print(f"⚑️ [Tier 2 Triggered] Event '{headline[:30]}...' escalated to Gemini. Reason: Local sentiment {local_analysis['label']} ({local_analysis['score']:.2f})")
74
-
75
- analyzer: GeminiAnalyzer = app.state.gemini_analyzer
76
- gemini_analysis = await analyzer.analyze_text(headline)
77
-
78
- # Tier 3: Generate actionable signal
79
- signal = generate_trade_hypothesis(gemini_analysis)
80
-
81
- if signal:
82
- print(f"🎯 [Tier 3] Actionable Signal Generated: {signal['type']} with {signal['confidence']} confidence.")
83
- final_payload = {"signal": signal, "analysis": gemini_analysis, "headline": headline}
84
- await app.state.signal_queue.put(final_payload)
85
-
86
- await asyncio.sleep(delay)
87
- print("βœ… [Pipeline] Mock real-time feed complete.")
88
 
 
 
 
 
 
 
 
89
 
90
- # --- Application Lifespan ---
91
  @asynccontextmanager
92
  async def lifespan(app: FastAPI):
93
- """Manages application startup and shutdown events."""
 
 
 
94
  async with httpx.AsyncClient() as client:
 
95
  app.state.gemini_analyzer = GeminiAnalyzer(client=client)
96
- app.state.signal_queue = asyncio.Queue()
97
-
98
- # Warm up the local model on startup
99
- LocalSentimentFilter.analyze("Warming up FinBERT model...")
100
-
101
- # Start the intelligence pipeline as a background task
102
- pipeline_task = asyncio.create_task(real_time_intelligence_pipeline(app))
 
 
 
 
103
 
104
- print("πŸš€ Sentinel TradeFlow Protocol is online.")
105
  yield
106
 
107
- print("⏳ Shutting down Sentinel protocols...")
108
- pipeline_task.cancel()
 
109
  try:
110
- await pipeline_task
111
  except asyncio.CancelledError:
112
- print("Intelligence pipeline successfully shut down.")
113
- print("βœ… Sentinel Protocol offline.")
114
 
115
- # --- App Initialization ---
116
- app = FastAPI(title="Sentinel TradeFlow", lifespan=lifespan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  templates = Jinja2Templates(directory="templates")
118
 
119
  # --- HTML Rendering Helper ---
120
- def render_signal_card(payload: dict) -> str:
121
- """Renders the final signal payload into a rich HTML card."""
122
- signal = payload.get("signal", {})
123
- analysis = payload.get("analysis", {})
124
- headline = payload.get("headline", "N/A")
125
-
126
- signal_type = signal.get("type", "INFO")
127
- confidence = signal.get("confidence", "N/A")
128
-
129
- # Dynamic styling based on signal
130
- if "SHORT" in signal_type:
131
- card_class = "signal-short"
132
- icon = "πŸ“‰"
133
- elif "LONG" in signal_type:
134
- card_class = "signal-long"
135
- icon = "πŸ“ˆ"
136
- else:
137
- card_class = ""
138
- icon = "ℹ️"
139
 
 
 
 
 
 
 
 
 
 
140
  return f"""
141
- <div class="card {card_class}">
142
- <header class="signal-header">
143
- <span>{icon} {signal_type}</span>
144
- <span>Confidence: <strong>{confidence}</strong></span>
145
- </header>
146
- <p class="headline"><strong>Source Headline:</strong> {headline}</p>
147
- <p><strong>Sentinel's Assessment:</strong> {analysis.get('summary', 'N/A')}</p>
148
  <div class="grid">
149
- <div><strong>Impact:</strong> {analysis.get('impact')}</div>
150
- <div><strong>Topic:</strong> {analysis.get('topic')}</div>
151
- <div><strong>Entities:</strong> {', '.join(analysis.get('entities', []))}</div>
152
  </div>
153
  </div>
154
  """
155
 
156
  # --- API Endpoints ---
 
157
  @app.get("/", response_class=HTMLResponse)
158
- async def serve_command_center(request: Request):
159
  return templates.TemplateResponse("index.html", {"request": request})
160
 
161
- @app.get("/api/signals/stream")
162
- async def signal_stream(request: Request):
163
- """SSE stream for pushing generated trade hypotheses to the UI."""
164
- queue: asyncio.Queue = request.app.state.signal_queue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  async def event_generator():
166
  while True:
167
  payload = await queue.get()
168
- html = render_signal_card(payload)
169
  data_payload = html.replace('\n', '')
170
- yield f"event: new_signal\ndata: {data_payload}\n\n"
 
171
  return StreamingResponse(event_generator(), media_type="text/event-stream")
 
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
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, BackgroundTasks
18
  from fastapi.responses import HTMLResponse, StreamingResponse
19
  from fastapi.templating import Jinja2Templates
20
+ from pydantic import BaseModel, constr
21
 
22
+ # Correct imports using relative paths
23
+ from .price_fetcher import PriceFetcher
24
  from .gemini_analyzer import GeminiAnalyzer
25
+ from newsapi import NewsApiClient
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # --- Pydantic Model for API Input Validation ---
28
+
29
+ class SentimentRequest(BaseModel):
30
+ """Ensures the text for sentiment analysis is a non-empty string."""
31
+ text: constr(strip_whitespace=True, min_length=1)
32
+
33
+ # --- Application Lifespan for Resource Management ---
34
 
 
35
  @asynccontextmanager
36
  async def lifespan(app: FastAPI):
37
+ """
38
+ Manages application startup and shutdown events using the modern
39
+ lifespan context manager.
40
+ """
41
  async with httpx.AsyncClient() as client:
42
+ app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
43
  app.state.gemini_analyzer = GeminiAnalyzer(client=client)
44
+ app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
45
+
46
+ app.state.sentiment_queue: asyncio.Queue = asyncio.Queue()
47
+ app.state.news_queue: asyncio.Queue = asyncio.Queue()
48
+
49
+ price_task = asyncio.create_task(
50
+ run_periodic_updates(app.state.price_fetcher, interval_seconds=30)
51
+ )
52
+ news_task = asyncio.create_task(
53
+ run_periodic_news_analysis(app, interval_seconds=900)
54
+ )
55
 
56
+ print("πŸš€ CryptoSentinel Pro started successfully.")
57
  yield
58
 
59
+ print("⏳ Shutting down background tasks...")
60
+ price_task.cancel()
61
+ news_task.cancel()
62
  try:
63
+ await asyncio.gather(price_task, news_task)
64
  except asyncio.CancelledError:
65
+ print("Background tasks cancelled successfully.")
66
+ print("βœ… Shutdown complete.")
67
 
68
+ async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
69
+ """Periodically updates prices."""
70
+ while True:
71
+ await fetcher.update_prices_async()
72
+ await asyncio.sleep(interval_seconds)
73
+
74
+ async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
75
+ """Periodically fetches and analyzes crypto news."""
76
+ while True:
77
+ print("πŸ“° Fetching latest crypto news for automated analysis...")
78
+ try:
79
+ top_headlines = app.state.news_api.get_everything(
80
+ q='bitcoin OR ethereum OR crypto OR blockchain',
81
+ language='en',
82
+ sort_by='publishedAt',
83
+ page_size=5
84
+ )
85
+ analyzer: GeminiAnalyzer = app.state.gemini_analyzer
86
+ for article in top_headlines.get('articles', []):
87
+ title = article.get('title')
88
+ if title and "[Removed]" not in title:
89
+ analysis = await analyzer.analyze_text(title)
90
+ analysis['url'] = article.get('url')
91
+ await app.state.news_queue.put(analysis)
92
+ except Exception as e:
93
+ print(f"❌ Error during news fetching or analysis: {e}")
94
+
95
+ await asyncio.sleep(interval_seconds)
96
+
97
+ # --- FastAPI App Initialization ---
98
+
99
+ app = FastAPI(title="CryptoSentinel Pro", lifespan=lifespan)
100
  templates = Jinja2Templates(directory="templates")
101
 
102
  # --- HTML Rendering Helper ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ def render_analysis_card(payload: dict, is_news: bool = False) -> str:
105
+ """Renders a dictionary of analysis into a styled HTML card."""
106
+ s = payload
107
+ text_to_show = s.get('summary', 'Analysis failed or not available.')
108
+ if is_news:
109
+ url = s.get('url', '#')
110
+ text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{s.get("summary", "N/A")}</a>'
111
+ impact_class = f"impact-{s.get('impact', 'low').lower()}"
112
+ sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
113
  return f"""
114
+ <div class="card {impact_class}">
115
+ <blockquote>{text_to_show}</blockquote>
116
+ <div class="grid">
117
+ <div><strong>Sentiment:</strong> <span class="{sentiment_class}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span></div>
118
+ <div><strong>Impact:</strong> {s.get('impact')}</div>
119
+ </div>
 
120
  <div class="grid">
121
+ <div><strong>Topic:</strong> {s.get('topic')}</div>
122
+ <div><strong>Entities:</strong> {', '.join(s.get('entities', []))}</div>
 
123
  </div>
124
  </div>
125
  """
126
 
127
  # --- API Endpoints ---
128
+
129
  @app.get("/", response_class=HTMLResponse)
130
+ async def serve_dashboard(request: Request):
131
  return templates.TemplateResponse("index.html", {"request": request})
132
 
133
+ @app.get("/api/prices", response_class=HTMLResponse)
134
+ async def get_prices_fragment(request: Request):
135
+ price_fetcher: PriceFetcher = request.app.state.price_fetcher
136
+ prices = price_fetcher.get_current_prices()
137
+ html_fragment = "".join(
138
+ f"<div><strong>{coin.capitalize()}:</strong> ${price:,.2f}</div>" if isinstance(price, (int, float))
139
+ else f"<div><strong>{coin.capitalize()}:</strong> {price}</div>"
140
+ for coin, price in prices.items()
141
+ )
142
+ return HTMLResponse(content=html_fragment)
143
+
144
+ @app.post("/api/sentiment")
145
+ async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks):
146
+ analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer
147
+ async def analysis_task_wrapper():
148
+ analysis_result = await analyzer.analyze_text(payload.text)
149
+ await request.app.state.sentiment_queue.put(analysis_result)
150
+ background_tasks.add_task(analysis_task_wrapper)
151
+ return HTMLResponse(content="<small>βœ… Queued for deep analysis...</small>")
152
+
153
+ @app.get("/api/sentiment/stream")
154
+ async def sentiment_stream(request: Request):
155
+ queue: asyncio.Queue = request.app.state.sentiment_queue
156
+ async def event_generator():
157
+ while True:
158
+ payload = await queue.get()
159
+ html = render_analysis_card(payload)
160
+ data_payload = html.replace('\n', '')
161
+ sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n"
162
+ yield sse_message
163
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
164
+
165
+ @app.get("/api/news/stream")
166
+ async def news_stream(request: Request):
167
+ queue: asyncio.Queue = request.app.state.news_queue
168
  async def event_generator():
169
  while True:
170
  payload = await queue.get()
171
+ html = render_analysis_card(payload, is_news=True)
172
  data_payload = html.replace('\n', '')
173
+ sse_message = f"event: news_update\ndata: {data_payload}\n\n"
174
+ yield sse_message
175
  return StreamingResponse(event_generator(), media_type="text/event-stream")