Phase 1: initial codes

This commit is contained in:
2026-05-26 11:04:27 -04:00
parent 03bff49523
commit f49a283059
32 changed files with 4194 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
from __future__ import annotations
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QListWidget, QListWidgetItem, QFrame, QProgressBar, QSizePolicy
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl
from PyQt6.QtGui import QColor, QFont, QDesktopServices
from utils.formatters import pnl_color
SENTIMENT_COLORS = {
"BULLISH": "#a6e3a1",
"BEARISH": "#f38ba8",
"NEUTRAL": "#6c7086",
"": "#6c7086",
}
class NewsWorker(QThread):
articles_ready = pyqtSignal(list)
error = pyqtSignal(str)
def __init__(self, symbol: str, api_key: str, analyze: bool, claude_client=None):
super().__init__()
self._symbol = symbol
self._api_key = api_key
self._analyze = analyze
self._claude = claude_client
def run(self):
try:
from ai.sentiment import fetch_news, analyze_sentiment, cache_news, get_cached_news
cached = get_cached_news(self._symbol)
if cached:
self.articles_ready.emit(cached)
return
articles = fetch_news(self._symbol, self._api_key)
if not articles:
self.articles_ready.emit([])
return
overall_sentiment = ""
if self._analyze and self._claude and articles:
try:
sentiment_text, _ = analyze_sentiment(self._symbol, articles, self._claude)
if "BULLISH" in sentiment_text:
overall_sentiment = "BULLISH"
elif "BEARISH" in sentiment_text:
overall_sentiment = "BEARISH"
else:
overall_sentiment = "NEUTRAL"
for a in articles:
a["sentiment"] = overall_sentiment
except Exception:
pass
cache_news(self._symbol, articles, overall_sentiment)
self.articles_ready.emit(articles)
except Exception as e:
self.error.emit(str(e))
class NewsItemWidget(QFrame):
def __init__(self, article: dict, parent=None):
super().__init__(parent)
self.setObjectName("card")
self.setCursor(Qt.CursorShape.PointingHandCursor)
self._url = article.get("url", "")
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(4)
top = QHBoxLayout()
title = QLabel(article.get("title", ""))
title.setWordWrap(True)
title.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
top.addWidget(title, stretch=1)
sentiment = article.get("sentiment", "")
if sentiment:
badge = QLabel(f" {sentiment} ")
badge_color = SENTIMENT_COLORS.get(sentiment.upper(), "#6c7086")
badge.setStyleSheet(f"background: {badge_color}; color: #1e1e2e; border-radius: 4px; padding: 1px 4px; font-size: 8pt; font-weight: bold;")
badge.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
top.addWidget(badge)
layout.addLayout(top)
meta = QLabel(f"{article.get('source', '')} · {article.get('published_at', '')[:10]}")
meta.setObjectName("subtitle")
layout.addWidget(meta)
desc = article.get("description", "")
if desc:
desc_label = QLabel(desc[:200] + ("" if len(desc) > 200 else ""))
desc_label.setWordWrap(True)
desc_label.setObjectName("subtitle")
layout.addWidget(desc_label)
def mousePressEvent(self, event):
if self._url:
QDesktopServices.openUrl(QUrl(self._url))
super().mousePressEvent(event)
class NewsWidget(QWidget):
def __init__(self, config, parent=None):
super().__init__(parent)
self._config = config
self._symbol = "AAPL"
self._worker: NewsWorker | None = None
self._setup_ui()
def _setup_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(12, 12, 12, 12)
root.setSpacing(8)
header_row = QHBoxLayout()
header = QLabel("News & Sentiment")
header.setObjectName("title")
header_row.addWidget(header)
header_row.addStretch()
self._symbol_label = QLabel(self._symbol)
self._symbol_label.setObjectName("subtitle")
header_row.addWidget(self._symbol_label)
self._fetch_btn = QPushButton("Fetch News")
self._fetch_btn.setObjectName("primary_btn")
self._fetch_btn.clicked.connect(self._fetch_news)
header_row.addWidget(self._fetch_btn)
root.addLayout(header_row)
# Sentiment gauge row
self._sentiment_frame = QFrame()
self._sentiment_frame.setObjectName("card")
self._sentiment_frame.setMaximumHeight(60)
sent_layout = QHBoxLayout(self._sentiment_frame)
self._overall_label = QLabel("Overall Sentiment: —")
self._overall_label.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
self._gauge = QProgressBar()
self._gauge.setRange(0, 100)
self._gauge.setValue(50)
self._gauge.setMaximumWidth(200)
self._gauge.setFormat("")
sent_layout.addWidget(self._overall_label)
sent_layout.addStretch()
sent_layout.addWidget(QLabel("Bearish"))
sent_layout.addWidget(self._gauge)
sent_layout.addWidget(QLabel("Bullish"))
root.addWidget(self._sentiment_frame)
# Articles scroll area
from PyQt6.QtWidgets import QScrollArea
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
self._articles_widget = QWidget()
self._articles_layout = QVBoxLayout(self._articles_widget)
self._articles_layout.setSpacing(8)
self._articles_layout.setContentsMargins(0, 0, 0, 0)
self._articles_layout.addStretch()
scroll.setWidget(self._articles_widget)
root.addWidget(scroll)
self._status_label = QLabel("")
self._status_label.setObjectName("subtitle")
root.addWidget(self._status_label)
def set_symbol(self, symbol: str):
self._symbol = symbol
self._symbol_label.setText(symbol)
def _fetch_news(self):
api_key = self._config.news_api_key
if not api_key:
self._status_label.setText("NewsAPI key not configured. Go to Settings.")
return
self._fetch_btn.setEnabled(False)
self._status_label.setText(f"Fetching news for {self._symbol}")
claude = None
if self._config.anthropic_key:
try:
from ai.claude_client import ClaudeClient
claude = ClaudeClient(self._config.anthropic_key)
except Exception:
pass
self._worker = NewsWorker(self._symbol, api_key, analyze=bool(claude), claude_client=claude)
self._worker.articles_ready.connect(self._on_articles_ready)
self._worker.error.connect(lambda e: (self._status_label.setText(f"Error: {e}"), self._fetch_btn.setEnabled(True)))
self._worker.start()
def _on_articles_ready(self, articles: list[dict]):
# Clear existing articles
while self._articles_layout.count() > 1:
item = self._articles_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
for article in articles:
widget = NewsItemWidget(article)
self._articles_layout.insertWidget(self._articles_layout.count() - 1, widget)
# Update sentiment gauge
sentiments = [a.get("sentiment", "").upper() for a in articles if a.get("sentiment")]
if sentiments:
bullish = sentiments.count("BULLISH")
bearish = sentiments.count("BEARISH")
total = len(sentiments)
score = int((bullish / total) * 100) if total else 50
self._gauge.setValue(score)
dominant = "BULLISH" if bullish > bearish else ("BEARISH" if bearish > bullish else "NEUTRAL")
color = SENTIMENT_COLORS.get(dominant, "#6c7086")
self._overall_label.setText(f"Overall: {dominant}")
self._overall_label.setStyleSheet(f"color: {color};")
else:
self._gauge.setValue(50)
self._overall_label.setText("Overall Sentiment: —")
self._status_label.setText(f"{len(articles)} articles | 100 req/day limit applies")
self._fetch_btn.setEnabled(True)