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]