指標の値がどう作られるかを知ると、応用しやすくなります。計算に何を使い、何を次の足へ持ち越すのでしょうか。平均や価格の元データを変えると、どうなるでしょうか。ソースコードから、それらも分かります。

PyneCoreのtaライブラリはPyne codeで書かれています。Persistent、関数呼び出し、条件は、この本で学んだ仕組みを使います。ソース全体を読めるので、既存の指標も、自分のプログラムと同じように追えます。

これは発展編で、ソースの読み方と実験の進め方を扱います。一度に全ライブラリを理解する必要はありません。短い関数から始め、それが呼び出す関数を追ってください。

実際の実装を開く

  1. 作成済みのmoving_averages.pyをPyneIDEで開きます。
  2. ta.ema(close, 10)で、macOSならCommand、WindowsやLinuxならCtrlを押しながらemaをクリックします。右クリックのGo to Definitionでも移動できます。
  3. インストール済みのPyneCoreのta.pyが、emaの定義位置で開きます。
  4. 説明と本体を読みます。別の関数を呼んでいれば、その定義へ進めます。
PyneIDEでライブラリのta.pyを開き、emaの定義と初期値の説明を表示している。
定義へ移動すると、インストールされたta.pyのEMA関数が開きます。自分のプログラムは別タブに残ります。

パラメーターの表示と定義への移動は、コードを書くときのヘルプで試しました。同じ操作でRSI、MACD、他の関数も読めます。taの全リファレンスは、関数を探し、パラメーターを理解する際に役立ちます。

SMA:式の裏側の合計処理

ta.smaの計算は次の2行です。

length = int(length)
return lib_math.sum(source, length) / length

平均はウィンドウの合計を要素数で割ったものです。長さを整数にして、合計をlib_math.sumで求めます。状態を管理するのは、sma本体ではなく呼び出した合計処理です。さらに追うと、math.pyを経由して_math_stateful.pyの実装へ進みます。

短い関数でも、状態を持つ可能性があります。呼び出す先の動作も計算の一部として読む習慣を付けてください。ループで作った移動平均では、同じウィンドウの値を自分で足しました。

EMAとRMA:保持した平均を更新する

ウォームアップ後、EMAは次を行います。

last_val = last_val + alpha * (source - last_val)
return last_val

last_valは前の平均、sourceは現在の値です。その差の一部だけ、前の平均を動かします。EMAのalphaは2 / (length + 1)です。長さ10なら、新しい価格と前の平均の差の約18.18%を、前の平均に加えます。

ソースではalphaもlast_valもPersistentです。last_valは初めは欠損なので、同じ長さのSMAで初期化を試みます。

if not (last_val == last_val):
    last_val = sma(source, length)
    return last_val

SMAに必要なデータがなければ、結果は欠損です。最初の平均ができてからは、前の値を更新します。not (last_val == last_val)は欠損判定で、欠損値が自分自身と等しくない性質を使います。自分の例題では、同じ目的でna(value)を使いました。

RMA(Wilderの平滑化)も同様に始まりますが、更新は違います。

last_val = (last_val * (length - 1) + source) / length
return last_val

長さ10なら、新しい値の重みは10分の1です。同じ長さのEMAではもっと大きくなるので、同じ価格列への反応が異なります。

ソース全体には、入力の欠損や長さ1の特別扱いもあります。同じ式の自作版でも、初期値、欠損の処理、実行する足が違えば結果が変わります。浮動小数点の演算順も影響するので、正確に比較するなら、その順序も保ってください。

RSI:上昇と下落を別々に平滑化する

RSIは、元データの上昇と下落を分けて扱います。PyneCoreの主な計算です。

rma_u = rma(builtins.max(source - prev_src, 0.0), length)
rma_d = rma(builtins.max(prev_src - source, 0.0), length)
prev_src = source

prev_srcは、前の有効な元データを保持するPersistent変数です。100から103なら上昇3、下落0、103から101なら上昇0、下落2です。RMAでそれぞれを別に平滑化し、2か所の呼び出しには独立した状態があります。

prev_srcを更新するのは、2つの差を計算した後です。前の値を永続変数で保持する発展課題と同じ順序です。

平滑化した2つの変化の比から、RSIを求めます。

if rma_d <= 1e-10:
    return 100.0
if rma_u <= 1e-10:
    return 0.0

return 100 - 100 / (1 + rma_u / rma_d)

下落がなければ100、上昇がなければ0です。実装では非常に小さな値も扱い、先に下落側を調べます。両方がしきい値以下なら100になります。式だけでは分からない細部です。

最初の有効価格だけでは変化は求められません。まず保存し、次の値から2つの平滑化を始めます。ウォームアップは変化の個数で決まります。RSIは確率ではなく、70という値は上昇する確率70%を意味しません。

MACD:移動平均から別の指標を作る

MACDは2つのEMAの差を求め、さらにそのEMAを求めます。欠損判定を除いた主な計算です。

fast = ema(source, fastlen)
slow = ema(source, slowlen)
macd_val = fast - slow
signal = ema(macd_val, siglen)

3つの戻り値があります。

return macd_val, signal, macd_val - signal

MACD線は短期と長期の平均の差、シグナル線はその平滑化です。ヒストグラムは2本の差なので、一致すれば0です。

3つの結果を、3つの変数で受け取れます。

macd_line, signal_line, histogram = ta.macd(close, 12, 26, 9)

順序は、MACD線、シグナル線、ヒストグラムです。2つの価格平均ができるまでは、差も欠損します。その後、シグナル線にもウォームアップが必要です。複合指標の初期欠損は、内部の計算からも生じます。

ATR:価格のギャップも含めた値幅

ATRの実装は短いものです。

return rma(tr(True), length)

学んだRMAで、True Range(真の値幅)を平滑化します。次の3つの距離の最大値です。

  • 現在の高値と安値の差。
  • 現在の高値と、前の終値との距離。
  • 現在の安値と、前の終値との距離。

前の終値100、現在の安値108、高値110なら、現在の足の値幅は2です。ただしTrue Rangeはmax(2, 10, 8)で10です。前の終値からの価格ギャップも含めます。

前の終値がない最初の足では、tr(True)はhigh - lowを使います。ソースでは、前の終値を実行環境の内部データから読みます。ATRは値動きの大きさを価格の単位で表し、方向は示しません。

ボリンジャーバンド:平均と標準偏差

ta.bbの中央線はSMAで、上下の境界は、そこから標準偏差の指定倍数だけ離れています。先に標準偏差と平均を計算します。

std_dev = stdev(series, length)
middle = sma(series, length)

中央線が有効なら、次を返します。

std_dev *= mult
return middle, middle + std_dev, middle - std_dev

標準偏差は、ウィンドウ内の値が平均の周りにどれほど散らばっているかを表します。中央100、標準偏差3、倍率2なら、上限106、下限94です。ここも3つを返しますが、順序はMACDとは異なり、中央、上限、下限です。

自作版:まず元の結果を再現する

bands_workshop.pyを作ります。簡単なライブラリ関数からバンドを組み立て、比較用に既存のta.bbの上限も求めます。

"""
@pyne edge
"""
from pynecore.lib import close, color, input, plot, script, ta


def custom_bands(source: float, length: int, multiplier: float) -> tuple[float, float, float]:
    middle = ta.sma(source, length)
    deviation = ta.stdev(source, length)
    upper = middle + multiplier * deviation
    lower = middle - multiplier * deviation
    return middle, upper, lower


@script.indicator("Bands Workshop", overlay=True)
def main(
    length: int = input.int(20, title="Length", minval=2, maxval=200),
    multiplier: float = input.float(2.0, title="Deviation multiplier", minval=0.1, maxval=5.0),
):
    builtin_middle, builtin_upper, builtin_lower = ta.bb(close, length, multiplier)
    own_middle, own_upper, own_lower = custom_bands(close, length, multiplier)
    plot(builtin_upper, "Built-in upper", color=color.blue, linewidth=3)
    plot(own_upper, "Custom upper", color=color.orange)
    plot(own_middle, "Custom middle", color=color.green)
    plot(own_lower, "Custom lower", color=color.gray)

tuple[float, float, float]は数値3つの結果型です。return middle, upper, lowerで返し、呼び出しの左の3つの変数がそれぞれ受け取ります。今はbuiltin_middleとbuiltin_lowerを描きませんが、後で確認にも使えます。

デモデータで実行すると、自作と既存の上限が重なります。太い青の上に細いオレンジが通ります。ローソク足にポインターを置き、Built-in upperとCustom upperを比べてください。長さ20なら、最初の19本はウィンドウがそろっていません。

bands_workshop.pyの横のチャートでBuilt-in upperとCustom upperが同じ値を示し、自作の中央線と下限も表示されている。
選択したローソク足では、既存と自作の上限はどちらも216.85で、線が重なっています。

詳しく確認するには、Open plot output CSVで値を開きます。自作版は、最初の有効な点と、その後の値も確認してください。見た目が同じ線でも、小さな差があることがあります。

自分で試す:中央線をEMAにする

custom_bandsの中央線だけをSMAからEMAに変えます。標準偏差の計算と既存のta.bbは、比較用に残してください。

実行前に予想します。自作の上限は、ずっと既存の線と一致するでしょうか。価格が急変した後はどうでしょうか。

解答

自作関数の最初の計算を変えます。

middle = ta.ema(source, length)

上下限は、このEMAからの距離になります。ta.stdevは変わりません。そのため、自作の中央と上下限は、通常、元のバンドから動きます。同じ標準偏差と倍率を加えるので、上限同士の差はEMAとSMAの差に一致します。

これが自分の実験版です。線が違うだけでは、取引ルールの成績がよくなるかは分かりません。同じデータと設定のストラテジーで、別に調べられます。

ライブラリをさらに読む

指標を1つ選び、入力、保持するもの、結果を自分の言葉で説明してください。計算全体を理解できるまで、内部の呼び出しを追います。RSIなら、rsi → rma → smaと進み、変化から平滑化、初期値までたどれます。

欠損、ウォームアップ、境界条件の分岐も読んでください。自作するときは、まず1点だけ変えて、元と比較します。意図した違いと計算の誤りを区別しやすくなります。

他の関数もCommandまたはCtrlを押しながらクリックして読めます。長い指標は時間がかかっても、見る順序は同じです。入力、保持するデータ、計算、結果を追ってください。