From 9c1220b605db2f7e68988398df4e610b51ed638b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:54:51 -0500 Subject: [PATCH] Add cryptocurrency support to stock display system. Added crypto configuration section and updated StockManager to handle both stocks and crypto symbols with separate icons and caching. --- config/config.json | 10 ++- src/stock_manager.py | 152 ++++++++++++++++++++++--------------------- 2 files changed, 87 insertions(+), 75 deletions(-) diff --git a/config/config.json b/config/config.json index cdcfdc66..1cf768a4 100644 --- a/config/config.json +++ b/config/config.json @@ -57,7 +57,15 @@ "enabled": true, "update_interval": 300, "symbols": [ - "ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SPYG", "SMCI" + "ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SPEST", "SMCI" + ], + "display_format": "{symbol}: ${price} ({change}%)" + }, + "crypto": { + "enabled": true, + "update_interval": 300, + "symbols": [ + "BTC-USD", "ETH-USD" ], "display_format": "{symbol}: ${price} ({change}%)" }, diff --git a/src/stock_manager.py b/src/stock_manager.py index d2a6b5ef..c469e320 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -23,6 +23,7 @@ class StockManager: self.config = config self.display_manager = display_manager self.stocks_config = config.get('stocks', {}) + self.crypto_config = config.get('crypto', {}) self.last_update = 0 self.stock_data = {} self.current_stock_index = 0 @@ -46,6 +47,11 @@ class StockManager: if not os.path.exists(self.ticker_icons_dir): logger.warning(f"Ticker icons directory not found: {self.ticker_icons_dir}") + # Set up the crypto icons directory + self.crypto_icons_dir = os.path.join('assets', 'stocks', 'crypto_icons') + if not os.path.exists(self.crypto_icons_dir): + logger.warning(f"Crypto icons directory not found: {self.crypto_icons_dir}") + # Set up the logo directory for external logos self.logo_dir = os.path.join('assets', 'stocks') if not os.path.exists(self.logo_dir): @@ -138,17 +144,22 @@ class StockManager: logger.error(f"Error extracting JSON data: {e}") return {} - def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: - """Fetch stock data from Yahoo Finance public API.""" + def _fetch_stock_data(self, symbol: str, is_crypto: bool = False) -> Dict[str, Any]: + """Fetch stock or crypto data from Yahoo Finance public API.""" # Try to get cached data first - cached_data = self.cache_manager.get_cached_data('stocks') + cache_key = 'crypto' if is_crypto else 'stocks' + cached_data = self.cache_manager.get_cached_data(cache_key) if cached_data and symbol in cached_data: logger.info(f"Using cached data for {symbol}") return cached_data[symbol] try: - # Use Yahoo Finance query1 API for chart data - encoded_symbol = urllib.parse.quote(symbol) + # For crypto, we need to append -USD if not already present + if is_crypto and not symbol.endswith('-USD'): + encoded_symbol = urllib.parse.quote(f"{symbol}-USD") + else: + encoded_symbol = urllib.parse.quote(symbol) + url = f"https://query1.finance.yahoo.com/v8/finance/chart/{encoded_symbol}" params = { 'interval': '5m', # 5-minute intervals @@ -209,14 +220,15 @@ class StockManager: "price": current_price, "change": change_pct, "open": prev_close, - "price_history": price_history + "price_history": price_history, + "is_crypto": is_crypto } # Cache the new data if cached_data is None: cached_data = {} cached_data[symbol] = stock_data - self.cache_manager.update_cache('stocks', cached_data) + self.cache_manager.update_cache(cache_key, cached_data) return stock_data @@ -312,67 +324,60 @@ class StockManager: logger.info(f"Stock symbols changed. New symbols: {new_symbols}") def update_stock_data(self): - """Update stock data for all configured symbols.""" + """Update stock and crypto data for all configured symbols.""" current_time = time.time() update_interval = self.stocks_config.get('update_interval', 300) # Check if we need to update based on time if current_time - self.last_update > update_interval: - symbols = self.stocks_config.get('symbols', []) - if not symbols: - logger.warning("No stock symbols configured") - return - - # Get cached data - cached_data = self.cache_manager.get_cached_data('stocks') + stock_symbols = self.stocks_config.get('symbols', []) + crypto_symbols = self.crypto_config.get('symbols', []) if self.crypto_config.get('enabled', False) else [] - # Check if market is open - if cached_data and not self.cache_manager._is_market_open(): - logger.info("Market is closed, using cached data") - self.stock_data = cached_data - self.last_update = current_time + if not stock_symbols and not crypto_symbols: + logger.warning("No stock or crypto symbols configured") return - # Update each symbol - for symbol in symbols: - # Check if data has changed before fetching - if cached_data and symbol in cached_data: - current_state = cached_data[symbol] - if not self.cache_manager.has_data_changed('stocks', current_state): - logger.info(f"Stock data hasn't changed for {symbol}, using existing data") - self.stock_data[symbol] = current_state - continue + # Update stocks + for symbol in stock_symbols: + data = self._fetch_stock_data(symbol, is_crypto=False) + if data: + self.stock_data[symbol] = data - data = self._fetch_stock_data(symbol) + # Update crypto + for symbol in crypto_symbols: + data = self._fetch_stock_data(symbol, is_crypto=True) if data: self.stock_data[symbol] = data self.last_update = current_time - def _get_stock_logo(self, symbol: str) -> Image.Image: - """Get stock logo image from local ticker icons directory. - - Args: - symbol: Stock symbol (e.g., 'AAPL', 'MSFT') - - Returns: - PIL Image of the logo or text-based fallback - """ - # Try to get the local ticker icon + def _get_stock_logo(self, symbol: str, is_crypto: bool = False) -> Image.Image: + """Get stock or crypto logo image from local directory.""" try: + # Try crypto icons first if it's a crypto symbol + if is_crypto: + icon_path = os.path.join(self.crypto_icons_dir, f"{symbol}.png") + if os.path.exists(icon_path): + with Image.open(icon_path) as img: + if img.mode != 'RGBA': + img = img.convert('RGBA') + max_size = min(int(self.display_manager.matrix.width / 1.2), + int(self.display_manager.matrix.height / 1.2)) + img = img.resize((max_size, max_size), Image.Resampling.LANCZOS) + return img.copy() + + # Fall back to stock icons if not crypto or crypto icon not found icon_path = os.path.join(self.ticker_icons_dir, f"{symbol}.png") if os.path.exists(icon_path): with Image.open(icon_path) as img: - # Convert to RGBA if not already if img.mode != 'RGBA': img = img.convert('RGBA') - # Resize to fit in the display - increased size by reducing divisor from 1.5 to 1.2 max_size = min(int(self.display_manager.matrix.width / 1.2), - int(self.display_manager.matrix.height / 1.2)) + int(self.display_manager.matrix.height / 1.2)) img = img.resize((max_size, max_size), Image.Resampling.LANCZOS) return img.copy() except Exception as e: - logger.warning(f"Error loading local ticker icon for {symbol}: {e}") + logger.warning(f"Error loading local icon for {symbol}: {e}") # If local icon not found or failed to load, create text-based fallback logger.warning(f"No local icon found for {symbol}. Using text fallback.") @@ -393,16 +398,16 @@ class StockManager: draw.text((x, y), text, font=font, fill=(255, 255, 255, 255)) return fallback - def _create_stock_display(self, symbol: str, price: float, change: float, change_percent: float) -> Image.Image: - """Create a display image for a stock with logo, symbol, price, and change.""" + def _create_stock_display(self, symbol: str, price: float, change: float, change_percent: float, is_crypto: bool = False) -> Image.Image: + """Create a display image for a stock or crypto with logo, symbol, price, and change.""" # Create a wider image for scrolling width = self.display_manager.matrix.width * 2 # Reduced from 3x to 2x since we'll handle spacing in display_stocks height = self.display_manager.matrix.height image = Image.new('RGB', (width, height), color=(0, 0, 0)) draw = ImageDraw.Draw(image) - # Draw large stock logo on the left - logo = self._get_stock_logo(symbol) + # Draw large stock/crypto logo on the left + logo = self._get_stock_logo(symbol, is_crypto) if logo: # Position logo on the left side with minimal spacing logo_x = 0 # Already at 0, keeping it at the far left @@ -430,34 +435,26 @@ class StockManager: price_bbox = draw.textbbox((0, 0), price_text, font=price_font) change_bbox = draw.textbbox((0, 0), change_text, font=small_font) - symbol_height = symbol_bbox[3] - symbol_bbox[1] - price_height = price_bbox[3] - price_bbox[1] - change_height = change_bbox[3] - change_bbox[1] + # Calculate total height needed + total_text_height = (symbol_bbox[3] - symbol_bbox[1]) + \ + (price_bbox[3] - price_bbox[1]) + \ + (change_bbox[3] - change_bbox[1]) - # Calculate total height needed for all text - total_text_height = symbol_height + price_height + change_height + 2 # 2 pixels for spacing - - # Calculate starting y position to center the text block + # Calculate starting y position to center all text start_y = (height - total_text_height) // 2 - # Position text elements centered between logo and chart - text_x = width // 3.5 # Changed from width//6 to width//4 to center between logo and chart - # Draw symbol - symbol_width = symbol_bbox[2] - symbol_bbox[0] - symbol_y = start_y - draw.text((text_x, symbol_y), symbol_text, font=symbol_font, fill=(255, 255, 255)) + symbol_x = (width - (symbol_bbox[2] - symbol_bbox[0])) // 2 + draw.text((symbol_x, start_y), symbol_text, font=symbol_font, fill=(255, 255, 255)) - # Draw price - aligned with symbol - price_width = price_bbox[2] - price_bbox[0] - price_x = text_x + (symbol_width - price_width) // 2 # Center price under symbol - price_y = symbol_y + symbol_height + 1 # 1 pixel spacing + # Draw price + price_x = (width - (price_bbox[2] - price_bbox[0])) // 2 + price_y = start_y + (symbol_bbox[3] - symbol_bbox[1]) draw.text((price_x, price_y), price_text, font=price_font, fill=(255, 255, 255)) - # Draw change with color based on value - aligned with price - change_width = change_bbox[2] - change_bbox[0] - change_x = price_x + (price_width - change_width) // 2 # Center change under price - change_y = price_y + price_height + 1 # 1 pixel spacing + # Draw change with color based on value + change_x = (width - (change_bbox[2] - change_bbox[0])) // 2 + change_y = price_y + (price_bbox[3] - price_bbox[1]) change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) @@ -565,8 +562,8 @@ class StockManager: self.frame_count += 1 def display_stocks(self, force_clear: bool = False): - """Display stock information with continuous scrolling animation.""" - if not self.stocks_config.get('enabled', False): + """Display stock and crypto information with continuous scrolling animation.""" + if not self.stocks_config.get('enabled', False) and not self.crypto_config.get('enabled', False): return # Start update in background if needed @@ -574,7 +571,7 @@ class StockManager: self.update_stock_data() if not self.stock_data: - logger.warning("No stock data available to display") + logger.warning("No stock or crypto data available to display") return # Get all symbols @@ -599,14 +596,21 @@ class StockManager: draw = ImageDraw.Draw(full_image) # Add initial gap before the first stock - current_x = width # Start with a full screen width gap + current_x = width # Draw each stock in sequence with consistent spacing for symbol in symbols: data = self.stock_data[symbol] + is_crypto = data.get('is_crypto', False) # Create stock display for this symbol - stock_image = self._create_stock_display(symbol, data['price'], data['change'], data['change'] / data['open'] * 100) + stock_image = self._create_stock_display( + symbol, + data['price'], + data['change'], + data['change'] / data['open'] * 100, + is_crypto + ) # Paste this stock image into the full image full_image.paste(stock_image, (current_x, 0))