Implementing the Janus factor in MQL5
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 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 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 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 |
- 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
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
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.
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
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.
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
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
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 |
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use