Overall Statistics
Total Trades
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Net Profit
0%
Sharpe Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
0.164
Tracking Error
0.18
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
# 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=30)

    # NOTE: Check the notebook, I selected these ranges based on the dataframe at the bottom
    # Tresholds For Leverage
    Leverage_Lower_Threshold = 0.00018/100
    Leverage_Upper_Threshold = 0.0002/100
    
    # No leverage thresholds
    Lower_Threshold = 0.00014/100
    Upper_Threshold = 0.00016/100

    # Leverages
    MyLeverage = 2
    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 = 0.5
    NLowRatio = 0.5

    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", "ASE"]]

## -------------------------------------------------------------------------- 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])

## 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]