preview
Reimagining Classic Strategies (Part 20): Modern Stochastic Oscillators

Reimagining Classic Strategies (Part 20): Modern Stochastic Oscillators

MetaTrader 5Examples |
243 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

The stochastic oscillator is a well-known technical indicator traditionally used to identify potential market reversals. In its classical form, it measures the momentum of price movements within a defined range. When price action pushes into extreme territories, the market is generally considered overbought or oversold. Under this conventional interpretation, traders look for selling opportunities in overbought conditions and buying opportunities in oversold conditions, based on the assumption that prices will eventually return toward equilibrium.

This approach has been used successfully for many years. However, this article suggests that the stochastic oscillator may have overlooked capabilities—specifically, that it may function more effectively as a trend-following indicator rather than strictly a mean-reversion tool. We show that, with slight adjustments to the interpretive rules, the oscillator can be repurposed into a useful method for identifying dominant market trends.

To support this, we re-examine the indicator’s signals and challenge the classical rules. We introduce an alternative framework that encourages buying in overbought conditions and selling in oversold conditions—an idea that may seem counterintuitive at first. Yet, as we demonstrate, the stochastic oscillator is more versatile than commonly assumed and is not limited to a single mode of interpretation.

In earlier analysis, we also found that the oscillator was more predictable than raw price movements. This insight motivated us to look deeper into the indicator in search of additional, previously untapped value.

To explore this fully, we present five distinct versions of a stochastic-based strategy. Although our final attempt to extract maximal value from the indicator was not successful, this result highlights a key point: four out of the five strategies performed well. This prompts us to reconsider how well we truly understand an indicator that many traders believe they already know.

Because we evaluate multiple versions of the strategy, several parameters must remain consistent across all of them. Each version will execute trades one at a time, ensuring that differences in profitability stem only from changes in the trading rules. All strategies will use the same position size, and every backtest will run over the identical historical window from 2021 through 2025. 

Figure 1: The backtest dates we will use for all versions of our strategy

Finally, each version will be tested under randomized execution delays to approximate real-world trading latency.

Figure 2: The test conditions we will test our strategies under emulate real market conditions


Establishing A Baseline

We will start off by first defining important global variables we need for our discussion.

//+------------------------------------------------------------------+
//|                                          Stochastic Strategy.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int      stoch_handler,atr_handler;
double   stoch_main_reading[],stoch_signal_reading[],atr_reading[];
double   bid,ask;

Next, we import the Trade library to assist with managing our positions.

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

When the trading application is initialized for the first time, we define the technical indicators required for our strategy.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our technical indicators
atr_handler    = iATR(Symbol(),PERIOD_D1,14);
stoch_handler  = iStochastic(Symbol(),PERIOD_D1,5,3,3,MODE_SMA,STO_LOWHIGH);
//---
   return(INIT_SUCCEEDED);
  }

When the trading application is no longer in use, we release the technical indicators that were relied upon.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(atr_handler);
   IndicatorRelease(stoch_handler);
  }

Each time new price levels are received, the appropriate indicator buffers and global variables are updated. We first implement the traditional interpretation of the stochastic oscillator: buying during oversold conditions and selling during overbought conditions.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Keep track of the time
datetime        current_time   = iTime(Symbol(),PERIOD_D1,0);
static datetime time_stamp;
   
   if(current_time != time_stamp)
      {
         //--- Update the time
         time_stamp = current_time;
         
         //--- Update our technical indicators
         CopyBuffer(stoch_handler,0,0,1,stoch_main_reading);
         CopyBuffer(stoch_handler,1,0,1,stoch_signal_reading);
         CopyBuffer(atr_handler,0,0,1,atr_reading);
         ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
         bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
         
         //--- Trading rules
         if(PositionsTotal() == 0)
            {
               if(stoch_main_reading[0] < 20) Trade.Buy(0.01,Symbol(),ask,(ask - (atr_reading[0]*2)),(ask + (atr_reading[0]*2)));
               
               if(stoch_main_reading[0] > 80) Trade.Sell(0.01,Symbol(),bid,(bid + (atr_reading[0]*2)),(bid - (atr_reading[0]*2)));
            }
      }
   
  }
//+------------------------------------------------------------------+

The equity curve produced by this benchmark is volatile and provides little confidence in the integrity of the proposed strategy. Normally, such a strategy would be abandoned, but we propose that all hope is not lost.

Figure 3: The equity curve produced by the traditional interpretation of the stochastic oscilator appear unreliable

Additionally, the strategy lost trades 49% of the time, resulting in a depreciation of investor equity over the backtest period.

Figure 4: The detailed statistics of the classical strategy leaves considerable room for improvement


Improving Beyond The Baseline

In our proposed solution, we reinterpret the indicator as a tool for trend identification. The dominant trend that we identify in any market will be defined by where current market prices fall, with respect to the observed range of the market.We begin by creating vectors to record previously observed highs and lows.

vector   high,low;

We then calculate the midpoint of the trading range over the previous 90 daily candles. The value of 90 was chosen arbitrarily, as it aligns with typical business cycles of institutional market participants that dominate the forex markets.

//--- Calculate the middle of the trading range
high.CopyRates(Symbol(),PERIOD_D1,COPY_RATES_HIGH,0,90);
low.CopyRates(Symbol(),PERIOD_D1,COPY_RATES_LOW,0,90);
double mid = ((high.Mean() + low.Mean())/2);

If no positions are open, we first seek to enter long trades during overbought price levels. In addition, we require further confirmation by observing the close price above the midpoint of the observed high–low range.

//--- Trading rules
if(PositionsTotal() == 0)
   {
      if((stoch_main_reading[0] > 80) && (iClose(Symbol(),PERIOD_D1,0) > mid)) Trade.Buy(0.01,Symbol(),ask,(ask - (atr_reading[0]*2)),(ask + (atr_reading[0]*2)));
      
      if((stoch_main_reading[0] < 20) && (iClose(Symbol(),PERIOD_D1,0) < mid)) Trade.Sell(0.01,Symbol(),bid,(bid + (atr_reading[0]*2)),(bid - (atr_reading[0]*2)));
   }

The equity curve realised under these conditions now demonstrates material upward growth when compared to the volatile and underperforming benchmark with which we began.

Figure 5: The equity curve produced by our refined application of the stochastic oscilator

The detailed statistics obtained show significant improvement. Total net profit increased dramatically to $184.35, compared to a benchmark performance of $-26.22. Additionally, the proportion of winning trades rose from 49% in the initial attempt to 55% at present.

Figure 6: The detailed statistics we have obtained from our revised strategy demonstrate the changes we made were meaningful


Reaching For Higher Performance Levels

We can still make meaningful improvements to the strategy by carefully performing candlestick analysis on lower time frames. The reasoning is that a trend observed on the daily time frame should also be reflected in the spread between the open and close prices on a lower time frame, helping us identify meaningful entry points.

vector  open,close;

On the 1-hour timeframe, we will copy the last 12 candles, to determine the dominant trend of the day. 

//--- Calculate the current trend on the lower time frame
 open.CopyRates(Symbol(),PERIOD_H1,COPY_RATES_OPEN,0,12);
 close.CopyRates(Symbol(),PERIOD_H1,COPY_RATES_CLOSE,0,12);
When combined, our revised trading rules form a filter consisting of three requirements. Each requirement reinforces the idea that a single dominant trend may be active in the market.
//--- Trading rules
if(PositionsTotal() == 0)
   {
      if((stoch_main_reading[0] > 80) && (iClose(Symbol(),PERIOD_D1,0) > mid) && (open.Mean() < close.Mean())) Trade.Buy(0.01,Symbol(),ask,(ask - (atr_reading[0]*2)),(ask + (atr_reading[0]*2)));
               
      if((stoch_main_reading[0] < 20) && (iClose(Symbol(),PERIOD_D1,0) < mid) && (open.Mean() > close.Mean())) Trade.Sell(0.01,Symbol(),bid,(bid + (atr_reading[0]*2)),(bid - (atr_reading[0]*2)));
   }

The equity curve produced by this refined strategy now grows more smoothly, in contrast to the jagged and volatile equity curve obtained from our initial attempt.

Figure 7: Our revised strategy is performing well above our previous best score

Total net profit has increased further to $223, while the Sharpe ratio improved to 0.88 from a previous value of 0.69. The total number of trades decreased from 123 to 118. A clear indication of improved efficiency is the ability to achieve the same objective with less effort. The changes implemented appear to have successfully achieved this outcome. Additionally, the percentage of winning trades increased to a new high of 56%.

Figure 8: The detailed results produced by our third iteration of our stochastic oscillator strategy


Algorithmic Discovery of Trading Rules From The Stochastic Oscillator

So far in our discussion, we have been manually defining trading rules and market filters to guide trade execution. While this process is a valuable exercise in creative thinking and market reasoning, there is a natural limit to how far human intuition alone can take us.

There may exist meaningful patterns, rules, and decision-making logic within the market data that are not immediately intuitive or easily recognized by human reasoning. To explore this possibility, we now turn our attention to algorithmic methods for discovering additional rules—specifically, rules for interpreting the stochastic oscillator.

To do this, we will write an MQL5 script that retrieves historical EUR/USD market data along with values from the Stochastic Oscillator indicator. This data will then be exported in CSV format. The dataset will include the standard open, high, low, and close (OHLC) price feeds, followed by the stochastic indicator buffers: the %K (main) line and the %D (signal) line.

Finally, we perform manual feature engineering on the stochastic data to enrich the dataset. This includes calculating the midpoint between the %K and %D lines, measuring the distance of the main reading from the 80 and 20 threshold levels, and deriving additional observations that help capture the indicator’s behavior. Together, these features form a high-dimensional representation of the stochastic oscillator, enabling more sophisticated algorithmic analysis.

//+------------------------------------------------------------------+
//|                                          Fetch Data Stochastic 2 |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- File name
string file_name = Symbol() + " Stochastic Strategy.csv";

int stoch_handler = iStochastic(Symbol(),PERIOD_CURRENT,5,3,3,MODE_EMA,STO_LOWHIGH);
double stoch_main[],stoch_signal[];
double stoch_o,stoch_h,stoch_l;

//--- Amount of data requested
input int size = 365;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

//--- 
   CopyBuffer(stoch_handler,0,0,size,stoch_main);
   stoch_o = stoch_main[0];
   stoch_h = stoch_main[0];
   stoch_l = stoch_main[0];
   ArraySetAsSeries(stoch_main,true);
   CopyBuffer(stoch_handler,1,0,size,stoch_signal);
   ArraySetAsSeries(stoch_signal,true);
   
   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
        
         FileWrite(file_handle,
                  //--- Time
                  "Time",
                   //--- OHLC
                   "Open",
                   "High",
                   "Low",
                   "Close",
                   //--- Stochastic Readings
                   "Stochastic Main",
                   "Stochastic Signal",
                   //--- Feature Engineering Stochastic Oscilator
                   "Stoch Main - Signal",
                   "Stoch M-S Mid",
                   "Stoch - 80",
                   "Stoch - 20",
                   "Stoch O",
                   "Stoch H",
                   "Stoch L",
                   "Stoch O-C",
                   "Stoch H-C",
                   "Stoch L-C"
                  );
        }

      else
        {
        
        //--- Set features
        stoch_h = (stoch_h < stoch_main[i]) ? stoch_main[i] : stoch_h;
        stoch_l = (stoch_l > stoch_main[i]) ? stoch_main[i] : stoch_l;
        
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   //--- OHLC
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   //--- Stochastic Readings
                   stoch_main[i],
                   stoch_signal[i],
                   //--- Stochastic Feature Engineering
                   stoch_main[i] - stoch_signal[i],
                   ((stoch_main[i] + stoch_signal[i])/2),
                   (stoch_main[i] - 80),
                   (stoch_main[i] - 20),
                   stoch_o,
                   stoch_h,
                   stoch_l,
                   stoch_o - stoch_main[i],
                   stoch_h - stoch_main[i],
                   stoch_l - stoch_main[i]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Analyzing Our Market Data in Python

Now that we have written out our CSV of historical market data, we are ready to analyze the data in Python. First, import the Python libraries we need.

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

Next, we read in the CSV file we wrote.

data = pd.read_csv("./EURUSD Stochastic Strategy.csv")
data

Separate the training set of observations, from the test set, which is reserved for backtesting in MetaTrader 5.

train = data.iloc[:-(365 * 5),:]
test = data.iloc[-(365 * 5):,:]

Load in the machine learning libraries we need. 

from sklearn.linear_model    import LinearRegression
from sklearn.model_selection import cross_val_score,TimeSeriesSplit

Define the forecasting horizon.

HORIZON = 5

Create a time series cross-validation object that will help us to assess the accuracy of each model we consider.

tscv = TimeSeriesSplit(n_splits=10,gap=HORIZON)

Label our dataset. We are interested in forecasting the stochastic main oscillator reading.

data['Target'] = data['Stochastic Main'].shift(-HORIZON)
data = data.iloc[:-HORIZON,:]

Let us compare the accuracy obtained by using the classical OHLC columns, the new stochastic features we have engineered, and lastly a combination of both.

X_classic = data.iloc[:,1:7].columns
X_new     = data.iloc[:,7:-1].columns
X_all     = data.iloc[:,1:-1].columns
y         = 'Target'

We will keep record of the accuracy levels we have observed as we change the inputs we feed our model.

scores = []

Keep the underlying model the same, to ensure that changes in error are coming from the inputs we have selected.

model = LinearRegression()

Record the accuracy associated with each possible set of inputs we have available.

scores.append(np.mean(np.abs(cross_val_score(model,data.loc[:,X_classic],data.loc[:,y],cv=tscv,scoring='neg_mean_squared_error'))))
scores.append(np.mean(np.abs(cross_val_score(model,data.loc[:,X_new],data.loc[:,y],cv=tscv,scoring='neg_mean_squared_error'))))
scores.append(np.mean(np.abs(cross_val_score(model,data.loc[:,X_all],data.loc[:,y],cv=tscv,scoring='neg_mean_squared_error'))))

When we plot the accuracy levels obtained by the different sets of inputs possible, we can clearly observe that the custom stochastic oscillator features we have engineered produced the lowest error levels we found possible.

sns.barplot(np.abs(scores),color='black')
plt.axhline(np.min(scores),linestyle=':',color='red')
plt.xticks([0,1,2],['OHLC Features','Custom Features','All Features'])


Figure 9: The custom features we have generated helped us best forecast the main buffer of the stochastic oscilator

In most dynamic processes, not all recorded variables are equally informative. Identifying which variables carry the most information can guide future feature engineering efforts, allowing us to focus on generating richer and more diverse variations of the most influential features. To quantify this, we use mutual information regression. Mutual information (MI) is a measure of statistical dependence that captures both linear and nonlinear relationships, making it well suited for assessing any form of dependency between two variables.

from sklearn.feature_selection import mutual_info_regression

Perform the statistical test. The test requires the new stochastic oscillator features we have generated and the present target we have selected.

scores = mutual_info_regression(data.loc[:,X_new],data.loc[:,'Target'])

It appears that from the 10 custom features we have generated, only 3 appear to have no meaningful relationship whatsoever with the target. This is inferred by the columns of MI scores that are almost 0. Therefore, it appears there is a meaningful relationship we have discovered algorithmically from the data we generated in our MQL5 script.

sns.barplot(scores,color='black')
plt.axhline(np.mean(scores),color='red',linestyle=':')


Figure 10: The MI scores we have observed suggest to us that the dataset we have engineered is meaningful and has a relationship we can learn


Exporting To ONNX

We are now ready to export our trained statistical model to the Open Neural Network Exchange (ONNX) format. ONNX enables the deployment of machine learning models without requiring the original training framework or programming language, making them portable across platforms and environments. To begin, we load the ONNX libraries required for this process.

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

Specify the input shape of our model.

initial_types = [('float input',FloatTensorType([1,len(X_new)]))]

Fit the model on the training data.

model = GradientBoostingRegressor()

model.fit(train.loc[:,X_new],train.iloc[:,-1])

Save the model as an ONNX prototype. 

onnx_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)

Save the ONNX prototype to disk, as an ONNX file.

onnx.save(onnx_proto,'EURUSD Stochastic GBR AI.onnx')


Imeplenting The Improvements

Let us now modify that trading strategy to include our ONNX file.

//+------------------------------------------------------------------+
//|                                               Stochastic AI.mq5  |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD Stochastic GBR AI.onnx" as const uchar onnx_proto[];

Define global variables we will use to handle our ONNX model in our application.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
vectorf  model_inputs,model_outputs;
long     model;

Specify system constants. These constants will specify the number of inputs and outputs our model has.

//+------------------------------------------------------------------+
//| System Definitions                                               |
//+------------------------------------------------------------------+
#define MODEL_INPUT_SHAPE  10
#define MODEL_OUTPUT_SHAPE 1

When our application is initialized, we will load our technical indicators and also keep track of the custom stochastic oscillator features we generated in the training set, such as the all time high and low readings. Then we will configure our ONNX model and ensure it is set up appropriately and working. 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our indicators
   atr_handler     = iATR("EURUSD",PERIOD_D1,14);
   stoch_handler   = iStochastic(Symbol(),PERIOD_CURRENT,5,3,3,MODE_EMA,STO_LOWHIGH);
   stoch_o = 22.69153;
   stoch_h = 98.551023;
   stoch_l = 1.372058;

//--- Setup the ONNX model
   model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT);

//--- Define the model parameter shape
   ulong input_shape[] = {1,MODEL_INPUT_SHAPE};
   ulong output_shape[] = {1,MODEL_OUTPUT_SHAPE};

   if(!OnnxSetInputShape(model,0,input_shape))
     {
      Print("ONNX Model Error: Incorrect Input Shape ",GetLastError());
      return(INIT_FAILED);
     }

   if(!OnnxSetOutputShape(model,0,output_shape))
     {
      Print("ONNX Model Error: Incorrect Output Shape ",GetLastError());
      return(INIT_FAILED);
     }

   model_inputs = vectorf::Zeros(MODEL_INPUT_SHAPE);
   model_outputs = vectorf::Zeros(MODEL_OUTPUT_SHAPE);

   if(model != INVALID_HANDLE)
     {
      return(INIT_SUCCEEDED);
     }

//---
   return(INIT_FAILED);
  }

When our trading application is no longer in use, we will release the technical indicators and the ONNX model we are no longer using.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free up memory we are no longer using when the application is off
   IndicatorRelease(atr_handler);
   IndicatorRelease(stoch_handler);
   OnnxRelease(model);
  }

If new price levels are received, we will perform our manual trading rules and then supplement them with the trading signals learned by our statistical model.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- When price levels change

   datetime current_time = iTime("EURUSD",PERIOD_D1,0);
   static datetime  time_stamp;

//--- Update the time
   if(current_time != time_stamp)
     {
     
      time_stamp = current_time;

      //--- Calculate the middle of the trading range produced over the last business cycle
      high.CopyRates(Symbol(),PERIOD_D1,COPY_RATES_HIGH,0,90);
      low.CopyRates(Symbol(),PERIOD_D1,COPY_RATES_LOW,0,90);
      double mid = ((high.Mean() + low.Mean())/2);

      //--- Calculate the current trend on the lower time frame
      open.CopyRates(Symbol(),PERIOD_H1,COPY_RATES_OPEN,0,12);
      close.CopyRates(Symbol(),PERIOD_H1,COPY_RATES_CLOSE,0,12);

      //--- Fetch indicator current readings
      CopyBuffer(atr_handler,0,0,1,atr_reading);
      CopyBuffer(stoch_handler,0,0,1,stoch_main);
      CopyBuffer(stoch_handler,1,0,1,stoch_signal);

      //--- Setting model inputs
      stoch_h = (stoch_h < stoch_main[0]) ? stoch_main[0] : stoch_h;
      stoch_l = (stoch_l > stoch_main[0]) ? stoch_main[0] : stoch_l;

      model_inputs[0] = (float)(stoch_main[0] - stoch_signal[0]);
      model_inputs[1] = (float)(((stoch_main[0] + stoch_signal[0])/2));
      model_inputs[2] = (float)((stoch_main[0] - 80));
      model_inputs[3] = (float)((stoch_main[0] - 20));
      model_inputs[4] = (float)(stoch_o);
      model_inputs[5] = (float)(stoch_h);
      model_inputs[6] = (float)(stoch_l);
      model_inputs[7] = (float)(stoch_o - stoch_main[0]);
      model_inputs[8] = (float)(stoch_h - stoch_main[0]);
      model_inputs[9] = (float)(stoch_l - stoch_main[0]);

      ask = SymbolInfoDouble("EURUSD",SYMBOL_ASK);
      bid = SymbolInfoDouble("EURUSD",SYMBOL_BID);

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {

         if(!(OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_outputs)))
           {
            Comment("Failed to obtain a forecast from our model: ",GetLastError());
           }

         else
           {
            Comment("Forecast: ",model_outputs);

            //--- Trading rules
            if((model_outputs[0] > stoch_main[0]) && (stoch_main[0] > 80) && (iClose(Symbol(),PERIOD_D1,0) > mid) && (open.Mean() < close.Mean()))
              {
               //--- Buy signal
               Trade.Buy(0.01,"EURUSD",ask,ask-(atr_reading[0] * 2),ask+(atr_reading[0] * 2),"");
              }

            else
               if((model_outputs[0] < stoch_main[0]) && (stoch_main[0] < 20) && (iClose(Symbol(),PERIOD_D1,0) < mid) && (open.Mean() > close.Mean()))
                 {
                  //--- Sell signal
                  Trade.Sell(0.01,"EURUSD",bid,bid+(atr_reading[0] * 2),bid-(atr_reading[0] * 2),"");
                 }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Lastly, undefine all system definitions you make in MQL5; this is good practice for developers.

#undef MODEL_INPUT_SHAPE
#undef MODEL_OUTPUT_SHAPE

When we observe the equity curve produced by our revised strategy, it appears we have added too much noise into the system. The curve has lost its previously smooth upward trend, and now looks more volatile than we would prefer. 

Figure 11: The equity curve we have obtained from our final iteration of our trading strategy appears to have noisy signals

Although the strategy remains profitable, its performance has declined significantly from the previous peak of $223. This does not imply that the stochastic oscillator lacks value as a foundation for statistical trading strategies; rather, it highlights the need for a more careful and rigorous methodology from the practitioner. Additionally, we can observe that the strategy placed no long trades and learned a bias for short trades.

For new readers, these results may appear unexpected. While developing our statistical model, we observed clear improvements in error metrics using custom stochastic features. However, returning readers will recognize this pattern.

As shown in our sister article series, Overcoming the Limitations of Machine Learning (Part 1): Lack of Interoperable Metrics, the statistical metrics used to train models often fail to reflect the real-world objectives of trading. Consequently, improvements in statistical error do not reliably translate into better trading performance.

In practice, modern statistical modeling is often a process akin to trial and error. Therefore, readers who achieve poor trading results despite careful analysis should not interpret this as a reflection of their lack of skill. A link to the related article is provided here. At this point, it is sound to conclude that excessive noise may have been inadvertently introduced into the trading system, and we therefore revert to version 3 as the best-performing iteration of the application.

Figure 12: The detailed statistical analysis of our final iteration of our stochastic trading strategy



Conclusion

This article demonstrates how a classical technical indicator can be repurposed beyond its conventional use. Readers gain insight into how familiar strategies can yield new value when viewed through a different analytical lens, and how new paradigms and trading rules can emerge through a thoughtful process of trial and error. Ultimately, every technical indicator in the MetaTrader 5 terminal holds untapped potential—the challenge lies in automatically uncovering the meaningful interpretations that remain hidden from view.

File Name File Description
Stochastic_Strategy.mq5  The traditional version of the stochastic oscillator strategy, it proved unprofitable during backtesting.
Stochastic_Strategy_2.mq5 Our first iteration of our application that relied on the daily range to set clear trend directions.
Stochastic_Strategy_3.mq5 The most profitable version of our trading application that used lower time frame analysis in addition to the previous changes.
Stochastic_AI.mq5 The second most profitable version of our trading strategy, it appeared to have too much noise in its signals.
Stochastic_Strategy.ipynb  The Jupyter notebook we wrote to analyze our historical EURUSD market data and custom features.
Fetch_Data_Stochastic_2.mq5 The MQL5 script we wrote to fetch OHLC EURUSD data and other custom made observations on the stochastic oscillator.
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
From Novice to Expert: Navigating Market Irregularities From Novice to Expert: Navigating Market Irregularities
Market rules are continuously evolving, and many once-reliable principles gradually lose their effectiveness. What worked in the past no longer works consistently over time. Today’s discussion focuses on probability ranges and how they can be used to navigate market irregularities. We will leverage MQL5 to develop an algorithm capable of trading effectively even in the choppiest market conditions. Join this discussion to find out more.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Creating Custom Indicators in MQL5 (Part 2): Building a Gauge-Style RSI Display with Canvas and Needle Mechanics Creating Custom Indicators in MQL5 (Part 2): Building a Gauge-Style RSI Display with Canvas and Needle Mechanics
In this article, we develop a gauge-style RSI indicator in MQL5 that visualizes Relative Strength Index values on a circular scale with a dynamic needle, color-coded ranges for overbought and oversold levels, and customizable legends. We utilize the Canvas class to draw elements like arcs, ticks, and pies, ensuring smooth updates on new RSI data.