Русский 中文 Español Deutsch 日本語 Português
preview
Implementing the Janus factor in MQL5

Implementing the Janus factor in MQL5

MetaTrader 5Examples | 4 April 2023, 16:41
4 087 0
Francis Dube
Francis Dube

Introduction

Every trader knows that market prices follow one of two broad patterns. Prices either form trends or they move horizontally. As a result  of this, market participants' strategies can largely be reduced  to being either trend following  or contrarian to one extent or another. The Janus factor is a theory of market behaviour  that captures this duality. In this article we will unpack its foundations and also demonstrate the implementation of indicators that facilitate this method of analysis.

Feedback

According  to Janus theory, markets are driven by the interplay between prices and how traders react to them. Anderson likened  this interplay  to a feedback system. Readers can learn more about feedback systems on wikipedia. When markets are trending, market participants show confidence by letting their profits ride. In an uptrend higher prices confirm participants' expectations of the trend, which in turn induces more buy-in. This has the effect of pushing prices even higher. 

fig.1 Positive FeedBack in an Uptrend

Fig.1 illustrates positive feedback in an uptrend.


In a downtrend falling prices  can instill fear in traders leading them to sell to minimize  losses. More selling puts pressure on prices pushing them lower. Trends in markets are therefore an example of positive feedback. Prices will continue to accelerate until traders' response to price changes. 

Fig.2 Poistive feedback In a Downtrend

Fig. 2 depicts positive feedback in a downtrend.

Negative feedback is seen  when traders have little confidence in the market, so they choose to book profits early after any movement in price. Early profit taking kills momentum  which limits the magnitude of price movements. Combine this with the constant tussle between bulls and bears, and the effect is a stabilization in prices.

Fig.3 Negative feed

Fig. 3 negative feedback in the market

Capital flow

An important element to this notion of feedback, are the types of symbols that traders favour when different conditions prevail. Anderson's analysis of stocks suggests that in an uptrend traders favoured stocks that showed relatively better performance  whilst divesting  from their underperforming counterparts. In a down trend underperforming stocks lost the most value as traders targeted them for shorting profits. 

In the absence of a trend, there was not much that separated stronger and weaker stocks as traders would pick specific price levels to enter and exit. With this in mind he hypothesized, that by analyzing the relative performance of a collection of stocks it would be possible to detect periods of negative and positive feedback.


Janus calculations

To ascertain performance, the periodic returns are calculated. The returns of a collection of stocks under study are  combined  to produce an average to be used as a baseline measure. Called the benchmark return. The collection of stocks under evaluation are referred to as an index.

In the book The Janus Factor -  Trend Follower's Guide to Market Dialectics, Anderson describes various metrics of stocks calculated on the basis of returns, that he uses  to gain insight into the behaviour of the market. 


The Janus Library - janus.mqh

The data and routines common to all Janus related calculations are contained in janus.mqh. The include file has declarations for three custom types:

    The CSymboldata class handles symbol data and related operations. 

    //+------------------------------------------------------------------+
    //|Class which manage the single Symbol                              |
    //+------------------------------------------------------------------+
    class CSymbolData
      {
    
    private:
       string            m_name;    // name of the symbol
       ENUM_TIMEFRAMES   m_timeframe;// timeframe
       int               m_length;  // length for copy rates
       MqlRates          m_rates[]; // store rates
       datetime          m_first;   // first date on server or local history
    
       datetime          SetFirstDate(void)
         {
          datetime first_date=-1;
          if((datetime)SymbolInfoInteger(m_name,SYMBOL_TIME)>0)
             first_date=(datetime)SeriesInfoInteger(m_name,m_timeframe,SERIES_FIRSTDATE);
          //---
          if(first_date==WRONG_VALUE || first_date==0)
            {
             if(TerminalInfoInteger(TERMINAL_CONNECTED))
               {
                while(!SeriesInfoInteger(m_name,m_timeframe,SERIES_SERVER_FIRSTDATE,first_date) && !IsStopped())
                   Sleep(10);
               }
            }
          //---
    #ifdef DEBUG
          Print(m_name," FirstDate ",first_date);
    #endif
          return first_date;
         }
    
    public:
                         CSymbolData(string name,ENUM_TIMEFRAMES tf=PERIOD_CURRENT)
         {
          m_name = name;
          m_length = 0;
          m_timeframe = tf;
          SymbolSelect(m_name,true);
         }
    
                        ~CSymbolData(void)
         {
          ArrayFree(m_rates);
         }
       datetime          GetFirstDate(void)
         {
          m_first = SetFirstDate();
          return m_first;
         }
       string            GetName(void)
         {
          return m_name;
         }
    
       int               GetLength(void)
         {
          return m_length;
         }
    
       void              SetLength(const int set_length)
         {
          if(set_length>0)
            {
             m_length=set_length;
             ArrayResize(m_rates,m_length,m_length);
             ArraySetAsSeries(m_rates,true);
            }
         }
    
       bool              Update(void)
         {
          int copied = CopyRates(m_name,m_timeframe,0,m_length,m_rates);
    #ifdef DEBUG
          Print("copied ", copied, " requested ", m_length);
    #endif
          //--
          return copied == m_length;
         };
    
       MqlRates          GetRateAtPos(const int i)
         {
          if(i<0 || i>=m_length)
            {
    #ifdef DEBUG
             Print("Array out of range ",i,".Size of array is ",m_length);
    #endif
             return (i<0)?m_rates[0]:m_rates[m_length-1];
            }
          return m_rates[i];
         }
      };
    CSymbolCollection is a container of CSymboldata objects which represents  the collection of symbols being analyzed. The code for both classes  were adapted from a listing from the Mql5 codebase. Modifications were made to the direction of indexing of the underlying buffers. It should be noted that all the classes implement right to left indexing format with zero index pointing to the latest bar. 

    To demonstrate the techniques  of the Janus factor we will apply it to the analysis of forex pairs. Anderson originally devised the method to analyze stocks, but stock symbol data availability is inconsistent amongst most brokers.

    //+------------------------------------------------------------------+
    //| Class that mange the collection of symbols                       |
    //+------------------------------------------------------------------+
    class CSymbolCollection
      {
    
    private:
    
       int               m_calculation_length; // global length
       ENUM_TIMEFRAMES   m_collection_timeframe;  // timeframe of data
       string            m_raw_symbols;    // delimited symbol list
       datetime          m_synced_first;  // synced first bar opentime for all symbols
       bool              m_synced;        // flag of whether all symbols are synchronized
       CSymbolData       *m_collection[]; // Collection of Symbol Pointer
       int               m_collection_length; // Collection of Symbol Length
       //+------------------------------------------------------------------+
       //|                                                                  |
       //+------------------------------------------------------------------+
       bool               CheckSymbolBars(const string __sym)
         {
          int bars=-1;
          bars=iBarShift(__sym,m_collection_timeframe,m_synced_first)+1;//SeriesInfoInteger(__sym,PERIOD_CURRENT,SERIES_BARS_COUNT);
    #ifdef DEBUG
          Print("Bars found in history for ",__sym," ",bars);
    #endif
          if(bars>=m_calculation_length)
             return(true);
          //---
          return(SyncSymbol(__sym));
         }
       //+------------------------------------------------------------------+
       //|                                                                  |
       //+------------------------------------------------------------------+
       bool               SyncSymbol(const string __sym)
         {
          //--- load data step by step
          bool downloaded=false;
          datetime times[1];
          int bars=-1;
          
         /* if(MQLInfoInteger(MQL_PROGRAM_TYPE)==PROGRAM_INDICATOR)
            {
    #ifdef DEBUG
             Print(" cannot download ",__sym," history from an indicator");
    #endif
             return(downloaded);
            }*/
              
    #ifdef DEBUG
          Print(" downloading ",__sym," history");
    #endif
          while(!IsStopped() && !downloaded && TerminalInfoInteger(TERMINAL_CONNECTED))
            {
             //---
             while(!SeriesInfoInteger(__sym,m_collection_timeframe,SERIES_SYNCHRONIZED) && !IsStopped())
                Sleep(5);
             //---
             bars=Bars(__sym,PERIOD_CURRENT);
             if(bars>=m_calculation_length)
               {
                downloaded=true;
                break;
               }
             //--- copying of next part forces data loading
             if(CopyTime(__sym,m_collection_timeframe,m_calculation_length-1,1,times)==1)
               {
                downloaded=true;
                break;
               }
             //---
             Sleep(5);
            }
    #ifdef DEBUG
          if(downloaded)
             Print(bars," ",__sym," bars downloaded ");
          else
             Print("Downloading ",__sym," bars failed");
    #endif
          return(downloaded);
         }
    
    public:
    
                         CSymbolCollection(const ENUM_TIMEFRAMES tf=PERIOD_CURRENT)
         {
          m_raw_symbols="";
          m_collection_length = 0;
          m_calculation_length = -1;
          m_synced_first=0;
          m_synced=false;
          m_collection_timeframe=tf;
         }
    
                        ~CSymbolCollection(void)
         {
    
          for(int i=0; i<m_collection_length; i++)
            {
             if(CheckPointer(m_collection[i])==POINTER_DYNAMIC)
                delete m_collection[i];
            }
         }
       //+------------------------------------------------------------------+
       //|return the set timeframe for bars stored in the collection        |
       //+------------------------------------------------------------------+
       ENUM_TIMEFRAMES   GetTimeFrame(void)
         {
          return(m_collection_timeframe);
         }
       
       //+------------------------------------------------------------------+
       //|Checks the history available and syncs it across all symbols      |
       //+------------------------------------------------------------------+
       bool              CheckHistory(const int size)
         {
          if(size<=0)
             return(false);
    
          int available=iBarShift(NULL,m_collection_timeframe,m_synced_first)+1;
    
          if(available<size)
             m_calculation_length=available;
          else
             m_calculation_length=size;
    
    #ifdef DEBUG
          Print("synced first date is ", m_synced_first);
          Print("Proposed size of history ",m_calculation_length);
    #endif
    
          if(m_calculation_length<=0)
             return(false);
    
          ResetLastError();
    
          for(int i=0; i<m_collection_length; i++)
            {
             m_synced=CheckSymbolBars(m_collection[i].GetName());
             if(!m_synced)
               {
                Print("Not Enough history data for ", m_collection[i].GetName(), " > ", GetLastError());
                return(m_synced);
               }
             m_collection[i].SetLength(m_calculation_length);
            }
    
          return m_synced;
    
         }
    
    
       //+------------------------------------------------------------------+
       //| Add a symbol by name to the collection                           |
       //+------------------------------------------------------------------+
       int               Add(string name)
         {
          CSymbolData *ref = new CSymbolData(name,m_collection_timeframe);
          datetime f=ref.GetFirstDate();
          int found=GetIndex(name);
          if(f==WRONG_VALUE || found>-1)
            {
    #ifdef DEBUG
             if(f==WRONG_VALUE)
                Print("Failed to retrieve information for symbol ",name,". Symbol removed from collection");
             if(found>-1)
                Print("Symbol ",name,"already part of collection");
    #endif
             delete ref;
             return(m_collection_length);
            }
          ArrayResize(m_collection, m_collection_length + 1,1);
          m_collection[m_collection_length] = ref;
          //---
          if(f>m_synced_first)
             m_synced_first=f;
          //---
    
          return(++m_collection_length);
    
         }
       //+------------------------------------------------------------------+
       //|Return symbol name                                                |
       //+------------------------------------------------------------------+
       string            GetSymbolNameAtPos(int pos)
         {
          return m_collection[pos].GetName();
         }
       //+------------------------------------------------------------------+
       //|return index of symbol                                            |
       //+------------------------------------------------------------------+
       int               GetIndex(const string symbol_name)
         {
          for(int i=0; i<m_collection_length; i++)
            {
             if(symbol_name==m_collection[i].GetName())
                return(i);
            }
          //---fail
          return(-1);
         }
       //+------------------------------------------------------------------+
       //| Return Collection length                                         |
       //+------------------------------------------------------------------+
       int               GetCollectionLength(void)
         {
          return m_collection_length;
         }
    
       //+------------------------------------------------------------------+
       //| Update every currency rates                                      |
       //+------------------------------------------------------------------+
       bool              Update(void)
         {
    
          int i;
          for(i = 0; i < m_collection_length; i++)
            {
             bool res = m_collection[i].Update();
             if(res==false)
               {
                Print("missing data on " + m_collection[i].GetName());
                return false;
               }
            }
          return true;
         }
       //+------------------------------------------------------------------+
       //|                                                                  |
       //+------------------------------------------------------------------+
       int               GetHistoryBarsLength(void)
         {
          return m_calculation_length;
         }
       //+------------------------------------------------------------------+
       //| Return MqlRates of currency at position                          |
       //+------------------------------------------------------------------+
       MqlRates          GetRateAtPos(int pos, int i)
         {
          return m_collection[pos].GetRateAtPos(i);
         }
    
       //+------------------------------------------------------------------+
       //| Return Open price of currency at position                        |
       //+------------------------------------------------------------------+
       double            GetOpenAtPos(int pos, int i)
         {
          return m_collection[pos].GetRateAtPos(i).open;
         }
    
       //+------------------------------------------------------------------+
       //| Return Close price of currency at position                       |
       //+------------------------------------------------------------------+
       double            GetCloseAtPos(int pos, int i)
         {
          return m_collection[pos].GetRateAtPos(i).close;
         }
    
       //+------------------------------------------------------------------+
       //| Return High price of currency at position                        |
       //+------------------------------------------------------------------+
       double            GetHighAtPos(int pos, int i)
         {
          return m_collection[pos].GetRateAtPos(i).high;
         }
    
       //+------------------------------------------------------------------+
       //| Return Low price of currency at position                         |
       //+------------------------------------------------------------------+
       double            GetLowAtPos(int pos, int i)
         {
          return m_collection[pos].GetRateAtPos(i).low;
         }
    
       //+------------------------------------------------------------------+
       //| Return Median price of currency at position                      |
       //+------------------------------------------------------------------+
       double            GetMedianAtPos(int pos, int i)
         {
          return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i))/2;
         }
    
       //+------------------------------------------------------------------+
       //| Return Typical price of currency at position                     |
       //+------------------------------------------------------------------+
       double            GetTypicalAtPos(int pos, int i)
         {
          return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i))/3;
         }
    
       //+------------------------------------------------------------------+
       //| Return Weighted price of currency at position                    |
       //+------------------------------------------------------------------+
       double            GetWeightedAtPos(int pos, int i)
         {
          return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i) * 2)/4;
         }
      };
    //+------------------------------------------------------------------+
    

     

    The code begins with three custom enumerations documented below.

    #include<Math\Stat\Math.mqh>
    #include <Arrays\ArrayObj.mqh>
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    enum ENUM_INDEX_TYPE
      {
       INDEX_FOREX_MAJORS=0,//forex majors only list
       INDEX_CUSTOM,//custom symbol list
      };
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    enum ENUM_PRICE
      {
       CLOSE=0,//close price
       MEDIAN,//median price
       TYPICAL,//typical price
       WEIGHTED//weighted price
      };
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    enum ENUM_DIFF_TYPE
      {
       DIFF_PERCENT=0,//percent difference
       DIFF_LOG//log difference
      };
    
    
    Enumeration
    Details
    Options
    ENUM_INDEX_TYPE
    represents the type of symbols included in a collection to be analyzed
    INDEX_FOREX -  this option builds a collection of symbols made up of all forex majors. A set of 28 symbols.
    INDEX_CUSTOM - with this option the collection of symbols needs to be specified.
    ENUM_PRICE
    allow selection of the price series to be used in calculations. It should be noted that price information is  stored as series with the latest prices at index 0.

    CLOSE - close price
    MEDIAN - the median price - high+low/2
    TYPICAL - the typical price - high+low+close/3
    WEIGHTED -  the weighted price high+low+close+close/4

     ENUM_DIFF_TYPE
     this represents distinct methods of differencing that can be applied on the selected price series.
    DIFF_LOG - here the log difference is used
    DIFF_PERCENT - the percent difference is used for this option

    To use the CJanus class there are 5 methods that must be understood. After creating a CJanus object and before any method can be called the, Initialize member function should be invoked first. The method does two important things, first it initializes the CSymbolCollection object and populates it with the selected symbols that will make up our index. It then checks and synchronizes the history bars of symbols in the collection.

    //+------------------------------------------------------------------+
    //|Janus class for calculating janus family of indicators.           |
    //+------------------------------------------------------------------+
    class CJanus
      {
    private:
       CSymbolCollection* m_symbol_list;    //object container of symbols
       ENUM_PRICE         m_price_type;     //applied price for calculations
       ENUM_DIFF_TYPE     m_diff_type;      //method of differencing applied
       ENUM_INDEX_TYPE    m_index_type;     //type of index
       int                m_hist_size;      // synchronized size of history across all selected symbols in collection
       ENUM_TIMEFRAMES    m_list_timeframe; //timeframe for bars to be used in calculations
       //---private methods
       double            market_return(const uint barshift, const uint symbolshift);
       void              market_offense_defense(const uint barshift,const uint symbolshift,const uint rs_period,double& out_offense,double& out_defense);
       double            rs(const uint barshift,const uint symbolshift,const uint rs_period,uint lag=0);
       double            rs_off_def(const uint barshift, const uint symbolshift,const uint lag,const double median,const double index_offense, const double index_defense, double &array[]);
       //---
    public:
       //constructor
                         CJanus(void):m_symbol_list(NULL),
                         m_price_type(WRONG_VALUE),
                         m_diff_type(WRONG_VALUE),
                         m_index_type(WRONG_VALUE),
                         m_list_timeframe(WRONG_VALUE),
                         m_hist_size(0)
         {
    
         }
       // destructor
                        ~CJanus(void)
         {
          if(CheckPointer(m_symbol_list)==POINTER_DYNAMIC)
             delete m_symbol_list;
         }
       //public methods
       bool              Initialize(const ENUM_PRICE set_price_type, const ENUM_DIFF_TYPE set_diff_type, const ENUM_INDEX_TYPE set_index_type, ENUM_TIMEFRAMES set_timeframe,const int history_size, const string symbol_list);
       bool              Update(void);
       int               HistorySize(void);
       string            GetSymbolAt(const int sym_ind);
       int               GetSymbolsTotal(void);
       double            CalculateReturn(const uint barshift, const string symbol__);
       double            CalculateBenchMarkReturn(const uint barshift);
       double            CalculateSummedIndexReturn(const uint barshift);
       void              CalculateBenchMarkOffenseDefense(const uint barshift,const uint rs_period,double& out_offense,double& out_defense);
       void              CalculateMarketOffenseDefense(const uint barshift,const string symbol__,const uint rs_period,double& out_offense,double& out_defense);
       double            CalculateRelativeStrength(const uint barshift,const string symbol__,const uint rs_period,uint lag=0);
       void              CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard);
       double            CalculateRelativeStrengthSpread(const uint barshift,const uint rs_period,const double rs_percent_top);
       double            CalculateRelativeStrengthSpreadChange(const uint barshift,const uint rs_period,const double rs_percent_top);
    
      };

    The input parameters of the Initialize() method are explained in the table below.

    Parameter
    Details
     Data type
    set_price_type
    an enumeration that sets the underlying price series to  be used for all calculations
    ENUM_PRICE
    set_diff_type
    sets the method of differencing applied to the price series
    ENUM_DIFF_TYPE
    set_index_type
    sets the type of symbol collection either custom or forex majors only, if custom is selected then the symbols have to be specified in the symbol_list parameters
    ENUM_INDEX_TYPE
    set_timeframe
    specifies the timeframe of the bars to be used in calculations
    ENUM_TIMEFRAME

    history_size
    defines the maximum number of bars that should be requested. Since we are dealing with multiple symbols, this is a way to ensure that all symbols in the collection have the same number of historical bar data available.
    integer

     symbol_list
    when set_index_type is set to INDEX_CUSTOM the user has to specify  a comma delimited list of symbols that will make up the collection of symbols to be analyzed
     string
    Other notable methods are :
    •  The Update() method refreshes symbol data to retrieve the latest quotes.
    • The HistorySize() method returns the number of history bars available. Note that this number can be less than what is specified when invoking the Initialize method. This is because it is possible that history across all symbols may vary, therefore the function returns the synchronized history size.
    • GetSymbolsTotals() returns the number of symbols that have been added and confirmed to be available in the terminal.
    • GetSymbolAt() gets the symbol name by index.

    The rest of the methods that are prefixed with Calculate, perform computations and return either one or two values. They all have as their first input parameter the barshift for the bar a calculation will be performed on.
    Each method will be explained as various indicators are implemented.


    Measuring performance

    As already mentioned performance is measured by calculating the returns. For our implementation  we have the option to select the applied price and the method used for calculating the returns. Calling the CalculateReturns() method computes the returns for the given barshift and symbol.

    //+------------------------------------------------------------------+
    //|private method that calculates the returns                        |
    //+------------------------------------------------------------------+
    double CJanus::market_return(const uint barshift, const uint symbolshift)
      {
       double curr,prev;
       curr=0;
       prev=1.e-60;
    
       switch(m_price_type)
         {
          case CLOSE:
             curr=m_symbol_list.GetCloseAtPos(symbolshift,barshift);
             prev=m_symbol_list.GetCloseAtPos(symbolshift,barshift+1);
             break;
          case MEDIAN:
             curr=m_symbol_list.GetMedianAtPos(symbolshift,barshift);
             prev=m_symbol_list.GetMedianAtPos(symbolshift,barshift+1);
             break;
          case TYPICAL:
             curr=m_symbol_list.GetTypicalAtPos(symbolshift,barshift);
             prev=m_symbol_list.GetTypicalAtPos(symbolshift,barshift+1);
             break;
          case WEIGHTED:
             curr=m_symbol_list.GetWeightedAtPos(symbolshift,barshift);
             prev=m_symbol_list.GetWeightedAtPos(symbolshift,barshift+1);
             break;
          default:
             return WRONG_VALUE;
         }
    
       if(prev==0)
          return(WRONG_VALUE);
    
       switch(m_diff_type)
         {
          case DIFF_PERCENT:
             return(((curr-prev)/prev)*100);
          case DIFF_LOG:
             return(MathLog(curr/prev));
          default:
             return(WRONG_VALUE);
         }
      }
    //+------------------------------------------------------------------+
    //|public method to calculate returns for single bar                 |
    //+------------------------------------------------------------------+
    double CJanus::CalculateReturn(const uint barshift, const string symbol_)
      {
       int sshift=m_symbol_list.GetIndex(symbol_);
       if(sshift>-1)
          return(market_return(barshift,sshift));
       else
          return(WRONG_VALUE);
      }

    The code for the IndexReturns indicator below displays the returns for the chart symbol and also the benchmark returns.

    //+------------------------------------------------------------------+
    //|                                                 IndexReturns.mq5 |
    //|                        Copyright 2023, MetaQuotes Software Corp. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2023, MetaQuotes Software Corp."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 2
    #property indicator_plots   2
    //--- plot IndexReturns
    #property indicator_label1  "IndexReturns"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrRed
    #property indicator_style1  STYLE_SOLID
    #property indicator_width1  1
    //--- plot Returns
    #property indicator_label2  "Returns"
    #property indicator_type2   DRAW_LINE
    #property indicator_color2  clrBlue
    #property indicator_style2  STYLE_SOLID
    #property indicator_width2  1
    #include<Janus.mqh>
    //inputs
    input ENUM_PRICE AppliedPrice=CLOSE;
    input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG;
    input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS;
    input string BenchMarkSymbols="";
    input int MaxBars = 300;
    //--- indicator buffers
    double         IndexReturnsBuffer[];
    double         ReturnsBuffer[];
    CJanus *janus;
    
    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- indicator buffers mapping
       SetIndexBuffer(0,IndexReturnsBuffer,INDICATOR_DATA);
       SetIndexBuffer(1,ReturnsBuffer,INDICATOR_DATA);
      
       ArraySetAsSeries(IndexReturnsBuffer,true);
       ArraySetAsSeries(ReturnsBuffer,true);  
    //--- 
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0);
       PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0);
       PlotIndexSetString(1,PLOT_LABEL,_Symbol+" Returns");
    //---
       IndicatorSetInteger(INDICATOR_DIGITS,8);
       IndicatorSetString(INDICATOR_SHORTNAME,"IndexReturns("+_Symbol+")");
    //---
       janus=new CJanus();   
    //--- 
       if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols))
         return(INIT_FAILED);     
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //|Custom indicator deinitialization function                        |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
     {
    //--- 
      switch(reason)
       {
        case REASON_INITFAILED:
          ChartIndicatorDelete(ChartID(),ChartWindowFind(),"IndexReturns("+_Symbol+")");
           break;
        default:
           break;
       } 
    //---
       if(CheckPointer(janus)==POINTER_DYNAMIC)
          delete janus;       
     }   
    //+------------------------------------------------------------------+
    //| Custom indicator iteration function                              |
    //+------------------------------------------------------------------+
    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[])
      {
    //---
       int limit;
       if(prev_calculated<=0)
         {
          limit=janus.HistorySize()-2;
          PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1);
          PlotIndexSetInteger(1,PLOT_DRAW_BEGIN,rates_total-limit+1);
         } 
       else 
         limit=rates_total-prev_calculated;
       //---  
       if(!janus.Update())
         return(prev_calculated); 
         
       for(int bar=limit;bar>=1;bar--)
        {
         ReturnsBuffer[bar]=janus.CalculateReturn(bar,_Symbol);
         IndexReturnsBuffer[bar]=janus.CalculateBenchMarkReturn(bar);
        }    
    //--- return value of prev_calculated for next call
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    The IndexReturns indicator

    IndexReturns Indicator

    The benchmark

    To calculate the benchmark return we use the individual returns of symbols for each corresponding time point to get the median value. This calculation is implemented by the CalculateBenchmarkReturns() method. 

    //+------------------------------------------------------------------+
    //|public method to calculate index returns                          |
    //+------------------------------------------------------------------+
    double CJanus::CalculateBenchMarkReturn(const uint barshift)
      {
       double sorted[];
       int size=m_symbol_list.GetCollectionLength();
    
       if(size<=0)
          return(WRONG_VALUE);
    
       ArrayResize(sorted,size);
    
       for(int i=0; i<size; i++)
         {
          sorted[i]=market_return(barshift,i);
         }
    
       if(!ArraySort(sorted))
         {
          Print("sorting error ",__LINE__," ",__FUNCTION__);
          return(0);
         }
    
       return(MathMedian(sorted));
      }


    Offense and defense

    In order to get a better sense of relative performance, Anderson came up with the notion of offensive and defensive scores for a symbol. The offensive score is the performance achieved when benchmark returns are above average. 

    The defensive score is calculated similarly using the returns achieved when benchmark returns are less than average. This value shows how well a symbol holds up when market prices are choppy. 

    To calculate these values the benchmark returns are partitioned into two: the offensive and defensive benchmark returns. These are calculated by selecting a suitable window length from which an average benchmark is computed. 

    In the CJanus class the median value is used as an average over the specified window. The offensive benchmark return is arrived at by accumulating the difference between the achieved returns and the average benchmark returns, for those benchmarks larger than or equal to the average.

    The defensive benchmark return is calculated similarly using benchmark values less than the window's average.

    //+------------------------------------------------------------------+
    //|public method to calculate Index offense and defense scores       |
    //+------------------------------------------------------------------+
    void CJanus::CalculateBenchMarkOffenseDefense(const uint barshift,const uint rs_period,double& out_offense,double& out_defense)
      {
       out_defense=out_offense=WRONG_VALUE;
    
       double index[],sorted[],median,i_offense,i_defense;
       median=i_offense=i_defense=0;
    
       ArrayResize(index,rs_period);
       ArrayResize(sorted,rs_period);
    
       int begin=0;
    
       for(int i=0; i<(int)rs_period; i++)
         {
          index[i]=CalculateBenchMarkReturn(barshift+i);
          if(i>=begin)
             sorted[i-begin]=index[i];
         }
    
       if(!ArraySort(sorted))
         {
          Print("sorting error ",__LINE__," ",__FUNCTION__);
          return;
         }
    
       median=MathMedian(sorted);
    
       i_offense=1.e-30;
       i_defense=-1.e-30;
    
       for(int i=begin; i<(int)rs_period; i++)
         {
          if(index[i]>=median)
             i_offense+=index[i]-median;
          else
             i_defense+=index[i]-median;
         }
    
       if(i_offense<0 || i_defense>0)
         {
    #ifdef DEBUG
          Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense);
    #endif
          return;
         }
    
       out_offense=i_offense;
       out_defense=i_defense;
    
       return;
      }

    Once the offensive and defensive benchmark returns have been calculated. They are used to calculate the offensive and defensive scores for a symbol. Over the same window used for the benchmarks, every time point where the benchmark return is larger than or equal to the window average, the achieved returns are added together. This summation is then expressed as a fraction of the corresponding offensive benchmark and multiplied by 100. 

    A similar computation is done using the defensive benchmark return. The CalculateSymbolOffenseDefense() method implements the calculation of both offensive and defensive scores.

    //+------------------------------------------------------------------+
    //|public method to calculate market offense and defense                                                                  |
    //+------------------------------------------------------------------+
    void CJanus::CalculateSymbolOffenseDefense(const uint barshift,const string symbol__,const uint rs_period,double &out_offense,double &out_defense)
      {
       out_defense=out_offense=0.0;
       int sshift=m_symbol_list.GetIndex(symbol__);
       if(sshift>-1)
          market_offense_defense(barshift,sshift,rs_period,out_offense,out_defense);
       else
          return;
      }
    //+------------------------------------------------------------------+
    //|private method that calculates market defense and offense values  |
    //+------------------------------------------------------------------+
    void CJanus::market_offense_defense(const uint barshift,const uint symbolshift,const uint rs_period,double& out_offense,double& out_defense)
      {
       out_defense=out_offense=0.0;
    
       double index[],sorted[],median,i_offense,i_defense;
       median=i_offense=i_defense=0;
    
       ArrayResize(index,rs_period);
       ArrayResize(sorted,rs_period);
    
       int begin=0;
    
       for(int i=0; i<(int)rs_period; i++)
         {
          index[i]=CalculateBenchMarkReturn(barshift+i);
          if(i>=begin)
             sorted[i-begin]=index[i];
         }
    
       if(!ArraySort(sorted))
         {
          Print("sorting error ",__LINE__," ",__FUNCTION__);
          return;
         }
    
       median=MathMedian(sorted);
    
       i_offense=1.e-30;
       i_defense=-1.e-30;
    
       for(int i=begin; i<(int)rs_period; i++)
         {
          if(index[i]>=median)
             i_offense+=index[i]-median;
          else
             i_defense+=index[i]-median;
         }
    
       if(i_offense<0 || i_defense>0)
         {
    #ifdef DEBUG
          Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense);
    #endif
          return;
         }
    
       double m_offense,m_defense;
       m_offense=m_defense=0;
    
       for(int i=0; i<(int)rs_period; i++)
         {
          if(index[i]>=median)
             m_offense+=market_return(barshift+i,symbolshift);
          else
             m_defense+=market_return(barshift+i,symbolshift);
         }
    
       out_defense= (m_defense/i_defense) * 100;
       out_offense= (m_offense/i_offense) * 100;
    
      }

    By plotting offensive and defensive scores, Anderson noticed the patterns that would emerge depending on prevailing market conditions. During times of positive feedback, the scores were widely dispersed with a significant difference between symbols with the best and worst performance. As opposed to periods  of negative feedback when  the difference between the best and worst performers narrowed. Anderson referred to this as expansion and contraction of the index.

    The code below describes the OffensiveDefensiveScatterPlot script, it has similar inputs as the indicator above and draws plots of offensive and defensive scores as an animation.

    //+------------------------------------------------------------------+
    //|                                OffensiveDefensiveScatterPlot.mq5 |
    //|                        Copyright 2023, MetaQuotes Software Corp. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2023, MetaQuotes Software Corp."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property script_show_inputs
    #include <Graphics\Graphic.mqh>
    #include<Janus.mqh>
    
    input ENUM_PRICE AppliedPrice=CLOSE;
    input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG;
    input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS;
    input uint AppliedPeriod = 25;
    input string BenchMarkSymbols="";
    input int MaxBars = 50;
    
    CJanus janus;
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //---
       if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols))
         {
          Print("error with janus object");
          return;
         }
    //---
       janus.Update();
    //---
       double y[];
       double x[];
    
       int size=janus.GetSymbolsTotal();
    
       ArrayResize(x,size);
       ArrayResize(y,size);
    
       long chart=0;
       string name="OffenseDefense";
    
       for(int k=MaxBars-(int)AppliedPeriod-1; k>=0; k--)
         {
          for(int i=0; i<size; i++)
            {
             string ssy=janus.GetSymbolAt(i);
             janus.CalculateSymbolOffenseDefense(k,ssy,AppliedPeriod,y[i],x[i]);
            }
    
          CGraphic graphic;
          if(ObjectFind(chart,name)<0)
             graphic.Create(chart,name,0,0,0,780,380);
          else
             graphic.Attach(chart,name);
          //---
          graphic.CurveAdd(x,y,ColorToARGB(clrBlue),CURVE_POINTS,"DefensiveOffensive ");
          //---
          graphic.CurvePlotAll();
          //---
          graphic.Update();
          Sleep(1*1000);
          graphic.Destroy();
          ChartRedraw();
    
         }
    
       ChartSetInteger(0,CHART_SHOW,true);
    
      }
    //+------------------------------------------------------------------+
    

    The script produces the graphic below

    Offense/Defense scatter plot


    Relative Strength

    When the offensive and defensive scores of a symbol are linked using the formula below we get the relative strength of a symbol.

    Relative Strength Formula

     Derivation of this formula is documented in Anderson's book. Relative strength is computed by the CalculateRelativeStrength() method.

    //+------------------------------------------------------------------+
    //|public method to calculate relative strength                      |
    //+------------------------------------------------------------------+
    double CJanus::CalculateRelativeStrength(const uint barshift,const string symbol__,const uint rs_period,uint lag=0)
      {
       int sshift=m_symbol_list.GetIndex(symbol__);
       if(sshift>-1)
          return(rs(barshift,sshift,rs_period,lag));
       else
          return(WRONG_VALUE);
      }
    


    //+------------------------------------------------------------------+
    //|private method that calculates the relative strength              |
    //+------------------------------------------------------------------+
    double CJanus::rs(const uint barshift,const uint symbolshift,const uint rs_period,uint lag=0)
      {
       if(lag>=rs_period)
          return(WRONG_VALUE);
    
       double index[],sorted[],median,i_offense,i_defense;
       median=i_offense=i_defense=0;
    
       ArrayResize(index,rs_period);
       ArrayResize(sorted,rs_period);
    
       int begin=(int)lag;
    
       for(int i=0; i<(int)rs_period; i++)
         {
          index[i]=CalculateBenchMarkReturn(barshift+i);
          if(i>=begin)
             sorted[i-begin]=index[i];
         }
    
       if(!ArraySort(sorted))
         {
          Print("sorting error ",__LINE__," ",__FUNCTION__);
          return(EMPTY_VALUE);
         }
    
       median=MathMedian(sorted);
    
       i_offense=1.e-30;
       i_defense=-1.e-30;
    
       for(int i=begin; i<(int)rs_period; i++)
         {
          if(index[i]>=median)
             i_offense+=index[i]-median;
          else
             i_defense+=index[i]-median;
         }
    
       if(i_offense<0 || i_defense>0)
         {
    #ifdef DEBUG
          Print("error invalid figures ","Offensive ",i_offense," Defensive ",i_defense);
    #endif
          return(WRONG_VALUE);
         }
    
       return(rs_off_def(barshift,symbolshift,lag,median,i_offense,i_defense,index));
      }

    The indicator code below plots the relative strength for a chart symbol

    //+------------------------------------------------------------------+
    //|                                             RelativeStrength.mq5 |
    //|                        Copyright 2023, MetaQuotes Software Corp. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2023, MetaQuotes Software Corp."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //--- plot RelativeStrength
    #property indicator_label1  "RelativeStrength"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrRed
    #property indicator_style1  STYLE_SOLID
    #property indicator_width1  1
    #include<Janus.mqh>
    //inputs
    input ENUM_PRICE AppliedPrice=CLOSE;
    input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG;
    input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS;
    input uint AppliedPeriod = 7;
    input string BenchMarkSymbols="";
    input int MaxBars = 300;
    //--- indicator buffers
    double         RelativeStrengthBuffer[];
    //---
    CJanus *janus;
    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- indicator buffers mapping
       SetIndexBuffer(0,RelativeStrengthBuffer,INDICATOR_DATA);
    //---    
       ArraySetAsSeries(RelativeStrengthBuffer,true);
    //---   
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0);
       IndicatorSetString(INDICATOR_SHORTNAME,"RS("+_Symbol+")("+string(AppliedPeriod)+")");
    //---
       janus=new CJanus();   
    //--- 
       if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols))
         return(INIT_FAILED);     
    //---
       
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //|Custom indicator deinitialization function                        |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
     {
    //--- 
      switch(reason)
       {
        case REASON_INITFAILED:
          ChartIndicatorDelete(ChartID(),ChartWindowFind(),"RS("+_Symbol+")("+string(AppliedPeriod)+")");
           break;
        default:
           break;
       } 
    //---
       if(CheckPointer(janus)==POINTER_DYNAMIC)
          delete janus;       
     }     
    //+------------------------------------------------------------------+
    //| Custom indicator iteration function                              |
    //+------------------------------------------------------------------+
    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],p
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[])
      {
    //---
       int limit;
       if(prev_calculated<=0)
         {
          limit=janus.HistorySize()-int(AppliedPeriod+2);
          PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1);
         } 
       else 
         limit=rates_total-prev_calculated;
       //---  
       if(!janus.Update())
         return(prev_calculated); 
         
       for(int i=limit;i>=1;i--)
        {
         RelativeStrengthBuffer[i]=janus.CalculateRelativeStrength(i,_Symbol,AppliedPeriod);
        } 
    //--- return value of prev_calculated for next call
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    The Relative Strength Indicator

    RelativeStrength Indicator

    Using relative strength values we get a clearer picture of expansion and contraction as it occurs over time. This is shown below by the indicator plot of the highest and lowest relative strength values. 

    Upper and lower RS

    The code for this indicator is shown below.

    //+------------------------------------------------------------------+
    //|                                    RelativeStrenghtBestWorst.mq5 |
    //|                        Copyright 2023, MetaQuotes Software Corp. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2023, MetaQuotes Software Corp."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 2
    #property indicator_plots   2
    //--- plot Upper
    #property indicator_label1  "Upper"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrBlue
    #property indicator_style1  STYLE_SOLID
    #property indicator_width1  1
    //--- plot Lower
    #property indicator_label2  "Lower"
    #property indicator_type2   DRAW_LINE
    #property indicator_color2  clrRed
    #property indicator_style2  STYLE_SOLID
    #property indicator_width2  1
    #include<Janus.mqh>
    //inputs
    input ENUM_PRICE AppliedPrice=CLOSE;
    input ENUM_DIFF_TYPE AppliedDiffType=DIFF_LOG;
    input ENUM_INDEX_TYPE SelectIndexType=INDEX_FOREX_MAJORS;
    input uint AppliedPeriod = 25;
    input string BenchMarkSymbols="";
    input int MaxBars = 300;
    //--- indicator buffers
    double         UpperBuffer[];
    double         LowerBuffer[];
    double rsv[];
    
    CJanus *janus;
    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- indicator buffers mapping
       SetIndexBuffer(0,UpperBuffer,INDICATOR_DATA);
       SetIndexBuffer(1,LowerBuffer,INDICATOR_DATA);
    //---   
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0);
       PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0);
    //--- 
       ArraySetAsSeries(UpperBuffer,true);
       ArraySetAsSeries(LowerBuffer,true);
    //---    
       IndicatorSetString(INDICATOR_SHORTNAME,"RS_Upperlower("+_Symbol+")("+string(AppliedPeriod)+")");
    //---   
       janus=new CJanus();   
    //--- 
       if(!janus.Initialize(AppliedPrice,AppliedDiffType,SelectIndexType,PERIOD_CURRENT,MaxBars,BenchMarkSymbols))
         return(INIT_FAILED);  
         
       ArrayResize(rsv,janus.GetSymbolsTotal());     
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //|Custom indicator deinitialization function                        |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
     {
    //--- 
      switch(reason)
       {
        case REASON_INITFAILED:
          ChartIndicatorDelete(ChartID(),ChartWindowFind(),"RS_Upperlower("+_Symbol+")("+string(AppliedPeriod)+")");
           break;
        default:
           break;
       } 
    //---
       if(CheckPointer(janus)==POINTER_DYNAMIC)
          delete janus;       
     }       
    //+------------------------------------------------------------------+
    //| Custom indicator iteration function                              |
    //+------------------------------------------------------------------+
    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[])
      {
    //---
       int limit;
       if(prev_calculated<=0)
         {
          limit=janus.HistorySize()-int(AppliedPeriod+2);
          PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,rates_total-limit+1);
         } 
       else 
         limit=rates_total-prev_calculated;
       //---  
       if(!janus.Update())
         return(prev_calculated); 
         
       for(int i=limit;i>=1;i--)
        {
          for(int k=0;k<ArraySize(rsv);k++)
            {
             string sym=janus.GetSymbolAt(k);
             rsv[k]= janus.CalculateRelativeStrength(i,sym,AppliedPeriod);
            }
            
          ArraySort(rsv);
          
         UpperBuffer[i]=rsv[ArraySize(rsv)-1];
         LowerBuffer[i]=rsv[0];    
        } 
    //--- return value of prev_calculated for next call
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    


    Relative strength leaders and laggards

    When the relative performance, given by the returns, of the best and worst performers are averaged we get the relative strength leaders and relative strength laggards respectively. These values give an indication of the best times to trade depending on the state of feedback. The relative strength (rs) leaders and laggards are calculated by specifying the number of the best and worst performers to work out an average. 

    //+------------------------------------------------------------------+
    //|public method to calculate relative strength leaders and laggards |
    //+------------------------------------------------------------------+
    void CJanus::CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard)
      {
       leader=laggard=0;
       
       uint lag=rs_period;
    
       double sorted[];
       int iwork[],k,n,isub;
       k=isub=-1;
    
       int size=m_symbol_list.GetCollectionLength();
    
       ArrayResize(sorted,size);
       ArrayResize(iwork,size);
    
       for(int i=0; i<size; i++)
         {
          sorted[i]=rs(barshift,uint(i),rs_period,lag);
          iwork[i]=i;
         }
    
       MathQuickSortAscending(sorted,iwork,0,size-1);
    
       k=(int)(rs_percent_top*(size+1))-1;
       if(k<0)
          k=0;
       n=k+1;
    
       while(k>=0)
         {
          isub=iwork[k];
          for(uint i=0; i<lag; i++)
            {
             laggard+=market_return(barshift+i,isub);
            }
          isub=iwork[size-1-k];
          for(uint i=0; i<lag; i++)
            {
             leader+=market_return(barshift+i,isub);
            }
          --k;
         }
       leader/=n*lag;
       laggard/=n*lag;
    
       return;
      }

    The rs_percent_top input for the CalculateRelativeStrengthLeadersLaggards() method denotes the fraction of symbols used to calculate averaged relative strength. For example setting rs_percent_top to 0.1 means the top and bottom 10% symbols will be used in the calculation of the leaders and laggards. 

    Below is a screenshot of the Leaders and Laggards indicator

    Leaders and Laggards


    The Spread

    When the markets are gripped by positive feedback the weakest and strongest securities diverge depending on the direction of the trend. In an uptrend traders favour stronger securities and in a down trend the weaker securities are shorted.

    If prices are stuck in a range there is relatively little difference between the weakest and strongest securities, there is convergence. In order recognize these developments we calculate the average difference in relative strength between the weakest and strongest securities. This is what the relative strength spread measures.

    The relative strength spread is implemented in the CalculateRelativeStrengthSpread() method.

    //+------------------------------------------------------------------+
    //|public method to calculate the relative strength spread.          |
    //+------------------------------------------------------------------+
    double CJanus::CalculateRelativeStrengthSpread(const uint barshift,const uint rs_period,const double rs_percent_top)
      {
       double sorted[],width,div;
       int k=0;
       int size=m_symbol_list.GetCollectionLength();
    
       width=div=0;
       ArrayResize(sorted,size);
    
       for(int i=0; i<size; i++)
         {
          sorted[i]=rs(barshift,uint(i),rs_period);
         }
    
       if(!ArraySort(sorted))
         {
          Print("sorting error ",__LINE__," ",__FUNCTION__);
          return(WRONG_VALUE);
         }
    
       k=(int)(rs_percent_top*(size+1))-1;
    
       if(k<0)
          k=0;
       double n=double(k+1);
    
       while(k>=0)
         {
          width+=sorted[size-1-k]-sorted[k];
          --k;
         }
    
       return(width/=n);
      }

    The relative strength spread indicator

    RelativeStrengthSpread Indicator

    The combination of spread and rs leader/laggards  indicators can be used to develop tangible trading rules. According to Anderson the best opportunities come from trading symbols that are near or at the upper and lower bounds of relative strength for the index under study.

    //+------------------------------------------------------------------+
    //|public method to calculate relative strength leaders and laggards |
    //+------------------------------------------------------------------+
    void CJanus::CalculateRelativeStrengthLeaderLaggard(const uint barshift,const uint rs_period,const double rs_percent_top,double& leader,double& laggard)
      {
       leader=laggard=0;
       
       uint lag=1;
    
       double sorted[];
       int iwork[],k,n,isub;
       k=isub=-1;
    
       int size=m_symbol_list.GetCollectionLength();
    
       ArrayResize(sorted,size);
       ArrayResize(iwork,size);
    
       for(int i=0; i<size; i++)
         {
          sorted[i]=rs(barshift,uint(i),rs_period,lag);
          iwork[i]=i;
         }
    
       MathQuickSortAscending(sorted,iwork,0,size-1);
    
       k=(int)(rs_percent_top*(size+1))-1;
       if(k<0)
          k=0;
       n=k+1;
    
       while(k>=0)
         {
          isub=iwork[k];
          for(uint i=0; i<lag; i++)
            {
             laggard+=market_return(barshift+i,isub);
            }
          isub=iwork[size-1-k];
          for(uint i=0; i<lag; i++)
            {
             leader+=market_return(barshift+i,isub);
            }
          --k;
         }
       leader/=n*lag;
       laggard/=n*lag;
    
       return;
      }


    When the trend of the spread is rising in combination with the rs leaders it is an indication of positive feedback along with net buying of strong performers. An opportune time to buy symbols showing the highest relative strength. If the rs laggards are rising instead, then target the weaker  symbols  for a short. In any of these situations it may be prudent to combine these rules  with an effective  entry method, that will be applied  to the chosen symbol(s). 

    Another option is to use the spread indicator, as a filter for risk. When there is positive feedback the market is less risky, with negative feedback being an indicator of possible choppy conditions. A time to probably stay out of the market. 


      Conclusion

       Although the analysis of forex symbols may not be the best application of the Janus factor, the whole point of the article is to familiarize readers with the method. More information about it can be found in Anderson's book referenced below. 

      The attached zip file contains all the code for the tools referenced in the article. The table below lists each one.

      For the indicators to work users should first make sure that history data for all symbols that make up the index is available.

      Filename Type Description
      Mql5/include/Janus.mqh Include  Include file containing definition of CSymbolData, CSymbolCollection and CJanus classes
      Mql5/scripts/OffensiveDefensiveScatterPlot.mq5 Script Script that draws animated scatter plot of offensive/defensive scores
      Mql5/indicators/IndexReturns.mq5 Indicator Code for indicator displaying symbol and benchmark returns
      Mql5/indicators/RelativeStrengthBestWorst.mq5
      Indicator Indicator showing plot of the highest and lowest  relative strength values.
      Mql5/indicators/RelativeStrength.mq5
      Indicator Indicator displaying relative strength for a symbol
      Mql5/indicators/RelativeStrengthSpread.mq5 Indicator Indicator displaying relative strength spread
      Mql5/indicatorr/RssLeaderlaggards.mq5 Indicator Indicator that plots the RS leaders and laggards



      How to use ONNX models in MQL5 How to use ONNX models in MQL5
      ONNX (Open Neural Network Exchange) is an open format built to represent machine learning models. In this article, we will consider how to create a CNN-LSTM model to forecast financial timeseries. We will also show how to use the created ONNX model in an MQL5 Expert Advisor.
      Category Theory in MQL5 (Part 5): Equalizers Category Theory in MQL5 (Part 5): Equalizers
      Category Theory is a diverse and expanding branch of Mathematics which is only recently getting some coverage in the MQL5 community. These series of articles look to explore and examine some of its concepts & axioms with the overall goal of establishing an open library that provides insight while also hopefully furthering the use of this remarkable field in Traders' strategy development.
      Alan Andrews and his methods of time series analysis Alan Andrews and his methods of time series analysis
      Alan Andrews is one of the most famous "educators" of the modern world in the field of trading. His "pitchfork" is included in almost all modern quote analysis programs. But most traders do not use even a fraction of the opportunities that this tool provides. Besides, Andrews' original training course includes a description not only of the pitchfork (although it remains the main tool), but also of some other useful constructions. The article provides an insight into the marvelous chart analysis methods that Andrews taught in his original course. Beware, there will be a lot of images.
      How to use MQL5 to detect candlesticks patterns How to use MQL5 to detect candlesticks patterns
      A new article to learn how to detect candlesticks patterns on prices automatically by MQL5.