Add cryptocurrency price command via CoinGecko API
New features: - !crypto <SYMBOL> command to query cryptocurrency prices - CoinGeckoAPI integration (free, no API key required) - Support for 20+ popular cryptocurrencies (BTC, ETH, DOGE, etc.) - 24-hour price change and volume data - Colored embeds matching price movement - Alias: !c for quick crypto queries Uses CoinGecko's free /simple/price endpoint with 30 calls/min rate limit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ A Discord bot that automatically posts hourly stock price updates for PayPal (PY
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `!stock <TICKER>` or `!price <TICKER>` - Get current stock price for any ticker
|
- `!stock <TICKER>` or `!price <TICKER>` - Get current stock price for any ticker
|
||||||
|
- `!crypto <SYMBOL>` or `!c <SYMBOL>` - Get current cryptocurrency price (BTC, ETH, etc.)
|
||||||
- `!market` - Check if NYSE is currently open and view trading hours
|
- `!market` - Check if NYSE is currently open and view trading hours
|
||||||
- `!ping` - Check bot responsiveness and latency
|
- `!ping` - Check bot responsiveness and latency
|
||||||
|
|
||||||
|
|||||||
76
bot.py
76
bot.py
@@ -9,6 +9,7 @@ import pytz
|
|||||||
from config import Config
|
from config import Config
|
||||||
from stock_api import YahooFinanceAPI, FinnhubAPI
|
from stock_api import YahooFinanceAPI, FinnhubAPI
|
||||||
from market_hours import MarketHours
|
from market_hours import MarketHours
|
||||||
|
from crypto_api import CoinGeckoAPI
|
||||||
|
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -39,6 +40,7 @@ class StockBot(commands.Bot):
|
|||||||
self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS]
|
self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS]
|
||||||
self.primary_ticker = Config.PRIMARY_TICKER
|
self.primary_ticker = Config.PRIMARY_TICKER
|
||||||
self.startup_announced = False
|
self.startup_announced = False
|
||||||
|
self.crypto_api = CoinGeckoAPI()
|
||||||
|
|
||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
"""Initialize the bot and set up scheduled tasks."""
|
"""Initialize the bot and set up scheduled tasks."""
|
||||||
@@ -212,6 +214,57 @@ class StockBot(commands.Bot):
|
|||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
def create_crypto_embed(self, crypto_data: dict) -> discord.Embed:
|
||||||
|
"""
|
||||||
|
Create a formatted Discord embed for cryptocurrency price data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crypto_data: Dictionary containing crypto information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Discord embed object
|
||||||
|
"""
|
||||||
|
symbol = crypto_data['symbol']
|
||||||
|
current_price = crypto_data['current_price']
|
||||||
|
change_dollar = crypto_data['change_dollar']
|
||||||
|
change_percent = crypto_data['change_percent']
|
||||||
|
|
||||||
|
# Determine color based on price movement
|
||||||
|
if change_dollar > 0:
|
||||||
|
color = discord.Color.green()
|
||||||
|
change_emoji = "📈"
|
||||||
|
elif change_dollar < 0:
|
||||||
|
color = discord.Color.red()
|
||||||
|
change_emoji = "📉"
|
||||||
|
else:
|
||||||
|
color = discord.Color.blue()
|
||||||
|
change_emoji = "➡️"
|
||||||
|
|
||||||
|
# Format change string
|
||||||
|
change_sign = "+" if change_dollar >= 0 else ""
|
||||||
|
change_str = f"{change_sign}${change_dollar} ({change_sign}{change_percent}%)"
|
||||||
|
|
||||||
|
# Create embed
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{change_emoji} {symbol} Price",
|
||||||
|
description=f"**${current_price:,.2f}**",
|
||||||
|
color=color,
|
||||||
|
timestamp=datetime.now(pytz.timezone('America/New_York'))
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(name="24h Change", value=change_str, inline=True)
|
||||||
|
embed.add_field(name="Previous Price", value=f"${crypto_data['previous_price']:,.2f}", inline=True)
|
||||||
|
|
||||||
|
# Add volume if available
|
||||||
|
volume = crypto_data.get('volume_24h', 0)
|
||||||
|
if volume > 0:
|
||||||
|
volume_str = f"${volume:,.0f}"
|
||||||
|
embed.add_field(name="24h Volume", value=volume_str, inline=False)
|
||||||
|
|
||||||
|
embed.set_footer(text="Data from CoinGecko")
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
|
||||||
# Create bot instance
|
# Create bot instance
|
||||||
bot = StockBot()
|
bot = StockBot()
|
||||||
@@ -240,6 +293,29 @@ async def get_stock_price(ctx, ticker: str = None):
|
|||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command(name='crypto', aliases=['c'])
|
||||||
|
async def get_crypto_price(ctx, symbol: str = None):
|
||||||
|
"""
|
||||||
|
Query cryptocurrency price.
|
||||||
|
|
||||||
|
Usage: !crypto BTC or !c ETH
|
||||||
|
"""
|
||||||
|
if not symbol:
|
||||||
|
await ctx.send("Please provide a crypto symbol. Usage: `!crypto BTC`")
|
||||||
|
return
|
||||||
|
|
||||||
|
symbol = symbol.upper()
|
||||||
|
logger.info(f"Crypto query for {symbol} by {ctx.author}")
|
||||||
|
|
||||||
|
crypto_data = bot.crypto_api.get_crypto_price(symbol)
|
||||||
|
if not crypto_data:
|
||||||
|
await ctx.send(f"Unable to retrieve data for crypto: {symbol}")
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = bot.create_crypto_embed(crypto_data)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
@bot.command(name='ping')
|
@bot.command(name='ping')
|
||||||
async def ping(ctx):
|
async def ping(ctx):
|
||||||
"""Check if bot is responsive."""
|
"""Check if bot is responsive."""
|
||||||
|
|||||||
113
crypto_api.py
Normal file
113
crypto_api.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CoinGeckoAPI:
|
||||||
|
"""CoinGecko implementation for cryptocurrency price data."""
|
||||||
|
|
||||||
|
BASE_URL = "https://api.coingecko.com/api/v3"
|
||||||
|
|
||||||
|
# Common crypto symbol to CoinGecko ID mapping
|
||||||
|
SYMBOL_MAP = {
|
||||||
|
'BTC': 'bitcoin',
|
||||||
|
'ETH': 'ethereum',
|
||||||
|
'USDT': 'tether',
|
||||||
|
'BNB': 'binancecoin',
|
||||||
|
'SOL': 'solana',
|
||||||
|
'XRP': 'ripple',
|
||||||
|
'USDC': 'usd-coin',
|
||||||
|
'ADA': 'cardano',
|
||||||
|
'DOGE': 'dogecoin',
|
||||||
|
'TRX': 'tron',
|
||||||
|
'DOT': 'polkadot',
|
||||||
|
'MATIC': 'matic-network',
|
||||||
|
'LTC': 'litecoin',
|
||||||
|
'SHIB': 'shiba-inu',
|
||||||
|
'AVAX': 'avalanche-2',
|
||||||
|
'LINK': 'chainlink',
|
||||||
|
'UNI': 'uniswap',
|
||||||
|
'XLM': 'stellar',
|
||||||
|
'ATOM': 'cosmos',
|
||||||
|
'XMR': 'monero'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_crypto_price(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieve cryptocurrency price data from CoinGecko.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: Crypto symbol (e.g., BTC, ETH, DOGE)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with crypto data or None if unavailable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Convert symbol to CoinGecko ID
|
||||||
|
symbol = symbol.upper()
|
||||||
|
coin_id = self.SYMBOL_MAP.get(symbol, symbol.lower())
|
||||||
|
|
||||||
|
# Fetch price data
|
||||||
|
url = f"{self.BASE_URL}/simple/price"
|
||||||
|
params = {
|
||||||
|
'ids': coin_id,
|
||||||
|
'vs_currencies': 'usd',
|
||||||
|
'include_24hr_change': 'true',
|
||||||
|
'include_24hr_vol': 'true',
|
||||||
|
'include_last_updated_at': 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if coin_id not in data:
|
||||||
|
logger.warning(f"No data available for crypto: {symbol}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
crypto_data = data[coin_id]
|
||||||
|
current_price = crypto_data.get('usd', 0)
|
||||||
|
change_percent = crypto_data.get('usd_24h_change', 0)
|
||||||
|
|
||||||
|
if current_price == 0:
|
||||||
|
logger.warning(f"Invalid price data for crypto: {symbol}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate previous price from 24h change
|
||||||
|
change_decimal = change_percent / 100
|
||||||
|
previous_price = current_price / (1 + change_decimal)
|
||||||
|
change_dollar = current_price - previous_price
|
||||||
|
|
||||||
|
return {
|
||||||
|
'symbol': symbol,
|
||||||
|
'coin_id': coin_id,
|
||||||
|
'current_price': round(current_price, 2),
|
||||||
|
'previous_price': round(previous_price, 2),
|
||||||
|
'change_dollar': round(change_dollar, 2),
|
||||||
|
'change_percent': round(change_percent, 2),
|
||||||
|
'volume_24h': crypto_data.get('usd_24h_vol', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Error fetching data for {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error fetching data for {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if CoinGecko API is accessible.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if accessible, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.BASE_URL}/ping"
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CoinGecko API unavailable: {e}")
|
||||||
|
return False
|
||||||
Reference in New Issue
Block a user