恥は/dev/nullへ by 初心者

プログラミング素人がのろのろと学んだことをつづっています♪

MT5でボリンジャーバンドの幅を調べるスクリプト

ボラティリティが無いとトレードしづらいので、トレード前にボリンジャーバンド(以下「BB」)の様子を見ています。しかし、手作業で主要通貨ペアのチャートを見るのは手間です。

そこで、BBの幅をエキスパートに出力するスクリプトを書いてみました。
なお、幅は(ポイント単位ではなく)pips単位で出力されます。


この記事に載っているコードは、次のようなセッティングになっています。

・計算するのは現在足時点
・計算期間は20
・BBの偏差は2
・対象は1時間足
・BBの幅が40pips以上の通貨ペアを出力


// BBの幅を調べるスクリプト
//     1時間足、期間 20、2シグマ、対象は現在足、pips数で出力
//     BBの幅が40pips以上の通貨ペアを出力

#property copyright "Copyright 2024, PHILOJUAN."
#property version   "1.00"
#property strict
#property script_show_inputs

#define NUM_SYMBOL 28        // 通貨ペアの個数

input int    InpTargetBar = 0;     // 対象とするbar (0は現在足, 1は1本前)
input int    InpPeriod    = 20;    // BBの計算期間
input double InpDeviation = 2.0;   // BBの偏差
input ENUM_TIMEFRAMES InpTimeFrame = PERIOD_H1; // 時間軸
input int    InpTargetWidth = 40;  // 基準値(BB幅 N pips以上を抽出)

// 通貨ペア名配列
string symbols[NUM_SYMBOL] = {
    "AUDCAD", "AUDCHF", "AUDJPY", "AUDNZD", "AUDUSD",
    "CADCHF", "CADJPY", "CHFJPY",
    "EURAUD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD", "EURUSD",
    "GBPAUD", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "GBPUSD",
    "NZDCAD", "NZDCHF", "NZDJPY", "NZDUSD",
    "USDCAD", "USDCHF", "USDJPY"
};

void OnStart()
{
    int count = 0;
    for(int i = 0; i < NUM_SYMBOL; i++) {
        double width = ReturnPipsOfBBWidth(symbols[i], InpTargetBar, InpPeriod, InpDeviation, InpTimeFrame);
        if(width >= InpTargetWidth) {
            count++;
            printf("[%s][%s] BBの幅(pips) = %s",
                   symbols[i], TimeFrameToString(InpTimeFrame), DoubleToString(width, 1));
        }
    }

    if(count == 0)
        printf("BB幅 %d pips以上の通貨ペアはありませんでした", InpTargetWidth);
}


//-----------------------------------------------------------------------------------
// ボリンジャーバンドの幅をPips数で返す関数(範囲は1シグマから3シグマまで)
//-----------------------------------------------------------------------------------
double ReturnPipsOfBBWidth(string symbol,        // 通貨ペア名
                           int    startPosition, // 対象(チャートの右端からN本前)
                           int    calcPeriod,    // 計算期間
                           double sigma,         // シグマ (2.0や3.0等)
                           ENUM_TIMEFRAMES tf    // 時間軸 (PERIOD_M30等)
)
{
    //----------------------
    // パラメーターチェック
    //----------------------
    int barMax      = Bars(symbol, tf) - 1;        // 最古足
    int requiredBar = startPosition + calcPeriod;  // 計算に使用するbarのうち一番古いもの

    if(startPosition < 0 || startPosition > barMax || barMax < requiredBar ||
       calcPeriod < 2    || sigma < 1.0            || sigma > 3.0) {
        Print("[ReturnPipsOfBBWidth Function] parameter is wrong.");
        return -1.0;
    }

    //----------------------
    // BBの計算
    //----------------------
    double sum         = 0.0;
    int    endPosition = startPosition + calcPeriod - 1;
    int    digits      = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
    // [memo] SYMBOL_DIGITSはintを返すのでint型にキャスト

    for(int i = startPosition; i <= endPosition; i++) {
        sum += iClose(symbol, tf, i);
    }
   
    double avg = sum / calcPeriod;
    sum = 0.0;                                // 変数をリセット

    for(int i = startPosition; i <= endPosition; i++) {
        sum += MathPow((iClose(symbol, tf, i) - avg), 2);
    }

    double variance  = sum / calcPeriod;      // 分散
    double deviation = MathSqrt(variance);    // 偏差

    double upper = avg + sigma * deviation;   // Upperバンド
    double lower = avg - sigma * deviation;   // Lowerバンド
    double diff  = upper - lower;             // バンド幅
    double multiplier = MathPow(10, digits);  // 乗数
    diff = diff * multiplier / 10;            // バンド幅(Pips単位)

    return diff;
}

//-----------------------------------------------------------------------------------
// 時間足を「H1」等の短縮表現に変換する関数
//-----------------------------------------------------------------------------------
// 例 PERIOD_H4   →   H4
string TimeFrameToString(ENUM_TIMEFRAMES intTF)
{
    if(intTF == PERIOD_CURRENT) intTF = Period();
    return StringSubstr(EnumToString(intTF), 7);
}

MT5のインディケーターが出力する値に関する疑問

この誤差は何?

EAの中で平均足を計算するコードを作成し、動作チェックを行いました。具体的には、EAで計算した平均足の値と、MT5のHeiken_Ashiインディケーターがデータウィンドウに表示した値を比較しました。

(備考)MT5においては、Heikin_Ashiではなく、Heiken_Ashiとなっています。

2023年のヒストリカルデータから生成したGBPUSDの15分足チャートを使って比較したところ、場合によって1ポイントの誤差が生じていることに気づきました。

// EAから出力した値
2023.10.31 22:45  close = 1.21531
2023.10.31 23:00  close = 1.21511

// Heiken_Ashiインディケーターがデータウィンドウに表示した値
2023.10.31 22:45  close = 1.21531
2023.10.31 23:00  close = 1.21512

23:00の行を見ると、EAから出力した値とHeiken_Ashiインディケーターがデータウィンドウに表示した値が異なっています。

切上げも四捨五入もされていない?

平均足の終値は、その足の4値(始値、高値、安値、終値)の平均値です。そこで、23:00の4値を調べて、電卓で計算してみました。計算結果は「1.215115」でした。

この結果を見た時は「MT5のHeiken_Ashiインディケーターでは、小数点第6位を見て切上げまたは四捨五入を行っている」と考えました。

しかし、そうではありませんでした。

22:45のローソク足の4値を元に平均値を電卓で計算したら、結果は「1.215315」でした。もしMT5のHeiken_Ashiインディケーターが切上げまたは四捨五入を行っているのなら、データウィンドウに表示される値は「1.21532」となるはずです。しかし、データウィンドウに表示されている値は以下のとおりでした。

// Heiken_Ashiインディケーターがデータウィンドウに表示した値
2023.10.31 22:45  close = 1.21531

これを見ると、切上げも四捨五入もされていません。

以下は、電卓で計算した値です。

// 4値を元に電卓で計算した値
2023.10.31 22:45  close = 1.215315
2023.10.31 23:00  close = 1.215115


4値の中身に問題は無し

念のため4値の中身も調べました。私がEAで計算する際に用いた4値と、MT5のHeiken_Ashiインディケーターが計算するのに用いた4値との間に誤差があるのだろうか、と。

しかし、Print文を用いて出力したログを見ると、EAの計算でもHeiken_Ashiインディケーターの計算でも同じ4値が使用されていました。

ついでに書いておきますと、データを出力する際にはDoubleToString(値, _Digits)を使用していました。このためGBPUSDの場合、小数点第5位までしか表示されません。

もし4値データに小数点第6位や第7位が存在する場合、それらが計算結果に影響を与えた可能性があります。

そこで、この実験に使用したヒストリカルデータをチェックしました。しかし、始値、高値、安値、終値のデータはいずれも小数点第5位までで、第6位以下は存在しませんでした。

実験したこと

Heiken_Ashiインディケーターのコードを見たところ、「切上げ、切捨て、四捨五入」といった処理を行っているようには見えなかったので、バッファに入れる数値によって出力される結果が変わっているのかなと想像しました。

Heiken_Ashiインディケーターのコードには以下の部分があります。

double ha_open  = (ExtOBuffer[i - 1] + ExtCBuffer[i - 1]) / 2;
double ha_close = (open[i] + high[i] + low[i] + close[i]) / 4;
double ha_high  = MathMax(high[i], MathMax(ha_open, ha_close));
double ha_low   = MathMin(low[i], MathMin(ha_open, ha_close));

ExtLBuffer[i] = ha_low;
ExtHBuffer[i] = ha_high;
ExtOBuffer[i] = ha_open;
ExtCBuffer[i] = ha_close;

このうち、最後の4行の右辺を(変数ではなく)具体的な数値に置き換えて、データウィンドウに表示される値を見てみました。

// パターンA    小数点第6位は全て5で、第5位を1~4とした場合
ExtOBuffer[i] = 1.215115;       → 1.21512   切上げ or 四捨五入?
ExtHBuffer[i] = 1.215125;       → 1.21513   切上げ or 四捨五入?
ExtLBuffer[i] = 1.215135;       → 1.21514   切上げ or 四捨五入?
ExtCBuffer[i] = 1.215145;       → 1.21514   切捨て?  

// パターンB    小数点第5位・6位は全て15で、第4位を1~8とした場合
ExtOBuffer[i] = 1.215115;       → 1.21512   切上げ or 四捨五入?
ExtHBuffer[i] = 1.215215;       → 1.21522   切上げ or 四捨五入?
ExtLBuffer[i] = 1.215315;       → 1.21531   切捨て?
ExtCBuffer[i] = 1.215415;       → 1.21541   切捨て?

ExtOBuffer[i] = 1.215515;       → 1.21551   切捨て?
ExtHBuffer[i] = 1.215615;       → 1.21561   切捨て?
ExtLBuffer[i] = 1.215715;       → 1.21572   切上げ or 四捨五入?
ExtCBuffer[i] = 1.215815;       → 1.21582   切上げ or 四捨五入?

矢印より右側の数値がデータウィンドウに表示された値です。

いずれも小数点第5位がどのように変化するのかをチェックしました。
パターンAを見ると、小数点第6位が全て同じ値にも関わらず、第5位に生じる影響が一貫していません。

パターンBでは、第6位を全て5、第5位を全て1にして、第4位を変化させてみました。しかし、こちらにも一貫性が無いように見えます。

念のため、代入先のバッファを入れ替えてみました(たとえば、ExtOBufferに代入する代わりに、ExtCBufferに代入)。しかし、代入先バッファが異なることによる影響は無さそうでした。

よって、どんな数値を代入するかによって結果が変わることは間違いなさそうですが、そこにどんな規則性があるのかは不明です。

もしかしたら、10進法で考えるのではなく、ビット演算など他の次元で考えたら答えが得られるかもしれませんが、自分にはその技量がありません(汗)。

MacでVimのcolorschemeが反映しない

初歩的なミスで少し時間を失いました。

Windows上で使っていたcolorschemeファイルをMac上に移動して

.vim/colors

の中に配置しましたが、ファイル内容が反映されませんでした。

しばらく考えた後、Windows用の改行コードのままになっていることに気づきました。そこで、以下のようにお決まりの手順で改行コードを修正しました。

1. Mac上でcolorschemeファイルを開く。

2. 不要な改行コードを可視化するために次のコマンドを実行
       :e ++ff=unix  

3. sコマンドで ^M を除去
       :%s/^M//g  

なお、^Mは特殊な文字なので「CTRL + V CTRL + M」で入力します。

MT5でspreadを取得する際の注意点

MT5でspreadの値を取得する方法はいくつかありますが、以下の記事によると、取得する方法によって値が異なる場合があるようです。

https://fmdsm.blog.fc2.com/blog-entry-94.html

上記記事の内容から「MqlRates.spread」「iSpread()」「spread[]」を使って取得したspreadの値は余り信用できないので、以下の2つを使って調べた方が無難だと感じました。

SymbolInfoInteger
SymbolInfoTick


spreadの値に疑問を感じたのは、OnCalculate関数の中にあるspread[]を使ってみたからです。そこで、ちょっと実験をしてみました。

実験したこと

以下のコードを実行してみました。

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[])
{
    ArraySetAsSeries(spread, true);

    double iclose  = iClose(NULL, PERIOD_M5, 0);
    double sp  = spread[0] * _Point;
    double symbolinfo_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double symbolinfo_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

    static int count = 0;

    if(count == 0) {
        printf("iclose = %f", iclose);
        printf("symbolinfo_bid = %f", symbolinfo_bid);
        printf("symbolinfo_ask = %f", symbolinfo_ask);
        printf("spread = %f", sp);
        printf("ask - bid = %f", symbolinfo_ask - symbolinfo_bid);
        count++;
    }
    return(rates_total);
}


次の結果がエキスパートに表示されました。
(各行の先頭にある〇行目は説明の便宜上、後付けしました)

1行目  iclose = 0.855450
2行目  symbolinfo_bid = 0.855450
3行目  symbolinfo_ask = 0.855480
4行目  spread = 0.000000
5行目  ask - bid = 0.000030

1行目と2行目はBid価格です。
3行目はAsk価格です。
spreadはAsk価格とBid価格の差です。3行目までを見るとAsk価格とBid価格に差があるのですが・・・、4行目を見るとspreadは0です。

このことから、spread[0]の値は正しくないと思われます(ただし、上記コードで取得したAsk価格やBid価格が正しければ、の話ですが)。

そこで、spread[0]の代わりにSymbolInfoIntegerでspreadの値を取得してみました。
それが以下のコードです。

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[])
{
    ArraySetAsSeries(spread, true);

    double iclose  = iClose(NULL, PERIOD_M5, 0);
    double sp  = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point;
    double symbolinfo_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double symbolinfo_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

    static int count = 0;

    if(count == 0) {
        printf("iclose = %f", iclose);
        printf("symbolinfo_bid = %f", symbolinfo_bid);
        printf("symbolinfo_ask = %f", symbolinfo_ask);
        printf("spread = %f", sp);
        printf("ask - bid = %f", symbolinfo_ask - symbolinfo_bid);
        count++;
    }
    return(rates_total);
}


結果はこうなりました。

1行目  iclose = 0.855440
2行目  symbolinfo_bid = 0.855440
3行目  symbolinfo_ask = 0.855490
4行目  spread = 0.000050
5行目  ask - bid = 0.000050

今度はspreadの値が、Ask価格とBid価格の差になっています。
よって、このspreadの値は正しいと思われます。

MacのVimでフォント設定

Debianで使っているvimrcファイルの一部をMac上のvimrcにコピペしたら、フォントに関するエラーが表示されました。

エラーが表示された時の設定

set guifont=HackGen\ 13 guifontwide=HackGen\ 13


調べたところ、WindowsMacでは次のようにする必要があると分かりました。

set guifont=HackGen:h13 guifontwide=HackGen:h13


ざっくり言うと、Linuxではフォント名とフォントサイズの間を「\ 」(バックスラッシュとスペース)にして、WindowsMacではフォント名とフォントサイズの間を「:h」にします。


まとめると、次のとおりです。

Linuxの場合
set guifont=フォント名\ フォントサイズ     (半角文字)
set guifontwide=フォント名\ フォントサイズ (全角文字)

WindowsやMacの場合
set guifont=フォント名:hフォントサイズ     (半角文字)
set guifontwide=フォント名:hフォントサイズ (全角文字)

Macのターミナルでコマンドプロンプトを少し変更

Macのターミナルは、デフォルトだとコマンドプロンプトがちょっと寂しい状況です(「デフォルトだと」と書きつつzshからbashに変更してありますが)。 白地の背景に黒い文字のみで、カレントディレクトリ名は表示されているものの、パスが表示されていません。

そこで、

hoge@Neko:~/Documents/buta$

のように現在位置をパスで表示して、ちょっと色を付けることにしました。

下記のように、コマンドプロンプトの色や書式を変更するにはPS1という変数に内容をセットします。

export PS1='\e[32m\u@\h\e[0m:\e[38;5;56m\w\e[0m$ '


パーツの説明を書いておきます。

\u はユーザー名
\h はホスト名(Macの名前)
\w はカレントディレクトリのパス


次に色の説明です。

\e[32m とすることで、それ以降の文字色がグリーンになります。
この結果、\u@\hの部分がグリーンになります。

\e[0m  とすることで、それ以降の文字色が黒になります。
この結果、 : が黒になります。

\e[38;5;56m とすることで、それ以降の文字色がパープルになります。
この結果、\wの部分がパープルになります。

\e[0m  とすることで、それ以降の文字色が黒になります。
この結果、 $ が黒になります。


\e[38;5;56m だけ少し分かりにくいので補足します。

\e[38はこの後に2つの引数を取ります。ex. \e[38;N;Nm
1つ目の引数を5にすると、2つ目の引数で0〜255を使って色を指定できます。

\e[38;5;56m

この場合、0〜255のうち、56番目の色を指定しています。

しかし、それぞれがどんな色か分からないので以下のスクリプトを使って色を出力してみました。

for ((i = 0; i <= 255; i++)); do
    printf '\e[38;5;%dm%d ' $i $i
done

printf '\e[0m'

echo 


出力結果は次のとおりです。


これを参考にして56番目の色を選びました。

Macでlocateとかupdatedbとか

Macでlocateコマンドを使うためにupdatedbを実行したら上手くいきませんでした。
manページを読んでみると、

/usr/libexec/locate.updatedb  

を実行すれば良いようです。

そこで、

$ sudo /usr/libexec/locate.updatedb

を実行してみら、新たなエラーメッセージが出ました。ホームディレクトリで行ったのが良くなかったようです。

次のようにルートディレクトリに移動して実行したら上手くいきました。

$ cd /
$ sudo /usr/libexec/locate.updatedb