Created with Highcharts 12.1.2Equity20122013201420152016201720182019202020212022202320242025850G1,050G-16000.001850G1,050G00.008-60G20G850G1,050G08001,200k01,000M08G08G
Overall Statistics
Total Orders
1258
Average Win
0.12%
Average Loss
-0.03%
Compounding Annual Return
-1.003%
Drawdown
12.400%
Expectancy
-0.662
Start Equity
1000000000000
End Equity
877312151015
Net Profit
-12.269%
Sharpe Ratio
-2.425
Sortino Ratio
-3.15
Probabilistic Sharpe Ratio
0.000%
Loss Rate
93%
Win Rate
7%
Profit-Loss Ratio
3.51
Alpha
-0.021
Beta
-0.048
Annual Standard Deviation
0.01
Annual Variance
0
Information Ratio
-0.814
Tracking Error
0.144
Treynor Ratio
0.525
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPX 32OCJVZOPY5PQ|SPX 31
Portfolio Turnover
0.01%
#region imports
from AlgorithmImports import *
from collections import deque, namedtuple
#endregion

Trade = namedtuple('Trade', ['Quantity', 'Start_Date', 'End_Date', 'Start_Price', 'End_Price', 'Underlying_Start_Price', 'Underlying_End_Price'], defaults=(None,) * 2)

class AssetTradeData:
    
    def __init__(self, symbol):
        self._symbol = symbol
        self._buys = {}
        self._sells = {}


    def update_buy(self, time, order):
        if time in self._buys:
            raise ValueError('Given time already exists. Please delete the existing element first')
        self._buys[time] =  order


    def update_sell(self, time, order):
        if time in self._sells:
            raise ValueError('Given time already exists. Please delete the existing element first')
        self._sells[time] =  order
            
    @property 
    def buys(self):
        return self._buys

    @property 
    def sells(self):
        return self._sells

    def pair_all_trades(self):
        buys: dict[DateTime, Order] = self.buys
        sells: dict[DateTime, Order] = self.sells
        
        all_trade_dates = sorted(list(buys.keys()) + list(sells.keys()))
        current_total = 0
        open_buys: Deque[Order] = deque()
        open_sells: Deque[Order] = deque()
        trades: List[Trade] = []
 
        for date in all_trade_dates: 
            if date in buys:

                # ToDo: sanity check that can be removed (may be allowed to buy and sell on same day for certain strategies)
                if date in sells: 
                    print(date)
                #assert date not in sells

                # Need a deep copy as we are going to change the order
                order = buys[date].Clone()

                # ToDo: sanity check that should be removed in future.
                assert order.Quantity > 0
                
                trades += self._pair_trades(open_sells, order, trades)
                # ToDo: sanity check that should be removed in future.
                assert order.Quantity >= 0
                if order.Quantity > 0:
                    assert len(open_sells) == 0
                    open_buys.append(order)

            else:           #date in sales
                # Need a deep copy as we are going to change the order
                order = sells[date].Clone()

                assert order.Quantity < 0

                trades += self._pair_trades(open_buys, order, trades)

                assert order.Quantity <= 0
                if order.Quantity < 0:
                    assert len(open_buys) == 0
                    open_sells.append(order)
            
            assert len(open_buys) == 0 or len(open_sells) == 0
        
        return trades



    def _pair_trades(self, open_trades: List[Order], order: Order, trades: List[Trade]) -> List[Trade]:
        '''
        Pairs closed trades (new balance 0) from order to open_trades in LIFO style (buy and sell order pairing). 
        The trades quantity is negative if it is a short trade and positive otherwise. 
        '''
        # Note: this procedure changes the variables that it receives. Do not call directly from outside the class. 

        paired_trades = []
        while len(open_trades) > 0 and abs(open_trades[-1].Quantity) <= abs(order.Quantity) and order.Quantity != 0:

            # ToDo: sanity check that should be removed in future.
            assert np.sign(open_trades[-1].Quantity * order.Quantity) < 0
            trade = self._create_trade(open_trades[-1].Quantity, open_trades[-1], order)
            paired_trades.append(trade)
            order.Quantity += open_trades[-1].Quantity
            open_trades.pop()
        
        # last trade to match in case current order smaller than last existing order
        if abs(order.Quantity) > 0 and len(open_trades) > 0:
            assert abs(open_trades[-1].Quantity) > abs(order.Quantity)
            assert np.sign(open_trades[-1].Quantity * order.Quantity) < 0
            
            trade = self._create_trade(order.Quantity, open_trades[-1], order)
            paired_trades.append(trade)
            open_trades[-1].Quantity += order.Quantity
            order.Quantity = 0
            assert open_trades[-1].Quantity != 0   

        return paired_trades

    def _create_trade(self, quantity, start_order, end_order):
        return Trade(
            quantity, 
            start_order.LastFillTime,
            end_order.LastFillTime, 
            start_order.Price, 
            end_order.Price
            )
                        
class Trades:
    def __init__(self, orders):
        self._order_data_by_symbol: dict[Symbol, Any] = {}
        orders = [order.Clone() for order in orders]
        self._orders: List[Order] = sorted(orders, key=lambda x: x.LastFillTime)
        
        # if data is not given in Eastern timezone convert and get read of timezone info
        self._timezone = pytz.timezone('US/Eastern')
        self._convert_order_timezone()

        self._add_extra_order_data()
        
        # ToDo: Restructure the design to be part of the Asset Data by Smbol class
        self._paired_order_data_dict: dict[Symbol, Any] = {}

        self.all_paired_data: List[Trade] = []

        self.update_orders(orders)
        for symbol in self._order_data_by_symbol:
            self._paired_order_data_dict[symbol] = self._order_data_by_symbol[symbol].pair_all_trades()


    def _convert_order_timezone(self):
        for order in self._orders:
            if order.LastFillTime.tzinfo is not None:  
                order.LastFillTime = order.LastFillTime.astimezone(self._timezone).replace(tzinfo=None)


    def _add_extra_order_data(self):
        # to be overriden in child classes in case some extra data need to be added to orders
        pass

    def update_orders(self, orders): 
        for order in orders:
            if order.Symbol not in self._order_data_by_symbol:
                self._order_data_by_symbol[order.Symbol] = AssetTradeData(order.Symbol)
            order_data = self._order_data_by_symbol[order.Symbol]
            is_buy = order.Quantity > 0
            if is_buy:
                order_data.update_buy(time=order.LastFillTime, order=order) 
            else: 
                order_data.update_sell(time=order.LastFillTime, order=order)

    def __getitem__(self, symbol):
         return self._order_data_by_symbol[symbol]



class OptionTrades(Trades):
    def __init__(self, orders: List[Order], algo):
        for order in orders: 
            assert order.SecurityType == SecurityType.Option or order.SecurityType == SecurityType.IndexOption
            
        
        #self._orders = orders
        self._underlying_to_orders_dict: Dict[Symbol, List[Order]]
        self._algo = algo

        super().__init__(orders)
                        
        
    def _add_extra_order_data(self):
        '''
        Overrides method called in constructor in base class to add underlying data to order data
        '''
        self._underlying_to_orders_dict = self._create_underlying_order_correspondence()
        self._get_underlying_prices()


    @property 
    def underlyings(self):
        return list(self._underlying_to_orders_dict.keys())

    def _create_underlying_order_correspondence(self):
        underlying = {}
        for order in self._orders: 
            if order.Symbol.Underlying not in underlying:
                underlying[order.Symbol.Underlying] = [order]
            else:
                underlying[order.Symbol.Underlying].append(order)
        return underlying


    def _get_underlying_prices(self):
        '''
        Adds underlying price to every order as a field UnderlyingPrice. 
        ToDo: fix dirty solution of adding an extra field to each instance of orders. 
        '''

        for symbol in self._underlying_to_orders_dict:
            orders = sorted(self._underlying_to_orders_dict[symbol], key=lambda x: x.LastFillTime)

            min_date = orders[0].LastFillTime
            max_date = orders[-1].LastFillTime + timedelta(days=1)
        
            df = self._algo.History(symbol, min_date, max_date, Resolution.Hour)  
            df.index = df.index.droplevel(0)
            
            for order in orders:
                
                #ToDo: Handle below case properly
                if order.Type == OrderType.OptionExercise:
                    order.UnderlyingPrice = None
                    continue
                order.UnderlyingPrice = df.loc[order.LastFillTime, 'close']
             
             
    def _create_trade(self, quantity, start_order, end_order):
        assert start_order.UnderlyingPrice is not None

        return Trade(
            quantity, 
            start_order.LastFillTime,
            end_order.LastFillTime, 
            start_order.Price, 
            end_order.Price,
            start_order.UnderlyingPrice, 
            end_order.UnderlyingPrice
            )
    
    
    def get_df_order_vs_underlying(self, underlying, order_direction = None):
        if underlying not in self._underlying_to_orders_dict:
            raise ValueError(f"{Symbol} is not in underlyings")

        orders = self._underlying_to_orders_dict[underlying]
        
        columns = ['underlying_price', 'order_price', 'percent', 'quantity', 'strike']
        index: List[datetime] = [order.LastFillTime for order in orders if order_direction is None or order_direction == order.Direction]
        data = np.empty((len(index),len(columns)), dtype='float')
        res = pd.DataFrame(columns=columns, index=index, data=data)

        for i, order in enumerate(orders):
            if order_direction is None or order_direction == order.Direction:
                res.iloc[i]['underlying_price'] = order.UnderlyingPrice
                res.iloc[i]['order_price'] = order.Price
                res.iloc[i]['percent'] = (order.Price / order.UnderlyingPrice) * 100 if order.UnderlyingPrice is not None else None
                res.iloc[i]['quantity'] = order.Quantity
                res.iloc[i]['strike'] = order.Symbol.ID.StrikePrice
        return res
#region imports
from AlgorithmImports import *
from options import *
#endregion
def default_strategy(algo: QCAlgorithm):


    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)

    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol
    strategy = {
        'Type': 'Sell',
        'Days to Expiry': 360,
        'Strike': 1.05,
        #'Amount Calculator': FractionToExpiryAmountCalculator(algo=algo, notional_frac=1.0, multiplier=1.5),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((5*1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=1.5/30),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=1),

        'Right': OptionRight.PUT, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW')], 

        'Resolution': Resolution.MINUTE,
        'Execution Time': 90,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        #'Monetization': monetize_delta_45,
        'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        'date_schedule_rule' : algo.DateRules.EveryDay,
        #'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyByStrikeAndExpiry
    }
    return strategy



def default_buy_by_delta_strategy(algo: QCAlgorithm):
    spy = algo.AddEquity('SPY').Symbol
    strategy = default_strategy(algo)
    strategy['buy_class'] = BuyByDeltaAndExpiry
    strategy.pop('Strike')
    strategy['target_delta'] = 0.05
    return strategy

def sell_call_strategy(algo: QCAlgorithm()):
    algo.set_name('sell_call_105_30')
    strategy = default_buy_by_delta_strategy(algo)
    strategy['Right'] =  OptionRight.CALL
    strategy['amount_calculator'] = FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/4), cap=None)
    strategy['date_schedule_rule'] = algo.DateRules.WeekStart
    strategy['time_schedule_rule'] = algo.TimeRules.BeforeMarketClose
    strategy['buy_class'] = BuyByStrikeAndExpiry
    strategy['Strike'] = 1.05
    return strategy 

def sell_call_by_delta_strategy(algo: QCAlgorithm()):
    algo.set_name('sell_call_delta_5_360')
    strategy = default_buy_by_delta_strategy(algo)
    strategy['Right'] =  OptionRight.CALL
    strategy['amount_calculator'] = FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/52), cap=None)
    strategy['date_schedule_rule'] = algo.DateRules.WeekStart
    strategy['time_schedule_rule'] = algo.TimeRules.BeforeMarketClose
    return strategy 

def default_daily_dynamic_hedge_strategy(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)
    monetize_delta_65 = MonetizeByDelta(algo=algo, delta=0.65)
    monetize_delta_70 = MonetizeByDelta(algo=algo, delta=0.7)



    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol

    algo.set_name('hedge_90_90_cap_15_mon_55')
    strategy = {
        # 'Type': 'Sell',
        'Type': 'Buy',
        'Days to Expiry': 90,
        'Strike': 0.9,
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 5 * (1.5/360), holding_symbol=spy, cap=0.01/52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(4*5*(1.5)/360), cap=0.015 / 52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(2*5*(1.5)/180), cap=None),


        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        #'Monetization': monetize_delta_50,

        #'Monetization': monetize_delta_45,

        'Monetization': monetize_delta_55,
        #'Monetization': monetize_delta_50,
        #'Monetization': monetize_delta_40,
        #'Monetization': monetize_delta_60,
        #'Monetization': monetize_delta_65,
        #'Monetization': monetize_delta_70,







        #'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        #'date_schedule_rule' : algo.DateRules.EveryDay,
        'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyByStrikeAndExpiry
    }
    return strategy

def default_daily_dynamic_hedge_strategy1(algo: QCAlgorithm):
        monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
        monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
        monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
        monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
        monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

        monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
        monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
        monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
        monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
        monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)

        monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
        monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

        spy = algo.AddEquity('SPY').Symbol
        spx = algo.AddIndex('SPX').Symbol
        strategy = {
            'Type': 'Buy',
            'Days to Expiry': 360,
            'Strike': 0.8,
            #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/180)),
            #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
            #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
            #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier=0.33 * 1.5/360, holding_symbol=spy),
            'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 1/52, holding_symbol=spy, cap=0.01 / 52),


            'Right': OptionRight.Put, 
            'underlying': spx,
            #'underlying': spy,
            'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
            #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

            #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
            #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
            #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
            'Resolution': Resolution.MINUTE,
            'Execution Time': 60,
            'Options Filter Time': 120,
            'Before Target Expiry': False,
            #'Monetization': monetize_110_percent_in_money,
            'Monetization': monetize_delta_45,
            #'Monetization': None,
            #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
            #'filter': VixThresholdFilter(algo=algo, threshold=20),
            'filter': None,
            'date_schedule_rule' : algo.DateRules.EveryDay,
            #'date_schedule_rule' : algo.DateRules.WeekStart, 
            'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
            'buy_class': BuyByStrikeAndExpiry
        }
        return strategy

def main_leg_spread(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)
    monetize_delta_65 = MonetizeByDelta(algo=algo, delta=0.65)
    monetize_delta_70 = MonetizeByDelta(algo=algo, delta=0.7)



    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol

    strategy = {
        'Type': 'Buy',
        'Days to Expiry': 360,
        'Strike': 0.85,
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 5 * (1.5/360), holding_symbol=spy, cap=0.01/52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(2*5*(1.5)/360), cap=0.015 / 52),
        'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/360), cap=None),


        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        #'Monetization': monetize_delta_50,
        #'Monetization': monetize_delta_45,
        #'Monetization': monetize_delta_55,
        #'Monetization': monetize_delta_50,
        'Monetization': monetize_delta_40,
        #'Monetization': monetize_delta_60,
        #'Monetization': monetize_delta_65,
        #'Monetization': monetize_delta_70,
        #'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        #'date_schedule_rule' : algo.DateRules.EveryDay,
        'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyByStrikeAndExpiry
    }
    return strategy

def secondary_leg_spread(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)
    monetize_delta_65 = MonetizeByDelta(algo=algo, delta=0.65)
    monetize_delta_70 = MonetizeByDelta(algo=algo, delta=0.7)
    monetize_delta_75 = MonetizeByDelta(algo=algo, delta=0.7)




    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol

    strategy = {
        'Type': 'Sell',
        'Days to Expiry': 360,
        'Strike': 0.8,
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 5 * (1.5/360), holding_symbol=spy, cap=0.01/52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(2*5*(1.5)/360), cap=0.015 / 52),
        'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/360), cap=None),


        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,

        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        #'date_schedule_rule' : algo.DateRules.EveryDay,
        'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyByStrikeAndExpiry
    }
    return strategy

def default_spread_strategy(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)
    monetize_delta_65 = MonetizeByDelta(algo=algo, delta=0.65)
    monetize_delta_70 = MonetizeByDelta(algo=algo, delta=0.7)



    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol

    algo.set_name('spread_360_85_80_mon_1_leg')
    strategy = {
        'Type': 'Buy',
        'Days to Expiry': 180,
        'Strike': 0.85,
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 5 * (1.5/360), holding_symbol=spy, cap=0.01/52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(2*5*(1.5)/360), cap=0.015 / 52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/360), cap=None),

        'main_leg': main_leg_spread(algo),
        'secondary_leg': secondary_leg_spread(algo),

        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        'Monetization': MonetizeSpreadByDelta(algo, delta=0.5, monetize_connections=False),

        #'Monetization': monetize_delta_45,

        #'Monetization': monetize_delta_55,
        #'Monetization': monetize_delta_50,
        #'Monetization': monetize_delta_40,
        #'Monetization': monetize_delta_60,
        #'Monetization': monetize_delta_65,
        #'Monetization': monetize_delta_70,

        #'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        #'date_schedule_rule' : algo.DateRules.EveryDay,
        'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuySpread
    }
    return strategy

def default_collar_strategy(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)
    monetize_delta_65 = MonetizeByDelta(algo=algo, delta=0.65)
    monetize_delta_70 = MonetizeByDelta(algo=algo, delta=0.7)



    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol

    algo.set_name('spread_360_85_80_mon_1_leg')
    strategy = {
        'Type': 'Buy',
        'Days to Expiry': 180,
        'Strike': 0.85,
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 5 * (1.5/360), holding_symbol=spy, cap=0.01/52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/180)),
        'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(2*5*(1.5)/360), cap=0.015 / 52),
        #'amount_calculator': FractionOfCashAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(5*(1.5)/360), cap=None),

        'main_leg': main_leg_spread(algo),
        'secondary_leg': secondary_leg_spread(algo),

        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        'Monetization': MonetizeSpreadByDelta(algo, delta=0.5, monetize_connections=False),

        #'Monetization': monetize_delta_45,

        #'Monetization': monetize_delta_55,
        #'Monetization': monetize_delta_50,
        #'Monetization': monetize_delta_40,
        #'Monetization': monetize_delta_60,
        #'Monetization': monetize_delta_65,
        #'Monetization': monetize_delta_70,

        #'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        #'filter': VixThresholdFilter(algo=algo, threshold=20),
        'filter': None,
        #'date_schedule_rule' : algo.DateRules.EveryDay,
        'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyCollar
    }
    return strategy


def default_daily_dynamic_hedge_strategy2(algo: QCAlgorithm):
    monetize_60_day_before_expiry = MonetizeFixedTimeBeforeExpiry(algo=algo, min_days_to_expiry=60)
    monetize_90_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=0.9)
    monetize_105_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.05)
    monetize_110_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.1)
    monetize_100_percent_in_money = MonetizeRelativeToUnderlying(algo=algo, percent_of_underlying=1.0)

    monetize_delta_40 = MonetizeByDelta(algo=algo, delta=0.4)
    monetize_delta_45 = MonetizeByDelta(algo=algo, delta=0.45)
    monetize_delta_50 = MonetizeByDelta(algo=algo, delta=0.5)
    monetize_delta_55 = MonetizeByDelta(algo=algo, delta=0.55)
    monetize_delta_60 = MonetizeByDelta(algo=algo, delta=0.6)

    monetization_list = [monetize_60_day_before_expiry, monetize_90_percent_in_money]
    monetize_list = ChainMonetizations(algo=algo, monetizations_list=monetization_list)

    spy = algo.AddEquity('SPY').Symbol
    spx = algo.AddIndex('SPX').Symbol
    strategy = {
        'Type': 'Buy',
        'Days to Expiry': 360,
        'Strike': 0.9,
        'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/180)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=((1.5)/360)),
        #'amount_calculator': ConstantFractionAmountCalculator(algo=algo, notional_frac=1.0, multiplier=(1/5)),
        #'amount_calculator': FractionOfHoldingAmountCalculator(algo=algo, notional_frac=1.0, multiplier= 0.33 * (1.5/180), holding_symbol=spy),


        'Right': OptionRight.Put, 
        'underlying': spx,
        #'underlying': spy,
        'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spy, 'SPY', Market.USA, '?SPY')],

        #'canonical_option_symbols': [Symbol.CreateCanonicalOption(spx, 'SPXW', Market.USA, '?SPXW'), 
        #                            Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')], 
        #                        Symbol.CreateCanonicalOption(spx, 'SPX', Market.USA, '?SPX')],
        'Resolution': Resolution.MINUTE,
        'Execution Time': 60,
        'Options Filter Time': 120,
        'Before Target Expiry': False,
        #'Monetization': monetize_110_percent_in_money,
        'Monetization': monetize_delta_55,
        #'Monetization': None,
        #'Filter': VixFilter(algo=algo, threshold=1.1, days=90),
        'filter': VixThresholdFilter(algo=algo, threshold=20),
        #'Filter': None,
        'date_schedule_rule' : algo.DateRules.EveryDay,
        #'date_schedule_rule' : algo.DateRules.WeekStart, 
        'time_schedule_rule': algo.TimeRules.BeforeMarketClose,
        'buy_class': BuyByStrikeAndExpiry
    }
    return strategy
# region imports
from AlgorithmImports import *
import QuantLib as ql
# endregion

# using seconds from datetime difference so we can pass 0 day option also
def get_t_days(expiration_date: datetime, calc_date: datetime) -> float:
    dt = (expiration_date - calc_date)
    days_dt: float = dt.days
    seconds_dt: float = dt.seconds / (24*60*60) # days
    t: float = seconds_dt + days_dt

    return t

def black_model(
    option_price: float, 
    forward_price: float, 
    strike_price: float, 
    option_type: int,
    expiration_date: datetime, 
    calc_date: datetime,
    discount_factor: float = 1
) -> Tuple[float]:

    implied_vol = ql.blackFormulaImpliedStdDev(option_type, strike_price, forward_price, option_price, discount_factor)
    strikepayoff = ql.PlainVanillaPayoff(option_type, strike_price)
    black = ql.BlackCalculator(strikepayoff, forward_price, implied_vol, discount_factor)

    # t: float = (expiration_date - calc_date).days / 360
    t: float = get_t_days(expiration_date, calc_date) / 360
    implied_vol = implied_vol / np.sqrt(t)
    opt_price: float = black.value()

    return implied_vol, black.delta(discount_factor * forward_price), opt_price

def black_model_opt_price(
    forward_price: float, 
    strike_price: float, 
    option_type: int, 
    implied_vol: float, 
    expiration_date: datetime, 
    calc_date: datetime,
    discount_factor: float = 1
) -> float:

    # t: float = (expiration_date - calc_date).days / 360
    t: float = get_t_days(expiration_date, calc_date) / 360
    iv: float = implied_vol * np.sqrt(t)

    strikepayoff = ql.PlainVanillaPayoff(option_type, strike_price)
    black = ql.BlackCalculator(strikepayoff, forward_price, iv, discount_factor)
    opt_price: float = black.value()

    return opt_price
# region imports
from AlgorithmImports import *
import QuantLib as ql
# endregion

# TODO use black model from option pricing class

class ImpliedVolatilityIndicator(PythonIndicator):
    def __init__(
        self, 
        algo: QCAlgorithm,
        continuous_future: Future, 
        name: str, 
        moneyness: float,
        target_expiry_days: int = 25, 
    ) -> None:

        super().__init__()
        
        self._algo: QCAlgorithm = algo
        self._continuous_future: Future = continuous_future
        self._moneyness: float = moneyness
        self._target_expiry_days: int = target_expiry_days

        self.name: str = name
        self.value: float = 0.

    def update(self, input: TradeBar) -> bool:
        if not isinstance(input, TradeBar):
            raise TypeError('ImpliedVolatilityIndicator.update: input must be a TradeBar')

        self.value = 0.
        found: bool = False

        # find two future contracts; one of them has sooner expiry than target expiry; other one expires later
        before_target, after_target = self._find_futures_contracts()
        symbols_to_cleanup: List[Symbol] = []

        if before_target is None and after_target is None:
            self._algo.log(
                f'ImpliedVolatilityIndicator.update: No futures contracts with targeted expiry found for: {self._continuous_future.symbol}'
            )
        else:
            # subscribe futures
            before_target_future, after_target_future = tuple(
                self._subscribe_futures(before_target, after_target, symbols_to_cleanup)
            )

            # find ATM options for two futures
            before_target_option_contract, after_target_option_contract = tuple(
                self._find_atm_options(
                    before_target_future,
                    after_target_future
                )
            )
        
            found = before_target_option_contract is not None and after_target_option_contract is not None
            if not found:
                self._algo.log(
                    f'ImpliedVolatilityIndicator.update: No option contracts with targeted expiry found for: {self._continuous_future.symbol}'
                )
            else:
                # underlying future has been subscribed
                # NOTE DataNormalizationMode.RAW for underlying future is required
                if all(
                    self._algo.subscription_manager.subscription_data_config_service.get_subscription_data_configs(c.underlying_symbol)
                    for c in [before_target_option_contract, after_target_option_contract]
                ):
                    # subscribe options
                    before_target_option, after_target_option = tuple(
                        self._subscribe_options(
                            before_target_option_contract, 
                            after_target_option_contract, 
                            symbols_to_cleanup
                        )
                    )

                    # calculate implied volatility
                    self.value = self._get_implied_volatility(
                        before_target_option, 
                        after_target_option, 
                        before_target_future, 
                        after_target_future
                    )

                # remove not needed assets from algorithm
                for asset in symbols_to_cleanup:
                    self._algo.remove_security(asset)

        return found

    def _find_futures_contracts(self) -> Tuple[Optional[Symbol]]:
        future_contracts: List[Symbol] = self._algo.future_chain_provider.get_future_contract_list(
            self._continuous_future.symbol, self._algo.time
        )

        if len(future_contracts) == 0:
            self._algo.log(
                f'ImpliedVolatilityIndicator._find_futures_contract: No futures found for: {self._continuous_future.symbol}'
            )
            return None, None

        target_expiry: datetime = self._algo.time + timedelta(days=self._target_expiry_days)

        # select two futures - one of them has sooner expiry than target expiry; other one expires later
        before_target: Symbol = max((f for f in future_contracts if f.ID.date <= target_expiry), key=lambda f: f.ID.date, default=None)
        after_target: Symbol = min((f for f in future_contracts if f.ID.date >= target_expiry), key=lambda f: f.ID.date, default=None)

        return before_target, after_target

    def _find_atm_options(
        self, 
        before_target_future: Future, 
        after_target_future: Future
    ) -> Tuple[Optional[OptionContract]]:

        for target_future in [before_target_future, after_target_future]:
            option_contract_symbols = list(self._algo.option_chain(target_future.symbol, flatten=True).contracts.values())

            if len(option_contract_symbols) == 0:
                self._algo.log(
                    f'ImpliedVolatilityIndicator._find_atm_options: No options found for: {target_future.symbol}'
                )
                yield None
        
            underlying_price: float = target_future.ask_price
            expiries: List[datetime] = list(map(lambda x: x.expiry, option_contract_symbols))
            expiry: datetime = min(expiries)

            contracts: List[OptionContract] = [
                x for x in option_contract_symbols 
                if x.strike != 0 
                and x.expiry == expiry
                and (1 - self._moneyness) <= abs(underlying_price / x.strike) <= (1 + self._moneyness)
            ]
        
            if len(contracts) == 0:
                self._algo.log(
                    f'ImpliedVolatilityIndicator._find_atm_options: No options filtered for: {target_future.symbol}'
                )
                yield None
            else:
                strike: float = max(map(lambda x: x.strike, contracts))
                atm_option: OptionContract = next(
                    filter(
                        lambda x: x.right == OptionRight.CALL and x.expiry == expiry and x.strike == strike, contracts
                    )
                )
                yield atm_option
        
    def _get_implied_volatility(
        self, 
        before_target_option, 
        after_target_option, 
        before_target_future: Future, 
        after_target_future: Future
    ) -> float:

        # calculate implied volatility for each option using black model
        implied_vols: List[float] = [
            self._black_model(
                o.ask_price, 
                f.ask_price, 
                o.strike_price, 
                ql.Option.Call if o.right == OptionRight.CALL else ql.Option.Put, 
                o.expiry, 
                self._algo.time
            )
            for o, f in zip(
                [before_target_option, after_target_option],
                [before_target_future, after_target_future],
            )
        ]

        # interpolate variances
        interpolated_variance: float = self._interpolate(
            (before_target_option.expiry - self._algo.time).days,
            (after_target_option.expiry - self._algo.time).days,
            implied_vols[0] ** 2,   # variance
            implied_vols[1] ** 2,   # variance
            self._target_expiry_days
        )

        # get volatility
        return np.sqrt(interpolated_variance)
    
    def _subscribe_futures(
        self, 
        before_target: Symbol, 
        after_target: Symbol, 
        symbols_to_cleanup: List[Symbol]
    ) -> Tuple[Future]:

        # subscribe futures to QC algorithm
        for s in [before_target, after_target]:
            if not self._algo.securities.contains_key(s):
                target_future: Future = self._algo.add_future_contract(s)
                self._algo.securities[target_future.symbol].set_data_normalization_mode(DataNormalizationMode.RAW)
                
                # add symbol for a later clean up; only those symbols which were not subscribed already (those might be traded)
                symbols_to_cleanup.append(target_future.symbol)
            else:
                target_future: Future = self._algo.securities.get(s)
            
            yield target_future

    def _subscribe_options(
        self, 
        before_target_option_contract: OptionContract, 
        after_target_option_contract: OptionContract, 
        symbols_to_cleanup: List[Symbol]
    ) -> Tuple:

        # subscribe futures to QC algorithm
        for oc in [before_target_option_contract, after_target_option_contract]:
            if not self._algo.securities.contains_key(oc):
                target_option = self._algo.add_future_option_contract(oc)

                # add symbol for a later clean up; only those symbols which were not subscribed already (those might be traded)
                symbols_to_cleanup.append(target_option.symbol)
            else:
                target_option = self._algo.securities.get(oc)
            
            yield target_option

    def _black_model(
        self,
        option_price: float, 
        forward_price: float, 
        strike_price: float, 
        option_type: int,
        expiration_date: datetime, 
        calc_date: datetime,
        discount_factor: float = 1.
    ) -> float:

        implied_vol: float = ql.blackFormulaImpliedStdDev(option_type, strike_price, forward_price, option_price, discount_factor)
        # strikepayoff: float = ql.PlainVanillaPayoff(option_type, strike_price)
        # black = ql.BlackCalculator(strikepayoff, forward_price, implied_vol, discount_factor)
        t: float = (expiration_date - calc_date).days / 360
        implied_vol: float = implied_vol / np.sqrt(t)

        return implied_vol #, black.delta(discount_factor * forward_price)

    def _interpolate(self, x1: float, x2: float, y1: float, y2: float, x: float) -> float:
        return ((y2 - y1) * x + x2 * y1 - x1 * y2) / (x2 - x1)
# region imports
from AlgorithmImports import *
from collections import deque
from typing import Optional
# endregion

class RealizedVolatilityIndicator(PythonIndicator):
    def __init__(
        self,
        algo: QCAlgorithm, 
        symbol: Symbol, 
        name: str, 
        daily_period: int,
        auto_updates: bool, 
        resolution: Resolution = Resolution.DAILY
    ) -> None:

        super().__init__()

        self._daily_period: int = daily_period
        self._auto_updates: bool = auto_updates

        self.name: str = name
        self.value: float = 0.

        self._first_bar: Optional[TradeBar] = None
        self._recent_bar: Optional[TradeBar] = None
        self._return_values = deque()
        self._rolling_sum: float = 0.
        self._rolling_sum_of_squares: float = 0.
        self._n: Optional[float] = None
        
        # register indicator for automatic updates
        if auto_updates:
            algo.register_indicator(symbol, self, resolution)

    @property
    def is_auto_updated(self) -> bool:
        return self._auto_updates

    def update(self, input: TradeBar) -> bool:
        if not isinstance(input, TradeBar):
            raise TypeError('RealizedVolatilityIndicator.update: input must be a TradeBar')
        
        if not self._first_bar:
            # store first bar
            self._first_bar = input
        else:
            log_return: float = np.log(input.close / self._recent_bar.close)
            self._return_values.append(log_return)
            
            # update rolling sums
            self._rolling_sum += log_return
            self._rolling_sum_of_squares += log_return ** 2

            is_ready: bool = (input.end_time - self._first_bar.time).days >= self._daily_period
            if is_ready:
                # store number of bars
                if not self._n:
                    self._n = len(self._return_values)
                
                mean_value_1: float = self._rolling_sum / self._n
                mean_value_2: float = self._rolling_sum_of_squares / self._n

                # adjust rolling sums
                removed_return: float = self._return_values.popleft()
                self._rolling_sum -= removed_return
                self._rolling_sum_of_squares -= removed_return ** 2

                self.value = np.sqrt(mean_value_2 - (mean_value_1 ** 2)) * np.sqrt(250) # annualized
        
        self._recent_bar = input
        
        return self._n is not None
# region imports
from AlgorithmImports import *
from dataclasses import dataclass
from abc import ABC, abstractmethod
from option_pricing.option_pricing_model import OptionPricingModel, IndexOptionPricingModel
# endregion

@dataclass
class StressOptionInfo():
    underlying_option_symbol: Symbol
    spot_price: float
    forward_spot_price: float
    discount_factor: float
    dividends: float
    implied_volatility: float
    option_price: float

class StressUnderlying(ABC):
    def __init__(
        self, 
        algo: QCAlgorithm, 
        underlying: Symbol, 
        underlying_price_change_perc: Optional[float], 
        underlying_vol_change_perc: Optional[float], 
        vix: Optional[Symbol], 
        vix_target: Optional[float]
    ) -> None:

        self._algo: QCAlgorithm = algo
        self._underlying: Symbol = underlying
        self._underlying_price_change_perc: float = underlying_price_change_perc
        self._underlying_vol_change_perc: float = underlying_vol_change_perc
        self._vix: Optional[Symbol] = vix
        self._vix_target: Optional[float] = vix_target

        if self._underlying.security_type == SecurityType.INDEX:
            self._option_pricing: Optional[OptionPricingModel] = IndexOptionPricingModel(algo)
        else:
            raise TypeError(f"StressUnderlying.__init__: underlying security type: {self._underlying.security_type} is not supported")

    @property
    def option_pricing(self) -> OptionPricingModel:
        return self._option_pricing

    @abstractmethod
    def _find_port_options(self) -> List[Symbol]:
        ...

    @abstractmethod
    def _stress(self) -> List[StressOptionInfo]:
        ...

class StressIndexUnderlying(StressUnderlying):
    def __init__(
        self, 
        algo: QCAlgorithm, 
        underlying: Symbol, 
        underlying_price_change_perc: Optional[float], 
        underlying_vol_change_perc: Optional[float], 
        vix: Optional[Symbol], 
        vix_target: Optional[float]
    ) -> None:

        super(StressIndexUnderlying, self).__init__(
            algo, 
            underlying, 
            underlying_price_change_perc,
            underlying_vol_change_perc, 
            vix, 
            vix_target
        )

    @classmethod
    def using_fixed_percentage(
        cls, 
        algo: QCAlgorithm, 
        underlying: Symbol, 
        underlying_price_change_perc: float, 
        underlying_vol_change_perc: float 
    ) -> 'StressIndexUnderlying':
    	return cls(algo, underlying, underlying_price_change_perc, underlying_vol_change_perc, None, None)

    @classmethod
    def using_vix(
        cls, 
        algo: QCAlgorithm, 
        underlying: Symbol, 
        vix: Symbol, 
        vix_target: float
    ) -> 'StressIndexUnderlying':
    	return cls(algo, underlying, None, None, vix, vix_target)

    @property
    def original_option_holdings(self) -> float:
        original_option_holdings: float = sum([
            holding.value.holdings_value for holding in self._algo.portfolio 
            if self._algo.securities[holding.key].type == SecurityType.INDEX_OPTION and \
            holding.value.invested
        ])

        return original_option_holdings

    @property
    def portfolio_value(self) -> float:
        # final portfolio value after stress
        portfolio_value: float = self._algo.portfolio.total_portfolio_value - self.original_option_holdings + self.option_holdings
        return portfolio_value

    @property
    def option_holdings(self) -> float:
        # calculate holdings value for all the stressed options in portfolio
        stressed_opt_holdings: float = 0.
        stressed_port_options_info: List[StressOptionInfo] = self._stress()

        for opt_info in stressed_port_options_info:
            option_symbol: Symbol = opt_info.underlying_option_symbol
            c_multiplier: int = self._algo.securities[option_symbol].contract_multiplier
            quantity: float = self._algo.portfolio[option_symbol].quantity
            option_price: float = opt_info.option_price # stressed option price
            
            stressed_holding_value: float = quantity * option_price * c_multiplier
            stressed_opt_holdings += stressed_holding_value

        return stressed_opt_holdings

    def _find_port_options(self) -> List[StressOptionInfo]:
        result: List[StressOptionInfo] = []
        
        # spot_price: float = self._algo.securities[self._underlying].price
        spot_price: float = self._algo.current_slice.bars.get(self._underlying).close

        # go through the options in a portfolio for specified underlying, those are INDEX_OPTIONs in this case
        for holding in self._algo.portfolio:
            if self._algo.securities[holding.key].type == SecurityType.INDEX_OPTION and \
                holding.key.underlying == self._underlying and \
                holding.value.invested:

                rfr: float = self._option_pricing.get_risk_free_rate()
                dividends: float = self.option_pricing.get_dividends(holding.key)
                discount_factor: float = self.option_pricing.get_discount_factor(rfr, dividends, holding.key.id.date)
                forward_price: float = self.option_pricing.get_forward_price(spot_price, discount_factor)
                
                if holding.key.id.date <= self._algo.time:
                    # self._algo.log(f'StressIndexUnderlying._find_port_options - option {holding.key.value} has already expired; now: {self._algo.time}; expiry: {holding.key.id.date}; time delta: {holding.key.id.date - self._algo.time} days')
                    continue
                
                black: BlackModelResult = OptionPricingModel.black_model(
                    self._algo.securities[holding.key].price, 
                    forward_price, 
                    holding.key.id.strike_price, 
                    holding.key.id.option_right, 
                    holding.key.id.date, 
                    self._algo.time, 
                    discount_factor
                )
                
                result.append(
                    StressOptionInfo(
                        holding.key, 
                        spot_price, 
                        forward_price, 
                        discount_factor,
                        dividends,
                        black.implied_volatility, 
                        black.option_price, 
                    )
                )

        return result

    def _stress(self) -> List[StressOptionInfo]:
        port_options_info: List[StressOptionInfo] = self._find_port_options()

        if len(port_options_info) == 0:
            self._algo.log(f'StressIndexUnderlying._stress No options found in portfolio for {self._underlying} underlying')
            return []
        
        # go through the options in portfolio, change underlying price and recalculate options' values
        # for a given percentage change in the volatility and spot price of the underlying
        # OR
        # for a targeted VIX value when self._vix is defined

        stressed_port_options_info: List[StressOptionInfo] = [] 
        for option_info in port_options_info:
            option_symbol: Symbol = option_info.underlying_option_symbol
            
            # target VIX value
            if self._vix is not None:
                vix_diff: float = (self._algo.current_slice.bars.get(self._vix).close - self._vix_target) / 100
                # vix_diff: float = (self._algo.securities[self._vix].price - self._vix_target) / 100
                stressed_spot_price: float = option_info.spot_price
                stressed_iv: float = option_info.implied_volatility * (1 + vix_diff)
            else:
                stressed_spot_price: float = option_info.spot_price * (1 + self._underlying_price_change_perc)
                stressed_iv: float = option_info.implied_volatility * (1 + self._underlying_vol_change_perc)
            
            # discount_factor and dividends are not changed in this instance
            stressed_forward_price: float = self.option_pricing.get_forward_price(
                stressed_spot_price, 
                option_info.discount_factor
            )

            stressed_opt_price: float = OptionPricingModel.black_model_opt_price(
                stressed_forward_price, 
                option_symbol.id.strike_price, 
                option_symbol.id.option_right, 
                stressed_iv, 
                option_symbol.id.date, 
                self._algo.time, 
                option_info.discount_factor
            )

            stressed_port_options_info.append(
                StressOptionInfo(
                    option_symbol, 
                    stressed_spot_price, 
                    stressed_forward_price, 

                    # discount_factor and dividends are not changed in this instance
                    option_info.discount_factor, 
                    option_info.dividends, 
                    stressed_iv, 
                    stressed_opt_price
                )
            )

        return stressed_port_options_info
# region imports
from AlgorithmImports import *
from options import *
from stats import *
import config
from indicators.realized_volatility import RealizedVolatilityIndicator
from indicators.implied_volatility import ImpliedVolatilityIndicator
from indicators.stress_test import StressIndexUnderlying
from option_pricing.iv_monitor import IVMonitor
# endregion

class SPX_Options(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2012, 1, 1)
        self.SetEndDate(2025, 1, 1)
        self.SetCash(10e11)

        self.SetSecurityInitializer(MySecurityInitializer(self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices)))

        self.strategies = []
        
        self.days_since_rebalance = 0
        
        spx = self.AddIndex("SPX").Symbol
        self.spy = self.add_equity("SPY").Symbol
        spy = self.spy
        
        self.strategies.append(config.default_daily_dynamic_hedge_strategy(algo=self))

        self.realized_option_profits = 0        
        self._strategy_parser(self.strategies)

        self._vix: Symbol = self.add_index("VIX", Resolution.MINUTE).symbol

        # stress indicator initialized with fixed percentage
        # self._stress_indicator: StressIndexUnderlying = StressIndexUnderlying.using_fixed_percentage(
        #     self, 
        #     underlying = spx, 
        #     underlying_price_change_perc=-0.1,
        #     underlying_vol_change_perc=-0.1
        # )
        #
        # OR
        #
        # stress indicator initialized with vix
        self._stress_indicator: StressIndexUnderlying = StressIndexUnderlying.using_vix(
            algo=self, 
            underlying=spx, 
            vix=self._vix, 
            vix_target=18
        )

        self._iv_monitor: IVMonitor = IVMonitor(
            algo=self, 
            underlying=spx, 
            include_dividends=True, 
            abs_delta_to_notify=0.05
        )

        # realized volatility indicator
        vol_period: int = 21
        self._realized_vol_indicator: RealizedVolatilityIndicator = RealizedVolatilityIndicator(
            algo=self,
            symbol=spx, 
            name='RealizedVolatility',
            daily_period=vol_period,
            auto_updates=True,
            resolution=Resolution.DAILY # only relevant with auto_updates = True
        )
        
        # implied volatility indicator
        # self._implied_vol_indicator: ImpliedVolatilityIndicator = ImpliedVolatilityIndicator(
        #     algo=self,
        #     continuous_future=self._future,
        #     name='ImpliedVolatility',
        #     moneyness=self._moneyness,
        #     target_expiry_days=60,
        # )
        # self.register_indicator(self._future.symbol, self._implied_vol_indicator, Resolution.DAILY)

        # charting
        PortfolioValue(self)
        OptionsPortfolioValue(self)
        Greeks(algo=self, strategies=self.strategies)
        HoldingsValue(self, spy)

        StressTestPortfolioValue(self, self._stress_indicator)
        StressTestHoldings(self, self._stress_indicator)

        ImpliedVsRealized(self, self._realized_vol_indicator, None)

        '''
        portfolio_contents_logger = LogPortfolioContents(self)
        self.Schedule.On(
            #self.DateRules.EveryDay('SPX'),  
            #self.TimeRules.BeforeMarketClose('SPX', 0),
            self.DateRules.WeekStart('SPX'), 
            self.TimeRules.BeforeMarketClose('SPX', 0),
            portfolio_contents_logger.log_portfolio_contents
            )
        '''
        '''
        self.Schedule.On(
                    self.DateRules.EveryDay('SPY'), 
                    self.TimeRules.BeforeMarketClose('SPY', 30), 
                    self._additional_spy
                    )
        '''
        '''
        self.Schedule.On(
                    self.DateRules.EveryDay('SPY'), 
                    self.TimeRules.BeforeMarketClose('SPY', 30), 
                    self.adjust_spy
                    )
        
        self.Schedule.On(
                    self.DateRules.EveryDay(), 
                    self.TimeRules.Midnight, 
                    self.increment_rebalance_day
                    )
        '''
        '''
        fwrds = forward_prices_and_discounts_structure(self, spx)
        self.Schedule.On(
            self.DateRules.EveryDay('SPX'), 
            self.TimeRules.BeforeMarketClose('SPX', 30),
            fwrds.get_options
            )
        '''        

    def on_order_event(self, order_event: OrderEvent) -> None:
        # add new option symbols to IV monitor structure after order is filled
        order = self.transactions.get_order_by_id(order_event.order_id)
        if order_event.status == OrderStatus.FILLED:
            if order_event.symbol.security_type == SecurityType.INDEX_OPTION:
                self._iv_monitor.add(order_event.symbol)
        
    def increment_rebalance_day(self):
        # update indicator manually
        # if not self._realized_vol_indicator.is_auto_updated:
        #     self._realized_vol_indicator.update(
        #         self.current_slice.bars.get(self._future.symbol)
        #     )

        self.days_since_rebalance += 1
    
    def _additional_spy(self):
        if self.realized_option_profits > 0:
            self.Log('Option profits ' +  str(self.realized_option_profits))
            price = self.Securities[self.spy].Price
            amount = np.floor(self.realized_option_profits / price)
            self.MarketOrder(self.spy, amount)
            self.realized_option_profits = 0
    
    def adjust_spy(self):
        if self.days_since_rebalance >= 30:
            if self.portfolio.cash != 0:
                amount = round(self.portfolio.cash / self.securities[self.spy].price)
                self.market_order(self.spy, amount)
            self.days_since_rebalance = 0
        
    def on_data(self, slice: Slice) -> None:
        pass

    def _strategy_parser(self, strategies):
        for strategy in self.strategies:

            self.Securities[strategy['underlying']].SetDataNormalizationMode(DataNormalizationMode.Raw)
            
            option_strategy = strategy['buy_class'](
                algo=self, **strategy               
                )
            option_strategy.schedule()
            strategy['strategy_instance'] = option_strategy
            
            if strategy['Monetization'] is not None:
                strategy['Monetization'].add_strategy(option_strategy)
                self.Schedule.On(
                    self.DateRules.EveryDay(option_strategy.underlying), 
                    self.TimeRules.BeforeMarketClose(option_strategy.underlying, 
                        strategy['Execution Time']), strategy['Monetization'].monetize
                    )

    '''
    def OnOrderEvent(self, orderEvent: OrderEvent) -> None:
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        ticket = orderEvent.Ticket
        if orderEvent.Status == OrderStatus.Filled:
            if ticket.OrderType == OrderType.OptionExercise:
                self.Log('Option Exercised')
            if ticket.OrderType == OrderType.OptionExercise and orderEvent.FillPrice > 0:
                self.realized_option_profits += orderEvent.FillPrice * orderEvent.AbsoluteFillQuantity
                self.Log('Profit')
            elif (ticket.SecurityType == SecurityType.Option or ticket.SecurityType == SecurityType.IndexOption) and orderEvent.Direction == OrderDirection.Sell:
                if orderEvent.FillPrice > 0:
                    self.Log('Profit')
                    self.realized_option_profits += orderEvent.FillPrice * orderEvent.AbsoluteFillQuantity 
    '''

    def on_securities_changed(self, changes):
        pass
        # self.Log('changed') 

class MySecurityInitializer(BrokerageModelSecurityInitializer):

    def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None:
        super().__init__(brokerage_model, security_seeder)

    def Initialize(self, security: Security) -> None:
        # First, call the superclass definition
        # This method sets the reality models of each security using the default reality models of the brokerage model
        super().Initialize(security)

        # Next, overwrite the security buying power        
        security.set_buying_power_model(BuyingPowerModel.Null)
        security.set_fee_model(ConstantFeeModel(0))
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
import pandas as pd

class Metric(ABC):
    @abstractmethod
    def calc(data: Union[pd.Series, pd.DataFrame]):
        pass


class MeanReturn(Metric):
    def __init__(N: int):
        self._N = N

    def calc(data: Union[pd.Series, pd.DataFrame]):
        return data.mean(axis=0) * self._N

class CAGR(Metric):
    pass

class StdDev(Metric):
    def __init__(N: int):
        self._N = N
    
    def calc(data: Union[pd.Series, pd.DataFrame]):
        return data.std(axis=0) * np.sqrt(self._N)

class MaxDrawdowns(Metric):
    pass


class SharpeRatio(Metric):

    def calc(data: Union[pd.Series, pd.DataFrame]):
        sr = (weekly_ret_series.mean(axis=0) / weekly_ret_series.std(axis=0)) * np.sqrt(N)

# region imports
from AlgorithmImports import *
# endregion

class ParityPair:
    def __init__(self):
        self.put = None
        self.call = None
        self.strike = None
        self.expiry = None

    def update_parity_pair(self, contract_symbol):
        if self.expiry == contract_symbol.id.date and \
            self.strike == contract_symbol.id.strike_price:

            if contract_symbol.id.OptionRight == OptionRight.PUT:
                assert self.call is not None 
                self.put = contract_symbol
            
            else:
                assert contract_symbol.id.OptionRight == OptionRight.PUT and self.put is not None
                self.call = contract_symbol
        else: 
            self.call = None
            self.put = None
            self.expiry = contract_symbol.id.date
            self.strike = contract_symbol.id.strike_price

            if contract_symbol.id.OptionRight == OptionRight.PUT:
                self.put = contract_symbol
            
            else:
                assert contract_symbol.id.OptionRight == OptionRight.CALL
                self.call = contract_symbol
             
class forward_prices_and_discounts_structure:

    def __init__(self, algo, underlying_symbol):
        self._algo = algo
        self._underlying_symbol = underlying_symbol
        self._dates_dict = {}

    def insert_dates(self, dates: List[date]):
        for date in dates: 
            if date not in self._dates_dict:
                self._dates_dict[date] = None

    def _build_dates_dict_from_portfolio(self):
        d = {}
        for x in self._algo.portfolio:
            symbol = x.key
            if symbol.id.SecurityType == SecurityType.INDEX_OPTION and symbol.id.date not in d:
                d[symbol.id.date] = None  
        return d

    def _get_parity_pairs(self, contracts):
        first_parity_pair = ParityPair()
        second_parity_pair = ParityPair()

        underlying_price = self._algo.securities[self._underlying_symbol].price
        
        # if the number is an integer or its fractional part is 0.5, there can be two different strikes equally close to it - from below
        # and from above, which might lead to unpredicted behaviour. We break parity by subtracting a small quantity. 
        if underlying_price.is_integer() or math.modf(underlying_price)[0] == 0.5: 
            underlying_price -= 0.0001

        sorted_contracts = sorted(contracts, key=lambda x: abs(x.id.strike_price - underlying_price))

        for i in (0,2):
            if sorted_contracts[i].id.OptionRight == sorted_contracts[i+1].id.OptionRight:
                self._algo.log('a')

            assert (
                sorted_contracts[i].id.strike_price == sorted_contracts[i+1].id.strike_price and \
                sorted_contracts[i].id.OptionRight != sorted_contracts[i+1].id.OptionRight

                )
            
            assert sorted_contracts[0].id.strike_price != sorted_contracts[2].id.strike_price

        for parity_pair, i in zip((first_parity_pair, second_parity_pair), (0,2)):
            parity_pair.update_parity_pair(sorted_contracts[i])
            parity_pair.update_parity_pair(sorted_contracts[i+1])
        
        return first_parity_pair, second_parity_pair

    def get_df_and_fp(contracts):
        pass

    def get_options(self):
        self._dates_dict = self._build_dates_dict_from_portfolio()
        option_contract_symbols = list(self._algo.option_chain(self._underlying_symbol))
        
        dates_contracts_dict = {}
        for symbol in option_contract_symbols:
            if symbol.id.date in self._dates_dict:
                if symbol.id.date in dates_contracts_dict:
                    dates_contracts_dict[symbol.id.date].append(symbol)
                else: 
                    dates_contracts_dict[symbol.id.date] = [symbol]
            
        parity_pairs_dict = {}
            
        for date in dates_contracts_dict:
            parity_pairs_dict[date] = self._get_parity_pairs(dates_contracts_dict[date])
# region imports
from AlgorithmImports import *
from .option_pricing_model import OptionPricingModel, IndexOptionPricingModel
from decimal import Decimal
# endregion

class IVMonitor():
    def __init__(
        self, 
        algo: QCAlgorithm, 
        underlying: Symbol, 
        include_dividends: bool = True, 
        abs_delta_to_notify: float = 0.
    ) -> None:

        self._algo: QCAlgorithm = algo
        self._underlying: Symbol = underlying
        self._include_dividends: bool = include_dividends
        self._abs_delta_to_notify: float = abs_delta_to_notify
        self._scheduler()

        if self._underlying.security_type == SecurityType.INDEX:
            self._option_pricing: Optional[OptionPricingModel] = IndexOptionPricingModel(algo)
        else:
            raise TypeError(f"IVMonitor.__init__: underlying security type: {self._underlying.security_type} is not supported")

        # QC's implied volatility indicator indexed by option symbol
        self._iv_by_option: Dict[Symbol, ImpliedVolatility] = {}

    def add(self, option_symbol: Symbol) -> None:
        if option_symbol not in self._iv_by_option:
            iv_indicator: ImpliedVolatility = ImpliedVolatility(
                option=option_symbol, 
                risk_free_rate_model=self._algo.risk_free_interest_rate_model, 
                dividend_yield_model=DividendYieldProvider.create_for_option(option_symbol) if self._include_dividends else ConstantDividendYieldModel(0), 
                mirror_option=None, 
                option_model=OptionPricingModelType.BLACK_SCHOLES
            )

            # iv_indicator: ImpliedVolatility = self._algo.iv(
            #     symbol=option_symbol, 
            #     dividend_yield=None if self._include_dividends else 0., 
            #     option_model=OptionPricingModelType.BLACK_SCHOLES
            # )

            self._algo.warm_up_indicator(option_symbol, iv_indicator)

            self._iv_by_option[option_symbol] = iv_indicator

    def _scheduler(self):
        self._algo.schedule.on(
            self._algo.date_rules.every_day(self._underlying), 
            self._algo.time_rules.before_market_close(self._underlying, 0), 
            self._compare_iv
        )

    def _compare_iv(self) -> None:
        # NOTE this can be deleted later - used only for assertion
        def trim_to_a_point(num: float, dec_point: int = 4) -> float:
            factor: int = 10**dec_point
            num = num * factor
            num = int(num)
            num = num / factor
            return num

        # NOTE this can be deleted later - used only for assertion
        def get_precision(num: float) -> int:
            d_num: Decimal = Decimal(str(num))
            sign, digits, exp = d_num.as_tuple()
            precision: int = abs(exp)
            return precision

        option_symbols_to_remove: List[Symbol] = []

        current_slice: Slice = self._algo.current_slice
        underlying_bar: TradeBar = current_slice.bars.get(self._underlying)

        # iterate through the option symbols and measure the difference in our black model IV value and QC's indicator IV value
        for option_symbol, iv_indicator in self._iv_by_option.items():
            # update indicator
            opt_bar: QuoteBar = current_slice.quote_bars.get(option_symbol)
            
            if underlying_bar and opt_bar:
                iv_indicator.update(IndicatorDataPoint(self._underlying, underlying_bar.end_time, underlying_bar.close))
                iv_indicator.update(IndicatorDataPoint(option_symbol, opt_bar.end_time, opt_bar.close))
            
            dte: int = (option_symbol.id.date - self._algo.time).days

            if dte <= 0:
                option_symbols_to_remove.append(option_symbol)
                # self._algo.log(f'IVMonitor._compare_iv - option {option_symbol.value} has already expired; now: {self._algo.time}; expiry: {option_symbol.id.date}; time delta: {option_symbol.id.date - self._algo.time} days')
                continue

            if not self._algo.portfolio[option_symbol].invested:
                continue

            if iv_indicator.is_ready:
                spot_price: float = underlying_bar.close

                if self._include_dividends:
                    dividends: float = self._option_pricing.get_dividends(option_symbol)
                else:
                    dividends: float = 0.

                rfr: float = self._option_pricing.get_risk_free_rate()
                discount_factor: float = self._option_pricing.get_discount_factor(rfr, dividends, option_symbol.id.date)
                forward_price: float = self._option_pricing.get_forward_price(spot_price, discount_factor)
                
                black: BlackModelResult = OptionPricingModel.black_model(
                    opt_bar.close, 
                    forward_price, 
                    option_symbol.id.strike_price, 
                    option_symbol.id.option_right, 
                    option_symbol.id.date, 
                    self._algo.time, 
                    discount_factor
                )

                # lagging 1 day
                precalculated_iv: float = self._algo.current_slice.option_chains.get(option_symbol.canonical).contracts.get(option_symbol).implied_volatility
                
                qc_indicator_iv: float = iv_indicator.current.value
                
                black_iv: float = black.implied_volatility

                # NOTE these can be removed later - assertion only
                qc_opt_price: float = opt_bar.close
                indicator_opt_price: float = iv_indicator.price.current.value
                black_opt_price: float = round(black.option_price, get_precision(qc_opt_price)) # round to get rid of black model floating point precision to a point when price is comparable to QC's price
                indicator_spot_price: float = iv_indicator.underlying_price.current.value

                assert trim_to_a_point(iv_indicator.dividend_yield.current.value) == trim_to_a_point(dividends), f'indicator dividends: {iv_indicator.dividend_yield.current.value}, black dividends: {dividends}'
                assert trim_to_a_point(iv_indicator.risk_free_rate.current.value) == trim_to_a_point(rfr), f'indicator RFR: {iv_indicator.risk_free_rate.current.value}, black RFR: {rfr}'
                assert indicator_opt_price == qc_opt_price, f'indicator option price does not equal to last available QC option price; QC option price {qc_opt_price}; indicator option price: {indicator_opt_price}'
                assert indicator_opt_price == black_opt_price, f'indicator option price does not equal to black model option price; black option price {black_opt_price}; indicator option price: {indicator_opt_price}'
                assert indicator_spot_price == spot_price, f'indicator spot price does not equal to QC spot price; QC spot price {spot_price}; indicator spot price: {indicator_spot_price}'

                diff: float = black_iv / qc_indicator_iv - 1
                if abs(diff) >= self._abs_delta_to_notify:
                    self._algo.log(
                        f'{option_symbol.value} - Black model IV: ' + '{:.2f}'.format(black_iv) + '; QC IV indicator: ' + '{:.2f}'.format(qc_indicator_iv) + '; diff: ' + '{:.2f}'.format(diff*100) + '%; DTE: ' + f'{dte}'
                    )
        
        # remove expired options
        for symbol in option_symbols_to_remove:
            del self._iv_by_option[symbol]
# region imports
from AlgorithmImports import *
import QuantLib as ql
from abc import ABC, abstractmethod
from dataclasses import dataclass
from math import e
# endregion

@dataclass
class BlackModelResult():
    implied_volatility: float
    delta: float
    option_price: float

class OptionPricingModel(ABC):
    def __init__(self, algo: QCAlgorithm) -> None:
        self._algo: QCAlgorithm = algo

    @abstractmethod
    def get_forward_price(
        self, 
        spot_price: float, 
        discount_factor: float, 
    ) -> float:
        ...

    @abstractmethod
    def get_risk_free_rate(self, calc_dt: Optional[datetime] = None) -> float:
        ...

    @abstractmethod
    def get_discount_factor(
        self, 
        rfr: float, 
        dividends: float, 
        expiry: datetime, 
        calc_dt: Optional[datetime] = None
    ) -> float:
        ...
    
    @abstractmethod
    def get_dividends(self, option_symbol: Symbol, calc_dt: Optional[datetime] = None) -> float:
        ...

    @staticmethod
    def black_model(
        option_price: float, 
        forward_price: float, 
        strike_price: float, 
        option_right: OptionRight, 
        expiration_date: datetime, 
        calc_dt: datetime,
        discount_factor: float = 1
    ) -> BlackModelResult:

        option_type = ql.Option.Call if option_right == OptionRight.CALL else ql.Option.Put
        implied_vol = ql.blackFormulaImpliedStdDev(option_type, strike_price, forward_price, option_price, discount_factor)
        strikepayoff = ql.PlainVanillaPayoff(option_type, strike_price)
        black = ql.BlackCalculator(strikepayoff, forward_price, implied_vol, discount_factor)

        # t: float = (expiration_date - calc_date).days / 360
        t: float = OptionPricingModel.get_t_days(expiration_date, calc_dt) / 360
        implied_vol = implied_vol / np.sqrt(t)
        opt_price: float = black.value()

        return BlackModelResult(
            implied_vol, 
            black.delta(discount_factor * forward_price), 
            opt_price
        )

    @staticmethod
    def black_model_opt_price(
        forward_price: float, 
        strike_price: float, 
        option_right: OptionRight, 
        implied_vol: float, 
        expiration_date: datetime, 
        calc_dt: datetime,
        discount_factor: float = 1
    ) -> float:

        option_type = ql.Option.Call if option_right == OptionRight.CALL else ql.Option.Put
        # t: float = (expiration_date - calc_date).days / 360
        t: float = OptionPricingModel.get_t_days(expiration_date, calc_dt) / 360
        iv: float = implied_vol * np.sqrt(t)

        strikepayoff = ql.PlainVanillaPayoff(option_type, strike_price)
        black = ql.BlackCalculator(strikepayoff, forward_price, iv, discount_factor)
        opt_price: float = black.value()

        return opt_price
    
    @staticmethod
    def get_t_days(expiration_date: datetime, calc_dt: datetime) -> float:
        dt = (expiration_date - calc_dt)
        days_dt: float = dt.days
        seconds_dt: float = dt.seconds / (24*60*60) # days
        t: float = seconds_dt + days_dt

        return t
        
class IndexOptionPricingModel(OptionPricingModel):
    def __init__(self, algo: QCAlgorithm) -> None:
        super(IndexOptionPricingModel, self).__init__(algo)

    def get_forward_price(
        self, 
        spot_price: float, 
        discount_factor: float
    ) -> float:

        F: float = spot_price / discount_factor
        return F

    def get_risk_free_rate(self, calc_dt: Optional[datetime] = None) -> float:
        if calc_dt is None: calc_dt = self._algo.time

        rfr: float = RiskFreeInterestRateModelExtensions.get_risk_free_rate(
            self._algo.risk_free_interest_rate_model, 
            calc_dt, calc_dt
        )
        return rfr

    def get_discount_factor(
        self, 
        rfr: float, 
        dividends: float, 
        expiry: datetime, 
        calc_dt: Optional[datetime] = None
    ) -> float:
        if calc_dt is None: calc_dt = self._algo.time

        discount_factor: float = e**((-rfr+dividends)*((expiry - calc_dt).days / 360))
        return discount_factor

    def get_dividends(self, option_symbol: Symbol, calc_dt: Optional[datetime] = None) -> float:
        if calc_dt is None: calc_dt = self._algo.time

        # https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/dividend-yield/key-concepts
        # DividendYieldProvider, which provides the continuous yield calculated from all dividend payoffs from the underlying Equity over the previous 350 days.
        # SPX => SPY dividends
        # NDX => QQQ dividends
        # VIX => 0
        return DividendYieldProvider.create_for_option(option_symbol).get_dividend_yield(calc_dt, self._algo.securities[option_symbol.underlying].price)
#region imports
from AlgorithmImports import *
from abc import ABC, abstractmethod
#endregion

class Filter(ABC):
    @abstractmethod
    def filter(self):
        pass

class AmountCalculator(ABC):
    def __init__(self, algo):
        self._algo = algo
    
    @abstractmethod
    def calc_amount(self, option):
        pass

    def _get_amount_as_fraction_of_portfolio(self, option, fraction_of_portfolio_value):
        multiplier = option.ContractMultiplier
        target_notional = self._algo.Portfolio.TotalPortfolioValue * fraction_of_portfolio_value
        notional_of_contract = multiplier * option.Underlying.Price
        amount = target_notional / notional_of_contract

        return amount

    def _get_amount_as_fraction_of_cash(self, option, fraction):
        holding_value = self._algo.portfolio.cash
        multiplier = option.ContractMultiplier
        target_notional = holding_value * fraction
        notional_of_contract = multiplier * option.Underlying.Price
        amount = target_notional / notional_of_contract
        return amount
    

class BuyStrategy(ABC):
    # ToDo: get rid of general params and make specific parameters/
    def __init__(self, 
            algo: QCAlgorithm, 
            underlying: Symbol,
            canonical_option_symbols: List[Symbol],
            amount_calculator: AmountCalculator,
            filter: Optional[Filter], 
            date_schedule_rule: Callable,
            time_schedule_rule: Callable,
            **params, 
            ):
        '''
        date_scheduler_rule: a pointer to a function in QCAlgorithm.DateRules
        time_schedlue_rule: a pointer to a function in QCAlgorithm.TimeRules
        '''
        self._params = params
        self._resolution = params['Resolution']
        self._algo = algo
        self._underlying = underlying
        self._filter = filter
        self._amount_calculator = amount_calculator
        self._active_options: Set[Symbol] = set()
        self._canonical_options = canonical_option_symbols
        self._current_orders = {}
        self._time_rule = time_schedule_rule
        self._date_rule = date_schedule_rule
        self._garbage_collection_set = set()   #Securities that we subscribed to but do not need. To be collected by garbage collection. 
        self._to_execute_dict = {}
        self._options_to_buy = None
    

    def schedule(self):
        algo = self._algo
        algo.Schedule.On(self._date_rule(self._underlying), self._time_rule(self._underlying, self._params['Options Filter Time']), self._scheduled_get_options)
        algo.Schedule.On(self._date_rule(self._underlying), self._time_rule(self._underlying, self._params['Execution Time']), self.buy)
        #algo.Schedule.On(algo.DateRules.EveryDay(), algo.TimeRules.Midnight, self._garbage_collection)
 


    def execute(self):
        pass

    @property 
    def underlying(self):
        return self._underlying 

    @property
    def active_options(self):
        return self._active_options

    def _get_options(self):
        pass

    def _get_amount(self, option):
        amount = self._amount_calculator.calc_amount(option)
        if self._params['Type'] == 'Sell':
            amount = -amount
        elif self._params['Type'] != 'Buy':
            raise NotImplementedError()
         
        return amount

    def _scheduled_get_options(self):
        self._options_to_buy = self._get_options()
        symbols = self._options_to_buy
        if symbols is None: 
            return
        for symbol in symbols: 
            option = self._subscribe_to_option(symbol)
            if not self._algo.securities[symbol].is_tradable:
                self._algo.securities[symbol].is_tradable = True


    def buy(self):
        if self._filter is not None:
            if not self._filter.filter():
                return

        #symbols = self._get_options()
        symbols = self._options_to_buy

        if symbols is None:
            #self._algo.Log('stop')
            return


        assert len(symbols) == 1
        for symbol in symbols: 
            option = self._subscribe_to_option(symbol)
            amount = self._get_amount(option)        
    
            if self._algo.securities[symbol].Price == 0:
                self._algo.log(f'No price data {self._algo.Time} :: {symbol.Value}')
                #return
        
            ticket = self._algo.market_order(symbol=symbol, quantity=amount)            
            #self._current_orders[ticket.OrderId] = {
            #                                        'ticket': ticket, 
            #}    
            #self._algo.Log(f'Time to expiry {(option.expiry -  self._algo.time).days}')
            self._active_options.add(symbol)
        
    
    def _get_options_chains(self):
        symbols = []
        for canonical_option in self._canonical_options:
            #symbols += list(self._algo.OptionChainProvider.GetOptionContractList(canonical_option, self._algo.Time))
            symbols += list(option.key for option in self._algo.option_chain(canonical_option).contracts)
        return symbols

    def get_Greeks(self)->Dict[Symbol, Dict[str, float]]:
        '''
        Obtains Greeks and Implied vol for options traded under current strategy
        and still held in portfolio. 
        ToDo: identify amounts that were bought only using current strategy. 
        ToDo: right now if another strategy bought same options, there will be
        ToDo: double counting. 
        '''

        slice = self._algo.CurrentSlice
        contracts: Dict[Symbol, OptionContract] = {}
        for canonical_option in self._canonical_options: 
            if canonical_option in slice.OptionChains:
                contracts.update({contract.Symbol: contract for contract in slice.OptionChains[canonical_option]})
        
        res = {}
        for option in self._active_options:
            if option in self._algo.Portfolio and self._algo.Portfolio[option].Quantity != 0:
                if option not in contracts:
                    self._algo.Log(f'{self._algo.Time} has problem with Greeks')
                    return
                
                res.update(
                    {
                        option: {
                            'ImpliedVol': contracts[option].ImpliedVolatility, 
                            'Greeks': contracts[option].Greeks
                        }
                    }
                    ) 
                
        return res

    
    def _garbage_collection(self):
        to_remove = []
        for symbol in self._active_options:
            if symbol not in self._algo.Portfolio or self._algo.Portfolio[symbol].Quantity == 0:
                self._algo.RemoveOptionContract(symbol)
                to_remove.append(symbol)

        for symbol in to_remove: 
            self._active_options.remove(symbol)

        to_remove = []
        for symbol in self._garbage_collection_set:
             if symbol not in self._algo.Portfolio or self._algo.Portfolio[symbol].Quantity == 0:
                self._algo.RemoveOptionContract(symbol)

        for symbol in to_remove: 
            self._garbage_collection.remove(symbol)


    

    def _filter_puts(self, symbols):
        puts = [symbol for symbol in symbols if symbol.ID.OptionRight == OptionRight.Put]
        return puts
        

    def _filter_calls(self, symbols):
        calls = [symbol for symbol in symbols if symbol.ID.OptionRight == OptionRight.Call]
        return calls


    def _get_n_day_options(self, symbols, days_to_expiry: int):
        expiry_symbol_dict = {}
        algo_time = datetime.combine(self._algo.time, time.min)
        for symbol in symbols:
            expiry_date = symbol.ID.Date
            if expiry_date in expiry_symbol_dict:
                expiry_symbol_dict[expiry_date].append(symbol)
            else:
                if (expiry_date - algo_time).days >= days_to_expiry: 
                        expiry_symbol_dict[expiry_date] = [symbol]

        dates = sorted(expiry_symbol_dict.keys())
        for date in dates:
            if len(expiry_symbol_dict[date]) > 1:
                return expiry_symbol_dict[date] 
        return None


    def _get_options_closest_to_target_expiry(self, symbols, days_to_expiry: int, before=False, after=False):

        expiry_symbol_dict = {}
        for symbol in symbols:
            expiry_date = symbol.ID.Date
            if expiry_date in expiry_symbol_dict:
                expiry_symbol_dict[expiry_date].append(symbol)
            else:
                if (expiry_date - self._algo.Time).days >= 1: 
                    if before: 
                        if (symbol.ID.Date - self._algo.Time).days <= days_to_expiry:
                            expiry_symbol_dict[expiry_date] = [symbol]
                    elif after: 
                        if (symbol.ID.Date - self._algo.Time).days >= days_to_expiry:
                            expiry_symbol_dict[expiry_date] = [symbol]
                    else: 
                        expiry_symbol_dict[expiry_date] = [symbol]

        dates = sorted(expiry_symbol_dict.keys(), key=lambda x: abs((x - self._algo.Time).days - days_to_expiry))

        for date in dates:
            if len(expiry_symbol_dict[date]) > 1:
                return expiry_symbol_dict[date] 
        return None


    def _get_puts(self):
        symbols = self._get_options_chains()
        return  self._filter_puts(symbols)


    def _get_calls(self):
        symbols = self._get_options_chains()
        return  self._filter_calls(symbols)


    def _get_option_closest_to_target_strike(self, symbols, target_strike: int):
        # ToDo: Closest from below or above: return here an array of closest and make the calling function pick. 

        diff = np.array([abs(x.ID.StrikePrice - target_strike * self._algo.Securities[x.ID.Underlying.Symbol].Price) for x in symbols])
        ind = np.argmin(diff)
        return symbols[ind]


    def _subscribe_to_option(self, symbol):
        
        if self._underlying.SecurityType == SecurityType.Index:
            return self._algo.AddIndexOptionContract(symbol=symbol, resolution=self._resolution)
        
        elif self._underlying.SecurityType == SecurityType.Equity:
            return self._algo.AddOptionContract(symbol=symbol, resolution=self._resolution)
        
        else:
            raise NotImplementedError



class BuySpread(BuyStrategy):

    def __init__(self, 
            algo: QCAlgorithm, 
            underlying: Symbol,
            canonical_option_symbols: List[Symbol],
            amount_calculator: AmountCalculator,
            filter: Optional[Filter], 
            date_schedule_rule: Callable,
            time_schedule_rule: Callable,
            **params, 
            ):
        '''
        date_scheduler_rule: a pointer to a function in QCAlgorithm.DateRules
        time_schedlue_rule: a pointer to a function in QCAlgorithm.TimeRules
        '''
        super().__init__(algo, 
            underlying,
            canonical_option_symbols,
            amount_calculator,
            filter, 
            date_schedule_rule,
            time_schedule_rule,
            **params)
        
        assert 'main_leg' in self._params
        self._main_leg = self._params['main_leg']

        assert 'secondary_leg' in self._params
        self._secondary_leg = self._params['secondary_leg']

        self._main_amount_calculator = self._main_leg['amount_calculator']
        self._connections = {}
        self._main_quantities = {}

    
    @property
    def connections(self):
        return self._connections
    
    
    def buy(self):
        if self._filter is not None:
            if not self._filter.filter():
                return

        symbols = self._options_to_buy

        if symbols is None:
            #self._algo.Log('stop')
            return


        assert len(symbols) == 2
        options = [self._subscribe_to_option(symbol) for symbol in symbols]
        amounts = self._get_amount(options)

        for symbol, amount in zip(symbols, amounts):        
    
            if self._algo.securities[symbol].Price == 0:
                self._algo.log(f'No price data {self._algo.Time} :: {symbol.Value}')
                return
        
            ticket = self._algo.market_order(symbol=symbol, quantity=amount)            
           
        self._active_options.add(symbols[0]) 
        if symbols[0] in self._connections:
            self._main_quantities[symbols[0]] += amounts[0]
            if symbols[1] in self._connections[symbols[0]]:
                self._connections[symbols[0]][symbols[1]] += amounts[1]
            else:
                self._connections[symbols[0]][symbols[1]] = amounts[1]
        else: 
            self._connections[symbols[0]] = {symbols[1]: amounts[1]}
            self._main_quantities[symbols[0]] = amounts[0]
     
    
    def _get_amount(self, options):
        amounts = []

        for leg in [self._main_leg, self._secondary_leg]:
            amount = leg['amount_calculator'].calc_amount(options[0])

            if leg['Type'] == 'Sell':
                amounts.append(-amount)
        
            elif leg['Type'] == 'Buy':
                amounts.append(amount)
        
            else: 
                raise NotImplementedError()
         
        return amounts

    def _get_options(self):
        res = []
        for leg in [self._main_leg, self._secondary_leg]:
            params = leg  
            if params['Right'] == OptionRight.Put:
                symbols = self._get_puts()

            elif params['Right'] == OptionRight.Call: 
                symbols = self._get_calls()

            else: 
                raise NotImplementedError()


            if leg['Before Target Expiry']:
                symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=params['Days to Expiry'], before=True, after=False)
            else:
                symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=params['Days to Expiry'], before=False, after=False)
        
            if symbols is None or len(symbols) == 0:
                self._algo.Log('No options to buy')
                return
        
            symbol = self._get_option_closest_to_target_strike(symbols, target_strike=params['Strike'])
            res.append(symbol)
        return res
        

class BuyCollar(BuySpread):
    
    def _search_strike_relative_to_budget(self, target_symbol, search_symbols):
        target_option = self._subscribe_to_option(target_symbol)
        def _get_options(self):
            res = []

        params = self._main_leg  
        if params['Right'] == OptionRight.Put:
            symbols = self._get_puts()
            secondary_symbols = self._get_calls()

        elif params['Right'] == OptionRight.Call: 
            symbols = self._get_calls()
            secondary_symbols = self._get_puts()

        else: 
            raise NotImplementedError()


        if params['Before Target Expiry']:
            symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=params['Days to Expiry'], before=True, after=False)
            secondary_symbols = self._get_options_closest_to_target_expiry(secondary_symbols, days_to_expiry=params['Days to Expiry'], before=True, after=False)
        else:
            symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=params['Days to Expiry'], before=False, after=False)
            secondary_symbols = self._get_options_closest_to_target_expiry(secondary_symbols, days_to_expiry=params['Days to Expiry'], before=False, after=False)

        if symbols is None or len(symbols) == 0:
            self._algo.Log('No options to buy')
            return
        assert symbols[0].id.date == secondary_symbols[0].id.date

    
        symbol = self._get_option_closest_to_target_strike(symbols, target_strike=params['Strike'])
        self._search_strike_relative_to_budget(secondary_symbols, symbol)
        res.append(symbol)
        return res


    

class BuyByStrikeAndExpiry(BuyStrategy):

    def _get_options(self):    
        if self._params['Right'] == OptionRight.Put:
            symbols = self._get_puts()

        elif self._params['Right'] == OptionRight.Call: 
            symbols = self._get_calls()

        else: 
            raise NotImplementedError()


        if self._params['Before Target Expiry']:
            symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=self._params['Days to Expiry'], before=True, after=False)
        else:
            symbols = self._get_options_closest_to_target_expiry(symbols, days_to_expiry=self._params['Days to Expiry'], before=False, after=False)
        
        if symbols is None or len(symbols) == 0:
            self._algo.Log('No options to buy')
            return
        
        symbol = self._get_option_closest_to_target_strike(symbols, target_strike=self._params['Strike'])
        return [symbol]
        

class BuyByDeltaAndExpiry(BuyStrategy):
    def __init__(self, 
            algo: QCAlgorithm, 
            underlying: Symbol,
            canonical_option_symbols: Symbol,
            target_delta: float,
            amount_calculator: AmountCalculator,
            threshold = 0,
            filter: Optional[Filter]=None, 
            **params,
            ):
        super().__init__(algo=algo, underlying = underlying, canonical_option_symbols = canonical_option_symbols, 
            amount_calculator = amount_calculator, filter = filter,  **params
        )

        self._threshold = threshold
        self._target_delta = target_delta

    
    def _get_option_by_delta(self, options: Dict[Symbol, Option]):
        deltas_dict = {}
        count = 0

        for symbol, option in options.items():
            
            #contract_data = OptionContract(option, self._underlying)
            contract_data = OptionContract(option)

            contract_data.Time = self._algo.Time
            result = option.EvaluatePriceModel(None, contract_data)
            delta = abs(result.Greeks.Delta)

            if delta in deltas_dict:
                count += 1
                deltas_dict[delta].append(symbol)
            
            elif delta >= self._threshold:
                count += 1
                deltas_dict[delta] = [symbol]
        
        if len(deltas_dict) == 0:
            return None 

        deltas = sorted(deltas_dict.keys(), key=lambda x: abs(x - self._target_delta))
        #self._algo.Log(f'The delta difference that we get is {abs(deltas[0]-self._target_delta)}')

        return deltas_dict[deltas[0]]
        
    def _get_options(self):
        if self._params['Right'] == OptionRight.Put:
            symbols = self._get_puts()
        elif self._params['Right'] == OptionRight.Call: 
            symbols = self._get_calls()
        else: 
            raise NotImplementedError()

        symbols = self._get_n_day_options(symbols, self._params['Days to Expiry'])

        if symbols is None or len(symbols) == 0:
            self._algo.Log('No options to buy')
            return None

        new_subscriptions = set()
        options = {}
        for symbol in symbols:
            options[symbol] = self._subscribe_to_option(symbol)
            if symbol not in self._algo.ActiveSecurities:
                new_subscriptions.add(symbol)
        
        symbols = self._get_option_by_delta(options)
        if symbols is None:
            self._algo.Log(f'No deltas') 
            return None

        for symbol in symbols:
            if symbol in new_subscriptions:
                new_subscriptions.remove(symbol)
        
        # Careful. There is an issue that removeSecurity removes the price. 
        #for subscription in new_subscriptions: 
        #    self._algo.RemoveOptionContract(subscription)
        self._garbage_collection_set.update(new_subscriptions)
        if len(symbols) > 1:
            symbols = [symbols[0]]
        assert len(symbols) == 1
        return symbols
    
    
            


class MonetizeOptions(ABC):
    def __init__(self, algo, option_strategy: Optional[BuyStrategy]=None):
        self._algo = algo
        self._option_strategy: Optional[BuyStrategy] = option_strategy

    @abstractmethod
    def monetize(self):
        pass

    def add_strategy(self, option_strategy: BuyStrategy):
        if self._option_strategy is not None:
            raise(ValueError('connection to buys strategy already exists'))
        self._option_strategy = option_strategy


class MonetizeFixedTimeBeforeExpiry(MonetizeOptions):
    def __init__(self, algo, min_days_to_expiry):
        self._min_days_to_expiry = min_days_to_expiry
        super().__init__(algo)


    def monetize(self):
        for holding in self._algo.Portfolio:
            if holding.Value.Type == SecurityType.IndexOption or holding.Value.Type == SecurityType.Option and holding.Value.HoldingsValue != 0:
                time_to_expiry = (self._algo.Securities[holding.Key].Expiry - self._algo.Time).days
                if time_to_expiry <= self._min_days_to_expiry:
                    self._algo.RemoveSecurity(holding.Key)


class MonetizeRelativeToUnderlying(MonetizeOptions):
    def __init__(self, algo, percent_of_underlying):
        self._percent_of_underlying = percent_of_underlying
        super().__init__(algo)


    def monetize(self):
        for holding in self._algo.Portfolio:
            if (holding.Value.Type == SecurityType.IndexOption or holding.Value.Type == SecurityType.Option) and holding.Value.HoldingsValue > 0:
                underlying_price = self._algo.Securities[holding.Value.Security.Underlying.Symbol].Price
                if self._algo.Securities[holding.Key].StrikePrice * self._percent_of_underlying > underlying_price:
                    self._algo.RemoveSecurity(holding.Key)


class MonetizeByDelta(MonetizeOptions):
    def __init__(self, algo, delta):
        self._delta = delta
        super().__init__(algo)
    
    def monetize(self):
        algo = self._algo
        slice = algo.CurrentSlice
        assert len(slice.OptionChains) == 1 or len(slice.OptionChains) == 0
        assert self._option_strategy is not None
        
        for canonical_option in self._option_strategy._canonical_options:
            if canonical_option not in slice.OptionChains:   
                continue
        
            option_contracts = slice.OptionChains[canonical_option]
            for contract in option_contracts:
                if (
                    contract.Symbol in self._option_strategy.active_options 
                    and contract.Symbol in algo.Portfolio 
                    and algo.Portfolio[contract.Symbol].HoldingsValue != 0
                ):
                
                    if abs(contract.Greeks.Delta) > self._delta:
                        algo.RemoveSecurity(contract.Symbol)


class MonetizeSpreadByDelta(MonetizeByDelta):
    def __init__(self, algo, delta, monetize_connections=True):
        self._monetize_connections = monetize_connections
        super().__init__(algo, delta)
    
    def monetize(self):
        algo = self._algo
        slice = algo.current_slice
        assert len(slice.option_chains) == 1 or len(slice.option_chains) == 0
        assert self._option_strategy is not None
        
        for canonical_option in self._option_strategy._canonical_options:
            if canonical_option not in slice.option_chains:   
                continue
        
            option_contracts = slice.option_chains[canonical_option]
            options_dict = {contract.symbol: contract for contract in option_contracts}

            symbols_to_pop = []
            for symbol in self._option_strategy._connections:
                if symbol in options_dict and abs(options_dict[symbol].Greeks.Delta) > self._delta:
                    quantity = -self._option_strategy._main_quantities[symbol]
                    quantity1 = 0
                    algo.market_order(symbol, quantity)
                    
                    #assert(len(self._option_strategy.connections[symbol])==1)
                    if self._monetize_connections:
                        for connected_symbol, quantity_to_offset in self._option_strategy._connections[symbol].items():
                            quantity1 += quantity_to_offset
                            algo.market_order(connected_symbol, -quantity_to_offset)
                        assert quantity == quantity1
                    symbols_to_pop.append(symbol)
            
            for symbol in symbols_to_pop:
                self._option_strategy._connections.pop(symbol)
                self._option_strategy._main_quantities.pop(symbol)






class ChainMonetizations(MonetizeOptions):
    def __init__(self, algo, monetizations_list):
        self._monetizations_list = monetizations_list
        super().__init__(algo)

    def monetize(self):
        for monetization in self._monetizations_list:
            monetization.monetize()



class VixThresholdFilter(Filter):
    def __init__(self, algo, threshold):
        self._algo = algo
        self._threshold = threshold

        self.vix_symbol = algo.AddIndex("VIX", Resolution.Hour).Symbol
        algo.Securities[self.vix_symbol].SetDataNormalizationMode(DataNormalizationMode.Raw)
        
    def filter(self):
        if self._algo.Securities[self.vix_symbol].Price > self._threshold:
            return False
        else: 
            return True

class VixFilter(Filter):
    def __init__(self, algo, threshold, days):
        self._algo = algo
        self._threshold = threshold

        self.vix_symbol = algo.AddIndex("VIX", Resolution.Daily).Symbol
        algo.Securities[self.vix_symbol].SetDataNormalizationMode(DataNormalizationMode.Raw)
        
        self.vix_sma = SimpleMovingAverage(days)
        self._algo.RegisterIndicator(self.vix_symbol, self.vix_sma, Resolution.Daily)
        algo.WarmUpIndicator(self.vix_symbol, self.vix_sma)
    
    def filter(self):
        if not self.vix_sma.IsReady:
            return False
        if self._algo.Securities[self.vix_symbol].Price >= self.vix_sma.Current.Value * self._threshold:
            return False
        else: 
            return True



class FractionOfCashAmountCalculator(AmountCalculator):
    def __init__(self, algo, notional_frac, multiplier, cap=None):
        super().__init__(algo)
        self._multiplier = multiplier
        self._notional_frac = notional_frac 
        self._cap = cap 

    def calc_amount(self, option):
        amount = self._get_amount_as_fraction_of_cash(option, self._notional_frac)
        amount = round(amount * self._multiplier)

        if self._cap is not None and (option.ask_price != 0 or option.bid_price != 0):
            assert option.ask_price != 0
            assert option.bid_price != 0
            holding_value = self._algo.portfolio.cash
            option_cost = ((option.ask_price + option.bid_price)/2) * option.ContractMultiplier
            max_amount = round((self._cap * holding_value)/option_cost)
            amount = min(amount, max_amount)

        return amount


class ConstantFractionAmountCalculator(AmountCalculator):
    def __init__(self, algo, notional_frac, multiplier, cap=None):
        super().__init__(algo)
        self._multiplier = multiplier
        self._notional_frac = notional_frac 
        self._cap = cap 

    def calc_amount(self, option):
        amount = self._get_amount_as_fraction_of_portfolio(option, self._notional_frac)
        amount = round(amount * self._multiplier)
        
        return amount

class FractionOfHoldingAmountCalculator(AmountCalculator):
    def __init__(self, algo, notional_frac, multiplier, holding_symbol, cap=None):
        super().__init__(algo)
        self._holding = holding_symbol
        self._multiplier = multiplier
        self._notional_frac = notional_frac
        self._cap = cap 

    def _get_amount_as_fraction_of_holding(self, option, fraction):
        holding_value = self._algo.portfolio[self._holding].holdings_value
        multiplier = option.ContractMultiplier
        target_notional = holding_value * fraction
        notional_of_contract = multiplier * option.Underlying.Price
        amount = target_notional / notional_of_contract
        return amount

    def calc_amount(self, option):
        amount = self._get_amount_as_fraction_of_holding(option, self._notional_frac)
        amount = round(amount * self._multiplier)

        if self._cap is not None and option.price != 0:
            holding_value = self._algo.portfolio[self._holding].holdings_value
            option_cost = option.price * option.ContractMultiplier
            max_amount = round((self._cap * holding_value)/option_cost)
            amount = min(amount, max_amount)

        return amount
        
class FractionToExpiryAmountCalculator(AmountCalculator):
    def __init__(self, algo, notional_frac, multiplier):
        super().__init__(algo)
        self._multiplier = multiplier
        self._notional_frac = notional_frac 


    def calc_amount(self, option):
        
        amount = self._get_amount_as_fraction_of_portfolio(option, self._notional_frac)
        days_to_expiry = (option.Expiry - self._algo.Time).days
        amount = round(amount * self._multiplier / days_to_expiry)
        
        return amount


class DeltaHedge:
    def __init__(self, algo: QCAlgorithm, underlying: Symbol, canonical_option: Symbol,
        date_schedule_rule, time_schedule_rule, multiplier=1):
        self._algo: QCAlgorithm = algo 
        self._underlying = underlying
        self._canonical_option = canonical_option

        self._time_rule = time_schedule_rule
        self._date_rule = date_schedule_rule
        self._algo.schedule.on(self._date_rule, self._time_rule, self.delta_hedge)
        self._multiplier = multiplier

    def delta_hedge(self):
        if self._canonical_option not in self._algo.current_slice.option_chains:
            self._algo.log('no chain data')
            return 
        if self._algo.securities[self._underlying].exchange.exchange_open:
            delta = 0
            contracts = self._algo.current_slice.option_chains[self._canonical_option].contracts

            for holding in self._algo.portfolio:
                symbol = holding.key
                if (symbol.ID.SecurityType == SecurityType.Option or symbol.ID.SecurityType == SecurityType.IndexOption) and \
                    self._algo.portfolio[symbol].quantity != 0: #and symbol.underlying == self._underlying:
                    if symbol in contracts:
                        delta += self._algo.portfolio[symbol].quantity * contracts[symbol].greeks.delta * self._algo.securities[symbol].contract_multiplier
                    else:
                        self._algo.Log(f'missing greek')
            
            delta *= self._multiplier
            #self._algo.Log(f'got delta to hedge {delta}')
            quantity = -(delta)

            amount_to_hedge = quantity - self._algo.portfolio[self._underlying].quantity
            #self._algo.Log(f'additional hedging {amount_to_hedge}')

            self._algo.market_order(symbol=self._underlying, quantity=round(amount_to_hedge))


class DeltaHedgeWithFutures(DeltaHedge):
    def __init__(self, algo: QCAlgorithm, canonical_option: Symbol, continuous_future: Future,
        date_schedule_rule, time_schedule_rule, multiplier=1, filter: Filter = None):
        self._algo: QCAlgorithm = algo 
        self._canonical_option = canonical_option
        self._continuous_future = continuous_future
        self._current_fut = None
        self._filter = filter

        self._time_rule = time_schedule_rule
        self._date_rule = date_schedule_rule
        self._algo.schedule.on(self._date_rule, self._time_rule, self.delta_hedge)
        self._multiplier = multiplier

    @property
    def _current_future(self):
        if self._current_fut is None:
            self._current_fut = self._continuous_future.mapped
        return self._current_fut  

    def delta_hedge(self):
        if self._current_future is None:
            self._algo.log('no mapped future to hedge')
            return

        if self._canonical_option not in self._algo.current_slice.option_chains:
            self._algo.log('no chain data')
            return 

        if self._filter is not None and not self._filter.filter():
            return

        if self._algo.securities[self._current_future].exchange.exchange_open:
            delta = 0
            contracts = self._algo.current_slice.option_chains[self._canonical_option].contracts

            for holding in self._algo.portfolio:
                symbol = holding.key
                if (symbol.ID.SecurityType == SecurityType.Option or symbol.ID.SecurityType == SecurityType.IndexOption) and \
                    self._algo.portfolio[symbol].quantity != 0: #and symbol.underlying == self._underlying:
                    if symbol in contracts:
                        delta += self._algo.portfolio[symbol].quantity * contracts[symbol].greeks.delta * self._algo.securities[symbol].contract_multiplier
                    else:
                        self._algo.Log(f'missing greek')
            
            #self._algo.Log(f'got delta to hedge {delta}')

            quantity_to_hedge = - delta // self._algo.securities[self._current_future].symbol_properties.contract_multiplier

            if self._to_rollover():
                self._algo.log('liquidating')
                self._algo.liquidate(self._current_future)
                self._current_fut = self._continuous_future.mapped 
            else: 
                current_quantity = self._algo.portfolio[self._current_future].quantity
                quantity_to_hedge = quantity_to_hedge - current_quantity
            

            #self._algo.Log(f'additional hedging {quantity_to_hedge}')

            self._algo.market_order(symbol=self._current_fut, quantity=quantity_to_hedge)


    def _to_rollover(self):
        
        if (self._algo.securities[self._current_future].expiry -self._algo.time).days <= 10:
            return True
        else:
            return False
#region imports
from AlgorithmImports import *
from abc import ABC, abstractmethod
from indicators.stress_test import StressIndexUnderlying
from indicators.realized_volatility import RealizedVolatilityIndicator
from indicators.implied_volatility import ImpliedVolatilityIndicator
#endregion

class Charts(ABC):
    def __init__(self, algo, chart_name, series_list):
        self._algo = algo
        self._chart_name = chart_name
        self._series_list = series_list
        self._add_chart()
        self._scheduler()

    def _add_chart(self):
        chart = Chart(self._chart_name)
        self._algo.AddChart(chart)
        for series in self._series_list:
            chart.AddSeries(Series(series['name'], series['type'], series['unit']))

    def _scheduler(self):
        algo = self._algo
        algo.Schedule.On(
            algo.DateRules.EveryDay('SPX'), 
            algo.TimeRules.BeforeMarketClose('SPX', 0), 
            self._update_chart
        )

    @abstractmethod
    def _update_chart(self):
        pass

class ImpliedVsRealized(Charts):
    def __init__(
        self, 
        algo, 
        rv_indicator: RealizedVolatilityIndicator, 
        iv_indicator: ImpliedVolatilityIndicator
    ):

        series_list = [
            {
            'name': 'Realized Volatility', 
            'type': SeriesType.Line, 
            'unit': '%'
            }, 
            {
            'name': 'Implied Volatility', 
            'type': SeriesType.Line, 
            'unit': '%'
            }, 
        ]

        self._rv_indicator: RealizedVolatilityIndicator = rv_indicator
        self._iv_indicator: ImpliedVolatilityIndicator = iv_indicator

        super().__init__(algo = algo, chart_name = 'Implied vs Realized Volatility', series_list = series_list)
    
    def _update_chart(self):
        self._algo.Plot(self._chart_name, self._series_list[0]['name'], self._rv_indicator.value * 100)
        # self._algo.Plot(self._chart_name, self._series_list[1]['name'], self._iv_indicator.value * 100)

class StressTestHoldings(Charts):
    def __init__(self, algo, stress_indicator: StressIndexUnderlying):
        series_list = [
            {
            'name': 'Stressed Option Holdings', 
            'type': SeriesType.Line, 
            'unit': '$'
            }, 
            {
            'name': 'Original Option Holdings', 
            'type': SeriesType.Line, 
            'unit': '$'
            }, 
        ]

        self._stress_indicator: StressIndexUnderlying = stress_indicator

        super().__init__(algo = algo, chart_name = 'Stress Test Holdings', series_list = series_list)
    
    def _update_chart(self):
        self._algo.Plot(self._chart_name, self._series_list[0]['name'], self._stress_indicator.option_holdings)
        self._algo.Plot(self._chart_name, self._series_list[1]['name'], self._stress_indicator.original_option_holdings)

class StressTestPortfolioValue(Charts):
    def __init__(self, algo, stress_indicator: StressIndexUnderlying):
        series_list = [
            {
            'name': 'Stressed Portfolio Value', 
            'type': SeriesType.Line, 
            'unit': '$'
            }, 
            {
            'name': 'Original Portfolio Value', 
            'type': SeriesType.Line, 
            'unit': '$'
            }
        ]

        self._stress_indicator: StressIndexUnderlying = stress_indicator

        super().__init__(algo = algo, chart_name = 'Stress Test', series_list = series_list)
    
    def _update_chart(self):
        self._algo.Plot(self._chart_name, self._series_list[0]['name'], self._stress_indicator.portfolio_value)
        self._algo.Plot(self._chart_name, self._series_list[1]['name'], self._algo.portfolio.total_portfolio_value)

class InvestedOptionCount(Charts):
    def __init__(self, algo):
        series_list = [
            {
            'name': 'Invested Option Count',  
            'type': SeriesType.BAR, 
            'unit': ''
            }, 
        ]

        super().__init__(algo = algo, chart_name = 'Invested Option Count', series_list = series_list)
    
    def _update_chart(self):
        options_invested: int = [
            holding.value for holding in self._algo.portfolio 
            if self._algo.securities[holding.key].type == SecurityType.INDEX_OPTION and \
            holding.value.invested
        ]

        self._algo.Plot(self._chart_name, self._series_list[0]['name'], len(options_invested))

class PortfolioValue(Charts):
    def __init__(self, algo):
        series_list = [
            {
            'name': 'Portfolio Value', 
            'type': SeriesType.Line, 
            'unit': '$'
            }
        ]
        super().__init__(algo = algo, chart_name = 'Portfolio', series_list = series_list)
    
    def _update_chart(self):
        self._algo.Plot(self._chart_name, self._series_list[0]['name'], self._algo.Portfolio.TotalPortfolioValue)

class HoldingsValue(Charts):
    def __init__(self, algo, holdings_symbol: Symbol):
            series_list = [
            {
            'name': holdings_symbol.to_string(), 
            'type': SeriesType.Line, 
            'unit': '$'
            }
            ]
            
            self._holdings_symbol = holdings_symbol
            super().__init__(algo = algo, chart_name = holdings_symbol.to_string(), series_list = series_list)

    def _update_chart(self):
        self._algo.Plot(
            self._chart_name, self._series_list[0]['name'], 
            self._algo.Portfolio[self._holdings_symbol].holdings_value
            )

class OptionsPortfolioValue(Charts):
    def __init__(self, algo):
        series_list = [
            {
            'name': 'Options Value', 
            'type': SeriesType.Line, 
            'unit': '$'
            }
        ]
        super().__init__(algo = algo, chart_name = 'Options Portfolio Value', series_list = series_list)

    def _update_chart(self):
        value = 0
        for holding in self._algo.Portfolio:
            if holding.Value.Type == SecurityType.IndexOption or holding.Value.Type == SecurityType.Option: 
                value += holding.Value.HoldingsValue
        self._algo.Plot(self._chart_name, self._series_list[0]['name'], value)

class Greeks(Charts):
    def __init__(self, algo, strategies):
        self._strategies = strategies
        self._algo = algo
        series_list = [
            {
            'name': 'Delta', 
            'type': SeriesType.Line, 
            'unit': ''
            }, 
            {
            'name': 'Gamma', 
            'type': SeriesType.Line, 
            'unit': ''
            },
            {
            'name': 'Implied Volatility', 
            'type': SeriesType.Line, 
            'unit': ''
            }, 
            {
            'name': 'Theta', 
            'type': SeriesType.Line, 
            'unit': ''
            }, 
            {
            'name': 'Vega', 
            'type': SeriesType.Line, 
            'unit': ''
            },
            {  
            'name': 'Theta_per_day',
            'type': SeriesType.Line, 
            'unit': ''
            },
        ]
        super().__init__(algo = algo, chart_name = 'Greeks', series_list = series_list)
    
    def _update_chart(self):
        algo = self._algo
        delta = 0
        gamma = 0
        implied_vol = 0
        theta = 0
        vega = 0 
        theta_per_day = 0 

        total_options_holdings = 0
        for strategy in self._strategies:
            greeks = strategy['strategy_instance'].get_Greeks()
            if greeks is None: 
                algo.Log('No Greeks')
                return
            strategy['CurrentGreeks'] = greeks
            for option in strategy['CurrentGreeks']:
                delta += strategy['CurrentGreeks'][option]['Greeks'].Delta * algo.Securities[option].ContractMultiplier * algo.Portfolio[option].Quantity 
                gamma += strategy['CurrentGreeks'][option]['Greeks'].Gamma * algo.Securities[option].ContractMultiplier * algo.Portfolio[option].Quantity 
                vega += strategy['CurrentGreeks'][option]['Greeks'].Vega * algo.Securities[option].ContractMultiplier * algo.Portfolio[option].Quantity 
                theta += strategy['CurrentGreeks'][option]['Greeks'].Theta * algo.Securities[option].ContractMultiplier * algo.Portfolio[option].Quantity 
                theta_per_day += strategy['CurrentGreeks'][option]['Greeks'].ThetaPerDay * algo.Securities[option].ContractMultiplier * algo.Portfolio[option].Quantity 

        algo.Plot(self._chart_name, "Delta", delta) 
        algo.Plot(self._chart_name, "Gamma", gamma)  
        algo.Plot(self._chart_name, "Vega", vega)
        algo.Plot(self._chart_name, "Theta", theta)     
        algo.Plot(self._chart_name, "Theta_per_day", theta_per_day)        
   
class LogPortfolioContents:
    def __init__(self, algo):
        self._algo = algo
    
    def log_portfolio_contents(self):
        algo = self._algo
        algo.log(f'Options Portfolio Contents: \n')
        for holding in algo.portfolio:
            holding = holding.value
            if holding.quantity == 0:
                continue
            if holding.Type == SecurityType.IndexOption or holding.Type == SecurityType.Option: 
                dict_to_log = {
                    'symbol': holding.symbol.value,
                    'expiry': holding.security.expiry,
                    'strike': holding.security.strike_price,
                    'weight' : holding.holdings_value / algo.portfolio.total_portfolio_value,
                    'notional_pct': abs(holding.security.underlying.price * holding.security.contract_multiplier * holding.quantity / algo.portfolio.total_portfolio_value)
                }
                algo.log(dict_to_log)
        algo.log(f'End Portfolio Contents')