| Overall Statistics |
|
Total Trades 20 Average Win 0% Average Loss -0.07% Compounding Annual Return -30.020% Drawdown 0.700% Expectancy -1 Net Profit -0.682% Sharpe Ratio -13.158 Probabilistic Sharpe Ratio 0% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0.019 Annual Variance 0 Information Ratio -13.158 Tracking Error 0.019 Treynor Ratio 0 Total Fees $20.00 Estimated Strategy Capacity $4700000.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:
# 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
# Think this is just a confusing way to do a simple thing.
# dist_to_ap = self.hh - ap
# n_risks_in_pos = int(dist_to_ap / self.Risk) #Round down
# ts = ap + n_risks_in_pos
# Far simpler, far cleaner.
new_stop = self.hh - self.Risk
# Safety, and adjust up to .02 below low.
old_stop = self.TrailStop if self.TrailStop is not None else 0
# 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
ISSUE -- is 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 = 3
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
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. '''