1. はじめに
みなさんどうもこんにちはガルシアです。
みなさん為替とか株とか興味ありますか? 私は金融商品そのものというより、チャートの「値動き」を眺めたりするのが好きなのですが、「何か法則性とか発見できたら面白そうだなー」という純粋な興味で、Pythonを使ってデータを解析してみました。
今回は、将来的に検証や機械学習、アルゴリズム開発なんかをやるときに使えそうな「データの基準作り」に挑んだ記録と、そこで得られた統計データ、そして失敗(気づき)を共有したいと思います。
2. なぜ「ジグザグ」なのか?
値動きのデータで動く売買システムを作ったり、機械学習を行ったりする際、生のローソク足データだとノイズが多すぎることがあります。そこで、一貫した基準を設ける必要が出てくる場合があります。
よく用いられるのが、値動きの上下を抽象化・一般化した「ジグザグ(ZigZag)」という考え方です。 要は、細かいギザギザを無視して「大きな波」として捉えよう、というものです。

しかし、既存のジグザグや値動きを抽象化するインジケーターは判定が大雑把すぎると感じていました。「もっと厳密に、しかし抽象度を保ったまま、無視すべきではない値動きを見逃さない、素晴らしいジグザグを作りたい」 そう思ったのが始まりでした。
3. まずはデータを知る:統計編
ちなみにみなさんはローソク足がどう作られるかご存知ですか?わざわざパワポでわかりやすく書いてみましたから、知らなかったら見ていってくれや!
まず大きく2種類ありますが、始値よりも終値が高かったら陽線、始値よりも終値が低かったら陰線です。 これだけです。ローソク足は主に4つの情報から構成されます。ある時間単位(例えば1分、1時間、1日など)の始値、高値、安値、終値です。文字通りその区切られた時間内での最初の値(始値)と最後の値(終値)、最も高い値と安い値です。
ひげ(箱から出てる線)は始値と終値の範囲外の値動きを表しています。そしてこれらは1分足、1時間足、日足というように、値動きを時間単位でグルーピングしたものを一般的にローソク足と呼ぶことが多いと思います。値動きのグルーピング方法は他にも色々ありますがここでは時間で区切ったものを指すことにします。
他にもいろいろな形があって可愛いですね!
4. 最強を目指して沼る:実装編
ということで、最強のジグザグを作る前に、まずは「ローソク足ってそもそもどういう動きをするのか」という疑問から、2024年のドル円相場(1年分・1分足約20万本)のデータを解析してみました。データはDukascopyという素晴らしいスイスのブローカーからダウンロードさせていただきました。ありがとう!ちゅ!
統計解析
以下スクリプトの一部です。何をやっているかというと陽線・陰線、ひげあり・なし、高値と安値の到達順序(高値と安値どちらの値が先か)で分類しています。Pandasで値動きをグルーピングしています。
コード
def classify_candles(time: pd.Timestamp, open: float, high: float, low: float, close: float, high_time: pd.Timestamp, low_time: pd.Timestamp) -> str:
# When there's no high_time and low_time
if pd.isna(high_time) and pd.isna(low_time):
return 'blank'
# Bullish
elif close > open:
# LH
if low_time < high_time:
# Full Wicks
if high > close and low < open:
return 'Bullish_LH_FullWick'
# Top Wick
elif high > close and low == open:
return 'Bullish_LH_TopWick'
# Bottom Wick
elif high == close and low < open:
return 'Bullish_LH_BottomWick'
# No Wicks
elif high == close and low == open:
return 'Bullish_LH_NoWick'
# Unknown
else:
return '__bullish_LH_UNKNOWN'
# HL
elif high_time < low_time:
# Full Wicks
if high > close and low < open:
return 'Bullish_HL_FullWick'
# Bottom Wicks
elif high == close and low < open:
return 'Bullish_HL_BottomWick'
# Unknown
else:
return '__bullish_HL_UNKNOWN'
# Unknown
else:
return '__bullish_UNKNOWN'
# Bearish
elif close < open:
# HL
if high_time < low_time:
# Full Wicks
if high > open and low < close:
return 'Bearish_HL_FullWick'
# Bottom Wick
elif high == open and low < close:
return 'Bearish_HL_BottomWick'
# Top Wick
elif high > open and low == close:
return 'Bearish_HL_TopWick'
# No Wicks
elif high == open and low == close:
return 'Bearish_HL_NoWick'
# Unknown
else:
return '__bearish_HL_UNKNOWN'
# LH
elif low_time < high_time:
# Full Wicks
if high > open and low < close:
return 'Bearish_LH_FullWick'
# Top Wick
elif high > open and low == close:
return 'Bearish_LH_TopWick'
# Unknown
else:
return '__bearish_LH_UNKNOWN'
#Unknwon
else:
return '__bearish_UNKNOWN'
# Doji
elif close == open:
# HL
if high_time < low_time:
# Full Wicks
if high > open and low < close:
return 'Doji_HL_FullWick'
# Bottom Wick
elif high == open and low < close:
return 'Doji_HL_BottomWick'
# Unknown
else:
return 'doji_HL_UNKNOWN'
# LH
elif low_time < high_time:
# Full Wicks
if high > open and low < close:
return 'Doji_LH_FullWick'
# Top Wick
elif high > open and low == close:
return 'Doji_LH_TopWick'
# Unknown
else:
return 'doji_LH_UNKNOWN'
# Single_Event
elif high_time == low_time:
return 'Single_Event'
else:
return "UNKNOWN"
以下実行結果(わかりにくいですすみません):
実行結果
1min
--- Candle Formation Analysis ---
Total Bullish Candles Analyzed: 199228
Clean Bullish Formation (Open->Low->High->Close): 175858 (88.27%)
-----------------------------------
Total Bearish Candles Analyzed: 175772
Clean Bearish Formation (Open->High->Low->Close): 160474 (91.30%)
2min
--- 2-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 98249
Clean Bullish Formation (Open->Low->High->Close): 85876 (87.41%)
-----------------------------------
Total Bearish Candles Analyzed: 89251
Clean Bearish Formation (Open->High->Low->Close): 80075 (89.72%)
4min
--- 4-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 48642
Clean Bullish Formation (Open->Low->High->Close): 42604 (87.59%)
-----------------------------------
Total Bearish Candles Analyzed: 45108
Clean Bearish Formation (Open->High->Low->Close): 40017 (88.71%)
8min
--- 8-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 24396
Clean Bullish Formation (Open->Low->High->Close): 21391 (87.68%)
-----------------------------------
Total Bearish Candles Analyzed: 22483
Clean Bearish Formation (Open->High->Low->Close): 19783 (87.99%)
16min
--- 16-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 12152
Clean Bullish Formation (Open->Low->High->Close): 10709 (88.13%)
-----------------------------------
Total Bearish Candles Analyzed: 11290
Clean Bearish Formation (Open->High->Low->Close): 9943 (88.07%)
60min
--- 60-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 3245
Clean Bullish Formation (Open->Low->High->Close): 2867 (88.35%)
-----------------------------------
Total Bearish Candles Analyzed: 3005
Clean Bearish Formation (Open->High->Low->Close): 2616 (87.05%)
240min (4h)
--- 240-Minute Candle Formation Analysis ---
Total Bullish Candles Analyzed: 841
Clean Bullish Formation (Open->Low->High->Close): 762 (90.61%)
-----------------------------------
Total Bearish Candles Analyzed: 726
Clean Bearish Formation (Open->High->Low->Close): 625 (86.09%)
陽線の約88%が「始値」の後に「安値」、次に「高値」で「終値」をつけ、残りの約12%の陽線が「始値」の後に「高値」、そして「安値」をつけ、始値よりも高い位置で「終値」をつける、という順番であったことがわかりました。
陰線の場合も同じ傾向で、約91%が先に「高値」、そして「安値」。陰線の約9%が先に「安値」、あとに「高値」となっていました。
これがなんで面白いかというと、一般的に陽線は「上昇」と考えることが多いので、始値後の安値から、右肩上がりに上がっていくという印象が強く、実際にデータでも約9割がそうでありますが、ジグザグのインジケータを用いて抽象化するときに残りの約1割を考慮しないと不整合が起きる原因となります。
さらに面白いのが、このグルーピングの時間を長くして同じ検証をして見ました。先ほどのデータは1分足でしたが、2分足、4足分、60分足、240分足まで試してみた結果、陽線・陰線全て1分足と大して変わらない実行結果でした。陽線の約87%~90%は常に安値が先、陰線の約86%~91%は高値が先という、多少の差はあれど異なる時間幅で同じ程度生み出されていました。
他にも高値安値をつけるタイミングとか色々面白いデータはあったのですが長くなるのでこれはここまでにします。
以上の統計結果から、ジグザグを作るならローソク足の構成要素
の順序に着目すれば、それなりのZigzagが引けるかも!ということで、ここから沼っていきます。
まず思い浮かんだ方法は二つのローソク足の内部構造を比較して、値動きを抽象化する(高値や安値を線で結ぶ)方法です。
こんな感じに隣り合うローソク足のタイプでつなぎ方を決める方法を考えていたのですが、さすがに全部コードで書くわけにはいかないのでスマートな方法はないか考えていたら、「1つ目のローソク足の終値の前の値動き」と、2つ目のローソク足の始値の後の値動きをプラス**(+)とマイナス(ー)で表現する方法」を思いつきました。
こうすれば全てのローソク足(たしか陽線陰線それぞれ17パターンと例外1つくらい)の前と後ろにプラスかマイナスの情報をつけることで、たった4パターン(++、ーー、 +ー、 ー+)を組み合わせる処理を書けば綺麗にできそう?!ってことで書いたのものの一部が以下です。
コード
def _handle_continuation(self) -> None:
# If connection is (++) and candle2 open is lower than the candle1 close, add 2 pivots.
if self.connection == ('+','+') and self.candle2['open_price'] < self.candle1['close_price']:
# First add down arrow pivot(indicating high) at the close of the candle1.
self.swings.append({
'time' : self.candle1['close_time'],
'price' : self.candle1['close_price'],
'type' : 'high'
})
# Then add up arrow pivot(indicating low) at the open of the candle2.
self.swings.append({
'time' : self.candle2['open_time'],
'price' : self.candle2['open_price'],
'type' : 'low'
})
# If connection is (--) and candle2 open is higher than the candle1 close, add 2 pivots.
elif self.connection == ('-','-') and self.candle2['open_price'] > self.candle1['close_price']:
# First add up pivot(indicating low) at the close of the candle1.
self.swings.append({
'time' : self.candle1['close_time'],
'price': self.candle1['close_price'],
'type' : 'low'
})
# Then add down pivot(indicating high) at the open of the candle2.
self.swings.append({
'time' : self.candle2['open_time'],
'price': self.candle2['open_price'],
'type' : 'high'
})
if self.candle2['pivots']: # Candles with wicks
for pivot in self.candle2['pivots']:
# LH pivots
if pivot == 'high_betweenOpenLow':
self.swings.append({
'time' : self.candle2['high_between_open_low_time'],
'price': self.candle2['high_between_open_low_price'],
'type' : 'high'
})
elif pivot == 'low_betweenHighClose':
self.swings.append({
'time' : self.candle2['low_between_high_close_time'],
'price': self.candle2['low_between_high_close_price'],
'type' : 'low'
})
# HL pivots
elif pivot == 'low_betweenOpenHigh':
self.swings.append({
'time' : self.candle2['low_between_open_high_time'],
'price': self.candle2['low_between_open_high_price'],
'type' : 'low'
})
elif pivot == 'high_betweenLowClose':
self.swings.append({
'time' : self.candle2['high_between_low_close_time'],
'price': self.candle2['high_between_low_close_price'],
'type' : 'high'
})
# Standard pivots
else:
self.swings.append({
'time' : self.candle2[ pivot +'_time'], # high_time / low_time
'price' : self.candle2[ pivot + '_price'], # high_price / low_price
'type' : pivot # high / low
})
def _handle_reversal(self) -> None:
# Appending pivot depending on the higher or lower close/open price
# If connection is (+-) and candle1 close_price is higher than equal to the candle2 open_price
if self.connection == ('+', '-') and self.candle1['close_price'] >= self.candle2['open_price']:
self.swings.append({
'time' : self.candle1['close_time'],
'price' : self.candle1['close_price'],
'type' : 'high'
})
# If connection is (+-) and candle1 close_price is lower to the candle2 open_price
elif self.connection == ('+', '-') and self.candle1['close_price'] < self.candle2['open_price']:
self.swings.append({
'time' : self.candle2['open_time'],
'price' : self.candle2['open_price'],
'type' : 'high'
})
# If connection is (-+) and candle1 close_price is lower than equal to the candle2 open_price
if self.connection == ('-', '+') and self.candle1['close_price'] <= self.candle2['open_price']:
self.swings.append({
'time' : self.candle1['close_time'],
'price' : self.candle1['close_price'],
'type' : 'low'
})
# If connection is (+-) and candle1 close_price is higher to the candle2 open_price
elif self.connection == ('-', '+') and self.candle1['close_price'] > self.candle2['open_price']:
self.swings.append({
'time' : self.candle2['open_time'],
'price' : self.candle2['open_price'],
'type' : 'low'
})
if self.candle2['pivots']: # Candles with wicks
for pivot in self.candle2['pivots']:
# LH pivots
if pivot == 'high_betweenOpenLow':
self.swings.append({
'time' : self.candle2['high_between_open_low_time'],
'price': self.candle2['high_between_open_low_price'],
'type' : 'high'
})
elif pivot == 'low_betweenHighClose':
self.swings.append({
'time' : self.candle2['low_between_high_close_time'],
'price': self.candle2['low_between_high_close_price'],
'type' : 'low'
})
# HL pivots
elif pivot == 'low_betweenOpenHigh':
self.swings.append({
'time' : self.candle2['low_between_open_high_time'],
'price': self.candle2['low_between_open_high_price'],
'type' : 'low'
})
elif pivot == 'high_betweenLowClose':
self.swings.append({
'time' : self.candle2['high_between_low_close_time'],
'price': self.candle2['high_between_low_close_price'],
'type' : 'high'
})
# Standard pivots
else:
self.swings.append({
'time' : self.candle2[ pivot +'_time'], # high_time / low_time
'price' : self.candle2[ pivot + '_price'], # high_price / low_price
'type' : pivot # high / low
})
コードを見てもわかりずらくて申し訳ないのですが、基本的にローソク足がなり得る全パターンにこの+ーの情報を付与し、それらのつながり方を見て、ジグザグを書いています。そして実際に出来上がったものがこちらです。
実際に線は引いていないのですが、山と谷を結ぶことでジグザグになります。(ホニョホニョしてるのが値動きです。tickデータと呼びます)
ですが、正直これは失敗です。ちなみに上記のコードは何回もコードを追記したバージョン5です。なぜバージョン1からバージョン5までいってしまったかというと、ローソク足単体から得るには適していない情報を得ようとしていたからです。それらは高値や安値などのように「〜値」とラベル付けされている情報の間にある値動きでした。例えば以下の場合、私のロジックでは適切に把握することができません。
そこで私は勢い任せにこの取れない値動きを取ろうとコードを付け足していったのです。その結果、バージョン5にまでなってしまって、そこでやっとキリがないことに気づきました。ローソク足から取得することが難しい情報=より細かい値動きを取ろうとしていたのです。抽象度を一定に保つ必要があるのに、これでは本末転倒です。さらに、ローソク足の実体(箱の中)の値動きに顕著なものがあっても、その情報を取得する手段もないのです。
まとめ
そもそもローソク足自体、値動きを「時間」という単位で恣意的に区切って抽象化しているものに過ぎない、ということに改めて気付かされました。そこで、気になったのでローソク足を構成している値動きそのものが、時間帯によってどのくらいの頻度で発生しているのかデータをとってみると、以下のような結果になりました。
[とっておいたはずのデータがどっかいっちゃったのでお見せできません。見つかったら載せます。]
もちろん結果は、時間帯によって値動きの量そのものが違いました。また時間帯による違いに限らず、同じ1分足でも、その隣にある1分足がもつ値動きの量は異なるのです。構成する値動きの量がひとつひとつ違うのであれば、それに適用するロジックに変えないと私が願ったようなジグザグは引けないのかもしれません。横軸に時間、縦軸に価格を設けて比較していましたが、そもそも「質」の違うものをあたかも同じものであるかのように見ているに過ぎなかったのです。ということで次からは量に着目していきたいと思います。読んでいただきどうもありがとうございました。






