В этой статье я попытаюсь воспроизвести предложенную структуру машинного обучения, используемую для прогнозирования движения цен на акции (Lin et al 2021). Эта структура использует технический анализ, пользовательские модели K-Line и исторические данные о ценах для прогнозного моделирования.

Код и модели доступны в этом репозитории GitHub.

Краткое содержание

Цель этого проекта — воспроизвести несколько алгоритмических торговых стратегий на основе машинного обучения, описанных в рецензируемых публикациях. В частности, я сосредоточился на анализе и реконструкции подхода, предложенного Lin et al. (2021) для предсказания тренда акций с использованием графика свечей. Для этого я использовал аналогичные технические индикаторы и стандартные модели машинного обучения, такие как Random Forest, Support Vector Machines, K-Nearest Neighbours, Gradient Boost и XGBoost, для прогнозирования движения цен на акции в течение 3–5 дней. Я также оценил производительность модели по следующим показателям: точность, точность, полнота, оценка F1.

Благодаря своей работе я обнаружил, что модели в этом проекте превосходят результаты, представленные Lin et al. (2021) в нескольких аспектах. Однако применимость в реальных условиях еще предстоит оценить (см. Результаты).

Примечание.Этот проект создан с чисто образовательной целью, я не получаю никаких прав ни на одну из его частей. Я уважаю академический процесс и права интеллектуальной собственности. Если у вас есть комментарии по этим вопросам, свяжитесь со мной по одному из контактов, указанных в конце статьи.

Введение

Цель этого проекта Python — проанализировать и потенциально превзойти исследовательскую статью «Прогнозирование тренда акций с использованием графика свечей», проведенную Лин и др. (2021). Вышеупомянутое исследование использовало графики свечей, технический анализ и ансамблевое обучение для прогнозирования тенденций движения цен на акции.

В этом проекте Python я использую такие модели, как KNN, SVM, XGBoost, RF и логистическая регрессия, чтобы учитывать исторические закономерности и точно прогнозировать тенденции акций. Результаты нашего исследовательского проекта показывают, что наши модели достигли значительно более высоких показателей точности, чем исходное исследование, с более поздними временными рамками и с использованием акций, котирующихся на NASDAQ. Это подтверждает, что простые статистические модели и технический анализ являются жизнеспособными инструментами для прогнозирования тенденций на фондовом рынке.

Исследовательская статья

Прогнозирование тренда акций с помощью свечных диаграмм (Лин и др., 2021 г.)

В своем исследовании «Прогнозирование тренда акций с использованием графика свечей» Лин и др. (2021) использовали модель с несколькими входными параметрами и сообщили о степени точности 60% своих выборочных данных. Авторы использовали ряд технических индикаторов, 8-триграммную схему для внутридневной классификации ценовых движений и предварительно выбранный набор моделей k-линий в качестве входных признаков.

Для обучения своих моделей Lin et al. предварительно выбрал множество алгоритмов, включая KNN, RF, LR и GBD, и обучил их различным входным функциям. Затем они использовали метод ансамбля, в котором была выбрана модель с наивысшим показателем точности, чтобы делать прогнозы на основе тестовых данных.

Файловая структура этого проекта выглядит следующим образом:

Входные переменные:

  1. Технические индикаторы. В документе использовалось около 19 индикаторов технического анализа. В своей репликации я использовал следующее:
  • Индикаторы перекрытия: скользящая средняя, ​​экспоненциальная скользящая средняя, ​​двойная экспоненциальная скользящая средняя, ​​адаптивная скользящая средняя Кауфмана, простая скользящая средняя, ​​параболический SAR.
  • Индикаторы импульса: индекс среднего направления движения, абсолютное колебание цены, индекс товарного канала, схождение/расхождение скользящих средних, индекс денежного потока, импульс, индекс относительной силы
  • Индикаторы объема: линия Чайкина A/D (AD), осциллятор Чайкина, балансовый объем
  • Индикаторы волатильности: Истинный диапазон, Средний истинный диапазон, Нормализованный средний истинный диапазон

2. Схема с 8 триграммами. В статье использовался китайский метод классификации движений цен акций в течение дня на 8 различных подклассов.

3. К-образные паттерны. Из-за отсутствия технического описания паттернов свечей, использованных в статье, я использовал наиболее популярные индикаторы паттернов ta-lib в качестве входных данных.

Целевые ярлыки

Целевые метки, используемые в этом проекте, были определены мной. В большинстве случаев 5-дневное и 3-дневное среднее процентных изменений исторической цены классифицировалось как положительное (1) или отрицательное (0). Принимались во внимание и другие варианты целевых меток.

Что касается исторических данных об акциях, в исходной статье их модель тестировалась на 3455 акциях на китайском фондовом рынке в период с 2000 по 2017 год. В отличие от этого, наш анализ будет проверять модель на репрезентативной выборке, взятой из NASDAQ за периоды 2015–2023 годов.

Модели

В этом проекте я буду использовать следующие модели:

  • Случайный лесной классификатор
  • K-классификатор ближайших соседей
  • Машина опорных векторов
  • Повышение градиента
  • XGBoost

Воссоздание стратегии

Чтобы протестировать стратегию, изложенную в этом проекте, нам потребуются следующие модули:

Модуль 1. Этот модуль извлекает обучающие и тестовые данные, генерирует все необходимые входные сигналы и подготавливает полученные потоки данных для прогнозирования модели.

Модуль 2. Этот модуль создает ряд моделей машинного обучения, использованных в исходной статье, инициализирует их оптимальными параметрами (достигнутыми путем настройки гиперпараметров) и обучает модели на обучающем наборе.

Модуль 3. Этот модуль проверяет точность предиктора с использованием тестовых данных и создает показатели для оценки производительности модели, включая точность, точность, полноту и показатель F2.

Кроме того, в Модуле 1 есть подмодуль, который анализирует исторические ценовые данные и добавляет самые популярные паттерны K-линии в каждую строку через TaLib.

Данные

Данные, используемые в этом проекте, были получены из Yahoo Finance API и включали 1760 компаний, зарегистрированных на NASDAQ.

Обучающие и тестовые наборы были распределены по отраслям следующим образом:

Целевая метка

Пусть Pᵢ обозначает цену закрытия акции в день i, где i=1,2,…,n. Мы хотим предсказать, будет ли акция в восходящем или нисходящем тренде в течение следующих x дней, основываясь на среднем процентном изменении цены за этот период.

Чтобы вычислить среднее процентное изменение цены за следующие $x$ дней, мы можем использовать формулу:

Это представляет собой среднее дневное процентное изменение цены за период с дня i+1 до дня i+x относительно цены в день i.

Чтобы определить целевую метку y_i, мы можем установить:

Это устанавливает y_i равным 1, если среднее дневное процентное изменение в течение следующих x дней положительно, и 0 в противном случае.

В этом примере я использовал x=5 и x=3 для проверки разных временных окон.

Настройка гиперпараметров

Чтобы найти оптимальные гиперпараметры для каждой модели, использовалась рандомизированная перекрестная проверка SciKit-Learn. В следующей таблице приведены оптимальные параметры для каждой модели:

Код

В следующем разделе я шаг за шагом объясню, как работает конвейер и каковы обязанности каждого модуля. Этот раздел очень технический и зависит от языка, поэтому, если он кажется сложным, вы можете перейти к следующим разделам.

Для запуска кода вам потребуется импортировать следующие библиотеки: [Пожалуйста, предоставьте список необходимых библиотек.]

import random
from tkinter import END
from sklearn import preprocessing
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.svm import SVC

import talib as tb
import os
import joblib
from collections import Counter
import pandas as pd
from sklearn.pipeline import Pipeline
import numpy as np
from sklearn import model_selection
import xgboost as xgb
from ta_patterns import create_signals
import matplotlib.pyplot as plt
import warnings
from sklearn import utils
import yfinance as yf
from collections import Counter
import pickle

Модуль 1

Модуль 1 отвечает за выборку обучающих и тестовых данных, генерацию всех необходимых входных сигналов и подготовку результирующих потоков данных для предсказаний модели. Этот модуль также анализирует исторические ценовые данные и добавляет самые популярные модели K-линий в каждую строку через TaLib.

Кроме того, этот модуль воспроизводит технические индикаторы, использованные в исходном исследовании, такие как модели 8-триграмм, и создает целевую метку. Целевая метка создается путем классификации 5-дневного и 3-дневного среднего процентного изменения исторических изменений цен как положительное (1) или отрицательное (0) в большинстве случаев. Также рассматриваются другие формы целевых меток.

В целом модуль 1 подготавливает данные для использования в моделях машинного обучения.

model_names = ["svm", "knn", "rf", "gb", "xgb_model"]

def format_data(df, start_time, end_time,custom=True,trigrams=True,patterns=True, avg_days=5, additional=["O", "M", "V"]):
    """Takes input features and creates TA indicators, the 8-trigram scheme and Target labels.
    
    Parameters
    ----------
    df: 
        Pandas DataFrame containing Open, High, Low, Close, Volume and Date columns.
    start_time: 
        Start time in datetime format when the stock is purchased
    end_time: 
        End time in datetime format when the stock is sold
    custom: 
        Boolean Value to specify whether to create custom signals or not
    trigrams: 
        Boolean value to specify whether to calculate 8 Trigrams or not
    patterns: 
        Boolean Value to specify whether to calculate typical candlestick patterns or not
    avg_days: 
        Integer denoting the number of days for which rolling average has to be calculated
    additional: 
        List containing values O,M,V which specify which additional stock market indicators are to be calculated

    Returns
    ----------
    Pandas DataFrame with columns - 
        Updated Open, High, Low and Closing Prices, Volume, Trigrams, Target and optionally Short Line Cdl, Long Line Cdl, Spinning Top and Closing Marubozu (if custom signals are required)
    """
    
    date_mask = (df["Date"] > start_time) & (df["Date"] <= end_time)
    df = df.loc[date_mask]

    short_ind = 5
    long_ind = 10


    # OVERLAP INDICATORS
    df["ma"] = tb.MA(df["Close"], timeperiod=short_ind)
    df["ema"] = tb.EMA(df["Close"], timeperiod=long_ind)
    df["dema"] = tb.DEMA(df["Close"], timeperiod=short_ind)
    df["kama"] = tb.KAMA(df["Close"], timeperiod=short_ind)
    df["sma"] = tb.SMA(df["Close"], timeperiod=long_ind)
    df["sar"] = tb.SAR(df["High"], df["Low"])

    # MOMENTUM INDICATORS
    df["adx"] = tb.ADX(df["High"], df["Low"],
                       df["Close"], timeperiod=long_ind)
    df["cci"] = tb.CCI(df["High"], df["Low"],
                       df["Close"], timeperiod=long_ind)
    df["apo"] = tb.APO(df["Close"], fastperiod=long_ind,
                       slowperiod=short_ind)
    df["bop"] = tb.BOP(df["Open"], df["High"], df["Low"], df["Close"])
    df["macd"], df["macdsignal"], df["macdhist"] = tb.MACD(
        df["Close"], fastperiod=12, slowperiod=26, signalperiod=9)
    df["mfi"] = tb.MFI(df["High"], df["Low"], df["Close"],
                       df["Volume"], timeperiod=long_ind)
    df["mom"] = tb.MOM(df["Close"], timeperiod=long_ind)
    df["rsi"] = tb.RSI(df["Close"], timeperiod=long_ind)

    # VOLUME INDICATORS
    df["ad"] = tb.AD(df["High"], df["Low"], df["Close"], df["Volume"])
    df["adosc"] = tb.ADOSC(df["High"], df["Low"], df["Close"],
                           df["Volume"], fastperiod=short_ind, slowperiod=long_ind)
    df["obv"] = tb.OBV(df["Close"], df["Volume"])
    df["trange"] = tb.TRANGE(df["High"], df["Low"], df["Close"])
    df["atr"] = tb.ATR(df["High"], df["Low"],
                       df["Close"], timeperiod=long_ind)
    df["natr"] = tb.NATR(df["High"], df["Low"],
                         df["Close"], timeperiod=long_ind)

    df.reset_index(drop=True, inplace=True)
    
    # 8 TRIGRAMS
    if trigrams == True:
        trigrams = []
        for i in range(1, len(df)):
            if (df.loc[i, "High"] > df.loc[i-1, "High"]) & (df.loc[i, "Low"] < df.loc[i-1, "Low"]) & (df.loc[i, "Close"] > df.loc[i-1, "Close"]):
                signal = 100  # "BullishHorn"
            elif (df.loc[i, "High"] > df.loc[i-1, "High"]) & (df.loc[i, "Low"] < df.loc[i-1, "Low"]) & (df.loc[i, "Close"] < df.loc[i-1, "Close"]):
                signal = -100  # "BearHorn"
            elif (df.loc[i, "High"] > df.loc[i-1, "High"]) & (df.loc[i, "Low"] > df.loc[i-1, "Low"]) & (df.loc[i, "Close"] > df.loc[i-1, "Close"]):
                signal = 100  # "BullishHigh"
            elif (df.loc[i, "High"] > df.loc[i-1, "High"]) & (df.loc[i, "Low"] > df.loc[i-1, "Low"]) & (df.loc[i, "Close"] < df.loc[i-1, "Close"]):
                signal = -100  # "BearHigh"
            elif (df.loc[i, "High"] < df.loc[i-1, "High"]) & (df.loc[i, "Low"] < df.loc[i-1, "Low"]) & (df.loc[i, "Close"] > df.loc[i-1, "Close"]):
                signal = 100  # "BullishLow"
            elif (df.loc[i, "High"] < df.loc[i-1, "High"]) & (df.loc[i, "Low"] < df.loc[i-1, "Low"]) & (df.loc[i, "Close"] < df.loc[i-1, "Close"]):
                signal = -100  # "BearLow"
            elif (df.loc[i, "High"] < df.loc[i-1, "High"]) & (df.loc[i, "Low"] > df.loc[i-1, "Low"]) & (df.loc[i, "Close"] > df.loc[i-1, "Close"]):
                signal = 100  # "BullishHarami"
            elif (df.loc[i, "High"] < df.loc[i-1, "High"]) & (df.loc[i, "Low"] > df.loc[i-1, "Low"]) & (df.loc[i, "Close"] < df.loc[i-1, "Close"]):
                signal = -100  # "BearHarami"
            else:
                signal = 0
            trigrams.append(signal)
    else:
        trigrams = [0]*(len(df.index)-1)
        
    df.drop(df.index[0], inplace=True)
    df["trigrams"] = trigrams


    # TARGET
    df["target"] = df["Close"].pct_change().rolling(
        avg_days).mean().shift(avg_days)

    df.dropna(inplace=True)

    columns = ["Open", "High", "Volume", "Low",
               "trigrams", "target"]
        
    if custom == True:
        df = create_signals( data=df)
        columns = columns + ["shortLineCdl",
                             "longLineCdl", "spinningTop", "closingMarubozu"]

    if "O" in additional:
        columns = columns + ["ma", "ema", "dema", "kama", "sma", "sar"]
    if "M" in additional:
        columns = columns + ["adx", "cci", "apo",
                             "bop", "macd", "mfi", "mom", "rsi"]
    if "V" in additional:
        columns = columns + ["ad", "adosc", "obv", "trange", "atr", "natr"]

    df = df[columns]

    return df

Многоуровневая классификация (необязательно)

Дополнительной функцией в этом проекте является реализация генератора меток, который может создавать несколько меток на основе коэффициента стандартного отклонения, что позволяет сравнивать эффективность стратегии в разных условиях.

Примечание. Этот код не соответствует производственным стандартам и использовался только экспериментально:

def create_target(x,classes,st_dev):
    """
    Returns the target label based on the value of x, the number of classes to be generated, and the standard deviation factor.

    Args:
    x (float): A numeric value representing the price movement.
    classes (int): An integer representing the number of target classes to be generated. Valid options are 2, 3, or 5.
    st_dev (float): A numeric value representing the standard deviation factor.

    Returns:
    int: The target label. The value of the label depends on the number of classes specified. If classes is 2, the function returns 1 if x > 0, and 0 otherwise. If classes is 3, the function returns 0 if x < -st_dev, 1 if -st_dev < x < st_dev, and 2 otherwise. If classes is 5, the function returns 0 if x < -2*st_dev, 1 if -2*st_dev < x < -st_dev, 2 if -st_dev < x < st_dev, 3 if st_dev < x < 2*st_dev, and 4 otherwise.
    """

        if classes ==2:
            if x > 0:
                return 1
            else:
                return 0
            
        if classes == 3:
            DEV_FACTOR = 0.7
           
            st_dev = st_dev*DEV_FACTOR
            if x < -st_dev:
                return 0
            elif x < st_dev and x >- st_dev:
                return 1
            else:
                return 2
            
        if classes == 5:
            DEV_FACTOR = 0.7
            st_dev = st_dev*DEV_FACTOR
            if x < -st_dev*2:
                return 0
            elif x < -st_dev and x > -st_dev*2:
                return 1
            elif x < st_dev and x > -st_dev:
                return 2
            elif x > st_dev and x < st_dev*2:
                return 3
            else:
                return 4

Подмодуль для создания паттернов K-line использует TaLib Python следующим образом:

import os
import pandas as pd
import numpy as np
import talib as ta
from candle_patterns import cs_patterns_rest

def create_signals(data):
    """
    Creates technical trading signals based on candlestick charting patterns.

    Args:
        data (pandas.DataFrame): A dataframe of OHLCV (Open, High, Low, Close, Volume) data.

    Returns:
        pandas.DataFrame: A dataframe of OHLCV data with additional columns for each signal generated.
    """

    for signal in cs_patterns_rest:
        try:
            values = cs_patterns_rest[signal](
                data.Open, data.High, data.Low, data.Close)
            data[signal] = values
        except Exception as e:
            print(str(e))
    data = data.reset_index()
    
    return data

Где популярные паттерны K-Line определены в файле Candle_patterns.py как словарь с лямбда-функциями.

Модуль 2

Модуль 2 отвечает за создание моделей машинного обучения, их инициализацию с оптимальными гиперпараметрами и их обучение на предварительно обработанных обучающих данных.

Сначала мы импортируем необходимые библиотеки для моделей машинного обучения и определяем функцию, которая инициализирует каждую модель оптимальными гиперпараметрами. Затем мы создаем экземпляр каждой модели и обучаем его на обучающих данных с помощью метода fit(). Наконец, мы сохраняем обученные модели в словаре для использования в будущем.

Вот код модуля 2:

def fit_models(sample, models, cv=0, classes=2,fit=False):
    
    """
    Function used to fit models and evaluate their performance on a given dataset.

    Parameters
    ----------
    sample: pandas dataframe
        Pandas dataframe with the necessary features 
    models: list
        List of ML models 
    cv: int 
        Number of cross validations
    classes: int
        Number of target classes
    fit: Bool 
        Flag to indicate if the model has to be fitted or not 

    Returns
    -------
    fitted_models : list
        List of fitted ML models
    X_test : year
        A feature matrix for Test set 
    y_test : year
        Labels for Test set
    """
    
        
    ITER_SIZE = 5
    _df = pd.DataFrame()

    for col in sample.columns:

        _df[col] = sample[col]
        for i in range(ITER_SIZE):
            _df[f"{col}_{i}"] = sample[col].shift(periods=i+1)
            if _df.shape[0] > 8000:
                raise Exception("Weird stuff going on")
        _df = _df.merge(_df, how="right")
    sample = _df

    X = sample.dropna().drop(["target"], axis=1)
    X = X.dropna().drop([f"target_{i}" for i in range(ITER_SIZE)], axis=1)
    
    sample.dropna(inplace=True)
    y = sample[
        "target"].shift(-1).apply(lambda x: create_target(x,classes=classes,st_dev=sample["target"].std()))
        
    scaler = StandardScaler()
    pipeline = Pipeline(steps=[("scaler", scaler), ])
    X = pipeline.fit_transform(X)
    
    split_size = int(len(X) * 0.8)
    X_train, y_train = X[:split_size], y[:split_size]
    X_test, y_test = X[split_size:], y[split_size:]
    
    y_train = np.stack(y_train.values.tolist(), axis=0)
    y_test = np.stack(y_test.values.tolist(), axis=0)
    X_train = np.asarray(X_train).astype(np.float32)
    y_train = np.asarray(y_train).astype(np.float32)
    X_test = np.asarray(X_test).astype(np.float32)
    y_test = np.asarray(y_test).astype(np.float32)
    X_train_lstm = X_train.reshape(X_train.shape[0], -1, ITER_SIZE+1)
    X_test_lstm = X_test.reshape(X_test.shape[0], -1, ITER_SIZE+1)
    if fit ==True:
        fitted_models = []
        if len(models) == 0:

            svm_params = {
                "svc__C": [1],
                "svc__gamma": [0.1]
            }
            knn_params = {
                "knn__n_neighbors": [150],
                "knn__weights": ["distance"],
                "knn__algorithm": ["auto"],
                "knn__leaf_size": [1]

            }
            rf_params = {
                "rf__n_estimators": [9],
                "rf__criterion": ["gini"],
                "rf__min_samples_leaf": [5],
                "rf__max_depth": [1]
            }
            gb_params = {
                "gb__n_estimators": [1],
                "gb__max_features": [7],
                "gb__max_depth": [1]
            }
            xgb_params = {
                "xgb__n_estimators": [10],

                "xgb__max_depth": [3],
                "xgb__min_child_weight": [10],
                "xgb__gamma": [0],
                "xgb__learning_rate": [0.1],
                "xgb__seed": [27],
                "xgb__subsample": [0.65],
            }

            print("\tFitting Models...")
          
            svm, svm_best_params = iterate_models(
                SVC(), X_train, y_train, svm_params, cv)

            knn, knn_best_params = iterate_models(KNeighborsClassifier(), X_train,
                                                y_train, knn_params, cv)

            rf, rf_best_params = iterate_models(RandomForestClassifier(),
                                                X_train, y_train, rf_params, cv)

            gb, gb_best_params = iterate_models(GradientBoostingClassifier(),
                                                X_train, y_train, gb_params, cv)
            xgb_model, xgb_best_params = iterate_models(xgb.XGBClassifier(),
                                                        X_train, y_train, xgb_params, cv)
            if cv != 0:
                print("SVM: ")
                print(svm_best_params)
                print("KNN: ")
                print(knn_best_params)
                print("RF: ")
                print(rf_best_params)
                print("GB: ")
                print(gb_best_params)
                print("XGB: ")
                print(xgb_best_params)

            fitted_models = [svm, knn, rf, gb, xgb_model]

        else:
            for i, model in enumerate(models):
                print(f"\tFitting Model_{model_names[i]}")
                if i == 5:
                    model.fit(X_train_lstm, y_train)
                else:
                    model.fit(X_train, y_train)
                fitted_models.append(model)
                filename = model_names[i]+".sav"
                pickle.dump(model, open(
                f"Lin_et_al_2021//ensemble_models//{classes}class//"+filename, 'wb'))
    else:
        fitted_models = [joblib.load(f"Lin_et_al_2021//ensemble_models//{classes}class//"+model) for model in os.listdir(f"Lin_et_al_2021//ensemble_models//{classes}class//")]
        
    return fitted_models, X_test, y_test

Модуль 3

Модуль 3 отвечает за оценку производительности конвейера на тестовом наборе. Он выполняет итерацию по конвейеру, определенному в Модуле 2, и использует обученные модели для прогнозирования набора тестов. Затем прогнозы оцениваются по показателям производительности, определенным в начале проекта, включая точность, достоверность, полноту и оценку F1.

Код модуля 3 выглядит следующим образом:

def predict_models(models, X_test, y_test):
    """
    Predicts the target values of a given list of models on a test set
    
    Args:
    models (list): A list of models to be used for prediction.
    X_test (numpy array or pandas dataframe): The test set input features.
    y_test (numpy array or pandas dataframe): The test set target values.
    
    Returns:
    y_preds (list): A list of target value predictions for each model.
    """

    y_preds = []
    for model in models:
        try:
            y_pred = model.predict(X_test)
            y_preds.append(y_pred)
        except Exception as e:
            print(str(e))
            break
    return y_preds


def score_predictions( models,ticker, X_test,y_test,average):
        """
        Calculates the evaluation metrics for multiple classification models.
        
        Parameters:
        models (list): List of trained models for prediction.
        ticker (str): Ticker symbol of the stock.
        X_test (array-like): Test dataset of features.
        y_test (array-like): Test dataset of target labels.
        average (str): Type of averaging to be used for the metrics calculation.
        Possible values are: "micro", "macro" and "weighted".
        
        Returns:
        pandas.DataFrame: DataFrame with evaluation metrics for each model.
        
        """

        scaler = StandardScaler()
    
        pipeline = Pipeline(
            steps=[("scaler", scaler)])
        X_test = pipeline.fit_transform(X_test)
        y_pred = predict_models(models, X_test, y_test
        df = pd.DataFrame({"ticker": ticker,
                        "svm_acc": metrics.accuracy_score(y_test, y_pred[0]),
                        "svm_prec": metrics.precision_score(y_test, y_pred[0], average=average),
                        "svm_recall": metrics.recall_score(y_test, y_pred[0], average=average),
                        "svm_f1": metrics.f1_score(y_test, y_pred[0], average=average),
                        "knn_acc": metrics.accuracy_score(y_test, y_pred[1]),
                        "knn_prec": metrics.precision_score(y_test, y_pred[1], average=average),
                        "knn_recall": metrics.recall_score(y_test, y_pred[1], average=average),
                        "knn_f1": metrics.f1_score(y_test, y_pred[1], average=average),
                        "rf_acc": metrics.accuracy_score(y_test, y_pred[2]),
                        "rf_prec": metrics.precision_score(y_test, y_pred[2], average=average),
                        "rf_recall": metrics.recall_score(y_test, y_pred[2], average=average),
                        "rf_f1": metrics.f1_score(y_test, y_pred[2], average=average),
                        "gb_acc": metrics.accuracy_score(y_test, y_pred[3]),
                        "gb_prec": metrics.precision_score(y_test, y_pred[3], average=average),
                        "gb_recall": metrics.recall_score(y_test, y_pred[3], average=average),
                        "gb_f1": metrics.f1_score(y_test, y_pred[3], average=average),
                        "xgb_acc": metrics.accuracy_score(y_test, y_pred[4]),
                        "xgb_prec": metrics.precision_score(y_test, y_pred[4], average=average),
                        "xgb_recall": metrics.recall_score(y_test, y_pred[4], average=average),
                        "xgb_f1": metrics.f1_score(y_test, y_pred[4], average=average), }, index=[0])
       
        return df

Запуск полного конвейера

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

N[Размер выборки]_[Начальный год]-[Конечный год]CL[Выходные классы]([Используемые индикаторы])D[Временной интервал]

Например, N1760_15–23CL2(OMV)D3.csv содержит результаты тестирования выборки из 1760 акций с 2015 по 2023 год с выходными данными бинарного класса с использованием индикаторов осцилляции, импульса и волатильности в трехдневном временном окне.

def run_simulation(N,START_TIME,END_TIME,AVG_DAYS,TRIGRAMS,ADDITIONAL,CLASSES,symbols):
    """
    Runs a simulation on a subset of stocks using various machine learning models.

    Args:
    - N (int): the number of stocks to include in the simulation
    - START_TIME (str): the start date for the simulation (in YYYY-MM-DD format)
    - END_TIME (str): the end date for the simulation (in YYYY-MM-DD format)
    - AVG_DAYS (int): the number of days to average the stock data over
    - TRIGRAMS (bool): whether to include trigram features in the model
    - ADDITIONAL (list): a list of additional features to include in the model
    - CLASSES (int): the number of classes for the classification problem (2 for binary classification, more than 2 for multiclass)
    - symbols (list): a list of stock symbols to include in the simulation

    Returns:
    - None

    Saves the results of the simulation to a CSV file with a unique ID based on the simulation parameters.

    Example:
    run_simulation(10, "2015-01-01", "2022-01-01", 30, True, ["rsi", "macd"], 2, ["AAPL", "GOOGL", "TSLA", "MSFT", "AMZN"])
    """
   
        scores = pd.DataFrame(columns=["ticker", "svm_acc", "svm_prec", "svm_recall", "svm_f1",
                               "knn_acc", "knn_prec", "knn_recall", "knn_f1",
                               "rf_acc", "rf_prec", "rf_recall", "rf_f1",
                               "gb_acc", "gb_prec", "gb_recall", "gb_f1"])

        
        TEST_ID = ["N", str(N), "_",  START_TIME[2:4], "-",
               END_TIME[2:4], "CL", str(CLASSES), "(", "".join(ADDITIONAL), ")", "D", str(AVG_DAYS)]

        if CUSTOM == True:
            TEST_ID.append("_C")
        if TRIGRAMS == True:
            TEST_ID.append("_T")
        TEST_ID = "".join(TEST_ID)
        print(TEST_ID)
        if CLASSES > 2:
            average = "weighted"
        else:
            average = "binary"
        
        prob_tickers = []
        models = []
        No_POS = 0
        No_NEG = 0
        for i,f in enumerate(symbols[:N]):
            
            print(f)
            print(f"{i+1} out of {N} ")

            try:
                if f+".csv" not in os.listdir("Lin_et_al_2021//data//stocks//"):
                    
                    df = yf.download(f, start=START_TIME, end=END_TIME)
                    df.reset_index(inplace=True)
                    df = format_data(df, start_time=START_TIME,
                                    end_time=END_TIME, avg_days=AVG_DAYS,
                                    trigrams=TRIGRAMS, additional=ADDITIONAL)
                    
                    df.to_csv("Lin_et_al_2021//data//stocks//"+f+".csv")
                else:
                    df = pd.read_csv("Lin_et_al_2021//data//stocks//"+f+".csv",index_col=0)
                models, X_test, y_test = fit_models(df, models, classes=CLASSES,fit=False) 
                
                if CLASSES == 2:
                    No_POS += Counter(y_test)[0]
                    No_NEG += Counter(y_test)[1]
                
                score_df = score_predictions(models,f,X_test,y_test,average)
                scores = pd.concat([scores, score_df], ignore_index=True)
                scores.to_csv(f"Lin_et_al_2021//ensemble_results//{TEST_ID}.csv")
            except Exception as e:
                print(str(e))
                prob_tickers.append(f)

            i += 1
        print(No_POS)
        print(No_NEG)
        print(prob_tickers)

Полученные результаты

Конвейер был протестирован с различными гиперпараметрами, и результаты были многообещающими. Например, применительно к 1760 компаниям, использующим двоичные целевые метки, 3-дневное временное окно, настраиваемые шаблоны K-линии и 8-триграммы, система продемонстрировала сильную прогностическую способность, о чем свидетельствуют показатели оценки.

Примечательно, что производительность моделей в этом пайплайне оказалась даже лучше, чем та, о которой сообщили Lin et al. (2021) по большинству показателей.

Спасибо за прочтение и следите за обновлениями во второй части, где я попытаюсь протестировать торговые сигналы через Backtrader и посмотреть, можно ли построить прибыльную торговую стратегию на этих моделях!

Для любого комментария, отзыва или замечания, не стесняйтесь обращаться ко мне на мой LinkedIn или Электронная почта.

Рекомендации

Лин, Яоху и Лин, Шанцун и Ян, Хайцзюнь и Ву, Харрис. (2021). Прогнозирование тренда акций с использованием свечных графиков и методов машинного обучения в сочетании с новой инженерной схемой. IEEE-доступ. 9. 101433–101446. 10.1109/ДОСТУП.2021.3096825.