Overall Statistics |
Total Trades 10001 Average Win 0.01% Average Loss -0.01% Compounding Annual Return -99.891% Drawdown 17.800% Expectancy -0.598 Net Profit -17.824% Sharpe Ratio -3.173 Probabilistic Sharpe Ratio 0% Loss Rate 85% Win Rate 15% Profit-Loss Ratio 1.77 Alpha -1.038 Beta 0.528 Annual Standard Deviation 0.312 Annual Variance 0.098 Information Ratio -3.47 Tracking Error 0.311 Treynor Ratio -1.879 Total Fees $11176.91 Estimated Strategy Capacity $410000.00 Lowest Capacity Asset PBTS X3BYSC52NXPH Portfolio Turnover 632.49% |
# 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 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, 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, 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() ## 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): 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(self.Time, extendedMarket=False) - self.Time).seconds <= 60*30 if data.ContainsKey(symbol) and exchange_hours.IsOpen(self.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(self.Time, extendedMarket=False) - self.Time).seconds <= 60*30 if data.ContainsKey(symbol) and exchange_hours.IsOpen(self.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(self.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 (((self.Time.today().weekday() == 0) or (self.Time.hour == 11)) and self.CheckTreshold(ratio, 1)): insights.append(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.append(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 (((self.Time.today().weekday() == 0) or (self.Time.hour == 11)) and self.CheckTreshold(ratio, -1)): insights.append(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.append(Insight.Price(symbol, self.AvergCosolidatorTime, direction=InsightDirection.Down, magnitude=ratio, confidence=0.8)) # Change Leverage algorithm.Securities[symbol].SetLeverage(self.DefaultLeverage) 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''' self.Time = algorithm.Time insights =[] rank = self.UpdateWindow(algorithm,data) if self.Time.minute % 30 == 0: # Perform Only every half hour rank = self.UpdateState(algorithm,data) lenght = len(rank) # NOTE: The minimum between the target amount and the half of tickers will be selected if lenght >= 2: top = rank[:min(self.NTopRatio,lenght//2)] topLow = rank[::-1][:min(self.NLowRatio,lenght//2)] # Create Insights insights += self.TopRanked(algorithm,rank) insights += self.TopLowRanked(algorithm,rank) # NOTE: If there is only one ticket both conditions will be evaluated. else: insights += self.TopRanked(algorithm, rank) if not(insights): insights += self.TopLowRanked(algorithm,rank) 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.HalfHourWindow = RollingWindow[float](2) self.CloseRatioWindow = RollingWindow[float](4) # Create a 30 Minutes consolidated bar self.InitConsolidator(algorithm, consolidator_time) # 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, consolidator_time:timedelta): self.consolidator = TradeBarConsolidator(consolidator_time) self.consolidator.DataConsolidated += self.UpdateIndicators algorithm.SubscriptionManager.AddConsolidator(self.Symbol, self.consolidator) def UpdateIndicators(self, sender: object, consolidated_bar: TradeBar) -> None: # Record two half hour cosolidated bars self.HalfHourWindow.Add(consolidated_bar.Close) # Create the ratio if self.HalfHourWindow.IsReady: # Bar[t]/Bar[t-1] self.CloseRatioWindow.Add((self.HalfHourWindow[0]/self.HalfHourWindow[1])-1) ## POSITION MANAGEMENT def UpdateWindow(self, bar): self.Window.Add(bar) @property def IsReady(self): return self.Window.IsReady and self.HalfHourWindow.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]