MAD Price Volume Anomaly Detection
http://tos.mx/uapaYamIntroduction
I've long been fascinated by the connections between volume and price, and since I trade on tick charts, time as well. And so I offer this indicator to the UseThinkScript community.
It is not to be confused with the MAD indicator that is now part of ThinkOrSwim. That one is Moving Average Difference. This one is Median Absolute Deviation. Much more on the method can be found here:
https://en.wikipedia.org/wiki/Median_absolute_deviation
Like z-scores, it is a method for determining whether a point in a data set lies sufficiently far from normal that it is an outlier. However, it is far better at identifying outliers (anomalies) in my opinion.
Use
I have coded a plethora of options into this one, including methods for calculating the price for the bar, as well as methods for determining how to code the bar as up or down for arrow signals.
Those signals are NOT necessarily entry and exit points, though sometimes they look like they might make good ones. They reflect the direction of the bar that is anomalous.
price calculation methods
* current bar range
This is the high of the bar minus the low
* last bar change
This is the close (or whatever price you specify) change from the previous bar
* true value of a bar
This is a formula for TVB -- close * 3 - (high + open + low)
* functional range
this is the close - open for a bar but influences volume as the ratio of close - open to high - low is used to adjust the volume for the bar so only a portion of volume that ties to movement through the bar is used.
up / down methods
* current bar midpoint
if the close is above the midpoint, the bar is up, if it is below, it is down
* last bar change
if the close is higher than the last close, the bar is up
* true value
if the true value (TVB) of a bar is > 0 then it is up
* current bar change
if the close is above the open, it is up.
methods for tick charts
Additionally, there are two methods that look at time for price change (or volume change if you change the price to be volume, but more on that later...)
- price x time
- price / time
other ideas
the price and volume data series can be defined as things other than price and volume. It might be interesting to look at volume to tick count perpahs.
threshold
This is the level at which a point is considered anomalous.
length
I set the default at 34, though sometimes it's nice much longer, and other times much shorter. Play with it. See what works for you. I make no prescriptions for your trading style, or for points you should find to be anomalies in the price action.
conclusion
I have a bunch of screen shots, and have not taken the time to annotate them. I have boxed a bunch of things but leave it to you, dear reader, to connect the dots to form whatever pictures you wish. I will, of course, be around to answer questions.
Code:
#########################################
#
# MEDIAN ABSOLUTE DEVIATION
# Price -- Volume Anomaly Detection
#
# (c) Mashume, 2022.9
# Version 1.0 public
# released to the usethinkscript.com
# community
#
# released under gpl v3 as opensource
#
#########################################
declare lower;
input price = CLOSE;
input Volume = VOLUME;
input length = 34;
input k = 1.4826;
input threshold = 4.0;
input method = {default "current bar range", "last bar change", "true value", "functional", "vol / time", "vol x time"};
input up_down = {default "current bar midpoint", "last bar change", "true value", "current bar change"};
def tvb = (CLOSE - LOW) + (CLOSE - OPEN) - (HIGH - CLOSE);
def range = high - low;
def movement = close - open;
def ratio = movement / range;
def pv;
switch (method) {
case "current bar range":
pv = (high - low) * volume;
case "last bar change":
pv = (price - price[1]) * volume;
case "true value":
pv = (tvb - tvb[1]) * volume;
case "functional":
pv = AbsValue(movement) * (volume * ratio);
case "vol / time":
pv = (price - price[1]) / (getTime() - getTime()[1]);
case "vol x time":
pv = (price - price[1]) * (getTime() - getTime()[1]);
};
def x_hat = Median(data = pv, length = length);
def mad = absValue(pv - x_hat);
def MADn = k * median(absValue(pv - x_hat));
plot indicator = mad / madn;
plot upper_limit = threshold;
def condition;
switch (up_down) {
case "current bar midpoint":
condition = if close < (high + low) / 2 then -1 else if close > (high + low) / 2 then 1 else 0;
case "last bar change":
condition = if close < close[1] then -1 else if close > close[1] then 1 else 0;
case "true value":
condition = if tvb < 0 then -1 else if tvb > 0 then 1 else 0;
case "current bar change":
condition = if close < open then -1 else if close > open then 1 else 0;
};
plot down = if condition == -1 AND indicator > threshold then indicator else double.nan;
plot up = if condition == 1 AND indicator > threshold then threshold / 2 else double.nan;
plot indicator_median = median(indicator, length);
def sumcount = if condition == -1 AND indicator > threshold then -1 else if condition == 1 AND indicator > threshold then 1 else 0;
addLabel(yes, "sum of anomalies last " + length + " bars: " + sum(sumcount, length) + " ", color.white);
down.setPaintingStrategy(PaintingStrategy.ARROW_DOWN);
down.SetDefaultColor(color.white);
up.setPaintingStrategy(PaintingStrategy.ARROW_UP);
up.SetDefaultColor(color.white);
upper_limit.setStyle(curve.MEDIUM_DASH);
upper_limit.SetDefaultColor(color.red);
indicator.SetPaintingStrategy(PaintingStrategy.SQUARED_HISTOGRAM);
indicator.SetDefaultColor(CreateColor(red = 122, green = 185, blue = 207));
AddLabel(yes, " " + method + " ", color.white);
Happy Trading,
Mashume
Last edited: