English Русский 中文 Español 日本語 Português
preview
Schneller Handelsstrategie-Tester in Python mit Numba

Schneller Handelsstrategie-Tester in Python mit Numba

MetaTrader 5Tester |
111 53
Maxim Dmitrievsky
Maxim Dmitrievsky

Warum ein schneller Tester von Nurtzerstrategien wichtig ist

Bei der Entwicklung von Handelsalgorithmen, die auf maschinellem Lernen basieren, ist es wichtig, die Ergebnisse ihres Handels in der Vergangenheit korrekt und schnell zu bewerten. Wenn wir den seltenen Einsatz des Testers in großen Zeitintervallen und mit einer geringen Historientiefe berücksichtigen, dann ist der Tester in Python durchaus geeignet. Wenn die Aufgabe jedoch mehrere Tests und hochfrequente Strategien beinhaltet, kann eine interpretierte Sprache zu langsam sein.

Angenommen, wir sind mit der Ausführungsgeschwindigkeit einiger Skripte nicht zufrieden, wollen aber unsere vertraute Python-Entwicklungsumgebung nicht aufgeben. An dieser Stelle kommt Numba zur Hilfe, das es uns ermöglicht, nativen Python-Code in schnellen Maschinencode zu konvertieren und zu kompilieren. Die Ausführungsgeschwindigkeit eines solchen Codes wird vergleichbar mit der Ausführungsgeschwindigkeit von Code in Programmiersprachen wie C und FORTRAN.


Kurzbeschreibung der Numba-Bibliothek

Numba ist eine Bibliothek für die Programmiersprache Python, die dazu dient, die Ausführung von Code zu beschleunigen, indem Funktionen auf Bytecode-Ebene mithilfe der JIT-Kompilierung (Just-In-Time) in Maschinencode übersetzt werden. Diese Technologie kann die Rechenleistung erheblich verbessern, insbesondere bei wissenschaftlichen Anwendungen, die häufig Schleifen und komplexe mathematische Operationen verwenden. Die Bibliothek unterstützt die Arbeit mit NumPy-Arrays und ermöglicht auch eine effiziente Arbeit mit Parallelität und GPU-Computing. 

Die gebräuchlichste Art, Numba zu verwenden, besteht darin, seine Sammlung von Dekoratoren auf Python-Funktionen anzuwenden, um Numba anzuweisen, sie zu kompilieren. Wenn eine mit Numba dekorierte Funktion aufgerufen wird, wird sie gerade rechtzeitig in Maschinencode kompiliert, sodass der gesamte oder ein Teil des Codes mit der Geschwindigkeit des nativen Maschinencodes ausgeführt werden kann.

Die folgenden Architekturen werden derzeit unterstützt:

  • OS: Windows (64 Bit), OSX, Linux (64 Bit).

  • Architektur: x86, x86_64, ppc64le, armv8l (aarch64), M1/Arm64.

  • GPUs: Nvidia CUDA.

  • CPython

  • NumPy 1.22 - 1.26

Es ist zu bedenken, dass das Pandas-Paket von der Numba-Bibliothek nicht unterstützt wird und die Arbeit mit Dataframes mit der gleichen Geschwindigkeit erfolgt. 


Handhabung des Codes aus diesem Artikel

Damit alles auf Anhieb funktioniert, sollten Sie die folgenden vorbereitenden Schritte durchführen:

  • Installieren Sie alle erforderlichen Pakete.

pip install numpy
pyp install pandas
pip install catboost
pip install scikit-learn
pip install scipy
    • Laden Sie die EURGBP_H1.csv-Daten herunter und legen Sie sie im Ordner „Files“ ab.
    • Laden Sie alle Python-Skripte herunter und legen Sie sie in einem Ordner ab.
    • Bearbeiten Sie die erste Zeichenkette von Tester_ML.py, sodass er wie folgt aussieht: from tester_lib import test_model.
    • Geben Sie den Pfad zur Datei im Skript Tester_ML.py an.
    • p = pd.read_csv('C:/Program Files/MetaTrader 5/MQL5/Files/'EURGBP_H1'.csv', sep='\s+').


    Wie nutzt man das Numba-Paket?

    Im Allgemeinen läuft die Verwendung des Numba-Pakets darauf hinaus, es zu installieren

    pip install numba
    conda install numba
    

    und die Anwendung des Dekorators vor der Funktion, die wir beschleunigen wollen, zum Beispiel:

    @jit(nopython=True)
    def process_data(*args):
            ...
    
    

    Der Dekorateur wird auf zwei verschiedene Arten aufgerufen. 

    1. Nopython-Modus
    2. Objektmodus

    Die erste Möglichkeit besteht darin, die dekorierte Funktion so zu kompilieren, dass sie vollständig ohne Beteiligung des Python-Interpreters ausgeführt wird. Dies ist die schnellste Methode und wird für die Verwendung empfohlen. Numba hat jedoch Einschränkungen, da es nur die in Python eingebauten Operationen und Numpy-Array-Operationen kompilieren kann. Wenn eine Funktion Objekte aus anderen Bibliotheken, wie z. B. Pandas, enthält, kann Numba sie nicht kompilieren und der Code wird vom Interpreter ausgeführt.

    Numba kann den Objektmodus verwenden, um Einschränkungen bei der Verwendung von Bibliotheken Dritter zu umgehen. In diesem Modus kompiliert Numba die Funktion unter der Annahme, dass alles ein Python-Objekt ist, und führt den Code im Wesentlichen im Interpreter aus.

    @jit(forceobj=true, looplift=True)

    kann die Leistung im Vergleich zum reinen Objektmodus verbessern, da Numba versucht, Schleifen in Funktionen zu kompilieren, die im Maschinencode ausgeführt werden, und den Rest des Codes im Interpreter ausführt. Um die beste Leistung zu erzielen, sollten Sie den Objektmodus ganz vermeiden!

    Das Paket unterstützt auch parallele Berechnungen, wenn dies möglich ist (Parallel=True). Bitte beachten Sie, dass beim ersten Aufruf einer Funktion diese in Maschinencode kompiliert wird, was einige Zeit in Anspruch nimmt. Dieser Code wird dann zwischengespeichert, sodass nachfolgende Aufrufe schneller erfolgen.


    Beispiel für die Beschleunigung der Funktion von deal markup

    Bevor wir mit der Beschleunigung des Testers beginnen, sollten wir etwas Einfacheres ausprobieren. Ein hervorragender Kandidat für diese Rolle ist die Funktion von deal markup. Diese Funktion nimmt einen Datenrahmen mit Kursen und kennzeichnet die Abschlüsse als Kauf und Verkauf (0 und 1). Solche Funktionen werden häufig verwendet, um Daten vorab zu kennzeichnen, damit später ein Klassifikator trainiert werden kann.

    def get_labels(dataset, min = 1, max = 15) -> pd.DataFrame:
        labels = []
        for i in range(dataset.shape[0]-max):
            rand = random.randint(min, max)
            curr_pr = dataset['close'].iloc[i]
            future_pr = dataset['close'].iloc[i + rand]
    
            if (future_pr + hyper_params['markup']) < curr_pr:
                labels.append(1.0)
            elif (future_pr - hyper_params['markup']) > curr_pr:
                labels.append(0.0)
            else:
                labels.append(2.0)
            
        dataset = dataset.iloc[:len(labels)].copy()
        dataset['labels'] = labels
        dataset = dataset.dropna()
        dataset = dataset.drop(
            dataset[dataset.labels == 2.0].index)
        return dataset
    

    Wir verwenden als Daten die minütlichen Schlusskurse des EURGBP für 15 Jahre:

    >>> pr = get_prices()
    >>> pr
                           close
    time                        
    2010-01-04 00:00:00  0.88810
    2010-01-04 00:01:00  0.88799
    2010-01-04 00:02:00  0.88786
    2010-01-04 00:03:00  0.88792
    2010-01-04 00:04:00  0.88802
    ...                      ...
    2024-10-09 19:03:00  0.83723
    2024-10-09 19:04:00  0.83720
    2024-10-09 19:05:00  0.83704
    2024-10-09 19:06:00  0.83702
    2024-10-09 19:07:00  0.83703
    
    [5480021 rows x 1 columns]
    

    Der Datensatz enthält mehr als fünf Millionen Beobachtungen, was für Tests völlig ausreichend ist.

    Lassen Sie uns nun die Ausführungsgeschwindigkeit dieser Funktion für unsere Daten messen:

    # get labels test
    start_time = time.time()
    pr = get_labels(pr)
    pr['meta_labels'] = 1.0
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time:.4f} seconds")
    

    Die Ausführungszeit betrug 74,1843 Sekunden.

    Versuchen wir nun, diese Funktion mit Hilfe des Numba-Pakets zu beschleunigen. Wir können sehen, dass die ursprüngliche Funktion auch das Pandas-Paket verwendet, und wir wissen, dass diese beiden Pakete nicht kompatibel sind. Verschieben wir alles, was mit Pandas zu tun hat, in eine separate Funktion und beschleunigen wir den Rest des Codes.

    @jit(nopython=True)
    def get_labels_numba(close_prices, min_val, max_val, markup):
        labels = np.empty(len(close_prices) - max_val, dtype=np.float64)
        for i in range(len(close_prices) - max_val):
            rand = np.random.randint(min_val, max_val + 1)
            curr_pr = close_prices[i]
            future_pr = close_prices[i + rand]
    
            if (future_pr + markup) < curr_pr:
                labels[i] = 1.0
            elif (future_pr - markup) > curr_pr:
                labels[i] = 0.0
            else:
                labels[i] = 2.0
    
        return labels
    
    def get_labels_fast(dataset, min_val=1, max_val=15):
        close_prices = dataset['close'].values
        markup = hyper_params['markup']
    
        labels = get_labels_numba(close_prices, min_val, max_val, markup)
    
        dataset = dataset.iloc[:len(labels)].copy()
        dataset['labels'] = labels
        dataset = dataset.dropna()
        dataset = dataset.drop(dataset[dataset.labels == 2.0].index)
    
        return dataset
    

    Der ersten Funktion ist ein Aufruf des Dekorators @jit vorangestellt. Dies bedeutet, dass diese Funktion in Bytecode kompiliert wird. Außerdem werden wir darin Pandas los und verwenden nur Listen, Schleifen und Numpy.

    Die zweite Funktion ist die vorbereitende Arbeit. Sie konvertiert den Pandas-Datenrahmen in ein Numpy-Array und übergibt es dann an die erste Funktion. Danach wird das Ergebnis genommen und der Pandas-Datenrahmen wieder zurückgegeben. Auf diese Weise wird die Hauptberechnung von markup beschleunigt.

    Nun wollen wir die Geschwindigkeit messen. Die Berechnungszeit wurde auf 12 Sekunden reduziert! Bei dieser Funktion konnten wir eine mehr als 5-fache Beschleunigung erzielen. Natürlich ist dies kein völlig sauberer Test, da die Pandas-Bibliothek immer noch für Zwischenberechnungen verwendet wird, aber es wurde eine erhebliche Beschleunigung bei der Berechnung der Kennzeichnungen erreicht.


    Beschleunigung des Strategieprüfers für Aufgaben des maschinellen Lernens

    Ich habe den Strategietester in eine separate Bibliothek verschoben, die Sie im Anhang unten finden können. Es enthält die Funktionen „tester“ und „slow_tester“ zum Vergleich.

    Der Leser könnte einwenden, dass die meisten Geschwindigkeitssteigerungen in Python aus der Vektorisierung stammen. Das ist richtig, aber manchmal müssen wir trotzdem Schleifen verwenden. So verfügt der Tester beispielsweise über eine recht komplexe Schleife, um die gesamte Historie zu durchlaufen und den Gesamtgewinn unter Berücksichtigung von Stop-Loss und Take-Profit zu kumulieren. Dies durch Vektorisierung zu realisieren, scheint keine einfache Aufgabe zu sein.

    Der Hauptteil der Testschleife (der Teil, der am längsten für die Ausführung benötigt) ist unten zu Referenzzwecken dargestellt.

    for i in range(dataset.shape[0]):
            line_f = len(report) if i <= forw else line_f
            line_b = len(report) if i <= backw else line_b
            
            pred = labels[i]
            pr = close[i]
            pred_meta = metalabels[i]  # 1 = allow trades
    
            if last_deal == 2 and pred_meta == 1:
                last_price = pr
                last_deal = 0 if pred < 0.5 else 1
                continue
            
            if last_deal == 0:
                if (-markup + (pr - last_price) >= take) or (-markup + (last_price - pr) >= stop):
                    last_deal = 2
                    profit = -markup + (pr - last_price)
                    report.append(report[-1] + profit)
                    chart.append(chart[-1] + profit)
                    continue
    
            if last_deal == 1:
                if (-markup + (pr - last_price) >= stop) or (-markup + (last_price - pr) >= take):
                    last_deal = 2
                    profit = -markup + (last_price - pr)
                    report.append(report[-1] + profit)
                    chart.append(chart[-1] + (pr - last_price))
                    continue
            
            # close deals by signals
            if last_deal == 0 and pred > 0.5 and pred_meta == 1:
                last_deal = 2
                profit = -markup + (pr - last_price)
                report.append(report[-1] + profit)
                chart.append(chart[-1] + profit)
                continue
    
            if last_deal == 1 and pred < 0.5 and pred_meta == 1:
                last_deal = 2
                profit = -markup + (last_price - pr)
                report.append(report[-1] + profit)
                chart.append(chart[-1] + (pr - last_price))
                continue
    

    Messen wir die Testgeschwindigkeit mit den Daten, die wir zuvor erhalten haben. Schauen wir uns zunächst die Geschwindigkeit des langsamen Tests an:

    # native python tester test
    start_time = time.time()
    tester_slow(pr, 
           hyper_params['stop_loss'], 
           hyper_params['take_profit'], 
           hyper_params['markup'],
           hyper_params['forward'],
           False)
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time:.4f} seconds")
    
    Execution time: 6.8639 seconds

    Es sieht nicht sehr langsam aus, man könnte sogar sagen, dass der Interpreter den Code recht schnell ausführt.

    Lassen Sie uns die Testfunktion wieder in zwei Funktionen aufteilen. Eine davon dient als Hilfsrechner, die zweite führt die Hauptberechnungen durch.

    Die Funktion „process data“ implementiert die Hauptschleife des Testers, die beschleunigt werden sollte, da Schleifen in Python langsam sind. Gleichzeitig bereitet die Funktion „Tester“ selbst zunächst die Daten für die Funktion „Daten verarbeiten“ vor, nimmt dann das Ergebnis entgegen und zeichnet das Diagramm.

    @jit(nopython=True)
    def process_data(close, labels, metalabels, stop, take, markup, forward, backward):
        last_deal = 2
        last_price = 0.0
        report = [0.0]
        chart = [0.0]
        line_f = 0
        line_b = 0
    
        for i in range(len(close)):
            line_f = len(report) if i <= forward else line_f
            line_b = len(report) if i <= backward else line_b
            
            pred = labels[i]
            pr = close[i]
            pred_meta = metalabels[i]  # 1 = allow trades
    
            if last_deal == 2 and pred_meta == 1:
                last_price = pr
                last_deal = 0 if pred < 0.5 else 1
                continue
            
            if last_deal == 0:
                if (-markup + (pr - last_price) >= take) or (-markup + (last_price - pr) >= stop):
                    last_deal = 2
                    profit = -markup + (pr - last_price)
                    report.append(report[-1] + profit)
                    chart.append(chart[-1] + profit)
                    continue
    
            if last_deal == 1:
                if (-markup + (pr - last_price) >= stop) or (-markup + (last_price - pr) >= take):
                    last_deal = 2
                    profit = -markup + (last_price - pr)
                    report.append(report[-1] + profit)
                    chart.append(chart[-1] + (pr - last_price))
                    continue
            
            # close deals by signals
            if last_deal == 0 and pred > 0.5 and pred_meta == 1:
                last_deal = 2
                profit = -markup + (pr - last_price)
                report.append(report[-1] + profit)
                chart.append(chart[-1] + profit)
                continue
    
            if last_deal == 1 and pred < 0.5 and pred_meta == 1:
                last_deal = 2
                profit = -markup + (last_price - pr)
                report.append(report[-1] + profit)
                chart.append(chart[-1] + (pr - last_price))
                continue
    
        return np.array(report), np.array(chart), line_f, line_b
    
    def tester(*args):
        '''
        This is a fast strategy tester based on numba
        List of parameters:
    
        dataset: must contain first column as 'close' and last columns with "labels" and "meta_labels"
    
        stop: stop loss value
    
        take: take profit value
    
        forward: forward time interval
    
        backward: backward time interval
    
        markup: markup value
    
        plot: false/true
        '''
        dataset, stop, take, forward, backward, markup, plot = args
    
        forw = dataset.index.get_indexer([forward], method='nearest')[0]
        backw = dataset.index.get_indexer([backward], method='nearest')[0]
    
        close = dataset['close'].to_numpy()
        labels = dataset['labels'].to_numpy()
        metalabels = dataset['meta_labels'].to_numpy()
        
        report, chart, line_f, line_b = process_data(close, labels, metalabels, stop, take, markup, forw, backw)
    
        y = report.reshape(-1, 1)
        X = np.arange(len(report)).reshape(-1, 1)
        lr = LinearRegression()
        lr.fit(X, y)
    
        l = 1 if lr.coef_[0][0] >= 0 else -1
    
        if plot:
            plt.plot(report)
            plt.plot(chart)
            plt.axvline(x=line_f, color='purple', ls=':', lw=1, label='OOS')
            plt.axvline(x=line_b, color='red', ls=':', lw=1, label='OOS2')
            plt.plot(lr.predict(X))
            plt.title("Strategy performance R^2 " + str(format(lr.score(X, y) * l, ".2f")))
            plt.xlabel("the number of trades")
            plt.ylabel("cumulative profit in pips")
            plt.show()
    
        return lr.score(X, y) * l
    
    

    Nun wollen wir den Numba-beschleunigten Strategietester testen:

    start_time = time.time()
    tester(pr, 
           hyper_params['stop_loss'], 
           hyper_params['take_profit'], 
           hyper_params['forward'],
           hyper_params['backward'],
           hyper_params['markup'],
           False)
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time:.4f} seconds")
    
    Execution time: 0.1470 seconds

    Die beobachtete Geschwindigkeitssteigerung beträgt fast das 50-fache! Mehr als 400.000 Geschäfte wurden abgeschlossen.

    Stellen Sie sich vor, Sie würden 1 Stunde pro Tag damit verbringen, Ihre Algorithmen zu testen, und mit dem Schnelltester bräuchten Sie dafür nur eine Minute.


    Testen von Strategien anhand von Tickdaten

    Erschweren wir uns die Aufgabe und laden wir den Tickverlauf der letzten 3 Jahre vom Terminal in eine .csv-Datei herunter.

    Um die Datei korrekt zu lesen, muss die Funktion zum Laden von Zitaten leicht geändert werden. Anstelle von Close werden wir Bid-Preise verwenden. Wir müssen auch Preise mit denselben Indizes entfernen.

    def get_prices() -> pd.DataFrame:
        p = pd.read_csv('files/'+hyper_params['symbol']+'.csv', sep='\s+')
        pFixed = pd.DataFrame(columns=['time', 'close'])
        pFixed['time'] = p['<DATE>'] + ' ' + p['<TIME>']
        pFixed['time'] = pd.to_datetime(pFixed['time'], format='mixed')
        pFixed['close'] = p['<BID>']
        pFixed.set_index('time', inplace=True)
        pFixed.index = pd.to_datetime(pFixed.index, unit='s')
        # Remove duplicate string by 'time' index
        pFixed = pFixed[~pFixed.index.duplicated(keep='first')]
        return pFixed.dropna()
    

    Das Ergebnis waren fast 62 Millionen Beobachtungen. Das Testprogramm akzeptiert Preise mit dem Spaltennamen „close“, also wird Bid in Close umbenannt.

    >>> pr
                               close
    time                            
    2022-01-03 00:05:01.753  0.84000
    2022-01-03 00:05:04.032  0.83892
    2022-01-03 00:05:05.849  0.83918
    2022-01-03 00:05:07.280  0.83977
    2022-01-03 00:05:07.984  0.83939
    ...                          ...
    2024-11-08 23:58:53.491  0.82982
    2024-11-08 23:58:53.734  0.82983
    2024-11-08 23:58:55.474  0.82982
    2024-11-08 23:58:57.040  0.82984
    2024-11-08 23:58:57.337  0.82982
    
    [61896607 rows x 1 columns]
    

    Führen wir ein kurzes Markup aus und messen wir die Ausführungszeit.

    # get labels test
    start_time = time.time()
    pr = get_labels_fast(pr)
    pr['meta_labels'] = 1.0
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time:.4f} seconds")
    

    Die Zeit von markup betrug 9,5 Sekunden.

    Führen wir nun den Schnelltest durch.

    # numba tester test
    start_time = time.time()
    tester(pr, 
           hyper_params['stop_loss'], 
           hyper_params['take_profit'], 
           hyper_params['forward'],
           hyper_params['backward'],
           hyper_params['markup'],
           True)
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time:.4f} seconds")
    

    Die Testzeit betrug 0,16 Sekunden. Der langsame Tester benötigte dafür 5,5 Sekunden.

    Der schnelle Tester mit Numba erledigte die Aufgabe 35 Mal schneller als der Tester mit reinem Python. Aus der Sicht des Beobachters erfolgt die Prüfung im Falle des schnellen Testers sofort, während die Verwendung des langsamen Testers eine gewisse Wartezeit erfordert. Dennoch muss man dem langsamen Tester ein Lob aussprechen, der ebenfalls gute Arbeit leistet und für das Testen von Strategien auch auf Tick-Daten gut geeignet ist.

    Insgesamt waren es 1e6 oder eine Million Geschäfte.

     


    Informationen zur Verwendung des Schnelltesters für Aufgaben des maschinellen Lernens

    Wenn Sie die vorgeschlagene Testversion tatsächlich verwenden wollen, könnten die folgenden Informationen für Sie nützlich sein.

    Fügen wir unserem Datensatz Merkmale hinzu, damit wir den Klassifikator trainieren können.

    def get_features(data: pd.DataFrame) -> pd.DataFrame:
        pFixed = data.copy()
        pFixedC = data.copy()
        count = 0
        for i in hyper_params['periods']:
            pFixed[str(count)] = pFixedC-pFixedC.rolling(i).mean()
            count += 1
        return pFixed.dropna()
    

    Dabei handelt es sich um einfache Indikatoren, die auf der Differenz zwischen den Preisen und den gleitenden Durchschnitten basieren.

    Als Nächstes erstellen wir ein Wörterbuch mit Modell-Hyperparametern, die für das Training und die Tests verwendet werden sollen. Wir werden sie bei der Erstellung eines neuen Datensatzes anwenden.

    hyper_params = {
        'symbol': 'EURGBP_H1',
        'markup': 0.00010,
        'stop_loss': 0.01000,
        'take_profit': 0.01000,
        'backward': datetime(2010, 1, 1),
        'forward': datetime(2023, 1, 1),
        'periods': [i for i in range(50, 300, 50)],
    }
    
    # catboost learning
    dataset = get_labels_fast(get_features(get_prices()))
    dataset['meta_labels'] = 1.0
    data = dataset[(dataset.index < hyper_params['forward']) & (dataset.index > hyper_params['backward'])].copy()
    

    Dabei ist zu beachten, dass der Tester nicht nur die Werte von „labels“ akzeptiert, sondern auch die von „meta_labels“. Wir brauchen sie vielleicht, wenn wir Filter für unser auf maschinellem Lernen basierendes Handelssystem verwenden wollen. Bei einem Wert von 1 ist der Handel erlaubt, bei einem Wert von 0 ist er untersagt. Da wir in diesem Demo-Beispiel keine Filter verwenden werden, erstellen wir einfach eine zusätzliche Spalte und füllen sie mit Einsen, die den Handel zu jeder Zeit ermöglichen.

    dataset['meta_labels'] = 1.0
    
    

    Jetzt können wir das Modell CatBoost auf dem generierten Datensatz trainieren, nachdem wir zuvor die Daten für die Vorwärts- und Rückwärtstests aus dem Verlauf entfernt haben, damit nicht mit ihnen trainiert wird.

    data = dataset[(dataset.index < hyper_params['forward']) & (dataset.index > hyper_params['backward'])].copy()
    
    X = data[data.columns[1:-2]]
    y = data['labels']
    
    train_X, test_X, train_y, test_y = train_test_split(
            X, y, train_size=0.7, test_size=0.3, shuffle=True)
    
    model = CatBoostClassifier(iterations=500,
                                   thread_count=8,
                                   custom_loss=['Accuracy'],
                                   eval_metric='Accuracy',
                                   verbose=True,
                                   use_best_model=True,
                                   task_type='CPU')
    
    model.fit(train_X, train_y, eval_set=(test_X, test_y),
                early_stopping_rounds=25, plot=False)
    

    Nach dem Training testen wir das Modell mit dem gesamten Datensatz, einschließlich der Testdaten. Die Funktion test_model befindet sich in der Datei tester_lib.py zusammen mit den Funktionen des schnellen und langsamen Testers selbst. Es ist ein Wrapper für den schnellen Tester und hat die Aufgabe, die Vorhersagewerte eines trainierten maschinellen Lernmodells zu ermitteln (in unserem Fall ist es CatBoost, aber es kann auch ein anderes sein).

    def test_model(dataset: pd.DataFrame, 
                   result: list, 
                   stop: float, 
                   take: float, 
                   forward: float, 
                   backward: float, 
                   markup: float, 
                   plt = False):
        
        ext_dataset = dataset.copy()
        X = ext_dataset[dataset.columns[1:-2]]
    
        ext_dataset['labels'] = result[0].predict_proba(X)[:,1]
        # ext_dataset['meta_labels'] = result[1].predict_proba(X)[:,1]
        ext_dataset['labels'] = ext_dataset['labels'].apply(lambda x: 0.0 if x < 0.5 else 1.0)
        # ext_dataset['meta_labels'] = ext_dataset['meta_labels'].apply(lambda x: 0.0 if x < 0.5 else 1.0)
        return tester(ext_dataset, stop, take, forward, backward, markup, plt)
    

    Der obige Code enthält auskommentierte Zeichenketten, die es ermöglichen, Meta-Kennzeichen zu erhalten, die angeben, ob gehandelt werden soll oder nicht. Mit anderen Worten: Das zweite maschinelle Lernmodell kann für diese Zwecke verwendet werden. Wir verwenden sie in diesem Artikel nicht.

    Beginnen wir direkt mit den Tests.

    # test catboost model
    test_model(dataset, 
               [model], 
               hyper_params['stop_loss'],
               hyper_params['take_profit'],
               hyper_params['forward'],
               hyper_params['backward'],
               hyper_params['markup'],
               True)
    

    Und wir erhalten das Ergebnis. Das Modell wurde überangepasst, wie man an den Testdaten rechts von der vertikalen Linie sehen kann. Aber das spielt für uns keine Rolle, denn wir testen ja den Tester.

    Da der Tester die Möglichkeit der Verwendung von Stop-Loss und Take-Profit impliziert und Sie diese vielleicht optimieren wollen, sollten wir die Optimierung nutzen, denn unser Tester ist jetzt sehr schnell!


    Optimierung der Parameter von Handelssystemen durch maschinelles Lernen

    Betrachten wir nun die Möglichkeit der Optimierung von Stop-Loss und Take-Profit. In der Tat ist es möglich, andere Parameter des Handelssystems zu optimieren, wie z.B. die Meta-Kennzeichnungen, aber das würde den Rahmen dieses Artikels sprengen und kann im nächsten Artikel behandelt werden.

    Wir führen zwei Arten der Optimierung durch:

    • Suche über Parameterraster
    • Optimierung mit der Methode L-BFGS-B

    Schauen wir uns zunächst kurz den Code für jede Methode an. Die Methode GRID_SEARCH wird unten angezeigt. 

    Sie nimmt als Argumente:

    • Testdaten
    • trainiertes Modell
    • das Wörterbuch mit den Hyperparametern des oben beschriebenen Algorithmus
    • Testobjekt
    Anschließend werden Bereiche von Parameterwerten erstellt, die in einer Schleife durchlaufen werden. Bei jeder Iteration wird der Tester aufgerufen, und es werden die Parameter ausgewählt, die dem größten R^2 entsprechen.

    # stop loss / take profit grid search
    def optimize_params_GRID_SEARCH(pr, model, hyper_params, test_model_func):
        best_r2 = -np.inf
        best_stop_loss = None
        best_take_profit = None
    
        # Ranges for stop_loss and take_profit
        stop_loss_range = np.arange(0.00100, 0.02001, 0.00100)
        take_profit_range = np.arange(0.00100, 0.02001, 0.00100)
    
        total_iterations = len(stop_loss_range) * len(take_profit_range)
        start_time = time.time()
    
        for stop_loss in stop_loss_range:
            for take_profit in take_profit_range:
                # Create a copy of hyper_params
                current_hyper_params = hyper_params.copy()
                current_hyper_params['stop_loss'] = stop_loss
                current_hyper_params['take_profit'] = take_profit
    
                r2 = test_model_func(pr,
                                     [model],
                                     current_hyper_params['stop_loss'],
                                     current_hyper_params['take_profit'],
                                     current_hyper_params['forward'],
                                     current_hyper_params['backward'],
                                     current_hyper_params['markup'],
                                     False)
    
                if r2 > best_r2:
                    best_r2 = r2
                    best_stop_loss = stop_loss
                    best_take_profit = take_profit
    
        end_time = time.time()
        total_time = end_time - start_time
        average_time_per_iteration = total_time / total_iterations
    
        print(f"Total iterations: {total_iterations}")
        print(f"Average time per iteration: {average_time_per_iteration:.6f} seconds")
        print(f"Total time: {total_time:.6f} seconds")
    
        return best_stop_loss, best_take_profit, best_r2
    

    Schauen wir uns nun den Code der Methode L-BFGS_B an. Nähere Informationen dazu finden Sie hier

    Die Funktionsargumente bleiben dieselben. Es wird jedoch eine Fitnessfunktion erstellt, über die der Strategietester aufgerufen wird. Die Grenzen der Optimierungsparameter und die Anzahl der Initialisierungen (Zufallspunkte des Parametersatzes) für den L-BFGS_B-Algorithmus werden festgelegt. Zufällige Initialisierungen sind notwendig, um zu verhindern, dass der Optimierungsalgorithmus in lokalen Minima stecken bleibt. Danach wird die Minimieren-Funktion aufgerufen, der die Parameter des Optimierers selbst übergeben werden.

    def optimize_params_L_BFGS_B(pr, model, hyper_params, test_model_func):
        def objective(x):
            current_hyper_params = hyper_params.copy()
            current_hyper_params['stop_loss'] = x[0]
            current_hyper_params['take_profit'] = x[1]
            
            r2 = test_model_func(pr,
                                [model],
                                current_hyper_params['stop_loss'],
                                current_hyper_params['take_profit'],
                                current_hyper_params['forward'],
                                current_hyper_params['backward'],
                                current_hyper_params['markup'],
                                False)
            return -r2
    
        bounds = ((0.001, 0.02), (0.001, 0.02))
        
        # Let's try some random starting points
        n_attempts = 50
        best_result = None
        best_fun = float('inf')
        
        start_time = time.time()
        for _ in range(n_attempts):
            # Random starting point
            x0 = np.random.uniform(0.001, 0.02, 2)
            
            result = minimize(
                objective,
                x0,
                method='L-BFGS-B',
                bounds=bounds,
                options={'ftol': 1e-5, 'disp': False, 'maxiter': 100}  # Increase accuracy and number of iterations
            )
            
            if result.fun < best_fun:
                best_fun = result.fun
                best_result = result
        # Get the end time and calculate the total time
        end_time = time.time()
        total_time = end_time - start_time
        print(f"Total time: {total_time:.6f} seconds")
    
        return best_result.x[0], best_result.x[1], -best_result.fun
    
    

    Jetzt können wir beide Optimierungsalgorithmen ausführen und die Ausführungszeit und die Genauigkeit betrachten.

    # using
    best_stop_loss, best_take_profit, best_r2 = optimize_params_GRID_SEARCH(dataset, model, hyper_params, test_model)
    best_stop_loss, best_take_profit, best_r2 = optimize_params_L_BFGS_B(dataset, model, hyper_params, test_model)
    

    Algorithmus für die Gittersuche:

    Total iterations: 400
    Average time per iteration: 0.031341 seconds
    Total time: 12.536394 seconds
    
    Best parameters: stop_loss=0.004, take_profit=0.002, R^2=0.9742298702323458
    

    Der Algorithmus L-BFGS-B:

    Total time: 4.733158 seconds
    
    Best parameters: stop_loss=0.0030492548809269732, take_profit=0.0016816794762543421, R^2=0.9733045271274298
    

    Mit meinen Standardeinstellungen war L-BFGS-B mehr als doppelt so schnell und zeigte Ergebnisse, die mit denen des Gittersuchalgorithmus vergleichbar waren.

    Man kann also beide Algorithmen verwenden und je nach Anzahl und Bereich der zu optimierenden Parameter den besten auswählen. 


    Schlussfolgerung

    Dieser Artikel zeigt die Möglichkeit auf, den Strategietester zu beschleunigen, der zum schnellen Testen von auf maschinellem Lernen basierenden Strategien verwendet werden kann. Numba liefert nachweislich den 50-fachen Geschwindigkeitsschub. Das Testen wird schnell und ermöglicht mehrere Tests und sogar eine Parameteroptimierung. 


    Anhänge:

    • tester_lib.py - Tester-Bibliothek
    • test tester.py - Skript zum Vergleich von langsamen (Python) und schnellen (Numba) Testern
    • tester ticks.py - Skript zum Vergleich von Testern anhand von Tickdaten
    • tester ML.py - Skript für das Training des Klassifikators und die Optimierung der Hyperparameter


    Übersetzt aus dem Russischen von MetaQuotes Ltd.
    Originalartikel: https://www.mql5.com/ru/articles/14895

    Beigefügte Dateien |
    tester_lib.py (6.86 KB)
    test_tester.py (3.43 KB)
    tester_ticks.py (3.38 KB)
    tester_ML.py (6.67 KB)
    Letzte Kommentare | Zur Diskussion im Händlerforum (53)
    Maxim Dmitrievsky
    Maxim Dmitrievsky | 15 Nov. 2024 in 16:49
    ys_mql5 #:

    Nun, die Standardabweichung in einem gleitenden Fenster mit festem Wert wird eine nicht normalisierte Schwankungsbreite haben, die von der Volatilität abhängt. Soweit ich weiß, wird zu diesem Zweck in der Regel der z-Score verwendet, da er ein normalisierter Wert ist. Das ist das Ende der Überlegungen )

    Verstanden, ich nehme Min/Max über die gesamte verfügbare Historie und setze sie als Grenzen, dann unterteile ich sie bei jeder Iteration des Optimierers in zufällige Bereiche. Sie können auch zscore tun. Ich dachte, eine solche Normalisierung könnte besser für den Optimierer sein (loswerden von kleinen Werten mit einer großen Anzahl von Nullen nach dem Komma), aber ich glaube nicht, dass es sein sollte.

    bestvishes
    bestvishes | 16 Nov. 2024 in 05:29
    Hallo Maxime, ich glaube, du bist die klügste Person im Forum, ich hoffe, dass ich im zweiten Artikel eine detaillierte Beschreibung sehen werde. dankbar
    Maxim Dmitrievsky
    Maxim Dmitrievsky | 17 Nov. 2024 in 07:37
    bestvishes #:
    Hallo Maxime, ich glaube, du bist die klügste Person im Forum, ich hoffe, dass ich im zweiten Artikel eine detaillierte Beschreibung sehen werde. danke

    Danke für das schmeichelhafte Feedback, ich werde versuchen, etwas Interessantes für Sie zu schreiben.

    pulsar86
    pulsar86 | 21 Nov. 2024 in 10:30
    def get_prices() -> pd.DataFrame:
    Try:
    # Laden einer kommagetrennten CSV-Datei
    p = pd.read_csv(f"files/{hyper_params['symbol']}.csv" )

    # Auf erforderliche Spalten prüfen
    required_columns = ['time', 'close' ]
    for col in required_columns:
    if col not in p.columns:
    raise KeyError(f"Column'{col}' is missing from the file." )

    # Konvertiere die Spalte 'time' in das Datetime-Format
    p ['time'] = pd.to_datetime(p['time'], errors='coerce' )

    # Setzen des Zeitindexes
    p. set_index('time', inplace=True )

    # Nur die Spalte 'close' belassen und Zeilen mit falschen Daten entfernen
    pFixed = p[[['close']].dropna( )

    return pFixed
    except Exception as e:
    print(f"Fehler bei der Verarbeitung der Daten: {e}" )
    return pd.DataFrame() #Rückgabe eines leeren DataFrame im Falle eines Fehlers
    Maxim Dmitrievsky
    Maxim Dmitrievsky | 6 Dez. 2024 in 01:20

    Ich habe etwas Zeit und bin fast fertig mit dem Modelltraining und der Optimierung der Hyperparameter in einer Flasche.

    Es wird möglich sein, viele Modelle auf einmal zu trainieren, sie dann zu optimieren, dann das beste Modell mit den besten Optimierungsparametern auszuwählen, zum Beispiel:

    models = []
    for i in range(20):
        print(f'Iteration: {i}')
        models.append(learnANDoptimize())
    
    models.sort(key=lambda x: x[0][0]['score'])
    
    
    index = -1
    test_model(models[index][0][0]['dataframe'],
                [models[index][-1]],
                hyper_params['stop_loss'],
                hyper_params['take_profit'],
                hyper_params['forward'],
                hyper_params['backward'],
                hyper_params['markup'],
                True)

    Und das Ergebnis ausgeben.

    Dann kann das Modell mit optimalen Hyperparametern in das Terminal exportiert werden. Oder man verwendet den Terminal-Optimierer selbst.

    Ich werde den Artikel später beginnen, ich habe es nicht vergessen.

    Volumetrische neuronale Netzwerkanalyse als Schlüssel zu zukünftigen Trends Volumetrische neuronale Netzwerkanalyse als Schlüssel zu zukünftigen Trends
    Der Artikel untersucht die Möglichkeit, die Preisprognose auf der Grundlage der Analyse des Handelsvolumens zu verbessern, indem die Prinzipien der technischen Analyse mit der Architektur des neuronalen Netzes LSTM integriert werden. Besonderes Augenmerk wird auf die Erkennung und Interpretation anomaler Volumina, die Verwendung von Clustern und die Erstellung von Merkmalen auf der Grundlage von Volumina und deren Definition im Rahmen des maschinellen Lernens gelegt.
    Der Algorithmus Atomic Orbital Search (AOS) Modifizierung Der Algorithmus Atomic Orbital Search (AOS) Modifizierung
    Im zweiten Teil des Artikels werden wir die Entwicklung einer modifizierten Version des AOS-Algorithmus (Atomic Orbital Search) fortsetzen und uns dabei auf bestimmte Operatoren konzentrieren, um seine Effizienz und Anpassungsfähigkeit zu verbessern. Nach einer Analyse der Grundlagen und der Mechanik des Algorithmus werden wir Ideen zur Verbesserung seiner Leistung und seiner Fähigkeit, komplexe Lösungsräume zu analysieren, diskutieren und neue Ansätze zur Erweiterung seiner Funktionalität als Optimierungswerkzeug vorschlagen.
    Arithmetischer Optimierungsalgorithmus (AOA): Von AOA zu SOA (Simpler Optimierungsalgorithmus) Arithmetischer Optimierungsalgorithmus (AOA): Von AOA zu SOA (Simpler Optimierungsalgorithmus)
    In diesem Artikel stellen wir den Arithmetischen Optimierungsalgorithmus (AOA) vor, der auf einfachen arithmetischen Operationen basiert: Addition, Subtraktion, Multiplikation und Division. Diese grundlegenden mathematischen Operationen dienen als Grundlage für die Suche nach optimalen Lösungen für verschiedene Probleme.
    Die Verwendung von Assoziationsregeln in der Forex-Datenanalyse Die Verwendung von Assoziationsregeln in der Forex-Datenanalyse
    Wie lassen sich die Vorhersageregeln der Supermarkt-Einzelhandelsanalyse auf den realen Devisenmarkt anwenden? Wie hängt der Kauf von Keksen, Milch und Brot mit Börsentransaktionen zusammen? Der Artikel behandelt einen innovativen Ansatz für den algorithmischen Handel, der auf der Verwendung von Assoziationsregeln beruht.