| 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