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

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

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

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

Zスコアとリターン

1本前の足のZスコアが現在の足のリターンにどのような影響を与えるかを調べてみる。

足は5分足を使っている。現在の足のリターンは計算期間1本(5分)のROCとして計算する。ただし、標準化してある。また、1本前の足のZスコアの計算期間は12本(60分)で固定である。

直前のZスコアと現在のリターンは負の比例

In [1]:
import forex_system as fs
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime

fs.remove_temp_folder()

timeframe = 5
minute = 60
size = 6

x = np.empty(size)
y = np.empty(size)
plt.figure(figsize=(6, 44))
cnt = 0
for symbol in ['AUDJPY', 'AUDUSD', 'EURAUD', 'EURGBP', 'EURJPY', 'EURUSD',
               'GBPAUD', 'GBPJPY', 'GBPUSD', 'USDJPY', 'RANDOM']:
    cnt += 1
    plt.subplot(11, 1, cnt)
    for year in [2012, 2013, 2014, 2015, 2016]:
        start = datetime.strptime(str(year) + '.01.01 00:00',
                                  '%Y.%m.%d %H:%M')
        end = datetime.strptime(str(year) + '.12.31 23:59',
                                '%Y.%m.%d %H:%M')
        roc0 = fs.i_roc(symbol, timeframe, 1, 0)[start:end]
        for i in range(size):
            x[i] = i - 2.5
            period = fs.convert_minute2period(minute, timeframe)
            zscore1  = fs.i_zscore(symbol, timeframe, period, 'MODE_SMA',
                                   1)[start:end]
            y[i] = np.mean(roc0[(zscore1>=x[i]-0.5) & (zscore1<x[i]+0.5)])
        mean = np.mean(y)
        std = np.std(y)
        y = (y - mean) / std
        plt.plot(x, y, label=str(year))
    plt.title('Z-Score and Return (' + symbol + ')')
    plt.xlabel('Z-Score')
    plt.ylabel('Standardized Return')
    plt.xlim(-2.5, 2.5)
    plt.ylim(-2.5, 2.5)
    plt.legend(loc='upper right')
    plt.axhline(y=0.0, color='black', linestyle=':')
    plt.tight_layout()
plt.savefig('zscore_and_return.png', dpi=150)
plt.show()

fs.remove_temp_folder()


1本前の足のZスコアと現在の足のリターンは負の比例という関係にあるように見える。

(2017/03/13更新)

確率のイメージ

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

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

最初は散布図に点がランダムにプロットされているだけのように見える。だが、しばらくすると、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更新)

ラウンドナンバーとボラティリティ

価格がラウンドナンバーを上抜き、または下抜きしたとき、ボラティリティがどうなるかを検証してみる。

100pipsの単位が更新されたとき、ラウンドナンバーを上抜き、または下抜きしたと見なすこととする。例えばUSDJPYなら100円未満から100円以上になったときに上抜きと見なすといった具合である。

ボラティリティを計る指標としてはTrue Rangeを用いた。先ず10pipsの単位で10pipsごとにラインを10本設け、それを上抜き、または下抜きした場合のTrue Rangeを求める。次にその平均を取ってベンチマークとする。そして、それぞれのTrue Rangeを平均で除した。つまりベンチマーク=1.0である。

10pipsの単位が0のライン、すなわちラウンドナンバーが他のラインと差があるかどうかを見てみる。使用した足の種類は5分足である。

ラウンドナンバーでボラティリティはやや拡大

検証結果によると、価格がラウンドナンバーを上抜き、または下抜きしたとき、ボラティリティはやや拡大する傾向があるように見える(Decimalが0.0のところ)。

ボラティリティはやや拡大しているとはいうものの、それほど明瞭ではない。ラウンドナンバーはしばらく更新されていないといった条件がないと、大した意味を持たないのかもしれない。USDJPYはいくらか明瞭な感じがする。

サンプルプログラム

○以下のコマンドを「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

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

def get_one(data):
    temp = data.copy()
    temp = np.floor(temp)
    ret = temp % 10
    ret = ret.astype(int)
    return ret

def get_hundredth(data):
    temp = data.copy()
    temp *= 100
    temp = np.floor(temp)
    ret = temp % 10
    ret = ret.astype(int)
    return ret

fs.remove_temp_folder()

timeframe = 5
n = 10
x = np.empty(n)
y = np.empty(n)
plt.figure(figsize=(6, 20))
cnt = 0
for symbol in ['AUDUSD', 'EURUSD', 'GBPUSD', 'USDJPY', 'RANDOM']:
    cnt += 1
    plt.subplot(5, 1, cnt)
    for year in [2012, 2013, 2014, 2015, 2016]:
        start = datetime.strptime(str(year) + '.01.01 00:00',
                                  '%Y.%m.%d %H:%M')
        end = datetime.strptime(str(year) + '.12.31 23:59',
                                '%Y.%m.%d %H:%M')
        ret = fs.i_trange(symbol, timeframe, 0)[start:end]
        high0 = fs.i_high(symbol, timeframe, 0)[start:end]
        high1 = fs.i_high(symbol, timeframe, 1)[start:end]
        low0 = fs.i_low(symbol, timeframe, 0)[start:end]
        low1 = fs.i_low(symbol, timeframe, 1)[start:end]
        for i in range(n):
            if symbol == 'USDJPY' or symbol == 'RANDOM':
                n_h0 = get_one(high0+0.1*i)
                n_h1 = get_one(high1+0.1*i)
                n_l0 = get_one(low0+0.1*i)
                n_l1 = get_one(low1+0.1*i)
            else:
                n_h0 = get_hundredth(high0+0.001*i)
                n_h1 = get_hundredth(high1+0.001*i)
                n_l0 = get_hundredth(low0+0.001*i)
                n_l1 = get_hundredth(low1+0.001*i)
            x[i] = 0.1 * i
            y[i] = np.mean(ret[(n_h0!=n_h1) | (n_l0!=n_l1)])
        benchmark = np.mean(y)
        y /= benchmark
        plt.plot(x, y, label=str(year))
    plt.title('Round Number and Volatility (' + symbol + ')')
    plt.xlabel('Decimal (100pips=1.0)')
    plt.ylabel('True Range (Benchmark=1.0)')
    plt.xlim(0.0, 0.9)
    plt.ylim(0.8, 1.2)
    plt.legend()
    plt.axhline(y=1.0, color='black', linestyle=':')
    plt.tight_layout()
plt.savefig('round_number_and_volatility.png', dpi=150)
plt.show()

fs.remove_temp_folder()
(2017/02/17更新)

電子書籍で数式を使うときの注意

多くのEPUBリーダーはMathJax未対応

よく使われるEPUBリーダーの多くはMathJaxに対応していないようである。私は普段、Google Play ブックスを使っているが、MathJaxに対応していない。iBooksも対応していない。ブログでは使えるし、電子書籍でも使えると思い込んでいたので意外だった。

ネットで調べてみると、対応しているEPUBリーダーは少なく、現状では電子書籍にMathJaxを使うのはあまりよくないようだ。もし電子書籍で数式を使いたいのであれば、画像に変換するべきものらしい。

MathJax対応のEPUBリーダーもあるが…

MathJaxに対応しているとされるEPUBリーダーを調べて、いくつか試してみた。ここではiPhoneのアプリに限って話す。私はiPhoneで電子書籍を読んでいるからである。私が実際に試してみて一応使えそうだと思ったのは「Adobe Digital Editions」と「Gitden Reader」の2つである。

Adobe Digital Editionsを使って気付いた点は

  • iPhoneを縦にして読むときの字が小さい。いくらか大きくすることもできるが、それでも小さい。
  • ページをめくると、字がぼやけ、元に戻るのに少し間がある。
  • 数式があると、少し縦に間延びし、後のページにずれていく。このため、目次で見たいページをクリックしても、ずれて表示される。

である。確かに数式はちゃんと表示されているが、使い勝手がイマイチな感じだ。

一方、Gitden Readerを使って気付いた点は

  • 動きがややもっさりしている。
  • 数式があると、少し縦に間延びし、後のページにずれていく。そして本の最後が尻切れになる。ただし、「スクロール」のモードにすれば最後まで表示される。だが、Adobe Digital Editionsと同様で、目次で見たいページをクリックしても、ずれて表示される。
  • 読み上げ機能がある。

である。使い勝手はそれほど悪くないが、やはり数式があると後のページにずれていくというのが大きなマイナス点ではある。読み上げ機能があるのはなかなかいい。とりあえずはGitden Readerを使ってみようと思う。

MathJaxで記述するときの注意

電子書籍向けにMathJaxで記述するとき、1つだけ気付いたことがある。数式で累乗を表す「^」が消えてしまうのである。

数式で累乗を表すときは例えば

y = x^2

とするのが普通だ。ブログではこれで問題ない。だが、私が電子書籍を作るときに使っているでんでんコンバーターの癖なのか、epubに変換するとなぜか

y = x2

に書き換えられてしまうのである。だが、これを防ぐのは簡単だ。

y = x ^ 2

のように離して記述すればいい。ブログでもこの書き方で問題ない。現時点で気付いている不具合はこれだけである。

(2017/02/16更新)