疑似乱数生成器を自作する

Pineスクリプトにはビルトイン関数 rand() が用意されてないので、自分で疑似乱数生成器を実装してみる。実装するのは、C++11の minstd_rand0 とその改良版の minstd_rand

minstd_rand0 は、1988年にStephen K. ParkとKeith Millerが発表[1]した乱数生成器の実装を元にしたもの。論文の中で下記のPascalコードが示されている。

function Random : real;
const
  a = 16807;
  m = 2147483647;
begin
  seed := (a * seed) mod m;
  Random := seed / m
end;

変数 seed1 から 2147483646 までの整数の中から一つを割り当てて初期化する。

これをPineスクリプトで実装すると下記のようになる。seedtimenow の値で初期化している。


//@version=4
study("minstd_rand0")

var seed = timenow

a = 16807
m = 2147483647
seed := (a * seed) % m
//random = seed / float(m)

plot(seed)

変数 seed を参照すれば、C++11の minstd_rand0 のように整数の乱数を得ることができる。float の乱数が欲しい場合は、コメントアウトした random の値を参照すれば良い。

実装が正しいかどうかは、変数 seed1 で初期化し10000番目に生成された seed の値が 1043618065 であることを確認すれば良い。

下記のコードを使って、乱数生成器の実装の正しさを確認した。


//@version=4
study("minstd_rand0 test")

var seed = 1

if barstate.isfirst
    for i = 1 to 10000
        a = 16807
        m = 2147483647
        seed := (a * seed) % m
        //random = seed / float(m)

plot(seed, color=seed == 1043618065 ? color.green : color.red)

次に改良版の minstd_rand を実装する。これは1993年にStephen K. Park、Keith W. Miller、Paul K. Stockmeyerが発表[2]した改良版で、a = 16807 の代わりに a = 48271 を使用することを推奨したものである。

よって、Pineスクリプトの実装は単に a の値を 48271 に変更するだけで済む。


//@version=4
study("minstd_rand")

var seed = timenow

a = 4827
m = 2147483647
seed := (a * seed) % m
//random = seed / float(m)

plot(seed)
References
  1. S. K. Park and K. W. Miller. 1988. Random number generators: good ones are hard to find. Commun. ACM 31, 10 (Oct. 1988), 1192–1201. DOI:https://doi.org/10.1145/63039.63042
  2. Diane Crawford. 1993. Technical correspondence. Commun. ACM 36, 7 (July 1993), 105. DOI:https://doi.org/10.1145/159544.376068

Pineスクリプトの落とし穴:平均足チャートを表示しているときの OHLC の値は実際の値と違う

平均足チャートを表示しているとき、組み込み(ビルトイン)変数 openhighlowclose の値は、実際の銘柄の値と違うことに注意。

違う値となる理由は、組み込み(ビルトイン)変数 openhighlowclose には、チャートに表示しているバー(ロウソク足)に対応する値が入るため。よって、平均足チャートのときは平均足の値が入るため、実際の銘柄の値とは異なる。

これを理解していないと、平均足チャート上に表示しているインディケーター(テクニカル指標)は、実は期待と違うものを表示している可能性がある。

平均足チャートを表示しているときに実際の銘柄の値を取得するには、下記のように security() を使う必要がある。


//@version=4
study("get OHLC with security()")

symbl = tickerid(syminfo.prefix, syminfo.ticker)

o = security(symbl, timeframe.period, open)
h = security(symbl, timeframe.period, high)
l = security(symbl, timeframe.period, low)
c = security(symbl, timeframe.period, close)

//plot(open)
//plot(o, linewidth=2)
//plot(high)
//plot(h, linewidth=2)
//plot(low)
//plot(l, linewidth=2)
plot(close)
plot(c, linewidth=2)

海外の取引所の時間を日本時間に変換する

Pineスクリプトで参照できるビルトイン変数 hour は、取引所のタイムゾーンの時間を保持している。これは、あくまで「取引所のタイムゾーン」であって、TradingViewのチャートで設定してるタイムゾーンの時間ではないことに注意。

TSEやOSEなど日本の取引所の銘柄については、取引所のタイムゾーンが東京のため、変数 hour は日本時間(UTC+9)を保持しているので問題ない。
※協定世界時(UTC)と日本時間の時差は+9時間(UTC+9)

海外の取引所(CME、FXCM、OANDAなど)の銘柄に対しては、日本とタイムゾーンが異なり、変数 hour はチャート上で表示される日本時間と一致しない。

例えば、TradingViewのタイムゾーン設定が "(UTC+9) Tokyo" となっていることを前提にすると、CMEの銘柄をチャートに表示しているとき、Pineスクリプトで hour == 9 の条件で何かプロットしようとしても、チャート上では23時にプロットされる。また、FXCM、OANDAの銘柄の場合は、hour == 9 の条件でプロットしても、チャート上では22時にプロットされる。

このようにそれぞれの取引所のタイムゾーンによって、変数 hour の値とチャート上の時間での表示にずれが生じる。

このような海外の取引所との時差の問題をPineスクリプト上で上手く扱うには、変数 time を利用する。

変数 time は、UTC±0 の時間をミリ秒の値で保持しているので、この値を日本時間(UTC+9)に変換すれば、取引所に関わらずチャート上の時間と一致した表示を行うことができる。


//@version=4
study("local hour", overlay=true, scale=scale.none)

// 1h のチャート表示用

offset = 9 // 協定世界時(UTC)と日本の時差は+9時間
localhour = (time / (1000 * 60 * 60) + offset) % 24 // UTC+offsetの時間に変換

plot(hour == 9 ? 10e20 : na, style=plot.style_histogram, color=color.black)
plot(localhour == 9 ? 10e20 : na, style=plot.style_histogram, color=color.red, linewidth=2)

Pineスクリプトの落とし穴:比較演算子の結果は true、false、na の3つの可能性がある

Pineスクリプトでは、比較演算子の結果は、truefalsena の3つの可能性がある。

na の値を持つ変数に対して比較演算子を用いた場合、比較演算子の結果は常に na となる。つまり、変数 a = na のとき、a == truea == falsea != truea != falsea == naa != na は全て na と評価される。

このように、比較演算子の結果は truefalse だけだと思い込んでいると、Pineスクリプトでは、予想に反した結果を得ることになるので注意が必要。

また、変数 a = na のとき、a == naa != na 両方とも結果が na となるので、変数の値が na かどうか確認したいときはビルトイン関数 na() を使う必要がある。関数 na() は、与えられた変数の値が na なら true、そうでなければ false を返す。

na との評価結果の確認には下記のコードを使用した。


//@version=4
study("comparison with na")

eval(c) => c ? 1 : (na(c) ? na : 0)

NA = bool(na)

b1 = bool(true)
c1 = NA == b1  // na == true
plot(eval(c1)) // na

b2 = bool(false)
c2 = NA == b2  // na == false
plot(eval(c2)) // na

b3 = bool(true)
c3 = NA != b3  // na != true
plot(eval(c3)) // na

b4 = bool(false)
c4 = NA != b4  // na != false
plot(eval(c4)) // na

b5 = bool(na)
c5 = NA == b5  // na == na
plot(eval(c5)) // na

b6 = bool(na)
c6 = NA == b6  // na != na
plot(eval(c6)) // na

O(1)の単純移動平均 (SMA) インディケーターを自作する

TradingViewにはすでに単純移動平均(SMA)のインディケーターが用意されているが、それを自作してみる。あえて自作してみることで、計算の工夫の仕方に気付いたり、Pineスクリプトに特有の簡潔な書き方などノウハウを学ぶことができる。

Pineスクリプトで単純移動平均を計算するコードを素直に書くと次のようになる。


//@version=4
study("my sma", overlay=true)

length = input(9)

sum = close
for i = 1 to length - 1
    sum := sum + close[i]
ma = sum / length

plot(bar_index >= length - 1 ? ma : na)

sum を求める処理は、ループ長が length に比例する for ループを使うため、計算量はO(n)となる。だが、この計算量は計算を工夫することによって、O(1)へ減らすことができる。

length=9 とした場合、最初から順に sum の計算を書き出してみると、


sum0 = close0
sum1 = close1 + close0
sum2 = close2 + close1 + close0
...
sum7  =  close7 + close6 + close5 + close4 + close3 + close2 + close1 + close0
sum8  =  close8 + close7 + close6 + close5 + close4 + close3 + close2 + close1 + close0
sum9  =  close9 + close8 + close7 + close6 + close5 + close4 + close3 + close2 + close1
sum10 = close10 + close9 + close8 + close7 + close6 + close5 + close4 + close3 + close2
...

ここで、sum9 に注目してみると、


sum9
= close9 + close8 + close7 + close6 + close5 + close4 + close3 + close2 + close1
= close9 + sum8 - close0

のように変形することができる。

sum10 でも同様に変形できることが分かる。


sum10
= close10 + close9 + close8 + close7 + close6 + close5 + close4 + close3 + close2
= close10 + sum9 - close1

これを踏まえて、下記のようにコードを書き直すことができる。


//@version=4
study("my sma2", overlay=true)

length = input(9)

sum = 0.0

if bar_index < length
    sum := close
    for i = 1 to bar_index
        sum := sum + close[i]
else
    sum := sum[1] + close - close[length]

ma = sum / length

plot(bar_index >= length - 1 ? ma : na)

さらに、関数 nz() を使うことで、このコードは下記のようにシンプルにすることができる。


//@version=4
study("my sma3", overlay=true)

length = input(9)

sum = 0.0
sum := nz(sum[1]) + close - nz(close[length])

ma = sum / length

plot(bar_index >= length - 1 ? ma : na)

最初のコードは、lengthに比例する for ループを使う計算があり計算量はO(n)だったが、書き直したコードでは length に関わらず計算量は一定でO(1)となる。

Pineスクリプトでは nz() をうまく利用できると、かなりシンプルなコードになる。Pineスクリプトプログラミングで多用する重要なテクニックの一つである。