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