Overall Statistics
Total Trades
16
Average Win
0.02%
Average Loss
-0.10%
Compounding Annual Return
-1.262%
Drawdown
0.300%
Expectancy
0.019
Net Profit
-0.110%
Sharpe Ratio
-7.39
Probabilistic Sharpe Ratio
19.533%
Loss Rate
14%
Win Rate
86%
Profit-Loss Ratio
0.19
Alpha
-0.069
Beta
0.02
Annual Standard Deviation
0.007
Annual Variance
0
Information Ratio
-6.086
Tracking Error
0.134
Treynor Ratio
-2.732
Total Fees
$16.00
Estimated Strategy Capacity
$320000.00
Lowest Capacity Asset
SPY 325YRWXDVRCRQ|SPY R735QTJ8XC9X
Portfolio Turnover
0.02%
from AlgorithmImports import *
from OrderStatuses import OrderStatusCodes

'''
    Class to handle bracket orders

    TODO: CREATE STOP LOSS AND PROFIT ORDERS BASED ON THE ACTUAL FILL PRICE 
'''
class BracketOrder:
    def __init__(self, algorithm, symbol, totQuantity, placementPrice, limitPrice, stopPrice, profitPrice):
        
        self.algorithm = algorithm
        self.symbol = symbol
        self.totQuantity = totQuantity
        self.placementPrice = placementPrice
        self.limitPrice = limitPrice
        self.stopPrice = stopPrice
        self.profitPrice = profitPrice

        # Initialize future properties
        self.filledQuantity = 0
        self.parentTicket = None
        self.stopOrderTicket = None
        self.profitOrderTicket = None

        # Place initial order
        self.PlaceInitialOrder()

    '''
        Place initial order
    '''
    def PlaceInitialOrder(self):
        self.parentTicket = self.algorithm.StopLimitOrder(self.symbol, self.totQuantity, self.placementPrice, self.limitPrice, tag="Initial order")
        self.algorithm.bracketOrders[self.parentTicket.OrderId] = self
        self.algorithm.Debug(f"Parent order placed for {self.symbol}")
        return      

    '''
        Handle order changes

    '''
    
    def HandleOrderChange(self, orderEvent):

        # If the orderstatus is PartiallyFilled or Filled it will have a filledQuantity > 0
        # Currently make no changes for order statuses that does not (partially) fill an order
        eventFillQuantity = orderEvent.FillQuantity

        if (orderEvent.FillQuantity == 0):
            return
        
        '''
            Handle scenario where parent order is updated

            Add/update stop and profit orders
        '''
        if (orderEvent.OrderId == self.parentTicket.OrderId):
            self.algorithm.Debug(f"Parent order ({self.symbol}) {OrderStatusCodes[orderEvent.Status]}, quantity: {eventFillQuantity}")
            
            self.filledQuantity += eventFillQuantity

            # Place/add to stop order (negative because we are selling)
            self.PlaceStopOrder(-eventFillQuantity)

            # Place/add to profit order (negative because we are selling)
            self.PlaceProfitOrder(-eventFillQuantity)
            return
        
        '''
            Handle scenario where stop loss is hit
        
            - Cancel all open orders on the symbol
            - Liquidate portfolio of that stock (should not be necessary, but extra precaution)
        '''
        if (orderEvent.OrderId == self.stopOrderTicket.OrderId):
            self.CancelTrade()
            self.algorithm.Debug(f"Stop order ({self.symbol}) {OrderStatusCodes[orderEvent.Status]}, quantity: {eventFillQuantity}")
            return
            

        '''
            Handle scenario where profit target is hit

            - Cancel all open orders on the symbol (for now)
            - Should in reality check if profit is partially filled (not equal to self.filledQuantity), and update stop loss order
            - Should probably cancel parent order in case it is partially filled
        '''
        if (orderEvent.OrderId == self.profitOrderTicket.OrderId):
            self.algorithm.Debug(f"Profit order ({self.symbol}) {OrderStatusCodes[orderEvent.Status]}, quantity: {eventFillQuantity}")
            self.CancelTrade()
            return
    
    '''
        Place/add to stop market order when the parent order is (partially) filled
    '''
    def PlaceStopOrder(self, stopQuantity):
        # Create new profit order ticket if non exists
        if (self.stopOrderTicket is None):
            self.stopOrderTicket = self.algorithm.StopMarketOrder(self.symbol, stopQuantity, self.stopPrice, tag=f"Stop order ({self.stopPrice})")
            # Connect the stop order ticket to this object
            self.algorithm.bracketOrders[self.stopOrderTicket.OrderId] = self
            self.algorithm.Debug(f"Stop order placed ({self.symbol})")
            return

        # Update existing stop order ticket
        updateSettings = UpdateOrderFields()
        updateSettings.Quantity = stopQuantity
        response = self.stopOrderTicket.Update(updateSettings)
        self.algorithm.Debug(f"Stop order updated ({self.symbol})")
        return

    '''
        Place/add to profit taking order when the parent order is (partially) filled
    '''
    def PlaceProfitOrder(self, profitQuantity):
        # Create new profit order ticket if non exists
        if (self.profitOrderTicket is None):
            self.profitOrderTicket = self.algorithm.LimitOrder(self.symbol, profitQuantity, self.profitPrice, tag=f"Profit order ({self.profitPrice})")
            # Connect the stop order ticket to this object
            self.algorithm.bracketOrders[self.profitOrderTicket.OrderId] = self
            self.algorithm.Debug(f"Profit order placed ({self.symbol})")
            return

        # Update existing profit order ticket
        updateSettings = UpdateOrderFields()
        updateSettings.Quantity = profitQuantity
        response = self.profitOrderTicket.Update(updateSettings)
        self.algorithm.Debug(f"Profit order updated ({self.symbol})")    
        return

    '''
        Cancel trade
    '''
    def CancelTrade(self):
        # Cancel open orders and liquidate position (pre-caution)
        self.algorithm.Transactions.CancelOpenOrders(self.symbol)
        self.algorithm.Liquidate(self.symbol, tag="LIQUIDATION ORDER INSIDE")

        # Remove order IDs from bracketOrders dictionary
        # self.algorithm.bracketOrders.pop(self.parentTicket.OrderId, None)
        # self.algorithm.bracketOrders.pop(self.profitOrderTicket.OrderId, None)
        # self.algorithm.bracketOrders.pop(self.stopOrderTicket.OrderId, None)
        return
#region imports
from AlgorithmImports import *
#endregion


OrderStatusCodes = {
    0:'NEW', # new order pre-submission to the order processor
    1:'SUBMITTED', # order submitted to the market
    2:'PARTIALLY FILLED', # partially filled, in market order
    3:'FILLED', # completed, filled, in market order
    5:'CANCELED', # order cancelled before filled
    6:'NONE', # no order state yet
    7:'INVALID', # order invalidated before it hit the market (e.g. insufficient capital)
    8:'CANCEL PENDING', # order waiting for confirmation of cancellation
    9:'UPDATE SUBMITTED' # Order update submitted to the market
}
# region imports
from AlgorithmImports import *
from QuantConnect.Securities.Option import OptionPriceModels
from BracketOrder import BracketOrder
# endregion

class BracketOrder:
    def __init__(self, algorithm, symbol, totQuantity, placementPrice, limitPrice, stopPrice, profitPrice):
        self.algorithm = algorithm
        self.symbol = symbol
        self.totQuantity = totQuantity
        self.placementPrice = placementPrice
        self.limitPrice = limitPrice
        self.stopPrice = stopPrice
        self.profitPrice = profitPrice
        self.entryTicket = None
        self.stopLossTicket = None
        self.takeProfitTicket = None
    
    def placeOrder(self):
        self.entryTicket = self.algorithm.MarketOrder(self.symbol, self.totQuantity)
        self.stopLossTicket = self.algorithm.StopMarketOrder(self.symbol, -self.totQuantity, self.stopPrice)
        self.takeProfitTicket = self.algorithm.LimitOrder(self.symbol, -self.totQuantity, self.profitPrice)
    
    
    def onOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            if orderEvent.OrderId == self.entryTicket.OrderId:
              # The main order (parent sell) has been filled, place SL and TP orders
              return
            elif orderEvent.OrderId == self.stopLossTicket.OrderId or orderEvent.OrderId == self.takeProfitTicket.OrderId:
                 # Either SL or TP has been filled, cancel the opposite order
                 if self.stopLossTicket is not None and self.takeProfitTicket is not None:
                     # Make sure both orders exist before attempting to cancel
                     if orderEvent.OrderId == self.stopLossTicket.OrderId:
                          #SL order was filled, cancel TP
                          self.algorithm.Transactions.CancelOrder(self.takeProfitTicket.OrderId)
                     elif orderEvent.OrderId == self.takeProfitTicket.OrderId:
                          # TP order was filled, cancel SL
                          self.algorithm.Transactions.CancelOrder(self.stopLossTicket.OrderId)



class MyAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2023, 1, 1)
        self.SetEndDate(2023, 2, 1)
        self.SetCash(1000000)

        equity = self.AddEquity("SPY", Resolution.Minute)
        equity.SetDataNormalizationMode(DataNormalizationMode.Raw)

        # Add an option universe
        option = self.AddOption("SPY", Resolution.Minute)
        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
        option.SetFilter(-70, 0, timedelta(55), timedelta(65))

        self.symbol = option.Symbol
        self.linked_orders = {}

        # Initiate bracket order handling dictionary
        self.bracketOrders = {}
        self.tickets = []


    def OnData(self, slice):
        # Check if it's 10:00 AM
        if self.Time.hour == 10 and self.Time.minute == 0:
            option_chain = slice.OptionChains.get(self.symbol, None)

            if option_chain is None or not option_chain:
                self.Debug("No option chain data available.")
                return

            puts_in_range = [option for option in option_chain if 55 <= (option.Expiry - self.Time).days <= 65]

            if not puts_in_range:
                self.Debug("No options within the specified range of days until expiry.")
                return

            puts_sorted_by_expiry = sorted(puts_in_range, key=lambda x: abs(x.Expiry - self.Time - timedelta(days=60)))

            put_to_sell = None
            min_delta_difference = float('inf')

            for put_option in puts_sorted_by_expiry:
                delta_difference = abs(put_option.Greeks.Delta - (-0.2))

                if delta_difference < min_delta_difference:
                    put_to_sell = put_option
                    min_delta_difference = delta_difference

            if put_to_sell is not None:
                ask_price = slice.Bars[put_to_sell.Symbol].Close
                stop_loss_price = round(ask_price * 3, 2)
                take_profit_price = round(ask_price * 0.5, 2)

                # Store the BracketOrder instance in a variable
                bracket_order_instance = BracketOrder(self, put_to_sell.Symbol, -1, ask_price, ask_price, stop_loss_price, take_profit_price)

                # Use the BracketOrder class to manage the orders
                bracket_order_instance.placeOrder()

                # Store the BracketOrder instance in a list or dictionary for future reference if needed
                self.bracketOrders[put_to_sell.Symbol] = bracket_order_instance

                # Print information about the selected put option for debugging
                self.Debug(f"Selected Contract Symbol: {put_to_sell.Symbol}, " +
                        f"Strike Price: {put_to_sell.Strike}, " +
                        f"Delta: {put_to_sell.Greeks.Delta}, " +
                        f"Days Until Expiry: {(put_to_sell.Expiry - self.Time).days}")
            else:
                self.Debug("No options found that meet the criteria")