Hi, after upgrading to 3.01 one of my strategy using MMI got clearly different performance metrics in a backtest. I found that recoded MMI is the culprit. It returns different data than the previous one. So I am wondering which version is "flawed" and which one is "correct".
I would not judge this only by the changed performance metrics. I would first compare both MMI implementations against the published/reference formula. If the new 3.01 MMI differs from that formula, then the recoded version is likely wrong. If the old version differed, then the old backtest results were based on a flawed indicator. Since MMI is often used as a trend filter, even small output differences can strongly affect entries/exits and performance.
The correct MMI should follow the reference definition: calculate the median of the data window, then count cases where price is above median and continues upward, or below median and continues downward. The classic Zorro/Financial Hacker version is:
Code
var MMI(var *Data,int Length)
{
var m = Median(Data,Length);
int i, nh=0, nl=0;
for(i=1; i<Length; i++) {
if(Data[i] > m && Data[i] > Data[i-1])
nl++;
else if(Data[i] < m && Data[i] < Data[i-1])
nh++;
}
return 100.*(nl+nh)/(Length-1);
}
Zorro’s manual says MMI measures mean-reversal tendency in a 0–100% range, with random numbers around 75%, and that source code is in indicators.c.
So the practical answer is: compare both versions against this reference formula. The version matching this formula is the correct one. If 3.01 changed the median handling, indexing direction, equal-value handling, or the denominator, it can absolutely produce different backtests.
Thank you for a reply! That was the first thing I did - compared both versions and I can confirm the source code is different, and when I copy pasted the previous implementation to my custom indicators script and use it, the backtest showed the original performance metrics - confirming the recoded version indeed changed the strategy.
Original code (indicators.c):
Code
// Zorro's Market Meanness Index
var MMI(var* Data,int TimePeriod)
{
// clip time period to history length
TimePeriod = Min(TimePeriod,1000);
checkLookBack(TimePeriod);
TimePeriod = Min((uint)TimePeriod,g->nBar-1);
if(TimePeriod < 2) return 75;
// calculate MMI statistics
var m = Median(Data,TimePeriod);
int i, nh=0, nl=0;
for(i=1; i<TimePeriod; i++) {
if(Data[i] > m && Data[i] > Data[i-1])
nl++;
else if(Data[i] < m && Data[i] < Data[i-1])
nh++;
}
return 100.*(nl+nh)/(TimePeriod-1);
}
Recoded version (indicators.c):
Code
// Zorro's Market Meanness Index
var MMI(var* Data,int TimePeriod)
{
checkLookBack(TimePeriod);
// clip time period to history length
TimePeriod = Min((uint)TimePeriod,g->nBar-2);
if(TimePeriod < 2) return 75;
// calculate MMI statistics
var m = Median(Data,TimePeriod|1);
int i, nh=0, nl=0;
for(i=1; i<TimePeriod; i++) {
if(Data[i] > m && Data[i] > Data[i-1])
nl++;
else if(Data[i] < m && Data[i] < Data[i-1])
nh++;
}
return 100.*(nl+nh)/(TimePeriod-1);
}
Here are the changes
It seems like the previous version is matching the "classic" version you mentioned slightly more, probably indicating the recoded version is somehow flawed.
The change was the '|1' that prevented even time periods, so that the median always was the data value in the middle. But this should normally only produce tiny differences with negligible effect on the backtest. How large was the difference that you got? Which time periods do you use for the MMI?
It can affect backtesting when MMI is used in a trade condition, filter, optimizer, or ML feature, because the changed MMI value can change whether a signal is true or false.
The original MMI code calculates a median over TimePeriod and then counts rising/falling behavior around that median. Zorro’s indicator source shows the old version using TimePeriod = Min(TimePeriod,g->nBar-1); and Median(Data,TimePeriod). The Median function sorts the data and returns the middle value within the given period.
When the backtest can change 1. When TimePeriod is even
Old:
var m = Median(Data,TimePeriod);
New:
var m = Median(Data,TimePeriod|1);
If TimePeriod = 100, then:
TimePeriod|1 = 101
So the median is calculated from 101 values instead of 100.
That can shift the median slightly. Since MMI compares every value against the median:
if(Data[i] > m && Data[i] > Data[i-1]) nl++; else if(Data[i] < m && Data[i] < Data[i-1]) nh++;
a small median shift can change nl or nh, which changes the returned MMI value.
This matters if your strategy does something like:
if(MMI(Price,100) > 75) enterLong();
A value changing from 74.9 to 75.2 can create a trade in one version but not the other.
2. Near the beginning of the backtest or WFO cycle
Old:
TimePeriod = Min(TimePeriod,g->nBar-1);
New:
TimePeriod = Min(TimePeriod,g->nBar-2);
The new version uses one less available bar when history is short.
This can affect the first valid bars after LookBack, or the beginning of a walk-forward cycle, especially if TimePeriod is close to the available history length.
Zorro uses LookBack to execute bars before trading begins so indicators have enough history. The manual says the first bar where trades can be entered is greater than or equal to LookBack, and LookBack should cover the longest period of all used indicators, assets, and time frames.
So this change can affect backtesting mostly when:
LookBack ? MMI period
or when TimePeriod is optimized and sometimes becomes large.
3. When MMI is used as a filter
Example:
vars Price = series(priceClose()); var MeanState = MMI(Price,100);
if(MeanState > 75) enterLong();
If the old version gives:
MMI_old = 74.8
and the new version gives:
MMI_new = 75.3
then the trade only happens with the new version.
That can change:
entry timing, number of trades, profit factor, drawdown, optimized parameters, WFO results
The Zorro manual notes that indicators often become buy/sell signals when they reach thresholds, cross each other, or cross the price curve.
4. When MMI period is optimized
This is a big one.
Example:
int MMIPeriod = optimize(100,50,300,10); var M = MMI(Price,MMIPeriod);
If some optimized values are even, the new version internally turns the median length odd:
100 -> 101 120 -> 121 200 -> 201
So the optimizer may find a different best parameter than before.
Zorro’s documentation specifically warns that when optimizing an indicator time period, LookBack should be set to at least the maximum period, otherwise the backtest period can change with the optimized value and affect results unexpectedly.