MAD Anomaly Detection For ThinkOrSwim

mashume

Expert
VIP
Lifetime

MAD Price Volume Anomaly Detection

http://tos.mx/uapaYam

Introduction
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
These take the change in price and either multiply by time or divide by time. Sometimes, anomalous bars happen when it takes traders a long time to decide where the market should go... this is often the case at reversals. Other times you'll see anomalous events happen when price moves very fast. These capture those kinds of events.

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.

scih9wh.png

MWUap7E.png

N4gZPl4.png

hAfxZdK.png


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:

MAD Price Volume Anomaly Detection

http://tos.mx/uapaYam

Introduction
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
These take the change in price and either multiply by time or divide by time. Sometimes, anomalous bars happen when it takes traders a long time to decide where the market should go... this is often the case at reversals. Other times you'll see anomalous events happen when price moves very fast. These capture those kinds of events.

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.

scih9wh.png

MWUap7E.png

N4gZPl4.png

hAfxZdK.png


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
I have interest in this as I’ve previously worked on similar things. If you had a wish list, for example backtest a default length, applying entry technics or strengthen technical analysis(which could be a lot but not out of the question), what would it be ?
 
So the wishlist for backtesting is this:
I trade on tick charts, which is the first issue. It's just hard to get tick data for futures. So If I could, I would look at trades for /6E on 50, 100, and 200 ticks. Then I would decide which anomaly signals presented the best chance of achieving, for this exercise say, 20 ticks movement in the direction of the trade before they went 10 ticks against.

I've written some extensive back testing tools in python, and worked with the TDA api to collect tick data into a database... perhaps I should pick that up again. But mostly I write scripts that do interesting things mathematically with time series data and perhaps provide some insight into the workings of the machine.

-mashume
 
So the wishlist for backtesting is this:
I trade on tick charts, which is the first issue. It's just hard to get tick data for futures. So If I could, I would look at trades for /6E on 50, 100, and 200 ticks. Then I would decide which anomaly signals presented the best chance of achieving, for this exercise say, 20 ticks movement in the direction of the trade before they went 10 ticks against.

I've written some extensive back testing tools in python, and worked with the TDA api to collect tick data into a database... perhaps I should pick that up again. But mostly I write scripts that do interesting things mathematically with time series data and perhaps provide some insight into the workings of the machine.

-mashume
You lost me a tick charts :). I have a plethora of models to work with but never used tick charts. If it wasn't tick charts, I would gladly help. I do, however, have some Bayesian sub scripts to get you started if that's a route you wish to go. Sadly, I have yet to complete k-means and KNN.

A quickly added kurtosis skewness as a supplement to your script that you might have handy: http://tos.mx/ldn3AGL I do like the afterhours /ES signals with it.
 

MAD Price Volume Anomaly Detection

http://tos.mx/uapaYam

Introduction
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
These take the change in price and either multiply by time or divide by time. Sometimes, anomalous bars happen when it takes traders a long time to decide where the market should go... this is often the case at reversals. Other times you'll see anomalous events happen when price moves very fast. These capture those kinds of events.

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.

scih9wh.png

MWUap7E.png

N4gZPl4.png

hAfxZdK.png


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

k = 1.4826 is a normalization factor if you want to show confidence intervals. 67%, 95% etc.

True MAD is more like this:
(I'm using Mean and not Median, but plan to try both).

input NormalMAD = yes;
def sample = (some data set on the chart, I use 30Day 5m and 30Day 30m);
def lin_reg = InertiaAll(sample);
def std_dev = StDevAll(sample);
def std_err = StErrAll(sample);
def MAD = std_dev * Sqr(2 / Double.Pi);
def sMAD = if NormalMAD then MAD * 1.4826 else MAD * 1; # 1.4826; #scales MAD to n-distribution
def cMAD = MAD / lin_reg; #coefficients to MAD

Example plots that plot intervals above if close > MAD or below if close < MAD.

plot variance1 = if close > MAD then close + MAD * hlc3 else if close < MAD then close - MAD * hcl3 else double.nan; (I use ohlc4).
plot variance1 = if close > MAD then close + sMAD * hlc3 else if close < MAD then close - sMAD * hcl3 else double.nan; #normalized plot

You're still measuring the absolute deviation from the median, however the factor to normalize isn't necessarily critical unless you want to measure standard deviations at the same time as MAD as Mean(Median) Absolute Deviation has no confidence interval.
 
k = 1.4826 is a normalization factor if you want to show confidence intervals. 67%, 95% etc.

True MAD is more like this:
(I'm using Mean and not Median, but plan to try both).

input NormalMAD = yes;
def sample = (some data set on the chart, I use 30Day 5m and 30Day 30m);
def lin_reg = InertiaAll(sample);
def std_dev = StDevAll(sample);
def std_err = StErrAll(sample);
def MAD = std_dev * Sqr(2 / Double.Pi);
def sMAD = if NormalMAD then MAD * 1.4826 else MAD * 1; # 1.4826; #scales MAD to n-distribution
def cMAD = MAD / lin_reg; #coefficients to MAD

Example plots that plot intervals above if close > MAD or below if close < MAD.

plot variance1 = if close > MAD then close + MAD * hlc3 else if close < MAD then close - MAD * hcl3 else double.nan; (I use ohlc4).
plot variance1 = if close > MAD then close + sMAD * hlc3 else if close < MAD then close - sMAD * hcl3 else double.nan; #normalized plot

You're still measuring the absolute deviation from the median, however the factor to normalize isn't necessarily critical unless you want to measure standard deviations at the same time as MAD as Mean(Median) Absolute Deviation has no confidence interval.

Sorry actually these would not have price twice. It would have a confidence interval where def ci95 = 1.96; #twoMAD
plot variance1 = if close > MAD then close + MAD * hlc3 else if close < MAD then close - MAD else double.nan; (I use ohlc4).
plot variance1 = if close > MAD then close + sMAD * ci95 else if close < MAD then close - sMAD * ci95 else double.nan; #normalized plot

And you also apply it to MAD but its not scaled correclty or you can just use MAD *1, 2, 3 intervals etc.
 
@quantumomegallc
I had wondered. I tried to 0uece this together from a few sources and got conflicting implementations, probably because of the differing use cases for what they were doing. Feel free to mod the code and paste it back here if you haven't already... I'm on mobile right now and can't do much more than a quick note to self kind of thing.

Thanks for looking at my code with a critical eye. I sincerely appreciate it.

-mashume
 

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

Thread starter Similar threads Forum Replies Date
M Modified Zscore: 0.6745(xi – x̃) / MAD For ThinkOrSwim Custom 1

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
132 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