どんな記事
この記事を読むと、次のことがわかるようになります。
- 上位足を確認することの有効性
- エッジのあるトレードの一例
- バックテストのやり方
- 単利で計算した、この手法の想定利益
「エントリーするときは、上位足を必ず確認した方が良い」
こんな話をよく聞くと思います。そして、この話を信じて実行している人も多いことでしょう。
しかし、ふとあることに気付きました。「上位足うんぬん~」に関するデータの裏付けをみたことがないということです。
と、いうわけでやってみました。上位足を考慮したバックテスト!
手法は千差万別なので、この記事の結果がすべてではありませんが、ひとつの、それなりの結果は提示できると思います。
バックテストの方法
どんなツールを使用して、どんな方法でバックテストを行ったのかの記録です。
使用したツール
バックテストには以下のツールを使用しました。
Python(Google Colaboratory)
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:本仕掛け
今回のバックテストは、エントリーを次のように組み合わせていくつかパターンを作成し行いました。
ENTRY_CycleStage14_FILTER_UpperCycleStage14
通常のパターン(エントリーが遅くなりがち)
ENTRY_CycleStage63_FILTER_UpperCycleStage63
早仕掛けのパターン(エントリーは早いが〝だまし〟が多くなりがち)
ENTRY_CycleStage14_FILTER_UpperCycleStage63
組み合わせたパターン(ハイブリットを目指す)
決済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時間足だけで判断した場合)
パターン | Entry | Filter | Exit1 | Exit2 |
---|---|---|---|---|
1 | SycleStage14 | none | Sycle36 | SO_Entry |
2 | SycleStage14 | UpperSycleStage14 | Sycle36 | SO_Entry |
3 | SycleStage14 | UpperSycleStage14 | Sycle36 | SO_Close |
4 | SycleStage63 | UpperSycleStage63 | Sycle36 | SO_Entry |
5 | SycleStage63 | UpperSycleStage63 | Sycle36 | SO_Close |
6 | SycleStage14 | UpperSycleStage63 | Sycle36 | SO_Entry |
検証を実施
バックテストの関数を作成
ここが一番重要だったりするのですが、ものすごく長くなってしまうので割愛します。こちらのサイトのコードを参考に作成しました((´・ω・`;)ヒィィッ すいません「バックテストを試してみました」)
参考までに、ストラテジー部分のみ掲載します。
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)
バックテストの実行と分析
諸々の準備が整ったら、バックテストを実行します。
パターン | Entry | Filter | Exit1 | Exit2 |
---|---|---|---|---|
1 | SycleStage14 | none | Sycle36 | SO_Entry |
2 | SycleStage14 | UpperSycleStage14 | Sycle36 | SO_Entry |
3 | SycleStage14 | UpperSycleStage14 | Sycle36 | SO_Close |
4 | SycleStage63 | UpperSycleStage63 | Sycle36 | SO_Entry |
5 | SycleStage63 | UpperSycleStage63 | Sycle36 | SO_Close |
6 | SycleStage14 | UpperSycleStage63 | Sycle36 | SO_Entry |
結果を分析
各ストラテジーの統計は以下の通りでした。
取引回数 | 勝率 | 平均利益/atr | 平均損失/atr | 累計損益/atr | RR比率 | 期待値/atr | |
---|---|---|---|---|---|---|---|
1 | 4656 | 31.06% | 2.998 | -1.248 | 348.931 | 2.40 | 0.071 |
2 | 2594 | 31.42% | 3.233 | -1.342 | 259.575 | 2.41 | 0.095 |
3 | 2870 | 32.65% | 3.008 | -1.296 | 324.621 | 2.32 | 0.109 |
4 | 3630 | 33.25% | 3.072 | -1.359 | 436.712 | 2.26 | 0.115 |
5 | 3985 | 33.75% | 2.937 | -1.322 | 477.544 | 2.22 | 0.115 |
6 | 3435 | 32.98% | 3.104 | -1.348 | 429.895 | 2.30 | 0.120 |
金額にかかる項目はすべてそのときのATRで割った数値を使っています。そうすることで、すべての銘柄の検証結果を同じように扱うことができます。