Overall Statistics |
Total Orders 1478 Average Win 0.01% Average Loss -0.04% Compounding Annual Return 2.951% Drawdown 12.800% Expectancy -0.466 Start Equity 1000000 End Equity 1029644.45 Net Profit 2.964% Sharpe Ratio -0.255 Sortino Ratio -0.28 Probabilistic Sharpe Ratio 20.546% Loss Rate 59% Win Rate 41% Profit-Loss Ratio 0.29 Alpha 0 Beta 0 Annual Standard Deviation 0.093 Annual Variance 0.009 Information Ratio 0.317 Tracking Error 0.093 Treynor Ratio 0 Total Fees $3481.03 Estimated Strategy Capacity $16000.00 Lowest Capacity Asset GDL TPNAWDP49CO5 Portfolio Turnover 1.50% |
#region imports from AlgorithmImports import * import pandas as pd from dateutil.relativedelta import relativedelta #endregion class ROCAndNearnessAlphaModel(AlphaModel): """ This class ranks securities in the universe by their historical rate of change and nearness to trailing highs. """ _securities = [] _month = -1 def __init__(self, algorithm, roc_lookback_months=6, nearness_lookback_months=12, holding_months=6, pct_long=50): """ Input: - algorithm The algorithm instance - roc_lookback_months Number of trailing months to calculate the rate of change over (> 0) - nearness_lookback_months Number of trailing months to calculate the nearness factor over (> 0) - holding_months Number of months to hold positions (> 0) - pct_long The percentage of the universe we go long (0 < pct_long <= 100) """ if roc_lookback_months <= 0 or nearness_lookback_months <= 0 or holding_months <= 0: algorithm.quit(f"Requirement violated: roc_lookback_months > 0 and nearness_lookback_months > 0 and holding_months > 0") if pct_long <= 0 or pct_long > 100: algorithm.quit(f"Requirement violated: 0 < pct_long <= 100") self._algorithm = algorithm self._roc_lookback_months = roc_lookback_months self._nearness_lookback_months = nearness_lookback_months self._lookback_months = max(roc_lookback_months, nearness_lookback_months) self._holding_months = holding_months self._pct_long = pct_long def update(self, algorithm, data): # Reset price history when corporate actions occur for symbol in set(data.splits.keys() + data.dividends.keys()): self._reset(algorithm.securities[symbol]) # Only emit insights when there is quote data, not when we get corporate action or alt data if data.quote_bars.count == 0: return [] # Rebalance monthly if self._month == algorithm.time.month: return [] self._month = algorithm.time.month # Rank symbols. A higher rank (or index in the list) is associated with relatively greater rate of change # over the roc lookback window and a closer closing price to the max price in the nearness lookback window ranking_df = pd.DataFrame() for security in self._securities: if data.contains_key(security.symbol) and self._is_ready(security) and security.is_tradable and security.price > 0: row = pd.DataFrame({'ROC': self._roc(security), 'Nearness': self._nearness(security)}, index=[security.symbol]) ranking_df = pd.concat([ranking_df, row]) ranked_symbols = ranking_df.rank().sum(axis=1).sort_values().index # Generate insights insights = [] num_long = int(len(ranked_symbols) * (self._pct_long / 100)) if num_long > 0: for symbol in ranked_symbols[-num_long:]: insights.append(Insight.price(symbol, Expiry.END_OF_MONTH, InsightDirection.UP)) return insights def on_securities_changed(self, algorithm, changes): for security in changes.added_securities: self._securities.append(security) # Warm up history self._warm_up_history(security) # Setup indicator consolidator self._set_up_consolidator(security) for security in changes.removed_securities: if security in self._securities: self._securities.remove(security) algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator) def _warm_up_history(self, security): """ Warms up the `historical_prices_by_symbol` dictionary with historical data as far back as the earliest lookback window. Input: - security Security that needs history warm up """ symbol = security.symbol # Get the historical data start_lookback = Expiry.END_OF_MONTH(self._algorithm.time) - relativedelta(months=self._lookback_months + 1) history = self._algorithm.history(symbol, start_lookback, self._algorithm.time, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW) # Rollback history timestamp by 1 day to ensure accurate monthly ROC values if history.shape[0] > 0: history = history.unstack(level=0) history = history.set_index(history.index.map(lambda x: x - timedelta(days=1))).stack().swaplevel() security.history = history.loc[symbol, ['open', 'high', 'close']] if symbol in history.index else pd.DataFrame() def _set_up_consolidator(self, security): security.consolidator = TradeBarConsolidator(timedelta(1)) security.consolidator.data_consolidated += self._consolidation_handler self._algorithm.subscription_manager.add_consolidator(security.symbol, security.consolidator) def _consolidation_handler(self, sender, consolidated): """ Updates the rolling lookback window with the latest data. Inputs - sender Function calling the consolidator - consolidated Tradebar representing the latest completed trading day """ security = self._algorithm.securities[consolidated.symbol] # Add new data point to history if consolidated.time not in security.history.index: row = pd.DataFrame({'open': consolidated.open, 'high': consolidated.high, 'close': consolidated.close}, index=[consolidated.time]) security.history = pd.concat([security.history, row]) # Remove expired history start_lookback = Expiry.END_OF_MONTH(self._algorithm.time) - relativedelta(months=self._lookback_months + 1) security.history = security.history[security.history.index >= start_lookback] def _reset(self, security): self._warm_up_history(security) # Reset the consolidator to clear our pricing data from before the corporate action self._algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator) self._set_up_consolidator(security) def _is_ready(self, security): """ Boolean to signal if the security has sufficient history to fill the ROC lookback window. """ return self._get_lookback(security, self._roc_lookback_months).shape[0] > 1 def _roc(self, security): """ Calculates the rate of change over the ROC lookback window. """ lookback = self._get_lookback(security, self._roc_lookback_months) start_price = lookback.iloc[0].open end_price = lookback.iloc[-1].close return (end_price - start_price) / start_price def _nearness(self, security): """ Calculates how close the closing price of the nearness lookback window was to its maximum price. """ lookback = self._get_lookback(security, self._nearness_lookback_months) return lookback.iloc[-1].close / lookback.high.max() def _get_lookback(self, security, num_months): """ Slices the historical data into the trailing `num_months` months. Input: - security The security for which to check history - num_months Number of trailing months in the lookback window Returns DataFrame containing data for the lookback window. """ start_lookback = Expiry.END_OF_MONTH(self._algorithm.time) - relativedelta(months=num_months + 1) end_lookback = Expiry.END_OF_MONTH(self._algorithm.time) - relativedelta(months=1) return security.history[(security.history.index >= start_lookback) & (security.history.index < end_lookback)]
#region imports from AlgorithmImports import * from universe import AssetManagementUniverseSelection from alpha import ROCAndNearnessAlphaModel from portfolio import NetDirectionWeightedPortfolioConstructionModel #endregion class AssetManagementFirmMomentum(QCAlgorithm): _undesired_symbols_from_previous_deployment = [] _checked_symbols_from_previous_deployment = False def initialize(self): holding_months = self.get_parameter("holding_months", 6) # Set backtest start date and warm-up period WARM_UP_FOR_LIVE_MODE = self.get_parameter("warm_up_for_live_mode", 0) MORNING_STAR_LIVE_MODE_HISTORY = timedelta(30) # US Fundamental Data by Morningstar is limited to the last 30 days if self.live_mode: self.set_warm_up(MORNING_STAR_LIVE_MODE_HISTORY, Resolution.DAILY) else: # Backtest mode if WARM_UP_FOR_LIVE_MODE: # Need to run a backtest before you can deploy this algorithm live self.set_start_date(2023, 3, 1) self.set_end_date(2024, 3, 1) else: # Regular backtest self.set_start_date(2023, 3, 1) self.set_end_date(2024, 3, 1) self.set_warm_up(timedelta(31 * holding_months), Resolution.DAILY) self.set_cash(1_000_000) self.settings.minimum_order_margin_portfolio_percentage = 0 self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.set_security_initializer(CustomSecurityInitializer(self)) self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW self.universe_settings.schedule.on(self.date_rules.month_start()) self.set_universe_selection(AssetManagementUniverseSelection(self, self.universe_settings)) self.set_alpha(ROCAndNearnessAlphaModel( self, self.get_parameter("roc_lookback_months", 6), self.get_parameter("nearness_lookback_months", 12), holding_months, self.get_parameter("pct_long", 50) )) self.set_portfolio_construction(NetDirectionWeightedPortfolioConstructionModel()) self.add_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) 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) class CustomSecurityInitializer(BrokerageModelSecurityInitializer): def __init__(self, algorithm): self._algorithm = algorithm super().__init__(algorithm.brokerage_model, SecuritySeeder.NULL) def initialize(self, security: Security) -> None: # First, call the superclass definition # This method sets the reality models of each security using the default reality models of the brokerage model super().initialize(security) # Seed the security with Daily bar bars = self._algorithm.history[TradeBar](security.symbol, 5, Resolution.DAILY) for bar in bars: security.set_market_price(bar)
#region imports from AlgorithmImports import * #endregion class NetDirectionWeightedPortfolioConstructionModel(PortfolioConstructionModel): """ This PCM allocates its portfolio based on the net direction of insights for all the symbols. A symbol that has two active insights with an up direction will have twice the allocation than a symbol with only one. Additionally, a symbol that has an up active insight and a down active insight will have no position. This PCM doesn't liquidate securities when they are removed from the universe. If it's removed from the universe, it will remain invested until all the security's insights expire. """ _insights = [] _month = -1 def create_targets(self, algorithm, insights): """ Called each time the alpha model emits a list of insights. Input: - algorithm Algorithm instance running the backtest - insights List of insights Returns a list of portfolio targets. """ for i in insights: self._insights.append(i) if algorithm.is_warming_up or algorithm.current_slice.quote_bars.count == 0 or self._month == algorithm.time.month: return [] self._month = algorithm.time.month # Remove insights of delisted symbols self._insights = [i for i in self._insights if algorithm.securities[i.symbol].is_tradable] # Classify insights active_insights = [] expired_insights = [] while (len(self._insights) > 0): insight = self._insights.pop() (active_insights if insight.is_active(algorithm.utc_time) else expired_insights).append(insight) self._insights = active_insights # Liquidate symbols that have expired insights with no active insights active_symbols = set([i.symbol for i in active_insights]) expired_symbols = set([i.symbol for i in expired_insights]) liquidate_symbols = expired_symbols.difference(active_symbols) portfolio_targets = [PortfolioTarget.percent(algorithm, symbol, 0) for symbol in liquidate_symbols] # Get net direction by symbol and total number of directional insights net_direction_by_symbol, num_directional_insights = self._get_net_direction(active_insights) # Create portfolio targets for active symbols for symbol, net_direction in net_direction_by_symbol.items(): percent = 0 if num_directional_insights == 0 else net_direction / num_directional_insights portfolio_targets.append( PortfolioTarget.percent(algorithm, symbol, percent) ) return portfolio_targets def _get_net_direction(self, insights): """ Determines the net direction of each symbol and the number of active directional insights. Input: - insights A list of active insights Returns a dictionary showing the net direction of each symbol, and the number of directional insights. """ net_direction_by_symbol = {} num_directional_insights = 0 for insight in insights: symbol = insight.symbol direction = insight.direction if symbol in net_direction_by_symbol: net_direction_by_symbol[symbol] += direction else: net_direction_by_symbol[symbol] = direction num_directional_insights += abs(direction) return net_direction_by_symbol, num_directional_insights def on_securities_changed(self, algorithm, changes): pass
#region imports from AlgorithmImports import * #endregion # 08/15/2023: -Adjusted algorithm so that it can warm-up properly even though US Fundamental data is limited to last 30-days in warm-up # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_ec4dc1c6b728147dbbd847040e91eb33.html
#region imports from AlgorithmImports import * #endregion class AssetManagementUniverseSelection(FundamentalUniverseSelectionModel): """ This universe selection model refreshes monthly to contain US securities in the asset management industry. """ _fine_symbols_by_date = {} def __init__(self, algorithm: QCAlgorithm, universe_settings: UniverseSettings = None): def select(fundamental): fine = [x for x in fundamental if x.has_fundamental_data] key = f"{algorithm.time.year}{algorithm.time.month}" if key in self._fine_symbols_by_date: symbols = [algorithm.symbol(security_id) for security_id in self._fine_symbols_by_date[key]] else: if not list(fine): algorithm.quit("Insufficient fundamental data in the ObjectStore. Run a backtest before deploying live.") return [] symbols = [f.symbol for f in fine if f.asset_classification.morningstar_industry_code == MorningstarIndustryCode.ASSET_MANAGEMENT] self._fine_symbols_by_date[key] = [str(symbol) for symbol in symbols] algorithm.object_store.save(self.OBJECT_STORE_KEY, json.dumps(self._fine_symbols_by_date)) return symbols super().__init__(None, select, universe_settings) self.OBJECT_STORE_KEY = str(algorithm.project_id) if algorithm.live_mode: if not algorithm.object_store.contains_key(self.OBJECT_STORE_KEY): algorithm.quit("No fundamental data in the ObjectStore. Run a backtest before deploying live.") return self._fine_symbols_by_date = json.loads(algorithm.object_store.read(self.OBJECT_STORE_KEY))