Bisher haben die Programme Kurse, Zähler und Einstellungen in getrennten Variablen gespeichert. Damit kannst du viele Indikatoren und Strategien erstellen. In größeren Programmen lohnt es sich unter Umständen, mehrere zusammengehörige Daten als Einheit zu behandeln: etwa die beiden Grenzen eines Kursbands oder Kurs und Zeitpunkt eines Signals.

Dafür gibt es benutzerdefinierte Typen, englisch user-defined types, kurz UDT. Eine Methode ist eine Operation für einen bestimmten Typ. Beides gehört zu Pyne code, wird aber für die weiteren Buchbeispiele nicht vorausgesetzt. Wenn du zuerst zu den Strategien gelangen möchtest, kannst du zur Fehlersuche weitergehen und später hierher zurückkehren.

Zusammengehörige Daten in einem Typ

Ein Kursbereich lässt sich durch zwei Zahlen beschreiben: seine untere und obere Grenze. Mit einem eigenen Typ können wir diesem Datenpaar einen Namen geben:

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

class definiert einen neuen Typ namens PriceRange. bottom und top sind seine Felder. Beide können Nachkommastellen speichern und haben den Standardwert null.

Die Kennzeichnung @udt macht daraus einen benutzerdefinierten Pyne-Typ. Wir importieren udt aus dem Modul pynecore.core.pine_udt. In Edge schreiben wir die Felder in die Typdefinition; die zugehörigen Methoden definieren wir separat. Vererbung verwenden wir in diesem Sprachumfang nicht.

Der Typ und ein daraus erzeugtes Objekt

Der Typ beschreibt, welche Daten wir zusammen speichern. Ein konkretes Datenpaar speichert ein daraus erzeugtes Objekt, auch Instanz genannt:

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

PriceRange() erzeugt eine neue Instanz mit den Standardwerten ihrer Felder. Die nächsten beiden Zeilen setzen die untere Grenze auf das Tief und die obere auf das Hoch der aktuellen Kerze. Über current_range.bottom und current_range.top liest du die Felder. Der Punkt kennzeichnet hier den Zugriff auf ein Feld.

Du kannst mehrere Objekte desselben Typs erzeugen. Ein anderes PriceRange kann beispielsweise einen anderen Kursbereich speichern. Die Feldnamen sind gleich, die Werte unabhängig.

Methode: eine Operation auf einem bestimmten Typ

Die Mitte des Kursbereichs erhalten wir aus dem Durchschnitt seiner Grenzen. Wir können die Berechnung auch als Methode dieses Typs schreiben:

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

@method kennzeichnet die Funktion als Methode. Der Typ ihres ersten Parameters bestimmt die Daten, auf denen sie arbeitet: hier ein Objekt PriceRange. self ist innerhalb der Methode der Name des Objekts, dessen Mitte wir berechnen möchten. Das ist ein Parametername; sein Typ wird durch PriceRange angegeben.

Die Definition steht außerhalb des Blocks class auf der äußersten Dateiebene. Wir importieren method aus dem Modul pynecore.core.pine_method.

Der Aufruf:

center = midpoint(current_range)

Wir rufen die bekannte Methode mit ihrem Namen auf und übergeben als erstes Argument das Objekt, auf dem sie arbeitet. Der Parameter self erhält dadurch das Objekt current_range. Die zurückgegebene Zahl wird in center gespeichert.

Diese Berechnung könntest du auch mit einer einfachen Funktion schreiben. Eine Methode hilft bei der Programmstruktur, wenn du mehrere Operationen für einen bestimmten Typ hast und deutlich machen möchtest, auf welchen Daten sie arbeiten.

Das vollständige Programm

Lege die Datei udt_ranges.py in der Gruppe Scripts an:

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

Führe sie auf den Demodaten aus. Die blaue Linie zeigt die Tiefs, die orangefarbene die Hochs und die grüne deren Mitte. Der grüne Punkt liegt auf jeder Kerze zwischen den beiden anderen Werten.

Die PyneIDE zeigt den benutzerdefinierten Typ PriceRange und die Methode midpoint; im Chart erscheinen drei Linien für untere und obere Kursgrenze sowie den Mittelkurs.
Die beiden Felder von PriceRange speichern die Kerzengrenzen. Die Methode midpoint berechnet deren Mitte.

Das Programm erzeugt auf jeder Kerze ein neues Objekt PriceRange. In diesem Beispiel behalten die Felder keine Werte der vorherigen Kerze; sie speichern immer die beiden Grenzen der aktuellen Kerze.

Zuweisen und Kopieren

Ein Objekt einer anderen Variablen zuzuweisen ist nicht dasselbe, wie es zu kopieren:

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

Die beiden Variablen verweisen auf dasselbe Objekt. Deshalb ändert die zweite Zeile auch den Wert von current_range.top.

Möchtest du eine getrennte Instanz mit denselben Feldwerten, erzeuge ein neues Objekt:

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

Bei diesem Typ aus zwei Zahlen verändert eine Änderung der oberen Grenze der neuen Instanz nicht das Original. Die beiden Zahlen werden in getrennten Objekten gespeichert. Wenn du später ein anderes Objekt oder ein Array in einem Feld speicherst, gibt die Zuweisung an eine neue Instanz ebenfalls nur eine Referenz weiter und kopiert den Inhalt nicht separat.

Ein Objekt zwischen Kerzen behalten

Möchtest du dasselbe Objekt weiterverwenden, kannst du die bereits bekannte Kennzeichnung Persistent nutzen. Importiere dafür Persistent aus dem Modul pynecore.

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

Das Objekt wird einmal erzeugt, seine beiden Felder aktualisieren wir auf jeder Kerze. Du kannst die Objekterzeugung am Anfang von main() und die beiden Feldzuweisungen durch diese drei Zeilen ersetzen. Mittelkursberechnung und Darstellung bleiben gleich.

In diesem Beispiel verändert sich der Chart dadurch nicht: Wir setzen dieselben Feldwerte. Bei einer anderen Aufgabe kannst du die Felder nur bei einem Ereignis ändern und so etwa einen früher markierten Kursbereich behalten. Der Typ allein macht das Objekt nicht persistent.

Probiere es selbst aus

Schreibe eine Methode namens width, die die Breite des Kursbereichs zurückgibt. Verwende nach dem Vorbild von midpoint einen Parameter self: PriceRange und berechne das Ergebnis aus den beiden Feldern.

Lösung der Aufgabe
@method
def width(self: PriceRange) -> float:
    return self.top - self.bottom

Der Aufruf innerhalb von main():

range_width = width(current_range)

Beträgt das Hoch einer Kerze 108 und ihr Tief 101, ist die Breite 7. Die Methode liest die Felder nur; sie verändert keine der Grenzen.