Overall Statistics
Total Trades
2407
Average Win
0.47%
Average Loss
-0.51%
Compounding Annual Return
-4.299%
Drawdown
58.400%
Expectancy
-0.056
Net Profit
-12.993%
Sharpe Ratio
0.124
Probabilistic Sharpe Ratio
3.878%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
0.92
Alpha
0.011
Beta
0.295
Annual Standard Deviation
0.423
Annual Variance
0.179
Information Ratio
-0.198
Tracking Error
0.442
Treynor Ratio
0.177
Total Fees
$6706.25
Estimated Strategy Capacity
$0
class CalculatingYellowGreenAnguilline(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2018, 1, 1)
        self.SetEndDate(2021, 3, 1)
        
        self.initialCash = 1000000
        self.SetCash(self.initialCash)

        # Take profit after our portfolio profit/loss is greater than this percentage
        self.takeProfitPercent = 0.05
        # Liquidate our portfolio when our profit/loss drops below this percentage
        self.stopLossPercent = -0.20
        self.lastTraded = datetime(1, 1, 1)

        # CL Futures with expiries between 30 and 90 days
        cl = self.AddFuture("CL", Resolution.Minute, Market.NYMEX)
        cl.SetFilter(30, 90)
    
        self.AddFutureOption(cl.Symbol, self.FilterFuturesOptionsContracts)
        
        # EMA indicators keyed by future option symbol
        self.ema = {}
        # Used to store Symbols with matching strike prices
        self.futureOptionCache = {}
        
    def FilterFuturesOptionsContracts(self, filterUniverse):
        return filterUniverse.Strikes(-1, 1)

    def OnData(self, data):
        # Check if all indicators are warmed up before we continue
        if not self.AllIndicatorsReady():
            return
        
        # Only trade once a day
        if self.Time - self.lastTraded < timedelta(days=1):
            return

        pnlPercent = self.Portfolio.TotalUnrealizedProfit / self.initialCash
        if pnlPercent >= self.takeProfitPercent or pnlPercent <= self.stopLossPercent:
            self.Liquidate()
            self.lastTraded = self.Time
            return
        
        expiring_options = []
        for symbol, emas in self.ema.items():
            # Loop on call options
            if symbol.ID.OptionRight == OptionRight.Put:
                continue
            
            # Remove contracts close to expiry from our portfolio
            if symbol.ID.Date - self.Time <= timedelta(days=30):
                self.Liquidate(symbol)
                self.Liquidate(symbol.Underlying)
                
                self.Debug(f'Removing {symbol} due to impending expiry (15 days)')
                expiring_options.append(symbol)
                continue
            
            # Slow and fast EMA (2 hours/6 hours) indicators for the call option
            slow = emas[0]
            fast = emas[1]
            
            call = symbol
            put = self.InvertOption(symbol)

            if fast > slow and self.Portfolio[put].Quantity >= 0:
                self.Liquidate(put)
                self.MarketOrder(symbol, 1)
                self.lastTraded = self.Time
            
            elif fast > slow and self.Portfolio[call].Quantity >= 0:
                self.Liquidate(call)
                self.MarketOrder(put, 1)
                self.lastTraded = self.Time
        
        # Remove all options we won't trade anymore from the 
        # indicators dictionary we created
        for symbol in expiring_options:
            del self.ema[symbol]
        
    def AllIndicatorsReady(self):
        '''
        Checks if all indicators are ready to be used
        '''
        
        for symbol in self.ema:
            for indicator in self.ema[symbol]:
                if not indicator.IsReady:
                    return False

        return True

    def OnSecuritiesChanged(self, changes):
        '''
        When any future or future option is added to the algorithm,
        this method will execute.
        
        For added futures options, we setup EMA indicators (slow/fast)
        for them.
        
        For removed futures options, we remove them from the dictionary of EMA indicators
        '''
        for security in changes.AddedSecurities:
            symbol = security.Symbol

            if symbol.SecurityType != SecurityType.FutureOption:
                continue
                
            if symbol not in self.ema:
                self.ema[symbol] = []

            emas = self.ema[symbol]

            if len(emas) == 0:
                emas.append(self.EMA(symbol, 120, Resolution.Minute))
                emas.append(self.EMA(symbol, 360, Resolution.Minute))

        for security in changes.RemovedSecurities:
            symbol = security.Symbol

            if symbol in self.ema:
                del self.ema[symbol]
                
    def InvertOption(self, symbol):
        '''
        Converts a call option symbol to a put option symbol, and vice versa.
        '''
        
        if symbol not in self.futureOptionCache:
            self.futureOptionCache[symbol] = {}

        cache = self.futureOptionCache[symbol]
        strike = symbol.ID.StrikePrice
        right = symbol.ID.OptionRight

        if strike not in cache:
            # Adds both calls and puts to the collection
            cache[strike] = [
                symbol, 
                Symbol.CreateOption(
                    symbol.Underlying, 
                    symbol.ID.Market, 
                    symbol.ID.OptionStyle, 
                    OptionRight.Put if right == OptionRight.Call else OptionRight.Call,
                    symbol.ID.StrikePrice,
                    symbol.ID.Date)
            ]

        for option in cache[strike]:
            if right != option.ID.OptionRight:
                return option