Overall Statistics |
Total Orders 12 Average Win 0.10% Average Loss -1.99% Compounding Annual Return -44.095% Drawdown 2.500% Expectancy -0.475 Start Equity 100000 End Equity 98106.32 Net Profit -1.894% Sharpe Ratio -4.242 Sortino Ratio 0 Probabilistic Sharpe Ratio 1.580% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.05 Alpha -0.407 Beta -0.01 Annual Standard Deviation 0.096 Annual Variance 0.009 Information Ratio -2.323 Tracking Error 0.191 Treynor Ratio 42.003 Total Fees $193.68 Estimated Strategy Capacity $230000.00 Lowest Capacity Asset TCS VL8CJEL7BM91 Portfolio Turnover 3.45% |
# 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: if self.db >= 2: self.algo.Debug(f'{self.symbol} -- {curr} > {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: if self.db >= 2: self.algo.Debug(f'{self.symbol} -- {curr} < {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 # 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 ## 7.31.2024 -- added eodx class MeasuredRedCoyote(QCAlgorithm): def Initialize(self): self.SetStartDate(2024, 7, 20) #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) 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.extended_hours = False self.debug_lvl = 2 # Prints details to log (State counts, Universe, etc) # enable ETH -- via universe settings. # https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/universe-selection/universe-settings if self.extended_hours: self.universe_settings.extended_market_hours = True # ---------------- Declarations ------------ # 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) # schedule eod event. self.Schedule.On( self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 5), self.EODX ) def EODX(self): self.liquidate(tag="EOD Exit") def OnEndOfDay(self): data = self.CurrentSlice # NOTE -- this was NOT running before.... but now I'm adding EODX to it #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): data = data.Bars 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) _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 ## ------------- 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 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: 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 ------------------ ## 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 NOT ENTRY) ## -------- 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