Overall Statistics |
Total Orders 569 Average Win 2.29% Average Loss -1.28% Compounding Annual Return 57.091% Drawdown 40.900% Expectancy 0.704 Start Equity 1000000 End Equity 9733221.76 Net Profit 873.322% Sharpe Ratio 1.318 Sortino Ratio 1.468 Probabilistic Sharpe Ratio 66.170% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 1.80 Alpha 0.303 Beta 1.15 Annual Standard Deviation 0.312 Annual Variance 0.097 Information Ratio 1.314 Tracking Error 0.242 Treynor Ratio 0.358 Total Fees $21480.27 Estimated Strategy Capacity $360000000.00 Lowest Capacity Asset LLY R735QTJ8XC9X Portfolio Turnover 6.14% |
#region imports from AlgorithmImports import * import numpy as np from collections import deque import statsmodels.api as sm import statistics as stat import pickle #endregion class Q2PlaygroundAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 3, 1) # Set Start Date self.SetEndDate(2024, 6, 1) # Set End Date self.SetCash(1000000) # Set Strategy Cash self.SetSecurityInitializer(BrokerageModelSecurityInitializer( self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices) )) ################################################################# self.universe_settings.resolution = Resolution.DAILY self._momp = {} # Dict of Momentum indicator keyed by Symbol self._lookback = 252 # Momentum indicator lookback period self._num_coarse = 200 # Number of symbols selected at Coarse Selection self._num_fine = 70 # Number of symbols selected at Fine Selection self._num_long = 5 # Number of symbols with open positions self._month = -1 self._rebalance = False self.current_holdings = set() # To track current holdings self.add_universe(self._coarse_selection_function, self._fine_selection_function) def _coarse_selection_function(self, coarse): '''Drop securities which have no fundamental data or have too low prices. Select those with highest by dollar volume''' if self._month == self.time.month: return Universe.UNCHANGED self._rebalance = True self._month = self.time.month selected = sorted([x for x in coarse if x.has_fundamental_data and x.price > 5], key=lambda x: x.dollar_volume, reverse=True) return [x.symbol for x in selected[:self._num_coarse]] def _fine_selection_function(self, fine): '''Select security with highest market cap''' selected = sorted(fine, key=lambda f: f.market_cap, reverse=True) return [x.symbol for x in selected[:self._num_fine]] def on_data(self, data): # Update the indicator for symbol, mom in self._momp.items(): mom.update(self.time, self.securities[symbol].close) if not self._rebalance: return # Selects the securities with highest momentum sorted_mom = sorted([k for k,v in self._momp.items() if v.is_ready], key=lambda x: self._momp[x].current.value, reverse=True) selected = sorted_mom[:self._num_long] new_holdings = set(selected) # Only rebalance if the new selection is different from current holdings if new_holdings != self.current_holdings: if len(selected) > 0: optimal_weights = self.optimize_portfolio(selected) self.adjust_portfolio(optimal_weights, selected) self.current_holdings = new_holdings self._rebalance = False def on_securities_changed(self, changes): # Clean up data for removed securities and Liquidate for security in changes.removed_securities: symbol = security.symbol if self._momp.pop(symbol, None) is not None: self.liquidate(symbol, 'Removed from universe') for security in changes.added_securities: if security.symbol not in self._momp: self._momp[security.symbol] = MomentumPercent(self._lookback) # Warm up the indicator with history price if it is not ready added_symbols = [k for k,v in self._momp.items() if not v.is_ready] history = self.history(added_symbols, 1 + self._lookback, Resolution.DAILY) history = history.close.unstack(level=0) for symbol in added_symbols: ticker = symbol.id.to_string() if ticker in history: for time, value in history[ticker].dropna().items(): item = IndicatorDataPoint(symbol, time.date(), value) self._momp[symbol].update(item) def optimize_portfolio(self, selected_symbols): short_lookback = 63 returns = self.history(selected_symbols, short_lookback, Resolution.DAILY)['close'].unstack(level=0).pct_change().dropna() n_assets = len(selected_symbols) n_portfolios = 1000 results = np.zeros((3, n_portfolios)) weights_record = [] for i in range(n_portfolios): weights = np.random.random(n_assets) weights /= np.sum(weights) portfolio_return = np.sum(returns.mean() * weights) * short_lookback portfolio_stddev = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * short_lookback, weights))) downside_stddev = np.sqrt(np.mean(np.minimum(0, returns).apply(lambda x: x**2, axis=0).dot(weights))) sortino_ratio = portfolio_return / downside_stddev results[0,i] = portfolio_return results[1,i] = portfolio_stddev results[2,i] = sortino_ratio weights_record.append(weights) best_sortino_idx = np.argmax(results[2]) return weights_record[best_sortino_idx] def adjust_portfolio(self, weights, selected): # First liquidate all positions for symbol in self.Portfolio.Keys: self.Liquidate(symbol) # Set holdings based on the optimal weights for i, symbol in enumerate(selected): self.SetHoldings(symbol, weights[i])