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