Overall Statistics
Total Trades
6
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
Tracking Error
0
Treynor Ratio
0
Total Fees
$6.00
'''
TRADING IDEA
- Decide on stocks to trade that are gapping up or down
- Trade based on opening range breakout
- Calculate a range based on high and low of the first n minutes (dynamic)
- Place Stop Limit orders based on maximum and minimum range values
- Liquidate if the price reaches the opposite end of range
- 10 minutes before market close, liquidate all positions that has not hit TP or SL
- TODO
- Take n mins consolidator closing to take trades or not
- Think of SL as ATR
- dont trade after 2 PM


Help and Resources
- TradeBarConsolidator -  https://www.quantconnect.com/forum/discussion/5273/consolidators-using-qcalgorithmframework-and-setuniverseselection/p1
- Remove Consolidator - https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/Alphas/GasAndCrudeOilEnergyCorrelationAlpha.py#L189
- Placing Orders - https://www.quantconnect.com/forum/discussion/1539/how-to-place-three-way-order/p1
'''
from datetime import datetime, time

class HorizontalUncoupledAntennaArray(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 4, 13)  # Set Start Date
        self.SetEndDate(2020, 4, 13)
        self.SetCash(10000)  # Set Strategy Cash

        self.UniverseSettings.Resolution = Resolution.Minute
        self.SetWarmUp(120)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash)
        self.SetAlpha(CustomAlphaModel())
        self.SetTimeZone(TimeZones.NewYork)
        
        self.AddUniverse(self.CoarseSelectionFilter, self.FineSelectionFilter)
        
        self.numberOfSymbolsCoarse = 1000
        self.numberOfSymbolsFine = 100
        
        self.Schedule.On(
                self.DateRules.EveryDay(), \
                self.TimeRules.At(15, 45), \
                self.EveryDayBeforeMarketClose \
        )

        
    '''
    Coarse selection function
        - Only run once every day
        - Sort by Daily Dollar volume
        - Returns symbols with price greater than 10
        - 2000 symbols returned
    '''
    def CoarseSelectionFilter(self, coarse):
        sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
        return [ x.Symbol for x in sortedByDollarVolume if x.Price > 10 and x.HasFundamentalData][:self.numberOfSymbolsCoarse]  
        
    '''
    Fine selection filter
        - calculate market capital
        - filter based on market cap
    '''
    def FineSelectionFilter(self, fine):
        market_cap = {}
        for i in fine:
            market_cap[i] = (i.EarningReports.BasicAverageShares.ThreeMonths * i.EarningReports.BasicEPS.TwelveMonths * i.ValuationRatios.PERatio)
        marketCapFilter = sorted([x for x in fine if market_cap[x] > 0], key=lambda x: market_cap[x], reverse=True)
        return [ x.Symbol for x in marketCapFilter][:self.numberOfSymbolsFine]
        
    def EveryDayBeforeMarketClose(self):
        self.Liquidate()
        
class CustomAlphaModel:
    def __init__(self):
        self.symbolDataBySymbol = {}
        self.day = 0
        self.shortSymbols = []
        self.buySymbols = []
        self.percentQuantity = 0.01
        
    def Update(self, algorithm, data):
        
        insights = []
        tradeBars = data.Bars
        for symb in tradeBars.Keys:
            if symb in self.symbolDataBySymbol and not (algorithm.Time.time().hour >= 14 and algorithm.Time.time().minute >= 0):
                if self.symbolDataBySymbol[symb].highRange > 0 and self.symbolDataBySymbol[symb].lowRange > 0 and \
                symb.Value not in self.buySymbols and symb.Value not in self.shortSymbols:
                    if self.symbolDataBySymbol[symb].currentFiveMinuteConsolidator is not None and \
                        self.symbolDataBySymbol[symb].currentFiveMinuteConsolidator.Close > self.symbolDataBySymbol[symb].highRange:
                        buyquantity = round((self.percentQuantity*algorithm.Portfolio.TotalPortfolioValue)/tradeBars[symb].Close)
                        algorithm.MarketOrder(symb, buyquantity)
                        algorithm.StopMarketOrder(symb, -1*buyquantity, self.symbolDataBySymbol[symb].lowRange)
                        self.buySymbols.append(symb.Value)
                        
                    if self.symbolDataBySymbol[symb].currentFiveMinuteConsolidator is not None and \
                        self.symbolDataBySymbol[symb].currentFiveMinuteConsolidator.Close < self.symbolDataBySymbol[symb].lowRange:
                         sellquantity = round((self.percentQuantity*algorithm.Portfolio.TotalPortfolioValue)/tradeBars[symb].Close)
                         algorithm.MarketOrder(symb, -1*sellquantity)
                         algorithm.StopMarketOrder(symb, sellquantity, self.symbolDataBySymbol[symb].highRange)
                         self.shortSymbols.append(symb.Value)
                         
        for symb in algorithm.Portfolio.Keys:
            if algorithm.Securities[symb].Holdings.UnrealizedProfitPercent > 0.01:
                algorithm.Liquidate(symb, 'One Percent Profit')
        return insights
        
    def OnSecuritiesChanged(self, algorithm, changes):
        if self.day != algorithm.Time.day:
            self.day = algorithm.Time.day
        
        symbols = [ x.Symbol for x in changes.AddedSecurities ]
        
        for removed in changes.RemovedSecurities:
            symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
            if symbolData is not None:
                symbolData.RemoveConsolidators(algorithm)
                
        for symbol in symbols:
            ## Create SymbolData objects for any new assets
            algorithm.Securities[symbol].SetDataNormalizationMode(DataNormalizationMode.SplitAdjusted);
            symbolData = SymbolData(algorithm, symbol)
            ## Assign object to a dictionary so you can access it later in the Update() method
            self.symbolDataBySymbol[symbol] = symbolData
        
class CustomFeeModel:
    def GetOrderFee(self, parameters):
        fee = max(1, parameters.Security.Price
                  * parameters.Order.AbsoluteQuantity
                  * 0.00001)
        return OrderFee(CashAmount(fee, 'USD'))
        
class SymbolData:
    
    def __init__(self, algorithm, symbol):
        self.Symbol = symbol
        self.Algorithm = algorithm
        self.currentFiveMinuteConsolidator = None;
        self.highRange = -1
        self.lowRange = -1
        self.currentDay = -1
        self.deviationSum = 0
        self.gapPercentLow = 2
        self.gapPercentHigh = 15
        history = algorithm.History([symbol], 10, Resolution.Daily)
        for index, row in history.iterrows():
            print(row['open'], row['close'])
            hiopen = row['high']- row['open']
            openlo = row['open']- row['low']
            self.deviationSum += min(hiopen, openlo)
            
        self.deviation = self.deviationSum/10
        
        self.lastClose = history.loc[symbol]['close'].iloc[len(history.index) - 1]
        self.gapUpPercent = -1
        self.gapDownPercent = -1
        
        self.five_consolidator = TradeBarConsolidator(5)    
        self.five_consolidator.DataConsolidated += self.FiveMinuteConsolidated
        
        self.range_consolidator = TradeBarConsolidator(65)  
        self.range_consolidator.DataConsolidated += self.RangeConsolidated
        
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.range_consolidator)  ## Register consolidator
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.five_consolidator)
        
        
        
    def FiveMinuteConsolidated(self, sender, bar):
        self.currentFiveMinuteConsolidator = bar
        pass
    
    def RangeConsolidated(self, sender, bar):
        if self.gapUpPercent == -1 and self.gapDownPercent == -1:
            if self.lastClose < bar.Open:
                # This is Gap up
                self.gapUpPercent = ((bar.Open - self.lastClose)/bar.Open)*100
                
            elif self.lastClose > bar.Open:
                # This is Gap down
                self.gapDownPercent = ((self.lastClose - bar.Open)/bar.Open )*100
                    
            if self.highRange == -1 and (self.gapPercentLow < self.gapUpPercent < self.gapPercentHigh or self.gapPercentLow < self.gapDownPercent < self.gapPercentHigh):
                self.highRange= max(bar.High, self.lastClose) + (0*self.deviation)
            if self.lowRange == -1 and (self.gapPercentLow < self.gapUpPercent < self.gapPercentHigh or self.gapPercentLow < self.gapDownPercent < self.gapPercentHigh):
                self.lowRange= min(self.lastClose, bar.Low) - (0*self.deviation)
        
            
    def RemoveConsolidators(self, algorithm):
        algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.range_consolidator)
        algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.five_consolidator)