Overall Statistics |
Total Trades 3109 Average Win 0.56% Average Loss -0.53% Compounding Annual Return 15.419% Drawdown 32.700% Expectancy 0.065 Net Profit 54.788% Sharpe Ratio 0.562 Probabilistic Sharpe Ratio 16.212% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.06 Alpha 0 Beta 0 Annual Standard Deviation 0.246 Annual Variance 0.06 Information Ratio 0.562 Tracking Error 0.246 Treynor Ratio 0 Total Fees $5288.12 Estimated Strategy Capacity $99000000.00 Lowest Capacity Asset LMT R735QTJ8XC9X Portfolio Turnover 64.78% |
from AlgorithmImports import * import talib from datetime import timedelta from QuantConnect.Data.UniverseSelection import * from QuantConnect.Algorithm.Framework.Alphas import * class TTMAlphaModel(AlphaModel): def __init__(self, period=20, BB_mult=2, ATR_mult = 1.5, rsi_period=14, rsi_overbought=70, rsi_oversold=30): self.period = period self.k = BB_mult self.ATR_mult = ATR_mult self.rsi_period = rsi_period self.rsi_overbought = rsi_overbought self.rsi_oversold = rsi_oversold self.last_close = {} self.bb = BollingerBands(period, BB_mult) self.kch = KeltnerChannels(period, ATR_mult, MovingAverageType.Exponential) self.atr = AverageTrueRange(period, MovingAverageType.Exponential) self.prev_squeeze = {} def Update(self, algorithm, data): insights = [] # Get current time current_time = algorithm.Time # ZO --- WEAK slipapge modelling, but best we have available at the moment. # algorithm.Debug(f'Time: {current_time}') algo = algorithm qb = algo.History(algo.Securities.Keys, 100, Resolution.Minute) for symbol, security in algorithm.Securities.items(): # security.SetSlippageModel(VolumeShareSlippageModel()) security.SetSlippageModel(ConstantSlippageModel(.0005)) # .05% slippage (default) try: df = qb.loc[symbol] spread_pct = (df.askclose.mean() - df.bidclose.mean()) / df.askclose.mean() security.SetSlippageModel(ConstantSlippageModel(spread_pct)) except: pass # Get current universe universe = algorithm.UniverseManager.ActiveSecurities # Get historical data for universe # Calculate TTM Squeeze and RSI indicators for each security in universe for security in universe: history = algorithm.History(security.Value.Symbol, 30, Resolution.Daily) bar = data.Bars.get(security.Value.Symbol) if bar: self.bb.Update(bar.EndTime, bar.Close) self.kch.Update(bar) self.atr.Update(bar) if not history.empty and self.bb.IsReady and self.kch.IsReady and self.atr.IsReady: # Get last close price current_close = data[security.Value.Symbol].Close # Calculate Bollinger Bands, Keltner Channels, and True Range bb_upper, _, bb_lower = talib.BBANDS(history['close'], timeperiod=self.period) kama = talib.KAMA(history['close'], timeperiod=self.period) # Calculate ATR atr = talib.ATR(history['high'], history['low'], history['close'], timeperiod=20) mom = talib.MOM(history['close'], timeperiod=20) if len(mom) < 5: continue smoothed_mom = mom.rolling(5).mean() kc_upper = kama + (self.ATR_mult * atr) kc_lower = kama - (self.ATR_mult * atr) # Calculate TTM Squeeze if bb_upper[-1] < kc_upper[-1] and bb_lower[-1] > kc_lower[-1]: squeeze = True else: squeeze = False if bb_upper[-2] < kc_upper[-2] and bb_lower[-2] > kc_lower[-2]: prev_squeeze = True else: prev_squeeze = False mom_bullish = smoothed_mom[-1] > smoothed_mom[-2] and smoothed_mom[-1] > 0 and smoothed_mom[-2] > 0 #Blue mom_bearish = smoothed_mom[-1] < smoothed_mom[-2] and smoothed_mom[-1] < 0 and smoothed_mom[-2] < 0 #Red mom_bullish_stop = smoothed_mom[-1] < smoothed_mom[-2] #Dark Blue mom_bearish_Stop = smoothed_mom[-1] > smoothed_mom[-2] #Yellow # # Calculate RSI # rsi = talib.RSI(history['close'], timeperiod=self.rsi_period) # overbought = rsi[-1] > self.rsi_overbought # oversold = rsi[-1] < self.rsi_oversold # stop = rsi[-1] < self.rsi_overbought and rsi[-1] > self.rsi_oversold # check for TTM Squeeze and mom is momentum indicator if mom_bullish: if squeeze and prev_squeeze: insights.append(Insight.Price(security.Value.Symbol, timedelta(30), InsightDirection.Up)) elif mom_bearish: if squeeze and prev_squeeze: insights.append(Insight.Price(security.Value.Symbol, timedelta(30), InsightDirection.Down)) if algorithm.Portfolio[security.Value.Symbol].Invested: if algorithm.Portfolio[security.Value.Symbol].IsLong and mom_bullish_stop: insights.append(Insight.Price(security.Value.Symbol, timedelta(1), InsightDirection.Flat)) #algorithm.Liquidate(security.Value.Symbol.Value, "Liquidated exit short") elif algorithm.Portfolio[security.Value.Symbol].IsShort and mom_bearish_Stop: insights.append(Insight.Price(security.Value.Symbol, timedelta(1), InsightDirection.Flat)) #algorithm.Liquidate(security.Value.Symbol.Value, "Liquidated exit short") # Update last_close self.last_close[security] = current_close #self.prev_squeeze[security.Value.Symbol] = squeeze return insights class TTMAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 3, 15) self.SetEndDate(2023, 4, 1) self.SetCash(100000) self.maxDD_security = 0.15 # Universe selection self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFunction) # Alpha model self.SetAlpha(TTMAlphaModel()) # Portfolio construction and risk management self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(self.maxDD_security)) self.Settings.RebalancePortfolioOnInsightChanges = True self.Settings.RebalancePortfolioOnSecurityChanges = False self.Settings.Resolution = Resolution.Minute # ZO -- not working. #self.UniverseSettings.ExtendedMarketHours = False self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # ZO # # CHANGE to minute -- force it to use minute on Data 0 # self.symbol = self.AddEquity("SPY", Resolution.Minute).Symbol # # Set benchmark # self.SetBenchmark(self.symbol) def CoarseSelectionFunction(self, coarse): """ Perform coarse filters on universe. Called once per day. Returns all stocks meeting the desired criteria. Attributes available: .AdjustedPrice .DollarVolume .HasFundamentalData .Price -> always the raw price! .Volume """ # Get the highest volume stocks stocks = [x for x in coarse if x.HasFundamentalData] sorted_by_dollar_volume = sorted( stocks, key=lambda x: x.DollarVolume, reverse=True ) top = 50 symbols = [x.Symbol for x in sorted_by_dollar_volume[:top]] # Print universe details when live mode if self.LiveMode: self.MyLog(f"Coarse filter returned {len(symbols)} stocks.") return symbols