Help with FTD Scan - Follow Through Day.

jborr4028

New member
Can anyone help with creating a scan for the FTD portion of this could. Was able to make the scan for the rally day. I've tried without luck to make the FTD.
https://usethinkscript.com/threads/ibd-distribution-days-study-for-thinkorswim.748/page-4#post-89113

# SMO_MktVolumesDaily.ts
# Version 2.0, 2022-01-30
# For daily charts only.
# Version 3.0, 2022-06-25
# Nasdaq total volume: $TVOL/Q ; NYSE total volume: $TVOL
# Distribution day count tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# tracking-distribution-days-a-crucial-habit/
# Stalling daysFromDate tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# can-slim-market-tops-stalling-distribution/

declare lower;
declare zerobase;

def length = 20; # volume moving average lenth in days

input volumeSymbol = {default NYSE, NASDAQ, SPX};
# Reset distribution day counts on FTD.
input distributionRstDay = 20191010;

def volCl;
def volHi;
def findSymbol;

# To make volume differences more visible, use a base volume number
# The subtracted volume number is then magnified to present a bigger difference
def volMin; # base number for volume
def dropThreshold;

switch (volumeSymbol) {

# It was found there may be erratic volume data on close values
# On 2/19/2020, NYSE volume close values were 0 on 2/18 & 2/12
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
#volCl = close("$TVOL");
volHi = high("$TVOL");
# use SPX volume change percentage to replace erratic NYSE volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
#volCl = close("$TVOL/Q");
volHi = high("$TVOL/Q");
# use SPX volume change percentage to replace erratic NASDAQ volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
#volCl = close("$TVOLSPC");
volHi = high("$TVOLSPC");
# use NYSE volume change percentage to replace erratic SPX volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}

def cls = close;

def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;

plot Vol = 3 * (volumes - volMin);
plot VolAvg = 3 * (Average(volumes, length) - volMin);

Vol.SetPaintingStrategy(PaintingStrategy.HISTOGRAM);
Vol.SetLineWeight(3);
Vol.DefineColor("Up", Color.UPTICK);
Vol.DefineColor("Down", Color.DOWNTICK);
Vol.AssignValueColor(if cls > cls[1] then Vol.color("Up")
else if cls < cls[1] then Vol.color("Down")
else GetColor(1));
VolAvg.SetDefaultColor(GetColor(8));

# Display useful texts starting at upper left corner
# End of Day volume change
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);

# InvalidDay was added since volume on 2019/11/29 (after Thanksgiving) was N/A.
addLabel( yes, if volChangePercentDay == 0 then "InvalidDay" else "" +
"VolmChg="+ Concat("", round(VolChangePercentDay)) +
"%", if VolChangePercentDay < 0 then
Color.DARK_GRAY else if cls > cls[1] then Color.DARK_GREEN
else Color.DARK_RED);

# Count distributionDay only if market price drops 0.2% or more
def downDay = cls <= (cls[1] * 0.998);

#def volIncrease = volumes > volumes[1];
def volIncrease = vol > vol[1];
#
# After 25 sessions, a distribution day expires
# Use 25 bar numbers to represent 25 live sessions. GetDay or alike includes weekends.
#

def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;

# a distribution day can fall off the count if the index rises 6% or more,
# on an intraday basis, from its close on the day the higher-volume loss appears.
# Remove distribution days after prices increases 6% WHEN market is in uptrend.
# Need to fix:
# During market bottomed on 2-28-2020, stock price rose to 9.8% with market still in
# correction. The high volume selloff on 2-28 would still be counted as a distribution.
# The highest date should be after the distribution day

# Get proper high for future 25 days
def prHi = high;
def prLo = low;

def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
# Note: This condition disqualifies D-Days after large bear rally
# This is acceptable for now since D-Days in bear market are not really useful
def priceInRange = (cls * 1.06 >= prHighest);

def distributionDay = downDay and volIncrease and LastDays and priceInRange;

# Count valid distribution days in last 25 days
def distDayCount = sum(distributionDay, 25);


# A broad market correction makes the distribution day count irrelevant
# reset distribution count to 0
# Distribution day count should reset after 2nd confirmation day
# To do: automate the reset day when correction or follow-up day appears
# input distributionRstDay = 20191010; a prior 2nd confirmation day
# input distributionRstDay = 20200402; a prior 2nd confirmation day
#input distributionRstDay = 20191010;

def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
# Need to use above variable to restart d-day count
def newDistDays = sum(distributionDay and newDistributionCycle, 25);

# Display bubble red is count > 5, yellow >3, else while
AddChartBubble(distributionDay and !newDistributionCycle, vol, concat("", distDayCount),
if distDayCount < 3 then color.WHITE
else if distDayCount < 5 then color.LIGHT_ORANGE
else color.RED);

# Show D-Day reset line at the reset date input by user
# It appears at the left side of the volume bar
AddVerticalLine( if (GetYYYYMMDD() == distributionRstDay ) then yes
else no,
" 2ndCnfm",
Color.GREEN, Curve.MEDIUM_DASH);


# to do: Comparison of preholiday data may be invalid.

#------------------------------------------------------------
# Stalling day counts
# 1. market has been rising and price is within 3% of 25 day high
# 2. Price making a high
# current close >= prior 2 day close, or
# current close >= prior day high
# 3. volume >= 95% of prior day volume
# 4. close in lower half of daily range
# 5. small gain within 0.4% for SPX & NASDAQ
# 6. The above IBD criteria disclosed in one article generates too many stalling days
# Additional rules from IBD book are used to further reduce stalling counts
# 6.1 close up smaller than prior 2 days
# 6.2 low is lower than high of prior day (No unfilled gap-up)
# 6.3 there is at least one decent gain in prior 2 days
# 6.4 daily trading range should be similar to last 2 days
# 7. stalling counts are reduced due to time (25 days) and significantly upward
# movement (6%) of the index
# Ex. 2019/11/12 was a stalling day on SPX, 2019/12/18 was stalling for Nasdaq

def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));

# Added a 0.2% gap from prior day high to allow 2020/05/26 to count
# as a stalling day for NASDAQ
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
# price trading range is the high - low plus the gapup if any
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);

def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;

# Count stalling days
def stallDayCount = sum(stallDay, 25);

# calculate new stalling days after the reset day (e.g. follow-up date)
def newStallDays = sum(StallDay and newDistributionCycle, 25);

# Display final distribution count (incl. stall days)
# red if >= 5, >3: yellow, else green
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;

AddChartBubble(distributionDay and newDistributionCycle, vol,
if volCl == 0 then concat("?", newDistDays) else concat("", newDistDays),
if totalNdDays < 3 then color.WHITE
else if totalNdDays < 5 then color.LIGHT_ORANGE
else color.RED);

AddChartBubble(stallDay AND lastDays, vol,
if volCl == 0 then "?S" + concat("", stallDayCount)
else "S" + concat("", stallDayCount),
if totalNdDays < 3 then color.WHITE
else if totalNdDays < 5 then color.LIGHT_ORANGE
else color.RED);
AddChartBubble(volCl == 0 AND !stallDay AND !distributionDay, vol, "?", color.LIGHT_GRAY);

addLabel(totalDdays != totalNdDays, "AllDdays =" + concat("", totalDdays), Color.GRAY);

#-------------------------------------------------------------------------------------
# Follow-through signals (FTD) are more likely to fail if distribution days
# occur in the first few days of a new uptrend. This is one key red flag.
# Quantification in script is implemented with a concept of critical score (critScore):
# critScore = 3 for the 1st 5 days after FTD
# critScore = 2 on the 6th, critScore = 1 on 7th day, critScore = 0 after 7th day
# Total Distribution days = critScore + regular D-day count
# critScore is used only if there is at least one D-day in the 1st 5th day after FTD
#-------------------------------------------------------------------------------------

def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;

def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;

def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;

# Actual distribution day count is shown but color depends on totalNdDaysC
addLabel(yes, "NewDdays =" + concat("",totalNdDays ),
if totalNdDaysC <=2 then Color.Green
else if totalNdDaysC <= 4 then Color.ORANGE
else Color.RED);

# Add an indication of 1st rally day to start FTD count
# in a market correction period
# pink rally day is a day satisfying the following conditions:
# 1). Close above ½ of daily TRUE range and below prior day close
# 2). Low is the lowest during the market correction,
# including future lows if available

# resolution of each 1st rally day is set to about 2 weeks
def rDayInterval = round(25/2, 0);

def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];

# market correction is currently defined as down about 8% from top
# need to be refined so that it will work in a bear market that is forming a bottom.
# In this case, the 8% drop may not be required.
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;

def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);

# The real rally day has its close higher than prior close
# A rally day is invalidated if the low is broken in subsequent days
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;

def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);

AddChartBubble(RallyDay1, vol, "R1", color.LIGHT_GREEN);

# RallyDay1 is still active only if prices have not undercut low of the following:
# 1. the low of on that day if pink rally
# 2. the low of before that day if real rally
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;

# Currently (April, 2020) a daily price (cls) increase of 1.25% minimum is
# the price requirement by either SPX or NASDAQ for a FTD.
# Must be day 4 after 1st rally attempt /w an exception (1st 3 days are strong).
# Must have higher volume on the FTD day

def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;

def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;

# Temp fix: Show FTD only if it's 14 days after the previous one
def oneFTD = sum(oneFTD[1], 14) == 0 and isFTD and !underRallyLow;
AddChartBubble(oneFTD, vol, "FTD", color.LIGHT_GREEN);
 
Last edited by a moderator:
Solution
set to D for DAY aggregation on the scanner and then try this:
Code:
def length = 20; # volume moving average lenth in days
input volumeSymbol = {default NYSE, NASDAQ, SPX};
input distributionRstDay = 20191010;
def volCl;
def findSymbol;
def volMin; # base number for volume
def dropThreshold;
switch (volumeSymbol) {
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") -...
set to D for DAY aggregation on the scanner and then try this:
Code:
def length = 20; # volume moving average lenth in days
input volumeSymbol = {default NYSE, NASDAQ, SPX};
input distributionRstDay = 20191010;
def volCl;
def findSymbol;
def volMin; # base number for volume
def dropThreshold;
switch (volumeSymbol) {
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}
def cls = close;
def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;
def Vol = 3 * (volumes - volMin);
def VolAvg = 3 * (Average(volumes, length) - volMin);
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);
def downDay = cls <= (cls[1] * 0.998);
def volIncrease = vol > vol[1];
def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;
def prHi = high;
def prLo = low;
def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
def priceInRange = (cls * 1.06 >= prHighest);
def distributionDay = downDay and volIncrease and LastDays and priceInRange;
def distDayCount = sum(distributionDay, 25);
def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
def newDistDays = sum(distributionDay and newDistributionCycle, 25);
def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);
def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;
def stallDayCount = sum(stallDay, 25);
def newStallDays = sum(StallDay and newDistributionCycle, 25);
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;
def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;
def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;
def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;
def rDayInterval = round(25/2, 0);
def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;
def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;
def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;
def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;
def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;
plot scan = isftd;
 
Solution
Can anyone help with creating a scan for the FTD portion of this could. Was able to make the scan for the rally day. I've tried without luck to make the FTD.
https://usethinkscript.com/threads/ibd-distribution-days-study-for-thinkorswim.748/page-4#post-89113

# SMO_MktVolumesDaily.ts
# Version 2.0, 2022-01-30
# For daily charts only.
# Version 3.0, 2022-06-25
# Nasdaq total volume: $TVOL/Q ; NYSE total volume: $TVOL
# Distribution day count tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# tracking-distribution-days-a-crucial-habit/
# Stalling daysFromDate tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# can-slim-market-tops-stalling-distribution/

declare lower;
declare zerobase;

def length = 20; # volume moving average lenth in days

input volumeSymbol = {default NYSE, NASDAQ, SPX};
# Reset distribution day counts on FTD.
input distributionRstDay = 20191010;

def volCl;
def volHi;
def findSymbol;

# To make volume differences more visible, use a base volume number
# The subtracted volume number is then magnified to present a bigger difference
def volMin; # base number for volume
def dropThreshold;

switch (volumeSymbol) {

# It was found there may be erratic volume data on close values
# On 2/19/2020, NYSE volume close values were 0 on 2/18 & 2/12
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
#volCl = close("$TVOL");
volHi = high("$TVOL");
# use SPX volume change percentage to replace erratic NYSE volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
#volCl = close("$TVOL/Q");
volHi = high("$TVOL/Q");
# use SPX volume change percentage to replace erratic NASDAQ volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
#volCl = close("$TVOLSPC");
volHi = high("$TVOLSPC");
# use NYSE volume change percentage to replace erratic SPX volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}

def cls = close;

def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;

plot Vol = 3 * (volumes - volMin);
plot VolAvg = 3 * (Average(volumes, length) - volMin);

Vol.SetPaintingStrategy(PaintingStrategy.HISTOGRAM);
Vol.SetLineWeight(3);
Vol.DefineColor("Up", Color.UPTICK);
Vol.DefineColor("Down", Color.DOWNTICK);
Vol.AssignValueColor(if cls > cls[1] then Vol.color("Up")
else if cls < cls[1] then Vol.color("Down")
else GetColor(1));
VolAvg.SetDefaultColor(GetColor(8));

# Display useful texts starting at upper left corner
# End of Day volume change
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);

# InvalidDay was added since volume on 2019/11/29 (after Thanksgiving) was N/A.
addLabel( yes, if volChangePercentDay == 0 then "InvalidDay" else "" +
"VolmChg="+ Concat("", round(VolChangePercentDay)) +
"%", if VolChangePercentDay < 0 then
Color.DARK_GRAY else if cls > cls[1] then Color.DARK_GREEN
else Color.DARK_RED);

# Count distributionDay only if market price drops 0.2% or more
def downDay = cls <= (cls[1] * 0.998);

#def volIncrease = volumes > volumes[1];
def volIncrease = vol > vol[1];
#
# After 25 sessions, a distribution day expires
# Use 25 bar numbers to represent 25 live sessions. GetDay or alike includes weekends.
#

def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;

# a distribution day can fall off the count if the index rises 6% or more,
# on an intraday basis, from its close on the day the higher-volume loss appears.
# Remove distribution days after prices increases 6% WHEN market is in uptrend.
# Need to fix:
# During market bottomed on 2-28-2020, stock price rose to 9.8% with market still in
# correction. The high volume selloff on 2-28 would still be counted as a distribution.
# The highest date should be after the distribution day

# Get proper high for future 25 days
def prHi = high;
def prLo = low;

def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
# Note: This condition disqualifies D-Days after large bear rally
# This is acceptable for now since D-Days in bear market are not really useful
def priceInRange = (cls * 1.06 >= prHighest);

def distributionDay = downDay and volIncrease and LastDays and priceInRange;

# Count valid distribution days in last 25 days
def distDayCount = sum(distributionDay, 25);


# A broad market correction makes the distribution day count irrelevant
# reset distribution count to 0
# Distribution day count should reset after 2nd confirmation day
# To do: automate the reset day when correction or follow-up day appears
# input distributionRstDay = 20191010; a prior 2nd confirmation day
# input distributionRstDay = 20200402; a prior 2nd confirmation day
#input distributionRstDay = 20191010;

def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
# Need to use above variable to restart d-day count
def newDistDays = sum(distributionDay and newDistributionCycle, 25);

# Display bubble red is count > 5, yellow >3, else while
AddChartBubble(distributionDay and !newDistributionCycle, vol, concat("", distDayCount),
if distDayCount < 3 then color.WHITE
else if distDayCount < 5 then color.LIGHT_ORANGE
else color.RED);

# Show D-Day reset line at the reset date input by user
# It appears at the left side of the volume bar
AddVerticalLine( if (GetYYYYMMDD() == distributionRstDay ) then yes
else no,
" 2ndCnfm",
Color.GREEN, Curve.MEDIUM_DASH);


# to do: Comparison of preholiday data may be invalid.

#------------------------------------------------------------
# Stalling day counts
# 1. market has been rising and price is within 3% of 25 day high
# 2. Price making a high
# current close >= prior 2 day close, or
# current close >= prior day high
# 3. volume >= 95% of prior day volume
# 4. close in lower half of daily range
# 5. small gain within 0.4% for SPX & NASDAQ
# 6. The above IBD criteria disclosed in one article generates too many stalling days
# Additional rules from IBD book are used to further reduce stalling counts
# 6.1 close up smaller than prior 2 days
# 6.2 low is lower than high of prior day (No unfilled gap-up)
# 6.3 there is at least one decent gain in prior 2 days
# 6.4 daily trading range should be similar to last 2 days
# 7. stalling counts are reduced due to time (25 days) and significantly upward
# movement (6%) of the index
# Ex. 2019/11/12 was a stalling day on SPX, 2019/12/18 was stalling for Nasdaq

def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));

# Added a 0.2% gap from prior day high to allow 2020/05/26 to count
# as a stalling day for NASDAQ
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
# price trading range is the high - low plus the gapup if any
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);

def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;

# Count stalling days
def stallDayCount = sum(stallDay, 25);

# calculate new stalling days after the reset day (e.g. follow-up date)
def newStallDays = sum(StallDay and newDistributionCycle, 25);

# Display final distribution count (incl. stall days)
# red if >= 5, >3: yellow, else green
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;

AddChartBubble(distributionDay and newDistributionCycle, vol,
if volCl == 0 then concat("?", newDistDays) else concat("", newDistDays),
if totalNdDays < 3 then color.WHITE
else if totalNdDays < 5 then color.LIGHT_ORANGE
else color.RED);

AddChartBubble(stallDay AND lastDays, vol,
if volCl == 0 then "?S" + concat("", stallDayCount)
else "S" + concat("", stallDayCount),
if totalNdDays < 3 then color.WHITE
else if totalNdDays < 5 then color.LIGHT_ORANGE
else color.RED);
AddChartBubble(volCl == 0 AND !stallDay AND !distributionDay, vol, "?", color.LIGHT_GRAY);

addLabel(totalDdays != totalNdDays, "AllDdays =" + concat("", totalDdays), Color.GRAY);

#-------------------------------------------------------------------------------------
# Follow-through signals (FTD) are more likely to fail if distribution days
# occur in the first few days of a new uptrend. This is one key red flag.
# Quantification in script is implemented with a concept of critical score (critScore):
# critScore = 3 for the 1st 5 days after FTD
# critScore = 2 on the 6th, critScore = 1 on 7th day, critScore = 0 after 7th day
# Total Distribution days = critScore + regular D-day count
# critScore is used only if there is at least one D-day in the 1st 5th day after FTD
#-------------------------------------------------------------------------------------

def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;

def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;

def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;

# Actual distribution day count is shown but color depends on totalNdDaysC
addLabel(yes, "NewDdays =" + concat("",totalNdDays ),
if totalNdDaysC <=2 then Color.Green
else if totalNdDaysC <= 4 then Color.ORANGE
else Color.RED);

# Add an indication of 1st rally day to start FTD count
# in a market correction period
# pink rally day is a day satisfying the following conditions:
# 1). Close above ½ of daily TRUE range and below prior day close
# 2). Low is the lowest during the market correction,
# including future lows if available

# resolution of each 1st rally day is set to about 2 weeks
def rDayInterval = round(25/2, 0);

def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];

# market correction is currently defined as down about 8% from top
# need to be refined so that it will work in a bear market that is forming a bottom.
# In this case, the 8% drop may not be required.
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;

def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);

# The real rally day has its close higher than prior close
# A rally day is invalidated if the low is broken in subsequent days
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;

def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);

AddChartBubble(RallyDay1, vol, "R1", color.LIGHT_GREEN);

# RallyDay1 is still active only if prices have not undercut low of the following:
# 1. the low of on that day if pink rally
# 2. the low of before that day if real rally
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;

# Currently (April, 2020) a daily price (cls) increase of 1.25% minimum is
# the price requirement by either SPX or NASDAQ for a FTD.
# Must be day 4 after 1st rally attempt /w an exception (1st 3 days are strong).
# Must have higher volume on the FTD day

def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;

def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;

# Temp fix: Show FTD only if it's 14 days after the previous one
def oneFTD = sum(oneFTD[1], 14) == 0 and isFTD and !underRallyLow;
AddChartBubble(oneFTD, vol, "FTD", color.LIGHT_GREEN);
@jborr4028 do you mind sharing the scan for R1 and if you are able to scan the FTD? I tried but unable to get any results.


Thanks
 
@jborr4028 do you mind sharing the scan for R1 and if you are able to scan the FTD? I tried but unable to get any results.


Thanks
This is the RallyDay Scan R1.

def Data = close;
# For daily charts only.
# Version 3.0, 2022-06-25
# Nasdaq total volume: $TVOL/Q ; NYSE total volume: $TVOL
# Distribution day count tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# tracking-distribution-days-a-crucial-habit/
# Stalling daysFromDate tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# can-slim-market-tops-stalling-distribution/

declare lower;
declare zerobase;

def length = 20; # volume moving average lenth in days

input volumeSymbol = {default NYSE, NASDAQ, SPX};
# Reset distribution day counts on FTD.
input distributionRstDay = 20191010;

def volCl;
def volHi;
def findSymbol;

# To make volume differences more visible, use a base volume number
# The subtracted volume number is then magnified to present a bigger difference
def volMin; # base number for volume
def dropThreshold;

switch (volumeSymbol) {

# It was found there may be erratic volume data on close values
# On 2/19/2020, NYSE volume close values were 0 on 2/18 & 2/12
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
#volCl = close("$TVOL");
volHi = high("$TVOL");
# use SPX volume change percentage to replace erratic NYSE volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
#volCl = close("$TVOL/Q");
volHi = high("$TVOL/Q");
# use SPX volume change percentage to replace erratic NASDAQ volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
#volCl = close("$TVOLSPC");
volHi = high("$TVOLSPC");
# use NYSE volume change percentage to replace erratic SPX volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}

def cls = close;

def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;

def Vol = 3 * (volumes - volMin);
def VolAvg = 3 * (Average(volumes, length) - volMin);

# Display useful texts starting at upper left corner
# End of Day volume change
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);


# Count distributionDay only if market price drops 0.2% or more
def downDay = cls <= (cls[1] * 0.998);

#def volIncrease = volumes > volumes[1];
def volIncrease = vol > vol[1];
#
# After 25 sessions, a distribution day expires
# Use 25 bar numbers to represent 25 live sessions. GetDay or alike includes weekends.
#

def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;

# a distribution day can fall off the count if the index rises 6% or more,
# on an intraday basis, from its close on the day the higher-volume loss appears.
# Remove distribution days after prices increases 6% WHEN market is in uptrend.
# Need to fix:
# During market bottomed on 2-28-2020, stock price rose to 9.8% with market still in
# correction. The high volume selloff on 2-28 would still be counted as a distribution.
# The highest date should be after the distribution day

# Get proper high for future 25 days
def prHi = high;
def prLo = low;

def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
# Note: This condition disqualifies D-Days after large bear rally
# This is acceptable for now since D-Days in bear market are not really useful
def priceInRange = (cls * 1.06 >= prHighest);

def distributionDay = downDay and volIncrease and LastDays and priceInRange;

# Count valid distribution days in last 25 days
def distDayCount = sum(distributionDay, 25);


# A broad market correction makes the distribution day count irrelevant
# reset distribution count to 0
# Distribution day count should reset after 2nd confirmation day
# To do: automate the reset day when correction or follow-up day appears
# input distributionRstDay = 20191010; a prior 2nd confirmation day
# input distributionRstDay = 20200402; a prior 2nd confirmation day
#input distributionRstDay = 20191010;

def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
# Need to use above variable to restart d-day count
def newDistDays = sum(distributionDay and newDistributionCycle, 25);

# Display bubble red is count > 5, yellow >3, else while
AddChartBubble(distributionDay and !newDistributionCycle, vol, concat("", distDayCount),
if distDayCount < 3 then color.WHITE
else if distDayCount < 5 then color.LIGHT_ORANGE
else color.RED);

# Show D-Day reset line at the reset date input by user
# It appears at the left side of the volume bar
AddVerticalLine( if (GetYYYYMMDD() == distributionRstDay ) then yes
else no,
" 2ndCnfm",
Color.GREEN, Curve.MEDIUM_DASH);


# to do: Comparison of preholiday data may be invalid.

#------------------------------------------------------------
# Stalling day counts
# 1. market has been rising and price is within 3% of 25 day high
# 2. Price making a high
# current close >= prior 2 day close, or
# current close >= prior day high
# 3. volume >= 95% of prior day volume
# 4. close in lower half of daily range
# 5. small gain within 0.4% for SPX & NASDAQ
# 6. The above IBD criteria disclosed in one article generates too many stalling days
# Additional rules from IBD book are used to further reduce stalling counts
# 6.1 close up smaller than prior 2 days
# 6.2 low is lower than high of prior day (No unfilled gap-up)
# 6.3 there is at least one decent gain in prior 2 days
# 6.4 daily trading range should be similar to last 2 days
# 7. stalling counts are reduced due to time (25 days) and significantly upward
# movement (6%) of the index
# Ex. 2019/11/12 was a stalling day on SPX, 2019/12/18 was stalling for Nasdaq

def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));

# Added a 0.2% gap from prior day high to allow 2020/05/26 to count
# as a stalling day for NASDAQ
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
# price trading range is the high - low plus the gapup if any
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);

def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;

# Count stalling days
def stallDayCount = sum(stallDay, 25);

# calculate new stalling days after the reset day (e.g. follow-up date)
def newStallDays = sum(StallDay and newDistributionCycle, 25);

# Display final distribution count (incl. stall days)
# red if >= 5, >3: yellow, else green
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;



#-------------------------------------------------------------------------------------
# Follow-through signals (FTD) are more likely to fail if distribution days
# occur in the first few days of a new uptrend. This is one key red flag.
# Quantification in script is implemented with a concept of critical score (critScore):
# critScore = 3 for the 1st 5 days after FTD
# critScore = 2 on the 6th, critScore = 1 on 7th day, critScore = 0 after 7th day
# Total Distribution days = critScore + regular D-day count
# critScore is used only if there is at least one D-day in the 1st 5th day after FTD
#-------------------------------------------------------------------------------------

def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;

def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;

def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;


# Add an indication of 1st rally day to start FTD count
# in a market correction period
# pink rally day is a day satisfying the following conditions:
# 1). Close above ½ of daily TRUE range and below prior day close
# 2). Low is the lowest during the market correction,
# including future lows if available

# resolution of each 1st rally day is set to about 2 weeks
def rDayInterval = round(25/2, 0);

def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];

# market correction is currently defined as down about 8% from top
# need to be refined so that it will work in a bear market that is forming a bottom.
# In this case, the 8% drop may not be required.
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;

def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);

# The real rally day has its close higher than prior close
# A rally day is invalidated if the low is broken in subsequent days
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;

def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);

plot scan = RallyDay1 ;

# RallyDay1 is still active only if prices have not undercut low of the following:
# 1. the low of on that day if pink rally
# 2. the low of before that day if real rally
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;

# Currently (April, 2020) a daily price (cls) increase of 1.25% minimum is
# the price requirement by either SPX or NASDAQ for a FTD.
# Must be day 4 after 1st rally attempt /w an exception (1st 3 days are strong).
# Must have higher volume on the FTD day

def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;

def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;
 
This is the RallyDay Scan R1.

def Data = close;
# For daily charts only.
# Version 3.0, 2022-06-25
# Nasdaq total volume: $TVOL/Q ; NYSE total volume: $TVOL
# Distribution day count tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# tracking-distribution-days-a-crucial-habit/
# Stalling daysFromDate tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# can-slim-market-tops-stalling-distribution/

declare lower;
declare zerobase;

def length = 20; # volume moving average lenth in days

input volumeSymbol = {default NYSE, NASDAQ, SPX};
# Reset distribution day counts on FTD.
input distributionRstDay = 20191010;

def volCl;
def volHi;
def findSymbol;

# To make volume differences more visible, use a base volume number
# The subtracted volume number is then magnified to present a bigger difference
def volMin; # base number for volume
def dropThreshold;

switch (volumeSymbol) {

# It was found there may be erratic volume data on close values
# On 2/19/2020, NYSE volume close values were 0 on 2/18 & 2/12
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
#volCl = close("$TVOL");
volHi = high("$TVOL");
# use SPX volume change percentage to replace erratic NYSE volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
#volCl = close("$TVOL/Q");
volHi = high("$TVOL/Q");
# use SPX volume change percentage to replace erratic NASDAQ volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
#volCl = close("$TVOLSPC");
volHi = high("$TVOLSPC");
# use NYSE volume change percentage to replace erratic SPX volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}

def cls = close;

def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;

def Vol = 3 * (volumes - volMin);
def VolAvg = 3 * (Average(volumes, length) - volMin);

# Display useful texts starting at upper left corner
# End of Day volume change
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);


# Count distributionDay only if market price drops 0.2% or more
def downDay = cls <= (cls[1] * 0.998);

#def volIncrease = volumes > volumes[1];
def volIncrease = vol > vol[1];
#
# After 25 sessions, a distribution day expires
# Use 25 bar numbers to represent 25 live sessions. GetDay or alike includes weekends.
#

def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;

# a distribution day can fall off the count if the index rises 6% or more,
# on an intraday basis, from its close on the day the higher-volume loss appears.
# Remove distribution days after prices increases 6% WHEN market is in uptrend.
# Need to fix:
# During market bottomed on 2-28-2020, stock price rose to 9.8% with market still in
# correction. The high volume selloff on 2-28 would still be counted as a distribution.
# The highest date should be after the distribution day

# Get proper high for future 25 days
def prHi = high;
def prLo = low;

def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
# Note: This condition disqualifies D-Days after large bear rally
# This is acceptable for now since D-Days in bear market are not really useful
def priceInRange = (cls * 1.06 >= prHighest);

def distributionDay = downDay and volIncrease and LastDays and priceInRange;

# Count valid distribution days in last 25 days
def distDayCount = sum(distributionDay, 25);


# A broad market correction makes the distribution day count irrelevant
# reset distribution count to 0
# Distribution day count should reset after 2nd confirmation day
# To do: automate the reset day when correction or follow-up day appears
# input distributionRstDay = 20191010; a prior 2nd confirmation day
# input distributionRstDay = 20200402; a prior 2nd confirmation day
#input distributionRstDay = 20191010;

def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
# Need to use above variable to restart d-day count
def newDistDays = sum(distributionDay and newDistributionCycle, 25);

# Display bubble red is count > 5, yellow >3, else while
AddChartBubble(distributionDay and !newDistributionCycle, vol, concat("", distDayCount),
if distDayCount < 3 then color.WHITE
else if distDayCount < 5 then color.LIGHT_ORANGE
else color.RED);

# Show D-Day reset line at the reset date input by user
# It appears at the left side of the volume bar
AddVerticalLine( if (GetYYYYMMDD() == distributionRstDay ) then yes
else no,
" 2ndCnfm",
Color.GREEN, Curve.MEDIUM_DASH);


# to do: Comparison of preholiday data may be invalid.

#------------------------------------------------------------
# Stalling day counts
# 1. market has been rising and price is within 3% of 25 day high
# 2. Price making a high
# current close >= prior 2 day close, or
# current close >= prior day high
# 3. volume >= 95% of prior day volume
# 4. close in lower half of daily range
# 5. small gain within 0.4% for SPX & NASDAQ
# 6. The above IBD criteria disclosed in one article generates too many stalling days
# Additional rules from IBD book are used to further reduce stalling counts
# 6.1 close up smaller than prior 2 days
# 6.2 low is lower than high of prior day (No unfilled gap-up)
# 6.3 there is at least one decent gain in prior 2 days
# 6.4 daily trading range should be similar to last 2 days
# 7. stalling counts are reduced due to time (25 days) and significantly upward
# movement (6%) of the index
# Ex. 2019/11/12 was a stalling day on SPX, 2019/12/18 was stalling for Nasdaq

def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));

# Added a 0.2% gap from prior day high to allow 2020/05/26 to count
# as a stalling day for NASDAQ
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
# price trading range is the high - low plus the gapup if any
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);

def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;

# Count stalling days
def stallDayCount = sum(stallDay, 25);

# calculate new stalling days after the reset day (e.g. follow-up date)
def newStallDays = sum(StallDay and newDistributionCycle, 25);

# Display final distribution count (incl. stall days)
# red if >= 5, >3: yellow, else green
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;



#-------------------------------------------------------------------------------------
# Follow-through signals (FTD) are more likely to fail if distribution days
# occur in the first few days of a new uptrend. This is one key red flag.
# Quantification in script is implemented with a concept of critical score (critScore):
# critScore = 3 for the 1st 5 days after FTD
# critScore = 2 on the 6th, critScore = 1 on 7th day, critScore = 0 after 7th day
# Total Distribution days = critScore + regular D-day count
# critScore is used only if there is at least one D-day in the 1st 5th day after FTD
#-------------------------------------------------------------------------------------

def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;

def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;

def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;


# Add an indication of 1st rally day to start FTD count
# in a market correction period
# pink rally day is a day satisfying the following conditions:
# 1). Close above ½ of daily TRUE range and below prior day close
# 2). Low is the lowest during the market correction,
# including future lows if available

# resolution of each 1st rally day is set to about 2 weeks
def rDayInterval = round(25/2, 0);

def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];

# market correction is currently defined as down about 8% from top
# need to be refined so that it will work in a bear market that is forming a bottom.
# In this case, the 8% drop may not be required.
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;

def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);

# The real rally day has its close higher than prior close
# A rally day is invalidated if the low is broken in subsequent days
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;

def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);

plot scan = RallyDay1 ;

# RallyDay1 is still active only if prices have not undercut low of the following:
# 1. the low of on that day if pink rally
# 2. the low of before that day if real rally
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;

# Currently (April, 2020) a daily price (cls) increase of 1.25% minimum is
# the price requirement by either SPX or NASDAQ for a FTD.
# Must be day 4 after 1st rally attempt /w an exception (1st 3 days are strong).
# Must have higher volume on the FTD day

def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;

def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;
And the FTD SCAN:


input Data = close;
# For daily charts only.
# Version 3.0, 2022-06-25
# Nasdaq total volume: $TVOL/Q ; NYSE total volume: $TVOL
# Distribution day count tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# tracking-distribution-days-a-crucial-habit/
# Stalling daysFromDate tracking by IBD:
# https://www.investors.com/how-to-invest/investors-corner/
# can-slim-market-tops-stalling-distribution/

declare lower;
declare zerobase;

def length = 20; # volume moving average lenth in days

input volumeSymbol = {default NYSE, NASDAQ, SPX};
# Reset distribution day counts on FTD.
input distributionRstDay = 20191010;

def volCl;
def volHi;
def findSymbol;

# To make volume differences more visible, use a base volume number
# The subtracted volume number is then magnified to present a bigger difference
def volMin; # base number for volume
def dropThreshold;

switch (volumeSymbol) {

# It was found there may be erratic volume data on close values
# On 2/19/2020, NYSE volume close values were 0 on 2/18 & 2/12
case NYSE:
volCl = if close("$TVOL") == 0 then high("$TVOL") else close("$TVOL");
#volCl = close("$TVOL");
volHi = high("$TVOL");
# use SPX volume change percentage to replace erratic NYSE volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 40000;
dropThreshold = .942;
case NASDAQ:
volcl = if close("$TVOL/Q") == 0 then high("$TVOL/Q") else close("$TVOL/Q");
#volCl = close("$TVOL/Q");
volHi = high("$TVOL/Q");
# use SPX volume change percentage to replace erratic NASDAQ volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOLSPC") - close("$TVOLSPC")[1])/close("$TVOLSPC")[1]) else volCl;
volMin = 30000;
dropThreshold = .931;
case SPX:
volcl = if close("$TVOLSPC") == 0 then high("$TVOLSPC") else close("$TVOLSPC");
#volCl = close("$TVOLSPC");
volHi = high("$TVOLSPC");
# use NYSE volume change percentage to replace erratic SPX volume
findSymbol = if volCl == 0 then volCl[1] * (1+ (close("$TVOL") - close("$TVOL")[1])/close("$TVOL")[1]) else volCl;
volMin = 10000;
dropThreshold = .942;
#case Other:
# findSymbol = close; #No plots if volume() is used here!?
}

def cls = close;

def lastBar = HighestAll(if (IsNaN(cls), Double.NaN, BarNumber()));
def volumes = if IsNaN(findSymbol) and BarNumber() == lastBar then volumes[1] else findSymbol;

def Vol = 3 * (volumes - volMin);
def VolAvg = 3 * (Average(volumes, length) - volMin);


# Display useful texts starting at upper left corner
# End of Day volume change
def VolChangePercentDay = if (IsNaN(volumes[1]), 0,
100 * (volumes - volumes[1])/volumes[1]);


# Count distributionDay only if market price drops 0.2% or more
def downDay = cls <= (cls[1] * 0.998);

#def volIncrease = volumes > volumes[1];
def volIncrease = vol > vol[1];
#
# After 25 sessions, a distribution day expires
# Use 25 bar numbers to represent 25 live sessions. GetDay or alike includes weekends.
#

def lastDays = if (BarNumber() > lastBar - 25) then 1 else 0;

# a distribution day can fall off the count if the index rises 6% or more,
# on an intraday basis, from its close on the day the higher-volume loss appears.
# Remove distribution days after prices increases 6% WHEN market is in uptrend.
# Need to fix:
# During market bottomed on 2-28-2020, stock price rose to 9.8% with market still in
# correction. The high volume selloff on 2-28 would still be counted as a distribution.
# The highest date should be after the distribution day

# Get proper high for future 25 days
def prHi = high;
def prLo = low;

def futureHigh = if isNaN(prHi[-25]) then futureHigh[1] else prHi[-25];
def prHighest = Highest(futureHigh, 25);
# Note: This condition disqualifies D-Days after large bear rally
# This is acceptable for now since D-Days in bear market are not really useful
def priceInRange = (cls * 1.06 >= prHighest);

def distributionDay = downDay and volIncrease and LastDays and priceInRange;

# Count valid distribution days in last 25 days
def distDayCount = sum(distributionDay, 25);


# A broad market correction makes the distribution day count irrelevant
# reset distribution count to 0
# Distribution day count should reset after 2nd confirmation day
# To do: automate the reset day when correction or follow-up day appears
# input distributionRstDay = 20191010; a prior 2nd confirmation day
# input distributionRstDay = 20200402; a prior 2nd confirmation day
#input distributionRstDay = 20191010;

def newDistributionCycle = GetYYYYMMDD() > distributionRstDay;
# Need to use above variable to restart d-day count
def newDistDays = sum(distributionDay and newDistributionCycle, 25);

# to do: Comparison of preholiday data may be invalid.

#------------------------------------------------------------
# Stalling day counts
# 1. market has been rising and price is within 3% of 25 day high
# 2. Price making a high
# current close >= prior 2 day close, or
# current close >= prior day high
# 3. volume >= 95% of prior day volume
# 4. close in lower half of daily range
# 5. small gain within 0.4% for SPX & NASDAQ
# 6. The above IBD criteria disclosed in one article generates too many stalling days
# Additional rules from IBD book are used to further reduce stalling counts
# 6.1 close up smaller than prior 2 days
# 6.2 low is lower than high of prior day (No unfilled gap-up)
# 6.3 there is at least one decent gain in prior 2 days
# 6.4 daily trading range should be similar to last 2 days
# 7. stalling counts are reduced due to time (25 days) and significantly upward
# movement (6%) of the index
# Ex. 2019/11/12 was a stalling day on SPX, 2019/12/18 was stalling for Nasdaq

def priceIsHigh = cls >= cls[2] or cls >= prHi[1];
def priceLowHalf = cls < (prHi - prLo)/2 + prLo;
def priceGainSmall = cls - cls[1] > 0 and
((cls - cls[1] < (cls[1] - cls[2])) or
((cls - cls[1] < cls[2] - cls[3])));

# Added a 0.2% gap from prior day high to allow 2020/05/26 to count
# as a stalling day for NASDAQ
def priceGapFill = prLo < prHi[1] * 1.002;
def priceGainOk = (cls[1] - cls[2] > 0.002 * cls[2]) or
(cls[2] - cls[3] > 0.002 * cls[3]);
# price trading range is the high - low plus the gapup if any
def priceRange = if prLo > prHi[1] then prHi-prHi[1] else prHi -prLo;
def priceRangeBig = priceGainOk and priceRange > 0.8 * min(priceRange[1], priceRange[2]);

def stallDay = cls - cls[25] > 0 and
cls >= 0.97 * Highest( prHi, 25) and
volumes > 0.95 * volumes[1] and
cls - cls[1] > 0 and
cls - cls[1] < 1.004 * cls[1] and
priceIsHigh and priceLowHalf and priceGainSmall and priceGapFill and
priceRangeBig and lastDays;

# Count stalling days
def stallDayCount = sum(stallDay, 25);

# calculate new stalling days after the reset day (e.g. follow-up date)
def newStallDays = sum(StallDay and newDistributionCycle, 25);

# Display final distribution count (incl. stall days)
# red if >= 5, >3: yellow, else green
def totalDdays = distDayCount+stallDayCount;
def totalNdDays = newDistDays+newStallDays;

#-------------------------------------------------------------------------------------
# Follow-through signals (FTD) are more likely to fail if distribution days
# occur in the first few days of a new uptrend. This is one key red flag.
# Quantification in script is implemented with a concept of critical score (critScore):
# critScore = 3 for the 1st 5 days after FTD
# critScore = 2 on the 6th, critScore = 1 on 7th day, critScore = 0 after 7th day
# Total Distribution days = critScore + regular D-day count
# critScore is used only if there is at least one D-day in the 1st 5th day after FTD
#-------------------------------------------------------------------------------------

def ftdBar = if GetYYYYMMDD() == distributionRstDay then barnumber() else 0;
def lastFtdBar = highestall(ftdBar);
def daysAfterFTD = lastBar - lastFtdBar;

def critScore = if daysAfterFTD <= 0 then 0 else
if daysAfterFTD <= 5 then 3 else
if daysAfterFTD <= 6 then 2 else
if daysAfterFTD <= 7 then 1 else 0;

def totalNdDaysC = totalNdDays + if totalNdDays > 0 then critScore else 0;

# Actual distribution day count is shown but color depends on totalNdDaysC

# Add an indication of 1st rally day to start FTD count
# in a market correction period
# pink rally day is a day satisfying the following conditions:
# 1). Close above ½ of daily TRUE range and below prior day close
# 2). Low is the lowest during the market correction,
# including future lows if available

# resolution of each 1st rally day is set to about 2 weeks
def rDayInterval = round(25/2, 0);

def futureLow = if isNaN(prLo[-rDayInterval]) then futureLow[1]
else prLo[-rDayInterval];
def futureCls = if isNaN(cls[-rDayInterval]) then futureCls[1]
else cls[-rDayInterval];

# market correction is currently defined as down about 8% from top
# need to be refined so that it will work in a bear market that is forming a bottom.
# In this case, the 8% drop may not be required.
def mktCr = prLo[1] <= highest(high, 25) * dropThreshold; #.931; #.92;
def prRng = TrueRange(prHi, cls, prLo); #prHi - prLo;

def pinkRday = cls > (prLo + prRng/2) and cls < cls[1] and
prLo <= lowest(prLo[1],rDayInterval) and
prLo <= lowest(futureLow, rDayInterval);

# The real rally day has its close higher than prior close
# A rally day is invalidated if the low is broken in subsequent days
def realRday = cls > cls[1] and
(prLo <= lowest(prLo[1],rDayInterval) or
prLo[1] <= lowest(prLo[1],rDayInterval)) and
(prLo <= lowest(futureLow, rDayInterval) or
prLo[1] <= lowest(futureLow, rDayInterval)) and
sum(realRday[1], rDayInterval) == 0 and
sum(pinkRday[1], rDayInterval) == 0;

def RallyDay1 = (mktCr or mktCr[1]) and (pinkRday or realRday);



# RallyDay1 is still active only if prices have not undercut low of the following:
# 1. the low of on that day if pink rally
# 2. the low of before that day if real rally
def pinkLow = if pinkRDay then prLo else pinkLow[1];
def rallyLow = if realRday then prLo[1] else rallyLow[1];
def rallyDayLow = if pinkLow == 0 then rallyLow else
if rallyLow == 0 then pinkLow else min(pinkLow, rallyLow);
def underRallyLow = prLo <= rallyDayLow;

# Currently (April, 2020) a daily price (cls) increase of 1.25% minimum is
# the price requirement by either SPX or NASDAQ for a FTD.
# Must be day 4 after 1st rally attempt /w an exception (1st 3 days are strong).
# Must have higher volume on the FTD day

def lastR1Bar = if (RallyDay1, barnumber(), lastR1Bar[1]);
def daysAfterR1 = barnumber() - lastR1Bar;

def isFTD = if daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125) and
volIncrease then 1 else 0 ;

# Temp fix: Show FTD only if it's 14 days after the previous one
def oneFTD = sum(oneFTD[1], 14) == 0 and isFTD and !underRallyLow;
AddChartBubble(oneFTD, vol, "FTD", color.LIGHT_GREEN);

def oneFTD1 = (oneFTD or isFTD[1]);

plot scan = daysAfterR1 >= 3 and daysAfterR1 < 20
and barnumber() > lastR1Bar and
barnumber() < lastR1Bar + 25 and lastR1Bar != 0 and
cls >= (cls[1] * 1.0125);
 

Join useThinkScript to post your question to a community of 21,000+ developers and traders.

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
396 Online
Create Post

Similar threads

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Back
Top