
知っておくべきMQL5ウィザードのテクニック(第21回):経済指標カレンダーデータによるテスト
はじめに
ウィザードで組み立てられたEAについての連載を続けます。アイデアを確認したり、より堅牢な取引システムを構築したりするために、テスト中に経済指標カレンダーニュースをEAに統合する方法を検討します。こちらの記事が参照されます。その記事は連載初のものであるため、それを読み、フォローアップすることをお勧めしますが、ここでは、ウィザードが組み立てたEAが、これらのMQL5 IDEツールからどのような利益を得ることができるかを厳密に取り上げます。新しい読者のために、こちらとこちらにMQL5ウィザードを使用してEAを開発および組み立てる方法についての入門記事があります。
経済データは、伝統的な指標、カスタム指標、その他のプライスアクションツールなどの形でより普及している「テクニカル」とは対照的に、証券の「ファンダメンタルズ」に傾いているため、取引システムのエッジや優位性の源となる可能性があります。これらの「ファンダメンタルズ」は、インフレ率、中央銀行金利、失業率、生産性データ、その他多くのニュースデータという形をとることができ、これらは通常、リリースがあるたびにボラティリティを示すように、証券価格に大きな影響を与えます。最も有名なのは、ほぼ毎月第一金曜日に発表される非農業部門雇用者数でしょう。さらに、必要なスポットライトが当たらず、ほとんどのトレーダーが見落としている重要なニュースデータが他にもあることは間違いません。だからこそ、これらの経済ニュースデータに基づいて戦略をテストすることで、これらのデータのいくつかを発見することができ、その結果、見込みのあるトレーダーに優位性をもたらすことができます。
SQLiteデータベースはMetaEditor IDE内で作成することができます。それらはデータリポジトリであるため、理論上は、指標バッファとして機能するように、EAのデータソースとしてこれらを使用することができるはずです。しかし、それ以上に、経済データをローカルに保存することができ、オフラインでのテストや、ニュースデータソースが未知の理由で破損した場合にも使用することができます。これは、一部の(または必然的にほとんどの)データポイントが古くなるにつれて、継続的なリスクとなります。そこでこの記事では、SQLiteデータベースを使用して経済指標カレンダーのニュースをアーカイブし、ウィザードで組み立てられたEAがこれを使用して売買シグナルを生成できるようにする方法を探ります。
現在の制限と回避策
ただし、これには裏があります。ストラテジーテスターで経済指標カレンダーのデータを読み込めないことに加え、ストラテジーテスター内でデータベースを読み込むテストをしたところ、同様の制約があるようです。この原稿を書いている時点では、次のコードでデータベースのデータを読み込もうとしています。
//+------------------------------------------------------------------+ //| Read Data //+------------------------------------------------------------------+ double CSignalEconData::Read(string DB, datetime Time, string Event) { double _data = 0.0; //--- create or open the database in the common terminal folder ResetLastError(); int _db_handle = DatabaseOpen(DB, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(_db_handle == INVALID_HANDLE) { Print("DB: ", DB, " open failed with err: ", GetLastError()); return(_data); } string _sql = " SELECT ACTUAL " + " FROM " + " ( " + " SELECT ACTUAL " + " FROM PRICES " + " WHERE DATE <= '" + TimeToString(Time) + "' " + " AND EVENT = '" + Event + "' " + " ORDER BY DATE DESC " + " LIMIT 1 " + " ) "; int _request = DatabasePrepare(_db_handle, _sql); if(_request == INVALID_HANDLE) { Print("request failed with err: ", GetLastError()); DatabaseClose(_db_handle); return(_data); } while(DatabaseRead(_request)) { //--- read the values of each field from the obtained entry ResetLastError(); if(!DatabaseColumnDouble(_request, 0, _data)) { Print(" DatabaseRead() failed with err: ", GetLastError()); DatabaseFinalize(_request); DatabaseClose(_db_handle); } } return(_data); }
私のコーディングミスかもしれませんが、エラー5601が発生し、アクセスしようとしているテーブルが存在しないというメッセージが表示されます。しかし、MetaEditorのデータベースIDEまたはチャートに添付されたスクリプトからSQLスクリプトを実行すると、期待通りの結果が返され、そのような問題は発生しません。つまり、ストラテジーテスターでこれを実行させるために追加コードを入れる必要があるか、ストラテジーテスターでのデータベースへのアクセスが許可されていないか、私の見落としかもしれません。サービスデスクのチャットボットでは対応できません。
では、この状況で何ができるでしょうか。上で述べたように、経済データをローカルのデータベースにアーカイブすることには明らかにメリットがあるため、それに基づいてEAをテストおよび開発することでこれをさらに進めないのは残念です。私が提案する回避策は、経済データをCSVファイルにエクスポートし、ストラテジーテスター中にこれを読み込むことです。
この場合、回避策としてCSVファイルに頼ったり使ったりしていますが、データベースに取って代わろうと考えるなら、CSVファイルには多くの課題や限界があります。データをデータベースに書き出してからCSVファイルに書き出すのではなく、単純にCSVファイルに直接エクスポートすればいいのでは、という意見もあるでしょう。理由はこれです。
CSVファイルはデータベースよりもデータの保存効率がはるかに悪くなります。これは多くの要因によって示されますが、中でもデータの完全性と検証はその筆頭です。データベースは主キーと外部キーによって整合性と制約のチェックをおこなうが、CSVファイルには明らかにこの機能が欠けています。第二に、大規模なデータセットを非常に効率的に検索できるインデックスのおかげで、パフォーマンスとスケーラビリティはデータベースの得意分野である一方、 CSVファイルは常に線形検索に依存しており、ビッグデータを扱う場合に非常に遅くなる可能性があります。
第三に、同時アクセスはほとんどのデータベースに組み込まれており、複数のユーザーがリアルタイムでアクセスできます。一方、CSVファイルではこれを処理できません。さらに、データベースは、ユーザー認証、ロールベースのアクセス制御、暗号化などの機能により、安全なアクセスを提供します。CSVファイルにはデフォルトでセキュリティがないため、機密データの保護が難しくなります。
さらに、データベースは、CSVにはないバックアップとリカバリーのための自動化ツールを提供します。データベースは、徹底的な分析のためにSQLを使用して結合や操作をおこなう複雑なクエリーをサポートしますが、CSVファイルでは同じ機能を実現するためにサードパーティのスクリプトが必要になります。データベースではトランザクションのACID準拠が提供されますが、CSVファイルでは提供されません。
続けて、データベースでは正規化(英語)もサポートされています。これは、CSV固有のフラット構造が多くの冗長性を生むに違いないのに対して、データの冗長性(英語)を減らし、よりコンパクトで重複の少ない効率的な保存を可能にしています。また、データベースはバージョン管理もサポートしています(多くのデータが時間の経過とともに更新される可能性があるため、これは重要である)が、CSVファイルはしていません。CSVはデータ更新時にデータが破損しやすく、複雑なデータ構造を管理する上で課題があります。CSVファイルに対するデータベースの決定的な利点は他にもたくさんありますが、ここではこれらの利点だけに絞って紹介します。これらの利点はそれぞれ、分析や研究のために経済データを管理する上で、特にCSVファイルでは扱いにくい長期間にわたって重要な役割を果たします。
ただし、ストラテジーテスターがアクセスできるようにデータをCSVファイルに書き出す前に、データベースを構築して経済データを読み込む必要があります。
SQLiteデータベースの構築
SQLiteデータベースを構築するには、スクリプトを使用します。コードを以下に掲載します。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ //| Sourced from: https://www.mql5.com/ja/articles/7463#database_functions //| and here: https://www.mql5.com/ja/docs/database/databasebind //+------------------------------------------------------------------+ void OnStart() { //--- create or open a database string _db_file = __currency + "_econ_cal.sqlite"; int _db_handle = DatabaseOpen(_db_file, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(_db_handle == INVALID_HANDLE) { Print("DB: ", _db_file, " open failed with code ", GetLastError()); return; } else Print("Database: ", _db_file, " opened successfully"); ... ... }
このコードのほとんどは こちらを参考にしています。データベースの作成は、ファイルの読み書きハンドルを宣言するように、ハンドルを介しておこなわれます。通貨ペアごとにデータベースを作っていますが、これは無駄が多く、非常に扱いにくいと認めざるを得ません。より良い方法は、通貨間の経済データポイントをすべて1つのデータベースにまとめることですが、そこまで熱心に取り組めませんでした。謝ります。ハンドルが作成されたら、次に進む前にこのハンドルが有効かどうかを確認する必要があります。有効であれば、データベースが空白であることを示すので、データを格納するテーブルの作成に進むことができます。この記事では、カレンダーイベントセクター のカレンダーイベント部門にのみ焦点を当てるため、表の価格を命名しました。これは、インフレ率データだけでなく、消費者物価指数や生産者物価指数も含む包括的なセクターです。私たちは取引通貨ペアの相対的なインフレ率に基づいてロングまたはショートの条件を設定するカスタムシグナルクラスの開発を検討しているため、これに注目しています。このような売買条件シグナルの開発には多くの代替アプローチが考えられますが、ここで選んだルートはおそらく最もシンプルなもののひとつでしょう。テーブルの作成では、ほとんどのデータベースオブジェクトと同じように、まずそのテーブルが存在するかどうかを確認し、存在する場合は削除して(ドロップして)、これから入力して使用するテーブルを作成します。コードは以下に掲載されています。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { ... //--- if the PRICES table exists, delete it if(DatabaseTableExists(_db_handle, "PRICES")) { //--- delete the table if(!DatabaseExecute(_db_handle, "DROP TABLE PRICES")) { Print("Failed to drop table PRICES with code ", GetLastError()); DatabaseClose(_db_handle); return; } } //--- create the PRICES table if(!DatabaseExecute(_db_handle, "CREATE TABLE PRICES(" "DATE TEXT ," "FORECAST REAL ," "ACTUAL REAL ," "EVENT TEXT);")) { Print("DB: ", _db_file, " create table failed with code ", GetLastError()); DatabaseClose(_db_handle); return; } //--- display the list of all fields in the PRICES table if(DatabasePrint(_db_handle, "PRAGMA TABLE_INFO(PRICES)", 0) < 0) { PrintFormat("DatabasePrint(\"PRAGMA TABLE_INFO(PRICES)\") failed, error code=%d at line %d", GetLastError(), __LINE__); DatabaseClose(_db_handle); return; } ... }
作成したテーブルには4つの列があります。経済ニュースがいつ発表されたかを記録するテキスト型のDATE列、予測された経済データポイントを記録するreal型 のFORECAST列、同じくreal型のACTUAL列があります。最後にEVENT列があり、これはテキスト型で、このデータポイントに適切なラベルを付けるのに役立ちます。つまり、各データポイントに使用されるラベルのタイプは、データのイベントコードに対応します。これは、経済指標カレンダーのデータを取得する際、CalendarValueHistoryByEvent関数を使用して、特定のイベントと対になっているカレンダーニュースの値を返すためです。これらのイベントにはそれぞれ文字列の説明コードがあり、データベースに保存する際にデータに割り当てるのはこれらのコードです。この経済指標カレンダーのデータを取得する「Get」関数を以下に示します。
//+------------------------------------------------------------------+ //| Get Currency Events //+------------------------------------------------------------------+ bool Get(string Currency, datetime Start, datetime Stop, ENUM_CALENDAR_EVENT_SECTOR Sector, string &Data[][4]) { ResetLastError(); MqlCalendarEvent _event[]; int _events = CalendarEventByCurrency(Currency, _event); printf(__FUNCSIG__ + " for Currency: " + Currency + " events are: " + IntegerToString(_events)); // MqlCalendarValue _value[]; int _rows = 1; ArrayResize(Data, __COLS * _rows); for(int e = 0; e < _events; e++) { int _values = CalendarValueHistoryByEvent(_event[e].id, _value, Start, Stop); // if(_event[e].sector != Sector) { continue; } printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", belongs to sector: " + EnumToString(_event[e].sector)); // _rows += _values; ArrayResize(Data, __COLS * _rows); for(int v = 0; v < _values; v++) { // printf(__FUNCSIG__ + " Calendar Event code: " + _event[e].event_code + ", for value: " + TimeToString(_value[v].period) + " on: " + TimeToString(_value[v].time) + ", has... "); // Data[_rows - _values + v - 1][0] = TimeToString(_value[v].time); // if(_value[v].HasForecastValue()) { Data[_rows - _values + v - 1][1] = DoubleToString(_value[v].GetForecastValue()); } if(_value[v].HasActualValue()) { Data[_rows - _values + v - 1][2] = DoubleToString(_value[v].GetActualValue()); } // Data[_rows - _values + v - 1][3] = _event[e].event_code; } } return(true); }
多次元文字列配列「_data」を使用して経済指標カレンダーのデータを取得し、その2番目の次元は、データの格納に使用するPRICESテーブルの列数と一致します。つまり、その行数は、PRICESテーブルに挿入するデータ行数と同じになります。配列からテーブルへのデータの読み込み高速化するために、まず DatabaseTransactionBegin()と DatabaseTransactionCommit()関数を使用して、それぞれデータの書き込み操作を開始し、終了します。これは、すでに上で参照した記事の中で、それらなしで作業する場合よりも効率的な方法として説明されています。次にデータバインド関数を使用して、配列のデータを実際にデータベースに書き込みます。データ列が宛先データテーブルと一致しているので、この処理も比較的簡単で、下のコードに示すように少し長くなるにもかかわらず、非常に効率的です。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ //| Sourced from: https://www.mql5.com/ja/articles/7463#database_functions //| and here: https://www.mql5.com/ja/docs/database/databasebind //+------------------------------------------------------------------+ void OnStart() { ... //--- create a parametrized _sql_request to add _points to the PRICES table string _sql = "INSERT INTO PRICES (DATE,FORECAST,ACTUAL,EVENT)" " VALUES (?1,?2,?3,?4);"; // _sql_request parameters int _sql_request = DatabasePrepare(_db_handle, _sql); if(_sql_request == INVALID_HANDLE) { PrintFormat("DatabasePrepare() failed with code=%d", GetLastError()); Print("SQL _sql_request: ", _sql); DatabaseClose(_db_handle); return; } //--- go through all the _points and add them to the PRICES table string _data[][__COLS]; Get(__currency, __start_date, __stop_date, __event_sector, _data); int _points = int(_data.Size() / __COLS); bool _request_err = false; DatabaseTransactionBegin(_db_handle); for(int i = 0; i < _points; i++) { //--- set the values of the parameters before adding a data point ResetLastError(); string _date = _data[i][0]; if(!DatabaseBind(_sql_request, 0, _date)) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } //--- if the previous DatabaseBind() call was successful, set the next parameter if(!DatabaseBind(_sql_request, 1, _data[i][1])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } if(!DatabaseBind(_sql_request, 2, _data[i][2])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } if(!DatabaseBind(_sql_request, 3, _data[i][3])) { PrintFormat("DatabaseBind() failed at line %d with code=%d", __LINE__, GetLastError()); _request_err = true; break; } //--- execute a _sql_request for inserting the entry and check for an error if(!DatabaseRead(_sql_request) && (GetLastError() != ERR_DATABASE_NO_MORE_DATA)) { PrintFormat("DatabaseRead() failed with code=%d", GetLastError()); DatabaseFinalize(_sql_request); _request_err = true; break; } else PrintFormat("%d: added data for %s", i + 1, _date); //--- reset the _sql_request before the next parameter update if(!DatabaseReset(_sql_request)) { PrintFormat("DatabaseReset() failed with code=%d", GetLastError()); DatabaseFinalize(_sql_request); _request_err = true; break; } } //--- done going through all the data points //--- transactions status if(_request_err) { PrintFormat("Table PRICES: failed to add %s data", _points); DatabaseTransactionRollback(_db_handle); DatabaseClose(_db_handle); return; } else { DatabaseTransactionCommit(_db_handle); PrintFormat("Table PRICES: added %d data", _points); } ... }
PRICEテーブルにデータが挿入されたので、データベースからCSVファイルを作成しなければなりません。ストラテジーテスター使用時のアクセスが禁止されているようだからです。要約すると、データベースの読み込みに使用するSQLを含むRead()関数は、MetaEditor内で以下の画像のように問題なく実行できます。
また、スクリプトsql_read(完全なソースは以下の通り)を同じような時間の入力を持つチャートにアタッチし、USDデータベースにクエリを実行すると、同じ結果が得られ、MetaEditor IDEまたはMT5端末環境でのデータベースに問題がないことを示唆しています。以下のログプリント画像をご覧ください。
スクリプトをアタッチして実行すれば、チャートにアタッチしたEAが問題なくデータベースの値を読み込める可能性があります。しかし、現在の私たちの目的では、ストラテジーテスターを実行する際にデータベースの値を読み取ることができないため、CSVファイルに頼る必要があります。
経済指標カレンダーデータの書き出し
データをCSVに書き出すには、以下のコードに示すように、組み込み関数の1つであるDatabaseExport()を使用するだけです。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { ... //--- save the PRICES table to a CSV file string _csv_file = "PRICES.csv"; if(DatabaseExport(_db_handle, "SELECT * FROM PRICES", _csv_file, DATABASE_EXPORT_HEADER | DATABASE_EXPORT_INDEX | DATABASE_EXPORT_QUOTED_STRINGS, ";")) Print("Database: table PRICES saved in ", _csv_file); else Print("Database: DatabaseExport(\"SELECT * FROM PRICES\") failed with code", GetLastError()); //--- close the database file and inform of that DatabaseClose(_db_handle); PrintFormat("Database: %s created and closed", _db_file); }
例えば、最初にデータをオブジェクト(配列など)に選択し、すべての配列値をループしてカンマ区切りの文字列に保存し、それを書き出すのであれば、同じ結果が得られるでしょうが、コーディングの手間はともかく、ここで採用した方法は、forループのアプローチよりも実行時間がはるかに短いことはほぼ間違いません。これは、SQLiteがC言語ライブラリであり、MQL5もまたC言語を多くベースにしているからかもしれません。
PRICESのデータベーステーブルデザインには、明示的なプライマリキーがありません。大規模なデータセットを持つこれらのキーは、データベースを迅速かつ強力なツールにするインデックスを作成する上で重要です。このテーブルを修正する方法としては、主キーの役割を果たすオートインクリメント列を追加するか、EVENT'列と'DATE'列をペアにして両方とも主キーにする方法があります。設計の観点から、両方の列の合計値はすべてのデータ行にわたって一意になるためです。EVENT'列に格納されているイベントのラベルとして採用されているコードが曖昧であるため、興味のあるデータポイントが実際に取得したものであることを確認するために、特別な注意を払う必要があります。
例えば、この記事ではGBPUSDのペアに焦点を当てますが、これはGBPとUSDの2つの通貨に興味があることを意味します(ユーロ圏だけでなく、その加盟国からも複数のデータが得られているため、ユーロは回避しています)。これらの曖昧さの少ない通貨のインフレデータのイベントコードを見ると、GBPは「cpi-yy」、USDは「core-pce-price-index-yy」となっています。ここで考慮しないUSDの前年比消費者インフレコードが他にもあるため、選択する際には慎重に検討する必要があることに留意してください。また、このラベル付けはそれ自体が標準的なものではないため、何年か、あるいはもっと先に、自動化システムもコードを更新する必要があるような改訂がおこなわれる可能性があります。これは、カレンダーデータからのデータ検証スクリーナーを使用して独自のカスタムラベルを作成し、適切なデータが正しくコード化されるようにするという考えを示している可能性がありますが、前述のように、コード化は予告なく変更される可能性があるため、時々人間による検査が必要になります。
MQL5シグナルクラス
前述のように、これにはCSVファイルを使用しています。これは簡単なプロセスのはずですが、ANSIとUTF8の形式の違いを認識していない場合、このデータを読み取るときにいくつかの問題が発生する可能性があります。標準のCSVリーダー機能は こちら を採用しています。書き出されたCSVデータを読み込み、証拠金通貨(GBP)と利益通貨(USD)の各通貨の指標を初期化する関数で読み込みます。その際、大容量のCSVファイルはRAMに負担をかけるため、読み込みに制限がかかります。これを回避する方法として、CSVファイルを時間ごとに分割して初期化時に1つのファイルだけを読み込み、そのファイルの最新のデータポイントがストラテジーテスターの現在の時間に対して古すぎる場合、そのファイルを「リリース」し、より新しいCSVファイルを読み込むようにすることが考えられます。
これらの回避策はすべて、ストラテジーテスターでデータベースアクセスが可能であれば存在しなかった問題に対処するものです。つまり、このシグナルクラスはデータベースから読み込むわけではありません。単に証拠金通貨と利益通貨の両方のCSVファイルの名前を入力として受け取るだけです。シグナルクラスでは、使用する唯一のシリーズバッファはm_timeクラスになります。厳密に言えば、現在の時刻で十分なのでバッファは必要ありませんが、ここではインデックス0の時間を取得するために使用されます。読み込まれたCSVファイルからカレンダーデータの値を取得するのはRead関数です。
//+------------------------------------------------------------------+ //| Read Data //+------------------------------------------------------------------+ double CSignalEconData::Read(datetime Time, SLine &Data[]) { double _data = 0.0; int _rows = ArraySize(Data); _data = StringToDouble(Data[0].field[1]); for(int i = 0; i < _rows; i++) { if(Time >= StringToTime(Data[i].field[0])) { _data = StringToDouble(Data[i].field[1]); } else if(Time < StringToTime(Data[i].field[0])) { break; } } return(_data); }
これはforループを使用しているため反復的ですが、もしインデックス付きデータベースから同じデータにアクセスできたとしたら、同じ操作はもっと速く実行できるでしょう。この記事で使用したような小さなデータセットでは、このパフォーマンスの違いは見過ごすことができるかもしれませんが、データセットのサイズが大きくなり、より多くの履歴データを見るようになると、ストラテジーテスター内でSQLiteを読み込むケースが強くなります。
read関数は、証拠金通貨と利益通貨の両方について呼び出され、最新のインフレプリントを返します。私たちのシグナルは、単純にこれらのプリントの相対的な大きさに基づいて生成されます。証拠金通貨が利益通貨よりもインフレ率が高ければ、そのペアを売ります。一方、証拠金通貨のインフレ率が低ければ、買います。このロジックをLongCondition()関数の一部として以下に示します。
//+------------------------------------------------------------------+ //| "Voting" that price will grow. | //+------------------------------------------------------------------+ int CSignalEconData::LongCondition(void) { int result = 0; m_time.Refresh(-1); double _m = Read(m_time.GetData(0), m_margin_data); double _p = Read(m_time.GetData(0), m_profit_data); if(_m < _p) { result = int(100.0 * ((_p - _m) / _p)); } return(result); }
ウィザードでEAを組み立て、コンパイルした後、最初のデフォルト設定を使用して、最適化をまったくおこなわずにEAを実行すると、次のような結果が得られます。
インフレは明らかに通貨ペアの動向を決定する要因です。今回使用したインフレデータは月次で発表されるため、テスト期間も月次となります。ただし、これは必ずしもそうであってはなりません。より下位の時間枠でも、同じポジションを維持しながら、より鋭い、あるいはより良いエントリポイントを探すことができるからです。テストしたペアはGBPUSDです。
結論
まとめると、SQLiteデータベースは、カスタマイズされたデータセットの保存とキュレーションを可能にするという多くの利点とメリットをもたらします。重要なニュースイベントの際に発表される経済指標カレンダーデータは、そのようなデータの1つであり、マーケットの動きの主な要因を理解するためのさらなる分析のためにアーカイブすることができます。この記事で取り上げたような、インフレ率に焦点を当てた非常にシンプルな戦略は、テクニカル指標も使用するシステムにとって、すべての違いをもたらす可能性があります。いつものように、これは投資アドバイスではないので、読者はこの記事やシリーズで共有されているアイデアに取り組む前に、自分自身でデューデリジェンスをおこなうことが推奨されます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/14993




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