| 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]]