MQL5について記事にするほどではないが、メモとして残しておきたいものをここでまとめておく。ただし、プログラミング全般に関するメモも含む。随時更新。
外出先からEAを操作
実は私はVPSを使ったことがない。
安いノートパソコンをEA専用にして稼働している。
WindowsもHome Editionなのでリモートデスクトップにはできない。何かソフトを入れてやろうとも思わない。
もし外出先からスマホでEAを操作したいなら、EAにそういう機能を持たせるやり方もある。
例えば、
といった機能をEAに持たせるのだ。
そうすればスマホから注文を入れたり、それを削除したりすることでEAを操作することができる。
指値注文を使わない利食い
指値注文の利食いを入れて最適化すると、パフォーマンスはイン期間では向上してもアウト期間ではかえって悪化することが多い。つまり再現性がない。
最適化した利食いにはイン期間で大勝したトレードの利益を取り切ろうとしてそれに引きずられる傾向があって、システムが不安定になる。その利食いはどうしても別の期間では「帯に短し襷に長し」となる。
それとは別に、利益を限定してしまうデメリットも大きい。
利食わないでいると相場が反転して負けトレードになってしまった、ということはよくある。また、「利食い千人力」などという格言が「早く利食え」と誘惑してくる。
確かに利食えば勝率は上がるが、長い目で見れば損益を減らしてしまうことのほうが多そうだ。利益を限定してしまうデメリットは少しばかり勝率が上がるくらいではカバーできないのだ。
指値注文の利食いと似たことをやりながら、なるべく利益を限定しない方法もある。
例えば、ある銘柄を買ったとする。
そのときの価格をentry_priceとする。説明の単純化のためにスプレッドは無視しよう。
また1本前の足の終値をclose1、2本前の足の終値をclose2とし、利食い幅は10(pips)としよう。
そこでエグジットの条件に
if(close2-entry_price<10 && close1-entry_price>10) 決済注文;
を加えるのだ。
こうすると10pips以上の利益が確保されるが、場合によっては15pipsになったり、20pipsになったり、それ以上になったりすることもあり、利益は限定されない。
もし足が5分足なら、利益が10pipsを超えてもすぐにはエグジットせず、足が完成されるまで最大5分間は待つ。
その間に利益を伸ばしてもらうのだ。
もちろん、反転して損失となることもあるが、長い目で見れば、若干勝率は下げても利益を伸ばしたほうがいい。
このやり方は単に指値で利食いするよりもアウト期間でのパフォーマンスが向上し、再現性もそれなりにあると思う。
それでも、10pipsの利益が100pipsになったり、200pipsになったりすることはまれで、利益が全く限定されないわけではない。
スタイラー
MetaEditorのツールバーに「スタイラー」というのがある。
インデントを調整してくれる機能らしい。
MQL5ではインデントをどうするのが公式なのか、という疑問があった。
MQL5リファレンスにあるサンプルコードやMT5にあるサンプルEAのコードを見ると、必ずしも統一されていない。
「スタイラー」で調整されたものが公式なのだろうか。
とりあえず気が付いた2つの例を挙げる。
// コメント
のようにコメントアウトから始まる行の場合、行頭にインデントを置いてもなくなり、左詰めになる。
void func() { int a=0; }
のような場合、インデントは「{」と「}」の行は2つで、「int a=0;」の行は3つとなる。
もちろん、このようにしなければならないわけではない。
ただ、これが公式ならば、MQL5リファレンスにあるサンプルコードやMT5にあるサンプルEAのコードもこれで統一してほしい。
更に言えば、MQL5もコーディング規約を公式に示してほしい。
そうすれば、コードを書く側もそれに合わせて書くし、他人の書いたコードも読みやすくなるだろう。
ストップリミット注文
バックテストではネガティブスリッページの反映が不十分で、パフォーマンスは過大評価されやすい。
そこで、ストップリミット注文を使ってみるのはどうか。
トリガー価格に到達したらトリガー価格のすぐ下に指値注文を置く。
こうすれば順張りでエントリーしながらスリッページがポジティブになるのも可能だ。
だが、価格が指値注文を置いた価格まで戻らない場合はエントリーされず、トレード機会を失うことにもなる。
また、価格が戻らないのは順張りではよいトレードであり、それを逃すことになる。
逆に戻る場合は順張りではよくないトレードである可能性があり、それを掴むことになる。
つまり、単にトレード機会を失うということではなく、収益が得られる可能性の高いトレード機会を失うことになるのだ。
それはさて、ある戦略で逆指値注文とストップリミット注文とでどのようなパフォーマンスの違いが生じるかバックテストしてみた。
逆指値注文でエントリーした場合の平均獲得pipsは7.06pips。
これをストップリミット注文に変えると3.71pipsとなった。
3.35pipsも悪化した。
実際のトレードにおいては逆指値注文ではネガティブスリッページが、ストップリミット注文ではポジティブスリッページが発生する可能性があり、そこまで差はないにしても、ストップリミット注文での平均獲得pipsが逆指値注文でのそれを上回るとは考えにくい。
また、トレード数は逆指値注文が1185回で、ストップリミット注文が1082回だった。
失ったトレード機会は103回で1割弱。
それくらいなら、とは思うが、実は失ったトレードの内訳は88勝15敗の勝率85%で、これは大きい。元々、勝率が40%程度の戦略なのだ。
しかも逆指値注文のペイオフレシオは2.5倍で、ストップリミット注文は2.4倍。
失われたトレードはというと3.1倍にもなる。
ペイオフレシオが3.1倍で勝率85%のトレードを103回も逃すというのはさすがにきつい。
ネガティブスリッページを避け、ポジティブスリッページを受けるメリットがあるとはいえ、少なくともこの戦略ではストップリミット注文は使いにくい。
補足
逆指値注文でもストップリミット注文でもエントリーしたトレードは注文方法の違いにより若干のパフォーマンスの違いがあると思う。
だが、ここでは同じと考えて失われたトレードのパフォーマンスを計算している。
もし同じでないとするとストップリミット注文でのパフォーマンスは必ずしも逆指値注文でのパフォーマンスに含まれないことになり、
失われたトレードのパフォーマンス=逆指値注文でのパフォーマンス-ストップリミット注文でのパフォーマンス
という計算が成り立たないことになる。
例えこの計算が成り立たないとしても、逆指値注文でのパフォーマンスがストップリミット注文でのそれよりトレード機会が多く、勝率が高く、ペイオフレシオも大きかったということは言える。
必要証拠金の計算
円口座でユーロドルを取引する場合の1ロット当たりの必要証拠金を調べてみた。
double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID); double margin; OrderCalcMargin(ORDER_TYPE_SELL,Symbol(),1.0,bid,margin); Print(margin);
を実行すると、2023/2/23 23:59の時点だと
570688.0
と出力された。
同時点のユーロ円は142.672円で、1ロットが10万通貨、レバレッジが25倍だとすると、
142.672*100000/25=570688
となるのでぴったり合う。
ユーロドルを取引しているのでユーロドル×ドル円を使うのかと思っていたが、それだと少しずれるので、ユーロ円を使っているようである。
今度はポンドドルで調べてみる。
すると、
809030.0
と出力された。
同時点のポンド円は161.806円なので、同様に計算すると
161.806*100000/25=647224
となり、合わない。
実は
161.806*100000/20=809030
と計算するとぴったり合う。
つまりレバレッジが20倍なのだ。
だが、ポンド円でもポンドドルでも確認してみるとレバレッジは25倍となっている。
ちなみにオージードルの場合は
366632.0
と出力され、同時点でのオージー円は91.658円だから
91.658*100000/25=366632
で25倍で計算されていることが分かる。
円口座でドルストレートを取引すると、クロス円で必要証拠金が計算されるということは分かったが、ポンドドルでレバレッジが20倍になる理由が分からない。
ともあれ、求めたmarginに基づいてロット数を計算すると、通貨ペアによっては過小になることがある点に注意が必要だ。
今までmarginを使ってロット数を計算していたのだが、少し小さい場合があることに気付いた。
そこで調べてみたところ、なぜかこういうことが起きているということが分かった次第である。
とりあえずmarginを使わない計算方法にしたほうがよさそうだ。
ついでにもう少し調べると、円口座ではmarginは
オージー円、オージードルの場合→オージー円(25倍) ユーロオージー、ユーロポンド、ユーロ円、ユーロドルの場合→ユーロ円(25倍) ポンドオージー→?(ほとんどポンド円だが、少しずれている。ポンドドル×ドル円でもない。20倍) ポンド円、ポンドドルの場合→ポンド円(20倍) ドル円の場合→ドル円(25倍)
で計算されている。
基本的に取引通貨ペアのベース通貨+円が使われると考えてよさそうだ。
ユーロポンドはユーロ円が使われているからか、20倍ではなく25倍となっている。
細かいところでは謎が残るが、とりあえずそういう仕様だということにしておこう。
ポートフォリオの最適化
MT5を使ったポートフォリオの最適化
- 複数の戦略を1つのEAにまとめる
- 全資金を100として、各戦略に配分する資金をinput変数として指定する
- 各戦略のロット数は配分された資金に応じて増減させる
- OnInit関数を使い、各戦略に配分された資金の合計が100とならない組み合わせはパスする
以下のA~Eの5つの戦略でポートフォリオの最適化をしてみた。
最適化はリカバリーファクターを基準とした。
左の数字はリカバリーファクター、右の数字は最適化した資金配分(合計100)。
最適化は各戦略の配分を0から90まで、10刻みで行った。
A: 27.43/10 B: 17.98/0 C: 17.04/20 D: 11.86/50 E: 9.03/20
リカバリーファクターを基準に最適化したにもかかわらず、配分は各戦略のリカバリーファクターに比例しないようだ。
リカバリーファクターで4番目の戦略に資金の半分を投入し、2番目の戦略を0にするというのはなかなかやりにくい。
なお、最適化したポートフォリオ全体のリカバリーファクターは51.15で、資金を20ずつに5等分したポートフォリオでは42.41だった。
ポートフォリオの最適化に再現性はあるのかという疑問はある。
ただ、パフォーマンスのいい戦略に重点配分すればよいというものでもなさそうだ。
芸がないが、等分配分が最も無難なのかもしれない。
ならば最適化は不要か。
BuyメソッドとSellメソッド
MQL5リファレンスを見ると、CTradeクラスのBuyメソッドでpriceを指定しない、または0にすると買値が使用されるとある。
だが、実際には価格をいくらに入力しても無視され、エラーにもならない。
恐らく買値で買われているのだろう。
同様にSellメソッドでpriceを指定しない、または0にすると売値が使用されるとある。
だが、やはり実際には価格をいくらに入力しても無視され、エラーにもならない。
恐らく売値で売られているのだろう。
これまで、成行で売買するときはpriceを0にしなければならないものだと思っていた。
だが、実際にはいくらに入力しても影響はないようだ。
とはいえ、わざわざでたらめな価格を入力する必要はない。
StringToTime
もし、2024年1月17日に
Print(StringToTime("12:00"));
を実行すると、
2024.01.17 12:00
と出力され、日付を補ってくれていることが分かる。
問題は補ってくれる日付がNYC基準ではなくて、日本時間基準だということだ。
冬時間の場合、NYC基準の17時に日本時間では日付が変わる。
このため、17時以降を指定した場合に問題が起きる。
例えば、やはり2024年1月17日に
Print(StringToTime("20:00"));
を実行すると、
2024.01.17 20:00
と出力される。
これは日本時間では2024年1月18日4時0分となる。
もし、これを翌18日の1時に実行したとする。
NYC基準ではまだ17日だが、
2024.01.18 20:00
と出力され、日付が変わってしまう。
もし
if(TimeCurrent()==StringToTime("20:00")) 処理
のように毎日20時0分に何らかの処理を行うとする。
一見、何の問題もなさそうだが、この時間が来る前に日付が翌日になってしまうので、永遠にこの時間は来ない。
そして何の処理も行われない。
今日の日付を省略して補ってもらおうとすると、予想外のことが起きるから、省略せずにちゃんと書いておいたほうがよい。
例えば
string sdate=TimeToString(TimeCurrent(),TIME_DATE); if(TimeCurrent()==StringToTime(sdate+" 20:00")) 処理
のようにするのが無難だ。
パフォーマンス指標の定数名
総損益やプロフィットファクターのようなパフォーマンス指標の定数名を調べたい場合、
を見ればいい。
あるいは
から「標準的な定数、列挙と構造体」→「環境状態」→「テスト統計」の順にクリックしても見ることができる。
例えば、OnTester関数内でプロフィットファクターを出力させたいときは
double pf=TesterStatistics(STAT_PROFIT_FACTOR); Print(pf);
のようにする。
論理否定演算子
aがbool型の変数であるとする。
aが真のときに処理をしたい場合、
if(a==true) 処理;
と書ける。
だが、
if(a)
処理;
とシンプルに書くこともできる。
aが偽のときに処理をしたい場合は
if(a==false) 処理;
と書ける。
だが、論理否定演算子の「!」を使って
if(!a)
処理;
とシンプルに書くこともできる。
論理否定演算子は便利だが、使わないほうがいい場合もある。
例えば、aが真のときは処理1を、偽のときは処理2を実行したい場合、
if(a) 処理1; else 処理2;
と書ける。
論理否定演算子を使って
if(!a) 処理2; else 処理1;
と書くこともできる。
だが、この場合はわざわざ論理否定演算子を使わなくても書けるし、そのほうがよりシンプルであるから使わないほうがいい。
ENUM_TIMEFRAMES型
その1
MQL5のENUM_TIMEFRAMES型の変数も最適化できる。
例えば、
input ENUM_TIMEFRAMES timeframe=PERIOD_M5;
のようにしておけば、変数timeframeを最適化できる。
「current」から「1 Month」まで22種類を選択できるので、スタートとストップを選択する。
ただし、ステップは選択できない。
選択できる種類は
current 1 Minute 2 Minutes (中略) 1 Week 1 Month
となっており、「corrent」より後は足の短い順に並んでいる。
その2
MQL5で例えば
Print(PERIOD_M5);
とすると何を出力するか。
MQL5リファレンスのどこかに書いてあったと思うが、ENUM_TIMEFRAMES型の各足で実際に出力させると、
M1:1 M5:5 M15:15 M30:30 H1:16385 H4:16388 D1:16408 W1:32769 MN1:49153
のようになっていた。
分足以外は不規則に見えるが、長い足ほど大きな数値のようだ。
とすると、例えば
Print(PERIOD_M5>=PERIOD_D1);
とすれば
false
が出力されることになる。
短期足と長期足でどの足を使用するか最適化する場合、上のような条件式を使えば
短期足>=長期足
となる無意味な組み合わせをスキップするという使い方もできる。
HistoryOrder~関数について
その1
MQL5で過去のオーダーを検索するとき、約定することなく消去されたオーダーは無視される。
約定はしていないが、まだ消去されていない、つまり現在生きているオーダーは過去のオーダーとして検索にかかる。
オーダーを短時間に繰り返しオープンしないようにするために過去のオーダー検索を利用する場合、注意が必要だ。
約定していなくてもまだ生きているオーダーは検索にかかるから、オーダーを短時間に繰り返し追加することは防げる。
だが、約定することなく消去されたオーダーは検索されないから、オーダーのオープンと消去を短時間に繰り返すことは防げない。
オーダーを短時間に繰り返しオープンすることを防ぐ簡単な方法はstatic変数を使ってオープン時間を記憶させることだ。
ただstatic変数はMT5を再起動したりすると記憶が失われるところが少し気に食わない。
もっとも、MT5が短時間に再起動を繰り返してその度にstatic変数が記憶を失うようなことは普通ないから、実務的にはそれで十分なのだろう。
その2
もし注文期限を超えてオーダーが消滅しても、同じ注文期限を持つオーダーを繰り返し出すことはできない。
なぜなら、その注文期限はすでに過去であり、オーダーを出してもエラーになるから。
それに注文を出す時間を設定しているなら、その時間は注文期限より前に設定しているはず。
注文期限より前の時間と注文期限以後の時間が同時に成立することはないから、そもそも注文期限を超えてオーダーが消滅した後にオーダーが出されるようなことはないわけだ。
仮に設定をミスして、注文を出す時間を注文期限より後に設定したとしても、そのオーダーはエラーになる。
というわけで、短時間にオーダーが出されることを防ぐのに過去のオーダーを検索することは、注文期限を超えて消滅したオーダーは検索できないにもかかわらず、問題はないということになる。
なお、「注文期限を超えて消滅した」オーダーは過去のオーダーに含まれないが、「キャンセルして消去した」オーダーは過去のオーダーに含まれる。ややこしい。
とすれば、過去のオーダーを検索したい場合、オーダーの消去に注文期限を使わず、キャンセルを使うのも一つの方法だ。
だが、注文期限を使うとMT5が起動していなくても消去できるので、なるべく注文期限を使いたいところではある。
ORDER_TIME_SETUPとORDER_TIME_DONEについて
MQL5で
ORDER_TIME_SETUP:約定するしないに関わらずオーダーをオープンした時間
ORDER_TIME_DONE:約定していないときはオーダーをオープンした時間、約定したときは約定した時間
だと思うが、確信はない。
iSpread関数について
その1
iSpread関数が返すスプレッドは始値時点なのか、終値時点なのか。
調べてみるとどちらでもない。
どうやら足の開始時間から終了時間までの間の最小スプレッドのようだ。
だから月足だとスプレッド0などということもある。
例えば、スプレッド(ポイント単位)が1つの足の中で
10 5 3 5 10
のように推移した場合、iSpread関数が返すスプレッドは
10 5 3 3 3
のように推移し、足が完成したときのスプレッドは3となる。
したがって、iSpread関数が返すスプレッドは小さめになる。
許容する最大スプレッドでエントリーを抑制したい場合はiSpread関数を使うのではなく、BIDとASKをリアルタイムで取得し、ASK-BIDを使ったほうがよさそうだ。
例えば
double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); // ASK double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID); // BID int spread=int(MathRound((ask-bid)/Point())); // スプレッド(ポイント単位)
として、このspreadを使えばいい。
その2
MT5でドル円各足のスプレッドの20本平均を見てみると、2024年2月19日22時0分で
M1:0.6 M5:0.4 M15:0.3 M30:0.3 H1:0.3 H4:0.3 D1:0.3 W1:0.3 MN1:0.2
となった(単位はpips)。
時間の長い足ほど小さくなる傾向があることが分かる。
これは足のスプレッドとして記録されている数値が、足の形成中に変化したスプレッドの中で最も小さいものが選ばれているからだ。
1か月の間には瞬間的にスプレッドが0になることもあるようだ。
だから月足だと0になったりすることもある。
全ティックでバックテストしても1分足のスプレッドが適用されるだけなので、リアルティックに比べると甘めの想定になる。
Zというテクニカル指標
MQL4/5でiBands関数を使う人は多いと思うが、私はiZという関数を作って代わりにすることが多い。
計算式は
z=(終値-移動平均)/標準偏差
だ。
ボリンジャーバンドの-2σで買い、+2σで売りたい場合、終値、上バンド、下バンドの3つが必要になるが、zなら1つで済む。
例えば
if(z<-2) 買い注文;
のように書ける。
HistoryDeal~の諸関数の戻り値について
HistoryDealsTotal()は取引数×2+1の数字を返す。
リスト内番号を0から検索すると、DEAL_TIMEでは
バックテストの開始時間 1番目の取引のin時間 1番目の取引のout時間 2番目の取引のin時間 2番目の取引のout時間 …
の順となり、DEAL_PROFITでは
初期証拠金 0 1番目の取引の損益 0 2番目の取引の損益 …
の順となる。
なお、HistoryDeal〜の諸関数は決済したポジションだけが対象ではない。現在保有しているポジションでもエントリー自体は完結した取引として対象となる。
DoubleToString関数について
MQL5のDoubleToString関数はDouble型の数値を四捨五入した上でString型に変換してくれる。切り捨てではない。
例えば、1.23456を小数点第3位に指定して変換してみる。
Print(DoubleToString(1.23456,3));
を実行すると
1.235
と出力される。切り捨てられて1.234にはならない。
ところで気を付けたいのは、例えば1.2345を小数点第3位に指定して変換してみた場合だ。
Print(DoubleToString(1.2345,3));
を実行すると
1.234
と出力される。
今度は切り捨てられてしまったように見える。四捨五入の仕方が違うのだろうか。
私は1.2345と入力しても、それがコンピューター側では1.2344999…9となったり、1.2345000…1となったりすることがあるのではないかと思う。そして前者の場合、四捨五入すれば1.234となるわけだ。
したがって、四捨五入の結果が予想していた数値と違うことがあるということは頭に入れておいたほうがいい。
ウォークフォワードテストのコードの書き方
MQL5でウォークフォワードテストのコードをどう書けばいいか。
簡単なのは手動で各インサンプル期間の最適パラメータを調べて、それをアウトオブサンプル期間に適用することだ。
例えば、平均期間、ストップロスを2016-2020年で最適化した結果、平均期間=20、ストップロス=100が最適だったとしたら、これを2021年に適用する。
次に、平均期間、ストップロスを2017-2021年で最適化した結果、平均期間=15、ストップロス=200が最適だったとしたら、これを2022年に適用する。
ma_period:平均期間 sl:ストップロス(ポイント)
であるとして、コードは
MqlDateTime mdt; TimeCurrent(mdt); if(mdt.year==2021) { ma_period=20; sl=100; } if(mdt.year==2022) { ma_period=15; sl=200; }
のように書く。
MQL5の公式サイトにはウォークフォワードテストを実行するための有料、無料のコードが提供されているが、かなりの数のファイルをインクルードする必要があるようだ。
他人の作った、よく分からないコードは使いたくない、コードの内容をチェックするのも大変だという人には上のやり方を試すのがいいかもしれない。
int型とdouble型について
その1
MQL5でdouble型の変数をint型に変換すると、小数点以下は四捨五入ではなくて切り捨てとなる。他の言語でもそうだと思うが、よくうっかりする。
例えばdouble型同士の変数を足したり引いたりして答えが10だとする。
だが実際には9.999…9や10.000…1になることがあり、int型に変換して切り捨てると、9になったり、10になったりしてしまうので、注意が必要だ。
また、
Print(1/2*100);
とすると
0
と出力される。
これは1と2がint型なので1/2は0.5ではなく、切り捨てられて0になるからだ。
Print(1/2*100.0);
としても後の祭りだ。
0.0
と出力されるだけで、大して変わらない。
Print(1.0/2*100);
または
Print(1/2.0*100);
なら
50.0
となる。常識だが、ついうっかりする。
その2
プログラミング言語では、int型の数とdouble型の数で四則計算をすると答えはdouble型になるのが普通だと思う。
MQL5でもそうか確認してみた。
int a=10; Print(a+0.2); Print(a-0.2); Print(a*0.2); Print(a/0.2);
を実行すると
10.2 9.8 2.0 50.0
と出力された。
MQL5でも、すべてdouble型に変換されているようだ。
ついでにint型の変数をdouble型の変数に代入するとどうなるか確認してみた。
int a=10; double b=a; Print(b);
を実行すると
10.0
と出力され、double型に変換されていることが分かる。
逆に
double a=10; int b=a;
とすると、コンパイルはできるが
possible loss of data due to type conversion from 'double' to 'int'
という警告が出る。
MqlDateTimeについて
MQL5で
MqlDateTime mdt;
とした後に
TimeCurrent(mdt);
としても
TimeToStruct(TimeCurrent(),mdt);
としても同じだ。
だが後者の方が出来ることが増える。
例えば
TimeToStruct(TimeCurrent()+45*60,mdt); Print(mdt.hour==10 && mdt.min==30);
とすると、
true
と出力するのは10時30分ではなくて、45分早い9時45分になる。
こういう使い方も考えられる。
例えばFOMCの日にtrueを返す関数を作ったとしよう。
ついでにFOMC前日、または翌日にもtrueを返すようにしたい場合、新たに関数を作ってもいいが、引数に246060を与えれば前日、-246060を与えれば翌日になるようにすれば便利だろう。
演算子について
MQL5で
Print(1.5 && true); Print(-1.5 && true); Print(0.0 && true); Print(-0.0 && true);
は
true true false false
と出力される。
double a; Print(a=1.5 && true); Print(a=0.0 && true);
は
true false
と出力される。
a==bのつもりがa=bと間違えても条件式は成立するので注意。
オーダー、ポジションのリスト内番号について
オーダー、ポジションのリスト内番号が時系列的にどうなっているか調べてみると、0が最も古く、大きくなるにしたがって新しくなった。
例えば0時、1時、2時、3時、4時にポジションを作ったとする。
リスト内番号は0、1、2、3、4の順となった。
ここで2時のポジションを決済する。
すると、0時、1時のポジションのリスト内番号は0、1のままだが、3時、4時は2、3に変わった。
さらに5時に新たにポジションを作ったが、リスト内番号は4になった。
整理するとエントリー時間とリスト内番号は
0時:0 1時:1 2時:2→決済 3時:3→2 4時:4→3 5時:4
となった。
iBarShift関数について
その1
MQL5のiBarShift関数は指定した時間から足が何本経過しているかを返す。
1時間足の場合、1時0分に指定すると、2時0分では「1」を返す。もし1時5分に指定すると、2時0分では「0」。2時5分でもやはり「0」。3時0分でようやく「1」。
1時間足なら1時間に1回、0分に更新で端数は切り捨てとなる。
その2
MQL5のiBarShiftは指定した時間がバックテストの開始日より1年以上前だと-1を返すようだ。
CopyBufferも指定した開始日がバックテストの開始日より1年以上前だと配列は空になってしまう。
バックテストの開始日より1年以上前のデータは持っていないという仕様なのだろうか。
指定した期間範囲内で最大のTRを返す関数を作りたかったのだが、MT5の仕様?でうまくいかない。
テクニカル指標関数のshiftも日足だと500本くらいでおかしな数値を出す。
時間足だともう少し多くても大丈夫なようだが。
バックテストで500本以上前のデータを使うことはあまりないとはいえ、知らないと間違える。
実際のトレードでもおかしな数値が出ないとも限らない。
200日移動平均線は大丈夫だろうが、500日とか1000日などは全然違う数値になっているかもしれないから使うのはやめておいたよさそう。
マジックナンバーの注意点
MQL5でコンパイルするとき、マジックナンバーが11桁以上だと
truncation of constant value
という警告が出て別の数値に変換される。
21桁以上だと
integer constant is too big, so it was converted into double
という警告が出てやはり別の数値に変換される。
マジックナンバーが想定していない数値に変換されているのは危険なので、10桁以下に収めるべきだ。
今は分からないが、昔、MQL4でマジックナンバーの桁数が大きすぎるとすべて0に変換された。別の数値に変換されるだけなら、重複する可能性は低いからまだいい。だが、すべて0に変換されるとなると重複するリスクが高くなる。
複数のEAを稼働させ、しかもマジックナンバーがすべて0になっていると、EA同士で互いのポジションに干渉して、最悪、エントリーとエグジットを高速で繰り返し、あっという間に破産するだろう。実際、私はデモ口座でだが、破産しかけたことがある。
バックテストはEA単体の動きしか分からず、複数のEAを稼働させたときに初めて起こる問題には気づきにくいので要注意だ。複数EAを稼働させる場合、先ずはデモ口座で稼働予定のすべてのEAを同時に稼働させて問題がないか確認する必要がある。
EAの暴走を防ぐ工夫
エントリーとエグジットの条件を排他的にする
EAで売買ルールを書くとき、私は
bool buy_entry=…; bool sell_entry=…; bool buy_exit=…; bool sell_exit=…;
のようにしている。
ところで、エントリーとエグジットを高速で繰り返して破産するのを避けるには、エグジットが偽のときのみ、エントリーが真となるように注意してルールを書く必要がある。
そうでないと、エントリーした瞬間にエグジットし、エグジットした瞬間にエントリーし、ということが高速で繰り返され、あっという間に破産するということが起こりかねない。しかし、細心の注意を払ってルールを書いても、うっかりということがあるだろう。
それを防ぐ方法の一つとして、売買ルールを書いた後に
buy_entry&=buy_exit==false; sell_entry&=sell_exit==false;
の2行を追加しておくのがよいと思う。こうすればエグジットが偽のときのみ、エントリーが真となることが担保される。
エグジット後、エントリーを一定時間抑制する
例えば1時間足で前の足の終値が移動平均線を上抜いたら買うとする。
このシグナルは次の足まで、つまり1時間は続くので、その間に利食いや損切りでエグジットすると、また買ってしまう。エントリーとエグジットの条件を排他的にしたとしても、利食い、損切りでエグジットするとこういうことが起こりうる。
このようなことを避ける方法として、次のエントリーをするにはエグジットから一定時間経過していることを要件とすべきだろう。少なくともエントリーの条件が継続する時間以上の時間を経過することを要件とするべきだ。エントリーしたその足で利食いや損切りが生じるような狭い値幅に設定しなければ、普通このようなことは起きない。とはいえ、極端な値動きというのは時折起こりうるので、やはりそれにも備えるべきだ。
他銘柄データを利用するときの注意点
OANDAのMT5でEURUSDをバックテストするときに
Print(iClose("US30",0,1));
としてダウ平均の終値を出力させてみると、問題なく出力する。
だが、USDJPYでバックテストすると「0」が出力される。エラーはない。
バックテストの開始時期を2016年以降に変更すると、なぜか出力するようになる。
US30のデータは2016年10月以前は存在しないようで、そのあたりが関係しているかもしれない。データが存在する前からバックテストしてデータ取得に失敗すると、それが後まで引きずるのだろうか。ではなぜEURUSDでは引きずらないのか?
ともあれ、バックテストで他銘柄のデータを使うときは注意が必要だ。
1つのEAで複数戦略
私はトレード戦略を関数にして1つのEAにしている。例えば、
void OnTick(void) { Strategy1(0.4); Strategy2(0.3); Strategy3(0.2); Strategy4(0.1); }
みたいにして資金を割り当てる。
こうすれば、複数戦略全体のバックテストをしたい場合に便利だ。資金割当を最適化すれば、ポートフォリオの最適化もできる。
もし個々の戦略単体のバックテストがしたい場合は対象戦略以外の資金割当を0にすればいい。例えば、
void Strategy1(double allocation) { if(allocation==0) return; //--- 中略 }
みたいにしておけば、対象戦略以外の処理をスキップできる。
ダイバージェンシーのコードの書き方
例えばMACDのダイバージェンシーを調べるとする。
今、高値が直近高値を更新し、直近高値がn本前の高値だったとすると、
H[0]>H[n]
と書けるだろう。
このとき、もし
MACD[0]<MACD[n]
だとしたら、これはダイバージェンシーと言えるのではないだろうか。