Overall Statistics |
Total Trades 9860 Average Win 0.09% Average Loss -0.07% Compounding Annual Return -99.802% Drawdown 56.200% Expectancy -0.238 Net Profit -55.565% Sharpe Ratio -2.395 Probabilistic Sharpe Ratio 0.000% Loss Rate 66% Win Rate 34% Profit-Loss Ratio 1.22 Alpha -1.072 Beta 0.313 Annual Standard Deviation 0.412 Annual Variance 0.169 Information Ratio -3.021 Tracking Error 0.417 Treynor Ratio -3.151 Total Fees $19720.20 Estimated Strategy Capacity $2500000.00 Lowest Capacity Asset RIBT VMDPBBYAJAZP Portfolio Turnover 734.40% |
# region imports from AlgorithmImports import * # endregion class HyperActiveYellowGreenFalcon(QCAlgorithm): # Daily Trade Volume Threshold VolumeThreshold = 2e6 # Daily Minutes Traded Threshold MinuteThreshold = 180 # Timedelta To Create Consolidator AvergCosolidatorTime = timedelta(minutes=60) # NOTE: Check the notebook, I selected these ranges based on the dataframe at the bottom # Tresholds For Leverage Leverage_Lower_Threshold = 0.01 Leverage_Upper_Threshold = 0.10 MyLeverage = 1 # No leverage thresholds Lower_Threshold = 0.01 Upper_Threshold = 0.10 DefaultLeverage = 1 # Maximum number of tickers to select NumberOfTickers = 20 # Ratios Top and Low Top: # Take 50% of number of tickers with the greater Close Ratio # and the other 50% with the lowest NTopRatio = 1 NLowRatio = 0 # Max number of open positions MaxPositions = 20 def Initialize(self) -> None: self.SetStartDate(2021, 9, 17) # Set Start Date self.SetCash(100000) # Set Strategy Cash ## Universe Settings # This allow a minute data feed resolution self.UniverseSettings.Resolution = Resolution.Minute # Add coarse universe selection model self.AddUniverseSelection(VolumeUniverseSelectionModel(volume_th=self.VolumeThreshold)) # Custom Alpha Model self.AddAlpha(CloseRatioAlphaModel(NumberOfTickers=20, MaxPositions=self.MaxPositions, AvergCosolidatorTime = self.AvergCosolidatorTime, MinuteThreshold=self.MinuteThreshold, Leverage_Lower_Threshold = self.Leverage_Lower_Threshold, Leverage_Upper_Threshold = self.Leverage_Upper_Threshold, Lower_Threshold = self.Lower_Threshold, Upper_Threshold = self.Upper_Threshold, MyLeverage = self.MyLeverage, DefaultLeverage = self.DefaultLeverage, NTopRatio = self.NTopRatio, NLowRatio = self.NLowRatio ) ) # EqualWeightingPortfolioConstructionModel # Set the rebalance to the same period that we are consolidating bars self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(rebalance = self.AvergCosolidatorTime)) # Keeps record of the SelctedData instances assigned self.SecuritiesTracker = dict() ## SECURITIES LOGIC: CREATION, INDICATORS, UPDATE, TACKING def ManageAdded(self, added:list): ''' Logic for securities added. Create the SymbolData objects that track the added securities. Inputs: added [list]: List of added securities ''' for security in added: if self.SecuritiesTracker.get(security.Symbol) is None: # Create SymbolData object self.SecuritiesTracker[security.Symbol] = None def ManageRemoved(self,removed:list): ''' Logic for securities removed. Remove the SymbolData objects. Inputs: removed [list]: List of removed securities ''' for security in removed: # Don't track anymore if self.Portfolio[security.Symbol].Invested: self.Liquidate(security.Symbol) self.SecuritiesTracker.pop(security.Symbol, None) def OnSecuritiesChanged(self,changes: SecurityChanges) -> None: # Gets an object with the changes in the universe # For the added securities we create a SymbolData object that allows # us to track the orders associated and the indicators created for it. self.ManageAdded(changes.AddedSecurities) # The removed securities are liquidated and removed from the security tracker. self.ManageRemoved(changes.RemovedSecurities) ## POSITION MANAGEMENT def OnData(self, data:Slice): for symbol, tracker in self.SecuritiesTracker.items(): exchange_hours = self.Securities[symbol].Exchange.Hours within_half = (exchange_hours.GetNextMarketClose(self.Time, extendedMarket=False) - self.Time).seconds <= 60*30 if data.ContainsKey(symbol) and exchange_hours.IsOpen(self.Time, extendedMarket=False) and within_half: # Market in last 30 minutes if self.Portfolio[symbol].Invested: self.Liquidate(symbol) ## -------------------------------------------------------------------------- UNIVERSE SELECTION MODEL --------------------------------------------------------------------- ## ## Due to the required minute data resolution for security selection ## It was necesary to implement the Number of traded minutes on the main algorithm class VolumeUniverseSelectionModel(FineFundamentalUniverseSelectionModel): # Reference to FineFundamental Universes ->: https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/universe-selection/fundamental-universes def __init__(self, volume_th:float, universe_settings: UniverseSettings = None) -> None: super().__init__(self.SelectCoarse, self.SelectFine, universe_settings) # Volume threshold self.volume_th = volume_th # Store the securities with SelectionData objects created self.Windows = dict() def SelectCoarse(self, coarse: List[CoarseFundamental]) -> List[Symbol]: # Reference to Coarse filters ->: https://www.quantconnect.com/docs/v2/writing-algorithms/universes/equity #1. Filt to securities with fundamental data tickers = [c for c in coarse if c.HasFundamentalData] #2. Filt securities with trade volume greater than <volume_th> by_volume = filter(lambda x: x.Volume > self.volume_th, tickers) return [c.Symbol for c in by_volume] def SelectFine(self, fine: List[FineFundamental]) -> List[Symbol]: # Reference to Fine filters ->: https://www.quantconnect.com/docs/v2/writing-algorithms/datasets/morningstar/us-fundamental-data#01-Introduction #1. Filt markets: Docs reference ->: https://www.quantconnect.com/docs/v2/writing-algorithms/datasets/morningstar/us-fundamental-data#06-Data-Point-Attributes return [x.Symbol for x in fine if x.SecurityReference.ExchangeId in ["NAS", "NYS"]] ## -------------------------------------------------------------------------- ALPHA MODEL -------------------------------------------------------------------------- ## class CloseRatioAlphaModel(AlphaModel): '''Base de insights on the Close prices ratio between time/(time-1)''' def __init__(self, NumberOfTickers: int, MaxPositions:int, AvergCosolidatorTime: timedelta, MinuteThreshold:float, Leverage_Lower_Threshold:float, Leverage_Upper_Threshold: float, Lower_Threshold:float, Upper_Threshold: float, MyLeverage:float = 2, DefaultLeverage:int=1, NTopRatio:float= 0.5, NLowRatio:float=0.5): ''' Inputs: - NumberOfTickers: Maximum number of tickers to select. The real selected amount depends of the filters as well. - NTopRatio and NLowRatio: Should sum up to 1 as a percentage of Number of tickers. Example: If NumberOfTickers = 10 and NTopRatio, NLowRatio equals to 0.5 each, 5 tickers will be selected for long and 5 for short - AvergCosolidatorTime: Timedelta To Create Consolidator - MinuteThreshold: Daily Minutes Traded Threshold - Leverage_Lower_Threshold - Leverage_Upper_Threshold: Create the range for leveraged insights. - Lower_Threshold - Upper_Threshold: Create the range for normal insights. - MyLeverage: Leverage for leveraged insights. ''' # NumberOfTickers assert NTopRatio + NLowRatio <= 1, 'NTopRatio and NLowRatio should sum up to 1 as a percentage of Number of tickers' self.NTopRatio = int(NumberOfTickers * NTopRatio) self.NLowRatio = int(NumberOfTickers * NLowRatio) # For securities selection self.AvergCosolidatorTime = AvergCosolidatorTime self.MinuteThreshold = MinuteThreshold # Set ranges self.Leverage_Lower_Threshold = Leverage_Lower_Threshold self.Leverage_Upper_Threshold = Leverage_Upper_Threshold self.Lower_Threshold = Lower_Threshold self.Upper_Threshold = Upper_Threshold # Leverages self.MyLeverage = MyLeverage self.DefaultLeverage = DefaultLeverage # Keeps record of the SelctedData instances assigned self.SecuritiesTracker = dict() # Initial reference of time self.LastResizeTime = datetime(year=1,month=1,day=1) # self.MaxPositions = MaxPositions ## SECURITIES LOGIC: CREATION, INDICATORS, UPDATE, TACKING def ManageAdded(self, algorithm:QCAlgorithm, added): ''' Logic for securities added. Create the SymbolData objects that track the added securities. Inputs: added [list]: List of added securities ''' for security in added: if self.SecuritiesTracker.get(security.Symbol) is None: # Create SymbolData object self.SecuritiesTracker[security.Symbol] = SelectionData(algorithm, security, algorithm.get_Time, self.AvergCosolidatorTime, self.MinuteThreshold) def ManageRemoved(self, algorithm:QCAlgorithm, removed): ''' Logic for securities removed. Remove the SymbolData objects. Inputs: removed [list]: List of removed securities ''' for security in removed: # Don't track anymore algorithm.SubscriptionManager.RemoveConsolidator(security.Symbol, self.SecuritiesTracker[security.Symbol].consolidator) self.SecuritiesTracker.pop(security.Symbol, None) def OnSecuritiesChanged(self, algorithm:QCAlgorithm, changes: SecurityChanges) -> None: # Gets an object with the changes in the universe # For the added securities we create a SymbolData object that allows # us to track the orders associated and the indicators created for it. self.ManageAdded(algorithm, changes.AddedSecurities) # The removed securities are liquidated and removed from the security tracker. self.ManageRemoved(algorithm, changes.RemovedSecurities) ## POSITION MANAGEMENT def CheckLeveraged(self,value:float, sign:int): return sign*self.Leverage_Lower_Threshold < value < sign*self.Leverage_Upper_Threshold def CheckTreshold(self, value:float, sign:int): if sign: return sign*self.Lower_Threshold < value < sign*self.Upper_Threshold return sign*self.Lower_Threshold > value > sign*self.Upper_Threshold def UpdateWindow(self, algorithm: QCAlgorithm, data:Slice): for symbol, tracker in self.SecuritiesTracker.items(): exchange_hours = algorithm.Securities[symbol].Exchange.Hours within_half = (exchange_hours.GetNextMarketClose(algorithm.Time, extendedMarket=False) - algorithm.Time).seconds <= 60*30 if data.ContainsKey(symbol) and exchange_hours.IsOpen(algorithm.Time, extendedMarket=False) and not(within_half): # Market in last 30 minutes # Update Tracked Windows tracker.UpdateWindow(data.Bars[symbol]) def UpdateState(self, algorithm: QCAlgorithm, data:Slice): rank = {} for symbol, tracker in self.SecuritiesTracker.items(): exchange_hours = algorithm.Securities[symbol].Exchange.Hours within_half = (exchange_hours.GetNextMarketClose(algorithm.Time, extendedMarket=False) - algorithm.Time).seconds <= 60*30 if data.ContainsKey(symbol) and exchange_hours.IsOpen(algorithm.Time, extendedMarket=False) and not(within_half): # Market in last 30 minutes # Filt by the Daily Minutes Traded Threshold if tracker.IsReady and tracker.IsSelected(exchange_hours.GetPreviousTradingDay(algorithm.Time).day): rank[symbol] = tracker.LastRatio return sorted(rank.items(), key=lambda item: item[1],reverse=True) def TopRanked(self,algorithm: QCAlgorithm, rank:list): insights = {} for symbol,ratio in rank: if self.CheckLeveraged(ratio, 1) or (((algorithm.Time.today().weekday() == 0) or (algorithm.Time.hour == 11)) and self.CheckTreshold(ratio, 1)): insights[symbol] = Insight.Price(symbol, self.AvergCosolidatorTime, direction=InsightDirection.Up, magnitude=ratio, confidence=1) # Change Leverage algorithm.Securities[symbol].SetLeverage(self.MyLeverage) elif self.CheckTreshold(ratio, 1): insights[symbol] = Insight.Price(symbol, self.AvergCosolidatorTime, direction=InsightDirection.Up, magnitude=ratio, confidence=0.8) # Change Leverage algorithm.Securities[symbol].SetLeverage(self.DefaultLeverage) return insights def TopLowRanked(self, algorithm: QCAlgorithm, rank:list): insights = {} for symbol,ratio in rank: if self.CheckLeveraged(ratio, -1) or (((algorithm.Time.today().weekday() == 0) or (algorithm.Time.hour == 11)) and self.CheckTreshold(ratio, -1)): insights[symbol] = Insight.Price(symbol, self.AvergCosolidatorTime, direction=InsightDirection.Down, magnitude=ratio, confidence=1) # Change Leverage algorithm.Securities[symbol].SetLeverage(self.MyLeverage) elif self.CheckTreshold(ratio, -1): insights[symbol] = Insight.Price(symbol, self.AvergCosolidatorTime, direction=InsightDirection.Down, magnitude=ratio, confidence=0.8) # Change Leverage algorithm.Securities[symbol].SetLeverage(self.DefaultLeverage) return insights def GetOpenPositions(self,algorithm:QCAlgorithm): positions = [] for symbol in self.SecuritiesTracker.keys(): if abs(algorithm.Portfolio[symbol].Quantity) > 0: positions.append(symbol) return positions def ResizePositions(self,algorithm:QCAlgorithm, data:Slice): self.LastResizeTime = algorithm.Time rank = self.UpdateState(algorithm,data) lenght = len(rank) ## Create insights based on ranges # NOTE: The minimum between the target amount and the half of tickers will be selected top = {} topLow = {} if lenght >= 2: # Create Insights top = rank[:int(self.NTopRatio*lenght)] topLow = rank[::-1][:int(self.NLowRatio*lenght)] top = self.TopRanked(algorithm,top) topLow = self.TopLowRanked(algorithm,topLow) # NOTE: If there is only one ticket both conditions will be evaluated. elif rank: if self.NTopRatio >= self.NLowRatio: top = self.TopRanked(algorithm, rank) elif not(top) or self.NTopRatio <= self.NLowRatio: topLow = self.TopLowRanked(algorithm,rank) insights = [] ## Select based on created positions and MaxPositions # Gives priority to the grater ratio if self.NTopRatio >= self.NLowRatio: insights += list(top.values()) insights = insights[:int(min(len(insights),self.MaxPositions)*self.NTopRatio)] insights += list(topLow.values()) insights = insights[:self.MaxPositions] else: insights += list(topLow.values()) insights = insights[:int(min(len(insights),self.MaxPositions)*self.NTopRatio)] insights += list(top.values()) insights = insights[:self.MaxPositions] openPos = self.GetOpenPositions(algorithm) # Flatten symbols with Open Positions and not selected in this round insights += [Insight.Price(s, self.AvergCosolidatorTime, direction=InsightDirection.Flat, magnitude=0, confidence=1) for s in openPos if top.get(s) is None and topLow.get(s) is None] return insights def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: '''Updates this alpha model with the latest data from the algorithm. This is called each time the algorithm receives data for subscribed securities Args: algorithm: The algorithm instance data: The new data available Returns: The new insights generated''' insights =[] if (algorithm.Time - self.LastResizeTime) >= self.AvergCosolidatorTime: # Perform Only every AvergCosolidatorTime insights += self.ResizePositions(algorithm,data) return insights ## -------------------------------------------------------------------------- SYMBOL DATA: HELPER --------------------------------------------------------------------- ## class SelectionData: # Reference to RollingWindows ->: https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/rolling-window # Reference to Type of Consolidator used ->: https://www.quantconnect.com/docs/v2/writing-algorithms/consolidating-data/consolidator-types/time-period-consolidators ## INITIALIZATION: def __init__(self, algorithm: QCAlgorithm, security, time: QCAlgorithm.get_Time, consolidator_time:timedelta, minute_th:int): self.Security = security self.Symbol = security.Symbol # Reference to main algorithm time self.get_Time = time # Minutes traded threshold self.minute_th = minute_th # Window: Average Trading Hours assumed as six # Two days are stored due to the possible multiple calls intra-day self.Window = RollingWindow[TradeBar](12*60) self.WarmUpWindow(algorithm,12*60) # Create Average Close Ratio self.ConsolidatorTime = consolidator_time.seconds//60 self.TimeWindow = RollingWindow[float]((consolidator_time.seconds//60)*2) self.CloseRatioWindow = RollingWindow[float]((consolidator_time.seconds//60)*4) # Create a 30 Minutes consolidated bar self.InitConsolidator(algorithm) # Register moving average indicator self.sma = SimpleMovingAverage('SMA'+self.Symbol.Value, 20) # DayTracked will help us reduce compute overhead self.DayTracked = {} @property def Time(self)-> datetime: # Allow access to the Time object directly return self.get_Time() def WarmUpWindow(self, algorithm: QCAlgorithm, period:int): # Manually WarmUp Rolling Window: This is not efficiente # but allows a faster response time to an addes security history_trade_bar = algorithm.History[TradeBar](self.Symbol, period, Resolution.Minute) for trade_bar in history_trade_bar: self.Window.Add(trade_bar) def InitConsolidator(self, algorithm: QCAlgorithm): self.consolidator = TradeBarConsolidator(timedelta(minutes=1)) self.consolidator.DataConsolidated += self.UpdateIndicators algorithm.SubscriptionManager.AddConsolidator(self.Symbol, self.consolidator) def UpdateIndicators(self, sender: object, consolidated_bar: TradeBar) -> None: self.Window.Add(consolidated_bar) self.sma.Update(consolidated_bar.EndTime, consolidated_bar.Close) self.TimeWindow.Add(self.sma.Current.Value) # Create the ratio if self.TimeWindow.IsReady: # Bar[t]/Bar[t-1] self.CloseRatioWindow.Add((self.TimeWindow[0]/self.TimeWindow[self.ConsolidatorTime])-1) ## POSITION MANAGEMENT @property def IsReady(self): return self.Window.IsReady and self.TimeWindow.IsReady and self.CloseRatioWindow.IsReady @property def LastRatio(self): if not(self.IsReady): return 0 return self.CloseRatioWindow[0] def IsSelected(self, past_day) -> bool: if not self.IsReady: return False tracked = self.DayTracked.get(past_day) if tracked is not None: return tracked else: # Reset self.DayTracked = {} # If past day meets the minutes traded threshold requirment # Filt to the past trade day and Trade Volume > 0 my_filter = lambda bar: (bar.EndTime.day == past_day) and (bar.Volume > 0) # Number of bars with at least some trade in the las day self.DayTracked[past_day] = len(list(filter(my_filter, self.Window))) >= self.minute_th return self.DayTracked[past_day]