Overall Statistics
Total Orders
57
Average Win
3.07%
Average Loss
-4.20%
Compounding Annual Return
-2.830%
Drawdown
38.600%
Expectancy
-0.046
Start Equity
100000
End Equity
91456.44
Net Profit
-8.544%
Sharpe Ratio
-0.397
Sortino Ratio
-0.356
Probabilistic Sharpe Ratio
1.413%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
0.73
Alpha
-0.068
Beta
0.058
Annual Standard Deviation
0.157
Annual Variance
0.025
Information Ratio
-0.859
Tracking Error
0.197
Treynor Ratio
-1.073
Total Fees
$315.03
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SLV TI6HUUU1DDUT
Portfolio Turnover
2.55%
Drawdown Recovery
191
# region imports
from AlgorithmImports import *
# endregion

class PreciousMetalsPairsTradingAlgorithm(QCAlgorithm):
    """Pairs trading strategy using gold (GLD) and silver (SLV) ratio mean reversion."""

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_cash(100000)
        
        # Add precious metals ETFs
        self._gold = self.add_equity("GLD", Resolution.DAILY).symbol
        self._silver = self.add_equity("SLV", Resolution.DAILY).symbol
        
        # Strategy parameters
        self._lookback_period = 60  # Days for calculating mean and std dev
        self._entry_threshold = 2.0  # Z-score threshold for entry
        self._exit_threshold = 0.5   # Z-score threshold for exit
        
        # Rolling window to store gold/silver ratio
        self._ratio_window = RollingWindow[float](self._lookback_period)
        
        # Track position state
        self._position_side = 0  # 0 = flat, 1 = long gold/short silver, -1 = short gold/long silver
        
        # Warm up algorithm
        self.set_warm_up(self._lookback_period, Resolution.DAILY)
        
        # Schedule rebalancing daily after market open
        self.schedule.on(self.date_rules.every_day(self._gold),
                        self.time_rules.after_market_open(self._gold, 30),
                        self._rebalance)

    def _rebalance(self):
        """Check ratio and execute pairs trading logic."""
        # Get current prices
        gold_price = self.securities[self._gold].price
        silver_price = self.securities[self._silver].price
        
        if gold_price == 0 or silver_price == 0:
            return
        
        # Calculate current gold/silver ratio
        current_ratio = gold_price / silver_price
        
        # Add to rolling window
        self._ratio_window.add(current_ratio)
        
        # Need full window before trading
        if not self._ratio_window.is_ready:
            return
        
        # Calculate z-score
        ratio_values = [self._ratio_window[i] for i in range(self._ratio_window.count)]
        mean_ratio = np.mean(ratio_values)
        std_ratio = np.std(ratio_values)
        
        if std_ratio == 0:
            return
        
        z_score = (current_ratio - mean_ratio) / std_ratio
        
        self.plot("Ratio", "Gold/Silver Ratio", current_ratio)
        self.plot("Ratio", "Mean Ratio", mean_ratio)
        self.plot("Statistics", "Z-Score", z_score)
        
        # Entry signals
        if self._position_side == 0:
            if z_score > self._entry_threshold:
                # Ratio is high: Gold overvalued, Silver undervalued
                # Short Gold, Long Silver
                self._enter_trade(-1)
            elif z_score < -self._entry_threshold:
                # Ratio is low: Gold undervalued, Silver overvalued
                # Long Gold, Short Silver
                self._enter_trade(1)
        
        # Exit signals
        elif self._position_side != 0:
            if abs(z_score) < self._exit_threshold:
                # Ratio reverted to mean - exit positions
                self._exit_trade()
            elif (self._position_side == 1 and z_score > self._entry_threshold) or \
                 (self._position_side == -1 and z_score < -self._entry_threshold):
                # Stop loss: ratio moved further against us
                self._exit_trade()
    
    def _enter_trade(self, side: int):
        """Enter pairs trade. Side: 1 = long gold/short silver, -1 = short gold/long silver."""
        self._position_side = side
        
        if side == 1:
            # Long gold, short silver
            self.set_holdings(self._gold, 0.5)
            self.set_holdings(self._silver, -0.5)
            self.debug(f"Entered Long Gold / Short Silver at ratio {self.securities[self._gold].price / self.securities[self._silver].price:.2f}")
        else:
            # Short gold, long silver
            self.set_holdings(self._gold, -0.5)
            self.set_holdings(self._silver, 0.5)
            self.debug(f"Entered Short Gold / Long Silver at ratio {self.securities[self._gold].price / self.securities[self._silver].price:.2f}")
    
    def _exit_trade(self):
        """Exit pairs trade and flatten positions."""
        self.liquidate()
        self.debug(f"Exited pairs trade at ratio {self.securities[self._gold].price / self.securities[self._silver].price:.2f}")
        self._position_side = 0