聖杯を求めて~クロス円のpair-trading戦略~

「今の時代出た情報が全て一瞬で織り込まれちゃうんだから値動きの先を追いかけるなんて無理でしょ」というスタンスの投資家は非常に多いように感じる。市場に対してセミストロングフォームの効率性を仮定していて、方向性に賭けることで安定的に収益を上げるのは不可能、もしくは一部の才能ある投資家だけだという考え方が支配的だ。

そこで市場の方向性に依らず絶対利益を追求するような投資戦略が求められるわけだが、今回それに近いことを実現しようと考えた実験の記録である。

en.wikipedia.org

以前にもこのブログでペアトレーディングを紹介したことがあると思う(イキってるわけではなく、日本語のWikiがなかったので英語のものを載せている)。簡単に紹介すると、例えばコカ・コーラペプシコのような強く相関する二つの銘柄の価格差は一定のレンジに収束しかつ平均回帰する性質があるため、高くなり過ぎたほうを売りやすくなり過ぎたほうを買うというトレードを行うことで両者の開いた価格差を取りに行けるというトレード手法のことを指す。詳しくは上のWikiを読んで欲しい

今回は米ドル円と加ドル円のペアトレーディングをmql4言語で作成したEAを用いて2週間自動取引をさせた。戦略は以下の通り

  1. ドル円と加ドル円の5分足データを直近の一定期間で取得し、乖離の平均と標準偏差を計算する。
  2. ドル円と加ドル円の乖離が2.5σ以上になったらエントリーする。具体的には、(米ドル円の価格)-(加ドル円)の価格を計算し、これが+2.5σ以上であれば米ドル円のショート加ドル円のロング、-2.5σ以下であれば反対の取引を行う。また、エントリーする枚数は勿論双方常に釣り合っている
  3. 乖離が平均に戻ったところでクローズする

以下のようなプログラムを作った。汚すぎて載せるのをやめようかと思ったが一部消して載せることにした。

#property strict

//使う関数の準備
int position_check(void){
   //存在するポジションをチェックする
}

double calculate_signal(int length){
   //ドル円とカナダドル円の乖離を正規化する

   double close_diff, std_of_diff, ave_of_diff=0, square_ave_of_diff=0;
   
   for(int i=0; i < length; i++){
      close_diff = iClose("USDJPY", 5, i) - iClose("CADJPY", 5, i);
      ave_of_diff += close_diff/length;
      square_ave_of_diff += close_diff*close_diff/length;
   }
   
   std_of_diff = sqrt(square_ave_of_diff - ave_of_diff*ave_of_diff);
   double result = (iClose("USDJPY", 5, 0) - iClose("CADJPY", 5, 0) - ave_of_diff)/std_of_diff;
   printf("Signal: " + DoubleToStr(result));
   
   if(MathAbs(result) > 2.5){
   printf("Difference: " + DoubleToStr(iClose("USDJPY", 5, 0) - iClose("CADJPY", 5, 0)));
   printf("Standard Deviation: " + DoubleToStr(std_of_diff));
   printf("Average: " + DoubleToStr(ave_of_diff));
   printf("Signal: " + DoubleToStr(result));
   printf("------------------------------------------------");
   }
   
   return result;
}

int my_order_send(double signal){
   double position_size = calculate_position_size()
   position_size = MathRound(100*position_size)/100;
   
   //乖離が2.5σを超えた瞬間エントリー
   if(signal < -2.5){ 
      printf("Trigger! Signal < -2.5");
      int usd_long = OrderSend("USDJPY", 0, position_size, Ask, 5, 0, 0, NULL, 10);
      int cad_short = OrderSend("CADJPY", 1, position_size, Bid, 5, 0, 0, NULL, 11);
      
      printf("USD/JPY Long Status: " + IntegerToString(usd_long));
      printf("CAD/JPY Short Status: " + IntegerToString(cad_short));
      
      return 1;
   }
   else if(signal > 2){
      printf("Trigger! Signal > 2.5");
      int usd_short = OrderSend("USDJPY", 1, position_size, Bid, 5, 0, 0, NULL, 20);
      int cad_long = OrderSend("CADJPY", 0, position_size, Ask, 5, 0, 0, NULL, 21);
      
      printf("USD/JPY Short Status: " + IntegerToString(usd_short));
      printf("CAD/JPY Long Status: " + IntegerToString(cad_long));
      
      return 2;
   }
   else{
      return 0;
   }
}

int my_order_close(void){
   int i = 0, order_count = OrdersTotal();
   
   while(i < order_count){
      if(OrderSelect(0, SELECT_BY_POS)){
         bool close_result = OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 0);
         printf(IntegerToString(close_result));
         }
      i++;
   }
   
   return 0;
}


int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
//---
   
  }

void OnTick()
  {
//---
   double new_signal;
   
   static datetime prev_time = Time[0];
   // 新しい足ができていないときはなにもせずに抜ける   
   if(Time[0] == prev_time) {
      return;//これでいいのか
   }
   prev_time = Time[0];
   
   new_signal  = calculate_signal(300);
   printf(IntegerToString(position_check()));
   if(position_check()==99){
      my_order_send(new_signal);
   }
   else{
      if(position_check()==1 && new_signal > 0){my_order_close();}
      else if(position_check()==2 && new_signal < 0){my_order_close();}//この二つは反対売買の条件
      else{
         if((AccountProfit()/AccountBalance() - 0.5)*(AccountProfit()/AccountBalance() + 0.5) > 0){my_order_close();}
      }//elseの中は損切の条件
   }
  }
//+------------------------------------------------------------------+

そして実験結果を申し上げると、2週間足らずでー58%の損失を計上してしまい、結果的には失敗に終わった。
観察して分かったことは以下の通り。

  • 東京時間では殆ど乖離は生じない。エントリーは全て22:30-3:30の間に集中していた。
  • 乖離差は正規分布からは程遠く、かなりの時間にわたって3σ以上が確認された。
  • ハイパーパラメータの設定が難しい。当初は損切ラインを近くに設定していたのだが、大きく乖離したあと最終的に戻ってくることが多く損切が裏目に出ることが多かった。

ただ一番の失敗原因は、この実験を行っていた8月中旬から米金利の上昇を受けてドルが大きく買われていたことによる。
平時であれば米ドルが加ドルより大きく強くなることはなく、だからこそ乖離を取りに行くことが出来たわけだが、米ドルが大きく上昇したポイントでプログラムは米ドル売り加ドル買いを行い、米ドル一強の状況下では加ドルの上昇は到底乖離を埋めることが出来るほど強いものではなく、更に乖離が新しいレベルで定着した時点でプログラムはポジションをクローズするため、結果的に-58%を記録してしまった。
この一回の失敗の前では累計で35%の利益を出していたので、(デモ口座ではあるが)実に入金額全てを溶かしたようなものである。

結果としては失敗したものの、平時の穏やかな相場であれば、例えば豪ドル円や新ドル円のような片方が強打されるということが起きない強く相関するペアで刺さるのではないかという希望も見えた。今後は今挙げたペアについても実験してみたいと思う