Overall Statistics
Total Orders
202
Average Win
0.57%
Average Loss
-0.24%
Compounding Annual Return
22.821%
Drawdown
6.100%
Expectancy
1.272
Start Equity
10000000
End Equity
15030247.13
Net Profit
50.302%
Sharpe Ratio
1.137
Sortino Ratio
1.25
Probabilistic Sharpe Ratio
86.286%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
2.42
Alpha
0.091
Beta
0.108
Annual Standard Deviation
0.09
Annual Variance
0.008
Information Ratio
-0.014
Tracking Error
0.151
Treynor Ratio
0.95
Total Fees
$16379.65
Estimated Strategy Capacity
$0
Lowest Capacity Asset
GLD T3SKPOF94JFP
Portfolio Turnover
3.29%
Drawdown Recovery
115
from AlgorithmImports import *
import numpy as np
from datetime import timedelta


class GoldVolatilityArbitrageStrategy(QCAlgorithm):
    """
    Institutional gold-focused strategy:
    1. Gold mean reversion on extreme moves
    2. Gold miners arbitrage (GDX/GDXJ vs GLD spread)
    3. Real rates regime trading (Gold vs TLT)
    4. Cross-asset correlations (Gold/USD, Gold/Equity risk)
    """

    def Initialize(self):
        self.SetStartDate(2024, 1, 1)
        self.SetCash(10_000_000)

        # Core gold instruments - DAILY resolution
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
        self.gdx = self.AddEquity("GDX", Resolution.Daily).Symbol
        self.gdxj = self.AddEquity("GDXJ", Resolution.Daily).Symbol
        self.slv = self.AddEquity("SLV", Resolution.Daily).Symbol

        # Macro instruments
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        self.uup = self.AddEquity("UUP", Resolution.Daily).Symbol
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.tip = self.AddEquity("TIP", Resolution.Daily).Symbol

        # Volatility proxy
        self.vxx = self.AddEquity("VXX", Resolution.Daily).Symbol

        # Data storage
        self.data = {}
        for symbol in [self.gld, self.gdx, self.gdxj, self.slv, self.tlt, self.uup, self.spy, self.tip, self.vxx]:
            self.data[symbol] = SymbolData(symbol, self, lookback=120)

        # Strategy state
        self.gold_zscore = 0.0
        self.miner_spread_zscore = 0.0
        self.real_rate_regime = "NEUTRAL"
        self.usd_regime = "NEUTRAL"
        self.crash_protection_active = False

        # Risk parameters
        self.max_gross_leverage = 2.5
        self.volatility_target = 0.15
        self.max_drawdown = 0.10
        self.peak_equity = self.Portfolio.TotalPortfolioValue

        # Reversion parameters
        self.gold_reversion_threshold = 2.0
        self.gold_exit_threshold = 0.5
        self.miner_arb_threshold = 1.5

        # Warmup
        self.SetWarmUp(60, Resolution.Daily)

        # Scheduling (QC uses PascalCase DateRules/TimeRules/Schedule)
        self.Schedule.On(
            self.DateRules.EveryDay(self.gld),
            self.TimeRules.AfterMarketOpen(self.gld, 30),
            self.AnalyzeMarketRegime
        )

        self.Schedule.On(
            self.DateRules.EveryDay(self.gld),
            self.TimeRules.AfterMarketOpen(self.gld, 60),
            self.ExecuteGoldMeanReversion
        )

        self.Schedule.On(
            self.DateRules.EveryDay(self.gld),
            self.TimeRules.AfterMarketOpen(self.gld, 120),
            self.ExecuteMinerArbitrage
        )

        self.Schedule.On(
            self.DateRules.EveryDay(self.gld),
            self.TimeRules.BeforeMarketClose(self.gld, 30),
            self.RiskManagement
        )

        self.Debug("Gold strategy initialized (WarmUp: 60 days).")

    def AnalyzeMarketRegime(self):
        if self.IsWarmingUp:
            return

        needed = [self.gld, self.tlt, self.uup, self.spy, self.tip]
        if not all(self.data[s].IsReady() for s in needed):
            return

        gld_prices = self.data[self.gld].GetPrices()
        if gld_prices is None or len(gld_prices) < 30:
            return

        # Z-score (shorter lookback for more signals)
        lookback = min(40, len(gld_prices))
        window = gld_prices[-lookback:]
        mu = float(np.mean(window))
        sd = float(np.std(window))
        self.gold_zscore = (float(gld_prices[-1]) - mu) / sd if sd > 1e-12 else 0.0

        # Real rates proxy: TLT/TIP ratio trend (align lengths)
        tip_prices = self.data[self.tip].GetPrices()
        tlt_prices = self.data[self.tlt].GetPrices()
        if tip_prices is not None and tlt_prices is not None:
            n = min(len(tip_prices), len(tlt_prices))
            if n >= 20:
                tip = tip_prices[-20:]
                tlt = tlt_prices[-20:]
                real_rate_proxy = tlt / (tip + 1e-10)
                real_rate_trend = float(real_rate_proxy[-1] / real_rate_proxy[0] - 1.0)

                if real_rate_trend > 0.02:
                    self.real_rate_regime = "RISING_RATES"
                elif real_rate_trend < -0.02:
                    self.real_rate_regime = "FALLING_RATES"
                else:
                    self.real_rate_regime = "NEUTRAL"

        # USD regime via SMA(20) vs SMA(50)
        uup_prices = self.data[self.uup].GetPrices()
        if uup_prices is not None and len(uup_prices) >= 50:
            sma20 = float(np.mean(uup_prices[-20:]))
            sma50 = float(np.mean(uup_prices[-50:]))

            if sma20 > sma50 * 1.02:
                self.usd_regime = "USD_BULL"
            elif sma20 < sma50 * 0.98:
                self.usd_regime = "USD_BEAR"
            else:
                self.usd_regime = "NEUTRAL"

        # Crash detection from SPY 5-day return
        spy_rets = self.data[self.spy].GetReturns()
        if spy_rets is not None and len(spy_rets) >= 5:
            recent = float(np.sum(spy_rets[-5:]))
            if recent < -0.05:
                self.crash_protection_active = True
            elif recent > 0.03:
                self.crash_protection_active = False

    def ExecuteGoldMeanReversion(self):
        if self.IsWarmingUp:
            return
        if not self.data[self.gld].IsReady():
            return

        gld_prices = self.data[self.gld].GetPrices()
        if gld_prices is None or len(gld_prices) < 20:
            return

        gld_rets = self.data[self.gld].GetReturns()
        if gld_rets is None or len(gld_rets) < 20:
            return

        # Short-term momentum
        short_term_return = float(gld_prices[-1] / gld_prices[-5] - 1.0) if len(gld_prices) >= 5 else 0.0

        # Volatility scaling
        ann_vol = float(np.std(gld_rets[-20:]) * np.sqrt(252))
        ann_vol = max(ann_vol, 1e-6)
        vol_scalar = float(np.clip(self.volatility_target / ann_vol, 0.5, 2.0))

        tpv = self.Portfolio.TotalPortfolioValue
        current_weight = float(self.Portfolio[self.gld].HoldingsValue / tpv) if tpv > 0 else 0.0

        # Extreme plummet: long
        if self.gold_zscore < -self.gold_reversion_threshold:
            target = 0.40 * vol_scalar
            if self.real_rate_regime == "FALLING_RATES":
                target *= 1.3
            if self.usd_regime == "USD_BEAR":
                target *= 1.2
            if self.crash_protection_active:
                target *= 1.5
            target = float(min(target, 0.80))

            if abs(current_weight - target) > 0.05:
                self.SetHoldings(self.gld, target)

        # Extreme spike: short
        elif self.gold_zscore > self.gold_reversion_threshold:
            target = -0.30 * vol_scalar
            if self.real_rate_regime == "RISING_RATES":
                target *= 1.3
            if self.usd_regime == "USD_BULL":
                target *= 1.2
            target = float(max(target, -0.50))

            if abs(current_weight - target) > 0.05:
                self.SetHoldings(self.gld, target)

        # Exit on reversion: reduce exposure (target closer to 0)
        elif abs(self.gold_zscore) < self.gold_exit_threshold and abs(current_weight) > 0.10:
            target = float(current_weight * 0.70)  # reduce 30%
            self.SetHoldings(self.gld, target)

        # Baseline small exposure when quiet
        elif abs(current_weight) < 0.05 and abs(self.gold_zscore) < 1.0:
            self.SetHoldings(self.gld, 0.15 if short_term_return > 0 else -0.10)

    def ExecuteMinerArbitrage(self):
        if self.IsWarmingUp:
            return
        if not all(self.data[s].IsReady() for s in [self.gld, self.gdx, self.gdxj]):
            return

        gld = self.data[self.gld].GetPrices()
        gdx = self.data[self.gdx].GetPrices()
        gdxj = self.data[self.gdxj].GetPrices()
        if gld is None or gdx is None or gdxj is None:
            return

        # Align lengths to avoid numpy shape mismatch
        n = min(len(gld), len(gdx), len(gdxj))
        if n < 40:
            return
        gld = gld[-n:]
        gdx = gdx[-n:]
        gdxj = gdxj[-n:]

        ratio = gdx / (gld + 1e-10)

        lookback = min(40, len(ratio))
        window = ratio[-lookback:]
        mu = float(np.mean(window))
        sd = float(np.std(window))
        self.miner_spread_zscore = (float(ratio[-1]) - mu) / (sd + 1e-10)

        tpv = self.Portfolio.TotalPortfolioValue
        current_gdx_w = float(self.Portfolio[self.gdx].HoldingsValue / tpv) if tpv > 0 else 0.0

        # Miners cheap: long miners (optionally reduce gold elsewhere)
        if self.miner_spread_zscore < -self.miner_arb_threshold:
            self.SetHoldings(self.gdx, 0.25)
            self.SetHoldings(self.gdxj, 0.15)

        # Miners expensive: short miners
        elif self.miner_spread_zscore > self.miner_arb_threshold:
            self.SetHoldings(self.gdx, -0.15)
            if self.Portfolio[self.gdxj].Invested:
                self.Liquidate(self.gdxj)

        # Close arb if converged (and not in strong main gold signal)
        elif abs(self.miner_spread_zscore) < 0.5:
            if abs(current_gdx_w) > 0.05 and abs(self.gold_zscore) <= 2.0:
                self.Liquidate(self.gdx)
                self.Liquidate(self.gdxj)

    def RiskManagement(self):
        if self.IsWarmingUp:
            return

        tpv = self.Portfolio.TotalPortfolioValue
        if tpv <= 0:
            return

        # Drawdown tracking
        if tpv > self.peak_equity:
            self.peak_equity = tpv
        dd = float((self.peak_equity - tpv) / self.peak_equity) if self.peak_equity > 0 else 0.0

        # Drawdown protection: scale down these exposures
        if dd > self.max_drawdown:
            scale = 0.5
            for symbol in [self.gld, self.gdx, self.gdxj, self.slv]:
                if self.Portfolio[symbol].Invested:
                    w = float(self.Portfolio[symbol].HoldingsValue / tpv)
                    self.SetHoldings(symbol, w * scale)

        # Gross leverage estimate (manual): sum(abs(weights))
        gross = 0.0
        for sym in [self.gld, self.gdx, self.gdxj, self.slv, self.tlt, self.uup, self.spy, self.tip, self.vxx]:
            if self.Portfolio[sym].Invested:
                gross += abs(float(self.Portfolio[sym].HoldingsValue / tpv))

        if gross > self.max_gross_leverage:
            scale = float(self.max_gross_leverage / gross)
            for sym in [self.gld, self.gdx, self.gdxj, self.slv, self.tlt, self.uup, self.spy, self.tip, self.vxx]:
                if self.Portfolio[sym].Invested:
                    w = float(self.Portfolio[sym].HoldingsValue / tpv)
                    self.SetHoldings(sym, w * scale)

        # Correlation risk (GLD vs GDX)
        if self.Portfolio[self.gld].Invested and self.Portfolio[self.gdx].Invested:
            gld_r = self.data[self.gld].GetReturns()
            gdx_r = self.data[self.gdx].GetReturns()
            if gld_r is not None and gdx_r is not None:
                n = min(len(gld_r), len(gdx_r))
                if n >= 20:
                    corr = float(np.corrcoef(gld_r[-n:], gdx_r[-n:])[0, 1])
                    if abs(corr) > 0.90:
                        gld_val = abs(float(self.Portfolio[self.gld].HoldingsValue))
                        gdx_val = abs(float(self.Portfolio[self.gdx].HoldingsValue))
                        if gdx_val < gld_val:
                            self.Liquidate(self.gdx)


class SymbolData:
    def __init__(self, symbol: Symbol, algorithm: QCAlgorithm, lookback: int = 120):
        self.Symbol = symbol
        self.algorithm = algorithm
        self.lookback = lookback

        self.prices = RollingWindow[float](lookback)
        self.volumes = RollingWindow[float](lookback)

        self.consolidator = TradeBarConsolidator(timedelta(days=1))
        self.consolidator.DataConsolidated += self.OnDataConsolidated
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidator)

    def OnDataConsolidated(self, sender, bar: TradeBar):
        self.prices.Add(float(bar.Close))
        self.volumes.Add(float(bar.Volume))

    def IsReady(self) -> bool:
        return self.prices.IsReady

    def GetPrices(self):
        if not self.IsReady():
            return None
        n = min(self.lookback, self.prices.Count)
        return np.array([self.prices[i] for i in range(n)], dtype=float)

    def GetReturns(self):
        prices = self.GetPrices()
        if prices is None or len(prices) < 2:
            return None
        return np.diff(np.log(prices))

    def Dispose(self):
        self.algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.consolidator)