Overall Statistics
Total Trades
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Net Profit
0%
Sharpe Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
-0.859
Tracking Error
0.207
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
# region imports
from AlgorithmImports import *

from math import log, sqrt, pi, exp
from scipy.stats import norm
import scipy.stats as stats
# endregion
Assignment1 = True
Assignment2 = False

## ------------------------ Assignment 1 AAPL Contracts info
if Assignment1:
    class FatYellowOwl(QCAlgorithm):

        def Initialize(self) -> None:
            self.SetStartDate(2022, 7, 14)
            self.SetEndDate(2022, 12, 1)
            self.SetCash(100000)
            self.SetWarmUp(20)
            
            # Universe settings
            self.UniverseSettings.Resolution = Resolution.Hour
            
            # Requesting data
            self.aapl = self.AddEquity("AAPL",Resolution.Hour)
            self.aapl.SetDataNormalizationMode(DataNormalizationMode.Raw)
            self.aapl = self.aapl.Symbol

            option = self.AddOption("AAPL",Resolution.Hour)
            self.option_symbol = option.Symbol
            
            # Volatility
            self.last_price = RollingWindow[float](2)
            self.std = StandardDeviation(25)

            # Risk Free rate
            self.yieldCurve = self.AddData(USTreasuryYieldCurveRate, "USTYCR", Resolution.Daily).Symbol
            self.last_rfr = RollingWindow[float](4)

            #Set Options Model: Equity Options are American Style
            # As such, need to use CrankNicolsonFD or other models.
            #BlackScholes Model is only for European Options
            option.PriceModel = OptionPriceModels.CrankNicolsonFD()

        ## ----------- Black Scholes ------------- ##
        # Price

        def bsm_price(self, option_type, sigma, s, k, r, T, q):
            # calculate the bsm price of European call and put options
            d1 = (np.log(s / k) + (r - q + sigma ** 2 * 0.5) * T) / (sigma * np.sqrt(T))
            d2 = d1 - sigma * np.sqrt(T)
            if option_type == 'c':
                return np.exp(-r*T) * (s * np.exp((r - q)*T) * stats.norm.cdf(d1) - k *  stats.norm.cdf(d2))
            if option_type == 'p':
                return np.exp(-r*T) * (k * stats.norm.cdf(-d2) - s * np.exp((r - q)*T) *  stats.norm.cdf(-d1))
            raise Exception(f'No such option type: {option_type}')

        def implied_vol(self, option_type, option_price, s, k, r, T, q):
            # apply bisection method to get the implied volatility by solving the BSM function
            precision = 0.00001
            upper_vol = 500.0
            max_vol = 500.0
            min_vol = 0.0001
            lower_vol = 0.0001
            iteration = 50

            while iteration > 0:
                iteration -= 1
                mid_vol = (upper_vol + lower_vol)/2
                price = self.bsm_price(option_type, mid_vol, s, k, r, T, q)

                if option_type == 'c':
                    lower_price = self.bsm_price(option_type, lower_vol, s, k, r, T, q)
                    if (lower_price - option_price) * (price - option_price) > 0:
                        lower_vol = mid_vol
                    else:
                        upper_vol = mid_vol
                    if mid_vol > max_vol - 5 :
                        return 0.000001

                if option_type == 'p':
                    upper_price = self.bsm_price(option_type, upper_vol, s, k, r, T, q)
                    if (upper_price - option_price) * (price - option_price) > 0:
                        upper_vol = mid_vol
                    else:
                        lower_vol = mid_vol

                if abs(price - option_price) < precision:
                    break

            return mid_vol

        def OnData(self, data: Slice) -> None:
            if data.get(self.yieldCurve):
                self.last_rfr.Add(data[self.yieldCurve].OneYear)
            if self.Time.hour == 10 and data.get(self.aapl): # Once per day
                self.last_price.Add(data[self.aapl].Price)
                if self.last_price.IsReady:
                    self.std.Update(self.Time, (self.last_price[0]-self.last_price[1])/self.last_price[1])
            if self.IsWarmingUp: #Wait until warming up is done
                return
            
            if self.Time == datetime(2022,10,14,15):
                rfr = self.last_rfr[0]
                vol = self.std.Current.Value*sqrt(252)
                T = abs(self.Time - datetime(2022,11,18)).days/365
                # Compute the price for the call option
                price = self.bsm_price('c',vol,data[self.aapl].Price, 145, rfr, T, 0)
                # Get the Implied volatility based on computed price 
                implied_volatility = self.implied_vol('c', price, data[self.aapl].Price, 145,rfr, T, 0)
                self.Log(f'At{self.Time} a Call option on AAPL with strike price 145 and expiry of 2022-11-18 has a price {price} and implied volatility {implied_volatility}')
            elif self.Time == datetime(2022,10,17,10):
                rfr = self.last_rfr[0]
                vol = self.std.Current.Value*sqrt(252)
                T = abs(self.Time - datetime(2022,11,11)).days
                # Compute the price for the call option
                price = self.bsm_price('p',vol,data[self.aapl].Price, 145, rfr, T, 0)
                # Get the Implied volatility based on computed price 
                implied_volatility = self.implied_vol('p', price, data[self.aapl].Price, 145,rfr, T, 0)
                self.Log(f'At{self.Time} a Put option on AAPL with strike price 130 and expiry of 2022-11-11 has a price {price} and implied volatility {implied_volatility}')
            elif self.Time == datetime(2022,10,17,13):
                self.interpolate_contract(data)
            
        def interpolate_contract(self,data):
            computed_prices = [None]
            computed_impvol = [None]
            deltas_t = [datetime(2022,12,16)]
            rfr = self.last_rfr[0]
            vol = self.std.Current.Value*sqrt(252)
            # Because we are using +- this will be month approximatly
            for i in range(10):
                delta_t_minus = abs(self.Time - (datetime(2022,12,16)-timedelta(days=i))).days
                delta_t_plus = abs(self.Time - (datetime(2022,12,16)+timedelta(days=i))).days
                deltas_t = [datetime(2022,12,16)+timedelta(days=i)] + deltas_t
                deltas_t.append(datetime(2022,12,16)+timedelta(days=i))
                # Compute the price and Implied volatility for the put option
                price = self.bsm_price('p', vol, data[self.aapl].Price, 145, rfr, delta_t_minus, 0)
                implied_volatility = self.implied_vol('p', price, data[self.aapl].Price, 145,rfr, delta_t_minus, 0)
                computed_prices = [price] + computed_prices
                computed_impvol = [implied_volatility] + computed_impvol

                # Compute the price and Implied volatility for the put option
                price = self.bsm_price('p', vol, data[self.aapl].Price, 145, rfr, delta_t_plus, 0)
                implied_volatility = self.implied_vol('p', price, data[self.aapl].Price, 145,rfr, delta_t_plus, 0)
                computed_prices.append(price)
                computed_impvol.append(implied_volatility)
            my_data = pd.DataFrame({'Price':computed_prices,'Implied_Volatility': computed_impvol}, index=deltas_t)
            intepolated = my_data.interpolate()
            self.Log(f'At {self.Time} the interpolated value for price and implied volatility with expiry 2022-12-16 is {str(intepolated.iloc[10])}')
            self.Log(f'The three previous values are {str(intepolated.iloc[7:10])}')
            self.Log(f'The three following values are {str(intepolated.iloc[11:14])}')
elif Assignment2:
    class FatYellowOwl(QCAlgorithm):
        # threshold to tel if an implied volatility is high (impvol/vol)
        ThHighVol = 1
        DesiredDelta = 1
        CallQuantity = 100

        def Initialize(self) -> None:
            self.SetStartDate(2020, 8, 11)
            self.SetEndDate(2022, 12, 1)
            self.SetCash(2000000)
            self.SetWarmUp(25)
            
            # Universe settings
            self.UniverseSettings.Resolution = Resolution.Hour
            
            # Requesting data
            self.underlaying = self.AddEquity("QQQ",Resolution.Hour)
            self.underlaying.SetDataNormalizationMode(DataNormalizationMode.Raw)
            self.underlaying = self.underlaying.Symbol

            option = self.AddOption("QQQ",Resolution.Hour)
            self.option_symbol = option.Symbol
            
            # Volatility
            self.last_price = RollingWindow[float](2)
            self.std = StandardDeviation(25)
            self.Schedule.On(self.DateRules.EveryDay("QQQ"),
                            self.TimeRules.BeforeMarketClose("QQQ", -1),
                            self.UpdateVol)

            # Risk Free rate
            self.yieldCurve = self.AddData(USTreasuryYieldCurveRate, "USTYCR", Resolution.Daily).Symbol
            self.last_rfr = RollingWindow[float](4)

            #Set Options Model: Equity Options are American Style
            # As such, need to use CrankNicolsonFD or other models.
            #BlackScholes Model is only for European Options
            option.PriceModel = OptionPriceModels.CrankNicolsonFD()

        ## ----------- Black Scholes ------------- ##
        # Update the daily
        def UpdateVol(self):
            self.last_price.Add(self.Securities[self.underlaying].Close)
            if self.last_price.IsReady:
                self.std.Update(self.Time, (self.last_price[0]-self.last_price[1])/self.last_price[1])

        # Delta
        def delta(self, option_type, s, k, sigma, r, T, q=0):
            '''
            Inputs
                - s: The underlying price.
                - k: The strike price of the option
                - sigma: The volatility of the underlying asset.
                - r: The risk-free interest rate
                - T: Time to expiration
                - q: The dividend yield
            '''
            d1 = (np.log(s / k) + (r - q + sigma ** 2 * 0.5) * T) / (sigma * np.sqrt(T))
            if option_type == "c":
                return exp(-q * T) * norm.cdf(d1)
            elif option_type == "p":
                return exp(-q * T) * (norm.cdf(d1)-1)

        def OnData(self, data: Slice) -> None:
            # Keep track of the one year interest rate available
            if data.get(self.yieldCurve):
                self.last_rfr.Add(data[self.yieldCurve].OneYear)
            if self.IsWarmingUp: #Wait until warming up is done
                return
            # Annual Volatility
            vol = self.std.Current.Value*sqrt(252)
            if self.Time == datetime(2021,11,1,12):
                chain = data.OptionChains.get(self.option_symbol)
                if chain:
                    # Select call contracts
                    calls = [contract for contract in chain if contract.Right == OptionRight.Call]
                if calls:
                    # Only expiry dates in 11/19/2021 and with strike prices > actual underlying price
                    my_call = filter(lambda c: c.Expiry == datetime(2021,11,19) and c.Strike >= c.UnderlyingLastPrice, calls)
                    self.my_call = sorted(my_call, key = lambda c: c.Strike - c.UnderlyingLastPrice)[0]
                    if self.my_call.ImpliedVolatility/vol > self.ThHighVol:
                        self.order = self.MarketOrder(self.my_call.Symbol, -self.CallQuantity)
                    else:
                        self.order = self.MarketOrder(self.my_call.Symbol, self.CallQuantity)
                    self.Schedule.On(self.DateRules.EveryDay("QQQ"),
                            self.TimeRules.AfterMarketOpen("QQQ", 10),
                            self.DeltaHedge)
                else:
                    self.Debug('No Call contracts finded')
            
        def DeltaHedge(self):
            # Risk Free Rate
            rfr = self.last_rfr[0]
            # Annual Volatility
            vol = self.std.Current.Value*sqrt(252)
            # Time to expiry
            T = abs(self.Time - datetime(2021,11,19)).days/365

            # Current Delta
            delta = self.delta('c', self.Securities[self.underlaying].Price, self.my_call.Strike, vol, rfr, T)
            # Current Shares
            holds = self.Securities[self.underlaying].Holdings.Quantity
            # Difference to the desired Delta converted to shares
            target = (self.DesiredDelta - delta) * 100
            quantity_order = target - holds
            if quantity_order > 0:
                self.Log(f'Current {self.underlaying.Value} Holds')
                self.order = self.MarketOrder(self.underlaying, -quantity_order)
                self.Log(f'For a Delta of {delta}, a market order of {-quantity_order} shares in ' +\
                        f'{self.underlaying.Value} was created to mantain the Desired delta of {self.DesiredDelta}')
else:
    class FatYellowOwl(QCAlgorithm):
        # threshold to tell if an implied volatility is high (impvol/vol)
        ThHighVol = 1
        # The Delta that want to be mantained during the holding of the call
        DesiredDelta = 0.5
        # Initial order quantity for call option
        CallQuantity = 100
        # Minimum difference to select the contract (strike - underlying), 
        # negative will allow strike prices below the current underlying.price
        # The contract with the lowest price will be selected 
        StrikeDiff = -5
        # Days to expiry
        TimeDelta = 90

        def Initialize(self) -> None:
            self.SetStartDate(2020, 8, 11)
            self.SetEndDate(2022, 12, 1)
            self.SetCash(2000000)
            self.SetWarmUp(25)
            
            # Universe settings
            self.UniverseSettings.Resolution = Resolution.Hour
            
            # Requesting data
            self.underlaying = self.AddEquity("QQQ",Resolution.Hour)
            self.underlaying.SetDataNormalizationMode(DataNormalizationMode.Raw)
            self.underlaying = self.underlaying.Symbol

            option = self.AddOption("QQQ",Resolution.Hour)
            self.option_symbol = option.Symbol
            
            # Volatility
            self.last_price = RollingWindow[float](2)
            self.std = StandardDeviation(25)
            self.Schedule.On(self.DateRules.EveryDay("QQQ"),
                            self.TimeRules.BeforeMarketClose("QQQ", -1),
                            self.UpdateVol)

            # Risk Free rate
            self.yieldCurve = self.AddData(USTreasuryYieldCurveRate, "USTYCR", Resolution.Daily).Symbol
            self.last_rfr = RollingWindow[float](4)

            #Set Options Model: Equity Options are American Style
            # As such, need to use CrankNicolsonFD or other models.
            #BlackScholes Model is only for European Options
            option.PriceModel = OptionPriceModels.CrankNicolsonFD()

        ## ----------- Black Scholes ------------- ##
        # Update the daily
        def UpdateVol(self):
            self.last_price.Add(self.Securities[self.underlaying].Close)
            if self.last_price.IsReady:
                self.std.Update(self.Time, (self.last_price[0]-self.last_price[1])/self.last_price[1])

        # Delta
        def delta(self, option_type, s, k, sigma, r, T, q=0):
            '''
            Inputs
                - s: The underlying price.
                - k: The strike price of the option
                - sigma: The volatility of the underlying asset.
                - r: The risk-free interest rate
                - T: Time to expiration
                - q: The dividend yield
            '''
            d1 = (np.log(s / k) + (r - q + sigma ** 2 * 0.5) * T) / (sigma * np.sqrt(T))
            if option_type == "c":
                return exp(-q * T) * norm.cdf(d1)
            elif option_type == "p":
                return exp(-q * T) * (norm.cdf(d1)-1)

        def OnData(self, data: Slice) -> None:
            # Keep track of the one year interest rate available
            if data.get(self.yieldCurve):
                self.last_rfr.Add(data[self.yieldCurve].OneYear)
            if self.IsWarmingUp: #Wait until warming up is done
                return
            # Annual Volatility
            vol = self.std.Current.Value*sqrt(252)
            if self.Time == datetime(2021,11,1,12):
                chain = data.OptionChains.get(self.option_symbol)
                if chain:
                    # Select call contracts
                    calls = [contract for contract in chain if contract.Right == OptionRight.Call]
                if calls:
                    # Only expiry dates in 11/19/2021 and with strike prices > actual underlying price
                    my_call = filter(lambda c: (c.Expiry - self.Time).days > self.TimeDelta and c.Strike - c.UnderlyingLastPrice >= self.StrikeDiff, calls)
                    self.my_call = sorted(my_call, key = lambda c: c.Strike - c.UnderlyingLastPrice)[0]
                    if self.my_call.ImpliedVolatility/vol > self.ThHighVol:
                        self.order = self.MarketOrder(self.my_call.Symbol, -self.CallQuantity)
                    else:
                        self.order = self.MarketOrder(self.my_call.Symbol, self.CallQuantity)
                    self.Schedule.On(self.DateRules.EveryDay("QQQ"),
                            self.TimeRules.AfterMarketOpen("QQQ", 10),
                            self.DeltaHedge)
                else:
                    self.Debug('No Call contracts finded')
            
        def DeltaHedge(self):
            # Risk Free Rate
            rfr = self.last_rfr[0]
            # Annual Volatility
            vol = self.std.Current.Value*sqrt(252)
            # Time to expiry
            T = abs(self.Time - datetime(2021,11,19)).days/365

            # Current Delta
            delta = self.delta('c', self.Securities[self.underlaying].Price, self.my_call.Strike, vol, rfr, T)
            # Current Shares
            holds = self.Securities[self.underlaying].Holdings.Quantity
            # Difference to the desired Delta converted to shares
            target = (self.DesiredDelta - delta) * 100
            quantity_order = target - holds
            if quantity_order > 0:
                self.Log(f'Current {self.underlaying.Value} Holds')
                self.order = self.MarketOrder(self.underlaying, -quantity_order)
                self.Log(f'For a Delta of {delta}, a market order of {-quantity_order} shares in ' +\
                        f'{self.underlaying.Value} was created to mantain the Desired delta of {self.DesiredDelta}')