ラベル 落とし穴・ハマりどころ の投稿を表示しています。 すべての投稿を表示
ラベル 落とし穴・ハマりどころ の投稿を表示しています。 すべての投稿を表示

Pineスクリプトの落とし穴:小数の計算結果は常に正しいとは限らない

例として、下記のユーロドル・ショートの円換算・損益計算を考える。

floor( (1.11170 - 1.11050) * 50000 * 109.450 )
= floor( 0.0012 * 50000 * 109.450 )
= floor( 60 * 109.450 )
= floor( 6567 )
= 6567

※floor(): 小数点以下切り捨て
※決済時に1円未満は切り捨てとなる証券会社を想定

この計算をPineスクリプトで行ったものが下記。


//@version=4
study("float number calc 1", precision=16)

eurusd_entry = 1.11170
eurusd_exit =  1.11050
qty = 50000
usdjpy = 109.450

pl_jpy = floor( (eurusd_entry - eurusd_exit) * qty * usdjpy )

plot(pl_jpy) // 6566 誤り

手計算では何でもない計算なのだが、Pineスクリプトで行うと計算結果は 6567 とはならずに 6566 となる。
※正確には、これはPineスクリプトの問題ではなく、コンピューター上で行う計算において一般的に存在する問題。PythonやC/C++などのプログラミング言語でも正しい結果が得られない。

正しい結果を得るには round() を利用して、下記のように修正する必要がある。


//@version=4
study("float number calc 2", precision=16)

eurusd_entry = 1.11170
eurusd_exit =  1.11050
qty = 50000
usdjpy = 109.450

d = eurusd_entry - eurusd_exit
//d_round = round(d * 100000) / 100000.0 // 旧バージョン round() での四捨五入の方法
d_round = round(d, 5)

//pl_usd_round = round(d_round * qty * 10) / 10.0 // 旧バージョン round() での四捨五入の方法
pl_usd_round = round(d_round * qty, 1)

pl_jpy = floor(pl_usd_round * usdjpy)

plot(pl_jpy) // 6567 正しい

コンピューター上の計算は全て2進数に変換されて行われるが、全ての小数を2進数で完全に表現できるわけではなく、小数によっては数値誤差が生じる。修正版のコードでは、この数値誤差を round() を利用して適宜補正しているため、正しい結果が得られる。
※詳しく知りたい場合は、Googleで「循環小数 数値誤差」と検索すれば情報が得られる

ユーロドルではレートが小数点以下5桁で固定なので、小数点以下5桁に四捨五入する必要がある。Pineスクリプトで面倒なのが、Pineスクリプトで提供されている round() は整数に四捨五入するだけで、指定した小数点以下の桁数に四捨五入することができない。そのため、小数点以下5桁に四捨五入したいときは、下記のように多少面倒な方法で round() を使う必要がある。
※2021年4月9日から round() で四捨五入する桁数の指定が可能になった。2番目の引数に桁数を指定すると、指定した小数点以下の桁数に四捨五入してくれる。

d = eurusd_entry - eurusd_exit
//d_round = round(d * 100000) / 100000.0  // 旧バージョン round()
d_round = round(d, 5)  // 新バージョン round()

また、下記の米ドル換算での損益額の計算については、ユーロドルの最小通貨単位が1万通貨、レートが小数点以下5桁で固定、よって、損益額は小数点以下1桁で固定となるので、下記のように round() を使って小数点以下1桁に四捨五入している。

//pl_usd_round = round(d_round * qty * 10) / 10.0  // 旧バージョン
pl_usd_round = round(d_round * qty, 1)  // 新バージョン

Pineスクリプトの落とし穴:関数呼び出しは毎回行われる必要がある

あまり知られていないが、Pineスクリプトでは関数呼び出しは毎回行われる必要がある。if文などで条件に合致したときだけ関数呼び出しを行っているコードは期待と違う結果をチャート上に表示している可能性が非常に高い。

コードに問題がある場合、Pine Editorでコードを開いた状態で、Pine Editorにある "Add to Chart" ボタンをクリックすると、Pine Editorのコンソールに下記の警告メッセージが出る。

  • (if文ブロック内で関数呼び出しがある場合)
    The function 'anonym_function_0' should be called on each calculation for consistency. It is recommended to extract the call from this scope.
    ※日本語訳:一貫性を保つため、関数は毎回呼ばれるべきです。関数の呼び出しをこのスコープの外に出すことを勧めます。
  • (三項演算子内で関数呼び出しがある場合)
    The function 'anonym_function_0' should be called on each calculation for consistency. It is recommended to extract the call from this ternary operator.
    ※日本語訳:一貫性を保つため、関数は毎回呼ばれるべきです。関数の呼び出しを三項演算子の外に出すことを勧めます。

下記は典型的な問題のあるコードの例。一見、変数 x と y は、どちらも偶数番目のバー(変数 bar_index が偶数)のときに一つ前のバーの終値の値を保持するように見えるが、変数 x と y の値は異なる値を保持する。


//@version=4
study("function call 1", overlay=true)

func(src) => src[1]

x = bar_index % 2 == 0 ? func(close) : na
y = bar_index % 2 == 0 ? close[1] : na

plot(x, style=plot.style_cross, linewidth=3, color=color.red)
plot(y, style=plot.style_cross, linewidth=3)

期待通りに変数 x と y が同じ値を保持するためには、下記のように関数 func を毎回呼び出すように修正する必要がある。


//@version=4
study("function call 2", overlay=true)

func(src) => src[1]

v = func(close) // 関数funcは毎回呼び出される

//x = bar_index % 2 == 0 ? func(close) : na
x = bar_index % 2 == 0 ? v : na
y = bar_index % 2 == 0 ? close[1] : na

plot(x, style=plot.style_cross, linewidth=3, color=color.red)
plot(y, style=plot.style_cross, linewidth=3)

関数を毎回呼び出すか呼び出さないかによって結果に違いが生まれるのは、Pineスクリプト側にバグがあるわけではなく、元々このような挙動をするようにデザインされている。

bar_index が偶数のときだけ関数 func が実行されると、実際にどのようなことが起きるか順に説明すると次のようになる。

  • bar_index==0 の場合 src0 (== close0)
  • bar_index==2 の場合 src1 (== close2)
    • src1[1] == src0 == close0
    • 関数 "func(src) => src[1]" は、func(close2) => close0 となり、"src[1]" と1つ前を意図しているにも関わらず、実際は2つ前の close0 の値を返すことになる
  • bar_index==4 の場合 src2 (==close4)
    • src2[1] == src1 == close2
    • 関数 "func(src) => src[1]" は、func(close4) => close2 となり、"src[1]" と1つ前を意図しているにも関わらず、実際は2つ前の close2の値を返すことになる
  • bar_index==6 の場合 src3 (== close6)
    • src3[1] == src2 == close4
    • 関数 "func(src) => src[1]" は、func(close6) => close4 となり、"src[1]" と1つ前を意図しているにも関わらず、実際は2つ前の close4 の値を返すことになる
  • (以下同様)
    ...

つまり、関数 func で利用される変数 src は、series(一次元)データだが、コードの見かけ上期待する

src0(=close0), src1(=close1), src2(=close2), ...

とならず、bar_index が偶数のときのみ実行されることにより、

src0(=close0), src1(=close2), src2(=close4), ...

というように断片的な series データが構成されてしまうことになる。

Pineスクリプトではこのような挙動があるので、sma() などのビルトイン関数についても同様の配慮が必要となる。

※ただし、下記のビルトイン関数は例外で毎回呼び出す必要はない。

abs, acos, asin, atan, ceil, cos, dayofmonth, dayofweek, exp, floor, heikinashi, hour, kagi, linebreak, log, log10, max, min, minute, month, na, nz, pow, renko, round, second, sign, sin, sqrt, tan, tickerid, time, timestamp, tostring, weekofyear, year

参考:
- Execution of Pine functions and historical context inside function blocks (Pine Script User Manual 4 documentation)
- Exceptions (Pine Script User Manual 4 documentation)

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

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

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

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

平均足チャートを表示しているときに実際の銘柄の値を取得するには、下記のように 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スクリプトでは、比較演算子の結果は、true、false、na の3つの可能性がある。

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

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

また、変数 a = na のとき、a == na、a != 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