これまでは、価格、カウンター、設定を別々の変数に入れていました。それでも多くの指標やストラテジーを作れます。ただ、大きなプログラムでは、関連するデータをまとめたいことがあります。例えば価格帯の上下限や、シグナルの価格と時刻です。

そのために使うのがユーザー定義型(user-defined type、略してUDT)です。メソッドは、ある型に対応する操作です。どちらもPyne codeの一部ですが、この本のその先の例題に必須ではありません。先にストラテジーまで進みたければ、デバッグへ進み、後で戻ってもかまいません。

関連するデータを型にまとめる

価格帯は、下限と上限の2つの数で表せます。独自の型で、この組に名前を付けます。

@udt
class PriceRange:
    bottom: float = 0.0
    top: float = 0.0

classが新しい型を定義し、名前はPriceRangeです。bottomとtopはフィールドです。どちらも小数を保存でき、既定値は0です。

@udtによって、Pyneのユーザー定義型になります。udtはpynecore.core.pine_udtから読み込みます。Edgeでは型の定義内にフィールドを書き、メソッドは別に定義します。この範囲の言語機能では、継承は使いません。

型と、型から作るオブジェクト

型は、一緒に保存するデータの構成を定めます。具体的な値を入れるのは、その型から作ったオブジェクト、またはインスタンスです。

current_range = PriceRange()
current_range.bottom = low
current_range.top = high

PriceRange()が、フィールドに既定値を持つ新しいインスタンスを作ります。次の2行で、下限を現在の安値、上限を高値にします。フィールドはcurrent_range.bottomやcurrent_range.topで読み取ります。ピリオドは、フィールドへのアクセスを表します。

同じ型から複数のオブジェクトを作れます。別のPriceRangeが異なる価格帯を保持していてもかまいません。フィールド名は同じでも、値は独立しています。

メソッド:特定の型に対する操作

価格帯の中間値は、上下限の平均です。型に対応するメソッドとして書けます。

@method
def midpoint(self: PriceRange) -> float:
    return (self.bottom + self.top) / 2

@methodは、関数をメソッドとして指定します。最初のパラメーターの型が、対象データを示します。ここではPriceRangeです。selfは、中間値を求める対象オブジェクトを、メソッド内部で呼ぶ名前です。これはパラメーター名であり、型はPriceRangeで指定しています。

この定義はclassの外、ファイルの最外層に書きます。methodはpynecore.core.pine_methodからインポートします。

呼び出しは次の形です。

center = midpoint(current_range)

既知のメソッドを名前で呼び、対象のオブジェクトを最初の引数に渡します。selfはcurrent_rangeを受け取り、返された数値をcenterに代入します。

普通の関数でも同じ計算を書けます。特定の型に関連する操作が複数あり、どのデータを扱うか明確にしたいときに、メソッドが整理に役立ちます。

プログラム全体

Scriptsにudt_ranges.pyを作ります。

"""
@pyne edge
"""
from pynecore.core.pine_method import method
from pynecore.core.pine_udt import udt
from pynecore.lib import color, high, low, plot, script


@udt
class PriceRange:
    bottom: float = 0.0
    top: float = 0.0


@method
def midpoint(self: PriceRange) -> float:
    return (self.bottom + self.top) / 2


@script.indicator("Price Range Objects", overlay=True)
def main():
    current_range = PriceRange()
    current_range.bottom = low
    current_range.top = high
    center = midpoint(current_range)
    plot(current_range.bottom, "Lower", color=color.blue)
    plot(current_range.top, "Upper", color=color.orange)
    plot(center, "Midpoint", color=color.green, linewidth=2)

デモデータで実行してください。青が安値、オレンジが高値、緑が中間値です。各ローソク足で、緑の点は他の2つの間にあります。

PyneIDEにユーザー定義型PriceRangeとmidpointメソッドのコードがあり、チャートに下限、上限、中間値の3本の線が表示されている。
PriceRangeの2つのフィールドがローソク足の上下限を保持し、midpointメソッドが中間値を計算します。

各ローソク足で新しいPriceRangeを作っています。この例のフィールドは前の値を残さず、現在の上下限を保存します。

代入とコピー

別の変数にオブジェクトを代入しても、コピーにはなりません。

other_range = current_range
other_range.top = other_range.top + 1.0

両方の変数は、同じオブジェクトを参照します。そのため2行目で、current_range.topも変わります。

同じフィールド値を持つ別のインスタンスが欲しければ、新しく作ります。

other_range = PriceRange()
other_range.bottom = current_range.bottom
other_range.top = current_range.top
other_range.top = other_range.top + 1.0

数値2つからなるこの型では、新しいインスタンスの上限を変えても、元の上限は変わりません。別々のオブジェクトに数値を保存しているためです。後でフィールドに別のオブジェクトや配列を入れた場合、それを新しいインスタンスに渡しても参照が渡されるだけで、中身が別にコピーされるわけではありません。

ローソク足の間でオブジェクトを保持する

同じオブジェクトを使い続けたければ、Persistentも使えます。pynecoreからPersistentをインポートしてください。

current_range: Persistent[PriceRange] = PriceRange()
current_range.bottom = low
current_range.top = high

オブジェクトは一度だけ作り、フィールドは各ローソク足で更新します。main()冒頭の作成と2つの代入を、この3行に置き換えられます。中間値の計算と描画はそのままです。

この例では同じフィールド値を入れるので、チャートは変わりません。別の用途では、イベント時だけフィールドを変えることで、以前に決めた価格帯などを保持できます。型を定義しただけで、オブジェクトが永続化されるわけではありません。

自分で試す

価格帯の幅を返すwidthメソッドを作ってください。midpointと同様にself: PriceRangeを受け取り、フィールド2つから計算します。

解答
@method
def width(self: PriceRange) -> float:
    return self.top - self.bottom

main()内の呼び出しです。

range_width = width(current_range)

高値が108、安値が101なら、幅は7です。メソッドはフィールドを読むだけで、上下限を変えません。