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.

This commit is contained in:
ChuckBuilds
2025-04-23 15:54:51 -05:00
parent d93f853490
commit 9c1220b605
2 changed files with 87 additions and 75 deletions

View File

@@ -57,7 +57,15 @@
"enabled": true, "enabled": true,
"update_interval": 300, "update_interval": 300,
"symbols": [ "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}%)" "display_format": "{symbol}: ${price} ({change}%)"
}, },

View File

@@ -23,6 +23,7 @@ class StockManager:
self.config = config self.config = config
self.display_manager = display_manager self.display_manager = display_manager
self.stocks_config = config.get('stocks', {}) self.stocks_config = config.get('stocks', {})
self.crypto_config = config.get('crypto', {})
self.last_update = 0 self.last_update = 0
self.stock_data = {} self.stock_data = {}
self.current_stock_index = 0 self.current_stock_index = 0
@@ -46,6 +47,11 @@ class StockManager:
if not os.path.exists(self.ticker_icons_dir): if not os.path.exists(self.ticker_icons_dir):
logger.warning(f"Ticker icons directory not found: {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 # Set up the logo directory for external logos
self.logo_dir = os.path.join('assets', 'stocks') self.logo_dir = os.path.join('assets', 'stocks')
if not os.path.exists(self.logo_dir): if not os.path.exists(self.logo_dir):
@@ -138,17 +144,22 @@ class StockManager:
logger.error(f"Error extracting JSON data: {e}") logger.error(f"Error extracting JSON data: {e}")
return {} return {}
def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: def _fetch_stock_data(self, symbol: str, is_crypto: bool = False) -> Dict[str, Any]:
"""Fetch stock data from Yahoo Finance public API.""" """Fetch stock or crypto data from Yahoo Finance public API."""
# Try to get cached data first # 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: if cached_data and symbol in cached_data:
logger.info(f"Using cached data for {symbol}") logger.info(f"Using cached data for {symbol}")
return cached_data[symbol] return cached_data[symbol]
try: try:
# Use Yahoo Finance query1 API for chart data # For crypto, we need to append -USD if not already present
encoded_symbol = urllib.parse.quote(symbol) 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}" url = f"https://query1.finance.yahoo.com/v8/finance/chart/{encoded_symbol}"
params = { params = {
'interval': '5m', # 5-minute intervals 'interval': '5m', # 5-minute intervals
@@ -209,14 +220,15 @@ class StockManager:
"price": current_price, "price": current_price,
"change": change_pct, "change": change_pct,
"open": prev_close, "open": prev_close,
"price_history": price_history "price_history": price_history,
"is_crypto": is_crypto
} }
# Cache the new data # Cache the new data
if cached_data is None: if cached_data is None:
cached_data = {} cached_data = {}
cached_data[symbol] = stock_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 return stock_data
@@ -312,67 +324,60 @@ class StockManager:
logger.info(f"Stock symbols changed. New symbols: {new_symbols}") logger.info(f"Stock symbols changed. New symbols: {new_symbols}")
def update_stock_data(self): 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() current_time = time.time()
update_interval = self.stocks_config.get('update_interval', 300) update_interval = self.stocks_config.get('update_interval', 300)
# Check if we need to update based on time # Check if we need to update based on time
if current_time - self.last_update > update_interval: if current_time - self.last_update > update_interval:
symbols = self.stocks_config.get('symbols', []) stock_symbols = self.stocks_config.get('symbols', [])
if not symbols: crypto_symbols = self.crypto_config.get('symbols', []) if self.crypto_config.get('enabled', False) else []
logger.warning("No stock symbols configured")
return
# Get cached data
cached_data = self.cache_manager.get_cached_data('stocks')
# Check if market is open if not stock_symbols and not crypto_symbols:
if cached_data and not self.cache_manager._is_market_open(): logger.warning("No stock or crypto symbols configured")
logger.info("Market is closed, using cached data")
self.stock_data = cached_data
self.last_update = current_time
return return
# Update each symbol # Update stocks
for symbol in symbols: for symbol in stock_symbols:
# Check if data has changed before fetching data = self._fetch_stock_data(symbol, is_crypto=False)
if cached_data and symbol in cached_data: if data:
current_state = cached_data[symbol] self.stock_data[symbol] = data
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
data = self._fetch_stock_data(symbol) # Update crypto
for symbol in crypto_symbols:
data = self._fetch_stock_data(symbol, is_crypto=True)
if data: if data:
self.stock_data[symbol] = data self.stock_data[symbol] = data
self.last_update = current_time self.last_update = current_time
def _get_stock_logo(self, symbol: str) -> Image.Image: def _get_stock_logo(self, symbol: str, is_crypto: bool = False) -> Image.Image:
"""Get stock logo image from local ticker icons directory. """Get stock or crypto logo image from local 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
try: 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") icon_path = os.path.join(self.ticker_icons_dir, f"{symbol}.png")
if os.path.exists(icon_path): if os.path.exists(icon_path):
with Image.open(icon_path) as img: with Image.open(icon_path) as img:
# Convert to RGBA if not already
if img.mode != 'RGBA': if img.mode != 'RGBA':
img = img.convert('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), 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) img = img.resize((max_size, max_size), Image.Resampling.LANCZOS)
return img.copy() return img.copy()
except Exception as e: 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 # 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.") 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)) draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
return fallback return fallback
def _create_stock_display(self, symbol: str, price: float, change: float, change_percent: float) -> Image.Image: 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 with logo, symbol, price, and change.""" """Create a display image for a stock or crypto with logo, symbol, price, and change."""
# Create a wider image for scrolling # 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 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 height = self.display_manager.matrix.height
image = Image.new('RGB', (width, height), color=(0, 0, 0)) image = Image.new('RGB', (width, height), color=(0, 0, 0))
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
# Draw large stock logo on the left # Draw large stock/crypto logo on the left
logo = self._get_stock_logo(symbol) logo = self._get_stock_logo(symbol, is_crypto)
if logo: if logo:
# Position logo on the left side with minimal spacing # Position logo on the left side with minimal spacing
logo_x = 0 # Already at 0, keeping it at the far left 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) price_bbox = draw.textbbox((0, 0), price_text, font=price_font)
change_bbox = draw.textbbox((0, 0), change_text, font=small_font) change_bbox = draw.textbbox((0, 0), change_text, font=small_font)
symbol_height = symbol_bbox[3] - symbol_bbox[1] # Calculate total height needed
price_height = price_bbox[3] - price_bbox[1] total_text_height = (symbol_bbox[3] - symbol_bbox[1]) + \
change_height = change_bbox[3] - change_bbox[1] (price_bbox[3] - price_bbox[1]) + \
(change_bbox[3] - change_bbox[1])
# Calculate total height needed for all text # Calculate starting y position to center 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
start_y = (height - total_text_height) // 2 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 # Draw symbol
symbol_width = symbol_bbox[2] - symbol_bbox[0] symbol_x = (width - (symbol_bbox[2] - symbol_bbox[0])) // 2
symbol_y = start_y draw.text((symbol_x, start_y), symbol_text, font=symbol_font, fill=(255, 255, 255))
draw.text((text_x, symbol_y), symbol_text, font=symbol_font, fill=(255, 255, 255))
# Draw price - aligned with symbol # Draw price
price_width = price_bbox[2] - price_bbox[0] price_x = (width - (price_bbox[2] - price_bbox[0])) // 2
price_x = text_x + (symbol_width - price_width) // 2 # Center price under symbol price_y = start_y + (symbol_bbox[3] - symbol_bbox[1])
price_y = symbol_y + symbol_height + 1 # 1 pixel spacing
draw.text((price_x, price_y), price_text, font=price_font, fill=(255, 255, 255)) 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 # Draw change with color based on value
change_width = change_bbox[2] - change_bbox[0] change_x = (width - (change_bbox[2] - change_bbox[0])) // 2
change_x = price_x + (price_width - change_width) // 2 # Center change under price change_y = price_y + (price_bbox[3] - price_bbox[1])
change_y = price_y + price_height + 1 # 1 pixel spacing
change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) 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) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color)
@@ -565,8 +562,8 @@ class StockManager:
self.frame_count += 1 self.frame_count += 1
def display_stocks(self, force_clear: bool = False): def display_stocks(self, force_clear: bool = False):
"""Display stock information with continuous scrolling animation.""" """Display stock and crypto information with continuous scrolling animation."""
if not self.stocks_config.get('enabled', False): if not self.stocks_config.get('enabled', False) and not self.crypto_config.get('enabled', False):
return return
# Start update in background if needed # Start update in background if needed
@@ -574,7 +571,7 @@ class StockManager:
self.update_stock_data() self.update_stock_data()
if not self.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 return
# Get all symbols # Get all symbols
@@ -599,14 +596,21 @@ class StockManager:
draw = ImageDraw.Draw(full_image) draw = ImageDraw.Draw(full_image)
# Add initial gap before the first stock # 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 # Draw each stock in sequence with consistent spacing
for symbol in symbols: for symbol in symbols:
data = self.stock_data[symbol] data = self.stock_data[symbol]
is_crypto = data.get('is_crypto', False)
# Create stock display for this symbol # 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 # Paste this stock image into the full image
full_image.paste(stock_image, (current_x, 0)) full_image.paste(stock_image, (current_x, 0))