Overall Statistics |
Total Trades 18 Average Win 0% Average Loss -0.08% Compounding Annual Return -30.229% Drawdown 0.700% Expectancy -1 Net Profit -0.688% Sharpe Ratio -12.23 Probabilistic Sharpe Ratio 0% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0.021 Annual Variance 0 Information Ratio -12.23 Tracking Error 0.021 Treynor Ratio 0 Total Fees $18.00 Estimated Strategy Capacity $4200000.00 Lowest Capacity Asset PRSP RFOC46E8Y8O5 |
#region imports from AlgorithmImports import * #endregion import datetime from datetime import timedelta class Strategy: """ """ def __init__(self, symbol, algo): self.algo = algo self.symbol = symbol self.warmed_up = False baseCons = TradeBarConsolidator(timedelta(days=1)) self.algo.SubscriptionManager.AddConsolidator(self.symbol, baseCons) #Maybe this needs to be added BEFORE? baseCons.DataConsolidated += self.On_XM self.cons = baseCons # self.sma = SimpleMovingAverage(200) # self.algo.RegisterIndicator(self.symbol, self.sma, self.cons) self.Bars = RollingWindow[TradeBar](10) self.strat_invested = False self.StopLoss = None self.TrailStop = None self.Tgt1 = None self.Tgt2 = None self.hh = 0 self.EntryOrder = None self.hh = 0 self._DISABLE = False #Used to TURN OFF the IsAllReady aspect self._STALE = False # TRIGGER this to stop self._KILL_DATE = None ## ------- Warmup (Not strictly needed...) lb = 50 + 10 hist = self.algo.History([self.symbol], lb, Resolution.Daily).loc[self.symbol] for row in hist.itertuples(): bar = TradeBar(row.Index, self.symbol, row.open, row.high, row.low, row.close, row.volume) self.cons.Update(bar) #THIS is how to update consolidator! self.warmed_up = True self.algo.Debug(f' ------------- {self.symbol} -- Warmup Completed ----------- ') def On_XM(self, sender, bar): bartime = bar.EndTime symbol = str(bar.get_Symbol()) if self.algo.db_lvl >= 4: self.algo.Debug(f'New {self.symbol} Bar @ {self.algo.Time}') self.Bars.Add(bar) if self.IsAllReady: if self.algo.db_lvl >= 3: self.algo.Debug(f'{self.symbol} Close Levels: {[i.Close for i in self.Bars]}') # IF not 'Killed' yet -- triggered to be killed in <= 3 days -- enter per usual. if self._KILL_DATE is None: self.algo.Debug(f'Entry logic for {self.symbol}...') self.EntryLogic() self.TrailStopLoss() def EntryLogic(self): if not self.algo.Portfolio[self.symbol].Invested: # and self.EntryOrder is None: # Cancel old orders, if present, pending and unfilled if self.EntryOrder != None: # self.algo.Liquidate(self.symbol, "Cancel + Replace (Entry)") self.EntryOrder.Cancel("Cancel + Replace") # TODO: remember to RESET this to none again, when we flatten round trip. # Set a Limit Order to be good until noon order_properties = OrderProperties() order_properties.TimeInForce = TimeInForce.GoodTilDate(self.algo.Time + timedelta(days=3)) stop_price = self.Bars[1].High + .02 limit_price = stop_price + self.Risk * .05 #This is going to be pretty fucking huge... I think? self.entry_price = stop_price #Not sure we want this... really the REAL fill, more likely. self.EntryOrder = self.algo.StopLimitOrder(self.symbol, self.PositionSize, stop_price, limit_price, "LE - STPLMT", order_properties) # Finsih Testing TODO: # RUN this intraday -- on 1m def TrailStopLoss(self): ''' Trailing Stop: A) Every increase in price from Entry Stop of Risk$, increase stop by Risk$ B) Every End of Day, increase Exit Stop to (Low of Current Bar - $0.02) Whichever is higher ''' # THIS should run every 1m, roughly -- but will be called within OnData, from Main. -- OR keep in cons, tor un daily, per spec? Okay too. if not self.IsAllReady: return # IF flat -- ignore, reset the trail level if not self.algo.Portfolio[self.symbol].Invested: self.TrailStop = None self.hh = 0 return # IF not flat, we need to determine the HH, vs the entry price # avg_entry_price = self.algo.Portfolio[self.symbol].Average # Think this is avg fill price https://www.quantconnect.com/docs/v2/writing-algorithms/portfolio/holdings ap = self.EntryOrder.AverageFillPrice # Track a new high... h = self.Bars[0].High # Look for a new highest high -- if so, update the trail level. # self.algo.Debug(f'checking for new high {h} > {self.hh}') if h > self.hh: self.hh = h old_stop = self.TrailStop if self.TrailStop is not None else 0 # Old Version of TrailStop (When things were easy) # Far simpler, far cleaner. # new_stop = self.hh - self.Risk # # Adjust up for Low-.02 (Daily bars, Daily events, regardless of main res) # self.TrailStop = max(self.Bars[0].Low - .02, new_stop) # #CANT go lower, ever. (Safety) # self.TrailStop = max(old_stop, self.TrailStop) dist = self.hh - ap risks_from_entry = (dist // self.Risk) eff_risks = risks_from_entry - 1 new_stop = ap + eff_risks * self.Risk # Adjust up for Low-.02 (Daily bars, Daily events, regardless of main res) self.TrailStop = max(self.Bars[0].Low - .02, new_stop) #CANT go lower, ever. (Safety) self.TrailStop = max(old_stop, self.TrailStop) self.algo.Debug(f'Stop Loss Set to {self.TrailStop} vs avgPrice {ap} -- Prior Stop: {old_stop}') # IF Stop level st -- tracks price vs that to trigger exit. if self.TrailStop: # self.algo.Debug(f'TRAILSTOP Active -- Tracking: {self.algo.Portfolio[self.symbol].Price} < {self.TrailStop} ? ') if self.algo.Portfolio[self.symbol].Price < self.TrailStop: self.algo.Liquidate(self.symbol, "TrailStop -- OCA") #Also cancels ALL self.hh = 0 self.TrailStop = None self.EntryOrder = None def KillConsolidator(self): self.algo.SubscriptionManager.RemoveConsolidator(self.symbol, self.cons) @property def Risk(self): if self.IsAllReady: return self.Bars[1].High - self.Bars[1].Low @property def IsAllReady(self): return self.warmed_up and self.Bars.IsReady @property def PositionSize(self): '''Returns SHARES to buy per symbol''' pfv = self.algo.Portfolio.TotalPortfolioValue mr = self.algo.Portfolio.MarginRemaining usd_value = pfv / len(self.algo.Strategies) if usd_value > mr: usd_value = mr return int( usd_value / self.algo.Portfolio[self.symbol].Price) @property def WeirdPosSize(self): # This makes virtually no sense -- and had some bugs in it vs impl -- but translated it per spec JIC (with fixes, in that it wonnt reject or do anything stupid or do nothing) pfv = self.algo.Portfolio.TotalPortfolioValue mr = self.algo.Portfolio.MarginRemaining # Margin Remaining usd_value = pfv / len(self.algo.Strategies) # Equal Weight max_risk = .01 numShares = max_risk * pfv / self.Risk stop_price = self.Bars[1].High + .02 limit_price = stop_price + self.Risk * .05 tradeSize = numShares * limit_price / pfv maxPortSize = .2 usd_value = np.min([tradeSize * pfv, maxPortSize * pfv, usd_value, mr]) return int( usd_value / self.algo.Portfolio[self.symbol].Price) # Need to handle the OnOrderEvents -- for Stop, Tgt1, Tgt2 # the rest -- we will handle manually for trail stop -- just slowly increase a value until it triggers below it, and then exit. Likely want to use 1m event there.
# region imports from AlgorithmImports import * from Consolidators import Strategy # endregion from enum import Enum ''' TSK _ Universe Technical LO Author: ZO Owner: TSK V1.0 -- built out basics, benchmarked universe for speed. V1.5 -- completed universe, and tested it out. V2.0 -- completed strategy logic -- ran into issues with TIF, and removal from algo. Resolved in 2.5 with kill date V2.5 -- Added KILL_DATE hidden class method -- for managing a partially killed / scheduled killed strategy, destructed when past it's kill date. Added strat_res -- as minute is better suited overall most likely, tho not needed. V3.0 -- Fixed trailstop, added crazy F1 Filter -- Should be all done / tested V3.5 -- Fixed Trailstop again. V4.0 -- Added Cancel + Replace (removed avoiding it + tested it) ISSUE -- the time in force. If we ccan remove that, we're golden. We WANT to remove things from the universe -- we need to. could flag them as 'dead', i.e. NOT ready... but that's problematic in it's own way. solutionw as scheduling a kill date, to 'remove' and disable new events when not null, and kill if strat instance past that date. ''' # This is whats creating the QC Bug... (when we set it to min, and thus rely on min data for benchmark resolution...) Doesnt need minute data, but should run regardless. class TrailRes(Enum): Day = 0 Min = 1 class MeasuredBrownTapir(QCAlgorithm): filt_1_pct = .15 filt_2_pct = .05 strat_res = Resolution.Minute # or .Daily, .Hour trail_style = TrailRes.Day #This can be used to run the Trailstop intraday, if desired. I don't think its needed -- but who knows. db_lvl = 2 def Initialize(self): self.SetStartDate(2021, 6, 12) # Set Start Date self.SetEndDate(2021, 6, 19) # Timing a week, fuck a month. self.SetCash(100000) # Set Strategy Cash self.bm = self.AddEquity("SPY", self.strat_res).Symbol self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # ADD slippage model here too -- this is ONLY fees, NO slippage. self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelection) self._univ = True self.Strategies = {} # Daily univ sel self.Schedule.On(self.DateRules.EveryDay(self.bm), self.TimeRules.BeforeMarketClose(self.bm, 1), self.EOD) # self.Debug(f'{self.Time} < {self.Time + timedelta(days=1)} ? {self.Time < self.Time + timedelta(days=1)}') #Time comparisons work. # Weekly Univ Sel # self.Schedule.On(self.DateRules.Every(DayOfWeek.Friday), # self.TimeRules.BeforeMarketClose(self.bm, 1), # self.EOW) # Monthly univ sel # self.Schedule.On(self.DateRules.MonthStart(self.bm), # self.TimeRules.BeforeMarketClose(self.bm, 1), # self.EOM) def EOM(self): self._univ = True def EOW(self): self._univ = True def EOD(self): # Try logging the symbols each EOD self.Debug(f'EOD ') # self.Debug(f'{[str(i.Value) for i in self.Securities.Keys]}') self._univ = True def CoarseSelection(self, coarse): if not self._univ: return Universe.Unchanged # hist = self.History([c.Symbol for c in coarse], 250, Resolution.Daily) all_symbols = [c.Symbol for c in coarse] # Baseline Filters (Applied to all, without hist data) above_10 = [c for c in coarse if c.AdjustedPrice > 10] adv_above_x = [c for c in above_10 if c.DollarVolume > 5 * 1000 * 1000] top_1500 = sorted(adv_above_x, key=lambda x: x.DollarVolume, reverse=True)[500:2000] #False is Ascending -- WE want True, Descending (First = Largest) # top_1500 = adv_above_x #Here to ignore the top_1500 # Done with Broad Filters (top level stuff -- no data needed) --------------- Try to remove as many as possible before this point. chosen = top_1500 chosen_symbols = [c.Symbol for c in top_1500] self.Debug(f'Chosen Len (End of Top lvl filter): {len(chosen_symbols)}') # ------------- Begin Technical Universe (Requires data request) ----------------------- # hist = self.History(chosen_symbols, 250, Resolution.Daily) outer = [] for symbol in chosen_symbols: try: df = hist.loc[symbol] # in_pipe = self.TechnicalPipeline(df) #Want to SEE these errors, at this stage. # if in_pipe: outer.append(symbol) except: self.Debug(f'{symbol} failed in universe -- no data.') continue in_pipe = self.TechnicalPipeline(df) if in_pipe: outer.append(symbol) # Remove, when tech universe built # outer = chosen # top_50 = sorted(outer, key=lambda x: x.DollarVolume, reverse=True)[:50] self.Debug(f'Final Len (End Universe): {len(outer)}') self._univ = False return outer # return [c.Symbol for c in top_50] def TechnicalPipeline(self, df): ''' 1) Stocks with x% gain in 20 days, calculated by min. close of last 20 days [psuedocode: C/MinC20>1.x] Any stocks with valid signal for the last 30 days is included in the universe 2) Stocks within x% of last 30 days max. close [psuedocode: C/MaxC30 > (1-x)] 3 Done 4) Stocks with minimal of 1% movement range in last 5 days, to filter out takeover targets [psuedocode: MinH5/MinL5 -1>0.01] 5 Done 6) Stocks with close price > SMA200 [psuedocode: C>AvgC200] 7) Stocks with previous bar volume < SMA10 volume [psuedocode: V1 < AvgV10] 8) Stock with Low <= Previous Low and High <= Previous High [psuedocode: L<=L1 and H<=H1] 9) Stock WITHOUT Close less than previous Close AND Volume > SMA10 volume [psuedocode: NOT(C<C1 and V>AvgV10)] ''' # Returns TRUE if passes, else False if df.shape[0] <= 200: return False # Returns TRUE if passes, else returns out False (each filter returns out -- to end calc if anything doesn't meet it.) # filt_1 = df.close.iloc[-1] / df.close.iloc[-20:].min() > (1 + self.filt_1_pct) # filt_1 = df.close.pct_change(20).tail(30).max() > (self.filt_1_pct) # filt_2 = max(df.close.tail(30).cummax() / df.close.tail(30).cummin()) > (1 + self.filt_1_pct) # if not filt_1: return False # TSK Added -- I'm 100% sure this is FAR above any real pct return calc in this period -- but if happy great. for i in range(30): calc = df.close.iloc[-1-i] / df.close.iloc[-30-i:].min() if calc > (1 + self.filt_1_pct): return False # if self.db_lvl >= 4: self.Debug(f'F1: {df.close.pct_change(20).tail(30).max() > (self.filt_1_pct)} -- {df.close.pct_change(20).tail(30).max()} > {(self.filt_1_pct)}') # # Alt Filt 2 -- don't think we want this. # # THIS is always going to be 1 if we take the max ( closes / max of closes) -- bc it will be highest close / highest close. So we take second value. # row = df.close.tail(30) / df.close.tail(30).max() # f2_ref = row.sort_values().iloc[-2] # filt_2 = f2_ref > (1 - self.filt_2_pct) #If desired -- uncomment this, and 2 above it. to run filt_2 = df.close.iloc[-1] / df.close.iloc[-30:].max() > ( 1 - self.filt_2_pct) if not filt_2: return False filt_4 = df.high.iloc[-5:].max() / df.low.iloc[-5:].min() - 1 > .01 if not filt_4: return False filt_6 = df.close.iloc[-1] > df.close.rolling(200).mean().iloc[-1] if not filt_6: return False filt_7 = df.volume.iloc[-1] < df.volume.rolling(10).mean().iloc[-1] if not filt_7: return False filt_8 = df.low.iloc[-1] <= df.low.iloc[-2] and df.high.iloc[-1] <= df.high.iloc[-2] if not filt_8: return False # WTF? why is this NOT, just invert it. filt_9 = df.close.iloc[-1] >= df.close.iloc[-2] and df.volume.iloc[-1] <= df.volume.rolling(10).mean().iloc[-1] if not filt_9: return False return True def OnSecuritiesChanged(self, changes): # Complicated as fuck now -- but should work by adding a kill date when 'removed' from universe. added = 0 for security in changes.AddedSecurities: # SET slippage model to VolumeShare version -- besti n class likely. security.SetSlippageModel(VolumeShareSlippageModel()) added += 1 symbol = security.Symbol if symbol not in self.Strategies: if str(symbol) == "SPY": continue if 'IEX' in str(symbol): continue # Also behaving strangely. try: # self.Debug(f'Type (of symbol) -- {symbol} -- {type(symbol)}') self.Strategies[symbol] = Strategy(symbol, self) except: self.Debug(f'Failed to add {symbol} to Strategies') else: #OTHERWISE -- if it IS present, and HAS a kill date, DISABLE it. if self.Strategies[symbol]._KILL_DATE != None: self.Strategies[symbol]._KILL_DATE = None self.Debug(f'{self.Time} -- {added} securities added.') # CANNOT do this -- otherwise old orders may fill AFTER they aren't present, and data is stale. **** # ALTS -- to POP it (as the cons is dead) and RE Add it! (IF its present). Fucking annoying. rmd = 0 for security in changes.RemovedSecurities: rmd += 1 symbol = security.Symbol # tst = self.Strategies.get(security.Symbol, None) if symbol in self.Strategies: # MAYBE we can schedule it to pop, kill in 3 days? idk how tho. # Save the date to a dict, today + timedelta(3) ... and loop through to see if anything needs to be killed? self.Strategies[symbol]._KILL_DATE = self.Time + timedelta(days = 3) self.Debug(f'{self.Time} -- {rmd} Securities Removed (Staged their kill date for {self.Time + timedelta(days=3)})') self.Debug(f'Symbol List (Strategies Dict) -- {[str(i) for i in self.Strategies.keys()]}') # NOW check if there's any to TRULY remove (after the 3 days is up) self.CheckForKills() def CheckForKills(self): # Begin with finding all strategies with kill dates -- to compare their kill date to now. # strats_with_kill_dates = [symbol for symbol, inst in self.Strategies.items() if inst._KILL_DATE != None] to_kill = [] for symbol, inst in self.Strategies.items(): if inst._KILL_DATE != None: if self.Time >= inst._KILL_DATE: inst.KillConsolidator() self.Liquidate(symbol, 'Cancel Any Pending (Killed)') to_kill.append(symbol) for kill in to_kill: if kill in self.Strategies: self.Strategies.pop(kill) self.Debug(f'Killed Symbols: {[str(i) for i in to_kill]}') def OnData(self, data: Slice): if self.trail_style == TrailRes.Min: # To run trailstop intraday for symbol, inst in self.Strategies.items(): if inst.IsAllReady: inst.TrailStopLoss() def OnOrderEvent(self, orderEvent): """ """ #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 ## ---------------- Upon Entry Fill -------------------- ## # s1_entry = order.Tag.startswith("S1 Entry") #Not doing entry for s1 like this, all markets (setholdings / liq) entry = order.Tag.startswith("LE - STPLMT") stop = order.Tag.startswith("SL") tgt1 = order.Tag.startswith("TGT1") tgt2 = order.Tag.startswith("TGT2") tstp = order.Tag.startswith("TrailStop -- OCA") # This should be a non event now -- was here for testing of TIF issues. # self.Debug(f'Symbol: {order_symbol} -- {order_symbol in self.Strategies} ? ') # --> {[str(i.Value) for i in self.Securities.Keys]}') inst = self.Strategies.get(order_symbol, None) if not inst: self.Liquidate(order_symbol, "ISSUE! This should not happen... should not be possible.") self.Debug(f'TIF Issue -- bc popping from Strategies when removed. --> {order_symbol} tag: {order.Tag}, Type --> {type(order_symbol)} ') return if entry: # ------------------ Stop Loss -------------------------- # stp = inst.Bars[0].Low - .02 ticket = self.StopMarketOrder(order_symbol, -1 * shares, stp, "SL") inst.StopLoss = ticket # Save for later -- to update share count here. # ------------------ Target ----------------------------- # tgt_1 = entry_price + inst.Risk * 2 tgt_2 = entry_price + inst.Risk * 3 q1 = -1 * int(shares // 2) q2 = -1 * int(shares + q1) # Add a negative to subtract, get remainder inst.Tgt1 = self.LimitOrder(order_symbol, q1, tgt_1, "TGT1") inst.Tgt2 = self.LimitOrder(order_symbol, q2, tgt_2, "TGT2") # self.StopLoss = None # self.TrailStop = None # self.Tgt1 = None # self.Tgt2 = None return if tgt1: # ADJUST qty of stop loss... stop_ticket = inst.StopLoss # get remaining tgt2 size (abs) tgt_2_qty = abs(inst.Tgt2.Quantity) #Lookup the order ticket object tags stop_ticket.UpdateQuantity(tgt_2_qty, "Adjust QTY -- Tgt1 filled.") if stop or tgt2 or tstp: self.Liquidate(order_symbol, "Exit -- OCA") inst.EntryOrder = None # RESSET !
#region imports from AlgorithmImports import * #endregion ''' https://docs.google.com/document/d/17V1CgpEl3V3KCRky14IVUiag4pgwRLhlbbiTXVnX_Uw/edit Equity Only Daily Timeframe for Universe Selection Minute Resolution for Trade Management Long Only Universe Selection: All US Stocks 1) Stocks with x% gain in 20 days, calculated by min. close of last 20 days [psuedocode: C/MinC20>1.x] Any stocks with valid signal for the last 30 days is included in the universe 2) Stocks within x% of last 30 days max. close [psuedocode: C/MaxC30 > (1-x)] 3) Stocks with price > $10 [psuedocode: C>12] 4) Stocks with minimal of 1% movement range in last 5 days, to filter out takeover targets [psuedocode: MinH5/MinL5 -1>0.01] 5) Stocks with minimal dollarvolume of $5million [psuedocode: AvgC50*AvgV50>5000000] 6) Stocks with close price > SMA200 [psuedocode: C>AvgC200] 7) Stocks with previous bar volume < SMA10 volume [psuedocode: V1 < AvgV10] 8) Stock with Low <= Previous Low and High <= Previous High [psuedocode: L<=L1 and H<=H1] 9) Stock WITHOUT Close less than previous Close AND Volume > SMA10 volume [psuedocode: NOT(C<C1 and V>AvgV10)] Trading Rules: Long Only Each Stop Limit order will be active on Open next day (T+1). Each Stop Limit order will be active for 3 days only (T+3). Cancel on End of 3rd day if not filled. All OHLC will be from PREVIOUS bar. Risk$: High-Low Slippage: Risk$ * 0.05 Entry Stop = High + $0.02 Entry Stop Limit = Stop + Slippage Exit Stop = Low - $0.02 Take Profit Limit Orders: 1) 25% of position: Entry Stop + Risk$ *2 2) 50% of position: Entry Stop + Risk$ *3 Trailing Stop: A) Every increase in price from Entry Stop of Risk$, increase stop by Risk$ (Is this a new stop, or the original stop?) -- I wrote it to just trail by the Risk amount, roughly. Otherwise, need more logic here. B) Every End of Day, increase Exit Stop to (Low of Current Bar - $0.02) Whichever is higher Trade Position [MaxRisk%] Maximum Risk% of Portfolio = 1% [NumOfShares] = MaxRisk% * Portfolio$ / Risk$ [TradeSize%] = NumOfShares * Entry Stop Limit / Portfolio [MaxPortSize%] Maximum % of Portfolio per trade position = 20% Trade Size = [TradeSize%] or [MaxPortSize%], whichever is lower Portfolio Management 1) Take all valid open orders until open positions are 80% of Buying Power [(Portfolio + 100% Margin) * 80%] 2) Cancel all triggered open orders when at 80% of Buying Power BUT keep untriggered open orders live. 3) In the event one opened order is closed, another open order can be triggered and entered. '''