Overall Statistics
Total Trades
31
Average Win
8.66%
Average Loss
-1.55%
Compounding Annual Return
18.096%
Drawdown
41.900%
Expectancy
1.472
Net Profit
3.739%
Sharpe Ratio
1.059
Probabilistic Sharpe Ratio
42.935%
Loss Rate
62%
Win Rate
38%
Profit-Loss Ratio
5.59
Alpha
0.352
Beta
7.31
Annual Standard Deviation
1.2
Annual Variance
1.439
Information Ratio
1.019
Tracking Error
1.124
Treynor Ratio
0.174
Total Fees
$350.45
Estimated Strategy Capacity
$390000000.00
Lowest Capacity Asset
ES XUERCWA6EWAP
Portfolio Turnover
401.85%
#region imports
from AlgorithmImports import *
#endregion

## PORTFOLIO MANAGMENT
IVT_PERCENTAGE = 0.2

# The following is a reference to the entry threshold to 
# limit the price were th order should take place
# Check StopLimitOrders for references:
# https://www.quantconnect.com/docs/v2/writing-algorithms/trading-and-orders/order-types/stop-limit-orders
LIMIT_TH = 0.2

## STD Comparisions
# If true the short positions can be opened when detected
ACTIVATE_SHORT = True
# STD is one standard deviation
# Each of the following works as (STD * SHORT_...)
SHORT_TH = 2 # Threshold to open Short Position
SHORT_SL = SHORT_TH + 0.1 # STD to Stop Loss
SHORT_TP = SHORT_TH - 0.1 # STD to take profit


# If true the long positions can be opened when detected
ACTIVATE_LONG = True
# Each of the following works as (STD * LONG_...)
LONG_TH = -2 # Threshold to open Long Position
LONG_SL = LONG_TH + 0.1 # STD to Stop Loss
LONG_TP = LONG_TH - 0.1  # STD to take profit

## UNIVERSE SETTINGS
MY_FUTURES = [Futures.Indices.SP500EMini]#Futures.Indices.NASDAQ100EMini]

# INDICATORS
BB_PERIOD = 30
INDICATORS_RESOLUTION = Resolution.Minute
#region imports
from AlgorithmImports import *

# Custom
from control_parameters import *
## endregion


class RiskWithBollingerBands(RiskManagementModel):
    ''''''
    def __init__(self,bb_period:int, bb_resolution:Resolution, bb_args:dict={},
                    LongProfitPercent:float = 2.5, ShortProfitPercent:float=0.9,
                    LongDrawdownPercent:float = 0.9, ShortDrawdownPercent:float=2.5,
                    ):
        '''
        Inputs:
            - bb_period [int]: Period to apply on BB
            - bb_args [QC BollingerBands parameters]: Should contain extra arguments for BB

            Standar Deviations triggers to use on the bollinger bands:
             - LongProfitPercent: When hits and Long take profit
             - LongDrawdownPercent: When hits and Long Stop Loss
             - ShortProfitPercent: When hits and Short take profit
             - ShortDrawdownPercent: When hits and Short Stop Loss
        '''
        self.bb_period = bb_period 
        self.bb_resolution = bb_resolution
        self.bb_args = bb_args
        self.LongProfitPercent = LongProfitPercent
        self.ShortProfitPercent = ShortProfitPercent
        self.LongDrawdownPercent = LongDrawdownPercent
        self.ShortDrawdownPercent = ShortDrawdownPercent
        self.trailingBBs = dict()

    def ManageRisk(self, algorithm, targets):
        '''
        Manages the algorithm's risk at each time step
        Inputs:
            - algorithm: The algorithm instance of QC.
            - targets: The current portfolio targets are to be assessed for risk
        '''
        riskAdjustedTargets = list()

        for kvp in algorithm.Securities:
            symbol = kvp.Key
            security = kvp.Value

            if security.Type == SecurityType.Future:
                # Get Canonical Object
                trailingBBState = self.trailingBBs.get(symbol.Canonical)
            # Next if not Future
            if not trailingBBState:
                continue
            # Remove if not invested
            if not security.Invested: # For positions closed outside the risk management model
                trailingBBState.position.pop(symbol, None) # remove from dictionary
                continue
            

            # Get position side
            position = PositionSide.Long if security.Holdings.IsLong else PositionSide.Short
            
            # Recorded Holdings Value
            
            stored_position = trailingBBState.position.get(symbol)
            # Add newly invested contract (if doesn't exist) or reset holdings state (if position changed)
            if stored_position is None or position != stored_position:
                # Create a BB State object if not existing or reset it if the position direction changed
                trailingBBState.position[symbol] = position

            # Check take profit trigger
            if ((position == PositionSide.Long and trailingBBState.Price > (trailingBBState.MiddleBand + trailingBBState.GetStandardDeviation(self.LongProfitPercent))) or
                (position == PositionSide.Short and trailingBBState.Price < (trailingBBState.MiddleBand - trailingBBState.GetStandardDeviation(self.ShortProfitPercent)))):
                # Update position
                riskAdjustedTargets.append(PortfolioTarget(symbol, 0))
                # Pop the symbol from the dictionary since the holdings state of the security has been changed
                trailingBBState.position.pop(symbol, None) # remove from dictionary
                continue

            elif ((position == PositionSide.Long and trailingBBState.Price < (trailingBBState.MiddleBand - trailingBBState.GetStandardDeviation(self.LongDrawdownPercent))) or
                (position == PositionSide.Short and trailingBBState.Price > (trailingBBState.MiddleBand + trailingBBState.GetStandardDeviation(self.ShortDrawdownPercent)))):
                # liquidate
                riskAdjustedTargets.append(PortfolioTarget(symbol, 0))
                # Pop the symbol from the dictionary since the holdings state of the security has been changed
                trailingBBState.position.pop(symbol, None) # remove from dictionary

        return riskAdjustedTargets

## SECURITIES LOGIC: CREATION, INDICATORS, UPDATE, TACKING
    def OnSecuritiesChanged(self, algorithm, 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.
        for security in changes.AddedSecurities:
            if self.trailingBBs.get(security.Symbol) is None: # Create SymbolData object
                self.trailingBBs[security.Symbol] = BBState(algorithm, security.Symbol, 
                                                            self.bb_period, self.bb_resolution, self.bb_args)
        
        # The removed securities are liquidated and removed from the security tracker.
        for security in changes.RemovedSecurities: # Don't track anymore
            self.SubscriptionManager.RemoveConsolidator(security.Symbol, self.trailingBBs[security.Symbol].consolidator)
            self.SecuritiesTracker.pop(security.Symbol, None)

class BBState:
    def __init__(self,algorithm, symbol:Symbol, 
                    period:int,bb_resolution: Resolution,bb_args,
                    position:PositionSide=None):
        if not(position):
            self.position = {}
        self.Symbol = symbol

        # Create
        self.bb_resolution = bb_resolution
        self.bb = BollingerBands('BB '+symbol.Value,period, 1, **bb_args)

        # Create consolidator for Symbol
        self.consolidator = QuoteBarConsolidator(bb_resolution)
        self.consolidator.DataConsolidated += self.UpdateBB
        algorithm.SubscriptionManager.AddConsolidator(self.Symbol, self.consolidator)
    
    def UpdateBB(self, sender: object, consolidated_bar: TradeBar) -> None:
        self.bb.Update(consolidated_bar.EndTime, consolidated_bar.Close)

    @property
    def Price(self):
        return self.bb.Price.Current.Value

    @property
    def StandardDeviation(self):
        return self.bb.StandardDeviation.Current.Value

    @property
    def MiddleBand(self):
        return self.bb.MiddleBand.Current.Value
    
    def GetStandardDeviation(self, decimal:float) -> float:
        return self.bb.StandardDeviation.Current.Value * decimal
# region imports
from AlgorithmImports import *

# Custom
from control_parameters import *
import symbol_data
from custom_risk_management import RiskWithBollingerBands
# endregion

class CalmLightBrownBadger(QCAlgorithm):

## INITIALIZATION
    def Initialize(self):
        self.SetStartDate(2021, 9, 17)  # Set Start Date
        self.SetEndDate(2021, 12, 17)  # Set End Date
        self.SetCash(100000)  # Set Strategy Cash

        # Broker and Account type: Margin allow the use of leverage and shorts
        self.SetBrokerageModel(BrokerageName.QuantConnectBrokerage, AccountType.Margin)
        
        # Universe Settings
        self.InitializeUniverse()
        
        # Add custom modification every time a security is initialized
        # feed data when adding contracts in the middle of OnData method
        self.SetSecurityInitializer(self.CustomSecurityInitializer)

        # Define risk management model: Usin default values
        self.AddRiskManagement(RiskWithBollingerBands(bb_period=BB_PERIOD, bb_resolution=INDICATORS_RESOLUTION,
                                                    ShortProfitPercent=SHORT_TP, ShortDrawdownPercent=SHORT_SL,
                                                    LongProfitPercent=LONG_TP, LongDrawdownPercent=LONG_SL,
                                                    ))
        self.AddRiskManagement(TrailingStopRiskManagementModel())

    def InitializeUniverse(self):
        # Initilize tracker parameters
        self.SecuritiesTracker = {}
        
        # Store desired futures 
        self.__Futures = set()

        # Universe selector for futures
        for f in MY_FUTURES:
            # Minute allows a good enough flow of data for risk management.
            self.__Futures.add(self.AddFuture(f,Resolution.Minute,
                                            dataNormalizationMode = DataNormalizationMode.BackwardsRatio,
                                            dataMappingMode = DataMappingMode.OpenInterest,
                                            contractDepthOffset= 0))

    def CustomSecurityInitializer(self, security):
        # When adding a future we could get 0 as Ask/Bid
        # prices since the data has not been feed. 
        # This solves that issue
        bar = self.GetLastKnownPrice(security)
        security.SetMarketPrice(bar)

    def InitCharts(self):
        
        chart = Chart("BollingerBands")
        self.AddChart(chart)

        series = {}
        Series("<seriesName>")
        chart.AddSeries(series)

## SECURITIES LOGIC: CREATION, INDICATORS, UPDATE, TACKING
    def InitIndicators(self, symbol):
        # Create the indicators related to the input symbol
        # The default MA is Simple, if changed to Exponential in the future
        # remember to change the 
        bb = BollingerBands('BB '+ symbol.Value,BB_PERIOD, 1)
        self.RegisterIndicator(symbol, bb, INDICATORS_RESOLUTION)
        return bb

    def ManageAdded(self, 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] = symbol_data.SymbolData(security, self.get_Time,
                                                                                self.InitIndicators(security.Symbol), short_th=SHORT_TH,long_th=LONG_TH)
    
    def ManageRemoved(self, removed):
        '''
        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)

## CHECK FOR BUYING POWER

    def CheckBuyingPower(self, symbol, quantity):
        '''
        Check for enough buying power.
        If the buying power for the target quantity is not enough,
        It will return the quantity for which the buying power is enough.
        '''
        if quantity < 0:
            order_direction = OrderDirection.Sell
        else:
            order_direction = OrderDirection.Buy
        # Get the buying power depending of the order direction and symbol
        buy_power = self.Portfolio.GetBuyingPower(symbol, order_direction)
        # Compute possible quantity
        q_t = abs(buy_power) / self.Securities[symbol].Price
        # Select minimum quantity
        return round(min(abs(quantity),q_t),8)*np.sign(quantity)

    def CheckOrdeQuatity(self, symbol, quantity):
        '''Check that the quantity of shares computed meets the minimum requirments'''
        q = abs(quantity)
        # There are requirements for the minimum or maximum that can be purchased per security.
        if q > self.Settings.MinAbsolutePortfolioTargetPercentage and q < self.Settings.MaxAbsolutePortfolioTargetPercentage:
            symbol_properties = self.Securities[symbol].SymbolProperties
            if symbol_properties.MinimumOrderSize is None or q > symbol_properties.MinimumOrderSize:
                return True
        return False

## POSITION MANAGEMENT
    def my_round(self, x, prec=2, base=0.25): 
        return (base * (np. array(x) / base).round()).round(prec)
    
    def OnData(self, data):
        for cont_symbol, tracked in self.SecuritiesTracker.items():
            self.GetView(tracked.bb, cont_symbol.Value)
            if self.Portfolio[cont_symbol].Invested:
                continue
            elif ACTIVATE_SHORT and self.Securities[cont_symbol].Exchange.Hours.IsOpen(self.Time, False) and tracked.IsReady and tracked.OverShort:
                symbol = self.Securities[cont_symbol].Mapped
                # Current asset quantity on portfolio
                hold_value = self.Portfolio[symbol].AbsoluteHoldingsValue
                current_wallet = self.Portfolio.MarginRemaining + hold_value
                # Current quantity
                quantity = self.Portfolio[symbol].Quantity
                target = -(current_wallet * IVT_PERCENTAGE)/self.Securities[symbol].Price
                target -= quantity
                quantity = self.CheckBuyingPower(symbol, int(target)) # Check for buying power
                if self.CheckOrdeQuatity(symbol, quantity): # Check for the minimum quantity
                    stopPrice = self.my_round(tracked.StandardDeviationReferencePrice(tracked.ShortThreshold + LIMIT_TH))
                    limitPrice = self.my_round(tracked.StandardDeviationReferencePrice(tracked.ShortThreshold - LIMIT_TH))
                    buy = self.StopLimitOrder(symbol, quantity, stopPrice, limitPrice)
                    tracked.Order = buy # Save in the security tracker the contract and order
            elif ACTIVATE_LONG and self.Securities[cont_symbol].Exchange.Hours.IsOpen(self.Time, False) and tracked.IsReady and tracked.OverLong:
                symbol = self.Securities[cont_symbol].Mapped
                # Current asset quantity on portfolio
                hold_value = self.Portfolio[symbol].AbsoluteHoldingsValue
                current_wallet = self.Portfolio.MarginRemaining + hold_value
                # Current quantity
                quantity = self.Portfolio[symbol].Quantity
                target = (current_wallet * IVT_PERCENTAGE)/self.Securities[symbol].Price
                target -= quantity
                quantity = self.CheckBuyingPower(symbol, int(target)) # Check for buying power
                if self.CheckOrdeQuatity(symbol, quantity): # Check for the minimum quantity
                    stopPrice = self.my_round(tracked.StandardDeviationReferencePrice(tracked.LongThreshold + LIMIT_TH))
                    limitPrice = self.my_round(tracked.StandardDeviationReferencePrice(tracked.LongThreshold - LIMIT_TH))
                    buy = self.StopLimitOrder(symbol, quantity, stopPrice, limitPrice)
                    tracked.Order = buy # Save in the security tracker the contract and order

    def GetView(self,bb, symbol_value):
        if self.Time.minute % 15 == 0:
            self.Plot("BollingerBands", symbol_value + " middleband", bb.MiddleBand.Current.Value)
            self.Plot("BollingerBands", symbol_value + " upperband", bb.UpperBand.Current.Value)
            self.Plot("BollingerBands", symbol_value + " lowerband", bb.LowerBand.Current.Value)
            self.Plot("BollingerBands", symbol_value + " price", bb.Price.Current.Value)
#region imports
from AlgorithmImports import *
#endregion

class SymbolData:
    # Object to Keep track of the securities

## INITIALIZATION
    def __init__(self, security, time, 
                bb,
                short_th:float=None, long_th:float=None):
        '''
        Inputs:
            - Symbol [QC Symbol]: Reference to the security.
            - time [Main Algo function]: This function returns the time of the main algorithm.
        '''
        self.Security = security
        self.__Symbol = security.Symbol
        self.get_Time = time
        self.__Order = None 
        self.bb = bb

        assert short_th or long_th, 'Both short_th and long_th cannot be null. One or both thresholds shoud be specified.'
        
        if short_th:
            self.ShortThreshold = short_th
        else:
            self.ShortThreshold = long_th
        if long_th:
            self.LongThreshold = long_th
        else:
            self.LongThreshold = short_th

        self.IsOverShort = False
        self.IsOverLong = False
    
    @property
    def Time(self):
        # Allow access to the Time object directly
        return self.get_Time()
    @property
    def IsReady(self):
        return self.bb.IsReady

## MANAGEMENT
    @property
    def Symbol(self):
        return self.__Symbol

    @Symbol.setter
    def Symbol(self,NewSymbol):
        self.__Order = None
        self.__Symbol = NewSymbol
    
    @property
    def Order(self):
        return self.__Order

    @Order.setter
    def Order(self, order):
        self.__Order = order
    
## MANAGEMENT LOGIC
    @property
    def OverShort(self):
        if self.IsReady:
            # We want a short, the threshold is positive so we check that the price is over the middle band + (STD*ShortThreshold)
            # We want a short, the threshold is negative so we check that the price is over the middle band - (STD*ShortThreshold)
            # In both cases we should check that the price is higher because with short we expect the price to raise
            current = self.Security.Price > (self.bb.MiddleBand.Current.Value + (self.bb.StandardDeviation.Current.Value * self.ShortThreshold))
            state = current and (current != self.IsOverShort)
            self.IsOverShort = current
            return state
        return False

    @property
    def OverLong(self):
        if self.IsReady:
            # We want a short, the threshold is positive so we check that the price is lower the middle band + (STD*ShortThreshold)
            # We want a short, the threshold is negative so we check that the price is lower the middle band - (STD*ShortThreshold)
            # In both cases we should check that the price is higher because with long we expect the price to drop.
            current = self.Security.Price < (self.bb.MiddleBand.Current.Value - (self.bb.StandardDeviation.Current.Value * self.LongThreshold))
            state = current and (current != self.IsOverLong)
            self.IsOverLong = current
            return state
        return False

    def StandardDeviationReferencePrice(self, decimal:float=1) -> float:
        return self.bb.MiddleBand.Current.Value + (decimal* self.bb.StandardDeviation.Current.Value)