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