ランダムウォークのトレンド継続期間 (2017/07/06)

ランダムウォークのデータにおいてトレンドが継続する期間がどのようであるかを調べてみる。

本来ならランダムウォークにトレンドは存在しない。だが、ランダムに動いてもトレンドに見えるようなことはよくある。ランダムウォークでもトレンドがどのくらい継続するものなのかということを知っておけば、実際のトレードにおいてトレンドがどのくらい継続するか想定する参考になるだろう。

ここではトレンドを安値が移動平均線の上、または高値が移動平均線の下にある状態とする。安値が移動平均線の上にあれば上昇トレンド、高値が移動平均線の下にあれば下降トレンドとなる。

トレンドには広く共有されている定義はないようである。そこでとりあえず上述のように定義する。当たらずといえども遠からずではないかと思う。

それでは実際にランダムウォークのデータを作成し、トレンドが継続する期間を調べてみる。

①この記事で使用するライブラリをインポートする。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit

②ランダムウォークのデータを作成する。

size = 10000000
index = pd.date_range('1/1/2000', periods=size, freq='S')
rnd = np.random.normal(0.0, 1.0/np.sqrt(1440*60), size)
rnd = pd.Series(rnd, index=index)
data = rnd.cumsum()
data = np.exp(data)
data = data.resample('T').ohlc()

ランダムウォークのデータは対数と考えられる。だが、為替などのデータは対数ではない。そこで比較しやすいよう、ランダムウォークのデータを変換しておく。

日付は適当である。こうしておくと四本値に変換するときに楽だというだけである。

データは1日の変動が1.0程度になるような乱数で作成した。ドル円で1日の値幅が1円くらいというイメージである。

③計算期間ごとにトレンド継続期間の標準偏差を求める。

trend_duration = np.empty(50)
period = np.array(range(5, 255, 5))
for i in range(50):
    high = data['high']
    low = data['low']
    ma = data['close'].rolling(window=period[i]).mean()
    above = low.iloc[period[i]-1:] > ma.iloc[period[i]-1:]
    above = above * (
            above.groupby((above!=above.shift()).cumsum()).cumcount()+1)
    below = high.iloc[period[i]-1:] < ma.iloc[period[i]-1:]
    below = below * (
            below.groupby((below!=below.shift()).cumsum()).cumcount()+1)
    trend_duration[i] = (above - below).std()

④グラフを表示する。

plt.plot(period, trend_duration)
plt.xlabel('Period')
plt.ylabel('Trend Duration')
plt.xlim(5, 250)
plt.savefig('trend_duration.png', dpi=150)
plt.show()

グラフからトレンド継続期間は計算期間に比例しているということが予想される。

⑤トレンド継続期間と計算期間の関係を示すモデルを作成する。

def model(period, slope, intercept):
    return slope * period + intercept

モデルは以下のようにしてみた。

トレンド継続期間 = 傾き * 計算期間 + 切片

⑥モデルの傾き、切片を求める。

popt, pcov = curve_fit(model, period, trend_duration)
slope = popt[0]
intercept = popt[1]
print('Trend Duration = ', slope, ' * Period + ', intercept)

Trend Duration =  0.594061280471  * Period +  -2.23805773853

トレンドの継続期間は計算期間の55-60%程度の長さと考えられる。乱数を使っているので、結果は毎回いくらか違う。

ランダムウォークデータ同士のペアとボラティリティ

各データとペアのボラティリティの関係

2つのランダムウォークデータのボラティリティとペアのボラティリティの関係を調べてみる。第1のランダムウォークデータをベースとし、平均は0、標準偏差は変動させる。第2のランダムウォークデータをクウォートとし、平均は0、標準偏差は1.0で固定とする。ベースの標準偏差(ボラティリティ)を変動させることによってペアのボラティリティがどうなるかを見る。

データは累積和で作成しているので対数として扱う。したがって、ペアは「ベース / クウォート」ではなくて、「ベース - クウォート」となる。

試しにシミュレーションしてみたところ、どうやら以下のような関係にあるように思われた。

ペアのボラティリティ = sqrt(ベースのボラティリティ^2 + クウォートのボラティリティ^2)

さて、この想定に基づき、実測値と予測値のグラフを作成した。

実測値の上にほぼ完全に予測値が重なってしまい、よく分からなくなってしまった。それくらい一致しているわけで、上の想定で合っていると考えていいだろう。つまり、ランダムウォークデータ同士のペアのボラティリティは(ベースのボラティリティの2乗 + クウォートのボラティリティの2乗)の平方根である。

サンプルプログラム

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

import matplotlib.pyplot as plt
import numpy as np

def create_randomwalk_data(mean, std, size):
    temp = np.random.normal(loc=mean, scale=std, size=size)
    data = np.cumsum(temp)
    return data

n = 1000000

base_volatility = np.empty(10)
base_quote_volatility = np.empty(10)
base_quote_volatility_pred = np.empty(10)
quote = create_randomwalk_data(0.0, 1.0, n)
for i in range(10):
    base_volatility[i] = 1 + i
    base = create_randomwalk_data(0.0, base_volatility[i], n)
    base_quote = base - quote
    diff = base_quote[1:] - base_quote[:-1]
    base_quote_volatility[i] = np.std(diff)
    base_quote_volatility_pred[i] = np.sqrt(base_volatility[i]**2 + 1**2)

plt.plot(base_volatility, base_quote_volatility, label='base_quote_volatility')
plt.plot(base_volatility, base_quote_volatility_pred,
         label='base_quote_volatility_pred')
plt.title('Base/Quote Volatility')
plt.xlabel('Base Volatility (Quote Volatility = 1.0)')
plt.ylabel('Base/Quote Volatility')
plt.xlim(1, 10)
plt.legend()
plt.tight_layout()
plt.savefig('base_quote_volatility.png', dpi=150)
plt.show()
(2017/02/07更新)

ランダムウォークの歪度と計算期間の関係

ランダムウォークの歪度は0のはずだが…

ランダムウォークの歪度と計算期間の関係を調べてみる。ところで、ランダムウォークは正規分布に従っており、歪度は0のはずである。しかし、乱数を使っていることにより、歪度がちょうど0になることはほとんどない。

歪度は計算期間の平方根に反比例

平均は計算期間に比例し、標準偏差は計算期間の平方根に比例することが知られている。私は経験的に歪度は計算期間の平方根に反比例しているように予想していた。そこで

歪度の予測値 = 基準となる計算期間の歪度 / sqrt(計算期間 / 基準となる計算期間)

として歪度の予測値を求め、実際の歪度と比較してみた。

手順としては先ず平均=0、標準偏差=0.0001、歪度=100に従う乱数で5分足データを1年分作成した。歪度が0だと小さすぎて比較しにくいかもしれないと考えた。そこで、あえて100という極端に大きい数値を指定した。

次に計算期間ごとの歪度とその予測値を求めてグラフにした。

実測値と予測値はほぼ一致しているように思われる。本来なら数学的に証明すべきだろうが、とりあえずランダムウォークの歪度は計算期間の平方根に反比例すると考えてよさそうだ。

サンプルプログラム

①以下のコマンドを端末にコピー&ペーストして「Enter」キーを押す。

%run -t ~/py/make_randomwalk_data.py --mean 0.0 --std 0.0001 --skew 100.0

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

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

fs.remove_temp_folder()

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')
period = np.array(range(5, 55, 5))
skew = np.empty(10)

for i in range(10):
    ret = fs.i_log_return('RANDOM', 5, period[i], 0)[start:end]
    skew[i] = stats.skew(ret)

pred = skew[0] / np.sqrt(period / period[0])

plt.plot(period, skew, label='skew')
plt.plot(period, pred, label='pred')
plt.title('Skew and Period')
plt.xlabel('Period')
plt.ylabel('Skew')
plt.legend()
plt.savefig('skew_and_period.png', format='png', dpi=150)
plt.show()

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