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

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

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

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更新)