Ранее цены, счётчики и настройки хранились в отдельных переменных. Этого достаточно для множества индикаторов и стратегий. Но в большой программе бывает удобно объединить связанные данные: например, границы диапазона или цену и время сигнала.

Для этого есть пользовательский тип, по-английски user-defined type, сокращённо UDT. Метод является операцией, связанной с определённым типом. Оба входят в Pyne code, но для следующих примеров необязательны. Если хотите сначала дойти до стратегий, переходите к отладке и вернитесь позже.

Связанные данные в одном типе

Диапазон задают два числа: нижняя и верхняя границы. Собственный тип даёт этой паре имя:

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

class определяет новый тип PriceRange. bottom и top являются его полями. Оба допускают дробные значения и по умолчанию равны нулю.

@udt делает тип пользовательским типом Pyne. udt импортируется из pynecore.core.pine_udt. В Edge в определении типа записываются поля, методы отдельно. Наследование в этом наборе языка не используется.

Тип и созданный объект

Тип описывает совместно хранимые данные. Конкретную пару хранит объект, или экземпляр:

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

PriceRange() создаёт экземпляр с полями по умолчанию. Следующие строки устанавливают минимум текущей свечи нижней границей, максимум верхней. Поля читаются через 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.

Это можно написать и простой функцией. Метод помогает организовать несколько операций одного типа и явно обозначить обрабатываемые данные.

Полная программа

Создайте udt_ranges.py в Scripts:

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

Запустите на демонстрационных данных. Синяя линия показывает минимумы, оранжевая максимумы, зелёная середину. Зелёная точка каждой свечи лежит между двумя другими.

В PyneIDE показаны тип PriceRange и метод midpoint; на графике три линии нижней границы, верхней и середины.
Поля PriceRange хранят границы свечи, метод midpoint вычисляет середину.

Каждая свеча создаёт новый PriceRange. Поля здесь не сохраняют прошлую свечу: они хранят границы текущей.

Присваивание и копирование

Присвоение объекта другой переменной не копирует его:

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

Обе переменные ссылаются на один объект. Вторая строка меняет и 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

У этого типа из двух чисел изменение новой верхней границы не меняет исходную. Числа хранятся в разных объектах. Если поле позднее содержит другой объект или массив, его передача новому экземпляру тоже передаёт ссылку, не копируя содержимое.

Сохранение объекта между свечами

Для повторного использования объекта примените знакомый Persistent. Импортируйте Persistent из pynecore.

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

Объект создаётся один раз, поля обновляются на каждой свече. Этими строками можно заменить создание и присваивания полей в начале main(). Середина и рисование прежние.

График здесь не меняется: поля получают те же значения. В другой задаче их можно менять только при событии, сохраняя ранее выбранный диапазон. Сам тип не делает объект постоянным между свечами.

Попробуйте самостоятельно

Создайте метод width, возвращающий ширину диапазона. По примеру midpoint используйте self: PriceRange и вычислите из полей.

Решение
@method
def width(self: PriceRange) -> float:
    return self.top - self.bottom

Вызов внутри main():

range_width = width(current_range)

Для максимума 108 и минимума 101 ширина 7. Метод только читает поля, не меняя границы.