Overall Statistics
Total Trades
1134
Average Win
0.13%
Average Loss
-0.24%
Compounding Annual Return
-1.189%
Drawdown
60.400%
Expectancy
-0.213
Net Profit
-11.727%
Sharpe Ratio
0.024
Probabilistic Sharpe Ratio
0.008%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
0.55
Alpha
-0.001
Beta
0.609
Annual Standard Deviation
0.154
Annual Variance
0.024
Information Ratio
-0.029
Tracking Error
0.129
Treynor Ratio
0.006
Total Fees
$1786.38
Estimated Strategy Capacity
$13000000.00
Lowest Capacity Asset
MGA R735QTJ8XC9X
Portfolio Turnover
0.20%
from AlgorithmImports import *

class ValueAlphaModel(AlphaModel):
    
    securities = []
    month = -1

    def __init__(self, portfolio_size):
        self.portfolio_size = portfolio_size

    def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        # Only rebalance when there is QuoteBar data
        if data.QuoteBars.Count == 0:
            return []
        
        # Rebalance bi-annually in January and July
        if self.month == data.Time.month or data.Time.month not in [1, 7]:
            return []
        self.month = data.Time.month
        
        insights = []
        expiry = timedelta(days=182)  # Insights will expire after approximately half a year

        # Create insights to long the top securities based on the scoring criteria from the universe selection model
        tradable_securities = [security for security in self.securities if security.Symbol in data.QuoteBars and security.Price > 0 and security.Fundamentals]
        for security in sorted(tradable_securities, key=lambda s: s.Fundamentals.ValuationRatios.EarningYield / (s.Fundamentals.OperationRatios.ROE.Value if s.Fundamentals.OperationRatios.ROE.Value != 0 else 1), reverse=True)[:self.portfolio_size]:
            insights.append(Insight.Price(security.Symbol, expiry, InsightDirection.Up))
        return insights

    def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        for security in changes.RemovedSecurities:
            if security in self.securities:
                self.securities.remove(security)
        self.securities.extend(changes.AddedSecurities)
# region imports
from AlgorithmImports import *

from universe import ScoredUniverseSelectionModel
from alpha import ValueAlphaModel
from portfolio import EqualWeightingRebalanceOnInsightsPortfolioConstructionModel
# endregion

class BuffetBargainHunterAlgorithm(QCAlgorithm):

    undesired_symbols_from_previous_deployment = []
    checked_symbols_from_previous_deployment = False

    def Initialize(self):
        self.AddEquity("SPY", Resolution.Daily) # Set resolution to Daily
        self.SetBenchmark("SPY")
        self.SetStartDate(2000, 1, 1)
        self.SetEndDate(2010, 6, 1)
        self.SetCash(1_000_000)
        
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        self.Settings.MinimumOrderMarginPortfolioPercentage = 0

        self.SetSecurityInitializer(BrokerageModelSecurityInitializer(self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices)))

        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
        self.AddUniverseSelection(ScoredUniverseSelectionModel(
            self,
            self.UniverseSettings,
            self.GetParameter("coarse_size", 500),
            self.GetParameter("fine_size", 250)
        ))

        self.AddAlpha(ValueAlphaModel(self.GetParameter("portfolio_size", 30)))

        self.SetPortfolioConstruction(EqualWeightingRebalanceOnInsightsPortfolioConstructionModel(self))

        self.AddRiskManagement(NullRiskManagementModel())

        self.SetExecution(ImmediateExecutionModel()) 

        self.SetWarmUp(timedelta(356))

    def OnData(self, data):
        # Exit positions that aren't backed by existing insights.
        # If you don't want this behavior, delete this method definition.
        if not self.IsWarmingUp and not self.checked_symbols_from_previous_deployment:
            for security_holding in self.Portfolio.Values:
                if not security_holding.Invested:
                    continue
                symbol = security_holding.Symbol
                if not self.Insights.HasActiveInsights(symbol, self.UtcTime):
                    self.undesired_symbols_from_previous_deployment.append(symbol)
            self.checked_symbols_from_previous_deployment = True
        
        for symbol in self.undesired_symbols_from_previous_deployment[:]:
            if self.IsMarketOpen(symbol):
                self.Liquidate(symbol, tag="Holding from previous deployment that's no longer desired")
                self.undesired_symbols_from_previous_deployment.remove(symbol)
from AlgorithmImports import *

class EqualWeightingRebalanceOnInsightsPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
    def __init__(self, algorithm):
        super().__init__()
        self.algorithm = algorithm
        self.new_insights = False

    def IsRebalanceDue(self, insights: List[Insight], algorithmUtc: datetime) -> bool:
        if not self.new_insights:
            self.new_insights = len(insights) > 0

        # Only allow rebalancing in January and July
        if algorithmUtc.month not in [1, 7]:
            return False

        is_rebalance_due = self.new_insights and not self.algorithm.IsWarmingUp and self.algorithm.CurrentSlice.QuoteBars.Count > 0
        
        if is_rebalance_due:
            self.new_insights = False
            
        return is_rebalance_due

#region imports
from AlgorithmImports import *
#endregion
# 05/24/2023: -Updated universe selection timing to run at the start of each month.
#             -Added warm-up.
#             -Removed the risk management model so the algorithm could warm-up properly.
#             -Added OnWarmupFinished to liquidate existing holdings that aren't backed by active insights.
#             https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_210cae3651bd5aa87f79cd88b5f8109b.html
#
# 05/24/2023: -Updated universe selection timing so that the first trading day of each month always has the latest universe already selected.
#              Before this change, if the first trading day of the month was a Monday, the universe selection would run on Tuesday morning, 
#              cancelling some of the month's insights during warm-up. 
#             https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_3806b7739d53f7b3687a948d0613cb25.html
#
# 05/26/2023: -Updated IsRebalanceDue function in the portfolio construction model to avoid MOO orders when deploying outside of regular trading hours
#             https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_d15053c45241cf1803495ca83f5452c5.html
#
# 07/13/2023: -Fixed warm-up logic to liquidate undesired portfolio holdings on re-deployment
#             -Set the MinimumOrderMarginPortfolioPercentage to 0
#             https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_ac226bd3f017d0d0812d3b41fc25772a.html
from AlgorithmImports import *

class ScoredUniverseSelectionModel(FineFundamentalUniverseSelectionModel):
    def __init__(self, algorithm: QCAlgorithm, universe_settings: UniverseSettings = None, coarse_size: int = 1000, fine_size: int = 30) -> None:
        self.algorithm = algorithm
        self.coarse_size = coarse_size
        self.fine_size = fine_size
        self.month = -1
        self.hours = None
        self.excluded_symbols = ["DBB", "UUP"]  # List of symbols to exclude
        super().__init__(self.SelectCoarse, self.SelectFine, universe_settings)

    def SelectCoarse(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
        if not self.hours or self.algorithm.LiveMode:
            self.hours = self.algorithm.MarketHoursDatabase.GetEntry(Market.USA, "SPY", SecurityType.Equity).ExchangeHours
        self.next_open = self.hours.GetNextMarketOpen(self.algorithm.Time, False)
        if self.month == self.next_open.month or self.next_open.month not in [1, 7]:
            return Universe.Unchanged
        self.month = self.next_open.month
        selected = [c for c in coarse if c.HasFundamentalData and c.Symbol.Value not in self.excluded_symbols]
        sorted_by_dollar_volume = sorted(selected, key=lambda c: c.DollarVolume, reverse=True)
        return [c.Symbol for c in sorted_by_dollar_volume[:self.coarse_size]]

    def SelectFine(self, fine: List[FineFundamental]) -> List[Symbol]:
        # Filtering out excluded symbols at the beginning of the fine selection
        fine = [f for f in fine if f.Symbol.Value not in self.excluded_symbols]

        relative_earnings_yield_rank = {}
        relative_roe_rank = {}
        scores = {}

        # Step 1: Compute relative rank based on earnings yield
        sorted_by_earnings_yield = sorted(fine, key=lambda x: x.ValuationRatios.EarningYield, reverse=True)
        total_positions = len(sorted_by_earnings_yield)
        for idx, f in enumerate(sorted_by_earnings_yield):
            relative_earnings_yield_rank[f.Symbol] = (idx + 1) / total_positions

        # Step 2: Compute relative rank based on ROE
        sorted_by_roe = sorted(fine, key=lambda x: x.OperationRatios.ROE.Value, reverse=True)
        for idx, f in enumerate(sorted_by_roe):
            relative_roe_rank[f.Symbol] = (idx + 1) / total_positions
        
        # Step 3: Compute score for each company
        for f in fine:
            scores[f.Symbol] = relative_earnings_yield_rank[f.Symbol] / relative_roe_rank[f.Symbol]
        
        # Step 4: Rank securities based on score
        sorted_by_score = sorted(fine, key=lambda x: scores[x.Symbol], reverse=True)
        
        return [c.Symbol for c in sorted_by_score[:self.fine_size]]