イールドカーブをプロットする

TradingViewでは、国債の金利を表示できるので、security() を使って金利データを参照し、独自にイールドカーブをプロットするインディケーターを作成することができる。

例えば、米国債利回りのシンボル名は下記の通り。シンボル名の US の部分を JP に変えれば日本国債、DE に変えればドイツ国債の利回りになる。

シンボル名説明
US01Y米国債1年 利回り
US02Y米国債2年 利回り
US03Y米国債3年 利回り
US05Y米国債5年 利回り
US07Y米国債7年 利回り
US10Y米国債10年 利回り
US20Y米国債20年 利回り
US30Y米国債30年 利回り

イールドカーブをプロットするPineスクリプトのコードが下記。


//@version=4
study("Yield Curve")

country = input("US", type=input.string)

round_float(v) =>
    round(v * 1000) / 1000

draw(x1, y1, x2, y2, n) =>
    l = line.new(x1, y1, x2, y2, color=color.black, style=line.style_arrow_right)
    txt = tostring(n) + ": " + tostring(y2)
    lb = label.new(x2, y2, txt, textcolor=y2 >= 0 ? color.black : color.red, style=label.style_none)
    [l, lb]

yld01_ = security(country + "01Y", timeframe.period, close)
yld02_ = security(country + "02Y", timeframe.period, close)
yld03_ = security(country + "03Y", timeframe.period, close)
yld05_ = security(country + "05Y", timeframe.period, close)
yld07_ = security(country + "07Y", timeframe.period, close)
yld10_ = security(country + "10Y", timeframe.period, close)
yld20_ = security(country + "20Y", timeframe.period, close)
yld30_ = security(country + "30Y", timeframe.period, close)

yld01 = round_float(yld01_)
yld02 = round_float(yld02_)
yld03 = round_float(yld03_)
yld05 = round_float(yld05_)
yld07 = round_float(yld07_)
yld10 = round_float(yld10_)
yld20 = round_float(yld20_)
yld30 = round_float(yld30_)

x_offset = 10

x1 = bar_index[80]
x2 = x1 + x_offset
lb0 = label.new(x1, yld01, "1: " + tostring(yld01), textcolor=yld01 >= 0 ? color.black : color.red, style=label.style_none)
[l1, lb1] = draw(x1, yld01, x2, yld02, 2)

x1 := x2
x2 := x1 + x_offset
h_line = line.new(x1, yld02, x1 + 60, yld02, color=yld10 > yld02 ? color.green : color.red)
[l2, lb2] = draw(x1, yld02, x2, yld03, 3)

x1 := x2
x2 := x1 + x_offset
[l3, lb3] = draw(x1, yld03, x2, yld05, 5)

x1 := x2
x2 := x1 + x_offset
[l4, lb4] = draw(x1, yld05, x2, yld07, 7)

x1 := x2
x2 := x1 + x_offset
[l5, lb5] = draw(x1, yld07, x2, yld10, 10)

x1 := x2
x2 := x1 + x_offset
[l6, lb6] = draw(x1, yld10, x2, yld20, 20)

x1 := x2
x2 := x1 + x_offset
[l7, lb7] = draw(x1, yld20, x2, yld30, 30)

line.delete(l1[1])
line.delete(l2[1])
line.delete(l3[1])
line.delete(l4[1])
line.delete(l5[1])
line.delete(l6[1])
line.delete(l7[1])
line.delete(h_line[1])

label.delete(lb0[1])
label.delete(lb1[1])
label.delete(lb2[1])
label.delete(lb3[1])
label.delete(lb4[1])
label.delete(lb5[1])
label.delete(lb6[1])
label.delete(lb7[1])

hline(0)

独自に追加した部分として、一般的に景気悪化の予兆として捉えられる長短金利の逆転現象を示す指標を追加している。これは、2年債利回りの水準に水平線を引き、2年債と10年債の利回りが逆転すると、水平線が赤色になる。逆転がない平常時は緑色となる。

イールドカーブは下記のように描画される。これは、インディケーターをチャートに3つ追加し、上から日本、米国、ドイツ国債を指定して表示したもの。

ドルインデックスを自作する

ドルインデックスとは、ドルに対し重み付けした6つの通貨(ユーロ、日本円、英ポンド、カナダドル、スウェーデンクローナ、スイスフラン)の幾何平均を計算したもの。TradingViewでは、シンボルに DXY を指定すれば表示できる。

通貨重み
ユーロ0.576
日本円0.136
英ポンド0.119
カナダドル0.091
スウェーデンクローナ0.042
スイスフラン0.036

元は1973年に米国の中央銀行によって開発されたものだが、1985年にドルインデックスの先物取引が開始されてから、 ICE Futures U.S.(先物取引所)によって算出・公表されている。算出は各通貨のスポット価格の配信レートを元にリアルタイムに約15秒毎に行われる。

計算式は次の通り。

index = 50.14348112 × EURUSD-0.576 × USDJPY0.136 × GBPUSD-0.119 × USDCAD0.091 × USDSEK0.042 × USDCHF0.036

この計算式をPineスクリプトで実装したものが下記。


//@version=4
study("US Dollar Index")

symbol_prefix = input("FX")

vs_eur = pow(security(symbol_prefix + ":EURUSD", timeframe.period, close), -0.576)
vs_jpy = pow(security(symbol_prefix + ":USDJPY", timeframe.period, close), 0.136)
vs_gbp = pow(security(symbol_prefix + ":GBPUSD", timeframe.period, close), -0.119)
vs_cad = pow(security(symbol_prefix + ":USDCAD", timeframe.period, close), 0.091)
vs_sek = pow(security(symbol_prefix + ":USDSEK", timeframe.period, close), 0.042)
vs_chf = pow(security(symbol_prefix + ":USDCHF", timeframe.period, close), 0.036)

index = 50.14348112 * vs_eur * vs_jpy * vs_gbp * vs_cad * vs_sek * vs_chf

plot(index)
plot(security("DXY", timeframe.period, close), color=color.gray)

symbol_prefix (FX:EURUSDFX の部分) のデフォルト値は FX に設定してあり、FXCMの配信レートを使ってインデックスを計算する。例えば、これを OANDA に変更すれば、OANDAの配信レートを使って計算するように変更できる。

参考:U.S. Dollar Index® Contracts - FAQ June 2015 (PDF)

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

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スクリプトプログラミングで多用する重要なテクニックの一つである。