行列代数ライブラリ『LibMatrix』(第一部)
概論
複雑な自動売買システムを作成するために、様々な数学の分野を分析するのは必要不可欠です。の分野の1つは線型代数学です。
現在、MQL4言語では、異なる線型代数法(とりわけ、行列や決定の理論)を実装する大規模で公的に利用可能なライブラリが存在しません。
この記事では、最も広く使用されている行列操作の実装を含む、MQL4言語でのLibMatrixライブラリについて書いています。
行列とは、数や記号や式などを行と列に沿って矩形状に配列したものです。
最初に全てのコードはC++言語で書かれていましたが、約4ヶ月後に少しの修正を伴いつつMQL4言語に書き直されました。
1. ライブラリの構造
まず最初に、提案するライブラリでの行列を使った動作の幾つかの特徴について見ていきましょう。
第一に、行列の保存には一次元配列が使われます。これは、MQL4言語が多次元配列を作成する機能を持っているにもかかわらず、サイズ変更は最初の次元のみ可能であるということに関連しています。N行M列の行列を格納する為に、N×Mサイズのデータの一次元配列を割り当てられます。配列への行列の格納は列ごとに行われ、つまり、初めに配列には第一の列の要素が入り、後に第二列と続いていきます。
第二に、サイズは整数であり、要素は実数である為、ライブラリの作成時に、データ配列へ行列のサイズについての追加情報を入れないと決定したことです。(実数としてのサイズの保存は、ライブラリの動作速度を著しく下げる可能性がある)
このように、 ライブラリで処理された形式の行列は、double型の1次元配列と2つのint型の整数を表す3つの変数が含まれます。
ライブラリは、LibMatrix.mqhとLibMatrix.mq4の2つのファイルから構成されています。一つ目のファイルは、必要な場所に応じた接続の為に使用されます。これはライブラリからインポートすることができる、関数のプロトタイプが含まれています。二つ目のファイルには、関数の実装(プログラムコード)が含まれています。まさに、この事について、これからお話をしようと思っています。
ライブラリで実装される関数は、幾つかのグループに分ける事ができます。
- 一般的な数学関数(MathLerp, MathInRangeRandom, MathDoublesEqual)
- 行列を扱う為の補助関数(MatrIndiciesToOffset, MatrCopy, MatrSetSize, MatrResize)
- 行と列を扱う為の関数(MatrSwapRows, MatrSwapCols, MatrCopyRow, MatrCopyCol, MatrRowIsZero, MatrColIsZero)
- 条件の一般的なテスト(MatrIsSquare, MatrIsElemOnMainDiagonal, MatrCompatiblityCheckAdd, MatrCompatiblityCheckMul)
- コンテンツの初期化(MatrLoadZero, MatrLoadIdentity, MatrLoadInRangeRandom)
- コンテンツの種類によって条件をチェックする関数(MatrIsZero, MatrIsDiagonal, MatrIsIdentity, MatrIsSymmetric, MatrIsAntisymmetric, MatrEqual)
- 要素単位のスカラー演算(MatrAddScalar, MatrSubScalar, MatrMulByScalar, MatrDivByScalar)
- 基本的な行列演算(MatrAddMatr, MatrSubMatr, MatrMulMatr, MatrTrace, MatrTranspose)
- その他(MatrGaussianElimination, MatrGJBatchSolve, MatrMinor, MatrAlgebraicComplement, MatrInvertUsingMinors, MatrInvertUsingGJ, MatrDet, MatrDetTriang, MatrRank, MatrRankTriang, MatrComputeConnectedMatr, MatrLerpMatr)
- 入力/出力(MatrPrint, FileWriteMatr, FileReadMatr)
グループのうちの一つ一つをより詳しく見ていきましょう。
2. 関数の説明
2.1. 一般的数学関数
まず初めに一般的な数学関数のグループを見ていきましょう。このグループには、行列に直接関連付けられてはいないが、ライブラリで使用されている関数が含まれています。
double MathLerp(double rangeLowLimit, double rangeHighLimit, double balance);
関数は次の数式に基づいて線形補間を行います。rangeLowLimit + balance * (rangeHighLimit - rangeLowLimit). balanceパラメータの値は、[0;1]の範囲内にあるべきです。
double MathInRangeRandom(double rangeLowLimit, double rangeHighLimit);
関数は範囲内に均一に分布する乱数を返します。[rangeLowLimit;rangeHighLimit]生成の為に標準的なMathRand関数が使われます。
bool MathDoublesEqual(double value1, double value2, double tolerance);
この関数は、指定された精度(tolerance)でdouble型のvalue1とvalue2の2つの値を比較するのに使用します。精度の損失の可能性があるので、おおよその比較を使用する必要があります。toleranceパラメータの値は、正の数値である必要があります。(デフォルト値としてライブラリで定義されたDEFAULT_TOLERANCE定数を使用することができます)
2.2. 行列を使った作業の為の補助関数
次に行列を使った作業の為の補助関数のグループを見ていきましょう。このグループには、1次元配列への行列のパッキングの方法に関連する操作実行の簡素化の為に作られた関数が統一されています。
int MatrIndiciesToOffset(int row, int col, int numRows, int numCols)
関数は、行列にパックされている配列の先頭に対する要素のオフセットを算出します。パラメータとして行(row)と列(col)の番号が送信され、これらが交差する場所には要素があり、また行列のサイズがあります。(numRowsは行列の行数、numColsは行列の列数)rowとcolのパラメータ値は、それぞれ[0;numRows-1]と[0;numCols-1]の範囲内にある必要があります。
void MatrCopy(double& src[], double& dst[]);
この関数は、src行列の全ての要素をdst行列にコピーします。パラメータとしては行列のデータ配列のみ引き渡されます。dst行列のサイズは、コピー完了時にsrc行列のサイズと同等になります。
void MatrSetSize(double& matr[], int numRows, int numCols);
この関数は、matr データ配列のサイズを、配列の中にnumRows行とnumCols列を格納できるように変更します。行列データの整合性は保証されません。numRowsとnumColsのパラメータの値は、厳密に正数でなければいけません。
void MatrResize(double& matr[], int numRowsOld, int numColsOld, int numRowsNew, int numColsNew);
この関数は、存在する行列のmatrデータ配列のサイズを変更します。numRowsOldとnumColsOldのパラメータは、初期の行列のサイズと等しい必要があります。新しいサイズは、numRowsNewとnumColsNewのパラメータで指定されます。一方または両方の行列のサイズが小さくなる場合を除き、新しいサイズも含めて、行列のデータの整合性は保証されます。新しい要素はゼロに初期化されます。
2.3. 行と列の操作
行列の行と列を扱う為の関数のグループを見ていきましょう。
void MatrSwapRows(double& matr[], int numRows, int numCols, int row1, int row2);
この関数は、numRowsの行とnumColsの列からなる、matr行列の為のrow1とrow2の二つの行の場所を入れ替えます。row1とrow2のパラメータ値は、[0;numRows-1]の範囲内にある必要があります。
void MatrSwapCols(double& matr[], int numRows, int numCols, int col1, int col2);
この関数は、col1とcol2の二つの列の場所を入れ替えます。col1とcol2のパラメータ値は、[0;numCols-1]の範囲内にある必要があります。
void MatrCopyRow(double& matr[], int numRows, int numCols, int rowToCopy, int rowToReplace);
この関数は、rowToCopyの行の内容を、rowToReplaceの行にコピーします。rowToCopyとrowToReplaceのパラメータ値は、[0;numRows-1]の範囲内にある必要があります。
void MatrCopyCol(double& matr[], int numRows, int numCols, int colToCopy, int colToReplace);
この関数は、colToCopyの列の内容を、colToReplaceの列にコピーします。colToCopyとcolToReplaceのパラメータ値は、[0;numCols-1]の範囲内にある必要があります。
bool MatrRowIsZero(double& matr[], int numRows, int numCols, int row, double tolerance);
この関数は、toleranceの精度で、rowの行がヌルであるかどうかをチェックします。もし行がヌルである場合、返される値はtrueとなります。
bool MatrColIsZero(double& matr[], int numRows, int numCols, int col, double tolerance);
この関数は、toleranceの精度で、colの列がヌルであるかどうかをチェックします。もし列がヌルである場合、返される値はtrueになります。
2.4. 条件の一般的なチェック
条件の一般的なチェックの為の関数のグループは、コードの読みやすさの向上の為に作成されています。
bool MatrIsSquare(int numRows, int numCols);
この関数は、numRowsとnumColsのサイズを持つ行列が正方形であるかどうかをチェックします。(つまり、numRows=numColsの条件を満たされているかどうかがチェックされます)正方行列にはtrueが返されます。
bool MatrIsElemOnMainDiagonal(int row, int col);
この関数は、[row][col]のインデックスを持つ要素が、行列の主対角線上にあるかどうかをチェックします。主対角線上にある要素にはtrueを返します。
bool MatrCompatiblityCheckAdd(int numRows1, int numCols1, int numRows2, int numCols2);
この関数は、足し算タイプの操作の実行の為の二つの行列の互換性をチェックします。ペアごとにサイズの等しさがチェックされ(numRows1=numRows2 とnumCols1=numCols2)、等しい場合はtrueが返されます。
bool MatrCompatiblityCheckMul(int numRows1, int numCols1, int numRows2, int numCols2);
この関数は、乗算操作の為の二つの行列の互換性をチェックします。第一の行列の列の数と(numRows1とnumCols1のサイズを持つ)第二の行列の行の数(numRows2とnumCols2のサイズを持つ)の等しさをチェックします。もし乗算が可能な場合、trueを返します。
2.5. 行列内容の初期化
行列内容の初期化関数のグループは、頻繁に使用される行列(例えば、零行列や単位行列)をロードする為に作成されました。メモリの割り当ては行われないので、グループの関数を呼び出す前に、MatrSetSizeまたはMatrResizeの関数の呼び出しを行う必要があります。
void MatrLoadZero(double& matr[], int numRows, int numCols);
この関数は、numRowsの列とnumColsの行から零行列を作るゼロでmatrデータ配列を初期化します。
void MatrLoadIdentity(double& matr[], int numRows, int numCols);
この関数は、行列の主対角線上にあるものを除き、matrデータ配列の全ての要素をゼロで初期化します。numRowsの行とnumColsの列の初期化される行列は、正方形である必要があります。
void MatrLoadInRangeRandom(double& matr[], int numRows, int numCols, double rangeLowLimit, double rangeHighLimit);
この関数は、[rangeLowLimit;rangeHighLimit]の範囲からの乱数によるmatr行列のデータ配列を初期化します。(乱数の生成にはMathInRangeRandom関数が使われます)
2.6. 内容のタイプによって状態をチェックする関数
内容のタイプによって状態をチェックする関数のグループを見てみましょう。このグループの関数は、toleranceの精度(このパラメータの値は正数である必要がある)で比較を行い、チェックした状態が正確な場合、trueを返します。
bool MatrIsZero(double& matr[], int numRows, int numCols, double tolerance);
この関数は、行列がヌルであるかどうかをチェックします。
bool MatrIsDiagonal(double& matr[], int numRows, int numCols, double tolerance);
この関数は、対角行列であるかどうかをチェックします。行列が正方形であり、主対角線外の全ての要素がゼロと等しい場合、行列は対角行列とみなされます。
bool MatrIsIdentity(double& matr[], int numRows, int numCols, double tolerance);
この関数は、行列が単位行列であるかどうかをチェックします。
bool MatrIsSymmetric(double& matr[], int numRows, int numCols, double tolerance);
この関数は、行列が対称であるかどうかをチェックします。(つまり、任意のiとjのインデックスにはa[i][j]=a[j][i]が実行されます)
bool MatrIsAntisymmetric(double& matr[], int numRows, int numCols, double tolerance);
この関数は、行列が歪対称行列または反対称行列かをチェックします。(つまり、任意のiとjのインデックスにはa[i][j]=-a[j][i]が実行され、任意のkにはa[k][k]=0が実行されます)
bool MatrEqual(double& matr1[], double& matr2[], int numRows, int numCols, double tolerance);
この関数は、同じサイズの行列の要素ごとの等しさをチェックします。
2.7. 要素単位のスカラー演算
要素単位のスカラー演算の関数のグループを見てみましょう。このグループの特徴は、関数で処理した結果が、matr行列の最初の配列に格納されず、新しい配列のresultに格納されることです。もしresultパラメータとして、またmatrパラメータとしてその配列を引き渡す場合、結果は初期の行列に格納されます。
void MatrAddScalar(double& matr[], int numRows, int numCols, double scalar, double& result[]);
この関数は、scalar値を行列の各要素に追加します。
void MatrSubScalar(double& matr[], int numRows, int numCols, double scalar, double& result[]);
この関数は、scalar値を行列の各要素から差し引きます。
void MatrMulByScalar(double& matr[], int numRows, int numCols, double scalar, double& result[]);
この関数は、行列の各要素にscalar値を掛けます。
void MatrDivByScalar(double& matr[], int numRows, int numCols, double scalar, double& result[]);
この関数は、行列の各要素をscalar値で割ります。
2.8. 行列の基本操作
行列の基本操作のグループは、加算/減算/乗算などの基本的な計算を実行します。、関数の処理結果は(MatrTrace以外)、result配列に返されます。
void MatrAddMatr(double& matr1[], double& matr2[], int numRows, int numCols, double& result[]);
この関数は、同じサイズの二つの行列を加算します。
void MatrSubMatr(double& matr1[], double& matr2[], int numRows, int numCols, double& result[]);
この関数は、matr1の行列からmatr2の行列を引きます。行列は同じサイズである必要があります。
void MatrMulMatr( double& matr1[], int numRows1, int numCols1, double& matr2[], int numRows2, int numCols2, double& result[], int& numRowsRes, int& numColsRes);
この関数は、matr1の行列にmatr2の行列を掛けます。第一の行列の列の数(numCols1)は、第二の行列の行数(numRows2)と等しい必要がある為、行列は乗算の為に互換性がある必要があります。行列の結果のサイズは、numRowsRes とnumColsResパラメータで引き渡される参照変数で返されます。
double MatrTrace(double& matr[], int numRows, int numCols);
この関数は、行列のトレース(対角要素の和)を算出します。行列は正方行列でなければなりません。
void MatrTranspose(double& matr[], int numRows, int numCols, double& result[], int& numRowsRes, int& numColsRes);
この関数は行列の転置を行います。(列と行の場所を入れ替えます)新しい行列のサイズは、numRowsResとnumColsResでパラメータで引き渡される参照変数で返されます。
2.9. その他の関数
その他の関数のグループは、行列の反転、ランクと行列式の算出などを可能にする為に作成されました。
int MatrGaussianElimination(double& matr[], int numRows, int numCols, double& result[], double tolerance);
この関数は、ピボットを使用したガウス消去法の実装です。numRows行とnumColsの列からなるmatr初期行列は、台形(三角形)の形になり、結果はresult配列に格納されます。
戻り値は行列の線形独立の行の数です。(つまり、そのランク)なぜなら、計算は関数の四捨五入の間違いをもたらし、比較の精度を指定する、正のtoleranceパラメータを引き渡す必要があります。
bool MatrGJBatchSolve( double& matr[], int numRows, int numCols, double& rhs[], int numRowsRHS, int numColsRHS, double& roots[], int& numRowsRoots, int& numColsRoots, double tolerance );
この関数は、同一の行列システムの幾つかの方程式のシステムから組み合わせを決める為にある、ジョルダン・ガウスの消去法を実装したものです。
システムの一般的な行列は、matr(データ配列)、numRows やnumCols(サイズ)のパラメータへ引き渡されます。右辺ベクトルの集合も行列として渡され(rhsパラメータ)、その各列はシステムの右辺を表しています。
つまり、numRowsRHS パラメータは、システム内の方程式の数(システムの行列の行数)を含み、numColsRHSパラメータは右辺ベクトルの数(解く必要のあるシステム数)を含んでいます。
結果はnumRowsRoots行とnumColsRoots列からのroots行列として返され、方程式の各システムの解は行列の列に配置され、指定されたシステムの行列と、rhs行列の番号と対応する列からのシステムの解と一致します。この関数は、方程式のシステムのセットが見つかった場合、trueを返します。
void MatrMinor( double& matr[], int numRows, int numCols, int rowToExclude, int colToExclude, double& result[], int& numRowsRes, int& numColsRes);
この関数は、rowToExclude行とcolToExclude列の交差点にある行列要素の小行列式を取得します。結果の行列要素は、result配列に返され、サイズはnumRowsResとnumColsResのパラメータへ引き渡される参照変数に返されます。
double MatrAlgebraicComplement(double& matr[], int numRows, int numCols, int elemRow, int elemCol, double tolerance);
この関数は、elemRow行とelemCol列の交差点にある要素の為の代数的補因子を算出します。行列式の計算には、toleranceパラメータで指定された精度を持つガウス消去法(MatrGaussianElimination関数として)が使用されます。
bool MatrInvertUsingMinors(double& matr[], int numRows, int numCols, double& result[], double tolerance);
この関数は、隣接行列によって正方行列matrの逆行列を計算します。アルゴリズムの漸近的複雑性はO(N^5)です。隣接行列の計算には、(順番にガウス消去法を使用する)MatrAlgebraicComplement関数を使用します。これに関連して、関数に精度を指定する正数のtoleranceパラメータを引き渡す必要があります。結果の行列の要素は、result配列に格納されます。逆行列の計算に成功した場合(行列式がゼロ以外である)、関数はtrueを返します。
bool MatrInvertUsingGJ(double& matr[], int numRows, int numCols, double& result[], double tolerance);
この関数は指定された正方行列matrの逆行列を、単位行列を書き足すことで算出します。つまり、MatrGJBatchSolve関数を使用する際(右辺行列は単位行列であり、精度はtoleranceパラメータの正数で指定されます)、線形方程式のシステムのNを解きます。(N=numRows=numCols)アルゴリズムの漸近的複雑性はO(N^3)です。結果の行列の要素は、result配列に格納されます。逆行列の計算に成功した場合(行列式がゼロ以外である)、関数はtrueを返します。
double MatrDet(double& matr[], int numRows, int numCols, double tolerance);
この関数は、正方行列を台形(三角形)にすることで、正方行列matrの行列式を算出します。ガウス消去法はtolerance の精度で使用されます。(このパラメータは正数である必要があります)アルゴリズムの漸近的複雑性はO(N^3)です。
double MatrDetTriang(double& matr[], int numRows, int numCols);
この関数は、三角行列matrの行列式を算出します。
double MatrRank(double& matr[], int numRows, int numCols, double tolerance);
この関数は、行列を台形(三角形)にすることで、matrの行列ランクを算出します。ガウス消去法はtolerance の精度で使用されます。(このパラメータは正数である必要があります)アルゴリズムの漸近的複雑性はO(N^3)です。
double MatrRankTriang(double& matr[], int numRows, int numCols, double tolerance);
この関数は、matrの行列ランクを算出します。行列は台形(三角形)にする必要があります。(例えば、MatrGaussianElimination関数を呼び出して)正数のtoleranceパラメータが、最初のヌル行の検索の精度を指定します。
void MatrComputeConnectedMatr(double& matr[], int numRows, int numCols, double& result[], double tolerance);
この関数は、正方行列matrの為の隣接行列を算出します。結果の行列の要素は、result配列に格納されます。正数のtoleranceパラメータは、補因子の計算に使用する精度を定義します。
void MatrLerpMatr(double& matr1[], double& matr2[], int numRows, int numCols, double balance, double& result[]);
この関数は、同じサイズのmatr1とmatr2の間に、要素の線形補間を行います。balanceパラメータは、線形補間係数を表し、[0;1]の範囲にある必要があります。結果の行列の要素は、result配列に返されます。
2.10. 入出力関数
入出力関数のグループは、行列の保存・ダウンロード、またログへの行列のデバッグの出力を行う為に作成されています。
void MatrPrint(double& matr[], int numRows, int numCols);
この関数は、ログへデバッグの出力を行う為のものです。(『ターミナル』パネル、『エキスパート』タブ)行列は、ターミナルのログに逆の順序で表示される事を考慮し、行単位で出力されます。(つまり、新しいものが上部に表示され、関数はログに行列の視覚的分析を簡易化する為、終りから始まる行列の行を出力します)
void FileWriteMatr(double& matr[], int numRows, int numCols, int handle);
この関数は、行列(サイズを含む)を、handleパラメータに引き渡されるファイルに保存します。ファイルはFILE_BIN|FILE_WRITEモードで開かれている必要があります。
void FileReadMatr(double& matr[], int& numRows, int& numCols, int handle);
この関数は、ファイルから行列をダウンロードします。まず初めに、numRowsとnumColsパラメータに引き渡された参照変数に、行列のサイズが計算され、それからmatr配列のサイズが行列のサイズに合わせて変更され、その後でファイルからmatr配列の内容のダウンロードが行われます。ハンドルファイルからの読み取りはhandleパラメータに引き渡され、ファイルはFILE_BIN|FILE_READモードで開かれている必要があります。
これで列挙した関数を読者に理解していただけたと思うので、ライブラリを使用した実用的な例に移りたいと思います。
3. 使用例
提案されたライブラリを使った、価格の一連の多項式回帰の作成例を見てみましょう。
多項式回帰の作成は、degree次のf(x)=a[0]+a[1]*x+...+a[degree]*x^degree 多項式の係数を求めることにあります。これは、システムの行列要素A[degree+1][degree+1]が、以下の方法で定義される、線形代数方程式を解く事によって行われます。: A[i][j]=(x[0]^(i+j)+x[1]^(i+j)+...+x[numPoints]^(i+j))/numPoints、そしてB[degree+1][1]の右辺ベクトルの要素は、B[i]=(y[0]*x[0]^i+y[1]*x[1]^i+...+y[numPoints]*x[numPoints]^i)/numPointsの式で定義されます。
提示された課題を解くために、多項式を作成し、初期間隔に右側に多項式が表示される(つまり、外挿)スクリプトを書きました(添付ファイルLibMatrixEx.mq4)外挿間隔での多項式の値は、価格変動の傾向を予測することができます。
スクリプトの操作は3つの垂直線を使って行われます。2つの線は分析間隔の選択の為に使用され、3つ目は多項式の表示が行われる極右点の選択に使用されます。
スクリプトの動作の為には、チャートに必要なパラメータを移し、設定する必要があります。delayは、チャートの更新周期(ミリ秒単位)、linesMarginは、扱う線の間の初期距離、linesWidthは、多項式チャートの線の幅です。また、扱う垂直線(colVLineInt とcolVLineExtのパラメータ)と、チャートの線(colIntとcolExtのパラメータ)の色を選択することもできます。
スクリプトの動作例
行列を使って実行するスクリプトの主要な機能を見てみましょう。
// 多項式回帰の作成 bool Regression(double& x[], double& y[], int numPoints, int polyDegree, double& poly[]) { // システムの行列を作成します double A[]; int numRowsA = polyDegree + 1; int numColsA = polyDegree + 1; MatrSetSize(A, numRowsA, numColsA); // 行列を埋める for (int row = 0; row < numRowsA; row++) { for (int col = 0; col < numColsA; col++) { int offset = MatrIndiciesToOffset(row, col, numRowsA, numColsA); A[offset] = SXY(x, y, numPoints, row + col, 0); } } // 右辺ベクトルを作成します double B[]; int numRowsB = polyDegree + 1; int numColsB = 1; MatrSetSize(B, numRowsB, numColsB); // 右辺ベクトルを埋めます for (row = 0; row < numRowsB; row++) { offset = MatrIndiciesToOffset(row, 0, numRowsB, numColsB); B[offset] = SXY(x, y, numPoints, row, 1); } // 線形代数連立方程式を解く int numRowsX, numColsX; bool status = MatrGJBatchSolve( A, numRowsA, numColsA, B, numRowsB, numColsB, poly, numRowsX, numColsX, DEFAULT_TOLERANCE ); if (!status) { Print(『システムを解く際にエラーが発生しました』); } return (status); }
関数は5つの引数を受け取ります。初めの2つは、多項式の作成が行われる座標点を保存する為のxとyの配列です。numPoints引数で点の数は渡されます。4つ目の引数は、求める多項式のレベルを指定します。(レベルは少なくとも1よりも少ない必要があります)5つ目の引数は、多項式の係数を受け取る配列への参照です。
初めに行列 Aが作成され、そのサイズの変更や上記に引用された式による記入が行われます。2次元インデックスで行列の要素を参照するために、MatrIndiciesToOffset関数が使用されます。
ベクトル列Bは、同様の方法で記入されます。MatrGJBatchSolveを呼び出し、すべての多項式の係数を求めます。
配置された線の検証と多項式の出力を行うその他のコードは、読者に困難を生じさせるものではないと思います。
まとめ
行列を扱う為のライブラリを提案し、実装された関数とその特徴を研究しました。
ローソクの終値による多項式回帰の作成例により、ライブラリの使用をデモンストレーションしました。
コードは綿密にテストされていますが、エラーがある可能性があります。もしエラーを見つけた場合は、その事について私に知らせていただければと思います。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/1365
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索