InvestmentTechHack

上位足を考慮した移動平均線のバックテスト|日経225 FX 4時間足

Posted on July 23rd, 2018Updated on October 30th, 2020

どんな記事

この記事を読むと、次のことがわかるようになります。

  • 上位足を確認することの有効性
  • エッジのあるトレードの一例
  • バックテストのやり方
  • 単利で計算した、この手法の想定利益

「エントリーするときは、上位足を必ず確認した方が良い」

こんな話をよく聞くと思います。そして、この話を信じて実行している人も多いことでしょう。

しかし、ふとあることに気付きました。「上位足うんぬん~」に関するデータの裏付けをみたことがないということです。

と、いうわけでやってみました。上位足を考慮したバックテスト!

手法は千差万別なので、この記事の結果がすべてではありませんが、ひとつの、それなりの結果は提示できると思います。

バックテストの方法

どんなツールを使用して、どんな方法でバックテストを行ったのかの記録です。

使用したツール

バックテストには以下のツールを使用しました。

Pythonを手軽に試したいなら、Google Colaboratoryがめちゃくちゃオススメです。Google Driveでドキュメントやスプレッドシートを作る感覚でPythonを試すことができます。今回のような、ちゃちゃっと検証したいときに非常に便利です。

使用した価格データ

価格データは以下を使用しました。

1分毎の価格データを含むcsvファイル
日経225

MT4のhstファイル
USDJPY、EURJPY、GBPJPY、AUDJPY、EURUSD、GBPUSD、AUDUSD

日経225は独自に保有しているもので、hstファイルはFXDDさんのものを使用しています。

検証する手法

検証した手法の詳細です。

※ 今回の検証では小次郎講師の「ステージ」という考え方を用いています。詳細については、権利の関係がありそうなので本家の記事をご覧ください。また、本家では、単純にステージだけでエントリーやイグジットを判断するのではなく、ローソク足の形やもみ合い放れ等も考慮して判断するようです(つまり、本家はこの検証結果よりも良い成績になると思われる)

仮説

このバックテストをするに至った仮説。

4時間足のトレードをする際に、上位足である日足の動向を考慮した方が良い成績になるのではないか

例)日足で上昇トレンドのときだけ、4時間足の買いエントリーを行う等

エントリー

今回の検証の基本的なコンセプトは以下の通りです。

日足のトレンドを、4時間足のエントリーのフィルターとする

バックテスト エントリー

日足のトレンドを確認すると言っても毎回チャートを切り替えるのは手間なので、次の考え方を用います。以下は4時間足であっても、120本の移動平均線は20日相当、240本は40日相当になるということです。

  • 4時間 ✕ 6 = 1日(24時間)
  • 4時間 ✕ 120 = 20日
  • 4時間 ✕ 240 = 40日

また、移動平均のもみ合い時の「だまし」を軽減するために、小次郎講師による移動平均線大循環分析の「ステージ」の考えを用います。

4時間足のトレンド
EMA5とEMA20、EMA40の関係でステージを判断する

日足のトレンド
4時間足のEMA5とEMA120、EMA240の関係でステージを判断する

上昇トレンド
ステージ6:早仕掛け、ステージ1:本仕掛け

下降トレンド
ステージ3:早仕掛け、ステージ4:本仕掛け

※ EMAは指数平滑移動平均線のこと、EMA5はローソク足5本のEMAを指す。

今回のバックテストは、エントリーを次のように組み合わせていくつかパターンを作成し行いました。

ENTRY_CycleStage14_FILTER_UpperCycleStage14
通常のパターン(エントリーが遅くなりがち)

ENTRY_CycleStage63_FILTER_UpperCycleStage63
早仕掛けのパターン(エントリーは早いが〝だまし〟が多くなりがち)

ENTRY_CycleStage14_FILTER_UpperCycleStage63
組み合わせたパターン(ハイブリットを目指す)

※ ENTRYは4時間足のステージを、FILTERは日足のステージを表します

決済1

決済1は、同じく小次郎講師による移動平均線大循環分析の「ステージ」の考えに基づくものです。主に、「利益確定」や、「トレンドがなくなったことを確認した損切り」の決済です。

今回のバックテストでは、ステージによる決済は以下で統一しています。

EXIT1_CycleStage36

  • 通常のパターン(エントリーが遅くなりがち)
  • 買いエントリー(ステージ6か1)はステージ3で決済
  • 売りエントリー(ステージ3か4)はステージ6で決済

バックテスト 決済

決済2

決済2は、ATR(その銘柄の平均的な最大変動幅)をもとにした「ロスカット」や「トレイリングストップ」の決済です。

今回のバックテストでは、以下の2パターンを組み替えて行います。

EXIT2_SO_Entry
ATRによるロスカット。エントリーの価格から3-ATRマイナス方向にロスカットを設置(固定)。

EXIT2_SO_Close
ATRによるトレイリングストップ。3-ATRマイナス方向にロスカットを設置し、終値がプラス方向に推移するのに追随させる。

バックテスト 決済

検証するパターン

今回のバックテストでは、ここまで解説してきたENTRYやFILTER、EXIT1、EXIT2を組み合わせた以下の6パターンの検証を行いました(FILTER_noneは4時間足だけで判断した場合)

パターンEntryFilterExit1Exit2
1SycleStage14noneSycle36SO_Entry
2SycleStage14UpperSycleStage14Sycle36SO_Entry
3SycleStage14UpperSycleStage14Sycle36SO_Close
4SycleStage63UpperSycleStage63Sycle36SO_Entry
5SycleStage63UpperSycleStage63Sycle36SO_Close
6SycleStage14UpperSycleStage63Sycle36SO_Entry

検証を実施

バックテストの関数を作成

ここが一番重要だったりするのですが、ものすごく長くなってしまうので割愛します。こちらのサイトのコードを参考に作成しました((´・ω・`;)ヒィィッ すいません「バックテストを試してみました」

参考までに、ストラテジー部分のみ掲載します。

GoogleColab
class Strategy_SycleEMA_Base(StrategyBase) :

  def __init__(self ,symbol ,np_arr_dic ,df) :

    StrategyBase.__init__(self ,symbol ,np_arr_dic ,df)

    self.ATR_N = 3

    self.ema5   = ta.EMA(self.c ,5)
    self.ema20  = ta.EMA(self.c ,20)
    self.ema40  = ta.EMA(self.c ,40)
    self.ema120 = ta.EMA(self.c ,120)
    self.ema240 = ta.EMA(self.c ,240)

    self.stage = np.array(list(map(lambda s ,m ,l :
                                  1 if s > m and m > l else \
                                  2 if m > s and s > l else \
                                  3 if m > l and l > s else \
                                  4 if l > m and m > s else \
                                  5 if l > s and s > m else \
                                  6 if s > l and l > m else \
                                  np.nan
                                  ,self.ema5 ,self.ema20 ,self.ema40)))

    self.upper_stage = np.array(list(map(lambda s ,m ,l :
                                    1 if s > m and m > l else \
                                    2 if m > s and s > l else \
                                    3 if m > l and l > s else \
                                    4 if l > m and m > s else \
                                    5 if l > s and s > m else \
                                    6 if s > l and l > m else \
                                    np.nan
                                    ,self.ema5 ,self.ema120 ,self.ema240)))

    self.atrL ,self.atrH = ta.MINMAX(self.atr20 ,1800)
    self.atrM = (self.atrH + self.atrL) / 2
    self.atr  = np.array(list(map(lambda a ,b :
                                 a if a > b else \
                                 b if a < b else \
                                 a if np.isnan(b)==True and np.isnan(a)==False else \
                                 b if np.isnan(a)==True and np.isnan(b)==False else \
                                 np.nan
                                 ,self.atr20 ,self.atrM)))


  def Base_record_mgmt(self ,i) :

    Pos             = self.count_pos()
    self.count_days = np.nan if Pos==0 else self.count_days + 1
    b4_EntrySig     = False  if Pos==0 else abs(self.mgmt[-1]["EntrySig"])==1
    MTM             = 0      if Pos==0 else ((self.o[i] - self.entry_price) * self.l_s)
    b4_SO_Close     = np.nan if Pos==0 else self.mgmt[-1]["SO_Close"]
    b4_SO_Entry     = np.nan if Pos==0 else self.mgmt[-1]["SO_Entry"]
    pre_SO_Close    = np.nan if Pos==0 and not b4_EntrySig else self.c[i-1] - self.atr[i-1] * self.ATR_N * self.l_s
    pre_SO_Entry    = np.nan if Pos==0 and not b4_EntrySig else self.entry_price - self.atr[i-1] * self.ATR_N * self.l_s
    SO_Close        = np.nan if Pos==0 and not b4_EntrySig else pre_SO_Close if b4_EntrySig or (self.l_s==1 and pre_SO_Close > b4_SO_Close) or (self.l_s==-1 and pre_SO_Close < b4_SO_Close) else b4_SO_Close
    SO_Entry        = np.nan if Pos==0 and not b4_EntrySig else pre_SO_Entry if b4_EntrySig or (self.l_s==1 and pre_SO_Entry > b4_SO_Entry) or (self.l_s==-1 and pre_SO_Entry < b4_SO_Entry) else b4_SO_Entry
    PL              = 0      if len(self.hst)==0 else self.hst[-1]['profit'] if self.mgmt[-1]['ExitSig']==1 else 0
    self.mgmt.append({
        'Time'        : self.df.index[i]
        ,'Open'       : self.o[i]
        ,'High'       : self.h[i]
        ,'Low'        : self.l[i]
        ,'Close'      : self.c[i]
        ,'ATR'        : self.atr[i]
        ,'EMA5'       : self.ema5[i]
        ,'EMA20'      : self.ema20[i]
        ,'EMA40'      : self.ema40[i]
        ,'EMA120'     : self.ema120[i]
        ,'EMA240'     : self.ema240[i]
        ,'Pos'        : Pos
        ,'CountDays'  : self.count_days
        ,'EntrySig'   : 1 if self.opbuy else -1 if self.opsell else 0
        ,'ExitSig'    : 1 if self.clbuy or self.clsell else 0
        ,'L/S'        : self.l_s
        ,'EntryPrice' : self.entry_price
        ,'MTM'        : MTM
        ,'PL'         : PL
        ,'Stage'      : self.stage[i]
        ,'UpperStage' : self.upper_stage[i]
        ,'SO'         : self.SO
        ,'SO_Close'   : SO_Close
        ,'SO_Entry'   : SO_Entry
    })



class ENTRY_CycleStage14_FILTER_UpperCycleStage14__EXIT1_CycleStage36__EXIT2_SO_Entry(Strategy_SycleEMA_Base) :

  def __init__(self ,symbol ,np_arr_dic ,df) :

    Strategy_SycleEMA_Base.__init__(self ,symbol ,np_arr_dic ,df)


  def onTick(self ,i) :

    if self.is_nan[i] : return

    if any([self.opbuy ,self.opsell ,self.clbuy ,self.clsell]) :
      if self.SO :
        self.order_proccesing(i ,Entry_Price=self.c[i-1] ,Exit_Price=self.mgmt[-2]['SO_Entry'] ,ATR=self.atr[i-1])
        self.SO = False
      else :
        self.order_proccesing(i ,Entry_Price=self.c[i-1] ,Exit_Price=self.c[i-1] ,ATR=self.atr[i-1])

    opbuy = opsell = clbuy_1 = clbuy_2 = clsell_1 = clsell_2 = False

    buy_filter  = self.upper_stage[i] == 1
    buy_sig     = self.stage[i]       == 1

    sell_filter = self.upper_stage[i] == 4
    sell_sig    = self.stage[i]       == 4

    self.opbuy  = buy_filter  and buy_sig  and self.count_buy()==0
    self.opsell = sell_filter and sell_sig and self.count_sell()==0

    if self.count_buy() > 0 :
      SO_Entry   = self.entry_price - self.atr[i-1] * self.ATR_N * self.l_s if self.count_days==0 else self.mgmt[-1]['SO_Entry']
      clbuy_1    = self.stage[i-1] < 3 and (self.stage[i] == 3 or self.stage[i] == 4)
      clbuy_2    = self.l[i] < SO_Entry
      self.clbuy = any([clbuy_1 ,clbuy_2 ,self.opsell])
      self.SO    = clbuy_2

    if self.count_sell() > 0 :
      SO_Entry    = self.entry_price - self.atr[i-1] * self.ATR_N * self.l_s if self.count_days==0 else self.mgmt[-1]['SO_Entry']
      clsell_1    = (self.stage[i-1] < 6 and self.stage[i-1] >= 4) and (self.stage[i] == 6 or self.stage[i] == 1)
      clsell_2    = self.h[i] > SO_Entry
      self.clsell = any([clsell_1 ,clsell_2 ,self.opbuy])
      self.SO     = clsell_2

    self.Base_record_mgmt(i)

バックテストの実行と分析

諸々の準備が整ったら、バックテストを実行します。

パターンEntryFilterExit1Exit2
1SycleStage14noneSycle36SO_Entry
2SycleStage14UpperSycleStage14Sycle36SO_Entry
3SycleStage14UpperSycleStage14Sycle36SO_Close
4SycleStage63UpperSycleStage63Sycle36SO_Entry
5SycleStage63UpperSycleStage63Sycle36SO_Close
6SycleStage14UpperSycleStage63Sycle36SO_Entry

結果を分析

各ストラテジーの統計は以下の通りでした。

取引回数勝率平均利益/atr平均損失/atr累計損益/atrRR比率期待値/atr
1465631.06%2.998-1.248348.9312.400.071
2259431.42%3.233-1.342259.5752.410.095
3287032.65%3.008-1.296324.6212.320.109
4363033.25%3.072-1.359436.7122.260.115
5398533.75%2.937-1.322477.5442.220.115
6343532.98%3.104-1.348429.8952.300.120

金額にかかる項目はすべてそのときのATRで割った数値を使っています。そうすることで、すべての銘柄の検証結果を同じように扱うことができます。

日足を考慮すると(2)

1は日足を考慮していないもの、2は1に加えて日足を考慮したもの(その他の条件は同じ)ですが、結果をみると勝率とRR比率、期待値のすべてにおいて改善したことがわかります。しかし、累計損益はというと減少しています。これは取引回数の減少によるものだと考えるのが妥当でしょう。

トレイリングストップを導入すると(3)

3は2のEXIT2をトレイリングストップに変更したものです。平均利益とRR比率が低下したものの、その他のすべての項目が改善されました。最終的な成果にあたる累計損益も2から改善しました。しかし、1と比べるとまだ物足りなさがあります。

エントリーを早める(4、5、6)

3までの結果を受けて、エントリーを早めてみることにしました。すると累計損益が大幅に改善され、1よりも良い結果になりました。もっとも良いものは5で、トレイリングストップの効果の高さを確認することができます。

銘柄ごとの損益曲線

バックテスト Nikkei225 バックテスト USDJPY バックテスト EURJPY バックテスト GBPJPY バックテスト AUDJPY バックテスト EURUSD バックテスト GBPUSD バックテスト AUDUSD

ストラテジーごとの損益曲線

バックテスト ストラテジー1 バックテスト ストラテジー2 バックテスト ストラテジー3 バックテスト ストラテジー4 バックテスト ストラテジー5 バックテスト ストラテジー6

まとめ

さて、いかがでしたでしょうか。

日足を考慮して4時間足でトレードすることは概ね有効だと言えそうです。注意点として、「どんなに勝率やRR比率があがったとしても取引回数がともなわなければ本末転倒になりかねない」ということがありそうです。また、単一銘柄では資産曲線がマイナスになる時期が長いものがあるので、分散投資の必要性も感じます。

この手法の想定される利益は

もっとも成績がよかった5の想定される利益は、以下のように算出することができます。100万円の運用資金だとすると、477万円の利益で、577万円まで資金が増えるであろうことがわかります。

5の累計損益/atr477.544
運用資金100万円
資金管理1-ATR=運用資金の1%

477.544×1万円+100万円=5,775,440円477.544 \times \text{1万円} + \text{100万円} = \text{5,775,440円}

4時間足以外で行うと

ちなみに、データは掲載しませんが、メインの足を1時間足に変更すると、一転、成績が良くなるどころか悪化してしまいました。つまり、上位足なら何でも良いということではないのかもしれないですね。とくに日足は、足種のなかでも少し特殊です。他の足種に比べて四本値に多くの意味を持つのが日足ですから、そのあたりも関係しているのかもしれません(いずれにしてもさらなる検証が必要ですが)

注意点

これらの結果は4時間足のものです。トレードする時間足を変えると、これらの結果もガラッと変わることがありますので十分ご注意ください。

取引回数としてのサンプルは十分、リーマンショックの期間も含まれているので、そこそこ参考になる数字だとは思います。しかし、銘柄数や年数のサンプルとしては、正直ちょっと不十分です。銘柄や年の変化は想像以上にトレード結果に影響を及ぼしますので。

また、バックテストには万全を期していますが、結果の完全性や手法を用いた際の利益を保証するものではありません。予めご了承ください。

タカハシ / 8年目の兼業トレーダー

元・日本料理の板前。現在は、投資やプログラミング、動画コンテンツの撮影・制作・編集などを。更新のお知らせは、各SNSやLINEで。LINEだと1対1でお話することもできます!

このブログと筆者について運用管理表

  • 記事をシェア
© Investment Tech Hack 2021.