サインツールの書き方と勝率の表示

巷にあふれるサインツールの書き方を学習する。

サインツールとは

一般的にサインツールは、カスタムインジケータだ。
どこで売買を行うか上矢印 or 下矢印で表示する。その際アラートが鳴ることもある。
自動売買は行わず、トレードは使用者の裁量に委ねている。

サインの出し方は主に2通りあると考えられる。
1つは、インジケータバッファを用いて矢印を表示するもの。
もう1つは、矢印のオブジェクトを指定した位置に描画していくもの。

前者はインジケータバッファにサインのデータが入っているため、サインツールのソースコードが手元に無くても、他のカスタムインジケータやEAなどからデータの参照・使用が容易になる。
そのため、そのサインツールをEA化し、自由にバックテストで回すということも非常に簡単に行える。
また、矢印はインジケータバッファなので、インジケータを取り除けば矢印も一緒に消える。

後者のオブジェクトを描画するタイプの場合であっても、オブジェクトが新しく生成されたかどうかを監視すればEA化することはできるだろうが、EAにおける一般的なインジケータの利用とは異なる事から、かなり億劫に感じる。

今回は前者のタイプを書いてみる。

ロジックの定義と注意点

ここでは単純なサインツールの完成を目標にする。

ロジック検証ではないので、ルールは単純なものにして、レートがボリンジャーバンドを抜けたら逆張りサインを出すことにする。
以下のように定義。

  • そのバーの始値がボリンジャーバンド内に収まっており、現在値(Close)がボリンジャーバンドを超えた時にサインを出す。
  • サインを出した後に、そのバーが条件を満たさなくなった場合はサインを消す。
    バーが確定した際に条件を満たしていればサインを残す。
  • 過去チャートについては、全てのバーが確定しているため、始値/終値を確認してサインを置く。
  • サインが置かれて確定した後は、以下の条件が満たされるまで新しいサインは出さない。
    上方向サイン(レートは下降)を出した後は、陽線でもってボリンジャーバンド内に戻る。
    下方向サイン(レートは上昇)を出した後は、陰線でもってボリンジャーバンド内に戻る。
    ※ボリンジャーバンドを抜けたバーの次バーの始値がボリンジャーバンド内に位置するケースがあるため。
    ※バンド内に戻るどころか突き抜けていった場合は無視。再度バンド内に値が落ち着くまで待機する。

サインツールを書く際の注意点として、基本的にバーを未来から過去へ遡ってはいけない。
逆走したサインツールを書くと、大体の場合において勝率が非常に高くなる。

未来から順に見るということは、本来分からないはずの未来のバーが、探している条件を満たしていないことを確認してしまうことになるためだ。

MAのようなカスタムインジケータであれば、対象のポジションから過去何本分かを計算すれば良いだけなので問題無いが、サインツールの場合は時系列を守らなければならない。
逆走してることを意識すれば書けなくは無いと思うが、多分面倒なのでオススメしない。

バッファを使ったサインツールを書く

まずは、確定したバーに対してサインを置いていく記述を行う。
分かりやすくするために、確定したバーと確定していないバーのそれぞれに対する処理は、if文で完全に分離させる。

//MT4用
#property copyright "nisai"
#property link      "https://nisaifx.com"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_width1 2
#property indicator_color1 DeepSkyBlue
#property indicator_width2 2
#property indicator_color2 OrangeRed

input int bandsPeriod=20;
input double bandsDeviation=2.0;
input int bandsShift=0;
input ENUM_APPLIED_PRICE bandsAppliedPrice=PRICE_CLOSE;

double buySignBuffer[];
double sellSignBuffer[];

int state=0;   //1:買いサイン後, -1:売りサイン後

int OnInit(){
   SetIndexBuffer(0,buySignBuffer);
   SetIndexStyle(0,DRAW_ARROW);
   SetIndexArrow(0,233);   //Up arrow

   SetIndexBuffer(1,sellSignBuffer);
   SetIndexStyle(1,DRAW_ARROW);
   SetIndexArrow(1,234);   //Down arrow

   return(INIT_SUCCEEDED);
}

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 position=rates_total-1;
   if(prev_calculated!=0){
      position=rates_total-prev_calculated;
   }
   for(int i=position;i>=0&&!IsStopped();i--){
      double upper=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_UPPER,i);
      double lower=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_LOWER,i);
      if(i==0){
         //確定していないバーの処理
      }else{
         //確定したバーの処理
         if(state==0){
            if(open[i]<upper && open[i]>lower){
               if(close[i]>upper){
                  sellSignBuffer[i]=high[i]+20*Point(); //高値より少し上に売りサインを置く
                  state=-1;
               }else if(close[i]<lower){
                  buySignBuffer[i]=low[i]-20*Point();   //安値より少し下に買いサインを置く
                  state=1;
               }
            }
         }else if(state==1){
            //買いサイン後
            if(open[i]<close[i] && close[i]>lower){
               state=0;
            }
         }else if(state==-1){
            //売りサイン後
            if(open[i]>close[i] && close[i]<upper){
               state=0;
            }
         }
      }
   }
   return rates_total;
}

チャートに適用してみる。

意図した通りにサインが置かれている。

続いて、インジケーターを適用した後の値動き(確定していないバー)でのサイン表示を行う。
バーが確定していなくても、現在レートはcloseで参照できる(Bidでも良い)。
レートがボリンジャーバンドを抜けたり戻ったりする度にサインを出したり消したりするので、stateの更新(サインの確定)は、そのバーが確定した後に行う。

バーが確定した際は、rates_totalとprev_calcuratedの差が”1”になるため、上記「確定したバーの処理」に移り、現在バーから1本前のバーに対する処理が1度だけ行われる。
その際、もしサインが置かれたままであればstateを更新する。

下記、ハイライト箇所を追記。

//MT4用
#property copyright "nisai"
#property link      "https://nisaifx.com"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_width1 2
#property indicator_color1 DeepSkyBlue
#property indicator_width2 2
#property indicator_color2 OrangeRed

input int bandsPeriod=20;
input double bandsDeviation=2.0;
input int bandsShift=0;
input ENUM_APPLIED_PRICE bandsAppliedPrice=PRICE_CLOSE;

double buySignBuffer[];
double sellSignBuffer[];

int state=0;   //1:買いサイン後, -1:売りサイン後

int OnInit(){
   SetIndexBuffer(0,buySignBuffer);
   SetIndexStyle(0,DRAW_ARROW);
   SetIndexArrow(0,233);   //Up arrow

   SetIndexBuffer(1,sellSignBuffer);
   SetIndexStyle(1,DRAW_ARROW);
   SetIndexArrow(1,234);   //Down arrow

   return(INIT_SUCCEEDED);
}

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 position=rates_total-1;
   if(prev_calculated!=0){
      position=rates_total-prev_calculated;
   }
   for(int i=position;i>=0&&!IsStopped();i--){
      double upper=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_UPPER,i);
      double lower=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_LOWER,i);
      if(i==0){
         //確定していないバーの処理
         if(state==0){
            if(open[i]<upper && open[i]>lower){
               if(close[i]>upper){
                  sellSignBuffer[i]=high[i]+20*Point();
               }else if(close[i]<lower){
                  buySignBuffer[i]=low[i]-20*Point();
               }else{
                  sellSignBuffer[i]=0;
                  buySignBuffer[i]=0;
               }
            }
         }
      }else{
         //参照しているポジション(i)に既にサインが置かれている場合はstateを更新
         if(sellSignBuffer[i]==high[i]+20*Point()){
            state=-1;
            return rates_total;
         }else if(buySignBuffer[i]==low[i]-20*Point()){
            state=1;
            return rates_total;
         }
         //確定したバーの処理
         if(state==0){
            if(open[i]<upper && open[i]>lower){
               if(close[i]>upper){
                  sellSignBuffer[i]=high[i]+20*Point(); //高値より少し上に売りサインを置く
                  state=-1;
               }else if(close[i]<lower){
                  buySignBuffer[i]=low[i]-20*Point();   //安値より少し下に買いサインを置く
                  state=1;
               }
            }
         }else if(state==1){
            //買いサイン後
            if(open[i]<close[i] && close[i]>lower){
               state=0;
            }
         }else if(state==-1){
            //売りサイン後
            if(open[i]>close[i] && close[i]<upper){
               state=0;
            }
         }
      }
   }
   return rates_total;
}

これで、リアルタイムチャートでもサインが表示されるようになった。
単純なサインツールであれば、以上で完了になる。

勝敗条件と注意点

続いて勝率を計算してテキストオブジェクトで表示させてみる。
勝率を計算するなら、決済条件を決めなければならない(そこまで決まっていればEAで良いのではという話もある)。

前述のソースコードに決済条件を加える。
決済条件があるという事は、仮想であってもポジション保有中かどうか判断しなければならない。
また、決済条件によっては複数のポジションを保有するケースも考えられるし、逆にポジションを1つに限定したいこともあるかもしれない。

この辺りはやりたいようにやってもらうとして、今回は前述のソースコードのままで置かれたサイン全ての勝敗をカウントする。
明確に勝敗が分かるよう、買いサインなら次の足が陽線かどうか、売りサインなら次の足が陰線かどうかで勝敗を決める。

厄介なのは、例えば±固定pipsで決済するとした場合、過去足に関しては固定pipsの値動きをどちらも満たしてしまうケースが存在する点だ。
SL/TPを置いていると考えた場合、先にどちらに到達したのかが不明なため、勝敗を正しく判定できない。
とりあえず今回は、陽線/陰線で判定するので明確だが、FXというよりバイナリーのツールっぽい感じになる。
なお、判定するバーが完全な十字線だった場合は負けとする。

勝敗判定と勝率表示の処理を書く

下記、ハイライト部分を追記。

//MT4用
#property copyright "nisai"
#property link      "https://nisaifx.com"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_width1 2
#property indicator_color1 DeepSkyBlue
#property indicator_width2 2
#property indicator_color2 OrangeRed

input int bandsPeriod=20;
input double bandsDeviation=2.0;
input int bandsShift=0;
input ENUM_APPLIED_PRICE bandsAppliedPrice=PRICE_CLOSE;

double buySignBuffer[];
double sellSignBuffer[];

int state=0;   //1:買いサイン後, -1:売りサイン後
double win=0;
double lose=0;

int OnInit(){
   SetIndexBuffer(0,buySignBuffer);
   SetIndexStyle(0,DRAW_ARROW);
   SetIndexArrow(0,233);   //Up arrow

   SetIndexBuffer(1,sellSignBuffer);
   SetIndexStyle(1,DRAW_ARROW);
   SetIndexArrow(1,234);   //Down arrow

   return(INIT_SUCCEEDED);
}

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 position=rates_total-1;
   if(prev_calculated!=0){
      position=rates_total-prev_calculated;
   }
   for(int i=position;i>=0&&!IsStopped();i--){
      double upper=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_UPPER,i);
      double lower=iBands(Symbol(),Period(),bandsPeriod,bandsDeviation,bandsShift,bandsAppliedPrice,MODE_LOWER,i);
      if(i==0){
         //確定していないバーの処理
         if(state==0){
            if(open[i]<upper && open[i]>lower){
               if(close[i]>upper){
                  sellSignBuffer[i]=high[i]+20*Point();
               }else if(close[i]<lower){
                  buySignBuffer[i]=low[i]-20*Point();
               }else{
                  sellSignBuffer[i]=0;
                  buySignBuffer[i]=0;
               }
            }
         }
      }else{
         //参照しているポジション(i)の1つ前のバーにサインが置かれている場合は勝敗判定
         if(i<rates_total-1){
            if(sellSignBuffer[i+1]==high[i+1]+20*Point()){
               if(open[i]>close[i]){
                  win++;
               }else{
                  lose++;
               }
            }else if(buySignBuffer[i+1]==low[i+1]-20*Point()){
               if(open[i]<close[i]){
                  win++;
               }else{
                  lose++;
               }
            }
         }
         //参照しているポジション(i)に既にサインが置かれている場合はstateを更新
         if(sellSignBuffer[i]==high[i]+20*Point()){
            state=-1;
            return rates_total;
         }else if(buySignBuffer[i]==low[i]-20*Point()){
            state=1;
            return rates_total;
         }
         //確定したバーの処理
         if(state==0){
            if(open[i]<upper && open[i]>lower){
               if(close[i]>upper){
                  sellSignBuffer[i]=high[i]+20*Point(); //高値より少し上に売りサインを置く
                  state=-1;
               }else if(close[i]<lower){
                  buySignBuffer[i]=low[i]-20*Point();   //安値より少し下に買いサインを置く
                  state=1;
               }
            }
         }else if(state==1){
            //買いサイン後
            if(open[i]<close[i] && close[i]>lower){
               state=0;
            }
         }else if(state==-1){
            //売りサイン後
            if(open[i]>close[i] && close[i]<upper){
               state=0;
            }
         }
      }
   }
   
   ObjectCreate("winRate",OBJ_LABEL,0,0,0);
   ObjectSet("winRate",OBJPROP_CORNER,1);
   ObjectSet("winRate",OBJPROP_XDISTANCE,10);
   ObjectSet("winRate",OBJPROP_YDISTANCE,10);
   ObjectSetText(
      "winRate",
      IntegerToString((int)win)
         +"/"
         +IntegerToString((int)(lose+win))
         +"("
         +IntegerToString((int)MathRound(win/(lose+win)*100))
         +"%)",
      14,
      "MS ゴシック",
      clrWhite
   );
   
   return rates_total;
}

ストラテジーテスターで動かしてみる。

右上に勝率が表示された。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

コメントする