Overall Statistics
Total Trades
42
Average Win
31.88%
Average Loss
-28.18%
Compounding Annual Return
-73.749%
Drawdown
88.900%
Expectancy
0.015
Net Profit
-73.941%
Sharpe Ratio
-0.451
Probabilistic Sharpe Ratio
5.888%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.13
Alpha
-0.507
Beta
-1.385
Annual Standard Deviation
1.044
Annual Variance
1.09
Information Ratio
-0.41
Tracking Error
1.086
Treynor Ratio
0.34
Total Fees
$2018.25
Estimated Strategy Capacity
$110000.00
# ----------------------------------------------------------------------
#
# Custom Buying power model to solve insufficient funds problem. There is a fix coming in December/January
#
# ----------------------------------------------------------------------

class CustomBuyingPowerModel(BuyingPowerModel):
    def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
        quantity = super().GetMaximumOrderQuantityForTargetBuyingPower(parameters).Quantity
        quantity = np.floor(quantity / 100) * 100
        return GetMaximumOrderQuantityResult(quantity)

    def HasSufficientBuyingPowerForOrder(self, parameters):
        return HasSufficientBuyingPowerForOrderResult(True)
# Your New Python File
from datetime import timedelta
from QuantConnect.Orders import OrderDirection
import numpy as np
import re


class OrderManagement:

    def __init__(self, algorithm):
        self.algo = algorithm

    def open_hedge(self):

        slice = self.algo.CurrentSlice

        for i in slice.OptionChains:
            chain = i.Value
            # spot_price = chain.Underlying.Price
            contracts = [x for x in chain if x.Right == 1]
            contracts = sorted(contracts, key=lambda x: (x.Expiry, x.Greeks.Delta))
            self.algo.hedge["contract"] = min(contracts, key=lambda x: abs(x.Greeks.Delta-(-0.30)))

        if self.algo.hedge["contract"] is None:
            return

        qty = (self.algo.Portfolio.Cash * 0.60) / (self.algo.hedge["contract"].AskPrice * 100)

        if qty < 1:
            return
        else:
            self.algo.Debug(f"H - S {self.algo.hedge['contract'].Strike} Ex {self.algo.hedge['contract'].Expiry} D{round(self.algo.hedge['contract'].Greeks.Delta, 3)}")

            self.algo.longHedgeOrder = self.algo.Buy(self.algo.hedge["contract"].Symbol,  np.floor(qty))
            self.algo.hedge["position"] = True
            self.algo.hedge["quantity"] = self.algo.longHedgeOrder.Quantity

    def open_spread(self, shortContract, longContract):

        # Get our margin
        margin = self.algo.Portfolio.GetBuyingPower(
            shortContract.Symbol, OrderDirection.Sell)

        # Get the quantities
        qty = margin * self.algo.investPercent / \
            ((shortContract.BidPrice + longContract.AskPrice) * 100)

        # Check that contracts are not the same
        if qty < 1:
            return
        else:

            # Log out what our contracts are:
            self.algo.Debug(f"S - Expiry: {shortContract.Expiry} Delta: {round(shortContract.Greeks.Delta, 6)} macd:{round(self.algo.macd.Signal.Current.Value, 2)}")
            self.algo.Debug(f"L - Expiry: {longContract.Expiry} Delta: {round(longContract.Greeks.Delta, 6)}")
            
            # Perform the order
            self.algo.longOrder = self.algo.Buy(longContract.Symbol,  np.floor(qty))
            self.algo.shortOrder = self.algo.Sell(shortContract.Symbol,  np.floor(qty))

            # Set in position as true so we don't continue buying
            self.algo.inPosition = True

            # Store the net Credit
            self.algo.netCredit = (
                np.abs(self.algo.shortOrder.AverageFillPrice) - np.abs(self.algo.longOrder.AverageFillPrice)) * np.abs(self.algo.longOrder.QuantityFilled) * 100

            # Set the openPortfolioValue for Profit Calculations
            self.algo.openPortfolioValue = self.algo.Portfolio.TotalPortfolioValue

            # store expiry
            self.algo.spread_expiry = shortContract.Expiry

    def close_all(self):
        # kill all positions
        self.algo.Liquidate()
        self.algo.inPosition = False
class PositionManagement:

    def __init__(self, algorithm):
        self.algo = algorithm
        self.previousGainCategory = 0

    def hedge_handler(self):
        # helper variables
        unreal = self.algo.Portfolio[self.algo.hedge["contract"].Symbol].UnrealizedProfit
        cost = self.algo.Portfolio[self.algo.hedge["contract"].Symbol].HoldingsCost
        qty = int(round(self.algo.hedge["quantity"] / 4))
            
        # wait for the gain categories to be set
        if self.algo.hedge["gainCategory"] == None:
            return
        
        # its selling more than we have.
        if self.algo.Portfolio[self.algo.hedge["contract"].Symbol].Quantity > 0:

            if self.algo.hedge["gainCategory"] == -1:
                self.algo.Liquidate()
                reset(self)
                
            elif self.algo.hedge["gainCategory"] == self.previousGainCategory:
                return

            elif self.algo.hedge["gainCategory"] > self.previousGainCategory:
                
                # handler any remainder quantities.
                if self.algo.Portfolio[self.algo.hedge["contract"].Symbol].Quantity < qty:
                    qty = self.algo.Portfolio[self.algo.hedge["contract"].Symbol].Quantity
                    
                self.algo.MarketOrder(self.algo.hedge["contract"].Symbol, -qty)
                
                self.algo.Debug(f'Hedge Sell | gain%: {safe_div(unreal, cost)} | scale:{self.algo.hedge["gainCategory"]} @ {self.algo.Time}')
        
                # set the previous gain category to the current catehory
                self.previousGainCategory = self.algo.hedge["gainCategory"]
            
        else:
            self.algo.Liquidate()
            reset(self)


def safe_div(x, y):
    if x == 0:
        return 0
    return round(x / y, 2)


def reset(self):
    # reset our hedge as we have none left
    self.previousGainCategory = 0
    self.algo.hedge["position"] = False
    self.algo.hedge["profile"] = 1
    self.algo.hedge["gainCategory"] = None
    self.algo.hedge["contract"] = None
    self.algo.hedge["scale"] = 0
    self.algo.hedge["stopLoss"] = 0
    self.algo.hedge["quantity"] = 0
from signals import *
from position import *
from order import *
from lib import CustomBuyingPowerModel
from datetime import timedelta
from QuantConnect.Data.Custom.CBOE import CBOE
from QuantConnect.Securities.Option import OptionPriceModels
from QuantConnect.Indicators import RollingWindow
from QuantConnect.Data.Market import TradeBar
from QuantConnect.Algorithm import *
from QuantConnect import *
from System import *
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Common")


class OptionsAlgorithm(QCAlgorithm):

    def Initialize(self):
        """
        Initializes the algorithm

        Args:
            self: write your description
        """
        # Base QuantConnect Parameters
        self.SetStartDate(2018, 1, 1)
        self.SetEndDate(2019, 1, 1)
        self.SetCash(150000)

        # Base Algorithm Paramters                                        # needs to be set in Quantconnect GUI
        self.investPercent = float(self.GetParameter("investPercent"))    # default 0.9
        self.shortDelta = float(self.GetParameter("shortDelta"))/-100     # default -0.25
        self.longDelta = float(self.GetParameter("longDelta"))/-100       # default -0.15
        self.t1DTE = int(self.GetParameter("t1DTE"))                      # default 25
        self.t2DTE = int(self.GetParameter("t2DTE"))                      # default 45

        # Handlers:
        self.order = OrderManagement(self)
        self.position = PositionManagement(self)
        self.signal = SignalManagement(self)

        # Helper Variables
        self.netCredit = None
        self.spread_expiry = None
        self.inPosition = False
        self.hedge = {
            "position": False,
            "quantity": None,
            "contract": None,
            "gainCategory": None,
            "profile": 1,
            "stopLoss": 0,
            "scale": 0,
        }
        self.stoppedOut = False
        self.openPortfolioValue = None

        # Set Option Instruments
        self.symbol = "SPY"
        self.option = self.AddOption(self.symbol, Resolution.Minute)
        self.option.PriceModel = OptionPriceModels.CrankNicolsonFD()
        self.option.SetFilter(-40, -10, timedelta(self.t1DTE), timedelta(self.t2DTE))
        self.option.SetBuyingPowerModel(CustomBuyingPowerModel.CustomBuyingPowerModel())
        self.vix = self.AddData(CBOE, "VIX", Resolution.Daily)

        # Set Other Securities
        self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))
        self.equity = self.AddEquity(self.symbol, Resolution.Daily)
        self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)

        # Initialize TA Parameters
        self.EMA10 = self.EMA(self.symbol, 10, Resolution.Daily)
        self.EMA25 = self.EMA(self.symbol, 25, Resolution.Daily)
        self.SMA50 = self.SMA(self.symbol, 50, Resolution.Daily)
        self.SMA125 = self.SMA(self.symbol, 125, Resolution.Daily)
        self.macd = self.MACD(self.symbol, 12, 26, 9, resolution = Resolution.Daily)
        self.windowSPX = RollingWindow[TradeBar](2)
        self.windowCross = RollingWindow[float](2)

        # Set warmup for Greeks and Indicators
        self.SetWarmUp(timedelta(days=125))

        # Check exits everyday
        self.Schedule.On(self.DateRules.EveryDay(
            self.symbol), self.TimeRules.AfterMarketOpen(self.symbol, 5), self.balance)

    def OnData(self, slice):
        # do some warmup checks
        if self.Time.hour == 9 and self.Time.minute == 31:

            if self.IsWarmingUp:
                return

            # add slice
            self.windowSPX.Add(slice[self.symbol])
            # add our ema to a window
            self.windowCross.Add(self.EMA10.Current.Value)
            # wait for windows to be ready
            if not self.windowSPX.IsReady and not self.windowCross.IsReady:
                return
            self.Log(f"o: {self.Securities['SPY'].Open} h: {self.Securities['SPY'].High} l: {self.Securities['SPY'].Low} c: {self.Securities['SPY'].Close}")
            # check for buy signals
            signal = self.signal.spread_buy()

            if self.inPosition:
                self.balance
            else:
                if signal:
                    # reset our stopped out variable so we can get into a hedge if we stop out.
                    self.stoppedOut = False
                    self.get_contracts(slice)

    def get_contracts(self, slice):
        # find contracts
        shortContract = None
        longContract = None

        for i in slice.OptionChains:
            chain = i.Value
            contracts = [x for x in chain if x.Right == 1]
            contracts = sorted(contracts, key=lambda x: (x.Expiry, x.Greeks.Delta))

            shortContract = min(contracts, key=lambda x: abs(x.Greeks.Delta-self.shortDelta))
            longContract = min(contracts, key=lambda x: abs(x.Greeks.Delta-self.longDelta))

        # Check that contracts are not the same
        if shortContract != longContract:
            self.order.open_spread(shortContract, longContract)

    def balance(self):

        # Determine if we need to hedge
        if not self.hedge["position"] and self.stoppedOut:
            signal = self.signal.hedge_buy()

            if signal:
                self.order.open_hedge()

        if self.hedge["position"]:

            signal = self.signal.hedge_sell()

            if signal:
                self.position.hedge_handler()

        # check that we have a spread open
        if self.openPortfolioValue is not None and self.inPosition:

            # Generate signals to sell
            signal = self.signal.spread_sell()

            if signal:
                self.order.close_all()
from QuantConnect import TradingDayType


class SignalManagement:

    def __init__(self, algorithm):
        self.algo = algorithm
        self.profiles = [
            {"hedge": [{0, 1}, {1, 2}, {2, 3}, {3, 4}], "range": [1, 2, 3, 4]},
            {"hedge": [{0, 1}, {0.3, 2}, {0.6, 3}, {0.9, 4}], "range": [0.3, 0.6, 0.9, 1.2]}
        ]
        self.tolerance = 0.1

    def spread_buy(self):
        # helper variables
        currEma10 = self.algo.windowCross[0]
        pastEma10 = self.algo.windowCross[1]

        # Conditions
        c1 = self.algo.SMA125.Current.Value < self.algo.Securities[self.algo.symbol].Open
        c2 = self.algo.EMA25.Current.Value < self.algo.Securities[self.algo.symbol].Open
        c3 = self.algo.EMA10.Current.Value > self.algo.EMA25.Current.Value
        c4 = pastEma10 < self.algo.EMA25.Current.Value and currEma10 > self.algo.EMA25.Current.Value
        c5 = self.algo.macd.Signal.Current.Value <= 2.1

        if c1 and c2 and c3 and c5:
            return True

        # if c4:
        #     self.algo.Debug('we got a cross')
        #     return True

    def spread_sell(self):
        # helper variables
        currClose = self.algo.windowSPX[0].Close
        oldClose = self.algo.windowSPX[1].Close
        days_till_expiry = list(self.algo.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, self.algo.Time, self.algo.Time + (self.algo.spread_expiry - self.algo.Time)))

        # Conditions
        c1 = safe_div(self.algo.Portfolio.TotalUnrealizedProfit, self.algo.netCredit) > 0.7
        c2 = safe_div(self.algo.Portfolio.TotalUnrealisedProfit, self.algo.Portfolio.TotalPortfolioValue) < -0.5
        c3 = self.algo.EMA10.Current.Value < self.algo.EMA25.Current.Value and self.algo.Portfolio.TotalUnrealisedProfit < 0
        c4 = ((currClose - oldClose) / oldClose) < -0.03
        c5 = len(days_till_expiry) > 7
        c6 = self.algo.macd.Signal.Current.Value > 2.1

        if c1:
            # self.algo.Debug('Sell Signal | 80% max profit')
            return True

        if c2:
            self.algo.Debug(f"Sell | -50 percent stop loss @ {self.algo.Time}")
            self.algo.stoppedOut = True
            return True

        if c3:
            self.algo.Debug('Sell | CROSS')
            # self.algo.stoppedOut = False
            return True

        # if c4:
        #     self.algo.Debug(f'Sell Signal | SPX falling {round(((currClose - oldClose) / oldClose), 2) * 100}% old close: {oldClose}. curr close: {currClose} @ {self.algo.Time}')
        #     self.algo.stoppedOut = True
        #     return True

        if c5 and c6:
            self.algo.Debug(f"Sell | macd: {round(self.algo.macd.Signal.Current.Value, 2)} days left: {len(days_till_expiry)}")
            self.algo.stoppedOut = True
            return True

    def hedge_buy(self):
        # helper variables
        currEma10 = self.algo.windowCross[0]
        pastEma10 = self.algo.windowCross[1]
        c1 = pastEma10 > self.algo.EMA25.Current.Value and currEma10 < self.algo.EMA25.Current.Value

        # Conditions
        if c1:
            self.algo.Debug(f"Hedge Buy Signal: 10EMA is {round(self.algo.EMA10.Current.Value, 2)} below 25 EMA {round(self.algo.EMA25.Current.Value, 2)} date: {self.algo.Time.date}")
            return True

    def hedge_sell(self):
        # helper variables
        unreal = self.algo.Portfolio[self.algo.hedge['contract'].Symbol].UnrealizedProfit
        cost = self.algo.Portfolio[self.algo.hedge['contract'].Symbol].HoldingsCost
        profitPercent = safe_div(unreal, cost)

        if self.algo.vix.Close > 35:
            if self.algo.hedge["profile"] == 1:
                # if a hedge went from non-volatile to volatile you need to restart the range checks
                # relative to where we are. so if you sold at 30% in non-V and it is volatile you're
                # hedge startegy needs to reset to the lowest bound in the gainCategory.
                self.algo.hedge["gainCategory"] = 0

            self.algo.hedge["profile"] = 0

        closestTuple = min(enumerate(self.profiles[self.algo.hedge["profile"]]["range"]), key=lambda x: abs(x[1]-profitPercent))
        self.algo.Debug(f"profit {profitPercent} profile: {self.algo.hedge['profile']} gc: {self.algo.hedge['gainCategory']}")
        if ((closestTuple[1] - self.tolerance) <= profitPercent):

            if closestTuple[0] == len(self.profiles[0]["range"])-1:
                self.algo.hedge["gainCategory"] = -1
                return True

            if profitPercent < (closestTuple[1] + self.tolerance):

                self.algo.hedge["gainCategory"] = closestTuple[0]

                hedgeProfile = self.profiles[self.algo.hedge["profile"]]["hedge"]

                if closestTuple[0] == 0 or self.algo.hedge["stopLoss"] >= list(hedgeProfile[closestTuple[0]-1])[0]:

                    self.algo.hedge["stopLoss"] = list(hedgeProfile[closestTuple[0]])[0]
                    self.algo.hedge["scale"] = list(hedgeProfile[closestTuple[0]])[1]

                return True


def safe_div(x, y):
    if x == 0:
        return 0
    return round(x / y, 2)