Here is my code for an updated Halt Estimate Indicator for Tier 2 Stocks. Thanks to
@Wannaspeed for the original solution! Btw I am only a novice so my coding may not be following best practices, but I spent a lot of time trying to follow the LULD Rules
here and
here to my ability & it seems to work pretty well. Please improve/fix anything that needs it.
This has been updated for the new February 2020 Limit Up Limit Down halt rules. They eliminated the doubling of the bands from 9:30 to 9:45, and also changed the doubling rules slightly from 3:35-4pm. I tried to follow all of the halt rules, and it also works during the 5 minutes after a halt.
It is somewhat future proof in case they change the rules again. The percentages, hours, and price ranges are adjustable near the top of the code. So, for example, there is now no doubling in the first 15min of opening, but if they change that back, you can just type in the new changeover time for "normal_Hours_Start". The "930" could be changed back to "945" and the new percentages for the opening period would need to be changed below. You can also update price boundaries (the changeover price for what they consider a low, mid, or high priced stock) if that changes.
Because of limits of thinkscript data, there are limits to accuracy of the estimated halt price. Here are some of the major obstacles I encountered:
(1) Determining the average price for transactions over the last 5 minutes
- We don't (to my knowledge) have access to the real average price a stock traded at in a given 1-minute bar, so I estimate it as (high+low)/2, the midpoint of the bar. For example, if a bar has 95% of its transactions near the low, and only 5% near the high, the estimate will be off.
(2) 30 Seconds Reference Price rule: "The reference price will only be updated if the new reference price is at least 1% away in either direction from the current. Each new Reference Price shall remain in effect for at least 30 seconds."
- We only have access to minute chart data, so reference prices that are held for 30 seconds are impossible to get exactly right. We could be updating the reference when we shouldn't or vice-versa. For example if a new bar starts with a huge move, we don't know if we are within the 30 second hold. If we are, the current price band should stay put. If not, it should update based on the current bar's movement (in addition to the previous 5 min of bars).
(3) Previous 5 Minutes
- The definition of previous 5 minutes seems to be a live rolling 5 minute period. Since we can access only full minute bar data, this won't be totally accurate either.
Given these limitations, for each bar there are 2 estimated halt prices calculated for the upper band and 2 for the lower band. One uses the midpoint of the current bar in the calculation, and the more conservative one assumes the current bar's price spent more time inward, closer to the reference. Other considerations are also calculated.
There are 3 display modes:
Range - Shaded area between the two calculated halt estimates of each band
Line - A line of the conservative halt estimate for each band
Area - Shades the entire area above the upper conservative band, and below the lower conservative band
I personally prefer Range. With it I have a better idea of where a halt is likely to occur.
Remember this indicator is just an estimate. A halt may (and will) sometimes occur before the price reaches a band, or it may not occur even though price has reached a band. It seems a halt also requires the price to touch the real band for 15 seconds for a halt to occur, so that may cause some misses too.
Btw the bubbles showing actual halts are a separate script I created
here. It only shows the length of a likely halt after a new bar after the fact. I like using them both together.
1-MINUTE CHART ONLY
TIER 2 STOCKS ONLY. Not accurate on tier 1 (S&P 500, Russell 1000 stocks)
Range
Line
Area
Code:
# LULD Halt Estimate Indicator 1.01 by Lenny
# - various code concepts from multiple posts at Usethinkscript.com
# v1.0 5/2/20 -original
# v1.01 6/9/20 -minor fix in situation where there is no lower band
# 1-MINUTE CHART ONLY
# TIER 2 STOCKS ONLY. Not accurate on tier 1 (S&P 500, Russell 1000 stocks)
# Works with 3 (or less) stock price ranges (Low, Mid, High)
# Works with 3 (or less) time periods for each price range (Open, Normal, Close)
# (If opening and/or closing hours are not used, enter same time and boundries as normal hours)
declare hide_on_daily;
#================= INPUTS =================#
input display_Mode = {default Range, Line, Area};
input show_today_only = yes;
input show_labels = yes;
# Hour ranges (enter duplicate of normal hours if no opening or closing hours difference)
input open_Time = 0930;
input normal_Hours_Start = 0930;
input normal_Hours_End = 1535;
input close_Time = 1600;
# Adjust bands very slightly inward for margin of error
# (by default 1% of the difference between ref price and band)
def Estimate_Adjustment_Percent = 1; # 1% recommended
#========== BOUNDRIES AND LIMITS ==========#
# Price range boundries (Low/Mid, Mid/High priced stocks)
def LowPriceMax = .75;
def MidPriceMax = 3.00;
# Low-priced stocks percentage / dollar(alt) limit bands
def LowPriceOpenPercent = 75; # Will choose lower of percent and dollar value
def LowPriceOpenAlt = .15;
def LowPriceNormalPercent = 75; # Will choose lower of percent and dollar value
def LowPriceNormalAlt = .15;
def LowPriceClosePercentUp = 150; # upper and lower have different rules for low priced stocks (currently)
def LowPriceCloseAltUp = .30; # Will choose lower of percent and dollar value
def LowPriceClosePercentDown = 0; # 0 = no limit, no lower band needed (currently)
def LowPriceCloseAltDown = 0; # 0 = no limit, no lower band needed (currently)
# Mid-priced stocks percentage limit bands
def MidPriceOpenPercent = 20;
def MidPriceNormalPercent = 20;
def MidPriceClosePercent = 40;
# High-priced stocks percentage limit bands
def HighPriceOpenPercent = 10;
def HighPriceNormalPercent = 10;
def HighPriceClosePercent = 10;
# Minimum reference price change (%)
def RefMinChangePercent = 1; # must move 1% to move bands (LULD rules)
#================= DEFs =====================#
def Active = SecondsFromTime(open_Time) >= 0 and SecondsTillTime(close_Time) - 1 >= 0;
def Today = GetDay() == GetLastDay();
def isMinuteAgg = (GetAggregationPeriod() == AggregationPeriod.MIN);
def Qualifies = if show_today_only then Today and Active and isMinuteAgg else Active and isMinuteAgg;
def FirstBar = Active == 1 and Active[1] == 0;
def CurrentMinute = RoundDown(SecondsFromTime(open_Time) / 60, 0) + 1;
def MinFromPrevBar = CurrentMinute - CurrentMinute[1];
def PriorClose = close(period = AggregationPeriod.DAY)[1];
def lastBarClose = close[1];
def ExtendBar1 = FirstBar[1] and IsNaN(close) and !IsNaN(close[1]); #1st bar bands will extend to right so 1st bar is visible live
def LowPriced = PriorClose < LowPriceMax;
def MidPriced = PriorClose >= LowPriceMax and PriorClose <= MidPriceMax;
def HighPriced = PriorClose > MidPriceMax;
def OpeningHours = SecondsFromTime(open_Time) >= 0 and SecondsTillTime(normal_Hours_Start) > 0;
def NormalHours = SecondsFromTime(normal_Hours_Start) >= 0 and SecondsTillTime(normal_Hours_End) > 0;
def ClosingHours = SecondsFromTime(normal_Hours_End) >= 0 and SecondsTillTime(close_Time) > 0;
#======= CALCULATE REFERENCE ESTIMATES =======#
def vol = volume;
def histAvgPrice = hl2;
def histAvgPriceVol = histAvgPrice * vol;
def Bar0AvgPriceUp = (open + low) / 2; # Bar0 = current bar
def Bar0AvgPriceVolUp = Bar0AvgPriceUp * vol;
def Bar0AvgPriceLw = (open + high) / 2;
def Bar0AvgPriceVolLw = Bar0AvgPriceLw * vol;
def Bar0AvgPriceHL2 = hl2;
def Bar0AvgPriceHL2Vol = Bar0AvgPriceHL2 * vol;
def Bar1AvgPriceVol = histAvgPriceVol[1];
def Bar2AvgPriceVol = histAvgPriceVol[2];
def Bar3AvgPriceVol = histAvgPriceVol[3];
def Bar4AvgPriceVol = histAvgPriceVol[4];
def Bar5AvgPriceVol = histAvgPriceVol[5];
def Bar0Within5Min = 1;
def Bar1Within5Min = CurrentMinute[1] >= CurrentMinute - 4 and CurrentMinute[1] > 0;
def Bar2Within5Min = CurrentMinute[2] >= CurrentMinute - 5 and CurrentMinute[2] > 0;
def Bar3Within5Min = CurrentMinute[3] >= CurrentMinute - 5 and CurrentMinute[3] > 0;
def Bar4Within5Min = CurrentMinute[4] >= CurrentMinute - 5 and CurrentMinute[4] > 0;
def Bar5Within5Min = CurrentMinute[5] >= CurrentMinute - 5 and CurrentMinute[5] > 0;
def totalAvgPriceVolUp =
(Bar0AvgPriceVolUp * Bar0Within5Min) + (Bar1AvgPriceVol * Bar1Within5Min) + (Bar2AvgPriceVol * Bar2Within5Min) +
(Bar3AvgPriceVol * Bar3Within5Min) + (Bar4AvgPriceVol * Bar4Within5Min) + (Bar5AvgPriceVol * Bar5Within5Min);
def totalAvgPriceVolLw =
(Bar0AvgPriceVolLw * Bar0Within5Min) + (Bar1AvgPriceVol * Bar1Within5Min) + (Bar2AvgPriceVol * Bar2Within5Min) +
(Bar3AvgPriceVol * Bar3Within5Min) + (Bar4AvgPriceVol * Bar4Within5Min) + (Bar5AvgPriceVol * Bar5Within5Min);
def totalAvgPriceVolHL2 =
(Bar0AvgPriceHL2Vol * Bar0Within5Min) + (Bar1AvgPriceVol * Bar1Within5Min) + (Bar2AvgPriceVol * Bar2Within5Min) +
(Bar3AvgPriceVol * Bar3Within5Min) + (Bar4AvgPriceVol * Bar4Within5Min) + (Bar5AvgPriceVol * Bar5Within5Min);
def totalVol =
(vol * Bar0Within5Min) + (vol[1] * Bar1Within5Min) + (vol[2] * Bar2Within5Min) +
(vol[3] * Bar3Within5Min) + (vol[4] * Bar4Within5Min) + (vol[5] * Bar5Within5Min);
def vwapUp = totalAvgPriceVolUp / totalVol;
def vwapLw = totalAvgPriceVolLw / totalVol;
def vwapHL2 = totalAvgPriceVolHL2 / totalVol;
#Reference must move at least 1% or previous ref constinues
def RefMinIncDecimal = (RefMinChangePercent *.01) + 1;
def RefMinDecDecimal = 1 - (RefMinChangePercent *.01);
def RefPriceUp =
if (FirstBar or MinFromPrevBar >= 5) then Bar0AvgPriceUp else
if (vwapUp >= RefPriceUp[1] * RefMinIncDecimal) or (vwapUp <= RefPriceUp[1] * RefMinDecDecimal) then vwapUp else RefPriceUp[1];
def RefPriceLw =
if (FirstBar or MinFromPrevBar >= 5) then Bar0AvgPriceLw else
if (vwapLw >= RefPriceLw[1] * RefMinIncDecimal) or (vwapLw <= RefPriceLw[1] * RefMinDecDecimal) then vwapLw else RefPriceLw[1];
def RefPriceHL2 =
if (FirstBar or MinFromPrevBar >= 5) then Bar0AvgPriceHL2 else
if (vwapHL2 >= RefPriceHL2[1] * RefMinIncDecimal) or (vwapHL2 <= RefPriceHL2[1] * RefMinDecDecimal) then vwapHL2 else RefPriceHL2[1];
#============= CALCULATE BANDS ==============#
def UpperBand1 =
if LowPriced and OpeningHours then RefPriceUp + Min(RefPriceUp * LowPriceOpenPercent * .01 , LowPriceOpenAlt) else
if LowPriced and NormalHours then RefPriceUp + Min(RefPriceUp * LowPriceNormalPercent * .01 , LowPriceNormalAlt) else
if LowPriced and ClosingHours then RefPriceUp + Min(RefPriceUp * LowPriceClosePercentUp * .01 , LowPriceCloseAltUp) else
if MidPriced and OpeningHours then RefPriceUp + (RefPriceUp * MidPriceOpenPercent * .01) else
if MidPriced and NormalHours then RefPriceUp + (RefPriceUp * MidPriceNormalPercent * .01) else
if MidPriced and ClosingHours then RefPriceUp + (RefPriceUp * MidPriceClosePercent * .01) else
if HighPriced and OpeningHours then RefPriceUp + (RefPriceUp * HighPriceOpenPercent * .01) else
if HighPriced and NormalHours then RefPriceUp + (RefPriceUp * HighPriceNormalPercent * .01) else
if HighPriced and ClosingHours then RefPriceUp + (RefPriceUp * HighPriceClosePercent * .01) else Double.NaN;
def UpperBand2 =
if LowPriced and OpeningHours then RefPriceHL2 + Min(RefPriceHL2 * LowPriceOpenPercent * .01 , LowPriceOpenAlt) else
if LowPriced and NormalHours then RefPriceHL2 + Min(RefPriceHL2 * LowPriceNormalPercent * .01 , LowPriceNormalAlt) else
if LowPriced and ClosingHours then RefPriceHL2 + Min(RefPriceHL2 * LowPriceClosePercentUp * .01 , LowPriceCloseAltUp) else
if MidPriced and OpeningHours then RefPriceHL2 + (RefPriceHL2 * MidPriceOpenPercent * .01) else
if MidPriced and NormalHours then RefPriceHL2 + (RefPriceHL2 * MidPriceNormalPercent * .01) else
if MidPriced and ClosingHours then RefPriceHL2 + (RefPriceHL2 * MidPriceClosePercent * .01) else
if HighPriced and OpeningHours then RefPriceHL2 + (RefPriceHL2 * HighPriceOpenPercent * .01) else
if HighPriced and NormalHours then RefPriceHL2 + (RefPriceHL2 * HighPriceNormalPercent * .01) else
if HighPriced and ClosingHours then RefPriceHL2 + (RefPriceHL2 * HighPriceClosePercent * .01) else Double.NaN;
def LowerBand1 =
if LowPriced and OpeningHours then RefPriceLw - Min(RefPriceLw * LowPriceOpenPercent * .01 , LowPriceOpenAlt) else
if LowPriced and NormalHours then RefPriceLw - Min(RefPriceLw * LowPriceNormalPercent * .01 , LowPriceNormalAlt) else
if LowPriced and ClosingHours and LowPriceClosePercentDown == 0 and LowPriceCloseAltDown == 0 then 0 else
if LowPriced and ClosingHours and (LowPriceClosePercentDown == 0 or LowPriceCloseAltDown == 0) then
RefPriceLw - Max(RefPriceLw * LowPriceClosePercentDown * .01 , LowPriceCloseAltDown) else
if LowPriced and ClosingHours then RefPriceLw - Min(RefPriceLw * LowPriceClosePercentDown * .01 , LowPriceCloseAltDown) else
if MidPriced and OpeningHours then RefPriceLw - (RefPriceLw * MidPriceOpenPercent * .01) else
if MidPriced and NormalHours then RefPriceLw - (RefPriceLw * MidPriceNormalPercent * .01) else
if MidPriced and ClosingHours then RefPriceLw - (RefPriceLw * MidPriceClosePercent * .01) else
if HighPriced and OpeningHours then RefPriceLw - (RefPriceLw * HighPriceOpenPercent * .01) else
if HighPriced and NormalHours then RefPriceLw - (RefPriceLw * HighPriceNormalPercent * .01) else
if HighPriced and ClosingHours then RefPriceLw - (RefPriceLw * HighPriceClosePercent * .01) else Double.NaN;
def LowerBand2 =
if LowPriced and OpeningHours then RefPriceHL2 - Min(RefPriceHL2 * LowPriceOpenPercent * .01 , LowPriceOpenAlt) else
if LowPriced and NormalHours then RefPriceHL2 - Min(RefPriceHL2 * LowPriceNormalPercent * .01 , LowPriceNormalAlt) else
if LowPriced and ClosingHours and LowPriceClosePercentDown == 0 and LowPriceCloseAltDown == 0 then 0 else
if LowPriced and ClosingHours and (LowPriceClosePercentDown == 0 or LowPriceCloseAltDown == 0) then
RefPriceHL2 - Max(RefPriceHL2 * LowPriceClosePercentDown * .01 , LowPriceCloseAltDown) else
if LowPriced and ClosingHours then RefPriceHL2 - Min(RefPriceHL2 * LowPriceClosePercentDown * .01 , LowPriceCloseAltDown) else
if MidPriced and OpeningHours then RefPriceHL2 - (RefPriceHL2 * MidPriceOpenPercent * .01) else
if MidPriced and NormalHours then RefPriceHL2 - (RefPriceHL2 * MidPriceNormalPercent * .01) else
if MidPriced and ClosingHours then RefPriceHL2 - (RefPriceHL2 * MidPriceClosePercent * .01) else
if HighPriced and OpeningHours then RefPriceHL2 - (RefPriceHL2 * HighPriceOpenPercent * .01) else
if HighPriced and NormalHours then RefPriceHL2 - (RefPriceHL2 * HighPriceNormalPercent * .01) else
if HighPriced and ClosingHours then RefPriceHL2 - (RefPriceHL2 * HighPriceClosePercent * .01) else Double.NaN;
# Adjust bands very slightly inward (by default 1% of the difference between ref price and band)
def EstimateAdj = Estimate_Adjustment_Percent * .01;
def adjustedUpper1 = if UpperBand1 <= UpperBand2 then UpperBand1 - ((UpperBand1 - RefPriceUp) * EstimateAdj) else UpperBand1;
def adjustedUpper2 = if UpperBand2 < UpperBand1 then UpperBand2 - ((UpperBand2 - RefPriceHL2) * EstimateAdj) else UpperBand2;
def adjustedLower1 = if LowerBand1 >= LowerBand2 and LowerBand1 != 0 then LowerBand1 + ((RefPriceLw - LowerBand1) * EstimateAdj) else LowerBand1;
def adjustedLower2 = if LowerBand2 > LowerBand1 and LowerBand2 != 0 then LowerBand2 + ((RefPriceHL2 - LowerBand2) * EstimateAdj) else LowerBand2;
# Adjust a band when prior bar closed at likely true band limit, but 15 seconds requirement likely carries band over to current bar
def carryOverBand = high == lastBarClose and low == lastBarClose;
def FinalUpper1 = if !FirstBar then if lastBarClose >= adjustedUpper1[1] and carryOverBand and MinFromPrevBar == 1
then lastBarClose else adjustedUpper1 else adjustedUpper1;
def FinalUpper2 = if !FirstBar then if lastBarClose >= adjustedUpper2[1] and carryOverBand and MinFromPrevBar == 1
then lastBarClose else Max(adjustedUpper1 , adjustedUpper2) else adjustedUpper2;
def FinalLower1 = if !FirstBar then if lastBarClose <= adjustedLower1[1] and carryOverBand and MinFromPrevBar == 1
then lastBarClose else adjustedLower1 else adjustedLower1;
def FinalLower2 = if !FirstBar then if lastBarClose <= adjustedLower2[1] and carryOverBand and MinFromPrevBar == 1
then lastBarClose else Min(adjustedLower1 , adjustedLower2) else adjustedLower2;
def nearestUpper = Min(FinalUpper1, FinalUpper2);
def nearestLower = Max(FinalLower1, FinalLower2);
# 1st bar bands will extend to right 1 bar so 1st bar is visible live
def FinalUpper1Ext = if ExtendBar1 then FinalUpper1[1] else FinalUpper1;
def FinalUpper2Ext = if ExtendBar1 then FinalUpper2[1] else FinalUpper2;
def FinalLower1Ext = if ExtendBar1 then FinalLower1[1] else FinalLower1;
def FinalLower2Ext = if ExtendBar1 then FinalLower2[1] else FinalLower2;
def nearestUpperExt = if ExtendBar1 then nearestUpper[1] else nearestUpper;
def nearestLowerExt = if ExtendBar1 then nearestLower[1] else nearestLower;
def mode;
switch (display_Mode) {
case Range: mode = 1; #Range
case Area: mode = 2; #Area
case Line: mode = 3; } #Line
AddCloud(if Qualifies and mode == 1 then FinalUpper1Ext else Double.NaN, FinalUpper2Ext, Color.GRAY, Color.GRAY, 1);
AddCloud(if Qualifies and mode == 1 then FinalLower1Ext else Double.NaN, FinalLower2Ext, Color.GRAY, Color.GRAY, 1);
AddCloud(if Qualifies and mode == 2 then nearestUpperExt else Double.NaN, Double.POSITIVE_INFINITY, Color.GRAY, Color.GRAY, 0);
AddCloud(if Qualifies and mode == 2 then nearestLowerExt else Double.NaN, Double.NEGATIVE_INFINITY, Color.GRAY, Color.GRAY, 0);
plot Upper_Line = if Qualifies and mode == 3 then nearestUpperExt else Double.NaN;
Upper_Line.SetDefaultColor(Color.GRAY);
Upper_Line.SetStyle(Curve.FIRM);
Upper_Line.HideBubble();
plot Lower_Line = if Qualifies and mode == 3 then nearestLowerExt else Double.NaN;
Lower_Line.SetDefaultColor(Color.GRAY);
Lower_Line.SetStyle(Curve.FIRM);
Lower_Line.HideBubble();
AddLabel(Qualifies and show_labels, "HaltUp: " + AsDollars(nearestUpper) + " HaltLw: " + (if nearestLower == 0 then "None" else AsDollars(nearestLower)), Color.ORANGE);