Die Berechnung der Kerzenspanne kennst du bereits: high - low. Brauchst du sie in mehreren Berechnungen, kannst du eine eigene Funktion daraus machen. Der Aufruf kann den Zweck der Berechnung zeigen, und die Formel steht nur an einer Stelle.

Eingabe und Ergebnis einer Funktion

def price_range(top: float, bottom: float) -> float:
    return top - bottom

def definiert eine Funktion. Sie heißt price_range und erwartet zwei Eingaben namens top und bottom. Beide sind Zahlen, die auch Nachkommastellen enthalten können.

-> float kennzeichnet den Ergebnistyp. return berechnet das Ergebnis, gibt es zurück und beendet den jeweiligen Funktionsaufruf.

price_range(105, 100) ergibt 5. Die Funktion weiß nicht, ob das Kurse sind, und wählt keine Kerze aus. Der aufrufende Code gibt beide Werte vor.

Der Funktionsrumpf ist hier um vier Leerzeichen eingerückt. In diesem Beispiel steht die Definition neben main() auf der äußersten Ebene der Datei. Deshalb übergeben wir ihr die beiden benötigten Kurse als Parameter. Du kannst eine Funktion auch innerhalb von main() anlegen; das probieren wir weiter unten aus.

Der Gültigkeitsbereich eines Namens

Die Parameter top und bottom sind nur innerhalb von price_range verfügbar. main() kann nicht direkt auf sie zugreifen. Dort verwendest du das zurückgegebene Ergebnis.

Der Gültigkeitsbereich eines Namens bestimmt, in welchem Codeabschnitt er verfügbar ist. Zwei getrennte Funktionen können dieselben Parameternamen verwenden, ohne dass daraus eine gemeinsame Variable wird.

Die auf Dateiebene definierte Funktion price_range erreicht die lokalen Variablen von main() nicht automatisch. Sie kann jedoch am Dateianfang importierte Namen wie close verwenden. Die Sichtbarkeit hängt also auch davon ab, wo du eine Funktion definierst.

Eine Funktionsdefinition und ein Funktionsaufruf sind ebenfalls verschieden. Die Definition beschreibt die Regel; der Aufruf führt sie mit konkreten Werten aus.

Das vollständige Programm

Lege price_functions.py an. Hier berechnet dieselbe Funktion die gesamte Kerzenspanne und die vorzeichenbehaftete Veränderung zwischen Eröffnungs- und Schlusskurs:

"""
@pyne edge
"""
from pynecore.lib import close, color, high, low, open, plot, script


def price_range(top: float, bottom: float) -> float:
    return top - bottom


@script.indicator("Price Functions", overlay=False)
def main():
    full_range = price_range(high, low)
    body_change = price_range(close, open)
    plot(full_range, "Full range", color=color.blue)
    plot(body_change, "Body change", color=color.orange)

Führe sie auf den Demodaten aus. Die gesamte Spanne ist nicht negativ, da das Hoch einer Kerze mindestens so groß wie ihr Tief ist. Die Veränderung zwischen Eröffnung und Schluss kann negativ sein. Die Funktion verändert keinen der übergebenen Kurse; sie gibt die Differenz der beiden Zahlen zurück.

Die beiden Funktionsaufrufe in price_functions.py zeichnen die gesamte Kerzenspanne und die vorzeichenbehaftete Veränderung des Körpers.
Dieselbe eigene Funktion berechnet aus unterschiedlichen Eingaben zwei getrennte Ergebnisse.

Die Funktion in diesem Beispiel ist kurz und von Hand leicht zu prüfen. In größeren Programmen kannst du auch komplexeren Teilaufgaben Namen geben. Eine einzelne Addition braucht aber nicht zwingend eine eigene Funktion.

Die Reihenfolge der Parameter

Der Aufruf price_range(high, low) übergibt high an top und low an bottom. Vertauschst du sie, kehrt sich das Vorzeichen der Subtraktion um.

Du kannst auch den Parameternamen angeben:

full_range = price_range(top=high, bottom=low)

Das ist länger, kann bei komplexeren Aufrufen aber lesbarer sein. Der angegebene Name muss exakt dem Parameternamen der Funktion entsprechen.

Eine Funktion innerhalb von main()

Eine innen definierte Funktion kann auch auf Variablen der umgebenden Funktion zugreifen. Das hilft etwa, wenn mehrere Berechnungen dieselbe per Input angegebene Einstellung verwenden.

Glätten wir die beiden Linien des vorherigen Programms mit einem gleitenden Durchschnitt. Dessen Länge geben wir über einen einzigen Input vor und verwenden sie für beide Berechnungen. Die innere Funktion erhält nur die beiden Kurse als Parameter; die Länge findet sie unter den Parametern von main().

Behalte in price_functions.py die äußere Funktion price_range und den Indikatordekorator bei. Ergänze im Import aus pynecore.lib die Namen input und ta und ersetze die Definition von main() durch:

def main(
    length: int = input.int(5, title="Average length", minval=1, maxval=200),
):
    def average_difference(top: float, bottom: float) -> float:
        return ta.sma(price_range(top, bottom), length)

    average_range = average_difference(high, low)
    average_body_change = average_difference(close, open)
    plot(average_range, "Average range", color=color.blue)
    plot(average_body_change, "Average body change", color=color.orange)

length ist der bereits bekannte ganzzahlige Input. Die innere Funktion average_difference erhält zwei Parameter: top und bottom werden vom Aufruf übergeben. length liest sie aus der umgebenden Funktion main(), deshalb musst du diesen Wert in den Aufrufen nicht erneut angeben.

Die Definition der inneren Funktion beginnt mit vier Leerzeichen, weil sie zu main() gehört. Ihr eigener Rumpf, die Zeile mit return, beginnt mit acht. Den Namen average_difference kannst du innerhalb von main() verwenden; auf der äußersten Dateiebene ist er nicht verfügbar.

price_range berechnet die Kursdifferenz, ta.sma gibt deren gleitenden Durchschnitt zurück. Die blaue Linie zeigt dadurch die durchschnittliche gesamte Kerzenspanne, die orangefarbene die durchschnittliche vorzeichenbehaftete Veränderung zwischen Eröffnung und Schluss. Beide Aufrufe laufen auf jeder Kerze, sodass beide Durchschnitte fortlaufend berechnet werden.

Starte das geänderte Programm und öffne über das Zahnrad Inputs. Ändere Average length von 5 auf 20 und klicke auf Save. Der geöffnete Chart wird automatisch neu berechnet, und beide Linien wechseln zu einem Durchschnitt über zwanzig Kerzen. Dafür musstest du die beiden Aufrufe der inneren Funktion nicht ändern. Mit Länge 1 erhältst du die beiden ursprünglichen Linien zurück.

Dieser Zugriff auf äußere Variablen heißt Closure. Einen Wert lesen zu können bedeutet nicht, dass du ihn durch eine innere Zuweisung automatisch auch überschreiben kannst. In diesem Beispiel lesen wir length nur.

Bei der Variante mit Parametern erkennst du alle übergebenen Werte am Aufruf und kannst dieselbe Funktion leicht mit einem anderen Kurspaar verwenden. Die innere Funktion ist praktisch, wenn mehrere Berechnungen auf denselben Variablen aus main() aufbauen.

Auch eine eigene Funktion kann Zustand haben

price_range rechnet ausschließlich mit den übergebenen Werten. Sie hat keine zwischen Kerzen gespeicherte Variable und verwendet keine Historie. Deshalb kannst du sie jederzeit mit zwei Werten aufrufen.

Die obige Funktion average_difference verwendet dagegen Zustand, weil das enthaltene ta.sma mit früheren Werten rechnet. Du kannst in einer eigenen Funktion auch direkt Variablen mit Persistent oder Series anlegen.

Eine Funktion zum Zählen von Ereignissen

Zählen wir steigende und fallende Kerzen getrennt. Dafür schreiben wir eine Funktion, die einen logischen Wert erhält und sich merkt, wie oft sie bisher wahr erhalten hat.

Importiere am Dateianfang Persistent:

from pynecore import Persistent

Schreibe die folgende Funktion vor main() auf die äußerste Dateiebene:

def count_events(event: bool) -> int:
    count: Persistent[int] = 0
    if event:
        count += 1
    return count

count beginnt bei null. Ist event wahr, steigt der Wert um eins, andernfalls bleibt er erhalten. return gibt in beiden Fällen die bisherige Summe zurück. Nach Ende des Funktionsaufrufs bleibt der Wert der Persistent-Variablen für den nächsten Aufruf erhalten.

Behalte overlay=False bei. Ersetze die bisherige Definition und den Rumpf von main() durch:

def main():
    bullish_count = count_events(close > open)
    bearish_count = count_events(close < open)
    plot(bullish_count, "Bullish count", color=color.green)
    plot(bearish_count, "Bearish count", color=color.red)

Wir rufen dieselbe Funktion von zwei verschiedenen Stellen im Code auf. Beide Aufrufstellen haben einen eigenen Zustand count: Die erste zählt steigende, die zweite fallende Kerzen. Der erste Aufruf erhöht nicht den Zähler des zweiten.

Folgen die ersten vier Kerzen beispielsweise in dieser Reihenfolge, erhältst du folgende Werte:

KerzeVerhältnis von Eröffnungs- und Schlusskursbullish_countbearish_count
ErsteSchlusskurs höher10
ZweiteSchlusskurs niedriger11
DritteBeide Kurse gleich11
VierteSchlusskurs höher21

Führe das Programm auf den Demodaten aus. Die grüne Linie steigt nur auf steigenden Kerzen um eins, die rote nur auf fallenden. Ein neuer Lauf baut beide Zähler von null auf.

Beide Funktionsaufrufe laufen auf jeder Kerze; die Bedingung für das Erhöhen steht in der Funktion. Bei Funktionen mit Zustand gehören Reihenfolge und Häufigkeit der Aufrufe zur Funktionsweise. Rufst du eine Funktion nur auf einigen Kerzen auf, entwickelt sich ihre Historie auch nur bei diesen Aufrufen weiter.

Brauchst du einen einzigen gemeinsamen Zähler, rufe die Funktion einmal auf und verwende die Ergebnisvariable an mehreren Stellen. Eine zweite Aufrufstelle bedeutet einen weiteren getrennten Zähler.

Mehrere Anweisungen in einer Funktion

Brauchst du ein Zwischenergebnis, kannst du es vor return berechnen:

def midpoint(top: float, bottom: float) -> float:
    total = top + bottom
    return total / 2

total ist eine lokale Variable. Am Ende des Aufrufs wird sie nicht zu einer Variablen von main(). Wir geben nur das Ergebnis der Funktion zurück.

Probiere es selbst aus

Ergänze eine Funktion midpoint und erstelle einen Overlay-Indikator, der die Mittelkurse der Kerzen zeichnet. Verwende die Parameter top und bottom der Funktion für Hoch und Tief.

Lösung der Aufgabe

Schreibe die obige Definition von midpoint vor main(). Im Indikatordekorator muss overlay=True stehen, im Rumpf:

center = midpoint(high, low)
plot(center, "Midpoint", color=color.blue, linewidth=2)

Du kannst die Berechnung und Darstellung der beiden bisherigen Linien aus dieser Variante entfernen. Entferne auch ungenutzte Imports.

Erkläre den Aufruf

Erkläre an price_range(close, open), welcher Wert an welchen Parameter geht, was in der Zeile mit return passiert und wo main() das Ergebnis erhält. Diese drei Schritte kannst du auch bei längeren Funktionsaufrufen verfolgen.