Ku-Chartのグラフ (2017/08/05)

今年の値動きをKu-Chartでグラフにしてみる。直近の値動きを見ると、ユーロの上昇が少し異常な気がした。それで、ユーロ、円、ドルの3通貨モデルのKu-Chartではどうなっているか気になったのである。

import forex_system as fs
import matplotlib.pyplot as plt
from datetime import datetime

start = datetime.strptime('2017.01.01 00:00', '%Y.%m.%d %H:%M')
end = datetime.strptime('2017.08.04 23:59', '%Y.%m.%d %H:%M')
ku_close = fs.i_ku_close(1440, 0, eur=1, jpy=1, usd=1)[start:end]
ku_close = ku_close - ku_close.iloc[0, :]
plt.title('Ku-Chart (EUR, JPY, USD)')
plt.ylabel('Change')
plt.xlabel('Date')
plt.plot(ku_close['EUR'], label='EUR', color='red')
plt.plot(ku_close['JPY'], label='JPY', color='turquoise')
plt.plot(ku_close['USD'], label='USD', color='orange')
plt.legend()
plt.savefig('ku_chart.png', dpi=150)
plt.show()

グラフを見ると特に異常ということはなさそうだ。ドルがだらだらと下落する中で、ユーロと円は反対の動きをしている。直近ではユーロが上昇、円が下落だから、やたらと対円、対ドルで強く見えただけのようだ。

相関係数とトレンド

相関係数とトレンドとの関係を考えてみる。

正相関ならトレンド的か?

時系列データにおいて、データの階差とその前のデータの階差とが正相関、つまり相関係数がプラスの場合、トレンド的と言えるだろうか。

また、逆にデータの階差とその前のデータの階差とが逆相関、つまり相関係数がマイナスの場合、平均回帰的と言えるだろうか。

この問いに対して何となく「そうだろう」と考えている人は少なくないのではないかと思われる。というか、私がそうだった(笑)。

ただ、共和分の時系列データは平均回帰的だが、必ずしも逆相関ではない。したがって、完全に対応しているわけではない。しかし、それでも対応しているケースが多いのではないかと私は考えていたのである。

y = sin(x)における相関係数

そういうケースが多いか少ないかということは置いておく。ここでは正相関であり、かつ平均回帰的であって、しかも、それが極端であるデータを作成してみる。これはいたって簡単で、「y = sin(x)」のデータを作成すればいいだけである。

先ず、データを作成して相関係数を見てみる。

In [1]:
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-360, 360, 1)
y = np.sin(x / 180 * np.pi)

diff = y[1:] - y[:-1]
diff0 = diff[1:]
diff1 = diff[:-1]
cor = np.corrcoef(diff1, diff0)[0, 1]
print('cor = ', cor)


cor =  0.999846842234


相関係数はほぼ1.0で、完全に近い正相関となっている。考えてみれば、これは当然である。sin(x)の向きはずっとプラスか、ずっとマイナスで、プラスからマイナス、またはマイナスからプラスに転じるのは頂点だけだからだ。

y = sin(x)のグラフ

では次に、同じデータを使ってグラフを作成して見てみる。

In [2]:
plt.plot(x, y, label='y = sin(x)')
plt.title('y = sin(x)')
plt.xlabel('x')
plt.ylabel('y')
plt.xticks([-360, -270, -180, -90, 0, 90, 180, 270, 360])
plt.legend(loc='upper right')
plt.axvline(x=0.0, color='black')
plt.axhline(y=0.0, color='black')
plt.tight_layout()
plt.savefig('sinx.png', dpi=150)
plt.show()



高校数学でよく見かけるグラフだが、改めて見ると完全に平均回帰的である。-1で買い、+1で売れば百戦百勝だ。

相関係数とトレンドは別物

ほぼ完全な正相関でありながら完全に平均回帰的であるデータを作成できるということを考えると、相関係数とトレンド(あるいは平均回帰)は全くの別物と見たほうがいいだろう。

(2017/03/20更新)

シグナル生成プログラム作成の小技

シグナルを生成するプログラムを作成するときに使える小技を紹介する。

長いので読む気が失せるかもしれない。だが、単に説明がくどいだけで難しくはない。実際にはほんの数行のプログラムである。

for文を使ったら負け

シグナル生成プログラムを作成する場合、for文を使うと処理が非常に遅くなる。for文の処理が遅いというのはPythonの仕様である。for文を使ったら負けだと考えよう。

日足のように数の少ないデータを使う場合はあまり気にならないかもしれない。だが、日中足のように膨大な数のデータを使う場合、それは耐え難いものになる。

配列関数を使おう

一方、配列関数には高速化されているものがある。今回使うのはPandasのfillna()関数だけである。

C++のような高速な言語であればfor文で処理するところではある。これを基本的にfillna()関数だけを使って処理できるように工夫する。これでfor文の数倍から数十倍は速くなる。

シグナル発生の条件

先ずはシグナル発生の条件を定義する。必要となるのは

  • 買いエントリー
  • 買いエグジット
  • 売りエントリー
  • 売りエグジット

の4つである。

上の4つによってシグナルは「1」(買いポジション)、「-1」(売りポジション)、「0」(ノーポジション)の3つの状態となる。

買いエントリーで「1」となり、買いエグジットで「0」となる。または、売りエントリーで「-1」となり、売りエグジットで「0」となる。エグジットの直後にエントリーが発生した場合は直接「1」から「-1」へ、または「-1」から「1」へとなり、つまりドテンである。

さて、ここでは1本前の足のZスコアを使って上の4つを条件付けることとする。先ずは適当なデータでZスコアを準備する。

In [1]:
import forex_system as fs
import numpy as np
from datetime import datetime

start = datetime.strptime('2016.01.01 00:00', '%Y.%m.%d %H:%M')
end = datetime.strptime('2016.12.31 23:59', '%Y.%m.%d %H:%M')
zscore1 = fs.i_zscore('USDJPY', 1440, 5, 'MODE_SMA', 1)[start:end]
print(zscore1.head(20))


2016-01-01   -0.901037
2016-01-04   -1.036465
2016-01-05   -1.704807
2016-01-06   -1.351713
2016-01-07   -1.334252
2016-01-08   -1.344266
2016-01-11   -1.238142
2016-01-12   -0.382672
2016-01-13   -0.249737
2016-01-14    0.375163
2016-01-15    1.295042
2016-01-18   -1.628893
2016-01-19   -0.535417
2016-01-20    0.250968
2016-01-21   -0.942332
2016-01-22    1.080308
2016-01-25    1.608687
2016-01-26    0.614979
2016-01-27    0.545221
2016-01-28    0.697498
Name: close, dtype: float64


買いエントリーの作成

1本前の足のZスコアが-1以下だったら買いエントリーとしよう。

In [2]:
buy_entry = zscore1 <= -1.0
print(buy_entry.head(20))


2016-01-01    False
2016-01-04     True
2016-01-05     True
2016-01-06     True
2016-01-07     True
2016-01-08     True
2016-01-11     True
2016-01-12    False
2016-01-13    False
2016-01-14    False
2016-01-15    False
2016-01-18     True
2016-01-19    False
2016-01-20    False
2016-01-21    False
2016-01-22    False
2016-01-25    False
2016-01-26    False
2016-01-27    False
2016-01-28    False
Name: close, dtype: bool


買いエグジットの作成

1本前の足のZスコアが0以上だったら買いエグジットとしよう。

In [3]:
buy_exit = zscore1 >= 0.0
print(buy_exit.head(20))


2016-01-01    False
2016-01-04    False
2016-01-05    False
2016-01-06    False
2016-01-07    False
2016-01-08    False
2016-01-11    False
2016-01-12    False
2016-01-13    False
2016-01-14     True
2016-01-15     True
2016-01-18    False
2016-01-19    False
2016-01-20     True
2016-01-21    False
2016-01-22     True
2016-01-25     True
2016-01-26     True
2016-01-27     True
2016-01-28     True
Name: close, dtype: bool


売りエントリーの作成

1本前の足のZスコアが1以上だったら売りエントリーとしよう。

In [4]:
sell_entry = zscore1 >= 1.0
print(sell_entry.head(20))


2016-01-01    False
2016-01-04    False
2016-01-05    False
2016-01-06    False
2016-01-07    False
2016-01-08    False
2016-01-11    False
2016-01-12    False
2016-01-13    False
2016-01-14    False
2016-01-15     True
2016-01-18    False
2016-01-19    False
2016-01-20    False
2016-01-21    False
2016-01-22     True
2016-01-25     True
2016-01-26    False
2016-01-27    False
2016-01-28    False
Name: close, dtype: bool


売りエグジットの作成

1本前の足のZスコアが0以下だったら売りエグジットとしよう。

In [5]:
sell_exit = zscore1 <= 0.0
print(sell_exit.head(20))


2016-01-01     True
2016-01-04     True
2016-01-05     True
2016-01-06     True
2016-01-07     True
2016-01-08     True
2016-01-11     True
2016-01-12     True
2016-01-13     True
2016-01-14    False
2016-01-15    False
2016-01-18     True
2016-01-19     True
2016-01-20    False
2016-01-21     True
2016-01-22    False
2016-01-25    False
2016-01-26    False
2016-01-27    False
2016-01-28    False
Name: close, dtype: bool


これで買いエントリー、買いエグジット、売りエントリー、売りエグジットのすべてがそろった。

買いシグナルの作成①

4つの条件がすべてそろったら、先ず買いシグナルを作成することにする。

第1工程として、買いエントリーをコピーして買いシグナルの土台とし、「False」のところを「NaN」で置き換える。この一見したところ無意味な「NaN」を利用することが今回紹介したかった小技である。後でこの「NaN」が役に立つ。

In [6]:
buy = buy_entry.copy()
buy[buy==False] = np.nan
print(buy.head(20))


2016-01-01    NaN
2016-01-04    1.0
2016-01-05    1.0
2016-01-06    1.0
2016-01-07    1.0
2016-01-08    1.0
2016-01-11    1.0
2016-01-12    NaN
2016-01-13    NaN
2016-01-14    NaN
2016-01-15    NaN
2016-01-18    1.0
2016-01-19    NaN
2016-01-20    NaN
2016-01-21    NaN
2016-01-22    NaN
2016-01-25    NaN
2016-01-26    NaN
2016-01-27    NaN
2016-01-28    NaN
Name: close, dtype: float6


「False」が「NaN」に置き換えられる。なお、「True」が「1.0」に変わっているが同じ値である。データの型が元々bool型であったところ、float型である「NaN」を挿入した影響でfloat型に変換されたのである。

買いシグナルの作成②

第2工程として、買いエグジットの「True」と同じ位置にある買いシグナルのデータを「0.0」で置き換える。

In [7]:
buy[buy_exit==True] = 0.0
print(buy.head(20))


2016-01-01    NaN
2016-01-04    1.0
2016-01-05    1.0
2016-01-06    1.0
2016-01-07    1.0
2016-01-08    1.0
2016-01-11    1.0
2016-01-12    NaN
2016-01-13    NaN
2016-01-14    0.0
2016-01-15    0.0
2016-01-18    1.0
2016-01-19    NaN
2016-01-20    0.0
2016-01-21    NaN
2016-01-22    0.0
2016-01-25    0.0
2016-01-26    0.0
2016-01-27    0.0
2016-01-28    0.0
Name: close, dtype: float64


買いシグナルの作成③

第3工程として、買いシグナルの「NaN」を前のデータで置き換える。

In [8]:
buy = buy.fillna(method='ffill')
print(buy.head(20))


2016-01-01    NaN
2016-01-04    1.0
2016-01-05    1.0
2016-01-06    1.0
2016-01-07    1.0
2016-01-08    1.0
2016-01-11    1.0
2016-01-12    1.0
2016-01-13    1.0
2016-01-14    0.0
2016-01-15    0.0
2016-01-18    1.0
2016-01-19    1.0
2016-01-20    0.0
2016-01-21    0.0
2016-01-22    0.0
2016-01-25    0.0
2016-01-26    0.0
2016-01-27    0.0
2016-01-28    0.0
Name: close, dtype: float64


「NaN」となっていたところは買いエントリーも買いエグジットも発生していない。そういうところではどうするか。何もしないことである。

何もしないということは前のデータが「1.0」なら「1.0」で、「0.0」なら「0.0」で埋めるということである。「NaN」がいくつも続いている場合でも、先頭の「NaN」が「1.0」、または「0.0」になり、その後の「NaN」もそれに続くという形で埋められる。これが「NaN」を使った理由である。

Zスコアが-1以下なら買いポジションを持っている。0以上であれば買いポジションを持っていない。これは明確である。だが、-1から0の間はどちらであるか不明確である。

これは前の足で買いポジションを持っていれば持っているし、持っていなければ持っていない。つまり、前の足の条件次第なのである。

for文を使い、その中でif文を使って場合分けすれば同じことができるし、普通はそうするだろう。だが、Pythonでそれをやると時間がかかりすぎる。そこで「NaN」とfillna()関数を利用するのである。

「NaN」は不要な、削除すべきデータのように思われがちだ。しかし、このように自分で「NaN」を挿入して活用する方法もあるのである。

売りシグナルの作成①

次は売りシグナルを作成する。買いシグナルの場合と同じなので説明は省略する。

In [9]:
sell = sell_entry.copy()
sell[sell==False] = np.nan
print(sell.head(20))


2016-01-01    NaN
2016-01-04    NaN
2016-01-05    NaN
2016-01-06    NaN
2016-01-07    NaN
2016-01-08    NaN
2016-01-11    NaN
2016-01-12    NaN
2016-01-13    NaN
2016-01-14    NaN
2016-01-15    1.0
2016-01-18    NaN
2016-01-19    NaN
2016-01-20    NaN
2016-01-21    NaN
2016-01-22    1.0
2016-01-25    1.0
2016-01-26    NaN
2016-01-27    NaN
2016-01-28    NaN
Name: close, dtype: float64


売りシグナルの作成②

In [10]:
sell[sell_exit==True] = 0.0
print(sell.head(20))


2016-01-01    0.0
2016-01-04    0.0
2016-01-05    0.0
2016-01-06    0.0
2016-01-07    0.0
2016-01-08    0.0
2016-01-11    0.0
2016-01-12    0.0
2016-01-13    0.0
2016-01-14    NaN
2016-01-15    1.0
2016-01-18    0.0
2016-01-19    0.0
2016-01-20    NaN
2016-01-21    0.0
2016-01-22    1.0
2016-01-25    1.0
2016-01-26    NaN
2016-01-27    NaN
2016-01-28    NaN
Name: close, dtype: float64


売りシグナルの作成③

In [11]:
sell = sell.fillna(method='ffill')
print(sell.head(20))


2016-01-01    0.0
2016-01-04    0.0
2016-01-05    0.0
2016-01-06    0.0
2016-01-07    0.0
2016-01-08    0.0
2016-01-11    0.0
2016-01-12    0.0
2016-01-13    0.0
2016-01-14    0.0
2016-01-15    1.0
2016-01-18    0.0
2016-01-19    0.0
2016-01-20    0.0
2016-01-21    0.0
2016-01-22    1.0
2016-01-25    1.0
2016-01-26    1.0
2016-01-27    1.0
2016-01-28    1.0
Name: close, dtype: float64


これで買いシグナルと売りシグナルが完成した。

シグナルの作成①

それでは買いシグナルと売りシグナルを合わせて全体のシグナルとする。

In [12]:
signal = buy - sell
print(signal.head(20))


2016-01-01    NaN
2016-01-04    1.0
2016-01-05    1.0
2016-01-06    1.0
2016-01-07    1.0
2016-01-08    1.0
2016-01-11    1.0
2016-01-12    1.0
2016-01-13    1.0
2016-01-14    0.0
2016-01-15   -1.0
2016-01-18    1.0
2016-01-19    1.0
2016-01-20    0.0
2016-01-21    0.0
2016-01-22   -1.0
2016-01-25   -1.0
2016-01-26   -1.0
2016-01-27   -1.0
2016-01-28   -1.0
Name: close, dtype: float64


買いはプラス、売りはマイナスとするので、売りシグナルを減じる。初めから売りシグナルをマイナスにしておいて加えてもいい。

売買ルールが排他的なものとなっているかぎり、買いと売りが相殺して0.0になるというようなことはない。もしそういうことが起きたとしたら、それはプログラムのせいではなくて、売買ルールがおかしいのである。

シグナルの作成②

微調整を行う。残った「NaN」を「0.0」で埋める。

たまたま最初のデータが「NaN」であった場合、それより前のデータがないので「NaN」を埋めることができない。後のデータで埋めることもできることはできる。だが、それをやると、先読みバイアスのミスを犯すリスクがある(1個だけだから大したことはないが)。

とりあえず「0.0」で埋めておくのが無難であろう。

In [13]:
signal = signal.fillna(0.0)
print(signal.head(20))


2016-01-01    0.0
2016-01-04    1.0
2016-01-05    1.0
2016-01-06    1.0
2016-01-07    1.0
2016-01-08    1.0
2016-01-11    1.0
2016-01-12    1.0
2016-01-13    1.0
2016-01-14    0.0
2016-01-15   -1.0
2016-01-18    1.0
2016-01-19    1.0
2016-01-20    0.0
2016-01-21    0.0
2016-01-22   -1.0
2016-01-25   -1.0
2016-01-26   -1.0
2016-01-27   -1.0
2016-01-28   -1.0
Name: close, dtype: float64


シグナルの作成③

最後の仕上げである。float型をint型に変換する。

int型に変換する必要性はない。だが、float型はデータにゴミがあって、「==」などがうまく機能しないことがある。int型にしておいたほうが無難だと思う。

In [14]:
signal = signal.astype(int)
print(signal.head(20))


2016-01-01    0
2016-01-04    1
2016-01-05    1
2016-01-06    1
2016-01-07    1
2016-01-08    1
2016-01-11    1
2016-01-12    1
2016-01-13    1
2016-01-14    0
2016-01-15   -1
2016-01-18    1
2016-01-19    1
2016-01-20    0
2016-01-21    0
2016-01-22   -1
2016-01-25   -1
2016-01-26   -1
2016-01-27   -1
2016-01-28   -1

Name: close, dtype: int64


最後に

お疲れ様でした。

この小技はRでも使える。というか、私はRでこの小技を使っていて、それをPythonでも使っているだけである。

単純ではあるが、シグナル生成だけではなく、けっこういろいろな場面で使えるので、知らなかった人は是非使ってみて下さい。

サンプルプログラム

上のプログラムを1つにまとめ、printなどで余分なものは除いた。「なんだ、こんなに短かったのか」と思われるかもしれない(笑)。

In [15]:
import forex_system as fs
import numpy as np
from datetime import datetime

start = datetime.strptime('2016.01.01 00:00', '%Y.%m.%d %H:%M')
end = datetime.strptime('2016.12.31 23:59', '%Y.%m.%d %H:%M')
zscore1 = fs.i_zscore('USDJPY', 1440, 5, 'MODE_SMA', 1)[start:end]

buy_entry = zscore1 <= -1.0
buy_exit = zscore1 >= 0.0
sell_entry = zscore1 >= 1.0
sell_exit = zscore1 <= 0.0
buy = buy_entry.copy()
buy[buy==False] = np.nan
buy[buy_exit==True] = 0.0
buy = buy.fillna(method='ffill')
sell = sell_entry.copy()
sell[sell==False] = np.nan
sell[sell_exit==True] = 0.0
sell = sell.fillna(method='ffill')
signal = buy - sell
signal = signal.fillna(0.0)
signal = signal.astype(int)
print(signal.head(20))


2016-01-01    0
2016-01-04    1
2016-01-05    1
2016-01-06    1
2016-01-07    1
2016-01-08    1
2016-01-11    1
2016-01-12    1
2016-01-13    1
2016-01-14    0
2016-01-15   -1
2016-01-18    1
2016-01-19    1
2016-01-20    0
2016-01-21    0
2016-01-22   -1
2016-01-25   -1
2016-01-26   -1
2016-01-27   -1
2016-01-28   -1
Name: close, dtype: int64


(2017/03/05更新)

確率のイメージ

確率をイメージしたアニメーション画像を作ってみた。

初めはランダムに見えるが…

最初は散布図に点がランダムにプロットされているだけのように見える。だが、しばらくすると、FX業界では割と有名な画像になる。

試行を繰り返して初めて見えてくる

物理学では二重スリット実験という有名な実験がある。電子を飛ばして写真乾板に当てる。だが、途中を2本のスリットのある板で塞ぐ。普通はスリットの後のところだけにしか電子は当たらないと思うだろう。

ところが、板で邪魔されているところにも電子は当たるのである。初めは電子がランダムに当たっているように見える。だが、電子を飛ばし続けると、電子の到達確率に応じた濃淡が生まれるのである。

今回のアニメーション画像はこの二重スリット実験を参考に作ってみた。色はR(赤)、G(緑)、B(青)をそれぞれ0-255の数値にして表すことができる。今回は元画像からR、G、Bがすべて128未満のピクセルに目がけて点を打ち込んだ。

物事の本質というのはわずかな試行や事例でそう簡単に見い出せるものではない。試行を繰り返し、多くの事例を集めて初めて見えてくるものなのである。

サンプルプログラム

①適当なpngファイルを用意し、「image_of_probability.png」ファイルとして「~/py」フォルダーに保存する。

②以下のプログラムをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

def func(i, x, y, frames):
    size = len(x)
    rng = int(size / frames)
    start = i * rng
    end = start + rng - 1
    plt.scatter(x[start:end], y[start:end], c='dimgray', marker='.')
    plt.title('Image of Probability')
    plt.xlim(0, 1)
    plt.ylim(0, 1)

plt.style.use('dark_background')
fig = plt.figure()
max_size = 100000
frames = 60
interval = 1000

img = Image.open("image_of_probability.png")
width = img.width
height = img.height
adj = np.sqrt(width * height / max_size)
width = int(width / adj)
height = int(height / adj)
img = img.resize((width, height))
size = width * height
x = np.zeros(size)
y = np.zeros(size)
for i in range(height):
    for j in range(width):
        rgb = img.getpixel((j, i))
        if rgb[0] < 128 and rgb[1] < 128 and rgb[2] < 128:
            x[width*i+j] = j / width
            y[width*i+j] = 1 - (i / height)

xy = np.c_[x, y]
np.random.shuffle(xy)
x = xy[:, 0]
y = xy[:, 1]

ani = animation.FuncAnimation(fig, func, fargs=(x, y, frames), frames=frames,
                              interval=interval)
plt.show()
ani.save('image_of_probability.gif', writer='imagemagick')

少し待つ(私の環境では15秒くらい)。設定によっては何も描画されていないウィンドウが表示されるが気にしない。終了すると「~/py」フォルダーに「image_of_probability.gif」ファイルが生成する。

(2017/02/19更新)

通貨の質量

独立した記事にするほどの分量でもないが、とりあえずメモとしてアップしておく。

質量とは動きにくさ

通貨の質量と言っても、500円硬貨は何グラムかという話ではない。物理学では質量とは動きにくさを意味する。ここでは通貨の動きにくさと言う意味で使うこととする。

動きにくさとは動きやすさの逆数

動きにくさとは見方を変えれば動きやすさの逆である。したがって、動きにくさは動きやすさの逆数として数値化することもできるだろう。

動きやすさとはボラティリティ

動きやすさとはつまりボラティリティである。ボラティリティが大きいということは小さな力で動かすことができ、ボラティリティが小さいということは大きな力でなければ動かすことができないということである。力とは例えば資金などであろうか。

つまり、質量とはボラティリティの逆数

以上をまとめると、質量とはボラティリティの逆数だということになる。物理学の運動方程式を用いれば以下のようになるだろう。

\[ \vec{F} = m\vec{a} \] \[ m = \frac {1} {v} \] \[ \vec{F} = \frac {\vec{a}} {v} \]

物理学で「v」としたら速度を意味することが多いので紛らわしいが、ここではボラティリティの意味で使う。ある方向に加速度が生じたら、つまり価格が上昇、または下落したら、その変動幅をボラティリティで除した量が力として働いたと考えることができる。

(2017/02/08更新)