Overall Statistics
Total Orders
41
Average Win
0%
Average Loss
0%
Compounding Annual Return
-57.445%
Drawdown
19.300%
Expectancy
0
Start Equity
100000
End Equity
86897
Net Profit
-13.103%
Sharpe Ratio
-1.218
Sortino Ratio
-2.318
Probabilistic Sharpe Ratio
13.802%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
-0.485
Beta
0.216
Annual Standard Deviation
0.382
Annual Variance
0.146
Information Ratio
-1.429
Tracking Error
0.388
Treynor Ratio
-2.156
Total Fees
$400.00
Estimated Strategy Capacity
$860000.00
Lowest Capacity Asset
PW R735QTJ8XC9X
Portfolio Turnover
1.55%
# region imports
from AlgorithmImports import *
# endregion


class SymbolData:
    
    def __init__(self, symbol, algo):
        self.symbol = symbol
        self.algo = algo
        
        self.bar_window = RollingWindow[TradeBar](2) #COULD make this closes only....
        
        self._state = 0
        
        self.db = self.algo.debug_lvl
        

    def Update(self, bar):
        self.bar_window.Add(bar) #     self.tradeBarWindow.Add(data["SPY"]) --- To Add (Then ref as if a BAR)
    
    @property
    def CheckStates(self):
        #IF crossed above, go from 0 to 1
        if self.state == 0:
            if self.CrossedAboveLvl:
                self.state = 1
                if self.db > 0:
                    self.algo.Debug(f'S0 -> S1  --- {self.symbol}')
            
        #IF crossed BELOW, go from 1 to 2
        if self.state == 1:
            if self.CrossedBelowLvl:
                self.state = 2
                if self.db > 0:
                    self.algo.Debug(f'S1 -> S2  --- {self.symbol}')
                
        return self.state
        
    @property
    def IsReady(self):
        return self.bar_window.IsReady
        
    @property
    def CrossedAboveLvl(self):
        if not self.IsReady:
            return False
    
        prev = self.bar_window[1].Close
        curr = self.bar_window[0].Close
        
        if curr > self.algo.high_cross_lvl and prev <= self.algo.high_cross_lvl:
            return True
        return False

    @property
    def CrossedBelowLvl(self):
        if not self.IsReady:
            return False
            
        prev = self.bar_window[1].Close
        curr = self.bar_window[0].Close
        
        if curr < self.algo.low_cross_lvl and prev >= self.algo.low_cross_lvl:
            return True
        return False
        
    # -------------------- Added a Property for state (increased safety)
    
    @property
    def state(self):
        return self._state
        
    @state.setter
    def state(self, new):
        if new in [0,1,2]:
            self._state = new
        
        
    
        
# region imports
from AlgorithmImports import *
# endregion
from SymbolData import SymbolData

class MeasuredRedCoyote(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2024, 6, 1) #Changed to test entries + stops
        # self.SetEndDate(2024, 8,31)

        self.SetCash(100000) 
        self.AddEquity("SPY", Resolution.Minute)
        
        self.AddUniverse(self.SelectCoarse, self.SelectFine)
        
        # ---------------- Parameters -------------- #
        self.n_shares = 10 * 1000               #10k
        self.lookback = 30
        
        self.max_beg_price = 1.00               # Price must start below, and be below this for lb days (and cross below again for state 2)
        self.high_cross_lvl = 1.20              # Price must cross this to enter state 1
        
        self.low_cross_lvl = self.max_beg_price 
        
        self.STOP_OFF = True                   # TURNS STOP LOSS OFF
        
        self.entry_offset_ticks = 4             # Number of ticks to submit stop order (ABOVE max_beg_price)
        
        
        self.stop_offset_ticks = 10             # Number of ticks for STOP LOSS (10 = .1 --> entry - .1)
        
        #Original calc...
        #self.tgt_ticks = 500                    # Number of ticks for Profit Target (400 = 4.0 --> entry + 4)
                                                # 5 - 10 POINTS in example -> 500 - 900 ticks.
        
        self.tgt_1 = 4.50                  #4.50 price 
        self.tgt_2 = 8.00                  # 8 price
        self.tgt_3 = 12.0
        self.tgt_4 = 14.0
        
        self.limit_entry_on = True             # Turn Stop Limit Entries on (Vs Stop Market)
        
        self.max_slip_ticks = 4                 # IF using Entry Limit, MAX ticks off trigger price to fill.
        
        self.debug_lvl  = 2                     # Prints details to log (State counts, Universe, etc)
        

        ## ---------------------------------- Explanation --------------------------------------------- ##
        
        # Logic + Params Explained:
        
        # 1. Universe: Select ALL symbols with price < $1, with MAX price of last (lookback) days < $1 ( < max_beg_price)
        # 2. Check State Updates on Universe:
        #     S0 - Below max_beg_price for past 30 (lookback) days -- (Inside Universe)
                    
        #     S1 - Crossed above 1.40    (high cross_lvl) 

        #     S2 - Crossed below 1.00 (again) (low cross lvl)
            
        # 3. Submit Stop Order 4 ticks above beg price (entry_offset_ticks + max_beg_price)
        
        # 4. IF Entry filled, Submit Stop + Target as well.
        #         (stop_offset_ticks & tgt_ticks)
                
        # 5. If day ended, cancel all pending orders, all symbols go back to state 0
        
        
        # ---------------- Declarations ------------ # 
        
        # self.state_1 = []
        # self.state_2 = []
        self.symbol_list = []
        
        self.stop_orders = {}
        
        self.symbol_data = {}
        
        # ---------------- Scheduled Events 
        
        self._test_entries = False                                               
        # TO TEST ENTRIES + EXITS (Sets all universe to state 2)
        
        
    def OnEndOfDay(self):
        data = self.CurrentSlice
        
        ## RESET STATES ####
        # self.state_1.clear()
        # self.state_2.clear() 
        
        # univ = [kvp.Key for kvp in self.Portfolio]
        # self.Debug(f'Universe -- {univ}')
        
        #Reset instances to state 0 (Begin) ------------------------------------ TO RM same day req, RM this block
        for symbol, obj in self.symbol_data.items():
            obj.state = 0
            
        # Clear Out Old Pending orders (IF NOT INVESTED) -- IF Invested, KEEP the orders.
        not_invested = [kvp.Key for kvp in self.Portfolio if not kvp.Value.Invested]
        for i in not_invested:
            self.Transactions.CancelOpenOrders(i)
            
        
        
    def SelectCoarse(self, coarse):
        filtered = [ x for x in coarse if x.Price < self.max_beg_price]
        
        # keep ONLY symbols that have been BELOW 1 for last 30 days. 
        # (Check if MAX of 30 days is > 1.0)
        
        _symbols = [x.Symbol for x in filtered]
        history = self.History(_symbols, self.lookback, Resolution.Daily)
        
        chosen = []
        for sym in _symbols:
            try:
                h = history.loc[sym]
            except:
                self.Debug(f'Error with hist.loc[{sym}]')
                
            max_in_lb = h.high.max()
            if max_in_lb < self.max_beg_price:
                chosen.append(sym)
                
        if self.debug_lvl > 0:
            self.Debug(f'Universe ( {len(chosen)} symbols ) ---- {[str(i) for i in chosen]}')
        self.symbol_list = chosen
        return self.symbol_list 
        
        
    def SelectFine(self, fine):
        return [f.Symbol for f in fine]
        

    def OnData(self, data):
        ''' OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
            Arguments:
                data: Slice object keyed by symbol containing the stock data
        '''
        
        data = data.Bars
        
        ## --------- EXTRA SAFETY ---------- ## 
        if not data.ContainsKey("SPY"):
            self.Debug(f'Returning out -- No Data for SPY. (No data period)')
            return
    
        self.ClearErrors() #Clear up any remainders...
        
        has_data = [i for i in self.symbol_list if data.ContainsKey(i)]
        
        # Begin looking for various states!!  (rolling windows to check CROSS)
        
        for symbol in has_data:
            # Check if FREE margin ? for 1 mroe entry? 
            
            # approx_margin = self.n_shares * 1
            # if self.Portfolio.MarginRemaining < approx_margin: continue 
        
            # CHECK if already invested --- 
            if self.Portfolio[symbol].Invested: continue
        
            # CHECK if already PENDING ----
            openOrders = self.Transactions.GetOpenOrders(symbol)
            if len(openOrders) > 0: continue

            # Check States of Symbols
            if symbol not in self.symbol_data:
                self.symbol_data[symbol] = SymbolData(symbol, self)                       #__init__(self, symbol, algo):
            bar = data[symbol]
            self.symbol_data[symbol].Update(bar)
            
            if self._test_entries:                                              # Temporary TESTING of entries
                self.symbol_data[symbol].state = 2
                
            _state = self.symbol_data[symbol].CheckStates
            
  
            
            if _state == 2:
                entry_price = self.low_cross_lvl + (self.entry_offset_ticks / 100)
                    
                remaining_usd = self.Portfolio.MarginRemaining                  
                

                #close = self.Securities[symbol].Close
                close = bar.Close
                if close == 0:
                    continue 
                
                # shares = int(remaining_usd / close)                             #Set Position Size.... (COULD be fixed? )
                
                shares = self.n_shares
                
                ## --------------- TEST ENTRIES --------------- ## 
                if self._test_entries:
                    # shares = int(remaining_usd / close)                         #Original
                    
                    if self.limit_entry_on:
                        entry_price = close                                     # TO MAKE THEM MARKETABLE LIMITS (for TESTING only) --- COMMENT this
                        entryTicket = self.StopLimitOrder(symbol, shares, entry_price, entry_price + self.max_slip_ticks * .01, "Entry")
                        self.Debug(f'Submitted Entry Order --  {symbol} @ {entry_price}')
                    else:
                        self.MarketOrder(symbol, shares, False, "Entry")
                    return 
                
                ## ------------- REAL Entries -------------- ## 
                if self.limit_entry_on:
                    entryTicket = self.StopLimitOrder(symbol, shares, entry_price, entry_price + self.max_slip_ticks * .01, "Entry")
                    self.Debug(f'Entry -- Stop Limit Order Submitted -- {symbol}')
                else:
                    entryTicket = self.StopMarketOrder(symbol, shares, entry_price, "Entry") 
                    self.Debug(f'Entry -- Stop Market Order Submitted -- {symbol}')
                    
                
                self.Debug(f'Submitted Entry Order --  {symbol} @ {entry_price}')
        
        if self.debug_lvl >= 2:
            state_1 = [k for k,v in self.symbol_data.items() if v.state == 1]
            state_2 = [k for k,v in self.symbol_data.items() if v.state == 2]    #SETTING TO STATE 0 UPON ENTRY ORDER, so this likely wont show much / any.
            self.Debug(f'S1 : {len(state_1)} ---- S2 : {len(state_2)}')

                
    def ClearErrors(self):
        
        #Clear ALL barely invested... (Remainders)
        
        barely_invested = [kvp.Key for kvp in self.Portfolio if kvp.Value.Invested and kvp.Value.Quantity < 10] #Tiny remainder shares.... way below 1/4 of 10k
        for symbol in barely_invested:
            self.Liquidate(symbol)
            
        is_short = [kvp.Key for kvp in self.Portfolio if kvp.Value.Invested and not kvp.Value.IsLong]
        for symbol in is_short:
            self.Liquidate(symbol)
            
        not_invested = [kvp.Key for kvp in self.Portfolio if not kvp.Value.Invested]
        #Clear all NOT invested (IF theres a leftover non - entry order, cancel it.)
        for symbol in not_invested:
            openOrders = self.Transactions.GetOpenOrders(symbol)
            
            #CHECK if leftover targets / stops (NOT entries)
            for i in openOrders:
                if i.Tag != "Entry":
                    self.Transactions.CancelOpenOrders(symbol)
                
            
    def OnOrderEvent(self, orderEvent):
        """
        Manages OCO like behavior of STP vs Trail orders. 
        Entry orders are ignored -- returns out. 
        IF stop FILLED -- CANCEL Trail
        if Trail filled -- Cancel Stop
        
        ## CAN ALSO USE DIRECTION --> OrderDirection.Buy
        """
        
        #ONLY concerned with FILLED orders. Wait on partials, etc.
        if orderEvent.Status != OrderStatus.Filled:
            return
                
        order_symbol = orderEvent.Symbol

        oid = orderEvent.OrderId
        order = self.Transactions.GetOrderById(oid)
        shares = orderEvent.AbsoluteFillQuantity
        entry_price = orderEvent.FillPrice 
        
        #dir = orderEvent.Direction
        
        fill_price = orderEvent.FillPrice 
        
        
        
        # ------- IF an Entry order  ----------- SEND Stop and Target! (AND RETURN OUT) ------- #
        
        if order.Tag == "Entry":
            
            #RESET to state 0 upon entry : )
            self.symbol_data[order_symbol].state = 0
            
            self.Debug(f'Entry Filled -- {order_symbol} @ {fill_price} -- Submitting OCO Stop & Tgt')
            
            ## ------------------ STOP LOSS ----------------- ## 
            if not self.STOP_OFF:
                if self._test_entries:
                    self.stop_offset_ticks = 40                                     #TO MAKE SURE TARGETS ARE HIT IN TEST
                
                stopTicket = self.StopMarketOrder(order_symbol, -shares, entry_price - (self.stop_offset_ticks / 100), "Stop")
                
                self.stop_orders[order_symbol] = stopTicket #ADD to dict...
                
                if self.debug_lvl >= 1:
                    self.Debug(f'SL Submitted -- {order_symbol} @ {entry_price - (self.stop_offset_ticks / 100)}')
                
            ## ----------------- Profit Targets ------------------ ## 
            
            #tgtTicket = self.LimitOrder(symbol, 100, 221.05, "New SPY trade") #LIMIT AND MARKET HAVE SAME STRUCTURE FOR TAGS! #SAMPLE -- 
            
            #TO TEST THE RESETTING ABILITY of the OCO aspect.
            if self._test_entries:
                tgt = self.LimitOrder(order_symbol, -int(shares / 4), entry_price + .05, "TGT")
            else:  
                tgt1 = self.LimitOrder(order_symbol, -int(shares / 4), self.tgt_1, "TGT") 
                
            tgt2 = self.LimitOrder(order_symbol, -int(shares / 4), self.tgt_2, "TGT")
            tgt3 = self.LimitOrder(order_symbol, -int(shares / 4), self.tgt_3, "TGT")
            tgt4 = self.LimitOrder(order_symbol, -int(shares / 4), self.tgt_4, "TGT4")
            
            if self.debug_lvl >= 1:
                self.Debug(f'TP1 Submitted -- {order_symbol} @ {self.tgt_1}')
                self.Debug(f'TP2 Submitted -- {order_symbol} @ {self.tgt_2}')
                self.Debug(f'TP3 Submitted -- {order_symbol} @ {self.tgt_3}')
                self.Debug(f'TP4 Submitted -- {order_symbol} @ {self.tgt_4}')
                
            return
        
        ## -------- IF EXIT ORDER FILLED -------- ## 
        self.Debug(f'Exit Order Fill -- {order_symbol} @ {fill_price} ')
        
            
        ## IF a Target order filled ... (RESET stop quantity) --- IF stop off, ignore
        if order.Tag == "TGT" and not self.STOP_OFF:
            #self.Debug(f' -------------------------------- TGT HIT (Resetting Quantity for SL)')
            new_shares_held = self.Portfolio[order_symbol].Quantity
            
            updateSettings = UpdateOrderFields()                                
            updateSettings.Quantity = -new_shares_held
            
            # order.UpdateOrderQuantity() #Unsure of syntax for this... 
            
            #Subtract Manually... (NOT based on new_shares_held via pf object)
            #old = self.stop_orders[order_symbol].Quantity
            #new = old - shares
            
            self.stop_orders[order_symbol].Update(updateSettings)
            #updateSettings.StopPrice = curr_trail
            #response = self.TrailOrder.Update(updateSettings)
            self.Debug(f"Stop Quantity Updated to {new_shares_held}")
            return
        
        # IF a Stop or Final Target Filled (Cancel all remaining)
        if order.Tag in ["Stop", "TGT4"]:
            #self.Debug(f'{self.Portfolio[order_symbol].Quantity} remaining...')
            self.Debug(f'Stop / Tgt4 hit (NOW flat -- cancel any pending)')
            # Logging:
            self.Debug(f'Running OCO -- {order_symbol} (Cancelling Alt Exits.)')
            #COULD just CANCEL ALL orders here... ? 
    
            self.Transactions.CancelOpenOrders(order_symbol)
            return