Overall Statistics
Total Trades
20
Average Win
0%
Average Loss
-0.07%
Compounding Annual Return
-31.789%
Drawdown
0.700%
Expectancy
-1
Net Profit
-0.731%
Sharpe Ratio
-13.1
Probabilistic Sharpe Ratio
0%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0.02
Annual Variance
0
Information Ratio
-13.1
Tracking Error
0.02
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 

            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


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 = 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.

'''