Overall Statistics
Total Trades
1040
Average Win
0.48%
Average Loss
-0.22%
Compounding Annual Return
22.737%
Drawdown
12.500%
Expectancy
0.227
Net Profit
26.918%
Sharpe Ratio
1.299
Probabilistic Sharpe Ratio
59.551%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
2.16
Alpha
0
Beta
0
Annual Standard Deviation
0.124
Annual Variance
0.015
Information Ratio
1.299
Tracking Error
0.124
Treynor Ratio
0
Total Fees
$2549.03
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY 31VFOSXP6TLK6|SPY R735QTJ8XC9X
#region imports
from AlgorithmImports import *
#endregion

class BaseOptionStrategy(QCAlgorithm):
    name = ""
    optionLegs          = []
    securityOptionLegs  = []
    expiryList          = []

    def __init__(self, name, optionLegs):
        self.name = name
        self.optionLegs = optionLegs
        
    # Method that returns the number of days this strategy expires in. If we have multiple explirations we return an array.
    def ExpiresIn(self, algo):
        expirations = list(set(self.ExpiryList()))
        if len(expirations) > 1:
            return [(ex - algo.Time.date()).days for ex in expirations]
        else:
            return (expirations[0] - algo.Time.date()).days
    
    def StrategyKey(self):
        strikesStr = "_".join([str(x.StrikePrice if self.IsHolding() else x.Strike) for x in self.SecurityOptionLegs()])
        return "{}_{}_{}".format(self.NameKey(), str(self.Expiration()), strikesStr)

    def UnrealizedProfit(self):
        if not self.IsHolding(): raise Exception("The {} strategy does not hold OptionHolding instances.".format(self.name))

        return sum([c.UnrealizedProfitPercent for c in self.optionLegs]) / len(self.optionLegs) * 100

    # Checks if the expiration is the same
    def SameExpiration(self):
        expirations = list(set(self.ExpiryList()))
        if len(expirations) > 1:
            return False
        else:
            return True

    def Open(self, algo, quantity, log = True):
        raise Exception("This method is not implemented")
    
    def Close(self, algo):
        algo.portfolio.RemoveTrade("{}s".format(self.NameKey()), self.StrategyKey())
        for contract in self.optionLegs:
            algo.Liquidate(contract.Symbol, tag = self.StrategyKey())
    
    def ExpiryList(self):
        self.expiryList = self.expiryList or [x.Expiry.date() for x in self.SecurityOptionLegs()]
        return self.expiryList

    def Expiration(self):
        return self.ExpiryList()[0]

    def SecurityOptionLegs(self):
        if self.IsHolding():
            self.securityOptionLegs = self.securityOptionLegs or [x.Security for x in self.optionLegs]
        else:
            self.securityOptionLegs = self.securityOptionLegs or self.optionLegs
        
        return self.securityOptionLegs

    def IsHolding(self):
        return isinstance(self.optionLegs[0], OptionHolding)

    # TODO simplify this method to use SecurityOptionLegs
    def Underlying(self):
        if self.IsHolding():
            return option.Security.Underlying.Symbol
        else:
            return option.UnderlyingSymbol
    
    def NameKey(self):
        return "_".join([x.lower() for x in self.name.split()])

class BullPutSpread(BaseOptionStrategy):
    shortPut = None
    longPut = None

    def __init__(self, shortPut, longPut):
        BaseOptionStrategy.__init__(self, "Bull Put Spread", [shortPut, longPut])
        self.longPut = longPut
        self.shortPut = shortPut

        self.__StrikeCheck()
        
        if self.SameExpiration() == False:
            raise Exception("The expiration should be the same for all options.")

    def Open(self, algo, quantity, log = True):
        if log: algo.portfolio.AddTrade("{}s".format(self.NameKey()), self.StrategyKey())
        algo.Buy(self.longPut.Symbol, quantity)
        algo.Sell(self.shortPut.Symbol, quantity)
    
    def __StrikeCheck(self):
        if self.IsHolding():
            longStrike = self.longPut.Security.StrikePrice
            shortStrike = self.shortPut.Security.StrikePrice
        else:
            longStrike = self.longPut.Strike
            shortStrike = self.shortPut.Strike
            
        if longStrike > shortStrike:
            raise Exception("The longPut strike has to be lower than the shortPut strike.")

class BearCallSpread(BaseOptionStrategy):
    shortCall = None
    longCall = None
    
    def __init__(self, shortCall, longCall):
        BaseOptionStrategy.__init__(self, "Bear Call Spread", [shortCall, longCall])
        self.longCall = longCall
        self.shortCall = shortCall

        self.__StrikeCheck()
        
        if self.SameExpiration() == False:
            raise Exception("The expiration should be the same for all options.")

    def Open(self, algo, quantity, log = True):
        if log: algo.portfolio.AddTrade("{}s".format(self.NameKey()), self.StrategyKey())
        algo.Buy(self.longCall.Symbol, quantity)
        algo.Sell(self.shortCall.Symbol, quantity)

    def __StrikeCheck(self):
        if self.IsHolding():
            longStrike = self.longCall.Security.StrikePrice
            shortStrike = self.shortCall.Security.StrikePrice
        else:
            longStrike = self.longCall.Strike
            shortStrike = self.shortCall.Strike
            
        if longStrike < shortStrike:
            raise Exception("The longCall strike has to be higher than the shortCall strike.")

# An iron condor is an options strategy consisting of two puts (one long and one short) and two calls (one long and one short), and four strike prices, all with the same expiration date. 
# The iron condor earns the maximum profit when the underlying asset closes between the middle strike prices at expiration.
class IronCondor(BaseOptionStrategy):
    bullPutSpread = None
    bearCallSpread = None

    def __init__(self, longCall, shortCall, longPut, shortPut):
        BaseOptionStrategy.__init__(self, "Iron Condor", [longCall, shortCall, longPut, shortPut])

        self.bullPutSpread = BullPutSpread(shortPut, longPut)
        self.bearCallSpread = BearCallSpread(shortCall, longCall)
    
    def Open(self, algo, quantity, log = True):
        if log: algo.portfolio.AddTrade("{}s".format(self.NameKey()), self.StrategyKey())
        self.bullPutSpread.Open(algo, quantity, False)
        self.bearCallSpread.Open(algo, quantity, False)
from AlgorithmImports import *
from itertools import groupby
from OptionStrategies import BullPutSpread, BearCallSpread, IronCondor

class OptionsFinder:

    def __init__(self, algo, underlying, rangeStart, rangeStop, minStrike, maxStrike):
        self.algo = algo
        self.underlying = underlying
        self.rangeStart = rangeStart
        self.rangeStop = rangeStop
        self.minStrike = minStrike
        self.maxStrike = maxStrike
        # Right now the below lines could be conditional as we don't need them if we are not using
        # the contractBid method where we look for credit and not really the strike.
        self.underlyingOption = self.algo.AddOption(self.underlying, Resolution.Hour)
        self.underlyingOption.SetFilter(
            lambda u: u.IncludeWeeklys()
                        .Strikes(self.minStrike, self.maxStrike)
                        .Expiration(timedelta(self.rangeStart), timedelta(self.rangeStop))
        )
        self.underlyingOption.PriceModel = OptionPriceModels.CrankNicolsonFD() 

    # Just adds the options data.
    def AddContract(self, strike = None, bid = None, optionType = OptionRight.Call):
        ''' We are adding the option contract data to the algo '''
        # Replace this line below with different methods that filter the contracts in a more special way.
        if strike is not None:
            return self.__GetContractStrike(strike, optionType)

        if bid is not None:
            return self.__GetContractBid(bid, optionType)

    def FindIronCondor(self, expiration, longCall, shortCall, longPut, shortPut):
        # Check if there is options data for this symbol.
        # Important that when we do GetValue we use the underlyingOption.Symbol because the
        # key returned by the equity is `TSLA` and the one from option is `?TSLA`
        chain = self.algo.sliceData.OptionChains.GetValue(self.underlyingOption.Symbol)

        if chain is None: return None

        PutSpread   = None
        CallSpread  = None

        # The way we are defining expiration here is by taking an absolute value. So it might just be __ExpiresIn(x) > expiration
        contracts = [x for x in chain if self.__ExpiresIn(x) == expiration]

        # Make sure we have the contracts sorted by Strike and Expiry
        contracts = sorted(contracts, key = lambda x: (x.Expiry, x.Strike))

        calls = [x for x in contracts if x.Right == OptionRight.Call]
        if len(calls) == 0: return None

        soldCall = min(calls, key=lambda x: abs(x.Greeks.Delta - shortCall["delta"]))
        # strike is above soldCall by value
        boughtCall = min(calls, key=lambda x: abs(x.Strike - (soldCall.Strike + longCall["value"])))
        if soldCall is not None and boughtCall is not None:
            CallSpread = BearCallSpread(soldCall, boughtCall)

        puts = [x for x in contracts if x.Right == OptionRight.Put]
        if len(puts) == 0: return None
        
        soldPut = min(puts, key=lambda x: abs(x.Greeks.Delta - shortPut["delta"]))
        # strike is below soldPut by value
        boughtPut = min(puts, key=lambda x: abs(x.Strike - (soldPut.Strike - longPut["value"])))
        if soldPut is not None and boughtPut is not None:
            PutSpread = BullPutSpread(soldPut, boughtPut)

        if CallSpread is not None and PutSpread is not None:
            return IronCondor(CallSpread.longCall, CallSpread.shortCall, PutSpread.longPut, PutSpread.shortPut)
    
        return None

        # # Iterate over the contracts that are grouped per day.
        # for expiry, group in groupby(contracts, lambda x: x.Expiry):
        #     group = list(group)
        #     # sort group by type
        #     group = sorted(group, key = lambda x: x.Right)

        #     if CallSpread is not None and PutSpread is not None:
        #         return IronCondor(CallSpread.longCall, CallSpread.shortCall, PutSpread.longPut, PutSpread.shortPut)

        #     # Reset the contracts after each expiration day. We want to force the options to Expire on the same date.
        #     PutSpread   = None
        #     CallSpread  = None

        #     for right, rightGroup in groupby(group, lambda x: x.Right):
        #         rightGroup = list(rightGroup)
        #         # sort the options by credit
        #         options = sorted(rightGroup, key = lambda x: x.Greeks.Delta)
        #         if right == OptionRight.Call and CallSpread is None:
        #             soldCall = min(options, key=lambda x: abs(x.Greeks.Delta - shortCall["delta"]))
        #             # strike is above soldCall by value
        #             boughtCall = min(options, key=lambda x: abs(x.Strike - (soldCall.Strike + longCall["value"])))
        #             if soldCall is not None and boughtCall is not None:
        #                 CallSpread = BearCallSpread(soldCall, boughtCall)

        #         if right == OptionRight.Put and PutSpread is None:
        #             soldPut = min(options, key=lambda x: abs(x.Greeks.Delta - shortPut["delta"]))
        #             # strike is below soldPut by value
        #             boughtPut = min(options, key=lambda x: abs(x.Strike - (soldPut.Strike - longPut["value"])))
        #             if soldPut is not None and boughtPut is not None:
        #                 PutSpread = BullPutSpread(soldPut, boughtPut)

        # return None

    def FindBearCallSpread(self, expiration, shortCall, longCall):
        # Check if there is options data for this symbol.
        # Important that when we do GetValue we use the underlyingOption.Symbol because the
        # key returned by the equity is `TSLA` and the one from option is `?TSLA`
        chain = self.algo.sliceData.OptionChains.GetValue(self.underlyingOption.Symbol)

        if chain is None: return None

        # The way we are defining expiration here is by taking an absolute value. So it might just be __ExpiresIn(x) > expiration
        contracts = [x for x in chain if self.__ExpiresIn(x) >= expiration]

        # Select only calls
        contracts = [x for x in chain if x.Right == OptionRight.Call]

        # Make sure we have the contracts sorted by Strike and Expiry
        contracts = sorted(contracts, key = lambda x: (x.Expiry, x.Strike))

        # Iterate over the contracts that are grouped per day.
        for expiry, group in groupby(contracts, lambda x: x.Expiry):
            group = list(group)

            # sort the options by credit
            options = sorted(group, key = lambda x: x.Greeks.Delta)

            soldCall = min(options, key=lambda x: abs(x.Greeks.Delta - shortCall["delta"]))
            # strike is above soldCall by value
            boughtCall = min(options, key=lambda x: abs(x.Strike - (soldCall.Strike + longCall["value"])))
            if soldCall is not None and boughtCall is not None:
                return BearCallSpread(soldCall, boughtCall)

        return None

    def FindBullPutSpread(self, expiration, shortPut, longPut):
                # Check if there is options data for this symbol.
        # Important that when we do GetValue we use the underlyingOption.Symbol because the
        # key returned by the equity is `TSLA` and the one from option is `?TSLA`
        chain = self.algo.sliceData.OptionChains.GetValue(self.underlyingOption.Symbol)

        if chain is None: return None

        # The way we are defining expiration here is by taking an absolute value. So it might just be __ExpiresIn(x) > expiration
        contracts = [x for x in chain if self.__ExpiresIn(x) >= expiration]

        # Select only puts
        contracts = [x for x in chain if x.Right == OptionRight.Put]

        # Make sure we have the contracts sorted by Strike and Expiry
        contracts = sorted(contracts, key = lambda x: (x.Expiry, x.Strike))

        # Iterate over the contracts that are grouped per day.
        for expiry, group in groupby(contracts, lambda x: x.Expiry):
            group = list(group)

            # sort the options by credit
            options = sorted(group, key = lambda x: x.Greeks.Delta)

            soldPut = min(options, key=lambda x: abs(x.Greeks.Delta - shortPut["delta"]))
            # strike is below soldPut by value
            boughtPut = min(options, key=lambda x: abs(x.Strike - (soldPut.Strike - longPut["value"])))
            if soldPut is not None and boughtPut is not None:
                return BullPutSpread(soldPut, boughtPut)

        return None

    def ContractsWithCredit(self, bid):
        # Check if there is options data for this symbol.
        # Important that when we do GetValue we use the underlyingOption.Symbol because the
        # key returned by the equity is `TSLA` and the one from option is `?TSLA`
        chain = self.algo.sliceData.OptionChains.GetValue(self.underlyingOption.Symbol)

        if chain is None: return None

        callContract        = None
        putContract         = None

        # Don't look at contracts that are closer than the defined rangeStart.
        contracts = [x for x in chain if self.__ExpiresIn(x) >= self.rangeStart and self.__ExpiresIn(x) <= self.rangeStop]

        # Limit the Strikes to be between -5% and +5% of the UnderlyingPrice.
        contracts = [x for x in contracts if x.UnderlyingLastPrice / 1.05 <= x.Strike <= x.UnderlyingLastPrice * 1.05 ]

        # Make sure we have the contracts sorted by Strike and Expiry
        contracts = sorted(contracts, key = lambda x: (x.Expiry, x.Strike))

        # Iterate over the contracts that are grouped per day.
        for expiry, group in groupby(contracts, lambda x: x.Expiry):
            group = list(group)
            # sort group by type
            group = sorted(group, key = lambda x: x.Right)

            if callContract is not None and putContract is not None:
                return [callContract.Symbol, putContract.Symbol]

            # Reset the contracts after each expiration day. We want to force the options to Expire on the same date.
            callContract = None
            putContract  = None

            for right, rightGroup in groupby(group, lambda x: x.Right):
                rightGroup = list(rightGroup)
                # sort the options by credit
                creditOptions = sorted(rightGroup, key = lambda x: x.BidPrice, reverse = True)
                if right == OptionRight.Call:
                    # find any contract that has a BidPrice > bid
                    creditCalls = [x for x in creditOptions if x.BidPrice > bid]
                    creditCalls = sorted(creditCalls, key = lambda x: x.BidPrice)
                    # if
                    #   we do have a call that can replace the one we are buying for a credit then return that.
                    # else
                    #   sort the calls by BidPrice and pick the highest bid contract
                    if len(creditCalls) > 0:
                        return [creditCalls[0].Symbol]
                    else:
                        # TODO: here instead of picking the bigest credit contract (this would be the one with the smallest Strike)
                        #       we should consider picking one that is closer to strike as possible.
                        callContract = creditOptions[0]

                # Only look for PUT contracts if we can't find a call contract with enough credit to replace the bid value.
                if right == OptionRight.Put and callContract is not None:
                    # find any contract that has a BidPrice > bid - callContract.BidPrice
                    creditPuts = [x for x in creditOptions if x.BidPrice > (bid - callContract.BidPrice)]
                    creditPuts = sorted(creditPuts, key = lambda x: x.BidPrice)
                    if len(creditPuts) > 0:
                        putContract = creditPuts[0]

        return []

    # Method that returns a boolean if the security expires in the given days
    # @param security [Security] the option contract
    def __ExpiresIn(self, security):
        return (security.Expiry.date() - self.algo.Time.date()).days

    def __GetContractBid(self, bid, optionType):
        # TODO: this method can be changed to return PUTs and CALLs if the strike selected is not
        #       sensible enough. Like don't allow selection of contracts furthen than 15% of the stock price.
        #       This should force the selection of puts to offset the calls or the other way around until we can get into
        #       a situation where just calls are present.

        # Check if there is options data for this symbol.
        # Important that when we do GetValue we use the underlyingOption.Symbol because the
        # key returned by the equity is `TSLA` and the one from option is `?TSLA`
        chain = self.algo.sliceData.OptionChains.GetValue(self.underlyingOption.Symbol)

        if chain is None: return None

        # Don't look at contracts that are closer than the defined rangeStart.
        contracts = [x for x in chain if (x.Expiry.date() - self.algo.Time.date()).days > self.rangeStart]

        contracts = sorted(
            contracts,
            key = lambda x: abs(x.UnderlyingLastPrice - x.Strike)
        )

        # Get the contracts that have more credit than the one we are buying. The bid here might be too small
        # so we should consider sorting by:
        # - strike > stock price
        # - closest expiration
        # - closest strike to stockPrice with credit! SORT THIS BELOW?!?
        # TODO: right now we have a problem as it keeps going down in strike when it's loosing. Maybe send in the profit and if it's over -100 -200% switch to PUT?!
        # TODO: consider using a trend indicator like EMA to determine a contract type switch CALL -> PUT etc.
        contracts = [x for x in contracts if x.BidPrice >= bid * 1.20]

        # If possible only trade the same contract type. This should have a limit of how low it should go before it switches to a put.
        # Maybe by doing the limit we can remove the trend indicator.

        if optionType == OptionRight.Call:
            typeContracts = [x for x in contracts if x.Right == optionType and (x.UnderlyingLastPrice / 1.05) < x.Strike]
            if len(typeContracts) > 0:
                contracts = typeContracts

        if optionType == OptionRight.Put:
            typeContracts = [x for x in contracts if x.Right == optionType and (x.UnderlyingLastPrice * 1.05) < x.Strike]
            if len(typeContracts) > 0:
                contracts = typeContracts

        # Grab us the contract nearest expiry
        contracts = sorted(contracts, key = lambda x: x.Expiry)

        if len(contracts) == 0:
            return None
        return contracts[0].Symbol

    def __GetContractStrike(self, strike, optionType):
        filtered_contracts = self.__DateOptionFilter()

        if len(filtered_contracts) == 0: return str()
        else:
            contracts = self.__FindOptionsStrike(filtered_contracts, strike, optionType)

            if len(contracts):
                self.algo.AddOptionContract(contracts[0], Resolution.Daily)
                return contracts[0]
            else:
                return str()

    def __FindOptionsBid(self, contracts, bid, optionType = OptionRight.Call):
        ''' We are filtering based on bid price. '''
        # select only the specified options types
        contracts = [x for x in contracts if x.ID.OptionRight == optionType]
        # TODO!! THis does not work. Most probably we have to do self.algo.AddOptionContract on all contracts above and then
        #.       use that to filter by BidPrice as the value does not seem to exist. :(
        # pick a contract that is above the bid price provided.
        contracts = [x for x in contracts if x.ID.BidPrice >= bid]

        contracts = sorted(
            contracts,
            key = lambda x: abs(bid - x.ID.BidPrice)
        )

        # prefer shorter expirations
        contracts = sorted(
            contracts,
            key = lambda x: x.ID.Date,
            reverse=False
        )

        # sorted the contracts according to their expiration dates and choose the ATM options
        return contracts


    # Returns options that are ATM.
    def __FindOptionsStrike(self, contracts, strike, optionType = OptionRight.Call):
        ''' We are filtering based on strike rank. '''

        # select only the specified options types
        contracts = [x for x in contracts if x.ID.OptionRight == optionType]

        # never want to go beow strike
        contracts = [x for x in contracts if x.ID.StrikePrice >= strike]

        contracts = sorted(
            contracts,
            key = lambda x: abs(strike - x.ID.StrikePrice)
        )

        # prefer shorter expirations
        contracts = sorted(
            contracts,
            key = lambda x: x.ID.Date,
            reverse=False
        )

        # sorted the contracts according to their expiration dates and choose the ATM options
        return contracts


        # find the strike price of ATM option
        # atm_strike = sorted(contracts,
        #                     key = lambda x: abs(x.ID.StrikePrice - strike))
        # atm_strike = atm_strike[0].ID.StrikePrice
        # strike_list = sorted(set([i.ID.StrikePrice for i in contracts]))
        # # find the index of ATM strike in the sorted strike list
        # atm_strike_rank = strike_list.index(atm_strike)
        # try:
        #     strikes = strike_list[(atm_strike_rank + self.minStrike):(atm_strike_rank + self.maxStrike)]
        # except:
        #     strikes = strike_list
        # filtered_contracts = [i for i in contracts if i.ID.StrikePrice in strikes]

        # # select only call options
        # call = [x for x in filtered_contracts if x.ID.OptionRight == optionType]
        # # sorted the contracts according to their expiration dates and choose the ATM options
        # return sorted(sorted(call, key = lambda x: abs(strike - x.ID.StrikePrice)),
        #                     key = lambda x: x.ID.Date, reverse=True)


    def __DateOptionFilter(self):
        ''' We are filtering the options based on the expiration date. It does return weekly contracts as well. '''
        contracts = self.algo.OptionChainProvider.GetOptionContractList(self.underlying, self.algo.Time.date())
        if len(contracts) == 0 : return []
        # fitler the contracts based on the expiry range
        contract_list = [i for i in contracts if self.rangeStart < (i.ID.Date.date() - self.algo.Time.date()).days < self.rangeStop]

        return contract_list

    # Returns options that can be rolled for a credit and higher strike
    def __CreditUpOptions(self, contracts, existing_contract):
        # TODO: this!
        return []
from AlgorithmImports import *
from OptionStrategies import BullPutSpread, BearCallSpread, IronCondor
import pickle
# Class that handles portfolio data. We have here any method that would search the portfolio for any of the contracts we need.
class PortfolioHandler:

    def __init__(self, algo):
        self.algo = algo
        # Create a RollingWindow to store the last of the trades bids for selling options.
        self.lastTradeBid = 0

    # Returns all the covered calls of the specified underlying
    # @param underlying [String]
    # @param optionType [OptionRight.Call | OptionRight.Put]
    # @param maxDays [Integer] number of days in the future that the contracts are filtered by
    def UnderlyingSoldOptions(self, underlying, optionType, maxDays = 60):
        contracts = []
        for option in self.algo.Portfolio.Values:
            security = option.Security
            if (option.Type == SecurityType.Option and
                str(security.Underlying) == underlying and
                security.Right == optionType and
                option.Quantity < 0 and
                (security.Expiry.date() - self.algo.Time.date()).days < maxDays):
                contracts.append(option)
        return contracts

    def BullPutSpreads(self, underlying):
        allContracts = {}
        contracts = []
        # select all the puts in our portfolio
        for option in self.algo.Portfolio.Values:
            security = option.Security
            if (option.Type == SecurityType.Option and
                security.Right == OptionRight.Put and
                str(security.Underlying) == underlying and
                option.Quantity != 0):
                allContracts.setdefault(int(security.Expiry.timestamp()), []).append(option)

        # if we have 2 contracts per expiration then we have a put spread. Let's filter for bull put spreads now.
        # shortPut: higher strike than longPut // sold
        # longPut: lower strike than shortPut // bought
        for t, puts in allContracts.copy().items():
            # if we have 2 puts with equal quantities then we have a put spread
            if len(puts) == 2 and sum(put.Quantity for put in puts) == 0:
                shortPut = next(filter(lambda put: put.Quantity < 0, puts), None)
                longPut = next(filter(lambda put: put.Quantity > 0, puts), None)
                if shortPut.Security.StrikePrice > longPut.Security.StrikePrice:
                    contract = BullPutSpread(shortPut, longPut)
                    if contract.StrategyKey() in self.ReadTrades("bull_put_spreads"): contracts.append(contract)

        return contracts
    
    def BearCallSpreads(self, underlying):
        allContracts = {}
        contracts = []
        # select all the calls in our portfolio
        for option in self.algo.Portfolio.Values:
            security = option.Security
            if (option.Type == SecurityType.Option and
                security.Right == OptionRight.Call and
                str(security.Underlying) == underlying and
                option.Quantity != 0):
                allContracts.setdefault(int(security.Expiry.timestamp()), []).append(option)

        # if we have 2 contracts per expiration then we have a call spread. Let's filter for bear call spreads now.
        # shortCall: lower strike than longCall // sold
        # longCall: higher strike than shortCall // bought
        for t, calls in allContracts.copy().items():
            # if we have 2 calls with equal quantities then we have a call spread
            if len(calls) == 2 and sum(call.Quantity for call in calls) == 0:
                shortCall = next(filter(lambda call: call.Quantity < 0, calls), None)
                longCall = next(filter(lambda call: call.Quantity > 0, calls), None)
                if shortCall.Security.StrikePrice < longCall.Security.StrikePrice:
                    contract = BearCallSpread(shortCall, longCall)
                    if contract.StrategyKey() in self.ReadTrades("bear_call_spreads"): contracts.append(contract)

        return contracts

    def IronCondors(self, underlying):
        allContracts = {}
        contracts = []
        for option in self.algo.Portfolio.Values:
            security = option.Security
            if (option.Type == SecurityType.Option and
                str(security.Underlying) == underlying and
                option.Quantity != 0):
                allContracts.setdefault(int(security.Expiry.timestamp()), []).append(option)

        # if we have 4 allContracts per expiration then we have an iron condor
        for t, c in allContracts.copy().items():
            if len(c) == 4:
                calls = [call for call in c if call.Security.Right == OptionRight.Call]
                puts = [put for put in c if put.Security.Right == OptionRight.Put]
                # if we have 2 calls and 2 puts with equal quantities then we have a condor
                if (len(calls) == 2 and
                    sum(call.Quantity for call in calls) == 0 and
                    len(puts) == 2 and
                    sum(put.Quantity for put in puts) == 0):
                    shortCall = next(filter(lambda call: call.Quantity < 0, calls), None)
                    longCall = next(filter(lambda call: call.Quantity > 0, calls), None)
                    shortPut = next(filter(lambda put: put.Quantity < 0, puts), None)
                    longPut = next(filter(lambda put: put.Quantity > 0, puts), None)
                    contract = IronCondor(longCall, shortCall, longPut, shortPut)
                    if contract.StrategyKey() in self.ReadTrades("iron_condors"): contracts.append(contract)

        return contracts

    # Add trades to the object store like the following params
    # @param key [String] // iron_condors
    # @param value [OptionStrategy] // Eg: IronCondor
    def AddTrade(self, key, value):
        jsonObj = self.ReadTrades(key)
        jsonObj.append(value)
        self.algo.ObjectStore.SaveBytes(str(key), pickle.dumps(jsonObj)) # Save object as JSON encoded string
    
    # Remove trades from the object store by these params
    # @param key [String] // iron_condors
    # @param value [OptionStrategy] // Eg: IronCondor
    def RemoveTrade(self, key, value):
        jsonObj = self.ReadTrades(key)
        jsonObj.remove(value)
        self.algo.ObjectStore.SaveBytes(str(key), pickle.dumps(jsonObj)) # Save object as JSON encoded string
    
    def ReadTrades(self, key):
        jsonObj = []
        if self.algo.ObjectStore.ContainsKey(key):
            deserialized = bytes(self.algo.ObjectStore.ReadBytes(key))
            jsonObj = (pickle.loads(deserialized))    
            if jsonObj is None: jsonObj = []
        
        jsonObj = list(set(jsonObj)) # there should be unique values in our array
        
        return jsonObj

    def PrintPortfolio(self):
        # self.Debug("Securities:")
        # self.Securities
        #   contains Securities that you subscribe to but it does not mean that you are invested.
        #   calling self.AddOptionContract will add the option to self.Securities
        for kvp in self.Securities:
            symbol = kvp.Key # key of the array
            security = kvp.Value # value of the array (these are not attributes)
            holdings = security.Holdings
            self.Debug(str(security.Symbol))
            # self.Debug(str(security.Underlying))
            # self.Debug(str(security.Holdings))

        # self.Debug("Portfolio:")
        # self.Portfolio
        #   contains the Security objects that you are invested in.
        for kvp in self.Portfolio:
            symbol = kvp.Key
            holding = kvp.Value
            holdings = holding.Quantity
            # self.Debug(str(holding.Holdings))
            
# region imports
from AlgorithmImports import *
from PortfolioHandler import PortfolioHandler
from OptionsFinder import OptionsFinder
# endregion

# https://app.optionalpha.com/community/templates/post/10-my-ic-10delta-1dte-10wide-stop-hedge
# In 2020, I made just under $40,000 using iron condors on SPY in addition to working a job, with two 1DTE $1-wide trades per week and as much as 
# 300 contracts per spread (usually $30-60k per iron condor). The initial capital allocation was less than $1,000, and I gradually deposited approximately 
# $20,000 capital over the course of a year. This strategy was accomplished with 5 Delta, 2 standard deviation iron condors (spreads opened at 0.04, 
# sold at 0.01 for 3% profit compounded twice a week) with a mean-reverting ATM hedge debit spread implemented in the event of a loss, and a loss was 
# realized about every three months. The original IC was stopped out at a price of 0.50, aka when the short leg hit ATM. Yet this strategy experienced 
# flaws in the 2021 market, as 2 standard deviation moves became increasingly prominent, with high volatility gaps occurring as often as twice a week in 
# Q4 2021 and Q1 2022 regardless of implied volatility. Furthermore, one loss was equivalent to approximately 1 or 2 months of profit if left un-hedged.

# Today, I present to you the evolution of my methodology.
# But first, here is some background information.


# SCANNER:

# VARIABLES:
# symbol: SPY
# amount: up to 50% of bot net liq
# vix_below: 30
# vix_criteria: true|false // Turning VIX Criteria off bypasses the VIX Below restriction.
# expiration: 1 day
# entry_restriction: true|false // Turning this off bypasses the Tuesday or Thursday and timed entry criteria.
# long_call: 10$ above short call
# short_call: .10 delta
# short_put: -.10 delta
# long_put: 10$ below short put
# price: 100% of bid/ask // optionalpha smartpricing (not possible on quant)

# VIX is below 30 -- NO
#     | YES
# SPY 14 day RSI intraday is above 30 and below 70  -- NO
#     | YES
# (Today is Tuesday || Thursday) && (0 positions Short put spread && Short call spread && Iron condor) -- NO
#     | YES
# Market time is 9:45am || 10:00am || 10:15am -- NO
#     | YES
# Iron condor available (Symbol, Expiration, Short call, Long call, Short put, Long put / SPY, 1 day, .10 delta, 10$ above short call, -.10 delta, 10$ below short put) -- NO
#     | YES
# Bot has available capital to open amount (50% of net liq) of iron condor at Price (100% of bid/ask) -- NO
#     | YES
# OPEN IRON CONDOR

# MONITORS:

# -- IC | Closer w/ Hedge Opener

# VARIABLES:
# symbol: SPY
# amount: up to 50% of bot net liq
# hedge_expiration: 2 - 3 days
# long_call: 10$ above short call
# short_call: .30 delta
# short_put: -.30 delta
# long_put: 10$ below short put
# price: 100% of bid/ask // optionalpha smartpricing (not possible on quant)

# For each Iron condor position -- NO
#     | YES
# Position premium decreased by 95% (95% profit) 
#     | YES                  | NO
# Close position             |
#                            |
#         (stock price > short call strike || stock price < short put strike) && current market after 12:00pm && position expires in 0 days
#                 | YES                                                                 | NO
#                 |                                   Current market time is after 03:15pm && position expires in 0 days -- NO
#                 |                                                                     | YES
#                 |                                                                Close Position
#                 |
#                 -------------------------- stock price > short call strike
#                                                | YES             | NO
#                          ----------------------                  ------------------------------------
#     (symbol, hedge_expiration, short_call, long_call) avl -- NO                           stock price < short put strike -- NO
#                          | YES                                                                            | YES
#      Bot has capital for same quantity as Short call spread (to hedge) -- NO           (symbol, hedge_expiration, short_put, long_put) avl -- NO
#                          | YES                                                                            | YES
#                   Close Iron condor above -- NO                          Bot has capital for same quantity as Short put spread (to hedge) -- NO
#                          | YES                                                                            | YES
#          OPEN (symbol, hedge_expiration, short_call, long_call)                               Close Iron condor above -- NO
#                                                                                                           | YES
#                                                                                     OPEN (symbol, hedge_expiration, short_put, long_put)


# -- CALL & PUT SPREAD HEDGE ADVANCED CLOSER (2x)

# VARIABLES:
# absolute_profit: 90%
# trailing_profit: True | False // Turn on to allow for Trailing Profit SmartStops that increase the probability of success.
# target: 50% // this is trailing profit and i assume it would mean cumulative profit?!?
# tail: 10%
# pdt: True | False // Turn on to wait 1 Market Day to close.  Turn off to allow 0 DTE closing.  Default now set to off.

# For each Short call spread position -- NO
#     | YES
# Position premium decreased by absolute_profit (90% profit) || (trailing_profit && position trails a trailing % target by trailing_profit [50%])
#           | YES                                                               | NO
#     Close Position                                                            |
#                                                                      ptd toggle is TRUE
#                                                                   | YES                  | NO
#                                            ------------------------                      ------------------
#          stock price > short call strike && position opened > 1 market day -- NO                 stock price > short call strike -- NO
#                                   | YES                                                                       | YES
#                           Close position                                                              Close position

# NOTES:
# - implement trailing stops https://medium.datadriveninvestor.com/cut-your-trading-losses-in-5-steps-on-quantconnect-ca7e2995a0ec

class IronPaycheck(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2021, 1, 1)  # Set Start Date
        self.SetEndDate(2022, 3, 1)
        self.SetCash(100000)  # Set Strategy Cash
        symbol = self.GetParameter("symbol")
        self.expiration = 1 # day
        self.entryRestriction = True # Turning this off bypasses the Tuesday or Thursday and timed entry criteria.

        equity = self.AddEquity(symbol, Resolution.Minute)
        self.symbol = equity.Symbol
        self.vixSymbol = self.AddIndex("VIX", Resolution.Minute).Symbol
        self.VIX = 0

        self.RSIValue = self.RSI(self.symbol, 14)
        self.SetWarmUp(timedelta(30))

        equity.SetDataNormalizationMode(DataNormalizationMode.Raw) # Not sure what this does yet
        # To fix the missing price history for option contracts https://www.quantconnect.com/forum/discussion/8779/security-doesn-039-t-have-a-bar-of-data-trade-error-options-trading/p1
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage) # not sure what this does

        self.sliceData = None

        self.portfolio = PortfolioHandler(self)
        self.optionHandler = OptionsFinder(self, self.symbol, self.expiration, self.expiration + 1, -20, 20)

        # TODO refresh the ObjectStore with the current portfolio trades on algo initialize so we can recover from a failure

        # (Today is Tuesday || Thursday)
        #     | YES
        # Market time is 9:45am || 10:00am || 10:15am -- NO
        #     | YES
        scannerTimes = [self.TimeRules.At(9, 45), self.TimeRules.At(10, 0), self.TimeRules.At(10, 15)]
        for t in scannerTimes:
            self.Schedule.On(
                self.DateRules.Every(DayOfWeek.Tuesday, DayOfWeek.Thursday), \
                t, \
                self.Scanner
            )

        # -- IC | Closer w/ Hedge Opener
        self.Schedule.On(
            self.DateRules.EveryDay(self.symbol), \
            self.TimeRules.Every(timedelta(minutes=15)), \
            self.MonitorIronCondor
        )


        # -- CALL & PUT SPREAD HEDGE ADVANCED CLOSER (2x)
        self.Schedule.On(
            self.DateRules.EveryDay(self.symbol), \
            self.TimeRules.Every(timedelta(minutes=15)), \
            self.MonitorHedges
        )

    # TODO see how we can add/remove orders here instead of manually
    # def OnOrderEvent(self, orderEvent):
    #     order = self.Transactions.GetOrderById(orderEvent.OrderId)
    #     if orderEvent.Status == OrderStatus.Filled: 
    #         self.Log("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent))

    def OnData(self, data: Slice):
        if self.IsWarmingUp: return
            
        self.sliceData = data
        if data.ContainsKey(self.vixSymbol):
            self.VIX = data[self.vixSymbol].Close
        # self.Scanner(data)

    # -- CALL & PUT SPREAD HEDGE ADVANCED CLOSER (2x)
    def MonitorHedges(self):
        if self.IsWarmingUp: return
        # VARIABLES:
        absoluteProfit = 90 # %
        trailingProfit = True # | False // Turn on to allow for Trailing Profit SmartStops that increase the probability of success.
        target = 50 # % // this is trailing profit and i assume it would mean cumulative profit?!?
        # tail = 10 # %
        pdt = False # | True // Turn on to wait 1 Market Day to close.  Turn off to allow 0 DTE closing.  Default now set to off.
        
        stockPrice = self.Securities[self.symbol].Price

        # For each Short call/put spread position -- NO
        for callSpread in self.portfolio.BearCallSpreads(self.symbol):
            # TODO: set a trailing profit algo?! That's a trailing stop loss set only after a certain profit target was reached
            if callSpread.UnrealizedProfit() >= absoluteProfit: # or (trailingProfit and ):
                callSpread.Close(self)
            else:
                if pdt:
                    if stockPrice > callSpread.shortCall.Security.StrikePrice and createdAt.day > 1:
                        # Close position
                        callSpread.Close(self)
                else:
                    if stockPrice > callSpread.shortCall.Security.StrikePrice:
                        # Close position
                        callSpread.Close(self)

        # For each Short call/put spread position -- NO
        for putSpread in self.portfolio.BullPutSpreads(self.symbol):
            # TODO: set a trailing profit algo?! That's a trailing stop loss set only after a certain profit target was reached
            if putSpread.UnrealizedProfit() >= absoluteProfit: # or (trailingProfit and ):
                putSpread.Close(self)
            else:
                if pdt:
                    # stock price < short put strike && position opened > 1 market day -- NO 
                    if stockPrice < putSpread.shortPut.Security.StrikePrice and createdAt.day > 1:
                        # Close position
                        putSpread.Close(self)
                else:
                    # stock price < short put
                    if stockPrice < putSpread.shortPut.Security.StrikePrice:
                        # Close position
                        putSpread.Close(self)

    # -- IC | Closer w/ Hedge Opener
    def MonitorIronCondor(self):
        if self.IsWarmingUp: return
        # VARIABLES:
        # amount: up to 50% of bot net liq
        hedgeExpiration = 2 - 3 # days
        hedgeLongCall = {"value": 10} # $ above short call
        hedgeShortCall = {"delta": .30} # delta
        hedgeShortPut = {"delta": -.30} # delta
        hedgeLongPut = {"value": 10} # $ below short put
        # price: 100% of bid/ask // optionalpha smartpricing (not possible on quant)

        # For each Iron condor position -- NO
        #     | YES
        for ic in self.portfolio.IronCondors(self.symbol):
            # Position premium decreased by 95% (95% profit) 
            #     | YES                  | NO
            # Close position             |
            if ic.UnrealizedProfit() > 95:
                ic.Close(self)
                continue

            shortCall = ic.bearCallSpread.shortCall
            shortPut = ic.bullPutSpread.shortPut
            stockPrice = self.Securities[self.symbol].Price
            expiresIn = ic.ExpiresIn(self)
            
            if expiresIn != 0: return

            #  (stock price > short call strike || stock price < short put strike) && current market after 12:00pm && position expires in 0 days
            if (stockPrice > shortCall.Security.StrikePrice or stockPrice < shortPut.Security.StrikePrice) and self.Time.hour >= 12 and expiresIn == 0:
                # stock price > short call strike
                if stockPrice > shortCall.Security.StrikePrice:
                    # (symbol, hedge_expiration, short_call, long_call) avl -- NO  
                    hedgeCallSpread = self.optionHandler.FindBearCallSpread(expiration = hedgeExpiration, shortCall = hedgeShortCall, longCall = hedgeLongCall)
                    if hedgeCallSpread is not None:
                        # Bot has capital for same quantity as Short call spread (to hedge) -- NO
                        # Close Iron condor above -- NO      
                        ic.Close(self)
                        # OPEN (symbol, hedge_expiration, short_call, long_call)
                        hedgeCallSpread.Open(self, self.OrderSize())
                elif stockPrice < shortPut.Security.StrikePrice:
                    #  (symbol, hedge_expiration, short_put, long_put) avl -- NO
                    hedgePutSpread = self.optionHandler.FindBullPutSpread(expiration = hedgeExpiration, shortPut = hedgeShortPut, longPut = hedgeLongPut)
                    if hedgePutSpread is not None:
                        # Bot has capital for same quantity as Short put spread (to hedge) -- NO
                        # Close Iron condor above -- NO
                        ic.Close(self)
                        # OPEN (symbol, hedge_expiration, short_put, long_put)
                        hedgePutSpread.Open(self, self.OrderSize())
            elif self.Time.hour >= 15 and self.Time.minute >= 15 and expiresIn == 0:
                # Current market time is after 15:15 && position expires in 0 days -- NO
                ic.Close(self)

    def Scanner(self):
        if self.IsWarmingUp: return
        
        # amount: up to 50% of bot net liq
        vixBelow = 30
        vixCriteria = False # Turning VIX Criteria off bypasses the VIX Below restriction.
        # self.expiration = 1 # day
        longCall = {"value": 10} # $ above short call
        shortCall = {"delta": .10} # delta
        shortPut = {"delta": -.10} # delta
        longPut = {"value": 10} # $ below short put
        # price: 100% of bid/ask // optionalpha smartpricing (not possible on quant)

        if self.sliceData is None:
            return
        # VIX is below 30 -- NO
        #     | YES
        if vixCriteria and self.VIX > vixBelow:
            return
        
        # SPY 14 day RSI intraday is above 30 and below 70  -- NO
        #     | YES
        if 30 > self.RSIValue.Current.Value or self.RSIValue.Current.Value > 70:
            return

        # 0 positions Short put spread && Short call spread && Iron condor -- NO
        #     | YES
        if len(self.portfolio.IronCondors(self.symbol)) > 0 or len(self.portfolio.BullPutSpreads(self.symbol)) > 0 or len(self.portfolio.BearCallSpreads(self.symbol)) > 0:
            return

        # Iron condor available (Symbol, Expiration, Short call, Long call, Short put, Long put / SPY, 1 day, .10 delta, 10$ above short call, -.10 delta, 10$ below short put) -- NO
        #     | YES
        ironCondor = self.optionHandler.FindIronCondor(expiration = self.expiration, longCall = longCall, shortCall = shortCall, longPut = longPut, shortPut = shortPut)
        if ironCondor is not None:
            # Bot has available capital to open amount (50% of net liq) of iron condor at Price (100% of bid/ask) -- NO
            #     | YES
            # OPEN IRON CONDOR
            ironCondor.Open(self, self.OrderSize())


    def OrderSize(self):
        return 10
        # leverage = self.Securities[self.symbol].Leverage
        # margin = self.Portfolio.MarginRemaining
        # orderSize = margin * leverage / self.Securities[self.symbol].Close