ある同じ期間についての1時間足でバックテスト結果と実際のトレード成績が綺麗に一致しない場合があります。
この1つの理由として、バックテストでは時間中の値動きの順序を把握できないという問題があります。例えば、ポジションの積み増しやトレイリングストップなど、損切りの位置を値動きに応じて動かすロジックを有効にしているとき、この問題が生じます。
バックテストの1時間足では、高値と安値のみが記録されており、その順番までは記録されていません。そのため、ポジションの積み増しやトレイリングストップを有効にしている場合、この順番によって「バックテストでは損切りにかかっているのに実際はポジションを保持している状況」が生じたり、またはその逆の状況が発生することがあります。
具体例
例)ある1時間足
始値 870000円
高値 880000円
安値 863000円
終値 875000円
この1時間足の情報からは以下の2つのストーリーが考えられますが、バックテストではそのどちらが起きたのかを判定できません。
(ケース1:ポジションを清算)
元の損切り位置 860000円
始値 870000円
まず高値をつける880000円 ⇒ トレイリングストップで損切りの位置を引き上げ865000円 ⇒ 反発して(安値が)損切りラインにかかる863000円 ⇒ 終値 875000円
(ケース2:ポジションを保持したまま)
元の損切り位置 860000円
始値 870000円
まず安値をつける 863000円 ⇒ 反発して高値をつける 880000円 ⇒ トレイリングストップで損切りの位置を引き上げ 865000円 ⇒ 終値 875000円
実際のログ
以下は実際のログです。なぜかバックテストの結果と一致しなかったので調べてみたところ、バックテストでは損切りにかかっていました。実際には安値をつけたあとに高値をつけたので、ポジションの清算は発生しませんでした。
▽ ログ
トレイリングストップの発動:ストップ位置を449037円に動かして、加速係数を0.2に更新します 時間: 2019/04/02 10:00 高値: 459100 安値: 454605 終値: 456148 トレイリングストップの発動:ストップ位置を451050円に動かして、加速係数を0.2に更新します 時間: 2019/04/02 11:00 高値: 458350 安値: 455300 終値: 457694 時間: 2019/04/02 12:00 高値: 460800 安値: 457644 終値: 459137 トレイリングストップの発動:ストップ位置を453000円に動かして、加速係数を0.2に更新します 時間: 2019/04/02 13:00 高値: 460244 安値: 458725 終値: 459376 時間: 2019/04/02 14:00 高値: 552401 安値: 458950 終値: 527176 トレイリングストップの発動:ストップ位置を472880円に動かして、加速係数を0.2に更新します 472880円の損切ラインに引っかかりました。 時間: 2019/04/02 15:00 高値: 587332 安値: 524240 終値: 531158 時間: 2019/04/02 16:00 高値: 534800 安値: 520500 終値: 529824
▽ 実際のBOTログ
11:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 11:00 高値: 458350 安値: 455300 終値: 457694 12:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 12:00 高値: 460800 安値: 457644 終値: 459137 12:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] トレイリングストップの発動:ストップ位置を453547円に動かして、加速係数を0.2に更新します 13:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 13:00 高値: 460244 安値: 458725 終値: 459376 14:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 14:00 高値: 552401 安値: 458950 終値: 527176 14:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] トレイリングストップの発動:ストップ位置を473318円に動かして、加速係数を0.2に更新します 15:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 15:00 高値: 587332 安値: 524240 終値: 531158 15:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] トレイリングストップの発動:ストップ位置を496121円に動かして、加速係数を0.2に更新します 16:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 16:00 高値: 534800 安値: 520500 終値: 529824 17:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 17:00 高値: 530710 安値: 524575 終値: 527774 18:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 18:00 高値: 530795 安値: 527233 終値: 527941 19:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 19:00 高値: 543977 安値: 527603 終値: 539533 20:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 20:00 高値: 540639 安値: 534243 終値: 538552 21:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 21:00 高値: 540688 安値: 536553 終値: 537095 22:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 22:00 高値: 543150 安値: 536240 終値: 540238 23:00 LINE Notify [自動売買BOT(チャネルブレイクアウト)] 時間: 2019/04/02 23:00 高値: 540718 安値: 537450 終値: 539587
他にも同様の現象として、高値の更新によるポジションの積み増し(と、それに伴う損切ラインの引き上げ)があります。この場合も、バックテストで直近の安値が更新後の損切ラインにかかっていた場合に、実際には損切りにかかったのかどうかを判断できません。
解決策
この問題は1時間足の情報量が足りないことに原因があるので、バックテストプログラムの改良では解決できません。根本的にこの問題を回避するには、1時間足のバックテストに1分足を使うか、あるいは実際の売買でリアルタイムの価格を一切使わないロジックに変更するしかありません。
妥協策として「高値をつけたあとに安値をつけてまた高値で終わる可能性はやや低い」と考えることはできます。つまり「始値を2回またぐ可能性は低い」と考えるわけです。このとき、終値が値上がりなら安値 ⇒ 高値の順番に動いたと仮定し、終値が値下がりなら高値 ⇒ 安値の順番に動いたと仮定します。
この場合は単に以下の順番でロジックを適用します。
(例)直近の1時間足が「始値<終値」の場合
時間中には「始値 ⇒ 安値 ⇒ 高値 ⇒ 終値」の順番で動いたと仮定します。
1.買いポジションを保有している場合
1)損切りの関数を適用
2)手仕舞い(逆シグナル)の関数を適用
3)ポジションの積み増しの関数を適用
4)トレイリングストップの関数を適用
2.売りポジションを保有している場合
1)ポジションの積み増しの関数を適用
2)トレイリングストップの関数を適用
3)損切りの関数を適用
4)手仕舞い(逆シグナル)の関数を適用
もちろん「始値>終値」の場合は、それぞれが逆になります。
バックテストコード
while i < len(price): # ポジションがある場合 if flag["position"]["exist"]: # 終値がポジションと同じ方向に動いた場合 # 買いなら(安値⇒高値)の順、売りなら(高値⇒安値)の順に適用 if ( flag["position"]["side"]=="BUY" and data["open_price"] < data["close_price"] ) \ or ( flag["position"]["side"]=="SELL" and data["open_price"] > data["close_price"] ): flag = stop_position( data,flag ) flag = close_position( data,last_data,flag ) flag = add_position( data,flag ) flag = trail_stop( data,flag ) # 終値がポジションと逆の方向に動いた場合 # 買いなら(高値⇒安値)の順、売りなら(安値⇒高値)の順に適用 else: flag = add_position( data,flag ) flag = trail_stop( data,flag ) flag = stop_position( data,flag ) flag = close_position( data,last_data,flag )
もちろんこの方法も完璧ではありません。始値と終値がほとんど同じ水準の場合などは、始値の水準を何度もまたぐことは珍しくありません。とくに揉みあいの場面では、バックテストの1時間足でどちらの順番に動いたかを判断することは不可能です。
しかし大きな値動きを伴う場面では、この順番で判定した方が(バックテスト上で)致命的な順番間違いを犯す可能性を減らすことができると思います。