Im Abschlussprojekt kommen die bisher gelernten Teile in einer Datei zusammen: gleitende Durchschnitte, Inputs, Bedingungen, eine eigene Funktion, Orders und Darstellung. Ziel ist ein Programm, dessen Entscheidungen du auf Daten und beschriebene Regeln zurückführen kannst.

Beschreibe das Programm ohne Code

Die Strategie beobachtet die Kreuzungen zweier EMA-Linien. Mit einem dritten, längeren SMA prüft sie außerdem, ob der Schlusskurs über dem Durchschnitt liegt.

Für einen Long-Einstieg muss der schnelle EMA den langsamen von unten nach oben kreuzen und der Schlusskurs über dem SMA-Trendfilter liegen. Die Längen müssen in der Reihenfolge schnell < langsam < Trend stehen. Erfüllen die angegebenen Längen das nicht, erteilt das Programm keine Einstiegsorder.

Bei einer EMA-Abwärtskreuzung erhält die Long-Position eine Market-Order zur Schließung. Zum Einstieg gehören außerdem ein Stop und ein Ziel in Ticks. Die Simulation verwendet eine feste Menge von einer Einheit, 0,1 Prozent Provision und zwei Ticks Slippage.

Das sind die Regeln der Aufgabe. Daraus leiten wir nicht ab, dass das Programm auf irgendeinem Markt profitabel wäre. Zur Prüfung verwenden wir die festen Demodaten des Buchs.

Das vollständige Programm

Lege die Datei final_strategy.py an:

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


def lengths_are_ordered(fast: int, slow: int, trend: int) -> bool:
    return fast < slow and slow < trend


@script.strategy(
    "Filtered Crossing Strategy",
    overlay=True,
    initial_capital=10000,
    default_qty_type=strategy.fixed,
    default_qty_value=1,
    pyramiding=1,
    commission_type=strategy.commission.percent,
    commission_value=0.1,
    slippage=2,
)
def main(
    fast_length: int = input.int(9, title="Fast EMA length", minval=1, maxval=200),
    slow_length: int = input.int(21, title="Slow EMA length", minval=1, maxval=200),
    trend_length: int = input.int(50, title="Trend SMA length", minval=1, maxval=200),
    use_trend_filter: bool = input.bool(True, title="Use trend filter"),
    stop_ticks: int = input.int(100, title="Stop distance (ticks)", minval=1),
    target_ticks: int = input.int(200, title="Target distance (ticks)", minval=1),
):
    fast_average = ta.ema(close, fast_length)
    slow_average = ta.ema(close, slow_length)
    trend_average = ta.sma(close, trend_length)
    crosses_up = ta.crossover(fast_average, slow_average)
    crosses_down = ta.crossunder(fast_average, slow_average)
    valid_lengths = lengths_are_ordered(fast_length, slow_length, trend_length)
    trend_passes = not use_trend_filter or close > trend_average
    entry_signal = valid_lengths and crosses_up and trend_passes
    if entry_signal:
        strategy.entry("Long", strategy.long)
    if crosses_down:
        strategy.close("Long")
    strategy.exit("Bracket", "Long", loss=stop_ticks, profit=target_ticks)
    plot(fast_average, "Fast EMA", color=color.blue, linewidth=2)
    plot(slow_average, "Slow EMA", color=color.orange, linewidth=2)
    plot(trend_average, "Trend SMA", color=color.gray, linewidth=2)
    plotshape(entry_signal, "Entry signal", style=shape.triangleup,
              location=location.belowbar, color=color.green)

Speichere sie und führe sie auf den Demodaten aus. Im Chart erscheinen drei Durchschnitte und grüne Dreiecke für die Einstiegsbedingung. Die tatsächlichen Ein- und Ausstiege zeigen die Trade-Markierungen der Strategie.

Der Strategiechart von final_strategy.py zeigt schnellen und langsamen EMA, den SMA-Trendfilter, Einstiegssignale und ausgeführte Trades.
Das Abschlussprojekt verwendet drei Durchschnitte, einen schaltbaren Filter und Ausstiegsregeln zu den Einstiegen.

Was wird auf jeder Kerze berechnet?

Die drei Durchschnitte und beide Kreuzungen laufen vor den Bedingungen. Wir berechnen sie auch dann, wenn der Filter später einen Einstieg verhindert. So entwickelt sich ihr Berechnungszustand über die gesamte Kerzenfolge.

Die eigene Funktion lengths_are_ordered prüft drei Zahlen und gibt einen logischen Wert zurück. Sie hat keinen eigenen Kerzenzustand. valid_lengths hält das aktuelle Prüfergebnis für die weiteren Ausdrücke bereit.

trend_passes ist wahr, wenn der Filter ausgeschaltet ist oder der Schlusskurs über dem SMA liegt. input.bool stellt eine schaltbare logische Eingabe bereit. not use_trend_filter ist bei ausgeschaltetem Filter wahr; die zweite Bedingung wird dann nicht benötigt.

Auch bei ausgeschaltetem Filter berechnen und zeichnen wir den Durchschnitt weiterhin. Der Schalter verändert die Einstiegsprüfung, nicht die Anzeige der Linie.

Ungültige Beziehungen zwischen den Längen

Die einzelnen Inputgrenzen legen nur fest, dass jede Länge zwischen 1 und 200 liegen muss. Du kannst also beispielsweise 30 als schnelle und 10 als langsame Länge eingeben. Das verletzt die Reihenfolge der Aufgabe.

Deshalb prüft die Kontrollfunktion diese Beziehung gesondert. Bei falscher Reihenfolge werden die Linien weiterhin gezeichnet, aber entry_signal kann nicht wahr sein. Das Programm erteilt so keine Einstiege mit einer von der Aufgabe abweichenden Längenbeziehung.

Im Debugger kannst du das direkt an valid_lengths prüfen. Bei ungeeigneten gespeicherten Inputs bleibt die Bedingung während des gesamten Laufs falsch.

Ein Signal wird nicht immer zu einem neuen Trade

Das grüne Dreieck markiert die Einstiegsbedingung. Die Strategie gibt die Order an den Simulator. Für ihre Ausführung können weitere Voraussetzungen gelten: etwa eine nächste Kerze, ausreichendes Kapital und eine erlaubte Menge.

Wegen pyramiding=1 lässt der Simulator bei einer bereits offenen Long-Position einen weiteren Long-Einstieg unter Umständen nicht zu. Setze deshalb die Anzahl von Signalen und Trades nicht automatisch gleich.

Am Anfang der Daten fehlt der SMA-Trendfilter mit Länge fünfzig noch. Bei eingeschaltetem Filter können wir dann nicht feststellen, dass der Kurs darüber liegt. Die Einstiegsbedingung ist in diesem Teil deshalb nicht erfüllt.

Prüfe die Funktionsweise

Arbeite zuerst mit den Standardwerten und prüfe unter Properties Menge, Kosten und Ausführungsmodus. Ein früher gespeicherter Wert kann die Einstellung aus dem Quellcode überschreiben.

Suche ein grünes Signal. Untersuche im Debugger oder über die Chartwerte beide EMA-Linien auf der aktuellen und vorherigen Kerze. Prüfe die Kreuzung des schnellen und langsamen Durchschnitts, den SMA-Filter und die Längenbeziehung. Alle drei Teile müssen wahr sein.

Suche anschließend den zugehörigen Einstieg in der Tabelle Trades. Prüfe, auf welcher Kerze und zu welchem Kurs er ausgeführt wurde. Prüfe beim Ausstieg, ob Kreuzung, Stop oder Kursziel die Schließung erklären.

Probiere vorhersehbare Fälle aus

Verwende zur Funktionsprüfung Änderungen, deren Ergebnis du vorhersagen kannst:

ÄnderungWas solltest du prüfen?
Schnelle und langsame Länge gleichKeine neuen Einstiegssignale wegen der Längenprüfung
Trendfilter ausgeschaltetDie EMA-Kreuzung braucht keinen Kurs über dem SMA
Stop- und Zielabstand vergrößertEin anderer Abstand kann zu einer anderen Ausstiegskerze führen
Menge verändertDasselbe Kurssignal kann einen anderen Geldbetrag ergeben

Ändere pro Lauf nur einen solchen Punkt. Notiere vor jedem Versuch deine Erwartung und danach das tatsächliche Ergebnis. Stimmen beide nicht überein, suche im Debugger die erste abweichende Entscheidung.

Ergebnisse vergleichen

Halte beim Vergleich mit ein- und ausgeschaltetem Filter Daten, Menge, Kosten und Längen gleich. Betrachte die Kapitalkurve unter Performance zusammen mit Trades. Wenige Trades oder eine offene letzte Position beeinflussen auch die Zusammenfassung.

Bei echten historischen Daten legst du den Zeitraum für die Einstellung und den getrennten Prüfzeitraum fest, bevor du Ergebnisse ansiehst. Änderst du nach dem Betrachten des Prüfzeitraums Parameter, wird auch dieser Zeitraum Teil der Einstellung.

Am Anfang der Eingabedaten müssen genügend Kerzen für die Aufwärmphase stehen. Führst du einen ausgeschnittenen Zeitraum separat aus, baust du Durchschnitte und Positionszustand neu auf. Der Anfang dieses Laufs kann deshalb von den Ergebnissen desselben Zeitraums inmitten einer längeren Datenreihe abweichen.

Probiere es selbst aus

Ergänze den Schalter Use crossings exit. Eingeschaltet soll die Schließung bei einer Abwärtskreuzung erhalten bleiben; ausgeschaltet sollen nur Stop und Ziel schließen. Berechne die Abwärtskreuzung in beiden Fällen.

Lösung der Aufgabe

Ergänze in der Parameterliste von main():

use_crossing_exit: bool = input.bool(True, title="Use crossings exit"),

Der Schließungszweig:

if use_crossing_exit and crosses_down:
    strategy.close("Long")

Die Zeile crosses_down = ta.crossunder(...) bleibt vor den Bedingungen. Der Aufruf von strategy.exit bleibt bei beiden Schalterstellungen erhalten.

Bei ausgeschalteter Kreuzungsschließung kann die Position auch nach einer EMA-Abwärtskreuzung offen bleiben. Das nächste Einstiegssignal kann trotzdem erscheinen, obwohl wegen der vorhandenen Position kein neuer Trade eröffnet wird.

Erstelle eine eigene Variante

Kopiere das Programm unter einem neuen Namen und beschreibe eine einzelne neue Regel, die du ergänzen möchtest. Formuliere, aus welchen Daten sie auf welcher Kerze entscheidet, wie sie sich von Hand prüfen lässt und welcher Input sie verändert.

Berechne die neue Regel zunächst als eigene Variable. Zeichne sie oder untersuche sie im Debugger, bevor du sie zur Ein- oder Ausstiegsbedingung machst. So kannst du einen Fehler in der Berechnung suchen, bevor du versuchst, das Ergebnis der gesamten Strategie zu erklären.

Quellcode, gespeicherte Einstellungen und verwendete Daten beschreiben gemeinsam dein Experiment. Wenn du sie aufbewahrst, lässt sich der Lauf erneut erzeugen und prüfen.