Overall Statistics
Total Orders
526
Average Win
3.49%
Average Loss
-1.52%
Compounding Annual Return
18.325%
Drawdown
55.600%
Expectancy
1.151
Start Equity
100000
End Equity
7915583.11
Net Profit
7815.583%
Sharpe Ratio
0.536
Sortino Ratio
0.631
Probabilistic Sharpe Ratio
0.508%
Loss Rate
35%
Win Rate
65%
Profit-Loss Ratio
2.29
Alpha
0.091
Beta
1.042
Annual Standard Deviation
0.255
Annual Variance
0.065
Information Ratio
0.478
Tracking Error
0.194
Treynor Ratio
0.131
Total Fees
$11717.28
Estimated Strategy Capacity
$740000.00
Lowest Capacity Asset
PTGX WCXSE456TVTX
Portfolio Turnover
0.43%
Drawdown Recovery
1996
# region imports
from AlgorithmImports import *
# endregion

class RetrospectiveBlueCow(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2000, 1, 1)
        self.set_cash(100000)
        
        # Allow orders even with small margin requirements
        self.settings.minimum_order_margin_portfolio_percentage = 0
        
        # Warm up to ensure price data is available
        self.set_warmup(0, Resolution.DAILY)
        
        # Add fundamental universe with annual rebalancing
        self._universe = self.add_universe(self._select_fundamentals)
        self.universe_settings.schedule.on(self.date_rules.year_start())
        
        # Track selected stocks for ranking
        self._stocks = {}
        self._rebalance_pending = False
        
    def _select_fundamentals(self, fundamentals: List[Fundamental]) -> List[Symbol]:
        """Filter and rank stocks based on fundamental criteria"""
        
        filtered = []
        
        for f in fundamentals:
            # Skip if missing required data
            if not f.has_fundamental_data:
                continue
                
            # Filter criteria
            market_cap = f.market_cap
            roic = f.operation_ratios.roic.one_year if f.operation_ratios.roic else 0
            debt_to_equity = f.operation_ratios.total_debt_equity_ratio.one_year if f.operation_ratios.total_debt_equity_ratio else 1
            pe_ratio = f.valuation_ratios.pe_ratio
            
            # Revenue CAGR 5Y
            revenue_growth_5y = f.operation_ratios.revenue_growth.five_years if f.operation_ratios.revenue_growth else 0
            
            # Apply filters
            # NYSE exchange code is 'NYS', NASDAQ is 'NAS'
            if (f.security_reference.exchange_id in ['NYS', 'NAS'] and
                market_cap > 2_000_000_000 and
                roic > 0.15 and
                revenue_growth_5y > 0.10 and
                debt_to_equity < 0.5 and
                pe_ratio < 15):
                
                # Calculate ranking score
                score = pe_ratio
                filtered.append((f.symbol, score, roic))
        
        # Sort by score and take top 50 stocks
        filtered.sort(key=lambda x: x[1], reverse=True)
        top_stocks = [x[0] for x in filtered[:50]]
        
        # Store stocks with their scores for logging
        self._stocks = {x[0]: x[1] for x in filtered[:50]}
        
        if len(top_stocks) > 0:
            self.log(f"\n{'='*60}")
            self.log(f"Selected {len(top_stocks)} stocks for rebalancing")
            self.log(f"Allocation per stock: {100/len(top_stocks):.2f}%")
            self.log(f"\nTop 50 Stocks:")
            for i, (symbol, score, roic) in enumerate(filtered[:50], 1):
                self.log(f"{i:2d}. {str(symbol.value):6s} - Score: {score:.4f} - ROI {roic:.4f}")
            self.log(f"{'='*60}\n")
        
        return top_stocks
    
    def on_securities_changed(self, changes: SecurityChanges) -> None:
        """Mark rebalance as pending when universe changes"""
        if len(changes.added_securities) > 0:
            self._rebalance_pending = True
    
    def on_data(self, data: Slice) -> None:
        """Rebalance when data is available"""
        
        if not self._rebalance_pending or self.is_warming_up:
            return
        
        # Get current universe members
        active_stocks = [s for s in self._universe.selected if data.contains_key(s) and data[s] is not None]
        
        if len(active_stocks) == 0:
            return
            
        # Equal weight allocation
        weight = 1.0 / len(active_stocks)
        
        # Create portfolio targets
        targets = [PortfolioTarget(symbol, weight) for symbol in active_stocks]
        
        # Rebalance portfolio
        self.set_holdings(targets, liquidate_existing_holdings=True)
        
        self.log(f"Rebalanced portfolio with {len(active_stocks)} stocks")
        self._rebalance_pending = False