Overall Statistics
Total Orders
495
Average Win
0.29%
Average Loss
-0.40%
Compounding Annual Return
-3.415%
Drawdown
24.400%
Expectancy
-0.193
Start Equity
500000
End Equity
411600.89
Net Profit
-17.680%
Sharpe Ratio
-1.191
Sortino Ratio
-0.861
Probabilistic Sharpe Ratio
0.002%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
0.72
Alpha
-0.054
Beta
0.015
Annual Standard Deviation
0.044
Annual Variance
0.002
Information Ratio
-0.767
Tracking Error
0.179
Treynor Ratio
-3.508
Total Fees
$1202.89
Estimated Strategy Capacity
$370000000.00
Lowest Capacity Asset
HG YW4KTTPXXAJH
Portfolio Turnover
5.28%
Drawdown Recovery
337
"""
HG Copper Futures Mean Reversion Strategy - CORRECT VERSION
Matching algo_beta's DAILY data with TIME COLUMNS structure

Data Structure:
- algo_beta: Daily rows with '16:00' column (price at 4:00 PM each day)
- lag=3: Compare today's 16:00 vs 3 TRADING DAYS ago 16:00
- Expected: 384 signals from 2020-2025

Pattern:
- At 16:00 each day: Check if price declined -5000 to -100 bps vs 3 days ago
- Entry: 16:00 close
- Exit: Next day 16:00 (1 trading day forward)
"""

from AlgorithmImports import *
import numpy as np
from datetime import timedelta


class HGDailyTimeColumnAlgorithm(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2025, 11, 1)
        self.SetCash(500000)  # Increased from 100k to avoid margin calls
        
        # CRITICAL: Set timezone to Central Time to match algo_beta data
        self.SetTimeZone("America/Chicago")  # Central Time (CT)
        
        # Add HG futures with continuous contract
        # CRITICAL: extendedMarketHours=True to get evening Globex session (17:00-23:59 CT)
        self.hg = self.AddFuture(
            Futures.Metals.Copper,
            resolution=Resolution.Minute,  # Need minute to capture 16:00
            extendedMarketHours=True,  # Enable 23/5 trading hours (includes evening session)
            dataNormalizationMode=DataNormalizationMode.BackwardsPanamaCanal
        )
        
        # Filter for front month contract (0-90 days to expiry)
        self.hg.SetFilter(0, 90)
        
        # Pattern parameters matching algo_beta
        self.entry_hour = 16
        self.entry_minute = 0
        self.exit_hour = 21  # Exit at 21:00 next day (matching algo_beta)
        self.exit_minute = 0
        # Note: With extendedMarketHours=True, we now have access to:
        # - Evening Globex session: 17:00-23:59 CT
        # - Can exit at 21:00 CT to match algo_beta's optimal horizon
        # - Gives ~29 hours for mean reversion (vs 17 hours at 09:00)
        self.lookback_days = 3  # 3 TRADING DAYS (not bars)
        self.min_threshold_bps = -5000
        self.max_threshold_bps = -100
        self.holding_days = 1  # Exit next trading day
        
        # POSITION SIZING - CHANGE THIS
        self.contracts_per_trade = 1  # Number of contracts to trade
        # For HG (Copper): 1 contract = 25,000 lbs
        # Margin required: ~$3,500-$7,500 per contract
        # Examples:
        #   1 contract  = ~$3,500 margin
        #   2 contracts = ~$7,000 margin
        #   5 contracts = ~$17,500 margin
        #   10 contracts = ~$35,000 margin
        
        # Store daily 16:00 prices (one per trading day)
        # This mimics algo_beta's '16:00' column
        self.daily_16_prices = {}  # {date: price}
        
        # Track positions
        self.current_contract = None
        self.entry_time = None
        self.entry_price = None
        self.exit_attempted = False  # Track if we've tried to exit today
        
        # Statistics
        self.trades = []
        self.pattern_checks = 0
        self.pattern_matches = 0
        
        # Diagnostics: Track what hours we see in the data
        self.hours_seen = set()
        self.first_day_hours_logged = False
        
        self.Debug("=" * 60)
        self.Debug("HG Mean Reversion - Daily Data (×1000 scaled)")
        self.Debug(f"Timezone: {self.TimeZone} (matching algo_beta CT)")
        self.Debug(f"Entry: {self.entry_hour:02d}:{self.entry_minute:02d} CT | Exit: {self.exit_hour:02d}:{self.exit_minute:02d} CT next day")
        self.Debug(f"Lookback: {self.lookback_days} days | Threshold: [{self.min_threshold_bps}, {self.max_threshold_bps}] bps")
        self.Debug(f"Expected: ~244 signals (QC data) | Position: {self.contracts_per_trade} contract(s)")
        self.Debug("=" * 60)
    
    
    def OnData(self, data):
        """Capture 16:00 price each trading day"""
        
        # DIAGNOSTIC: Log hours seen in first trading day (only once)
        if not self.first_day_hours_logged:
            self.hours_seen.add(self.Time.hour)
            # Log after seeing enough data (after 16:00 on day 2)
            if self.Time.date().day >= 3 and self.Time.hour >= 16:
                self.Debug("\n" + "=" * 60)
                self.Debug("DIAGNOSTIC: Hours available in QC data:")
                sorted_hours = sorted(list(self.hours_seen))
                self.Debug(f"Hours seen: {sorted_hours}")
                if 21 in self.hours_seen:
                    self.Debug("✅ 21:00 (9 PM) IS available in data")
                else:
                    self.Debug("❌ 21:00 (9 PM) NOT available in data")
                if 9 in self.hours_seen:
                    self.Debug("✅ 09:00 IS available")
                else:
                    self.Debug("❌ 09:00 NOT available (trading starts 09:30)")
                self.Debug("=" * 60 + "\n")
                self.first_day_hours_logged = True
        
        # Update current contract - select contract with most volume/liquidity
        if len(data.FutureChains) > 0:
            chain = list(data.FutureChains.Values)[0]
            if len(chain) > 0:
                # Select contract with highest open interest (most liquid)
                # This automatically rolls as contracts approach expiry
                contracts = sorted(chain, key=lambda x: x.OpenInterest, reverse=True)
                if len(contracts) > 0:
                    new_contract = contracts[0].Symbol
                    if self.current_contract != new_contract:
                        # Log contract switches
                        if self.current_contract is None or self.pattern_checks < 5:
                            self.Debug(f"[{self.Time.date()}] Contract: {new_contract} (Expiry: {contracts[0].Expiry.date()})")
                        self.current_contract = new_contract
        
        # Capture 16:00 price (mimics algo_beta's '16:00' column)
        if (self.Time.hour == self.entry_hour and 
            self.Time.minute == self.entry_minute and
            self.current_contract is not None):
            
            if data.Bars.ContainsKey(self.current_contract):
                raw_price = data.Bars[self.current_contract].Close
                
                # CRITICAL: Scale price by 1000 to match algo_beta data format
                # algo_beta stores HG with multiplier=1000 (e.g., 2.8250 → 2825)
                scaled_price = raw_price * 1000
                
                date = self.Time.date()
                
                # Store scaled price (matches algo_beta's '16:00' column)
                self.daily_16_prices[date] = scaled_price
                
                if len(self.daily_16_prices) <= 3:
                    self.Debug(f"[{date}] 16:00: ${raw_price:.4f} → {scaled_price:.0f} (scaled)")
                
                # Check pattern immediately after capturing today's price
                self.CheckPatternAndTrade()
        
        # Check exit conditions
        self.CheckExitConditions()
    
    
    def CheckPatternAndTrade(self):
        """
        Check pattern at 16:00 daily (after price captured)
        This mimics: df['HG', '16:00'] vs df['HG', '16:00'].shift(3)
        """
        self.pattern_checks += 1
        
        # Already in position
        if self.Portfolio.Invested:
            return
        
        # Need at least 4 days of 16:00 prices (today + 3 days back)
        if len(self.daily_16_prices) < self.lookback_days + 1:
            if self.pattern_checks <= 10:
                self.Debug(f"Check #{self.pattern_checks}: Need {self.lookback_days + 1} days, have {len(self.daily_16_prices)}")
            return
        
        # Get sorted dates
        sorted_dates = sorted(self.daily_16_prices.keys())
        
        # Get today's date
        today = self.Time.date()
        
        # Find today in the list
        if today not in self.daily_16_prices:
            return
        
        today_idx = sorted_dates.index(today)
        
        # Need at least 3 days before today
        if today_idx < self.lookback_days:
            return
        
        # Get prices: today vs 3 trading days ago
        current_price = self.daily_16_prices[today]
        lookback_date = sorted_dates[today_idx - self.lookback_days]
        lookback_price = self.daily_16_prices[lookback_date]
        
        # Calculate log return (matches algo_beta: log(current/old) * 10000)
        log_return_bps = np.log(current_price / lookback_price) * 10000
        
        # Log only first 10 checks and every 500th check to avoid rate limiting
        if self.pattern_checks <= 10 or self.pattern_checks % 500 == 0:
            in_range = "✓ MATCH" if self.min_threshold_bps <= log_return_bps <= self.max_threshold_bps else "✗ no match"
            self.Debug(f"[{today}] Check #{self.pattern_checks}: "
                      f"{lookback_date} {lookback_price:.0f} -> "
                      f"{today} {current_price:.0f} | "
                      f"Return: {log_return_bps:.2f} bps | {in_range}")
        
        # Check if decline within threshold (pattern match)
        if self.min_threshold_bps <= log_return_bps <= self.max_threshold_bps:
            self.pattern_matches += 1
            # Only log first 10 and every 50th match
            if self.pattern_matches <= 10 or self.pattern_matches % 50 == 0:
                self.Debug(f"✓ MATCH #{self.pattern_matches}: {today} | {log_return_bps:.2f} bps")
            self.EnterPosition(log_return_bps, lookback_date, lookback_price, current_price)
    
    
    def EnterPosition(self, log_return_bps, lookback_date, lookback_price, current_price):
        """Enter position at 16:00"""
        if self.current_contract is None:
            return
        
        # Use configurable position size
        quantity = self.contracts_per_trade
        ticket = self.MarketOrder(self.current_contract, quantity)
        
        self.entry_time = self.Time
        self.entry_price = current_price  # Stored as scaled price
        
        # Only log first 10 entries and every 50th
        if len(self.trades) + 1 <= 10 or (len(self.trades) + 1) % 50 == 0:
            self.Debug(f"ENTRY #{len(self.trades) + 1}: {self.Time.date()} | "
                      f"{lookback_price:.0f} → {current_price:.0f} | "
                      f"Trigger: {log_return_bps:.2f} bps")
    
    
    def CheckExitConditions(self):
        """Exit next trading day at 16:00 - with failsafe for stuck positions"""
        if not self.Portfolio.Invested or self.entry_time is None:
            return
        
        # Calculate days held
        days_held = (self.Time.date() - self.entry_time.date()).days
        
        # EMERGENCY FAILSAFE: Exit if held more than 5 days (something went wrong)
        if days_held > 5:
            self.Debug(f"⚠️ EMERGENCY EXIT: Held {days_held} days! Liquidating all.")
            self.Liquidate()
            self.entry_time = None
            self.entry_price = None
            return
        
        # Normal exit: First bar on or after 09:30 after holding period
        # Check if we're past exit time and haven't exited yet today
        is_exit_time = (days_held >= self.holding_days and 
                       self.Time.hour >= self.exit_hour and
                       not self.exit_attempted)
        
        # If it's a new day, reset exit_attempted flag
        if self.entry_time and self.Time.date() != self.entry_time.date():
            if self.Time.hour < self.exit_hour:
                self.exit_attempted = False
        
        # Debug: Log first few exit attempts
        if is_exit_time and len(self.trades) < 3:
            self.Debug(f"[{self.Time}] ✅ EXIT TIME REACHED: Closing position #{len(self.trades)+1}")
        
        if not is_exit_time:
            return
        
        # Mark that we've attempted exit today
        self.exit_attempted = True
        
        if days_held >= self.holding_days:
            # Get current contract value for exit
            if not self.Securities.ContainsKey(self.current_contract):
                self.Debug(f"⚠️ Contract missing! Liquidating all.")
                self.Liquidate()
                self.entry_time = None
                self.entry_price = None
                return
            
            # Scale exit price to match scaled entry price
            raw_exit_price = self.Securities[self.current_contract].Price
            exit_price = raw_exit_price * 1000
            
            if self.entry_price > 0:
                trade_return_bps = np.log(exit_price / self.entry_price) * 10000
                
                self.trades.append({
                    'entry_time': self.entry_time,
                    'exit_time': self.Time,
                    'entry_price': self.entry_price,
                    'exit_price': exit_price,
                    'return_bps': trade_return_bps,
                    'days_held': days_held
                })
                
                # Log first 10 exits, then every 50th, then last 10
                should_log = (len(self.trades) <= 10 or 
                            len(self.trades) % 50 == 0)
                
                if should_log:
                    hours_held = (self.Time - self.entry_time).total_seconds() / 3600
                    self.Debug(f"EXIT #{len(self.trades)}: {self.entry_time.strftime('%Y-%m-%d %H:%M')} → "
                              f"{self.Time.strftime('%Y-%m-%d %H:%M')} ({hours_held:.1f}h) | "
                              f"{self.entry_price:.0f} → {exit_price:.0f} | "
                              f"Return: {trade_return_bps:.2f} bps")
            
            # Close position
            self.Liquidate(self.current_contract)
            self.entry_time = None
            self.entry_price = None
            self.exit_attempted = False  # Reset for next trade
    
    
    def OnEndOfAlgorithm(self):
        """Summary comparing with algo_beta"""
        self.Debug("\n" + "=" * 60)
        self.Debug("BACKTEST COMPLETE")
        self.Debug("=" * 60)
        self.Debug(f"Trading days: {len(self.daily_16_prices)}")
        self.Debug(f"Pattern checks: {self.pattern_checks}")
        self.Debug(f"Pattern matches: {self.pattern_matches} (expected: ~384)")
        self.Debug(f"Trades executed: {len(self.trades)}")
        
        if len(self.trades) == 0:
            self.Debug("⚠️ NO TRADES EXECUTED")
            return
        
        # Calculate statistics
        returns = [t['return_bps'] for t in self.trades]
        wins = len([r for r in returns if r > 0])
        hit_rate = (wins / len(returns) * 100) if len(returns) > 0 else 0
        avg_return = np.mean(returns)
        
        self.Debug(f"\nPerformance:")
        self.Debug(f"  Hit Rate: {hit_rate:.1f}% (expected: ~58%)")
        self.Debug(f"  Avg Return: {avg_return:.2f} bps (expected: ~10 bps)")
        self.Debug(f"  Winners: {wins} | Losers: {len(returns) - wins}")
        
        # Check for stuck positions
        if self.Portfolio.Invested:
            self.Debug(f"\n⚠️ WARNING: Still holding position!")
            self.Debug(f"  Entry time: {self.entry_time}")
            self.Debug(f"  Days held: {(self.Time.date() - self.entry_time.date()).days}")
        
        self.Debug("=" * 60)
        
        # Calculate statistics
        returns = [t['return_bps'] for t in self.trades]
        wins = len([r for r in returns if r > 0])
        losses = len([r for r in returns if r <= 0])
        hit_rate = (wins / len(returns) * 100) if len(returns) > 0 else 0
        avg_return = np.mean(returns)
        std_return = np.std(returns)
        
        self.Debug("\n" + "=" * 60)
        self.Debug("COMPARISON WITH ALGO_BETA")
        self.Debug("=" * 60)
        self.Debug("Expected (algo_beta):")
        self.Debug("  Observations: 384")
        self.Debug("  Hit Rate: 58% (55% per output)")
        self.Debug("  MU (avg return): 10 bps at 21:00 (best horizon)")
        self.Debug("  TDR: 322")
        self.Debug("  Cum PL: 251 bps")
        
        self.Debug(f"\nActual (QuantConnect):")
        self.Debug(f"  Pattern Matches: {self.pattern_matches}")
        self.Debug(f"  Trades Executed: {len(self.trades)}")
        self.Debug(f"  Hit Rate: {hit_rate:.2f}%")
        self.Debug(f"  Avg Return: {avg_return:.2f} bps")
        self.Debug(f"  Std Dev: {std_return:.2f} bps")
        
        # Status checks
        match_pct = abs(self.pattern_matches - 384) / 384 * 100
        hr_diff = abs(hit_rate - 55)
        
        self.Debug("\n" + "=" * 60)
        self.Debug("VALIDATION STATUS")
        self.Debug("=" * 60)
        
        if match_pct < 10:
            self.Debug(f"✅ Pattern count: {self.pattern_matches} vs 384 (±{match_pct:.1f}%)")
        else:
            self.Debug(f"❌ Pattern count: {self.pattern_matches} vs 384 (±{match_pct:.1f}%)")
            
        if hr_diff < 5:
            self.Debug(f"✅ Hit rate: {hit_rate:.1f}% vs 55% (±{hr_diff:.1f}%)")
        else:
            self.Debug(f"❌ Hit rate: {hit_rate:.1f}% vs 55% (±{hr_diff:.1f}%)")
        
        if self.pattern_matches < 300:
            self.Debug("\n*** WARNING: Pattern count too low! ***")
            self.Debug("Check:")
            self.Debug("1. Are 16:00 prices being captured daily?")
            self.Debug("2. Is lookback calculation correct?")
            self.Debug("3. Are there data gaps?")
        
        # Last 10 trades
        self.Debug("\n" + "=" * 60)
        self.Debug("LAST 10 TRADES (compare with algo_beta output)")
        self.Debug("=" * 60)
        for i, trade in enumerate(self.trades[-10:], 1):
            self.Debug(f"{i}. {trade['entry_time'].strftime('%Y-%m-%d')} | "
                      f"Return: {trade['return_bps']:>7.2f} bps | "
                      f"Entry: {trade['entry_price']:.4f} | "
                      f"Exit: {trade['exit_price']:.4f}")
        
        # Compare with algo_beta's last 10
        self.Debug("\nalgo_beta's Last 10 Observations:")
        self.Debug("2024-11-22:   71 bps")
        self.Debug("2024-12-13:   -2 bps")
        self.Debug("2024-12-16: -108 bps")
        self.Debug("2024-12-17: -137 bps")
        self.Debug("2024-12-18:  -14 bps")
        self.Debug("2024-12-19:   37 bps")
        self.Debug("2024-12-20:   -9 bps")
        self.Debug("2024-12-31:    0 bps")
        self.Debug("2025-01-01:   28 bps")
        self.Debug("2025-01-02:   96 bps")
        
        self.Debug("\n" + "=" * 60)
        self.Debug("COMPARISON WITH ALGO_BETA")
        self.Debug("=" * 60)
        self.Debug("Expected (algo_beta):")
        self.Debug("  Observations: 384")
        self.Debug("  Hit Rate: 58% (55% per output)")
        self.Debug("  MU (avg return): 10 bps at 21:00 (best horizon)")
        self.Debug("  TDR: 322")
        self.Debug("  Cum PL: 251 bps")
        
        self.Debug(f"\nActual (QuantConnect):")
        self.Debug(f"  Pattern Matches: {self.pattern_matches}")
        self.Debug(f"  Trades Executed: {len(self.trades)}")
        self.Debug(f"  Hit Rate: {hit_rate:.2f}%")
        self.Debug(f"  Avg Return: {avg_return:.2f} bps")
        self.Debug(f"  Std Dev: {std_return:.2f} bps")
        
        # Status checks
        match_pct = abs(self.pattern_matches - 384) / 384 * 100
        hr_diff = abs(hit_rate - 55)
        
        self.Debug("\n" + "=" * 60)
        self.Debug("VALIDATION STATUS")
        self.Debug("=" * 60)
        
        if match_pct < 10:
            self.Debug(f"✅ Pattern count: {self.pattern_matches} vs 384 (±{match_pct:.1f}%)")
        else:
            self.Debug(f"❌ Pattern count: {self.pattern_matches} vs 384 (±{match_pct:.1f}%)")
            
        if hr_diff < 5:
            self.Debug(f"✅ Hit rate: {hit_rate:.1f}% vs 55% (±{hr_diff:.1f}%)")
        else:
            self.Debug(f"❌ Hit rate: {hit_rate:.1f}% vs 55% (±{hr_diff:.1f}%)")
        
        if self.pattern_matches < 300:
            self.Debug("\n*** WARNING: Pattern count too low! ***")
            self.Debug("Check:")
            self.Debug("1. Are 16:00 prices being captured daily?")
            self.Debug("2. Is lookback calculation correct?")
            self.Debug("3. Are there data gaps?")
        
        # Last 10 trades
        self.Debug("\n" + "=" * 60)
        self.Debug("LAST 10 TRADES (compare with algo_beta output)")
        self.Debug("=" * 60)
        for i, trade in enumerate(self.trades[-10:], 1):
            self.Debug(f"{i}. {trade['entry_time'].strftime('%Y-%m-%d')} | "
                      f"Return: {trade['return_bps']:>7.2f} bps | "
                      f"Entry: {trade['entry_price']:.4f} | "
                      f"Exit: {trade['exit_price']:.4f}")
        
        # Compare with algo_beta's last 10
        self.Debug("\nalgo_beta's Last 10 Observations:")
        self.Debug("2024-11-22:   71 bps")
        self.Debug("2024-12-13:   -2 bps")
        self.Debug("2024-12-16: -108 bps")
        self.Debug("2024-12-17: -137 bps")
        self.Debug("2024-12-18:  -14 bps")
        self.Debug("2024-12-19:   37 bps")
        self.Debug("2024-12-20:   -9 bps")
        self.Debug("2024-12-31:    0 bps")
        self.Debug("2025-01-01:   28 bps")
        self.Debug("2025-01-02:   96 bps")