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.
    ※日本語訳:一貫性を保つため、関数は毎回呼ばれるべきです。関数の呼び出しを三項演算子の外に出すことを勧めます。

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


//@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)

期待通りに変数 xy が同じ値を保持するためには、下記のように関数 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)

plot() の便利ワザ

plot() を工夫して利用することにより下記のようなトリッキーな表示を行うことができる。

  • 動的に変動する水平線を表示する
  • 垂直線を表示する

動的に変動する水平線を表示する

水平線を表示させる関数として hline() があるが、この関数には定数しか設定できない。変数の値の変化によって、チャート上で動的に変動する水平線を表示させるには、別の方法を考える必要がある。

簡単なものとして plot() を利用する方法がある。plot() の引数には trackprice があり、これを true に設定すると、plot() の直近の値を水平線で表示してくれる。そして、transp=100 に設定し透明度を100にする。すると、plot() の実態の線は透明で見えなくなるが、trackprice=true で表示される水平線は、この透明度設定の影響を受けないため表示が残る。こうして動的に変動する水平線を表示することが可能となる。


//@version=4
study("dynamic horizontal line", overlay=true)

plot(highest(high, 20), trackprice=true, transp=100, color=color.green)
plot(lowest(low, 20), trackprice=true, transp=100, color=color.red)

//plot(close, trackprice=true, transp=100) // debug 水平線が動的に変動するか確認
//plot(highest(high, 20), color=color.green) // debug
//plot(lowest(low, 20), color=color.red) // debug

垂直線を表示する

ある条件になったらチャート上に垂直線を引きたいというとき、簡単な方法として plot() を使う方法がある。

下記の例のように、study()scale=scale.none を指定し、plot() で巨大な値 1e10 をヒストグラムでチャート上に表示させることで垂直線もどきを実現している。

ポイントは scale=scale.none を指定していること。これにより、巨大な値が表示画面に収まるようにチャートがリサイズされるのを防いでいる。


//@version=4
study("vertical line", overlay=true, scale=scale.none)

ma = sma(close, 20)
co = crossover(close, ma)
cu = crossunder(close, ma)

plot(co ? 1e10 : na, style=plot.style_histogram, color=color.green)
plot(cu ? 1e10 : na, style=plot.style_histogram, color=color.red)

規模の大きいシステムを実装する際のコンパイラ制限値まとめ

Pineスクリプトで規模の大きいシステムを実装する際に関係する代表的なコンパイラ制限値を調査した。

  • Pineスクリプトで利用できるローカル変数の数は最大999個まで
  • Pineスクリプトでコンパイル可能なローカルスコープの数は最大499個まで

Pineスクリプトで利用できるローカル変数の数は最大999個まで

Pineスクリプトで利用できるローカル変数の最大数を調べてみたところ、変数が1000個に達した時点で下記のエラーメッセージが出た。

Script has too many local variables (1001) in "#f1": #loc_0, #loc_1, #loc_2, #loc_3, #loc_4... The limit is 1000

1000に達するとエラーとなるので、実際に利用できる変数の最大数は999個までとなる。
openclose 等のビルトイン変数は、この数に含まれない。

変数の最大数を確かめるために、下記のようなコードを利用した。長いので途中は省略している。


//@version=4
study("max limit num of variables")

x1 = open + high + low + close
x2 = x1
x3 = x2
x4 = x3
//
//省略...
//
x997 = x996
x998 = x997
x999 = x998
//x1000 = x999 // Script has too many local variables...The limit is 1000

plot(x999)
//plot(x1000)

また、mutable 変数の場合の制限についても調べてみた。
※変数に := 演算子を使うと、その変数は mutable(変更可能な)変数としてPineコンパイラに認識される。

mutable 変数の場合、値を変更する度に別の一つの変数として数えられるようで、998回まで := 演算子を使った値の変更が可能だが、999回で下記の "The limit is 1000" エラーが出た。

Script has too many local variables (1002) in "#f1": #mut_0, #mut_0, #mut_0, #mut_0, #mut_0... The limit is 1000

mutable 変数の制限の確認には、下記のようなコードを使用した。長いので途中は省略している。


//@version=4
study("max limit num of variables")

x = close

x := x[1]
x := x[1] - x[2]
x := x[2] - x[3]
//
//省略...
//
x := x[996] - x[997]
x := x[997] - x[998]
//x := x[998] - x[999] // Script has too many local variables...The limit is 1000

plot(x)

Pineスクリプトでコンパイル可能なローカルスコープの数は最大499個まで

Pineスクリプトの関数、if文、for文などはそれぞれローカルスコープを持つ。そのローカルスコープを最大どれだけ持てるのか、別の言い方をすると、関数をどれだけ定義できるか、if文やfor文をどれだけ書けるか、その最大数を調べてみた。

簡単なコードで確認したところ、ローカルスコープが500個できるようにした時点でエラーが出た。

Script has too many local scopes: 501. The limit is 500.

500個でエラーが出るので、実際可能なのは499個まで。関数、iffor それぞれの確認コードを作ったが、ローカルスコープの最大数は関数、iffor など合わせて全体で499個までとなる。例えば、関数を499個定義したら、それ以上 iffor など一つも使うことはできないことになる。


//@version=4
study("max limit num of scopes")

x = close

func1(src) => src[1]
x := func1(close)
func2(src) => src[2]
x := func2(close)
func3(src) => src[3]
x := func3(close)
//
//省略...
//
func499(src) => src[499]
x := func499(close)
//func500(src) => src[500] // Script has too many local scopes: 501. The limit is 500.
//x := func500(close)

plot(x)

//@version=4
study("max limit num of scopes")

x = close

if x > close[1]
    x := open[1]
if x > close[2]
    x := open[2]
if x > close[3]
    x := open[3]
//
//省略...
//
if x > close[499]
    x := open[499]
//if x > close[500] // Script has too many local scopes: 501. The limit is 500.
//    x := open[500]

plot(x)

//@version=4
study("max limit num of scopes")

x = close

for i = 1 to 1
    x := close[1]
for i = 2 to 2
    x := close[2]
for i = 3 to 3
    x := close[3]
//
//省略...
//
for i = 499 to 499
    x := close[499]
//for i = 500 to 500 // Script has too many local scopes: 501. The limit is 500.
//    x := close[500]

plot(x)

コンパイル時の典型的エラーと対処法

Pineスクリプトのコンパイル時に発生する典型的なエラーとして下記が挙げられる。この記事では、それぞれのエラーの原因と対処法をサンプルコードを付けてまとめた。

  • Undeclared identifier
  • Value with NA type cannot be assigned to a variable that was defined without type keyword
  • Variable 'x' was declared with 'series[integer]' type. Cannot assign it expression of type 'series[float]'
  • Cannot call 'operator +' with arguments (series[bool], series[bool])

エラー:Undeclared identifier

このエラーが出る典型的な原因として、下記の変数が関係している可能性がある。
  1. self-referenced(自己参照)変数
  2. forward-referenced(前方参照)変数
1. self-referenced(自己参照)変数による Undeclared identifier エラー
下記は self-referenced(自己参照)変数を使ったコードの例。

//@version=4
study("self-referenced variable")

// elapsedは自己参照変数
elapsed = nz(elapsed[1]) + 1

plot(elapsed)

変数 elapsed は、宣言時に自己 elapsed[1] を参照している。このような変数を self-referenced(自己参照)変数という。

自己参照変数はPineスクリプトv2まではサポートされていたが、v3以降では、自己参照変数があるコードは Undeclared identifier というエラーメッセージが出るようになった。v3以降では、下記のように書き直す必要がある。


//@version=4
study("self-referenced variable")

// 変数の初期化を追加
// 整数0で初期化することで、elapsedは整数型であることがPineコンパイラに伝わる
elapsed = 0

// = を := に変更する (elapsedはmutable変数となる)
elapsed := nz(elapsed[1]) + 1

plot(elapsed)

なお、書き直すと、elapsed は自己参照変数ではなくなり、mutable(変更可能な)変数としてPineコンパイラに認識されるようになる。

参考:Self-referenced variables are removed

2. forward-referenced(前方参照)変数による Undeclared identifier エラー

次に、下記は forward-referenced(前方参照)変数を使ったコードの例。変数 s は前方参照変数と呼ばれる。
※変数 s が実際に定義されるのは3行後、その定義前に変数sが参照されるため、「前方参照」変数と呼ばれる


//@version=2
study("forward-referenced variable", overlay=true)

s1 = nz(s[1]) // 変数sは前方参照変数
a = 0.2
s1_w = s1 * (1 - a)
s = close * a + s1_w // 変数sが定義されるのはここ

plot(s)

Pineスクリプトv2でサポートしていた forward-referenced(前方参照)変数は、v3以降サポートされなくなった。そのため、前方参照変数を含むコードをv3以降でコンパイルすると Undeclared identifier エラーが出る。

前方参照変数を含むコードは下記のように修正できる。参照前に変数の宣言を新たに追加し、実際に定義していた箇所の = 演算子を := 演算子に置き換えて mutable 変数に変更する。


//@version=4
study("forward-referenced variable", overlay=true)

s = 0.0 // 参照が行われる前に宣言を新たに追加
s1 = nz(s[1])
a = 0.2
s1_w = s1 * (1 - a)
//s = close * a + s1_w
s := close * a + s1_w // =演算子を:=演算子に変更

plot(s)

参考:Forward-referenced variables are removed


エラー:Value with NA type cannot be assigned to a variable that was defined without type keyword

変数を na で初期化している場合、その変数が原因でこのエラーが出る。

Pineスクリプトv2やv3では、変数を na で初期化しても問題なかったが、v4以降ではコンパイルエラーが出るようになった。


//@version=4
study("init with na", overlay=true)

crossed_line = na // v4以降ではコンパイルエラー

crossed_line := crossed_line[1]

ma = sma(close, 200)

if crossover(close, ma)
    crossed_line := close
if crossunder(close, ma)
    crossed_line := na

plot(crossed_line, style=plot.style_cross, linewidth=2)

上記のコードはv4以降、Value with NA type cannot be assigned to a variable that was defined without type keyword というコンパイルエラーが出る。
※エラーを日本語に訳すと「型キーワードを指定せずに定義された変数にNA型の値を割り当てることはできません」

このエラーへの対処法は、変数を na で初期化したいときは、型キーワードを指定すれば良い。上記コードの例の場合、float 型の close 値を代入する箇所があるので、それに合わせて変数に float 型を指定すれば良い。


//@version=4
study("init with na", overlay=true)

crossed_line = float(na)  // float型を指定してnaを割り当て
//float crossed_line = na // この書き方でも良い

crossed_line := crossed_line[1]

ma = sma(close, 200)

if crossover(close, ma)
    crossed_line := close
if crossunder(close, ma)
    crossed_line := na

plot(crossed_line, style=plot.style_cross, linewidth=2)

参考:na value


エラー:Variable 'x' was declared with 'series[integer]' type. Cannot assign it expression of type 'series[float]'

このエラーが出る典型的な例として、float 型の変数を 0 で初期化している場合が考えられる。

Pineスクリプトv2では、float 型の変数を 0(整数)で初期化しても問題なかったが、v3以降ではコンパイルエラーが発生するようになった。下記はこのようなエラーが発生するコードの例。


//@version=4
study("init with zero")

cost = 0 // v3以降ではエラーになる
cost := nz(cost[1])

if crossover(close[1], sma(close[1], 200))
    cost := open

plot(cost)

上記のコードは、v3では Variable `cost` was declared with series[integer] type. Cannot assign it expression of type series、v4では Variable 'cost' was declared with 'series[integer]' type. Cannot assign it expression of type 'series[float]'. というエラーメッセージが出る。
※エラーメッセージを日本語に訳すと「series[integer] 型(integer 型の一次元データ)で宣言された変数に、series[float] 型(float 型の一次元データ)を割り当てることはできません」

このコンパイルエラーへの対処法は、float 型の値で更新される変数に対して、明示的に 0.0float 型のゼロ)で初期化する必要がある。


//@version=4
study("init with zero")

cost = 0.0 // float型のゼロで初期化
cost := nz(cost[1])

if crossover(close[1], sma(close[1], 200))
    cost := open

plot(cost)

エラー:Cannot call 'operator +' with arguments (series[bool], series[bool])

論理値に対して明示的な型変換をせずに数値演算を行うとこのエラーが出る。

Pineスクリプトv2では、論理値から数値への暗黙的な型変換が行われ、論理値に対して数値演算を行うことができたが、v3以降では、暗黙的な型変換による論理値の数値演算はサポートされなくなり、このようなコードはエラーが出るようになった。
※v2では true1.0false0.0 へ暗黙的に変換される


//@version=2
study("bool to num", overlay=true)

b1 = close > open
b2 = close > close[1]

x = b1 + b2 // 暗黙的な型変換が行われ、v2ではエラーにならない

plotchar(x == 1, char='/', size=size.tiny)
plotchar(x == 2, char='|', size=size.tiny)

v3以降でこのエラーを回避するには、論理値を明示的に数値へ変換する関数を用意すれば良い。下記の修正版コードでは、関数 b_to_n を定義し、この関数を介して、論理値は整数に変換されるように修正している。


//@version=4
study("bool to num", overlay=true)

b_to_n(b) =>
    b ? 1 : 0

b1 = close > open
b2 = close > close[1]

//x = b1 + b2
x = b_to_n(b1) + b_to_n(b2) // 関数 b_to_n() が論理値を整数へ変換

plotchar(x == 1, char='/', size=size.tiny)
plotchar(x == 2, char='|', size=size.tiny)

参考:Math operations with booleans are forbidden