Overall Statistics
Total Orders
1073
Average Win
4.33%
Average Loss
-2.80%
Compounding Annual Return
79.202%
Drawdown
62.000%
Expectancy
0.318
Start Equity
10000
End Equity
331525.97
Net Profit
3215.260%
Sharpe Ratio
1.426
Sortino Ratio
1.89
Probabilistic Sharpe Ratio
68.907%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.54
Alpha
0.433
Beta
1.305
Annual Standard Deviation
0.422
Annual Variance
0.178
Information Ratio
1.442
Tracking Error
0.328
Treynor Ratio
0.461
Total Fees
$4203.49
Estimated Strategy Capacity
$1000000000.00
Lowest Capacity Asset
GOOG T1AZ164W5VTX
Portfolio Turnover
43.73%
Drawdown Recovery
793
#region imports
from AlgorithmImports import *
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
import numpy as np
import pandas as pd
#endregion

class IRPrecisionFalcon(QCAlgorithm):
    def Initialize(self):
        # 1. STRICT ACCOUNT CONSTRAINTS
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2025, 12, 30)
        self.SetCash(10000) 
        
        # 2. ASSETS & BENCHMARK
        self.tickers = ["AAPL", "AMZN", "GOOGL", "META", "MSFT", "NVDA", "TSLA", "AMD"]
        self.symbols = [self.AddEquity(t, Resolution.Daily).Symbol for t in self.tickers]
        
        # Reserved 'self._bench' for IR tracking (against QQQ)
        self._bench = self.AddEquity("QQQ", Resolution.Daily).Symbol 
        self.SetBenchmark(self._bench)
        
        # 3. ML PARAMETERS
        self.classifier = None
        self.scaler = StandardScaler()
        self.min_confidence = 0.70 # High bar to reduce Tracking Error
        
        # 4. SETTINGS
        self.SetWarmUp(252)
        # Train monthly - captures new market correlations
        self.Train(self.DateRules.MonthStart(), self.TimeRules.At(0, 0), self.TrainModel)

    def TrainModel(self):
        # Fetch data: 500 days captures roughly 2 years of regimes
        history = self.History(self.symbols + [self._bench], 500, Resolution.Daily)
        if history.empty: return
        
        # Vectorized close prices (prevents indexing errors)
        all_closes = history['close'].unstack(level=0)
        
        # Calculate 10-day returns for all assets at once
        # This removes the need for .iloc[-10] which caused your crash
        ret_10d = all_closes.pct_change(10).dropna()
        
        features, labels = [], []
        bench_ret = ret_10d[self._bench.Value]
        
        for s in self.symbols:
            if s.Value not in ret_10d.columns: continue
            
            s_ret = ret_10d[s.Value]
            active_ret = s_ret - bench_ret # The Alpha component
            
            # Use data to build features and look-forward labels
            for i in range(len(active_ret) - 5):
                # Features: [Current Return, Current Alpha]
                features.append([s_ret.iloc[i], active_ret.iloc[i]])
                
                # Label: 1 if the stock beats QQQ over the NEXT 5 trading days
                # This focuses the model specifically on maximizing the IR Numerator
                f_s = (all_closes[s.Value].iloc[i+5] / all_closes[s.Value].iloc[i]) - 1
                f_b = (all_closes[self._bench.Value].iloc[i+5] / all_closes[self._bench.Value].iloc[i]) - 1
                labels.append(1 if f_s > f_b else 0)

        if features:
            self.classifier = RandomForestClassifier(n_estimators=100, max_depth=4, random_state=42)
            self.classifier.fit(self.scaler.fit_transform(features), labels)

    def OnData(self, data):
        if self.IsWarmingUp or self.classifier is None: return

        # 1. EVALUATE RECENT DATA
        # Fetch 11 days to safely calculate 10-day pct_change
        hist = self.History(self.symbols + [self._bench], 11, Resolution.Daily)['close'].unstack(level=0)
        if len(hist) < 11: return
        
        cur_rets = hist.pct_change(10).iloc[-1]
        b_ret = cur_rets[self._bench.Value]
        
        candidates = []
        for s in self.symbols:
            if s.Value not in cur_rets: continue
            s_ret = cur_rets[s.Value]
            
            # Predict probability of beating the benchmark
            feat = self.scaler.transform([[s_ret, s_ret - b_ret]])
            prob = self.classifier.predict_proba(feat)[0][1]
            
            if prob > self.min_confidence:
                candidates.append((s, prob))

        # 2. EXECUTION: OPTIMIZING INFORMATION RATIO
        # Scenario A: WE HAVE ALPHA SIGNAL
        if candidates:
            # Pick the single best stock to maximize concentration of alpha
            best_s = sorted(candidates, key=lambda x: x[1], reverse=True)[0][0]
            
            if not self.Portfolio[best_s].Invested:
                # Clear portfolio before switching to ensure NO margin is used
                self.Liquidate()
                # 0.98 ensures transaction fees are paid from cash, NOT margin
                self.SetHoldings(best_s, 0.98) 
        
        # Scenario B: NO CLEAR ALPHA SIGNAL
        # Default to the Benchmark (QQQ). 
        # Holding the benchmark makes your 'Tracking Error' ZERO.
        # This prevents the IR from falling during uncertain times.
        elif not self.Portfolio[self._bench].Invested:
            self.Liquidate()
            self.SetHoldings(self._bench, 0.98)