
MQL5でJanus factorを実装する
はじめに
トレーダーなら誰でも知っていることですが、市場価格は大きく分けて2つのパターンのどちらかに従っています。価格は、トレンドを形成するか、水平方向に動くかのどちらかです。その結果、市場参加者の戦略は、大きく分けて、トレンドフォローか逆張りかのどちらかに絞られることになります。Janus factorは、この二面性を捉えた市場行動論です。今回は、その基礎を紐解くと同時に、この分析方法を促進する指標の実装を紹介します。
フィードバック
Janus factorによれば、市場は価格とそれに対するトレーダーの反応との相互作用によって動きます。アンダーソンは、この相互作用をフィードバックシステムに例えました。フィードバックシステムについては、ウィキペディアで詳しく知ることができます。市場がトレンドにあるとき、市場参加者は利益を乗せることで自信を示します。上昇トレンドでは、価格が上昇することで参加者のトレンドへの期待が確認され、買いが増えます。これは、価格をさらに高くする効果があります。
図1:上昇トレンドにおけるポジティブフィードバック
下降トレンドでは、価格の下落がトレーダーに恐怖心を与え、トレーダーが損失を最小限に抑えるために売りに走ることがあります。売りが増えれば、圧力がかかって価格が下がります。したがって、市場の動向はポジティブフィードバックの一例です。トレーダーが価格変動に反応するまで、価格は加速し続けます。
図2:下降トレンドにおけるポジティブフィードバック
ネガティブフィードバックは、トレーダーが市場に対する信頼が薄いため、価格が変動した後に早期に利益を確定することを選択した場合に見られます。早期の利益確定はモメンタムを失わせ、値動きの大きさを制限します。これに強気と弱気のせめぎ合いが加わり、価格が安定するのです。
図3:市場におけるネガティブフィードバック
資本流出
このフィードバックという概念で重要なのは、異なる条件が重なったときにトレーダーが好む銘柄のタイプです。アンダーソンの銘柄分析によると、トレーダーは上昇トレンドの中で、相対的にパフォーマンスの良い銘柄を支持し、パフォーマンスの悪い銘柄を売っていることがわかります。下降トレンドでは、業績不振の銘柄がトレーダーの空売りの対象となり、最も価値を失いました。
トレンドがない場合、トレーダーは特定の価格水準を選んでエントリーしたりエグジットしたりするので、強い株と弱い株を分けるものはあまりありませんでした。そこで彼は、ある銘柄の相対的なパフォーマンスを分析することで、ネガティブフィードバックとポジティブフィードバックの時期を検出することができるという仮説を立てました。
Janus計算
パフォーマンスを把握するために、定期的なリターンを算出します。調査対象の銘柄のリターンを合算して平均値を出し、基準値として使用します。これは、ベンチマークリターンと呼ばれます。評価対象銘柄の集合体をインデックスと呼びます。
『Janus Factor - Trend Follower's Guide to Market Dialectics』の中で、アンダーソンは、リターンをベースに計算された銘柄の様々な指標を紹介し、マーケットの挙動を洞察するために利用しています。
ジJanusライブラリ - janus.mqh
Janusに関連するすべての計算に共通するデータとルーチンは、janus.mqhに含まれています。インクルードファイルには、3つのカスタムタイプの宣言があります。
CSymboldataクラスは、銘柄データおよび関連する操作を処理します。
//+------------------------------------------------------------------+ //|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は CSymboldata オブジェクトのコンテナで、解析対象の銘柄のコレクションを表します。両クラスのコードは、Mql5のコードベースからのものを流用しました。下層バッファのインデックス付けの方向性を修正しました。なお、すべてのクラスで右から左へのインデックス形式が採用されており、ゼロインデックスは最新のバーを指しています。
Janus factorの技術を実証するために、FXペアの分析に応用してみましょう。アンダーソンはもともと株式分析のためにこの方法を考案しましたが、株式銘柄データの入手は多くの証券会社で一貫していません。
//+------------------------------------------------------------------+ //| 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; } }; //+------------------------------------------------------------------+
このコードは、以下に示す3つのカスタム列挙から始まります。
#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 };
列挙 | 詳細 | オプション |
---|---|---|
ENUM_INDEX_TYPE | 解析対象のコレクションに含まれる銘柄の種類 | INDEX_FOREX:すべてのFXメジャーで構成される銘柄のコレクションを構築する。28個の銘柄のセット。 INDEX_CUSTOM:銘柄のコレクションの指定が必要。 |
ENUM_PRICE | 計算で使用する価格系列を選択できるようにする。なお、価格情報は、インデックス0を最新価格とするシリーズとして保存される。 | CLOSE:終値 MEDIAN:価格の中央値((高値+安値)/2) TYPICAL:典型的な価格((高値+安値+終値)/3) WEIGHTED:加重された価格((高値+安値+終値+終値)/4) |
ENUM_DIFF_TYPE | 選択した価格系列に適用可能な異なる差分法 | DIFF_LOG:対数の差分 DIFF_PERCENT:割合の差分 |
CJanusクラスを使用するには、5つのメソッドを理解する必要があります。CJanusオブジェクトを作成した後、どのメソッドも呼び出す前に、まずInitializeメンバー関数を呼び出す必要があります。このメソッドは2つの重要なことをおこないます。まず、CsymbolCollectionオブジェクトを初期化し、インデックスを構成する選択された銘柄でそれを埋めるのです。そして、コレクション内の銘柄の履歴バーをチェックし、同期させます。
//+------------------------------------------------------------------+ //|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); };
Initialize()メソッドの入力パラメータについて、下表に説明します。
パラメータ | 詳細 | データ型 |
---|---|---|
set_price_type | すべての計算に使用される基礎となる価格系列を設定する列挙型 | ENUM_PRICE |
set_diff_type | 価格系列に適用される差分法を設定 | ENUM_DIFF_TYPE |
set_index_type | 銘柄コレクションの種類をカスタムまたはFXメジャーのみに設定。カスタムを選択した場合、symbol_listパラメータで銘柄を指定する必要 | ENUM_INDEX_TYPE |
set_timeframe | 計算に使用するバーの時間枠を指定 | ENUM_TIMEFRAME |
履歴サイズ | 要求されるべきバーの最大数を定義複数の銘柄を扱っているため、コレクション内のすべての銘柄が同じ数の履歴バーデータを利用できるようにするための方法です。 | integer |
symbol_list | set_index_typeがINDEX_CUSTOM に設定されている場合、ユーザーは分析する銘柄のコレクションを構成する銘柄のコンマ区切りリストを指定する必要があります。 | string |
- Update()メソッドは、銘柄データを更新し、最新の相場を取得します。
- HistorySize()メソッドは、利用可能な履歴バーの数を返します。この数値は、Initializeメソッドを呼び出す際に指定された数値よりも小さくすることができます。これは、すべての銘柄における履歴が変化する可能性があるため、この関数は同期した履歴のサイズを返します。
- GetSymbolsTotals()は、追加され、ターミナルで利用可能であることが確認された銘柄の数を返します。
- GetSymbolAt()は、インデックスで銘柄名を取得します。
Calculateを先頭に持つ残りのメソッドは、計算を実行し、1つまたは2つの値を返します。これらはすべて、最初の入力パラメータとして、計算が実行される小節のバーシフトを持ちます。
各メソッドについては、各種指標を実施しながら解説していきます。
パフォーマンスを測定する
すでに述べたように、パフォーマンスはリターンを計算することで測定されます。私たちの実装では、適用価格とリターンの計算方法を選択するオプションがあります。CalculateReturns() メソッドを呼び出すと、指定されたバーシフトと銘柄のリターンが計算されます。
//+------------------------------------------------------------------+ //|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); }
以下のIndexReturns指標のコードは、チャート銘柄のリターンと、ベンチマークのリターンを表示します。
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
IndexReturns指標
ベンチマーク
ベンチマークリターンを算出するために、対応する各時点の銘柄の個別リターンを使用し、中央値を求めます。この計算は、CalculateBenchmarkReturns()メソッドで実装されています。
//+------------------------------------------------------------------+ //|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)); }
攻守
相対的なパフォーマンスを知るために、アンダーソンは銘柄の攻守のスコアという概念を発想しました。攻めのスコアは、ベンチマークのリターンが平均を上回ったときに達成されるパフォーマンスです。
守りのスコアは、ベンチマークのリターンが平均を下回った場合に達成されるリターンを用いて同様に算出されます。この値は、市場価格が乱高下しているときに、銘柄がどれだけ持ちこたえることができるかを示しています。
これらの値を算出するために、ベンチマークリターンは、攻めのベンチマークリターンと守りのベンチマークリターンの2つに分割されます。これらは、適切なウィンドウ長を選択し、そこから平均ベンチマークを計算することで算出されます。
CJanusクラスでは、指定されたウィンドウの平均値として中央値が使用されます。攻めのベンチマークリターンは、達成されたリターンと平均ベンチマークリターンとの差を、平均より大きいか等しいベンチマークについて累積することによって得られます。
守りのベンチマークリターンは、ウィンドウの平均値よりも低いベンチマーク値を用いて同様に計算されます。
//+------------------------------------------------------------------+ //|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; }
攻めおよび守りのベンチマークリターンが算出された時点で、銘柄の攻守の点数を算出するために使用されます。ベンチマークに使用したのと同じウィンドウで、ベンチマークのリターンがウィンドウ平均より大きいか等しいすべてのタイムポイントで、達成されたリターンが一緒に加算されます。そして、この合計値を対応する攻撃型ベンチマークの分数で表し、100を乗じます。
同様の計算を、守りのベンチマークリターンを用いておこないます。CalculateSymbolOffenseDefense()メソッドは、攻守両方のスコアを計算することを実装しています。
//+------------------------------------------------------------------+ //|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; }
攻守のスコアをプロットすることで、アンダーソンは市場の状況によって現れるパターンに着目しました。ポジティブフィードバックの時間帯は、スコアが大きく分散しており、パフォーマンスが最も良い銘柄と悪い銘柄の間に大きな差がありました。一方、ネガティブフィードバックの時期には、ベストパフォーマーとワーストパフォーマーの差が縮まりました。アンダーソンはこれをインデックスの膨張と収縮と呼びました。
以下のコードは、OffensiveDefensiveScatterPlotスクリプトを説明するもので、上記の指標と同様の入力を持ち、攻撃と防御のスコアのプロットをアニメーションとして描画するものです。
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
このスクリプトでは、以下のグラフィックが生成されます。
相対力
銘柄の攻撃力と防御力を以下の式で結ぶと、その銘柄の相対力がわかります。
この式の導出は、アンダーソンの著書に記載されています。相対力は、CalculateRelativeStrength()メソッドで計算されます。
//+------------------------------------------------------------------+ //|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)); }
以下の指標コードは、チャート銘柄の相対力をプロットします。
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
RelativeStrength指標
相対力の値を使用すると、時間の経過とともに発生する拡大や縮小をより明確に把握することができます。これは、相対力の最高値と最低値の指標プロットによって以下に示されます。
この指標のコードを以下に示します。
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
相対力のリーダーとラガード
ベストパフォーマーとワーストパフォーマーのリターンで示される相対的なパフォーマンスを平均化するとそれぞれが相対力のリーダー、相対力のラガードとなります。これらの値は、フィードバックの状態によって、取引に最適なタイミングを示すものです。相対力(rs)のリーダーとラガードは、ベストパフォーマーとワーストパフォーマーの数を指定し、平均を算出することで算出します。
//+------------------------------------------------------------------+ //|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; }
CalculateRelativeStrengthLeadersLaggards()メソッドのrs_percent_top入力は、平均化された相対力を計算するために使用する銘柄の割合を示すものです。例えば、rs_percent_topを0.1に設定すると、上位10%と下位10%の銘柄がリーダー、ラガードの計算に使われることになります。
以下は、Leaders and Laggards指標のスクリーンショットです。
スプレッド
市場がポジティブフィードバックに支配されると、トレンドの方向によって、最も弱い証券と最も強い証券が乖離します。上昇トレンドでは、トレーダーは強い証券を支持し、下降トレンドでは弱い証券を空売りしています。
価格がレンジに収まっている場合、最も弱い証券と最も強い証券の差が比較的小さく、収束していると言えるでしょう。このような動きを認識するために、最も弱い証券と最も強い証券の間の相対力の差の平均値を計算します。これが相対力スプレッドを測るものです。
相対力スプレッドは、CalculateRelativeStrengthSpread()メソッドで実装されています。
//+------------------------------------------------------------------+ //|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); }
相対力スプレッド指標
スプレッドとrsリーダー/ラガード指標を組み合わせることで、具体的な売買ルールを策定することができます。アンダーソンによれば、最高のチャンスは、対象となるインデックスの相対力の上限と下限に近い、あるいは下限にある銘柄を取引することです。
//+------------------------------------------------------------------+ //|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; }
rsリーダーとの組み合わせでスプレッドのトレンドが上昇している場合、強いパフォーマーのネット買いと共にポジティブフィードバックを示すものです。相対力が最も高い銘柄を購入する好機です。代わりにrsのラガーが上昇しているのであれば、弱い銘柄を狙ってショートします。このような状況では、選択した銘柄に適用される効果的なエントリー方法とこれらのルールを組み合わせることが賢明かもしれません。
もう1つの選択肢は、リスクのフィルターとして、スプレッド指標を使用することです。ポジティブなフィードバックがある場合、市場のリスクは低く、ネガティブなフィードバックがある場合は、混乱する可能性があることを示す指標となります。おそらく市場には手を出さない方がいい時期です。
結論
FXの銘柄の分析は、Janus factorの最適な応用とは言えないかもしれませんが、この記事の要点は、読者にこの方法を知ってもらうことです。詳しくは後述のアンダーソンの著書に掲載されています。添付のZIPファイルには、記事中で参照したツールの全コードが含まれています。下の表は、それぞれの一覧です。
指標を機能させるためには、まず、指標を構成するすべての銘柄の履歴データが利用可能であることを確認する必要があります。
ファイル名 | 種類 | 詳細 |
---|---|---|
Mql5/include/Janus.mqh | インクルード | CSymbolData, CSymbolCollection, CJanusクラスの定義を含むインクルードファイル。 |
Mql5/scripts/OffensiveDefensiveScatterPlot.mq5 | スクリプト | 攻守の得点の散布図をアニメーションで描くスクリプト |
Mql5/indicators/IndexReturns.mq5 | 指標 | 銘柄とベンチマークのリターンを表示する指標のコード |
Mql5/indicators/RelativeStrengthBestWorst.mq5 | 指標 | 相対力の最高値と最低値のプロットを示す指標 |
Mql5/indicators/RelativeStrength.mq5 | 指標 | 銘柄の相対力を表示する指標 |
Mql5/indicators/RelativeStrengthSpread.mq5 | 指標 | 相対力スプレッドを表示する指標 |
Mql5/indicatorr/RssLeaderlaggards.mq5 | 指標 | RSのリーダーとラガードをプロットした指標 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/12328





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索