Overall Statistics |
Total Orders 22213 Average Win 0.07% Average Loss -0.10% Compounding Annual Return 26.178% Drawdown 49.400% Expectancy 0.141 Start Equity 1000000 End Equity 4756408.52 Net Profit 375.641% Sharpe Ratio 0.763 Sortino Ratio 0.826 Probabilistic Sharpe Ratio 25.609% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 0.70 Alpha 0.048 Beta 0.924 Annual Standard Deviation 0.24 Annual Variance 0.058 Information Ratio 0.282 Tracking Error 0.13 Treynor Ratio 0.198 Total Fees $44194.11 Estimated Strategy Capacity $79000000.00 Lowest Capacity Asset TMO R735QTJ8XC9X Portfolio Turnover 20.90% |
#region imports from AlgorithmImports import * from sklearn.ensemble import RandomForestRegressor #endregion class RandomForestAlphaModel(AlphaModel): _securities = [] _scheduled_event = None _time = datetime.min _rebalance = False def __init__(self, algorithm, minutes_before_close, n_estimators, min_samples_split, lookback_days): self._algorithm = algorithm self._minutes_before_close = minutes_before_close self._n_estimators = n_estimators self._min_samples_split = min_samples_split self._lookback_days = lookback_days def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: if not self._rebalance or data.quote_bars.count == 0: return [] # Fetch history on our universe symbols = [s.symbol for s in self._securities] df = algorithm.history(symbols, 2, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW) if df.empty: return [] self._rebalance = False # Make all of them into a single time index. df = df.close.unstack(level=0) # Feature engineer the data for input input_ = df.diff() * 0.5 + df * 0.5 input_ = input_.iloc[-1].fillna(0).values.reshape(1, -1) # Predict the expected price predictions = self._regressor.predict(input_) # Get the expected return predictions = (predictions - df.iloc[-1].values) / df.iloc[-1].values predictions = predictions.flatten() insights = [] for i in range(len(predictions)): insights.append( Insight.price(df.columns[i], timedelta(5), InsightDirection.UP, predictions[i]) ) algorithm.insights.cancel(symbols) return insights # for i in range(len(predictions)): # # Check if the prediction is positive (for long) or negative (for short) # if predictions[i] > 0: # direction = InsightDirection.UP # Long signal # else: # direction = InsightDirection.DOWN # Short signal # insights.append(Insight.price(df.columns[i], timedelta(5), direction, abs(predictions[i]))) # algorithm.insights.cancel(symbols) # return insights def _train_model(self): # Initialize the Random Forest Regressor self._regressor = RandomForestRegressor(n_estimators=self._n_estimators, min_samples_split=self._min_samples_split, random_state = 1990) # Get historical data history = self._algorithm.history([s.symbol for s in self._securities], self._lookback_days, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW) # Select the close column and then call the unstack method. df = history['close'].unstack(level=0) # Feature engineer the data for input. input_ = df.diff() * 0.5 + df * 0.5 input_ = input_.iloc[1:].ffill().fillna(0) # Shift the data for 1-step backward as training output result. output = df.shift(-1).iloc[:-1].ffill().fillna(0) # Fit the regressor self._regressor.fit(input_, output) def _before_market_close(self): if self._time < self._algorithm.time: self._train_model() self._time = Expiry.end_of_month(self._algorithm.time) self._rebalance = True def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None: for security in changes.removed_securities: if security in self._securities: self._securities.remove(security) for security in changes.added_securities: self._securities.append(security) # Add Scheduled Event if self._scheduled_event == None: symbol = security.symbol self._scheduled_event = algorithm.schedule.on( algorithm.date_rules.every_day(symbol), algorithm.time_rules.before_market_close(symbol, self._minutes_before_close), self._before_market_close ) self._train_model()
# region imports from AlgorithmImports import * from alpha import RandomForestAlphaModel from portfolio import MeanVarianceOptimizationPortfolioConstructionModel # endregion class RandomForestAlgorithm(QCAlgorithm): _undesired_symbols_from_previous_deployment = [] _checked_symbols_from_previous_deployment = False def initialize(self): # self.set_start_date(2022, 6, 1) # self.set_end_date(2023, 6, 1) self.set_start_date(2018, 1, 1) # self.set_end_date(2024, 8, 31) self.set_cash(1_000_000) self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.settings.minimum_order_margin_portfolio_percentage = 0 self.AddEquity("XLK", Resolution.Daily) self.benchmarkTicker = 'XLK' self.SetBenchmark(self.benchmarkTicker) self.initBenchmarkPrice = 0 self.benchmarkExposure = 1 self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW # tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOG", "META", "TSLA", "AVGO", "ORCL", "NFLX", # "ADBE", "CRM", "AMD", "CSCO", "IBM", "TXN", "QCOM", "NOW", "INTU", "UBER"] # symbols = [ Symbol.create(ticker, SecurityType.EQUITY, Market.USA) for ticker in tickers] # self.add_universe_selection(ManualUniverseSelectionModel(symbols)) # Use the following method for a Classic Algorithm self.AddUniverse(self.Universe.ETF("SCHG", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) symbol = Symbol.Create("SCHG", SecurityType.Equity, Market.USA) # self.AddUniverse(self.Universe.ETF("VONG", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) # symbol = Symbol.Create("VONG", SecurityType.Equity, Market.USA) # self.AddUniverse(self.Universe.ETF("IWF", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) # symbol = Symbol.Create("IWF", SecurityType.Equity, Market.USA) # self.AddUniverse(self.Universe.ETF("SPYG", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) # symbol = Symbol.Create("SPYG", SecurityType.Equity, Market.USA) # self.AddUniverse(self.Universe.ETF("IWY", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) # symbol = Symbol.Create("IWY", SecurityType.Equity, Market.USA) # self.AddUniverse(self.Universe.ETF("SPGP", Market.USA, self.UniverseSettings, self.ETFConstituentsFilter)) # symbol = Symbol.Create("SPGP", SecurityType.Equity, Market.USA) # Use the following method for a Framework Algorithm self.AddUniverseSelection(ETFConstituentsUniverseSelectionModel(symbol, self.UniverseSettings, self.ETFConstituentsFilter)) # Add dynamic universe selection with fundamental filtering # self.AddUniverse(self.FundamentalUniverseSelection) self.add_alpha(RandomForestAlphaModel( self, self.get_parameter("minutes_before_close", 5), self.get_parameter("n_estimators", 100), self.get_parameter("min_samples_split", 5), self.get_parameter("lookback_days", 360) )) self.set_portfolio_construction(MeanVarianceOptimizationPortfolioConstructionModel(self, lambda time: None, PortfolioBias.LONG, period=self.get_parameter("pcm_periods", 5))) # self.set_portfolio_construction(MeanVarianceOptimizationPortfolioConstructionModel( # self, # lambda time: None, # PortfolioBias.LONG_SHORT, # Allows both long and short positions # period=self.get_parameter("pcm_periods", 5) # )) self.add_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) self.set_warm_up(timedelta(5)) def ETFConstituentsFilter(self, constituents): # Get the 10 securities with the largest weight in the index selected = sorted([c for c in constituents if c.Weight], key=lambda c: c.Weight, reverse=True)[:20] self.weightBySymbol = {c.Symbol: c.Weight for c in selected} return list(self.weightBySymbol.keys()) def FundamentalUniverseSelection(self, fundamental): """ Selects stocks based on fundamental data. - Filters stocks in the energy sector with a positive market cap. - Sorts filtered stocks by market capitalization in descending order. - Selects the top 20 stocks by market cap. :param fundamental: List of fundamental data objects :return: List of selected stock symbols """ energy_sector_code = MorningstarSectorCode.ENERGY # Define sector code for energy # Filter stocks based on sector and market cap filtered = [ x for x in fundamental if x.AssetClassification.MorningstarSectorCode == energy_sector_code and x.MarketCap > 0 ] # Sort filtered stocks by market capitalization sorted_by_market_cap = sorted(filtered, key=lambda x: x.MarketCap, reverse=True) # Return top 20 stocks by market capitalization return [x.Symbol for x in sorted_by_market_cap][:20] def on_data(self, data): self.UpdateBenchmarkValue() self.Plot('Strategy Equity', self.benchmarkTicker, self.benchmarkValue) # Exit positions that aren't backed by existing insights. # If you don't want this behavior, delete this method definition. if not self.is_warming_up 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.has_active_insights(symbol, self.utc_time): 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.is_market_open(symbol): self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired") self._undesired_symbols_from_previous_deployment.remove(symbol) def UpdateBenchmarkValue(self): ''' Simulate buy and hold the Benchmark ''' # if self.initBenchmarkPrice is None: if self.initBenchmarkPrice == 0: # Use if Plotting Short Position of Benchmark self.initBenchmarkCash = self.Portfolio.Cash self.initBenchmarkPrice = self.Benchmark.Evaluate(self.Time) self.benchmarkValue = self.initBenchmarkCash else: currentBenchmarkPrice = self.Benchmark.Evaluate(self.Time) # self.benchmarkValue = (currentBenchmarkPrice / self.initBenchmarkPrice) * self.initBenchmarkCash # Use if Plotting Short Position of Benchmark lastReturn = ((currentBenchmarkPrice / self.initBenchmarkPrice) - 1) * self.benchmarkExposure self.benchmarkValue = (1 + lastReturn) * self.initBenchmarkCash
# region imports from AlgorithmImports import * from alpha import RandomForestAlphaModel from portfolio import MeanVarianceOptimizationPortfolioConstructionModel # endregion class RandomForestAlgorithm(QCAlgorithm): _undesired_symbols_from_previous_deployment = [] _checked_symbols_from_previous_deployment = False def initialize(self): self.set_start_date(2022, 6, 1) self.set_end_date(2023, 6, 1) self.set_cash(1_000_000) self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.settings.minimum_order_margin_portfolio_percentage = 0 self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW tickers = ["SHY", "TLT", "IEI", "SHV", "TLH", "EDV", "BIL", "SPTL", "TBT", "TMF", "TMV", "TBF", "VGSH", "VGIT", "VGLT", "SCHO", "SCHR", "SPTS", "GOVT"] symbols = [ Symbol.create(ticker, SecurityType.EQUITY, Market.USA) for ticker in tickers] self.add_universe_selection(ManualUniverseSelectionModel(symbols)) self.add_alpha(RandomForestAlphaModel( self, self.get_parameter("minutes_before_close", 5), self.get_parameter("n_estimators", 100), self.get_parameter("min_samples_split", 5), self.get_parameter("lookback_days", 360) )) self.set_portfolio_construction(MeanVarianceOptimizationPortfolioConstructionModel(self, lambda time: None, PortfolioBias.LONG, period=self.get_parameter("pcm_periods", 5))) self.add_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) self.set_warm_up(timedelta(5)) def on_data(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.is_warming_up 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.has_active_insights(symbol, self.utc_time): 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.is_market_open(symbol): self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired") self._undesired_symbols_from_previous_deployment.remove(symbol)
# We re-define the MeanVarianceOptimizationPortfolioConstructionModel because # - The model doesn't warm-up with ScaledRaw data (https://github.com/QuantConnect/Lean/issues/7239) # - The original definition doesn't reset the `roc` and `window` in the `MeanVarianceSymbolData` objects when corporate actions occur from AlgorithmImports import * from Portfolio.MinimumVariancePortfolioOptimizer import MinimumVariancePortfolioOptimizer ### <summary> ### Provides an implementation of Mean-Variance portfolio optimization based on modern portfolio theory. ### The default model uses the MinimumVariancePortfolioOptimizer that accepts a 63-row matrix of 1-day returns. ### </summary> class MeanVarianceOptimizationPortfolioConstructionModel(PortfolioConstructionModel): def __init__(self, algorithm, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT, lookback = 1, period = 63, resolution = Resolution.DAILY, target_return = 0.10, optimizer = None): """Initialize the model Args: rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function. If None will be ignored. The function returns the next expected rebalance time for a given algorithm UTC DateTime. The function returns null if unknown, in which case the function will be called again in the next loop. Returning current time will trigger rebalance. portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long) lookback(int): Historical return lookback period period(int): The time interval of history price to calculate the weight resolution: The resolution of the history price optimizer(class): Method used to compute the portfolio weights""" super().__init__() self._algorithm = algorithm self._lookback = lookback self._period = period self._resolution = resolution self._portfolio_bias = portfolio_bias self._sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0) lower = algorithm.settings.min_absolute_portfolio_target_percentage*1.1 if portfolio_bias == PortfolioBias.LONG else -1 upper = 0 if portfolio_bias == PortfolioBias.SHORT else 1 self._optimizer = MinimumVariancePortfolioOptimizer(lower, upper, target_return) if optimizer is None else optimizer self._symbol_data_by_symbol = {} self._new_insights = False def is_rebalance_due(self, insights, algorithmUtc): if not self._new_insights: self._new_insights = len(insights) > 0 is_rebalance_due = self._new_insights and not self._algorithm.is_warming_up and self._algorithm.current_slice.quote_bars.count > 0 if is_rebalance_due: self._new_insights = False return is_rebalance_due def create_targets(self, algorithm, insights): # Reset and warm-up indicators when corporate actions occur data = algorithm.current_slice reset_symbols = [] for symbol in set(data.dividends.keys()) | set(data.splits.keys()): symbol_data = self._symbol_data_by_symbol[symbol] if symbol_data.should_reset(): symbol_data.clear_history() reset_symbols.append(symbol) if reset_symbols: self._warm_up(algorithm, reset_symbols) return super().create_targets(algorithm, insights) def should_create_target_for_insight(self, insight): if len(PortfolioConstructionModel.filter_invalid_insight_magnitude(self._algorithm, [insight])) == 0: return False symbol_data = self._symbol_data_by_symbol.get(insight.symbol) if insight.magnitude is None: self._algorithm.set_run_time_error(ArgumentNullException('MeanVarianceOptimizationPortfolioConstructionModel does not accept \'None\' as Insight.magnitude. Please checkout the selected Alpha Model specifications.')) return False symbol_data.add(self._algorithm.time, insight.magnitude) return True def determine_target_percent(self, activeInsights): """ Will determine the target percent for each insight Args: Returns: """ targets = {} # If we have no insights just return an empty target list if len(activeInsights) == 0: return targets symbols = [insight.symbol for insight in activeInsights] # Create a dictionary keyed by the symbols in the insights with an pandas.series as value to create a data frame returns = { str(symbol.id) : data.return_ for symbol, data in self._symbol_data_by_symbol.items() if symbol in symbols } returns = pd.DataFrame(returns) # The portfolio optimizer finds the optional weights for the given data weights = self._optimizer.optimize(returns) weights = pd.Series(weights, index = returns.columns) # Create portfolio targets from the specified insights for insight in activeInsights: weight = weights[str(insight.symbol.id)] # don't trust the optimizer if self._portfolio_bias != PortfolioBias.LONG_SHORT and self._sign(weight) != self._portfolio_bias: weight = 0 targets[insight] = weight return targets def on_securities_changed(self, algorithm, changes): # clean up data for removed securities super().on_securities_changed(algorithm, changes) for removed in changes.removed_securities: symbol_data = self._symbol_data_by_symbol.pop(removed.symbol, None) symbol_data.reset() # initialize data for added securities symbols = [x.symbol for x in changes.added_securities] for symbol in [x for x in symbols if x not in self._symbol_data_by_symbol]: self._symbol_data_by_symbol[symbol] = self.MeanVarianceSymbolData(symbol, self._lookback, self._period) self._warm_up(algorithm, symbols) def _warm_up(self, algorithm, symbols): history = algorithm.history[TradeBar](symbols, self._lookback * self._period + 1, self._resolution, data_normalization_mode=DataNormalizationMode.SCALED_RAW) for bars in history: for symbol, bar in bars.items(): self._symbol_data_by_symbol.get(symbol).update(bar.end_time, bar.value) class MeanVarianceSymbolData: def __init__(self, symbol, lookback, period): self._symbol = symbol self._roc = RateOfChange(f'{symbol}.ROC({lookback})', lookback) self._roc.updated += self._on_rate_of_change_updated self._window = RollingWindow[IndicatorDataPoint](period) def should_reset(self): # Don't need to reset when the `window` only contain data from the insight.magnitude return self._window.samples < self._window.size * 2 def clear_history(self): self._roc.reset() self._window.reset() def reset(self): self._roc.updated -= self._on_rate_of_change_updated self.clear_history() def update(self, time, value): return self._roc.update(time, value) def _on_rate_of_change_updated(self, roc, value): if roc.is_ready: self._window.add(value) def add(self, time, value): item = IndicatorDataPoint(self._symbol, time, value) self._window.add(item) # Get symbols' returns, we use simple return according to # Meucci, Attilio, Quant Nugget 2: Linear vs. Compounded Returns – Common Pitfalls in Portfolio Management (May 1, 2010). # GARP Risk Professional, pp. 49-51, April 2010 , Available at SSRN: https://ssrn.com/abstract=1586656 @property def return_(self): return pd.Series( data = [x.value for x in self._window], index = [x.end_time for x in self._window]) @property def is_ready(self): return self._window.is_ready
#region imports from AlgorithmImports import * #endregion # 05/25/2023 -Set the universe data normalization mode to raw # -Added warm-up # -Made the following updates to the portfolio construction model: # - Added IsRebalanceDue to only rebalance after warm-up finishes and there is quote data # - Reset the MeanVarianceSymbolData indicator and window when corporate actions occur # - Changed the minimum portfolio weight to be algorithm.Settings.MinAbsolutePortfolioTargetPercentage*1.1 to avoid errors # -Adjusted the history requests to use scaled raw data normalization # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_587cc09bd82676a2ede5c88b100ef70b.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_fa3146d7b1b299f4fc23ef0465540be0.html