Overall Statistics
Total Trades
20
Average Win
0.02%
Average Loss
-0.03%
Compounding Annual Return
7.590%
Drawdown
0.100%
Expectancy
0.330
Net Profit
0.118%
Sharpe Ratio
7.179
Probabilistic Sharpe Ratio
90.324%
Loss Rate
23%
Win Rate
77%
Profit-Loss Ratio
0.73
Alpha
0.046
Beta
-0.007
Annual Standard Deviation
0.007
Annual Variance
0
Information Ratio
2.456
Tracking Error
0.291
Treynor Ratio
-7.619
Total Fees
$51.80
"""
This file contains QuantConnect order codes for easy conversion and more 
intuitive custom order handling

References:
    https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderTypes.cs
    https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderRequestStatus.cs
"""

OrderTypeKeys = [
    'Market', 'Limit', 'StopMarket', 'StopLimit', 'MarketOnOpen',
    'MarketOnClose', 'OptionExercise',
]

OrderTypeCodes = dict(zip(range(len(OrderTypeKeys)), OrderTypeKeys))

OrderDirectionKeys = ['Buy', 'Sell', 'Hold']
OrderDirectionCodes = dict(zip(range(len(OrderDirectionKeys)), OrderDirectionKeys))

## NOTE ORDERSTATUS IS NOT IN SIMPLE NUMERICAL ORDER

OrderStatusCodes = {
    0:'New', # new order pre-submission to the order processor
    1:'Submitted', # order submitted to the market
    2:'PartiallyFilled', # 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:'CancelPending', # order waiting for confirmation of cancellation
    9:'UpdateSubmitted', # order update is submitted, waiting for confirmation
}
import pandas as pd
import numpy as np
import decimal as d
from datetime import datetime, timedelta, time
from order_codes import (OrderTypeCodes, OrderDirectionCodes, OrderStatusCodes)

#--------------------------#
# Globals/Inputs           |
#--------------------------#

#individual trade parameters
RO = 7 # Risk Out
RT = 15 # Profit Target
SL = 20 # Disaster Stop

direction = 1 #long = 1, short = -1

#entry parameters
#-------------------------#
# Note: script works well for bars/setups that are contained w/in a 100-handle block. 
# It isn't perfect for bars/setups that transition between 100 handle blocks.
#------------------------#
broken_marker = 12 # spot on 100 handle block that breaks (based on bar close). For transitions between 100-handle blocks, the low will always be <100
target = 26 # profit target, code will exit trade if we hit this w/o filling entry order. also used as limit for high/low of opening bar
pullback = -4 # pullback amount for entry, negative numbers = no pullback vs marker.... entryPrice = self.floor100(tradeBar.Low) + broken_marker - pullback*direction
extreme_for_opening_bar = 2 # extreme value (high/low) on the entry bar for method #2
method = 1 #criteria for placing an order:
#(method #1) close and open on opposite sides of broken_marker, extreme of bar does not hit target
#(method #2) for a long trade: close above broken_marker, high < target, low > extreme_for_opening_bar (reverse for short trade) 

# early exit parameters
distance_to_risk_marker = 25 # distance to risk marker to lean against. Always a positive number, trade direction will ensure orders are adjusted in the correct manner. To disable early exits, make this value > disaster stop
distance_above_risk_marker_for_exit = 4 #how far above (for longs) the risk marker to move limit orders when an early exit is triggered

#go again parameters
MAX_GO_AGAINS = 3 # maximum number of times the script will go again if a trade gets risk out-->stop out

EXPIRY_MIN_DAYS = 100 # days
EXPIRY_LIQUIDATE_DAYS = 5 # days (5 days matches TOS continuous contract)

class MultidimensionalVentralThrustAssembly(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 4, 18)
        self.SetEndDate(2020, 4, 22)
        self.SetCash(1000000)
        self.SetTimeZone("America/Los_Angeles")
        
        futureNQ = self.AddFuture(Futures.Indices.NASDAQ100EMini, Resolution.Second)
        futureNQ.SetFilter(timedelta(5), timedelta(90))
        futureNQ.FeeModel = ConstantFeeModel(0)
        
        self.spy = self.AddEquity("SPY", Resolution=Resolution.Second)
        self.SetBenchmark("SPY")
        
        self.consolidators = dict()
        self.order_tickets = dict()
        
        # daily cleanup of open spots, open positions.
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 14), self.onEndOfDay)
        
        # globals for code functionality. Should really try to find a better solution...
        self.openSpot = 100000000
        self.goAgainCount = 0
        self.goAgain = False
        self.earlyExit = False
        
        # globals for logging stats
        self.total = 0 #total trades
        self.RO = 0 # risk out
        self.ROGA = {0:0, 1:0, 2:0, 3:0} # risk out on go again
        self.DS = 0 # hit disaster stop count
        self.DSGA = {0:0, 1:0, 2:0, 3:0} #hit disaster stop on go again count
        self.PT = 0 # hit runner target count
        self.PTGA = {0:0, 1:0, 2:0, 3:0} # hit runner target on go again count
        self.GA = {0:0, 1:0, 2:0, 3:0}  # go again count
        self.EE = 0 #early exit count (risk marker broke)
        
        
    def OnData(self,slice):
        
        # only run during market hours, 
        # don't trade first 5min and don't trade last 30min                         
        #------------------------------------------------------#
        start_time = time(hour=6, minute=35)
        end_time = time(hour=12, minute=30)
        if not start_time < self.Time.time() < end_time: 
            self.Liquidate()
            return
        
        # check if bracket order has been sent
        # sometimes OnOrderEvent doesn't send the bracket order
        #------------------------------------------------------#
        self._confirm_bracket_order()
        
        base_symbol_invested = [self._get_base_symbol(x.Symbol) \
                                for x in self.Portfolio.Values if x.Invested]
        
        for chain in slice.FutureChains:

            # find the front contract expiring no earlier than in N days
            #------------------------------------------------------#
            contracts = [i for i in chain.Value \
                         if i.Expiry > self.Time+timedelta(EXPIRY_LIQUIDATE_DAYS)]
            
            # if there is more than one contract, 
            # trade the one with the closest expiry date
            #------------------------------------------------------#
            if len(contracts) > 0:
                front = sorted(contracts, key=lambda x: x.Expiry, reverse=True)[0]
                sym = front.Symbol
                
                # check if algo is already invested in symbol
                # if so, we don't need to do anything with the new data
                #------------------------------------------------------#
                chain_base_sym = chain.Value.Symbol.Value
                if chain_base_sym in base_symbol_invested: continue
            
                # code for handling entry order that hasn't been filled, or
                # going again if we got risk out/stop out
                # ----------------------------------------------------- #
                
                # code to exit a limit order if our open spot is closed before we get filled
                if front.Symbol.Value in self.order_tickets:
                    openingOrder = self.order_tickets[front.Symbol.Value].ticket
                    if (front.LastPrice - self.openSpot) * direction >= 0:
                        openingOrder.Cancel()
                        self.openSpot = 100000000 #just a big number that will never get hit, it will get reset w/ a new order
                        
                # code to go again after a risk-out stop-out
                elif self.goAgain:
                    qty = 2*direction
                    entryPrice = self.openSpot - target + broken_marker - pullback*direction
                    if ((entryPrice - 0.25*direction) - front.LastPrice)*direction < 0:
                        newTicket = self.StopMarketOrder(sym, qty, entryPrice, 'go again (' + str(self.goAgainCount) + ')')
                    else: 
                        newTicket = self.LimitOrder(sym, qty, entryPrice,'go again (' + str(self.goAgainCount) + ')')
                    self.order_tickets[sym.Value] = symbolOrderData(sym.Value, newTicket, 'Limit')
                    self.goAgain = False
                    self.goAgainCount = self.goAgainCount + 1
                    
        return        
    
    def On2MinDataConsolidated(self, sender, tradeBar):
        
        # only run during market hours                         
        # don't trade first 5min and don't trade last 30min                         
        #----------------------------------------------------#
        start_time = time(hour=6, minute=35)
        end_time = time(hour=12, minute=30)
        if not start_time < self.Time.time() < end_time: return
        
        sym = tradeBar.Symbol
        
        # code for early exit if a risk marker breaks #
        # also prevents more than 1 trade at a time   #
        # ------------------------------------------- #
        if self.Portfolio[sym].Invested: 
            orderData = self.order_tickets[sym.Value]
            entry = orderData.ticket.AverageFillPrice
            risk_marker = entry - (distance_to_risk_marker*direction)
            if (tradeBar.Close - risk_marker)*direction < 0  and sym.Value in self.order_tickets:
                self.earlyExit = True

                updateOrderFields = UpdateOrderFields()
                updateOrderFields.LimitPrice = risk_marker + distance_above_risk_marker_for_exit*direction
                updateOrderFields.Tag = 'marker broke, early exit at (-' + str(distance_to_risk_marker - distance_above_risk_marker_for_exit) + ')'
                orderData.limit_order.Update(updateOrderFields)
                orderData.runner_order.Update(updateOrderFields)
                
            return # don't run the code below, we're already in a trade
        
        if sym.Value in self.order_tickets: return #already have an order ticket submitted, don't need the code below
        
        # code for entering trades based on 2min bar properties #
        # ----------------------------------------------------- #
        normBar = self.normBarTo100Handle(tradeBar)
        #####
        extreme = normBar.High
        other_extreme = normBar.Low
        if direction == -1: #if we're going short, reverse the assignments
            extreme = normBar.Low
            other_extreme = normBar.High
            
        if method==1:
            if not ((normBar.Close - broken_marker)*direction > 0 and\
                    (target - extreme)*direction > 0 and\
                    (broken_marker - normBar.Open)*direction > 0): return
        elif method ==2:
            if not ((normBar.Close - broken_marker)*direction > 0 and\
                    (target - extreme)*direction > 0 and\
                    (other_extreme - extreme_for_opening_bar)*direction > 0): return
        else:
            self.Log('you did not pick a valid method for starting a trade...')
            self.Debug('you did not pick a valid method for starting a trade...')
    
        qty = 2*direction
        self.openSpot = self.floor100(tradeBar.Low) + target
        
        # add the order data to the symbol data dict
        #------------------------------------------------------#
        entryPrice = self.floor100(tradeBar.Low) + broken_marker - pullback*direction
        
        if (entryPrice - tradeBar.Close) * direction < 0:
            newTicket = self.LimitOrder(sym, qty, entryPrice, 'opening order')
        else:
            newTicket = self.StopMarketOrder(sym, qty, entryPrice, 'opening order')
        self.order_tickets[sym.Value] = symbolOrderData(sym.Value, newTicket, 'Limit')
        self.goAgain = False
        self.goAgainCount = 0
        return

    def OnOrderEvent(self, orderEvent):
        """
        This function is triggered automatically every time an order event occurs.
        """
        
        if OrderStatusCodes[orderEvent.Status] in ['Submitted', 'CancelPending','UpdateSubmitted']: return

        k = str(orderEvent.Symbol.Value)
        symbol = str(orderEvent.Symbol)
        
        if (not k in self.order_tickets.keys()): 
            self.Log('missing key in order tickets: {}'.format(k))
            self.Log('order tickets keys: {}'.format(self.order_tickets.keys()))
            return
        
        elif (k in self.order_tickets.keys()):
        
            orderData = self.order_tickets[k]
            orig_order_id = orderData.ticket.OrderId
            order = self.Transactions.GetOrderTicket(orig_order_id)
            
            # sometimes order is nonetype due to security price
            # is equal to zero
            #------------------------------------------------------#
            if not order: 
                self.Debug('order is nonetype: {}'.format(k))
                del self.order_tickets[k] # delete order ticket data  
                return
            
            # if original order is filled but bracket order not submitted,
            # submit bracket order
            #------------------------------------------------------#
            if (OrderStatusCodes[order.Status]=='Filled') and \
                (not orderData.bracket_submit):
                
                if (orderEvent.OrderId == orig_order_id):
                    price = orderEvent.FillPrice
                    qty = orderEvent.FillQuantity
                    #qty = self.CalculateOrderQuantity(k, 0.0)
                    self._place_bracket_order(k,symbol,price,qty)
            
            elif OrderStatusCodes[order.Status] == 'Canceled' and orderEvent.OrderId == orig_order_id:
                self.Debug('canceled the original limit order')
                self.goAgain = False
                self.goAgainCount = 0
                del self.order_tickets[k]
                return
            
            # Check if we have filled/canceled all our orders. 
            # If so, delete the order_tickets[k] object
            #------------------------------------------------------#
            elif OrderStatusCodes[orderData.limit_order.Status] in ['Filled', 'Canceled'] and \
                OrderStatusCodes[orderData.runner_order.Status] in ['Filled', 'Canceled'] and \
                OrderStatusCodes[orderData.stop_market_order.Status] in ['Filled', 'Canceled']: 
                del self.order_tickets[k]
                
            #debug stuff
            elif orderData.stop_market_order.Status not in OrderStatusCodes.keys():
                self.Debug('Found and order status that is not in OrderStatusCodes: ' + str(orderData.stop_market_order))
                
            # Otherwise, one of the exit orders was filled,
            # so let's update the other order tickets
            # and log some stats for use later...
            #------------------------------------------------------#
            
            # first, if stop loss is hit, cancel other orders
            elif (orderData.bracket_submit) and \
                (OrderStatusCodes[orderData.stop_market_order.Status]=='Filled'):
                
                if orderData.take_profit_done and self.goAgainCount < MAX_GO_AGAINS:
                    self.goAgain = True
                    self.GA[self.goAgainCount] = self.GA[self.goAgainCount] + 1
                    # the code to re-enter trades is in the OnData section
                
                #logging/stats
                elif not orderData.take_profit_done and OrderStatusCodes[orderEvent.Status]=='Filled':
                    self.DS = self.DS + 1
                    if self.goAgainCount > 0:
                        self.DSGA[self.goAgainCount] = self.DSGA[self.goAgainCount] + 1
                
                self.Transactions.CancelOpenOrders(symbol)
                    
                
            # second, if take profit is hit, change stop loss to breakeven and adjust quantity to 1
            elif (orderData.bracket_submit) and \
                (OrderStatusCodes[orderData.limit_order.Status]=='Filled') and \
                not orderData.take_profit_done and not self.earlyExit:
                updateOrderFields = UpdateOrderFields()
                updateOrderFields.StopPrice = orderData.ticket.AverageFillPrice + 0.25*direction
                updateOrderFields.Tag = 'stop out'
                qty = orderData.stop_market_order.Quantity
                
                if qty%2==0:
                    updateOrderFields.Quantity = orderData.stop_market_order.Quantity * 1/2
                    orderData.stop_market_order.Update(updateOrderFields)
                    orderData.take_profit_done = True
                
                elif not orderData.take_profit_done: 
                    #we had a partial fill, let's just exit everyting after take profit is hit
                    self.Debug('partial fill...cancelling bracket orders for: {}'.format(self.Time, k))
                    self.Liquidate(symbol)
                    
                #logging/stats
                if not self.earlyExit:
                    self.RO = self.RO + 1
                    if self.goAgainCount > 0:
                        self.ROGA[self.goAgainCount] = self.ROGA[self.goAgainCount] + 1
                else:
                    self.EE = self.EE + 1
            
            # third, if runner target is hit, cancel orders and reset order_tickets
            elif (orderData.bracket_submit) and \
                (OrderStatusCodes[orderData.runner_order.Status]=='Filled'):
                # self.Transactions.CancelOpenOrders(symbol)
                orderData.stop_market_order.Cancel()
                self.Liquidate(symbol)
                
                #logging/stats
                if not self.earlyExit:
                    self.PT = self.PT + 1
                    if self.goAgainCount > 0:
                        self.PTGA[self.goAgainCount] = self.PTGA[self.goAgainCount] + 1
                else:
                    self.EE = self.EE + 1
                    
    def OnSecuritiesChanged(self, changes):
        
        # this code rolls in and out of contracts, keeping 2min bar consolidator
        for security in changes.AddedSecurities:
            if hasattr(security, 'Expiry'):
                if (security.Expiry - self.Time) < timedelta(days=EXPIRY_LIQUIDATE_DAYS): #roll to next contract 5 days early (copy behavior of ToS)
                    self.Debug('rolling out of...' + str(security.Expiry) + '...today is ' + str(self.Time))
                    continue
                consolidator = TradeBarConsolidator(timedelta(minutes=2))
                consolidator.DataConsolidated += self.On2MinDataConsolidated
                self.SubscriptionManager.AddConsolidator(security.Symbol, consolidator)
                self.consolidators[security.Symbol] = consolidator
            
        for security in changes.RemovedSecurities:
            self.Debug('now we are rolling out of' + str(security.Symbol) + '...' + str(security.Expiry) + '...today is ' + str(self.Time))
            consolidator = self.consolidators.pop(security.Symbol)
            self.SubscriptionManager.RemoveConsolidator(security.Symbol, consolidator)
            consolidator.DataConsolidated -= self.On2MinDataConsolidated
    
    def OnEndOfAlgorithm(self):
        #Stats!!!
        self.Log('RO = ' + str(RO) + '# Risk Out')
        self.Log('RT = ' + str(RT) + ' # Profit Target')
        self.Log('SL = ' + str(SL) + ' # Disaster Stop')
        
        self.Log('direction = ' + str(direction) + ' #long = 1, short = -1')


        self.Log('broken_marker = ' + str(broken_marker) + ' # spot on 100 handle block that breaks (based on bar close). For transitions between 100-handle blocks, the low will always be <100')
        self.Log('target = ' + str(target) + ' # profit target, code will exit trade if we hit this w/o filling entry order')
        self.Log('pullback = ' + str(pullback) + ' # pullback amount for entry, negative numbers = no pullback vs marker.... entryPrice = self.floor100(tradeBar.Low) + broken_marker - pullback*direction')
        self.Log('extreme_for_opening_bar = ' + str(extreme_for_opening_bar) + ' # extreme value (high/low) on the entry bar for method #2')
        self.Log('method = ' + str(method) + ' #criteria for placing an order:')
        self.Log('#(method #1) close and open on opposite sides of broken_marker, extreme of bar does not hit target')
        self.Log('#(method #2) for a long trade: close above broken_marker, high < target, low > extreme_for_opening_bar (reverse for short trade) ')
        
        self.Log('# early exit parameters')
        self.Log('distance_to_risk_marker = ' + str(distance_to_risk_marker) + ' # distance to risk marker to lean against. Always a positive number, trade direction will ensure orders are adjusted in the correct manner')
        self.Log('distance_above_risk_marker_for_exit = ' + str(distance_above_risk_marker_for_exit) + ' #how far above (for longs) the risk marker to move limit orders when an early exit is triggered')
        
        self.Log('#go again parameters')
        self.Log('MAX_GO_AGAINS = ' + str(MAX_GO_AGAINS) + ' # maximum number of times the script will go again if a trade gets risk out-->stop out')
        
        self.Log('total: ' + str(self.total))
        self.Log('RO: ' + str(self.RO))
        self.Log('PT: ' + str(self.PT))
        self.Log('EE: ' + str(self.EE))
        self.Log('DS: ' + str(self.DS))
        self.Log('GA: ' + str(self.GA))
        self.Log('ROGA: ' + str(self.ROGA))
        self.Log('PTGA: ' + str(self.PTGA))
        self.Log('DSGA: ' + str(self.DSGA))
        
        self.Log('Risk Out %: ' + str(round(self.RO/self.total,2)))
        self.Log('Runner Target %: ' + str(round(self.PT/self.total,2)))
        self.Log('Disaster Stop %: ' + str(round(self.DS/self.total,2)))
        if self.DS > 0:
            pf = (RO*self.RO + RT*self.PT) / (2*SL*self.DS)
            self.Log('Profit Factor: ' + str(round(pf,2)) + '\n')
            
        t = 0
        for i in self.GA: t=t+self.GA[i]
        self.Log('Go Again %: ' + str(round(t/self.total,2)))
        
        if self.GA[0] > 0:
            self.Log('RO on Go Again (1): ' + str(round(self.ROGA[1]/self.GA[0],2)))
            self.Log('DS on Go Again (1): ' + str(round(self.DSGA[1]/self.GA[0],2)))
            self.Log('PT on Go Again (1): ' + str(round(self.PTGA[1]/self.GA[0],2)))
            if self.DSGA[1] > 0:
                pf = (RO*self.ROGA[1] + RT*self.PTGA[1]) / (2*SL*self.DSGA[1])
                self.Log('Profit Factor: ' + str(round(pf,2)) + '\n')
        else: self.Log('did not go again (1) on any trades')
        
        if self.GA[1] > 0:
            self.Log('RO on Go Again (2): ' + str(round(self.ROGA[2]/self.GA[1],2)))
            self.Log('DS on Go Again (2): ' + str(round(self.DSGA[2]/self.GA[1],2)))
            self.Log('Runner on Go Again (2): ' + str(round(self.PTGA[2]/self.GA[1],2)))
            if self.DSGA[2] > 0:
                pf = (RO*self.ROGA[2] + RT*self.PTGA[2]) / (2*SL*self.DSGA[2])
                self.Log('Profit Factor: ' + str(round(pf,2)) + '\n')
        else: self.Log('did not go again (2) on any trades')
        
        if self.GA[2] > 0:
            self.Log('RO on Go Again (3): ' + str(round(self.ROGA[3]/self.GA[2],2)))
            self.Log('Runner on Go Again (3): ' + str(round(self.PTGA[3]/self.GA[2],2)))
            self.Log('DS on Go Again (3): ' + str(round(self.DSGA[3]/self.GA[2],2)))
            
            if self.DSGA[3] > 0:
                pf = (RO*self.ROGA[3] + RT*self.PTGA[3]) / (2*SL*self.DSGA[3])
                self.Log('Profit Factor: ' + str(round(pf,2)) + '\n')
        else: self.Log('did not go again (3) on any trades')
        
        return
    
    def onEndOfDay(self):
        self.counter = 0
        self.Liquidate()
        return
    
    def to100Handle(self, number):
        return number - Math.Floor(number/100)*100

    def to10Handle(self, number):
        return number - Math.Floor(number/10)*10
    
    def floor100(self, number):
        return Math.Floor(number/100)*100
    
    def floor10(self, number):
        return Math.Floor(number/10)*10
    
    def normBarTo100Handle(self, Bar):
        normBar = Bar.Clone()
        
        if self.floor100(Bar.Low) == self.floor100(Bar.High): # if we don't move between 100 handle blocks
            normBar.Open = self.to100Handle(Bar.Open)
            normBar.Close = self.to100Handle(Bar.Close)
            normBar.Low = self.to100Handle(Bar.Low)
            normBar.High = self.to100Handle(Bar.High)
                
        # transtion between 100 handle block (produce #'s >100, and low as a part of 100 handle block)
        elif (self.floor100(Bar.Low) < self.floor100(Bar.High)):
            if self.floor100(normBar.Open) == self.floor100(Bar.High):
                normBar.Open = self.to100Handle(normBar.Open) + 100
            else: 
                normBar.Open = self.to100Handle(normBar.Open)
            
            if self.floor100(normBar.Close) == self.floor100(Bar.High):
                normBar.Close = self.to100Handle(normBar.Close) + 100
            else: 
                normBar.Close = self.to100Handle(normBar.Close)
            
            normBar.High = self.to100Handle(normBar.High) + 100
            normBar.Low = self.to100Handle(Bar.Low)

        normBar.Volume = Bar.Volume
        
        return normBar
    
    #--------------------------#
    # ALGORITHM HELPER FUNCS   |
    #--------------------------#                
                
    def _calc_order_quantity(self, sym, pct):
        """
        Compute order quantity based on percentage of portfolio value.
        This is required because SetHoldings doesn't return an order ticket.
        
        This function returns None when security price is less than 1.
        """
        price = float(self.Securities[sym].Price)
        self.Log('{} calc order quantity price: {}'.format(sym, price))
        if price < 1: return None
        qty = (pct * float(self.Portfolio.TotalPortfolioValue)) / price
        return int(qty)
    
    def _get_base_symbol(self, symbol):
        """
        Get minimum symbol for comparisons.
            Example: 'ES XXXXXXXX' will return 'ES'
        """
        sym = str(symbol).split(' ')[0]
        return sym
        
    def _check_holdings_expiry(self):
        """
        Check if current holdings are expiring within certain number
        of days. If so liquidate the contract, cancel open orders and delete
        order data from dict.
        """
        
        invested = [x.Symbol for x in self.Portfolio.Values if x.Invested]
        #self.Log('[{}] invested:\n{}'.format(self.Time, invested))
        
        for sym in invested:
            k = sym.Value
            if (k in self.order_tickets.keys()):
                
                expiry = self.order_tickets[k].contract.Expiry
                
                if (expiry < self.Time + timedelta(EXPIRY_LIQUIDATE_DAYS)):
                    self.Debug('expiry too close liquidating: {}'.format(k))
                    self.Liquidate(sym)
                    self.Transactions.CancelOpenOrders(sym)
                    del self.order_tickets[k]     
        return
    
    def _confirm_bracket_order(self):
        """
        confirm bracket orders have been submitted.
        sometimes OnOrderEvent is not sending orders
        """
        
        invested = [x.Symbol for x in self.Portfolio.Values if x.Invested]
        #self.Log('[{}] invested:\n{}'.format(self.Time, invested))
        
        for sym in invested:
            
            k = sym.Value

            if (k in self.order_tickets.keys()): 
                
                orderData = self.order_tickets[k]
                orig_order_id = orderData.ticket.OrderId
                order = self.Transactions.GetOrderTicket(orig_order_id)

                if (not orderData.bracket_submit):
                    price = order.AverageFillPrice
                    qty = order.QuantityFilled
                    self._place_bracket_order(k,sym,price,qty)
                    
        return
            
    def _place_bracket_order(self,k,symbol,price,qty):              
            
            if qty%2==0:
                profit_target = price + direction*d.Decimal(RO)
                limitTicket = self.LimitOrder(symbol, -1*qty/2, profit_target, 'risk out')
                self.order_tickets[k].add_limit_order(limitTicket)
                
                runner_target = price + direction*d.Decimal(RT)
                runnerTicket = self.LimitOrder(symbol, -1*qty/2, runner_target, 'runner')
                self.order_tickets[k].add_runner_order(runnerTicket)
            
            else:
                profit_target = price + direction*d.Decimal(RO)
                limitTicket = self.LimitOrder(symbol, -1*qty, profit_target, 'partial fill...')
                self.order_tickets[k].add_limit_order(limitTicket)
                self.order_tickets[k].add_runner_order(limitTicket) #partial fill, just need to populate the value w/ something...
            
            self.total = self.total + 1
            self.earlyExit = False
            
            stop_loss = price - direction*d.Decimal(SL)
            stopTicket = self.StopMarketOrder(symbol, -1*qty, stop_loss, 'disaster stop')
            self.order_tickets[k].add_stop_market_order(stopTicket)
            
            self.order_tickets[k].is_bracket()
            
            # self.Log('check bracket >> bracket order submitted: {}'.format(k))

class symbolOrderData:
    """
    Class object to hold symbol bracket order data
    
    Attributes:
    -----------
        symbol: str 
        ticket: ticket object for opening order
            does not work with setHoldings
        order_type: str
            must match QC order types in order_codes.py
        bracket_submit: bool
            toggled when bracket order is submitted
        limit_order: None or ticket object
        runner_order: None or ticker object
        stop_market_order: None or ticket object
        contract: None or futures contract object
    
    Methods:
    --------
        add_limit_order: set limit_order attr to limitTicket
        add_runner_order: set runner_order to runnerTicket
        add_stop_market_order: set stop_market_order attr to stopMarketTicket
        is_bracket: if limit, runner, and stopMarket orders are submitted toggle
            bracket_submit attr
    """
    
    def __init__(self, symbol, ticket, order_type, contract=None):
        
        self.symbol = symbol
        self.ticket = ticket
        self.order_type = order_type
        if contract:
            self.contract = contract
        self.bracket_submit = False  
        self.take_profit_done = False
        self.limit_order = None
        self.runner_order = None
        self.stop_market_order = None
        
    def add_limit_order(self, limitTicket):
        self.limit_order = limitTicket
        return
    
    def add_runner_order(self, runnerTicket):
        self.runner_order = runnerTicket
        return
    
    def add_stop_market_order(self, stopMarketTicket):
        self.stop_market_order = stopMarketTicket
        return
    
    def is_bracket(self):
        if (self.limit_order is not None) and (self.runner_order is not None) and (self.stop_market_order is not None):
            self.bracket_submit = True
        return