Building a Precious Metals Price Tracker with Python

18 min read Commodities

Track gold, silver, platinum, and palladium prices in real-time with Python. This comprehensive tutorial shows you how to build a production-ready precious metals tracker using pandas for data analysis, matplotlib for visualization, and automated price alerts. Perfect for investors, developers, and data analysts.

Precious metals have been stores of value for thousands of years, and today's investors need sophisticated tools to track their performance. Whether you're managing a portfolio, conducting research, or building a fintech application, Python provides the perfect ecosystem for working with commodities data.

This tutorial builds a complete price tracking system from scratch. You'll learn how to fetch real-time and historical data for gold (XAU), silver (XAG), platinum (XPT), and palladium (XPD), analyze trends with pandas, create professional visualizations, and set up automated alerts. All code is production-ready and follows Python best practices.

1. Project Setup and Dependencies

Installing Required Libraries

First, set up a virtual environment and install the necessary packages:

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install requests pandas matplotlib numpy redis python-dotenv

# Create requirements.txt
cat > requirements.txt << EOF
requests==2.31.0
pandas==2.1.0
matplotlib==3.8.0
numpy==1.26.0
redis==5.0.0
python-dotenv==1.0.0
EOF

Project Structure

Organize your project with a clean structure:

precious-metals-tracker/
├── venv/
├── src/
│   ├── __init__.py
│   ├── api_client.py       # API integration
│   ├── data_handler.py     # Pandas data operations
│   ├── alerts.py           # Price alert system
│   ├── visualizer.py       # Matplotlib charts
│   └── utils.py            # Helper functions
├── data/
│   ├── cache/              # Cached API responses
│   └── exports/            # CSV exports
├── config.py               # Configuration
├── .env                    # API keys (git-ignored)
├── main.py                 # Entry point
└── requirements.txt

Configuration Setup

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    """Application configuration."""

    # API Configuration
    API_KEY = os.getenv('UNIRATE_API_KEY')
    API_BASE_URL = 'https://api.unirateapi.com/v1'

    # Supported precious metals
    METALS = {
        'XAU': {'name': 'Gold', 'color': '#FFD700'},
        'XAG': {'name': 'Silver', 'color': '#C0C0C0'},
        'XPT': {'name': 'Platinum', 'color': '#E5E4E2'},
        'XPD': {'name': 'Palladium', 'color': '#CED0DD'}
    }

    # Redis cache settings
    REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
    REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
    CACHE_TTL = 1800  # 30 minutes

    # Data directories
    DATA_DIR = 'data'
    CACHE_DIR = os.path.join(DATA_DIR, 'cache')
    EXPORT_DIR = os.path.join(DATA_DIR, 'exports')

    # Alert thresholds (percentage change)
    ALERT_THRESHOLD = 2.0  # Alert on 2% change

    @classmethod
    def ensure_directories(cls):
        """Create necessary directories."""
        os.makedirs(cls.CACHE_DIR, exist_ok=True)
        os.makedirs(cls.EXPORT_DIR, exist_ok=True)

Environment Variables: Create a .env file in your project root with your API key: UNIRATE_API_KEY=your_api_key_here. Never commit this file to version control.

2. Precious Metals API Integration

API Client Implementation

Build a robust API client with caching and error handling:

# src/api_client.py
import requests
import redis
import json
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Dict, List, Optional
from config import Config

class PreciousMetalsAPI:
    """
    API client for precious metals price data.
    Supports gold, silver, platinum, and palladium.
    """

    def __init__(self, api_key: str, use_cache: bool = True):
        self.api_key = api_key
        self.base_url = Config.API_BASE_URL
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'PreciousMetalsTracker/1.0'
        })

        # Redis cache
        self.use_cache = use_cache
        if use_cache:
            self.redis = redis.Redis(
                host=Config.REDIS_HOST,
                port=Config.REDIS_PORT,
                decode_responses=True
            )

    def get_current_prices(
        self,
        metals: List[str] = None,
        base_currency: str = 'USD'
    ) -> Dict[str, Decimal]:
        """
        Get current prices for specified metals.

        Args:
            metals: List of metal codes (e.g., ['XAU', 'XAG'])
            base_currency: Currency for prices (default: USD)

        Returns:
            Dict mapping metal codes to prices per troy ounce
        """
        if metals is None:
            metals = list(Config.METALS.keys())

        cache_key = f"current_prices:{base_currency}"

        # Check cache
        if self.use_cache:
            cached = self.redis.get(cache_key)
            if cached:
                all_prices = json.loads(cached)
                return {
                    metal: Decimal(all_prices[metal])
                    for metal in metals
                    if metal in all_prices
                }

        # Fetch from API
        try:
            response = self.session.get(
                f"{self.base_url}/latest/{base_currency}",
                params={'api_key': self.api_key},
                timeout=10
            )
            response.raise_for_status()
            data = response.json()

            # Convert XAU/XAG/XPT/XPD rates to prices per troy ounce
            prices = {}
            for metal in metals:
                if metal in data['rates']:
                    # Rate is troy ounces per base currency, invert it
                    rate = Decimal(str(data['rates'][metal]))
                    prices[metal] = Decimal('1') / rate

            # Cache the results
            if self.use_cache:
                cache_data = {k: str(v) for k, v in prices.items()}
                self.redis.setex(
                    cache_key,
                    Config.CACHE_TTL,
                    json.dumps(cache_data)
                )

            return prices

        except requests.RequestException as e:
            raise RuntimeError(f"Failed to fetch prices: {e}")

    def get_historical_prices(
        self,
        metal: str,
        start_date: str,
        end_date: str,
        base_currency: str = 'USD'
    ) -> Dict[str, Decimal]:
        """
        Get historical prices for a metal over a date range.

        Args:
            metal: Metal code (e.g., 'XAU')
            start_date: Start date (YYYY-MM-DD)
            end_date: End date (YYYY-MM-DD)
            base_currency: Currency for prices

        Returns:
            Dict mapping dates to prices
        """
        try:
            response = self.session.get(
                f"{self.base_url}/timeseries",
                params={
                    'api_key': self.api_key,
                    'base': base_currency,
                    'start_date': start_date,
                    'end_date': end_date
                },
                timeout=15
            )
            response.raise_for_status()
            data = response.json()

            prices = {}
            for date, rates in data['rates'].items():
                if metal in rates:
                    rate = Decimal(str(rates[metal]))
                    prices[date] = Decimal('1') / rate

            return prices

        except requests.RequestException as e:
            raise RuntimeError(f"Failed to fetch historical data: {e}")

    def get_price_for_date(
        self,
        metal: str,
        date: str,
        base_currency: str = 'USD'
    ) -> Decimal:
        """Get price for a specific metal on a specific date."""
        try:
            response = self.session.get(
                f"{self.base_url}/historical/{date}/{base_currency}",
                params={'api_key': self.api_key},
                timeout=5
            )
            response.raise_for_status()
            data = response.json()

            if metal in data['rates']:
                rate = Decimal(str(data['rates'][metal]))
                return Decimal('1') / rate

            raise ValueError(f"Metal {metal} not found in response")

        except requests.RequestException as e:
            raise RuntimeError(f"Failed to fetch price for {date}: {e}")

3. Working with Pandas DataFrames

Data Handler Implementation

# src/data_handler.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List
from decimal import Decimal
from config import Config

class MetalsDataHandler:
    """Handle precious metals data with pandas."""

    def __init__(self, api_client):
        self.api = api_client

    def create_current_prices_df(self) -> pd.DataFrame:
        """
        Create DataFrame with current prices for all metals.

        Returns:
            DataFrame with columns: metal, name, price, timestamp
        """
        prices = self.api.get_current_prices()

        data = []
        for metal_code, price in prices.items():
            data.append({
                'metal': metal_code,
                'name': Config.METALS[metal_code]['name'],
                'price_usd': float(price),
                'timestamp': datetime.now()
            })

        df = pd.DataFrame(data)
        df['price_usd'] = df['price_usd'].round(2)

        return df

    def create_historical_df(
        self,
        metal: str,
        days: int = 365
    ) -> pd.DataFrame:
        """
        Create DataFrame with historical prices.

        Args:
            metal: Metal code (e.g., 'XAU')
            days: Number of days of history

        Returns:
            DataFrame with date index and price column
        """
        end_date = datetime.now()
        start_date = end_date - timedelta(days=days)

        prices = self.api.get_historical_prices(
            metal,
            start_date.strftime('%Y-%m-%d'),
            end_date.strftime('%Y-%m-%d')
        )

        # Convert to DataFrame
        df = pd.DataFrame([
            {'date': date, 'price': float(price)}
            for date, price in prices.items()
        ])

        df['date'] = pd.to_datetime(df['date'])
        df = df.sort_values('date')
        df.set_index('date', inplace=True)

        return df

    def create_multi_metal_df(
        self,
        metals: List[str],
        days: int = 90
    ) -> pd.DataFrame:
        """
        Create DataFrame with multiple metals for comparison.

        Returns:
            DataFrame with date index and columns for each metal
        """
        dfs = []

        for metal in metals:
            df = self.create_historical_df(metal, days)
            df.columns = [metal]
            dfs.append(df)

        # Merge all DataFrames
        result = pd.concat(dfs, axis=1)
        result.fillna(method='ffill', inplace=True)  # Forward fill missing values

        return result

    def calculate_statistics(self, df: pd.DataFrame) -> Dict:
        """
        Calculate statistical metrics for price data.

        Args:
            df: DataFrame with price data

        Returns:
            Dict with statistics
        """
        stats = {
            'current': float(df['price'].iloc[-1]),
            'mean': float(df['price'].mean()),
            'median': float(df['price'].median()),
            'std': float(df['price'].std()),
            'min': float(df['price'].min()),
            'max': float(df['price'].max()),
            'change_pct': self._calculate_percent_change(df)
        }

        return stats

    def _calculate_percent_change(self, df: pd.DataFrame) -> float:
        """Calculate percentage change from first to last value."""
        if len(df) < 2:
            return 0.0

        first = df['price'].iloc[0]
        last = df['price'].iloc[-1]

        return float(((last - first) / first) * 100)

    def calculate_moving_averages(
        self,
        df: pd.DataFrame,
        windows: List[int] = [7, 30, 90]
    ) -> pd.DataFrame:
        """
        Add moving average columns to DataFrame.

        Args:
            df: DataFrame with price column
            windows: List of window sizes for moving averages

        Returns:
            DataFrame with additional MA columns
        """
        result = df.copy()

        for window in windows:
            col_name = f'MA_{window}'
            result[col_name] = result['price'].rolling(
                window=window,
                min_periods=1
            ).mean()

        return result

Usage Examples

# Example: Working with the data handler
from src.api_client import PreciousMetalsAPI
from src.data_handler import MetalsDataHandler
from config import Config

# Initialize
api = PreciousMetalsAPI(Config.API_KEY)
handler = MetalsDataHandler(api)

# Get current prices
current_df = handler.create_current_prices_df()
print("Current Precious Metals Prices:")
print(current_df)
print()

# Get gold historical data
gold_df = handler.create_historical_df('XAU', days=365)
gold_stats = handler.calculate_statistics(gold_df)

print("Gold Statistics (1 year):")
for key, value in gold_stats.items():
    print(f"  {key}: ${value:.2f}" if key != 'change_pct'
          else f"  {key}: {value:.2f}%")
print()

# Compare all metals
multi_df = handler.create_multi_metal_df(['XAU', 'XAG', 'XPT', 'XPD'], days=90)
print("90-day correlation matrix:")
print(multi_df.corr())

4. Implementing Price Alert System

Alert Manager

# src/alerts.py
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
from typing import Dict, List, Optional
from decimal import Decimal
import redis
from config import Config

class PriceAlert:
    """Individual price alert configuration."""

    def __init__(
        self,
        metal: str,
        target_price: Decimal,
        direction: str,  # 'above' or 'below'
        email: Optional[str] = None
    ):
        self.metal = metal
        self.target_price = target_price
        self.direction = direction
        self.email = email
        self.created_at = datetime.now()
        self.triggered = False

    def check(self, current_price: Decimal) -> bool:
        """Check if alert should trigger."""
        if self.triggered:
            return False

        if self.direction == 'above':
            return current_price >= self.target_price
        else:  # below
            return current_price <= self.target_price

class AlertManager:
    """Manage price alerts for precious metals."""

    def __init__(self, api_client):
        self.api = api_client
        self.redis = redis.Redis(
            host=Config.REDIS_HOST,
            port=Config.REDIS_PORT,
            decode_responses=True
        )
        self.alerts_key = 'metal_alerts'

    def add_alert(self, alert: PriceAlert) -> str:
        """
        Add a new price alert.

        Returns:
            Alert ID
        """
        alert_id = f"{alert.metal}_{datetime.now().timestamp()}"

        alert_data = {
            'id': alert_id,
            'metal': alert.metal,
            'target_price': str(alert.target_price),
            'direction': alert.direction,
            'email': alert.email,
            'created_at': alert.created_at.isoformat(),
            'triggered': False
        }

        # Store in Redis
        self.redis.hset(
            self.alerts_key,
            alert_id,
            json.dumps(alert_data)
        )

        return alert_id

    def check_all_alerts(self) -> List[Dict]:
        """
        Check all active alerts against current prices.

        Returns:
            List of triggered alerts
        """
        triggered_alerts = []

        # Get current prices
        current_prices = self.api.get_current_prices()

        # Get all alerts
        alerts_data = self.redis.hgetall(self.alerts_key)

        for alert_id, alert_json in alerts_data.items():
            alert_data = json.loads(alert_json)

            if alert_data['triggered']:
                continue

            metal = alert_data['metal']
            if metal not in current_prices:
                continue

            current_price = current_prices[metal]
            target_price = Decimal(alert_data['target_price'])
            direction = alert_data['direction']

            # Check if alert triggers
            should_trigger = (
                (direction == 'above' and current_price >= target_price) or
                (direction == 'below' and current_price <= target_price)
            )

            if should_trigger:
                alert_data['triggered'] = True
                alert_data['triggered_at'] = datetime.now().isoformat()
                alert_data['triggered_price'] = str(current_price)

                # Update in Redis
                self.redis.hset(
                    self.alerts_key,
                    alert_id,
                    json.dumps(alert_data)
                )

                triggered_alerts.append(alert_data)

                # Send notification if email provided
                if alert_data['email']:
                    self._send_email_notification(alert_data, current_price)

        return triggered_alerts

    def _send_email_notification(
        self,
        alert_data: Dict,
        current_price: Decimal
    ):
        """Send email notification for triggered alert."""
        metal_name = Config.METALS[alert_data['metal']]['name']

        subject = f"Price Alert: {metal_name} {alert_data['direction']} ${alert_data['target_price']}"

        body = f"""
        Your price alert has been triggered!

        Metal: {metal_name} ({alert_data['metal']})
        Target Price: ${alert_data['target_price']}
        Direction: {alert_data['direction']}
        Current Price: ${current_price:.2f}

        Alert created: {alert_data['created_at']}
        Triggered: {alert_data['triggered_at']}
        """

        # Note: Configure SMTP settings in your environment
        # This is a placeholder implementation
        print(f"Email notification: {subject}")
        print(body)

    def get_active_alerts(self) -> List[Dict]:
        """Get all active (non-triggered) alerts."""
        alerts_data = self.redis.hgetall(self.alerts_key)

        active = []
        for alert_json in alerts_data.values():
            alert_data = json.loads(alert_json)
            if not alert_data['triggered']:
                active.append(alert_data)

        return active

    def remove_alert(self, alert_id: str):
        """Remove an alert."""
        self.redis.hdel(self.alerts_key, alert_id)

Production Note: For email notifications, configure SMTP settings using environment variables. Consider using services like SendGrid or AWS SES for reliable email delivery in production.

5. Data Visualization with Matplotlib

Chart Generator

# src/visualizer.py
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
from typing import List, Optional
from config import Config
import os

class MetalsVisualizer:
    """Create charts for precious metals data."""

    def __init__(self, output_dir: Optional[str] = None):
        self.output_dir = output_dir or Config.EXPORT_DIR

        # Set style
        plt.style.use('seaborn-v0_8-darkgrid')

    def plot_price_history(
        self,
        df: pd.DataFrame,
        metal: str,
        title: Optional[str] = None,
        show_ma: bool = True,
        save: bool = True
    ):
        """
        Plot price history for a single metal.

        Args:
            df: DataFrame with date index and price column
            metal: Metal code
            title: Chart title
            show_ma: Show moving averages
            save: Save to file
        """
        fig, ax = plt.subplots(figsize=(12, 6))

        # Plot price
        metal_name = Config.METALS[metal]['name']
        color = Config.METALS[metal]['color']

        ax.plot(
            df.index,
            df['price'],
            color=color,
            linewidth=2,
            label=f'{metal_name} Price'
        )

        # Add moving averages
        if show_ma and 'MA_7' in df.columns:
            ax.plot(df.index, df['MA_7'], '--', alpha=0.7, label='7-day MA')
            ax.plot(df.index, df['MA_30'], '--', alpha=0.7, label='30-day MA')

        # Formatting
        ax.set_title(title or f'{metal_name} Price History', fontsize=16, fontweight='bold')
        ax.set_xlabel('Date', fontsize=12)
        ax.set_ylabel('Price (USD per troy oz)', fontsize=12)
        ax.legend(loc='best')
        ax.grid(True, alpha=0.3)

        # Format x-axis dates
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
        plt.xticks(rotation=45)

        plt.tight_layout()

        if save:
            filename = f"{metal}_history_{pd.Timestamp.now().strftime('%Y%m%d')}.png"
            filepath = os.path.join(self.output_dir, filename)
            plt.savefig(filepath, dpi=300, bbox_inches='tight')
            print(f"Chart saved: {filepath}")

        plt.show()

    def plot_multi_metal_comparison(
        self,
        df: pd.DataFrame,
        metals: List[str],
        normalize: bool = True,
        save: bool = True
    ):
        """
        Compare multiple metals on one chart.

        Args:
            df: DataFrame with columns for each metal
            metals: List of metal codes
            normalize: Normalize to percentage change
            save: Save to file
        """
        fig, ax = plt.subplots(figsize=(14, 7))

        for metal in metals:
            if metal not in df.columns:
                continue

            data = df[metal].copy()

            if normalize:
                # Normalize to percentage change from start
                data = ((data / data.iloc[0]) - 1) * 100

            metal_name = Config.METALS[metal]['name']
            color = Config.METALS[metal]['color']

            ax.plot(
                df.index,
                data,
                color=color,
                linewidth=2,
                label=metal_name
            )

        # Formatting
        title = 'Precious Metals Comparison'
        if normalize:
            title += ' (% Change)'
            ax.set_ylabel('Change from Start (%)', fontsize=12)
        else:
            ax.set_ylabel('Price (USD per troy oz)', fontsize=12)

        ax.set_title(title, fontsize=16, fontweight='bold')
        ax.set_xlabel('Date', fontsize=12)
        ax.legend(loc='best')
        ax.grid(True, alpha=0.3)

        # Format x-axis
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
        plt.xticks(rotation=45)

        plt.tight_layout()

        if save:
            filename = f"metals_comparison_{pd.Timestamp.now().strftime('%Y%m%d')}.png"
            filepath = os.path.join(self.output_dir, filename)
            plt.savefig(filepath, dpi=300, bbox_inches='tight')
            print(f"Chart saved: {filepath}")

        plt.show()

    def plot_correlation_heatmap(
        self,
        df: pd.DataFrame,
        save: bool = True
    ):
        """Plot correlation heatmap for multiple metals."""
        import numpy as np

        fig, ax = plt.subplots(figsize=(10, 8))

        # Calculate correlation
        corr = df.corr()

        # Create heatmap
        im = ax.imshow(corr, cmap='RdYlGn', aspect='auto', vmin=-1, vmax=1)

        # Set ticks and labels
        metals = [Config.METALS[m]['name'] for m in corr.columns]
        ax.set_xticks(range(len(metals)))
        ax.set_yticks(range(len(metals)))
        ax.set_xticklabels(metals)
        ax.set_yticklabels(metals)

        # Rotate labels
        plt.setp(ax.get_xticklabels(), rotation=45, ha='right')

        # Add correlation values
        for i in range(len(corr)):
            for j in range(len(corr)):
                text = ax.text(j, i, f'{corr.iloc[i, j]:.2f}',
                             ha='center', va='center', color='black')

        ax.set_title('Precious Metals Price Correlation',
                    fontsize=16, fontweight='bold', pad=20)

        # Add colorbar
        cbar = plt.colorbar(im, ax=ax)
        cbar.set_label('Correlation Coefficient', rotation=270, labelpad=20)

        plt.tight_layout()

        if save:
            filename = f"correlation_heatmap_{pd.Timestamp.now().strftime('%Y%m%d')}.png"
            filepath = os.path.join(self.output_dir, filename)
            plt.savefig(filepath, dpi=300, bbox_inches='tight')
            print(f"Chart saved: {filepath}")

        plt.show()

6. Historical Data Analysis

Advanced Analytics

import pandas as pd
import numpy as np
from scipy import stats

class MetalsAnalyzer:
    """Advanced analysis for precious metals data."""

    @staticmethod
    def calculate_volatility(df: pd.DataFrame, window: int = 30) -> pd.Series:
        """
        Calculate rolling volatility (standard deviation of returns).

        Args:
            df: DataFrame with price column
            window: Rolling window size

        Returns:
            Series with volatility values
        """
        returns = df['price'].pct_change()
        volatility = returns.rolling(window=window).std() * np.sqrt(252)
        return volatility

    @staticmethod
    def find_support_resistance(
        df: pd.DataFrame,
        window: int = 20
    ) -> dict:
        """
        Find support and resistance levels.

        Returns:
            Dict with support and resistance prices
        """
        rolling_min = df['price'].rolling(window=window).min()
        rolling_max = df['price'].rolling(window=window).max()

        return {
            'support': float(rolling_min.iloc[-1]),
            'resistance': float(rolling_max.iloc[-1]),
            'current': float(df['price'].iloc[-1])
        }

    @staticmethod
    def calculate_rsi(df: pd.DataFrame, period: int = 14) -> pd.Series:
        """
        Calculate Relative Strength Index (RSI).

        Args:
            df: DataFrame with price column
            period: RSI period

        Returns:
            Series with RSI values (0-100)
        """
        delta = df['price'].diff()

        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()

        rs = gain / loss
        rsi = 100 - (100 / (1 + rs))

        return rsi

# Usage example
from src.api_client import PreciousMetalsAPI
from src.data_handler import MetalsDataHandler
from config import Config

api = PreciousMetalsAPI(Config.API_KEY)
handler = MetalsDataHandler(api)
analyzer = MetalsAnalyzer()

# Get gold data
gold_df = handler.create_historical_df('XAU', days=365)

# Calculate metrics
volatility = analyzer.calculate_volatility(gold_df)
support_resistance = analyzer.find_support_resistance(gold_df)
rsi = analyzer.calculate_rsi(gold_df)

print("Gold Analysis:")
print(f"Current Volatility: {volatility.iloc[-1]:.2%}")
print(f"Support Level: ${support_resistance['support']:.2f}")
print(f"Resistance Level: ${support_resistance['resistance']:.2f}")
print(f"Current RSI: {rsi.iloc[-1]:.2f}")

7. CSV Export and Data Persistence

Export Manager

import pandas as pd
import os
from datetime import datetime
from config import Config

class DataExporter:
    """Export precious metals data to various formats."""

    def __init__(self, output_dir: str = None):
        self.output_dir = output_dir or Config.EXPORT_DIR

    def export_to_csv(
        self,
        df: pd.DataFrame,
        filename: str,
        include_timestamp: bool = True
    ) -> str:
        """
        Export DataFrame to CSV file.

        Returns:
            Path to exported file
        """
        if include_timestamp:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filename = f"{filename}_{timestamp}.csv"

        filepath = os.path.join(self.output_dir, filename)
        df.to_csv(filepath, index=True)

        print(f"Data exported to: {filepath}")
        return filepath

    def export_summary_report(
        self,
        data_handler,
        metals: list = None
    ) -> str:
        """
        Generate and export comprehensive summary report.

        Returns:
            Path to report file
        """
        if metals is None:
            metals = list(Config.METALS.keys())

        report_data = []

        for metal in metals:
            # Get 1-year data
            df = data_handler.create_historical_df(metal, days=365)
            stats = data_handler.calculate_statistics(df)

            report_data.append({
                'Metal': Config.METALS[metal]['name'],
                'Code': metal,
                'Current Price': f"${stats['current']:.2f}",
                'Avg Price (1Y)': f"${stats['mean']:.2f}",
                '1Y High': f"${stats['max']:.2f}",
                '1Y Low': f"${stats['min']:.2f}",
                'Change (1Y)': f"{stats['change_pct']:.2f}%",
                'Volatility': f"${stats['std']:.2f}"
            })

        report_df = pd.DataFrame(report_data)

        filename = f"metals_summary_report_{datetime.now().strftime('%Y%m%d')}.csv"
        filepath = self.export_to_csv(report_df, filename, include_timestamp=False)

        return filepath

8. Advanced Features and Optimization

Complete Application

# main.py
import argparse
from src.api_client import PreciousMetalsAPI
from src.data_handler import MetalsDataHandler
from src.alerts import AlertManager, PriceAlert
from src.visualizer import MetalsVisualizer
from src.utils import DataExporter
from config import Config
from decimal import Decimal

def main():
    """Main application entry point."""
    parser = argparse.ArgumentParser(
        description='Precious Metals Price Tracker'
    )
    parser.add_argument(
        '--mode',
        choices=['current', 'history', 'alerts', 'compare', 'export'],
        required=True,
        help='Operation mode'
    )
    parser.add_argument('--metal', help='Metal code (XAU, XAG, XPT, XPD)')
    parser.add_argument('--days', type=int, default=90, help='Days of history')

    args = parser.parse_args()

    # Initialize components
    Config.ensure_directories()
    api = PreciousMetalsAPI(Config.API_KEY)
    handler = MetalsDataHandler(api)
    visualizer = MetalsVisualizer()
    exporter = DataExporter()

    if args.mode == 'current':
        # Display current prices
        df = handler.create_current_prices_df()
        print("\n" + "="*50)
        print("CURRENT PRECIOUS METALS PRICES")
        print("="*50)
        print(df.to_string(index=False))
        print("="*50 + "\n")

    elif args.mode == 'history':
        # Show price history
        metal = args.metal or 'XAU'
        df = handler.create_historical_df(metal, args.days)
        df = handler.calculate_moving_averages(df)

        stats = handler.calculate_statistics(df)

        print(f"\n{Config.METALS[metal]['name']} Statistics ({args.days} days):")
        for key, value in stats.items():
            if key == 'change_pct':
                print(f"  {key}: {value:.2f}%")
            else:
                print(f"  {key}: ${value:.2f}")

        # Generate chart
        visualizer.plot_price_history(df, metal, show_ma=True)

    elif args.mode == 'compare':
        # Compare all metals
        metals = list(Config.METALS.keys())
        df = handler.create_multi_metal_df(metals, args.days)

        visualizer.plot_multi_metal_comparison(df, metals, normalize=True)
        visualizer.plot_correlation_heatmap(df)

    elif args.mode == 'export':
        # Export summary report
        filepath = exporter.export_summary_report(handler)
        print(f"Summary report exported to: {filepath}")

    elif args.mode == 'alerts':
        # Check alerts
        alert_mgr = AlertManager(api)
        triggered = alert_mgr.check_all_alerts()

        if triggered:
            print(f"\n{len(triggered)} alert(s) triggered:")
            for alert in triggered:
                print(f"  {alert['metal']}: ${alert['triggered_price']}")
        else:
            print("No alerts triggered")

if __name__ == '__main__':
    main()

Running the Application:
python main.py --mode current
python main.py --mode history --metal XAU --days 365
python main.py --mode compare --days 180
python main.py --mode export

Conclusion

You now have a complete, production-ready precious metals price tracking system built with Python. This application demonstrates professional software engineering practices: clean architecture, proper error handling, caching for performance, comprehensive data analysis with pandas, and beautiful visualizations with matplotlib.

You can extend this foundation to add features like automated trading signals, portfolio management, SMS alerts, web dashboards with Flask or Django, or integration with other financial data sources. The modular design makes it easy to add new capabilities without refactoring existing code.

Related Articles

Power Your App with Precious Metals Data

UniRate API provides reliable precious metals pricing for gold, silver, platinum, and palladium with historical data back to 1968. Perfect for the price tracker you just built. Simple REST API, generous free tier, and affordable pricing.

Get Started Free