最適化を最適化する・・・//MQL4の高速化(3) 1秒でも速く

最近みつけた EA2Chart というブログは、市販のEAの売買履歴をチャート上に再現するテンプレートを配布されているようです。残高が右肩上がりの売買履歴を、漫然と眺めるよりも、チャート上のどんなポイントで売買しているのか?を知った上で、EAを購入した方が良いと思うので、参考にしたい人はどうぞ。ZuluTrade のシグナルプロバイダの売買状況もチャート化されているようです。
(あと…EAについて思ったこと その1 その2 その3がお気に入りです。私も7割程度同意見です。ただ、これは万人に受け入れられる内容ではないでしょうね..。まぁ、EAの考え方、使い方は人それぞれということで…









さてさて、1日だけ空いてしまいましたが、本題に入ります。



(1) 基本は関数呼び出しの削減


前々回に for ループで実行時間を測定する方法を紹介しましたが、これを活用すると、StringConcatenate が実は + 演算子より遅い(!)と分かったり、どの関数がどれくらい遅いのか感覚的につかめてきます。
結局のところ、全般的にどの関数もそれなりに時間が掛かることがわかるので、関数呼び出しをできるだけ減らすのが高速化の第一歩です。具体的に言うと、

if( Minute()==0 || Minute()==15 || Minute()==30 || Minute()==45 )

のように書かれていた場合、Minutes() 関数が4回呼ばれていますから、

int _Minute = Minute();
if( _Minute==0 || _Minute==15 || _Minute==30 || _Minute==45 )

と修正するだけで、Minute() 関数の呼び出しを1回だけに減らすことができます。
Minute() のような関数は、start()内で呼ぶ必要がありますが、IsTesting , IsVisualMode , IsOptimization のような関数は、init()内で1度判定すれば十分ですので、init()内で済ませられる関数はinit()内にまとめるのも有効です。




(2) 画面描画の抑止


不要な関数呼び出しを減らすという点では、ビジュアルモードではないバックテストの時は、Comment() や ObjectCreate() のような画面描画に関わる処理を行わないのも有効です。

int init() {
  if ( IsTesting() && !IsVisualMode() ) { Show.Objects	= false;	// test
					  Show.Comments	= false;	}
  //...
}
//...
	if (Show.Comments) {
		Comment(//...

↑ init()内でテスト状態を判定して、start()内では画面処理をしないようにする一例です。(http://forum.mql4.com/29591)



(3) MQL4が短絡評価 (ショートサーキット)できない点に注意する。


http://forum.mql4.com/29591 に書かれている通り、MQL4 では、if 文内の評価式をすべて評価してしまいます。

if( 評価式A && 評価式B && 評価式C )

と書かれていたら、評価式A が false であった時点で、if 文内は実行されないことが確定するのですが、MQL4 では律儀に、評価式B,C ともに計算が行われるのです。ということは、評価式BやC が非常に遅い計算式だとしたら、無駄な計算が行われることになってしまいます。そこで、

if(評価式A){
  if(評価式B){
    if(評価式C){
     //
    }
  }
}

と書いた方が、評価式A が false の場合に計算量を減らすことができるわけです。


↑但し、このテクニックは評価式C が相当遅くて、且つ、A , B を判定したのち、C が実行されないことが確率的に高い場合のみ有効です。下手に全て分解すると、逆に普段のテストがわずかに遅くなることがあるので、要注意です。




(4) 一定時間毎にしか値の変わらない値は再計算させない。


次は、某氏のEAを体感できるほど高速化した手法です。
EAの中に以下のような式があったとします。

int start(){
double UPPER = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_UPPER, 1);
double BASE  = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_BASE,  1);
double LOWER = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_LOWER, 1);

Symbol() が3回呼ばれているので、グローバル変数に _Symbol を設定して、Symbol()の呼び出しを減らす…のは、全く効果がないとは言いませんが、わずかしか短縮されません。
また、LOWER は、iBands を呼ばなくても

LOWER = BASE - (UPPER - BASE);

で計算できるやん!!…と、気づいた人は鋭い洞察力の持ち主ですが、実のところ短縮効果はほとんどありません。iBands の主要なパラメータが一致しているので、3回の呼び出しを2回に減らしても計算量が減らない(内部で結果がキャッシュされているらしい..)のです。


引数をよく見ると、この式は、PERIOD_M15,,,,,,1 (←最後が1) なので、15分毎にしか値が変わりません。…ということは、15分毎に計算してやれば良いはずです。

static double UPPER,BASE,LOWER;
static datetime LastCalc = 0;
if(LastCalc != iTime(Symbol(),PERIOD_M15,1) ){
   UPPER = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_UPPER, 1);
   BASE  = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_BASE,  1);
   LOWER = iBands(Symbol(), PERIOD_M15, 51, 2, 0, PRICE_CLOSE, MODE_LOWER, 1);
   LastCalc = iTime(Symbol(),PERIOD_M15,1);
}

↑書き方はいろいろあると思いますが、static な変数にiBandsの計算結果と、計算時刻を記憶させて、時刻が変わるたびに計算するようにしているだけです。Tick 更新毎に再計算するのに比べて、15分おきに計算するだけで済むのは非常に効率的です。フリーのEAで、この方法が適用出来る場合はぜひ試してみてください。
(他のテクニックなんてどうでもいいぐらい重要です。笑。




(5) 同じ計算は何度もやらない。


残りはどうでもよいテクニックなのですが、、、フリーのEAには以下のような計算式がよくあります。

extern double TakeProfit = 50;
//(中略)
ticket=OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+TakeProfit*Point,"macd sample",16384,0,Green);

この中で、TakeProfit * Point の値は、テスト期間中は一切変化しません。なので、init 内で済ませるようにして

extern double TakeProfit = 50;
double _TakeProfit;
int init(){
  _TakeProfit = TakeProfit*Point;
}
//(中略)
ticket=OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+_TakeProfit,"macd sample",16384,0,Green);

と書けば、掛け算1回分減らすことができます。シンプルなEAでは、削減効果はほとんど無いのですが、ソースがぐちゃぐちゃのEAでは、よくよく見ると同じ計算式が10回も有ったりして、それは流石に減らした方が良いことがあります。




(6) tick単位で計算不要ならスキップさせてみる。


高速化の基本は、高速化の前後で結果が変わらないこと..だと思いますが、考え方を変えて、Every Tick モードよりも粗く、Control Points モードよりは正確にテストできればよい としてしまうのも有りだと思います。
具体的には、 例えば 値幅が 2pips 動くごとに計算するようにさせるのです。1pip単位で上下動する微小なレンジ状態で、1pip動くたびに無駄な計算を繰り返すのを省くわけです。

int start()
{
  static double _LastBid = 0;
  if(MathAbs(_LastBid-Bid)/Point < 2 )return(0);
  _LastBid = Bid;

↑2pips毎にしか動作しないようにするサンプルです。ポジション無しの時だけ有効にするとか、Optimization の時だけ使うとか、インジケータの値取得だけに限定するとか、工夫の余地はあると思います。パラメータが山ほどあって大雑把に最適化したい時などに検討してみてください。(。。。ってそういう時は Control Points を使うかな。。^^;


(7) どうしようもなく重い iCustom には、計算結果のキャッシュを考える?


実用的に試した訳では無いですが、異常に重いインジケータをEAから使う場合で、且つ、インジケータのパラメータを最適化する必要がない場合、時刻が決まればインジケータの値は求まりますから、バックテスト前か、初回のテストで全期間に渡って計算しておいて、それを保存(キャッシュ)して、Optimization の時に再利用する…ということも考えられます。
(..ただ、下手にデータベースにキャッシュしようとすると、読み込み時間が掛かってしまい、意味が無くなってしまう可能性があります。メモリ上に map か hash で一時的に貯めるだけなら実用性はあるかも。。)


キャッシュ方法を考えるよりも、インジケータのアルゴリズム自体の軽量化(RCI系の改良に見られる) や、iCustom を使わずにEA内で計算する( VQ を組み組んだ EA の例 )の方が現実的かもしれません。