mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-12 05:42:59 +00:00
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:
@@ -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}%)"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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")
|
|
||||||
|
if not stock_symbols and not crypto_symbols:
|
||||||
|
logger.warning("No stock or crypto symbols configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get cached data
|
# Update stocks
|
||||||
cached_data = self.cache_manager.get_cached_data('stocks')
|
for symbol in stock_symbols:
|
||||||
|
data = self._fetch_stock_data(symbol, is_crypto=False)
|
||||||
|
if data:
|
||||||
|
self.stock_data[symbol] = data
|
||||||
|
|
||||||
# Check if market is open
|
# Update crypto
|
||||||
if cached_data and not self.cache_manager._is_market_open():
|
for symbol in crypto_symbols:
|
||||||
logger.info("Market is closed, using cached data")
|
data = self._fetch_stock_data(symbol, is_crypto=True)
|
||||||
self.stock_data = cached_data
|
|
||||||
self.last_update = current_time
|
|
||||||
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
|
|
||||||
|
|
||||||
data = self._fetch_stock_data(symbol)
|
|
||||||
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user