Initial commit: Discord stock price bot with hourly PYPL updates

Implemented Discord bot for automated stock price tracking and reporting with the following features:

- Hourly PayPal (PYPL) stock price updates during NYSE market hours
- Custom branding for PYPL as "Steaks Stablecoin Value"
- Manual stock price queries via !stock and !price commands
- Multi-provider stock API support (Yahoo Finance and Finnhub)
- NYSE market hours detection with holiday awareness
- Discord embed formatting with color-coded price changes
- Docker containerization for consistent deployment
- Comprehensive documentation and deployment guides

Technical stack:
- Python 3.9+ with discord.py
- Finnhub API for stock price data
- APScheduler for hourly automated updates
- Docker support for local and Unraid deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Simard
2025-12-02 21:54:11 -06:00
commit 5964cadf94
14 changed files with 946 additions and 0 deletions

5
stock_api/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .base import StockAPIBase
from .yahoo import YahooFinanceAPI
from .finnhub_api import FinnhubAPI
__all__ = ['StockAPIBase', 'YahooFinanceAPI', 'FinnhubAPI']

39
stock_api/base.py Normal file
View File

@@ -0,0 +1,39 @@
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any
class StockAPIBase(ABC):
"""
Abstract base class for stock price data providers.
This design allows seamless switching between different stock APIs.
"""
@abstractmethod
def get_stock_price(self, ticker: str) -> Optional[Dict[str, Any]]:
"""
Retrieve current stock price and related data for a given ticker.
Args:
ticker: Stock ticker symbol (e.g., 'PYPL', 'AAPL')
Returns:
Dictionary containing:
- ticker: str
- current_price: float
- previous_close: float
- change_dollar: float
- change_percent: float
- market_open: bool
Returns None if ticker is invalid or data unavailable.
"""
pass
@abstractmethod
def is_available(self) -> bool:
"""
Check if the API service is currently available.
Returns:
True if API is accessible, False otherwise.
"""
pass

82
stock_api/finnhub_api.py Normal file
View File

@@ -0,0 +1,82 @@
import finnhub
import logging
from typing import Optional, Dict, Any
from .base import StockAPIBase
from datetime import datetime
import pytz
logger = logging.getLogger(__name__)
class FinnhubAPI(StockAPIBase):
"""Finnhub implementation of stock price provider."""
def __init__(self, api_key: str):
"""
Initialize Finnhub API client.
Args:
api_key: Finnhub API key
"""
self.client = finnhub.Client(api_key=api_key)
self.api_key = api_key
def get_stock_price(self, ticker: str) -> Optional[Dict[str, Any]]:
"""
Retrieve stock price data from Finnhub.
Args:
ticker: Stock ticker symbol
Returns:
Dictionary with stock data or None if unavailable
"""
try:
# Get current quote data
quote = self.client.quote(ticker)
if not quote or quote.get('c') is None or quote.get('c') == 0:
logger.warning(f"No data available for ticker: {ticker}")
return None
current_price = float(quote['c']) # Current price
previous_close = float(quote['pc']) # Previous close
if current_price == 0 or previous_close == 0:
logger.warning(f"Invalid price data for ticker: {ticker}")
return None
change_dollar = current_price - previous_close
change_percent = (change_dollar / previous_close) * 100
# Use MarketHours utility for accurate market status
from market_hours import MarketHours
market_open = MarketHours.is_market_open()
return {
'ticker': ticker.upper(),
'current_price': round(current_price, 2),
'previous_close': round(previous_close, 2),
'change_dollar': round(change_dollar, 2),
'change_percent': round(change_percent, 2),
'market_open': market_open
}
except Exception as e:
logger.error(f"Error fetching data for {ticker}: {e}")
return None
def is_available(self) -> bool:
"""
Check if Finnhub API is accessible.
Returns:
True if accessible, False otherwise
"""
try:
test_quote = self.client.quote("AAPL")
return test_quote is not None and test_quote.get('c') is not None
except Exception as e:
logger.error(f"Finnhub API unavailable: {e}")
return False

75
stock_api/yahoo.py Normal file
View File

@@ -0,0 +1,75 @@
import yfinance as yf
import logging
from typing import Optional, Dict, Any
from .base import StockAPIBase
logger = logging.getLogger(__name__)
class YahooFinanceAPI(StockAPIBase):
"""Yahoo Finance implementation of stock price provider."""
def get_stock_price(self, ticker: str) -> Optional[Dict[str, Any]]:
"""
Retrieve stock price data from Yahoo Finance.
Args:
ticker: Stock ticker symbol
Returns:
Dictionary with stock data or None if unavailable
"""
try:
stock = yf.Ticker(ticker)
# Use history() method instead of info to avoid rate limiting
# Get last 2 days of data to calculate change
hist = stock.history(period="2d")
if hist.empty or len(hist) < 1:
logger.warning(f"No historical data available for ticker: {ticker}")
return None
# Get most recent price data
current_price = float(hist['Close'].iloc[-1])
# Get previous close (either from previous day or use current data)
if len(hist) >= 2:
previous_close = float(hist['Close'].iloc[-2])
else:
previous_close = float(hist['Open'].iloc[-1])
change_dollar = current_price - previous_close
change_percent = (change_dollar / previous_close) * 100
# Market is considered open if we have today's data
market_open = True # Simplified - actual market status requires additional API call
return {
'ticker': ticker.upper(),
'current_price': round(current_price, 2),
'previous_close': round(previous_close, 2),
'change_dollar': round(change_dollar, 2),
'change_percent': round(change_percent, 2),
'market_open': market_open
}
except Exception as e:
logger.error(f"Error fetching data for {ticker}: {e}")
return None
def is_available(self) -> bool:
"""
Check if Yahoo Finance API is accessible.
Returns:
True if accessible, False otherwise
"""
try:
test_stock = yf.Ticker("AAPL")
info = test_stock.info
return info is not None and len(info) > 0
except Exception as e:
logger.error(f"Yahoo Finance API unavailable: {e}")
return False