ifieryarrows commited on
Commit
8a34489
·
verified ·
1 Parent(s): 8d37fa0

Sync from GitHub

Browse files
Files changed (3) hide show
  1. app/commentary.py +175 -0
  2. app/main.py +69 -0
  3. app/settings.py +4 -0
app/commentary.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Commentary Generator using OpenRouter API.
3
+ Generates human-readable market analysis from FinBERT + XGBoost results.
4
+ """
5
+
6
+ import httpx
7
+ import logging
8
+ from typing import Optional
9
+ from datetime import datetime
10
+
11
+ from .settings import get_settings
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def generate_commentary(
17
+ current_price: float,
18
+ predicted_price: float,
19
+ predicted_return: float,
20
+ sentiment_index: float,
21
+ sentiment_label: str,
22
+ top_influencers: list[dict],
23
+ news_count: int = 0,
24
+ ) -> Optional[str]:
25
+ """
26
+ Generate AI commentary using OpenRouter API.
27
+
28
+ Args:
29
+ current_price: Current copper price
30
+ predicted_price: Model's predicted price for tomorrow
31
+ predicted_return: Expected return percentage
32
+ sentiment_index: Current sentiment score (-1 to 1)
33
+ sentiment_label: Bullish/Bearish/Neutral
34
+ top_influencers: List of top feature influencers
35
+ news_count: Number of news articles analyzed
36
+
37
+ Returns:
38
+ AI-generated commentary or None if failed
39
+ """
40
+ settings = get_settings()
41
+
42
+ if not settings.openrouter_api_key:
43
+ logger.warning("OpenRouter API key not configured, skipping commentary")
44
+ return None
45
+
46
+ # Build the prompt
47
+ influencers_text = "\n".join([
48
+ f" - {inf.get('feature', 'Unknown')}: {inf.get('importance', 0)*100:.1f}%"
49
+ for inf in top_influencers[:5]
50
+ ])
51
+
52
+ change_direction = "artış" if predicted_return > 0 else "düşüş"
53
+ change_emoji = "📈" if predicted_return > 0 else "📉"
54
+
55
+ prompt = f"""Sen bir finans analisti asistanısın. Aşağıdaki bakır (copper) piyasası verilerini analiz et ve yatırımcılar için kısa, anlaşılır bir Türkçe yorum yaz.
56
+
57
+ ## Güncel Veriler:
58
+ - **Güncel Fiyat:** ${current_price:.4f}
59
+ - **Yarınki Tahmin:** ${predicted_price:.4f} ({change_emoji} %{abs(predicted_return*100):.2f} {change_direction})
60
+ - **Piyasa Duyarlılığı:** {sentiment_label} (Skor: {sentiment_index:.3f})
61
+ - **Analiz Edilen Haber Sayısı:** {news_count}
62
+
63
+ ## En Etkili Faktörler (XGBoost Model):
64
+ {influencers_text}
65
+
66
+ ## Talimatlar:
67
+ 1. 3-4 paragraf yaz (toplam 150-200 kelime)
68
+ 2. Teknik terimler kullanma, sade ve anlaşılır ol
69
+ 3. İlk paragrafta genel durumu özetle
70
+ 4. İkinci paragrafta önemli faktörleri açıkla
71
+ 5. Son paragrafta kısa vadeli görünümü belirt
72
+ 6. 🎯 emoji ile önemli noktaları vurgula
73
+ 7. Bu finansal tavsiye DEĞİLDİR uyarısını ekle
74
+
75
+ Yorumunu yaz:"""
76
+
77
+ try:
78
+ async with httpx.AsyncClient(timeout=30.0) as client:
79
+ response = await client.post(
80
+ "https://openrouter.ai/api/v1/chat/completions",
81
+ headers={
82
+ "Authorization": f"Bearer {settings.openrouter_api_key}",
83
+ "Content-Type": "application/json",
84
+ "HTTP-Referer": "https://copper-mind.vercel.app",
85
+ "X-Title": "CopperMind AI Analysis",
86
+ },
87
+ json={
88
+ "model": settings.openrouter_model,
89
+ "messages": [
90
+ {
91
+ "role": "system",
92
+ "content": "Sen uzman bir emtia piyasası analistsin. Bakır fiyatları hakkında kısa ve öz Türkçe yorumlar yapıyorsun."
93
+ },
94
+ {
95
+ "role": "user",
96
+ "content": prompt
97
+ }
98
+ ],
99
+ "max_tokens": 500,
100
+ "temperature": 0.7,
101
+ }
102
+ )
103
+
104
+ if response.status_code == 200:
105
+ data = response.json()
106
+ commentary = data.get("choices", [{}])[0].get("message", {}).get("content", "")
107
+ if commentary:
108
+ logger.info(f"AI commentary generated successfully ({len(commentary)} chars)")
109
+ return commentary.strip()
110
+ else:
111
+ logger.warning("Empty response from OpenRouter")
112
+ return None
113
+ else:
114
+ logger.error(f"OpenRouter API error: {response.status_code} - {response.text}")
115
+ return None
116
+
117
+ except Exception as e:
118
+ logger.error(f"Failed to generate AI commentary: {e}")
119
+ return None
120
+
121
+
122
+ # Cache for commentary (simple in-memory)
123
+ _commentary_cache: dict = {
124
+ "commentary": None,
125
+ "generated_at": None,
126
+ "expires_at": None,
127
+ }
128
+
129
+
130
+ async def get_cached_commentary(
131
+ current_price: float,
132
+ predicted_price: float,
133
+ predicted_return: float,
134
+ sentiment_index: float,
135
+ sentiment_label: str,
136
+ top_influencers: list[dict],
137
+ news_count: int = 0,
138
+ ttl_minutes: int = 60,
139
+ ) -> Optional[str]:
140
+ """
141
+ Get cached commentary or generate new one if expired.
142
+ """
143
+ global _commentary_cache
144
+
145
+ now = datetime.now()
146
+
147
+ # Check if cache is valid
148
+ if (
149
+ _commentary_cache["commentary"]
150
+ and _commentary_cache["expires_at"]
151
+ and now < _commentary_cache["expires_at"]
152
+ ):
153
+ logger.debug("Returning cached AI commentary")
154
+ return _commentary_cache["commentary"]
155
+
156
+ # Generate new commentary
157
+ commentary = await generate_commentary(
158
+ current_price=current_price,
159
+ predicted_price=predicted_price,
160
+ predicted_return=predicted_return,
161
+ sentiment_index=sentiment_index,
162
+ sentiment_label=sentiment_label,
163
+ top_influencers=top_influencers,
164
+ news_count=news_count,
165
+ )
166
+
167
+ if commentary:
168
+ from datetime import timedelta
169
+ _commentary_cache = {
170
+ "commentary": commentary,
171
+ "generated_at": now,
172
+ "expires_at": now + timedelta(minutes=ttl_minutes),
173
+ }
174
+
175
+ return commentary
app/main.py CHANGED
@@ -312,6 +312,75 @@ async def health_check():
312
  )
313
 
314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  # =============================================================================
316
  # Root redirect (optional convenience)
317
  # =============================================================================
 
312
  )
313
 
314
 
315
+ # =============================================================================
316
+ # AI Commentary Endpoint
317
+ # =============================================================================
318
+
319
+ @app.get(
320
+ "/api/commentary",
321
+ summary="AI-generated market commentary",
322
+ description="Returns an AI-generated analysis of the current copper market situation using FinBERT + XGBoost results."
323
+ )
324
+ async def get_commentary(
325
+ symbol: str = Query(default="HG=F", description="Symbol to analyze")
326
+ ):
327
+ """
328
+ Generate AI commentary for the current market situation.
329
+
330
+ Uses OpenRouter API with a free LLM to create human-readable
331
+ analysis from FinBERT sentiment and XGBoost predictions.
332
+ """
333
+ from app.commentary import get_cached_commentary
334
+
335
+ settings = get_settings()
336
+
337
+ # Check if OpenRouter is configured
338
+ if not settings.openrouter_api_key:
339
+ return {
340
+ "symbol": symbol,
341
+ "commentary": None,
342
+ "error": "AI commentary not configured. Set OPENROUTER_API_KEY environment variable.",
343
+ "generated_at": None,
344
+ }
345
+
346
+ # Get the latest analysis
347
+ try:
348
+ analysis = await get_analysis(symbol)
349
+ except HTTPException:
350
+ return {
351
+ "symbol": symbol,
352
+ "commentary": None,
353
+ "error": "No analysis data available to comment on.",
354
+ "generated_at": None,
355
+ }
356
+
357
+ # Get news count
358
+ with SessionLocal() as session:
359
+ week_ago = datetime.now() - timedelta(days=7)
360
+ news_count = session.query(func.count(NewsArticle.id)).filter(
361
+ NewsArticle.published >= week_ago
362
+ ).scalar() or 0
363
+
364
+ # Generate commentary
365
+ commentary = await get_cached_commentary(
366
+ current_price=analysis.current_price,
367
+ predicted_price=analysis.predicted_price,
368
+ predicted_return=analysis.predicted_return,
369
+ sentiment_index=analysis.sentiment_index,
370
+ sentiment_label=analysis.sentiment_label,
371
+ top_influencers=[inf.dict() for inf in analysis.top_influencers],
372
+ news_count=news_count,
373
+ ttl_minutes=60, # Cache for 1 hour
374
+ )
375
+
376
+ return {
377
+ "symbol": symbol,
378
+ "commentary": commentary,
379
+ "error": None if commentary else "Failed to generate commentary",
380
+ "generated_at": datetime.now(timezone.utc).isoformat() if commentary else None,
381
+ }
382
+
383
+
384
  # =============================================================================
385
  # Root redirect (optional convenience)
386
  # =============================================================================
app/settings.py CHANGED
@@ -50,6 +50,10 @@ class Settings(BaseSettings):
50
  tz: str = "Europe/Istanbul"
51
  scheduler_enabled: bool = True
52
 
 
 
 
 
53
  @property
54
  def symbols_list(self) -> list[str]:
55
  """Parse comma-separated symbols into a list."""
 
50
  tz: str = "Europe/Istanbul"
51
  scheduler_enabled: bool = True
52
 
53
+ # OpenRouter AI Commentary
54
+ openrouter_api_key: Optional[str] = None
55
+ openrouter_model: str = "meta-llama/llama-3.2-3b-instruct:free"
56
+
57
  @property
58
  def symbols_list(self) -> list[str]:
59
  """Parse comma-separated symbols into a list."""