Overall Statistics
Total Orders
1709
Average Win
0.82%
Average Loss
-0.76%
Compounding Annual Return
10.635%
Drawdown
63.900%
Expectancy
0.162
Start Equity
100000
End Equity
257012.57
Net Profit
157.013%
Sharpe Ratio
0.323
Sortino Ratio
0.374
Probabilistic Sharpe Ratio
1.279%
Loss Rate
44%
Win Rate
56%
Profit-Loss Ratio
1.08
Alpha
-0.012
Beta
1.282
Annual Standard Deviation
0.268
Annual Variance
0.072
Information Ratio
0.05
Tracking Error
0.189
Treynor Ratio
0.068
Total Fees
$5261.91
Estimated Strategy Capacity
$1900000.00
Lowest Capacity Asset
TNXP VJ0LBTYJ4I91
Portfolio Turnover
2.50%
from AlgorithmImports import *
import numpy as np

class ValueInvestingInsightsAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2016, 1, 1)  # Set Start Date
        self.SetEndDate(2025, 5, 1)    # Set End Date
        self.SetCash(100000)           # Set Strategy Cash
        self.AddEquity("SPY", Resolution.Daily)
        
        # Universe selection for value investing
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        self.value_stocks = []
        self.rebalance_time = datetime.min

    def CoarseSelectionFunction(self, coarse):
        # Filter for stocks with fundamental data and price > 5
        filtered = [x for x in coarse if x.HasFundamentalData and x.Price > 5]
        # Sort by dollar volume and take top 1000
        sorted_by_dollar_volume = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        return [x.Symbol for x in sorted_by_dollar_volume[:1000]]


    def FineSelectionFunction(self, fine):
        """
        Composite z-score ranking with assignment to self.value_stocks:
        1. Define metrics
        2. Compute mean & std for each metric over valid values
        3. Calculate per-symbol z-scores and composite
        4. Assign top 20 symbols to self.value_stocks
        """
        # 1) Define metrics to use
        metrics = {
            'EarningYield':    lambda x: x.ValuationRatios.EarningYield,
            'BookValueYield':  lambda x: x.ValuationRatios.BookValueYield,
            'CFYield':         lambda x: x.ValuationRatios.CFYield,
            'FCFYield':        lambda x: x.ValuationRatios.FCFYield
        }

        # 2) Compute mean & std for each metric across valid entries
        stats = {}
        for name, func in metrics.items():
            vals = [func(x) for x in fine if func(x) is not None and func(x) > 0]
            if len(vals) >= 2:
                arr = np.array(vals, dtype=float)
                stats[name] = (arr.mean(), arr.std(ddof=0))
            else:
                stats[name] = (None, None)

        # 3) Build composite z-scores
        composites = []
        for x in fine:
            z_sum = 0.0
            count = 0
            for name, func in metrics.items():
                val = func(x)
                mu, sigma = stats[name]
                if mu is not None and sigma and val is not None and val > 0:
                    z_sum += (val - mu) / sigma
                    count += 1
            if count > 0:
                composites.append((x.Symbol, z_sum / count))

        # 4) Sort by composite z-score descending and take top 20
        composites.sort(key=lambda item: item[1], reverse=True)
        self.value_stocks = [symbol for symbol, _ in composites[:20]]

        return self.value_stocks

    def OnData(self, data):
        # Rebalance monthly
        if self.Time < self.rebalance_time:
            return

        self.rebalance_time = self.Time + timedelta(30)

        # Liquidate positions not in value stocks
        for symbol in self.Portfolio.Keys:
            if symbol not in self.value_stocks:
                self.Liquidate(symbol)

        # Invest equally in value stocks
        for symbol in self.value_stocks:
            if self.Securities[symbol].Invested:
                continue
            self.SetHoldings(symbol, 1 / len(self.value_stocks))