Торговый бот и прогнозы на Python

  • Создание торгового бота на Python
  • Часть 1 — Класс бота
  • Часть 2 — Пользовательский интерфейс
  • Часть 3 — Выбор и анализ моделей
  • Торговые прогнозы с использованием ИИ и Python

Создание торгового бота на Python

Создание торгового бота на Python включает в себя несколько этапов, включая настройку среды разработки, подключение к торговой платформе, реализацию торговой стратегии, тестирование стратегии на истории и развертывание бота.

Знакомство

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

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

Шаг 1: Настройка среды разработки

Прежде чем приступить к созданию торгового бота, вам необходимо настроить среду разработки Python. Установите Python на свой компьютер и выберите редактор кода или интегрированную среду разработки (IDE), например Visual Studio Code, PyCharm или Jupyter Notebook.

Шаг 2: Выберите торговую платформу и API

Чтобы взаимодействовать с рыночными данными в режиме реального времени и совершать сделки, вам потребуется доступ к API торговой платформы. Популярные платформы, такие как Alpaca, Coinbase, Binance или Interactive Brokers, предоставляют API для разработчиков. Выберите платформу в соответствии с вашими торговыми потребностями и подпишитесь на ключ API.

Шаг 3: Установите необходимые библиотеки

Python предлагает несколько библиотек для создания торговых ботов. Установите необходимые библиотеки с помощью pip или Anaconda. Некоторые часто используемые библиотеки включают в себя:

  • pandas: Для манипулирования и анализа данных.
  • numpy: для численных расчетов.
  • requests: Для выполнения HTTP-запросов к API торговой платформы.
  • websocket: для потоковой передачи рыночных данных в режиме реального времени.
  • ccxt: Для взаимодействия с различными криптовалютными биржами.

Вы можете установить эти библиотеки, выполнив следующую команду в своем терминале:pip install pandas numpy requests websocket ccxt

Шаг 4: Подключитесь к API торговой платформы

На этом шаге вы установите соединение с API торговой платформы, используя свой ключ API. Обратитесь к документации выбранной платформы, чтобы понять, как подключиться к их API. Как правило, для проверки подлинности запросов необходимо указать ключ API и секрет в коде.

Шаг 5: Получение рыночных данных

Чтобы принимать обоснованные торговые решения, вам необходим доступ к рыночным данным, таким как цена, объем и книга заказов. Используйте API для получения рыночных данных в реальном времени или за прошлые периоды. Например, вы можете использовать библиотеку запросов для отправки requests и получения ответов JSON от конечных точек API.

Шаг 6: Реализуйте свою торговую стратегию

Торговый бот работает на основе определенной торговой стратегии. Определите свою торговую стратегию и реализуйте ее на Python. Это могут быть технические индикаторы, ценовые модели или другие факторы, определяющие, когда покупать или продавать. Используйте такие библиотеки, как pandas и numpy, для управления данными и их анализа.

Несколько примеров торговых стратегий, которые можно реализовать на Python:

Стратегия пересечения скользящих средних: Эта стратегия предполагает использование двух скользящих средних разных периодов времени (например, 50-дневной и 200-дневной скользящих средних) для генерации сигналов на покупку и продажу. Когда краткосрочная скользящая средняя пересекает долгосрочную скользящую среднюю, она генерирует сигнал на покупку, а когда краткосрочная скользящая средняя пересекает ниже долгосрочной скользящей средней, она генерирует сигнал на продажу.import pandas as pd

def moving_average_crossover_strategy(data, short_window, long_window):
# Compute short-term moving average
data[‘short_ma’] = data[‘close’].rolling(window=short_window).mean()

# Compute long-term moving average
data[‘long_ma’] = data[‘close’].rolling(window=long_window).mean()

# Generate buy/sell signals
data[‘signal’] = 0
data.loc[data[‘short_ma’] > data[‘long_ma’], ‘signal’] = 1
data.loc[data[‘short_ma’] < data[‘long_ma’], ‘signal’] = -1

return data

# Example usage
price_data = pd.read_csv(‘price_data.csv’) # Assuming you have a CSV file with price data
strategy_data = moving_average_crossover_strategy(price_data, 50, 200)
print(strategy_data)

Стратегия полос Боллинджера: В этой стратегии используются полосы Боллинджера, которые представляют собой полосы волатильности, расположенные выше и ниже скользящей средней. Когда цена касается нижней полосы, это может указывать на состояние перепроданности, а когда она касается верхней полосы, это может указывать на состояние перекупленности.import pandas as pd
import numpy as np

def bollinger_bands_strategy(data, window, num_std):
# Compute rolling mean and standard deviation
data[‘rolling_mean’] = data[‘close’].rolling(window=window).mean()
data[‘rolling_std’] = data[‘close’].rolling(window=window).std()

# Compute upper and lower bands
data[‘upper_band’] = data[‘rolling_mean’] + (data[‘rolling_std’] * num_std)
data[‘lower_band’] = data[‘rolling_mean’] — (data[‘rolling_std’] * num_std)

# Generate buy/sell signals
data[‘signal’] = 0
data.loc[data[‘close’] < data[‘lower_band’], ‘signal’] = 1
data.loc[data[‘close’] > data[‘upper_band’], ‘signal’] = -1

return data

# Example usage
price_data = pd.read_csv(‘price_data.csv’) # Assuming you have a CSV file with price data
strategy_data = bollinger_bands_strategy(price_data, 20, 2)
print(strategy_data)

Стратегия возврата к среднему: Эта стратегия предполагает, что цена актива в конечном итоге вернется к своему среднему или среднему значению. Он включает в себя определение периодов перекупленности или перепроданности и открытие позиций, чтобы извлечь выгоду из ожидаемого возврата к среднему.import pandas as pd

def mean_reversion_strategy(data, window, num_std):
# Compute rolling mean and standard deviation
data[‘rolling_mean’] = data[‘close’].rolling(window=window).mean()
data[‘rolling_std’] = data[‘close’].rolling(window=window).std()

# Compute upper and lower bounds
data[‘upper_bound’] = data[‘rolling_mean’] + (data[‘rolling_std’] * num_std)
data[‘lower_bound’] = data[‘rolling_mean’] — (data[‘rolling_std’] * num_std)

# Generate buy/sell signals
data[‘signal’] = 0
data.loc[data[‘close’] > data[‘upper_bound’], ‘signal’] = -1 # Overbought condition
data.loc[data[‘close’] < data[‘lower_bound’], ‘signal’] = 1 # Oversold condition

return data

# Example usage
price_data = pd.read_csv(‘price_data.csv’) # Assuming you have a CSV file with price data
strategy_data = mean_reversion_strategy(price_data, 20, 1.5)
print(strategy_data)

Стратегия прорыва: Эта стратегия направлена на то, чтобы извлечь выгоду из выхода цены из определенного диапазона или уровня поддержки/сопротивления. Он включает в себя определение периодов консолидации и открытие позиций, когда цена пробивается выше или ниже диапазона.import pandas as pd

def breakout_strategy(data, window):
# Compute rolling highest high and lowest low
data[‘rolling_high’] = data[‘high’].rolling(window=window).max()
data[‘rolling_low’] = data[‘low’].rolling(window=window).min()

# Generate buy/sell signals
data[‘signal’] = 0
data.loc[data[‘close’] > data[‘rolling_high’], ‘signal’] = 1 # Breakout above the range
data.loc[data[‘close’] < data[‘rolling_low’], ‘signal’] = -1 # Breakout below the range

return data

# Example usage
price_data = pd.read_csv(‘price_data.csv’) # Assuming you have a CSV file with price data
strategy_data = breakout_strategy(price_data, 20)
print(strategy_data)

Шаг 7: Совершайте сделки

Как только ваша торговая стратегия определит торговую возможность, вам необходимо совершить сделку. Используйте API торговой платформы для программного размещения ордеров на покупку или продажу. Обязательно обрабатывайте ошибки и внедряйте соответствующие меры управления рисками для защиты своего капитала.

Шаг 8: Запустите своего торгового бота

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

Шаг 9: Тестирование на истории и оптимизация

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

Шаг 10: Непрерывное совершенствование

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

Часть 1 — Класс бота

Обзор

Этот проект был у меня в голове уже довольно давно. Действительно, это было главной мотивацией, направляющей мою учебу:

  • Что мне нужно знать, чтобы создавать прибыльные алгоритмы автоматической торговли, развертывать их в облаке и отслеживать их во время работы?

Компоненты

Проект состоит из 3 основных компонентов:

  • Класс Bot
    — Обновляет текущие остатки на портфеле
    — Обновление рыночных данных
    — Вычисляет новый прогноз на основе новых данных
    — Вычисляет новый «идеальный портфель» на основе прогноза
    — Выполняет заказы для достижения такого портфеля
    — Сохраняет то, что произошло с .csv файлами (данные, стоимость портфеля, прогноз, заказы и т. Д.)
  • Приборная панель с подсветкой Streamlit
    — Считывает данные из .csv файлов, которые бот постоянно обновляет
    — Показывает всю информацию в аккуратном пользовательском интерфейсе
  • Модель
    — Самая модульная составляющая в проекте
    — В этой первой статье мы будем использовать модель-заполнитель

В этой статье описывается класс бота. Обратите внимание, что он еще не был учтен для производства.

Вы можете изучить код этого проекта, посетив этот репозиторий.

Класс bot

Поскольку прогнозы делаются моделью, а сбор и обработка данных в этом случае не очень интересны (объединение строк в существующий DataFrame, сохранение в .csv, простые операции), сок класса bot состоит из:

  • Правильное соблюдение сроков выполнения операций
  • Перевод прогнозов модели в заявки на биржу
  • Размещение действительных ордеров на бирже для достижения желаемого портфеля
    — Это включает в себя округление чисел, проверку доступных остатков и проверку минимальной стоимости заказа.
  • Быть стабильным, чтобы не прерывать торговлю в критические моменты

Хорошо иметь быстрого бота, однако мы имеем дело с Python, а не с оптимизацией скорости. Бот предназначен для выполнения своей рутины каждые 1 минуту, что достаточно быстро, чтобы обнаруживать тенденции на ранней стадии.

Запуск бота

config = dotenv_values(".env")

api_key_testnet = config["API_Key_Testnet"]
api_secret_testnet = config["Secret_Key_Testnet"]

modelName = config["modelName"]


async def main():

    smartBot1 = tradingBot(
        modelName=modelName,
        symbol="BTCUSDT",
        minPctChange=2/100,
        exposureMultiplier=100,
        API = [api_key_testnet, api_secret_testnet],
        )

    await asyncio.gather(
        smartBot1.mainLiveTradeLoop()
    )

if __name__ == "__main__":
    asyncio.run(main())
  • «modelName» используется для поиска папки модели, содержащей ее скейлеры и параметры. Подробнее об этих параметрах позже.
  • «символ» определяет, на каком рынке Binance будет торговаться.
  • «minPctChange» ограничивает размер корректировок, которые бот может вносить в свой портфель (избегает спам-заказов)
  • «Мультипликатор воздействия» входит в уравнение, которое переводит прогноз в идеальную подверженность активу
  • Ключ API и секрет для подключения к бирже

Этот раздел кода находится в конце «liveBotClass.py», который определяет класс бота, поэтому бота можно инициализировать, набрав в терминале из корневой папки:

python scripts/liveBotClass.py

Инициализация ботов

После создания экземпляра бота он запускает функцию __init__:

class tradingBot:
    
    # Initialize the class
    def __init__(
        self,
        modelName,
        symbol,
        minPctChange,
        exposureMultiplier,
        API,
        ):

        self.name = f"{modelName}_bot"
        print(f"\nHello, I'm {self.name}.\n")
        self.symbol = symbol
        self.minPctChange = minPctChange
        self.exposureMultiplier = exposureMultiplier
        self.API = API
        self.modelName = modelName

        self.initializeBot()

        print("Starting trading.\n")
    #-----

Внутри self.initializeBot() находятся все функции инициализации, которые вызываются только при инициализации бота. Они проверяют, есть ли уже папка для этого бота, если да, он загружает все прошлые данные, сохраненные ботом перед закрытием. В противном случае он создает папку и файлы. Таким образом, бот может продолжить с того места, на котором остановился, если что-то пойдет не так.

Если вы обратили внимание, возможно, вы заметили, что self.mainTradeLoop() вызывается внутри асинхронной функции, чтобы запустить бота

 ##### ASYNC LOOP #####
    async def mainLiveTradeLoop(self):

        while True:
            print("\n---|---|---|---|---|---|---|---|---|---\n")
            while True:

                self.refreshAll()

                now = dt.datetime.now(pytz.timezone("UTC"))
                timeGap = now.minute-self.lastMarketDataTS.minute

                if timeGap <= 1:

                    self.smartSignals()
                    self.saveFinish()

                    break

                print(f"DATA IS LATE: now-{now.minute} vs last-{self.lastMarketDataTS.minute+1} -> timeGap = {timeGap}\n")
                print("Sleeping 1 second")

                await asyncio.sleep(1)
        
            now = dt.datetime.now(pytz.timezone("UTC"))
            timeToWait = round(61-(now.second+(now.microsecond)/1000000),4)

            print(f"Seconds now: {now.second}")
            print(f"Waiting {timeToWait} seconds")

            await asyncio.sleep(timeToWait) 
    #-----

Эта функция вызывает refreshAll(), smartSignals() и saveFinish() соответственно в начале каждой минуты. Перед вызовом smartSignals() и saveFinish() он проверяет, актуальны ли данные, сравнивая текущую минуту с самой последней минутой в рыночных данных (lastMarketDataTS).

Большая часть кода в этом основном цикле служит для правильного обеспечения соблюдения этого времени.

Функции обновления

refreshAll() — это короткая функция, но она вызывает кучу других функций:

##### REFRESH BOT #####
    def refreshAll(self):
        # update balances and average price
        self.refreshPortfolio()
        # update market data
        self.refreshSaveData()
        # update prediction to enable decision making
        self.refreshPred()

    def refreshPortfolio(self):
        
        while True:
            try:
                client = Client(self.API[0], self.API[1], testnet=True)
                balances = pd.DataFrame.from_records(client.get_account()["balances"])
                self.price = float(client.get_avg_price(symbol=self.symbol)["price"])
                client.close_connection()
            
            except Exception as e:
                if isinstance(e, socket.error):
                    print(f"Connection error:\n{e}")
                else:
                    print(f"Ooops there was a problem refreshing the portfolio:\n{e}")
            else:
            
                newNominal = balances[(balances["asset"]==self.symbol[:-4]) | (balances["asset"]=="USDT")]["free"].values


                self.Portfolio["nominal"] = newNominal
                self.Portfolio["nominal"] = self.Portfolio["nominal"].astype("float")

                self.Portfolio["inUSD"] = self.Portfolio["nominal"]*[self.price,1]
                
                self.pfValUSD = self.Portfolio["inUSD"].sum()
                self.pfValNonUSD = (self.Portfolio["inUSD"]/[self.price,self.price]).sum()

                self.cryptoRatio = self.Portfolio["inUSD"][0]/self.Portfolio["inUSD"].sum()
                break
    
    def refreshSaveData(self):
        
        if dt.datetime.now(pytz.timezone("UTC")) - dt.datetime.fromtimestamp(self.lastMarketDataTS.value/1000000000,tz=pytz.timezone("UTC")) > dt.timedelta(seconds=121):
            newRow = BinanceData.download(
                self.symbol,
                start = self.lastMarketDataTS + dt.timedelta(minutes=1),
                end = dt.datetime.now(pytz.timezone("UTC")) - dt.timedelta(minutes=1),
                interval="1m").get(["Open", "High", "Low", "Close", "Volume"])
            
            self.marketData = pd.concat([self.marketData,newRow]).iloc[-self.dataLength:,:]
            self.lastMarketDataTS = self.marketData.index[-1]

            # SAVE
            newRow.to_csv(f"{self.botFolderPath}/{self.name}_data.csv",mode="a",header=False)

    def refreshPred(self):

        processedData = processData(self.marketData,self.modelParamsDict["timePeriods"],scalers=self.scalers)
        modelPred = self.model.predict(processedData.iloc[-1:])
        descaledModelPred = self.targetScaler.inverse_transform(modelPred.reshape(-1, 1))[0][0]
        self.currentPrediction = (descaledModelPred/100)*self.price
    
    def saveFinish(self):
        self.refreshPortfolio()
        predDict = {"Timestamp":self.lastMarketDataTS,"pfVal":self.pfValUSD,"cryptoRatio":self.cryptoRatio,"prediction":self.currentPrediction}
        predDF = pd.DataFrame(predDict,index=[0])
        predDF.to_csv(f"{self.botFolderPath}/{self.name}_preds.csv",mode="a",index=False,header=False)
    #-----

Хотя он немного длиннее, этот код не должен быть сложным для понимания. Чтобы было понятнее, вот ментальная карта refreshAll():

Зеленые прямоугольники представляют обновления в атрибутах класса, а желтые прямоугольники представляют обновления в .csv файлах.

saveFinish() не используется в refreshAll(), он вызывается после smartSignals().

После запуска refreshAll() бот обновляется и готов принять решение.

Торговые функции

Все торговые вычисления являются производными от smartSignals(), точно так же, как все обновления данных выполняются путем вызова refreshAll(). Эти функции составляют изрядный объем кода, но являются последней частью класса бота.

##### MAKE ORDERS #####
    def smartSignals(self):
        
        # in this example the model predicts a moving average. predictedMA is the current value of the moving average the model predicts
        predictedMA = talib.MA(self.marketData["Close"],timeperiod=self.modelParamsDict["rollingMeanWindow"]).iloc[-1]

        # if price is above current and predicted MA, this means the price is going down. thus, sell all
        if (self.price > predictedMA) & (self.price > self.currentPrediction):
            self.targetRatio = 0

        # else, compute predicted percentual change of the MA and use it to generate the target ratio of exposure
        else:
            percentualPred = (self.currentPrediction-predictedMA)/predictedMA

            # the operation below is mostly arbitrary and can be optimized through backtests
            # it converts the predicted percentual change of the MA into a target ratio of exposure
            self.targetRatio = min(max(percentualPred*self.exposureMultiplier,0),1)
        
        # if the target ratio is below minPctChange, set it to 0
        if self.targetRatio < self.minPctChange:
            self.targetRatio = 0
        
        # with targetRatio set, we now place orders to achieve such ratio
        self.achieveIdealPortfolio()

    def achieveIdealPortfolio(self,saveOrder=True):
        
        # utility functions (basically to avoid minimum notional)
        def upThenDown():
            self.placeOrder(sell=False,amount=(self.pfValNonUSD*(1-self.cryptoRatio)),saveOrder=saveOrder, refreshPf=True)
            self.placeOrder(sell=True,amount=(self.pfValNonUSD*(1-self.targetRatio)),saveOrder=saveOrder)
        def downThenUp():
            self.placeOrder(sell=True,amount=(self.pfValNonUSD*self.cryptoRatio),saveOrder=saveOrder, refreshPf=True)
            self.placeOrder(sell=False,amount=(self.pfValNonUSD*self.targetRatio),saveOrder=saveOrder)

        # if the difference between the target ratio and the current ratio is less than minPctChange, do nothing
        percentChange = self.targetRatio-self.cryptoRatio
        if abs(percentChange)>self.minPctChange:
            
            minNotionalThreshold = 12
            minNotionalRatio = minNotionalThreshold/self.pfValUSD
            
            # avoid minimum notional
            if abs(percentChange)<minNotionalRatio:
                if (self.cryptoRatio>1.2*minNotionalRatio):
                    downThenUp()
                else:
                    upThenDown()
                    
            else:
                self.placeOrder(sell=(percentChange<0),amount=abs(self.pfValNonUSD*percentChange),saveOrder=saveOrder)
           
    def placeOrder(self, sell, amount, saveOrder=True, refreshPf=False):
        
        def roundAndSendOrder(self, client, sell, amount):
            
            amountToOrder = math.floor(amount*10000)/10000
            print(f"\n-----> {'SELL' if sell else 'BUY'} {amountToOrder} {self.symbol[:-4]} | {round(amountToOrder*self.price,2)} USD | {round((amountToOrder*self.price*100)/self.pfValUSD,2)}% <-----\n")

            if sell:
                order = client.order_market_sell(
                    symbol= self.symbol,
                    quantity = amountToOrder)
            else:
                order = client.order_market_buy(
                    symbol= self.symbol,
                    quantity = amountToOrder)
            
            return order

        while True:
            try:
                client = Client(self.API[0], self.API[1], testnet=True)
                
                # SELL
                if sell:
                    amountCrypto = self.pfValNonUSD*self.cryptoRatio
                    if amount > amountCrypto:
                        print("Not enough crypto to sell!")
                        amount = amountCrypto*0.9999
                    
                    order = roundAndSendOrder(self, client, sell, amount)
                
                # BUY
                else:
                    amountUSD = self.pfValNonUSD*(1-self.cryptoRatio)
                    if amount > amountUSD:
                        print("Not enough USD to buy!")
                        amount = amountUSD*0.9999

                    order = roundAndSendOrder(self, client, sell, amount)

                client.close_connection()
            
            except Exception as e:
                if isinstance(e, socket.error):
                    print(f"Connection error:\n{e}")
                else:
                    print(f"Ooops there was a problem placing an order:\n{e}")
            else:
                if refreshPf: self.refreshPortfolio()
                if saveOrder: self.saveOrder(order)
                break
        
    def saveOrder(self,order):
        order['effectivePrice'] = [round(float(order['cummulativeQuoteQty'])/float(order['executedQty']),2)]
        order['pfValue'] = self.pfValUSD
        order.pop('fills')
        orderDF = pd.DataFrame.from_dict(order)
        orderDF = orderDF[['symbol','pfValue','orderId','executedQty','cummulativeQuoteQty','effectivePrice','side','status','type','transactTime']].copy()

        orderDF["transactTime"] = pd.to_datetime(orderDF["transactTime"],unit="ms")
        orderDF.to_csv(f"{self.botFolderPath}/{self.name}_log.csv",mode="a",index=False,header=False)
    #-----

Чтобы было понятнее, вот карта smartSignals():

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

Наконец, бот вызывает saveFinish() в конце процедуры. saveFinish() сохраняет метку времени, стоимость портфеля, подверженность активам и текущий прогноз в preds.csv. Эти данные позже используются для отображения пользовательского интерфейса.

Часть 2 — Пользовательский интерфейс

Обзор

Во второй статье о торговом боте мы рассмотрим пользовательский интерфейс бота. Он был построен с использованием пакета Streamlit, который довольно удобен.

Если вы не читали первую, вы можете прочитать ее здесь. Он описывает класс бота, который обрабатывает интерфейс между ботом и биржей. Обратите внимание, что теперь я переделал бота, чтобы он торговал на 1-часовом таймфрейме, а не на 1-минутном, как в первой статье.

Кроме того, это репозиторий проекта, обязательно проверьте его.

Загрузка данных

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

def loadFromCSV():
    log = pd.read_csv(f"{path}/{botName}_log.csv",index_col=0)
    log["transactTime"] = pd.to_datetime(log["transactTime"])
    log["pfValue"] = log["pfValue"].astype('float')

    histData = pd.read_csv(f"{path}/{botName}_data.csv",index_col=0)
    histData.index = pd.to_datetime(histData.index)
    
    preds = pd.read_csv(f"{path}/{botName}_preds.csv",index_col=0)
    preds.index= pd.to_datetime(preds.index)

    return log, histData, preds


modelName = dotenv_values(".env")["modelName"]

botName = modelName+"_bot"

path = f"botsStates/{botName}"


if os.path.isfile(f"{path}/{botName}_log.csv") and os.path.isfile(f"{path}/{botName}_data.csv") and os.path.isfile(f"{path}/{botName}_preds.csv"):
    log, histData, preds = loadFromCSV()
    if len(preds)>0:
        mainLayout(log, histData, preds)
else:
    st.header("Data not found")
    st_autorefresh(interval= 2 * 1000)

Как видите, некоторые данные собираются из файла .env.

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

Первая часть mainLayout() устанавливает переменные:

def mainLayout(log, histData, preds):

    # PAGE HEADER
    st.header(f"{botName} Dashboard")

    # INITIALIZE TABS
    tradingTab, metricsTab, logsTab, aboutTab, settingsTab = st.tabs([
        "? Trading",
        "? Metrics",
        "? Logs",
        "? About",
        "⚙ Settings"
        ])

    # LOAD MODEL PARAMETERS
    with open( f"models/{modelName}/{modelName}_params.json", 'r' ) as file:
            modelParamsDict = json.load( file )
    
    # INITIALIZE INITIAL BOT STATE VARIABLES
    pfValueInitial = preds["pfVal"].iloc[0]
    timeStarted = preds.index[0]
    assetBeginningPrice = histData[histData.index==timeStarted.strftime("%Y-%m-%d %H:%M")]["Close"][0]

    # TOTAL SECONDS RUNNING
    nowTS = dt.datetime.now(dt.timezone.utc).timestamp()
    startedTS = (timeStarted).timestamp()
    deltaTS = nowTS-startedTS
    totalSecondsRunning = deltaTS+10

    # GUARANTEE SLIDER PERSISTANCE
    sliderValkey = "sliderVal"

    def lookBackRangeSlider(sliderPos=max(math.ceil(totalSecondsRunning/60/60),2)):
        return st.slider(
        "Graph range:",
        2,
        max(math.ceil(totalSecondsRunning/60/60),2)+10,
        sliderPos,
        key=sliderValkey)

Комментарии в коде упрощают понимание. lookBackRangeSlider() используется во вкладке торговли.

? Вкладка «Торговля»

Вкладки «Торговля» и «Метрики» отображают полезные данные. Давайте заглянем во вкладку «? Торговля», на которой есть несколько графиков. Обратите внимание, что это самый обширный раздел кода в этой статье.

    # ----------------------------------------TRADING TAB----------------------------------------

    with tradingTab:


        # --------------------PREPARE DATA--------------------

        # ---------- SLIDER ----------
        if sliderValkey not in st.session_state:
            nHoursBack =  totalSecondsRunning/60/60-lookBackRangeSlider()-1
        else:
            nHoursBack = totalSecondsRunning/60/60-lookBackRangeSlider(st.session_state[sliderValkey]+(1 if st.session_state[sliderValkey] == math.ceil(totalSecondsRunning/60/60)-1 else 0))-1
            
        timeStampLookBack = (timeStarted+dt.timedelta(hours=nHoursBack+1)).strftime("%Y-%m-%d %H:%M")

        # trim log df
        logTrim = log[log["transactTime"]>=timeStampLookBack]

        # trim preds df
        predsTrim = preds[timeStampLookBack:]


        # ---------- CREATE PLOTTING DATA ----------
        
        # ohlcv
        histDataPlot = histData[timeStampLookBack:]
        
        # asset performance
        assetPerformancePlot = ((histDataPlot["Close"]*100/assetBeginningPrice)-100)[timeStampLookBack:]
        
        # portfolio value
        pfValPlot = predsTrim["pfVal"]
        
        # target MA
        predictionMAPlot = talib.MA(histData["Close"],timeperiod=modelParamsDict["rollingMeanWindow"]).rename("target")[timeStampLookBack:]
        
        # error
        errorPlot = pd.DataFrame()
        errorPlot["error"] = ((predsTrim["prediction"] - predictionMAPlot.shift(-modelParamsDict["predictionHorizon"]))/predictionMAPlot)*100
        errorPlot.dropna(inplace=True)
        errorPlot = errorPlot.sort_index()

        # assetRatio
        # extend asset ratio df back to start of graph
        dateRange = pd.date_range(start=pd.Timestamp(timeStampLookBack).round('H'),freq='1H',periods=len(histDataPlot))
        dateRangeSeries = pd.Series(dtype='float64').reindex(dateRange)
        dateRangeSeries.name = "dateRange"
        
        assetRatioPlot = predsTrim["assetRatio"]
        assetRatioPlot.name = "assetRatio"
        assetRatioPlot = pd.merge(assetRatioPlot.tz_localize(None),dateRangeSeries,left_index=True,right_index=True,how="outer")["assetRatio"]
        
    
        # -------------------- PLOT --------------------


        # ---------- FIG 1 ----------
        # PORTFOLIO VS UNDERLYING ASSET
        if len(predsTrim)>1:
            fig1 = px.line(x=assetPerformancePlot.index,y=assetPerformancePlot).update_traces(line=dict(color="gray"))
            
            fig1.update_layout(yaxis_tickformat = '%')

            fig1.add_traces(list(px.line(x=predsTrim.index, y=(pfValPlot/pfValueInitial)*100-100).update_traces(line=dict(color="blue")).select_traces()))
            
        else: fig1 = px.line()
        # ----------


        # ---------- FIG 2 ----------
        # RATIO OF EXPOSURE
        fig2 = go.Scatter(x=assetRatioPlot.index,y=assetRatioPlot*100,mode="lines",marker_color='Orange')
        # ----------


        # -------------------- ASSEMBLE LEFT FIGURE --------------------
        Fig1 = make_subplots(rows=2, cols=1,shared_xaxes=True,subplot_titles=["Portfolio vs. Underlying Asset","Asset Exposure"])

        # fig1
        for d in fig1.data:
            Fig1.add_trace(d, row=1, col=1)
        # fig2
        Fig1.add_trace(fig2, row=2, col=1)

        # update figure layout
        Fig1.update_layout(showlegend=False,height=600)
        # --------------------------------------------------------------------------------



        # ---------- FIG 3 ----------
        # PRICE
        fig3 = go.Candlestick(
            x = histDataPlot.index,
            open = histDataPlot["Open"],
            high = histDataPlot["High"],
            low = histDataPlot["Low"],
            close = histDataPlot["Close"],
            increasing_line_color = "rgba(220,220,220,0.8)",
            decreasing_line_color = "rgba(128,128,128,0.8)",
        )
        # ----------


        # ---------- FIG 4 ----------
        # PREDICTION ERROR
        fig4 = go.Scatter(x=errorPlot.index,y=errorPlot["error"],mode="markers",marker_color='red')
        # ----------


        # -------------------- ASSEMBLE RIGHT FIGURE --------------------
        Fig2 = make_subplots(rows=2, cols=1,shared_xaxes=True,subplot_titles=["Price, Current Target, Shifted Target, Model prediction", "Prediction Error"])

        # fig3
        Fig2.add_trace(fig3, row=1, col=1)
        # preds, target, shifted target
        Fig2.add_trace(go.Scatter(x=predsTrim.index,y=predsTrim["prediction"],mode="lines",marker_color='aqua',opacity=.5), row=1, col=1)
        Fig2.add_trace(go.Scatter(x=predictionMAPlot.index,y=predictionMAPlot,mode="lines",marker_color='red ',opacity=.5), row=1, col=1)
        Fig2.add_trace(go.Scatter(x=predictionMAPlot.shift(-modelParamsDict["predictionHorizon"]).index,y=predictionMAPlot.shift(-modelParamsDict["predictionHorizon"]),mode="lines",marker_color='green ',opacity=.5), row=1, col=1)
        
        # fig4
        Fig2.add_trace(fig4, row=2, col=1)

        # hide range slider
        Fig2.update_xaxes(rangeslider_visible=False)

        # update figure layout
        Fig2.update_layout(showlegend=False,height=600)
        # --------------------------------------------------------------------------------


        # render figures in page
        col1, col2 = st.columns(2)
        with col1:
            st.plotly_chart(Fig1,True)
        with col2:
            st.plotly_chart(Fig2,True)

Этот код создает следующую страницу:

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

Пока мы здесь, давайте заглянем на вкладку « ? О программе». Не код, так как в нем нет ничего интересного, а то, что в нем написано. Он объясняет бота и графики на вкладке «? Торговля».

Теперь давайте заглянем на вкладку « ? Метрики».

? Вкладка «Метрики»

На этой вкладке собрана важная информация о производительности бота. Это код:

    # ---------------------------------------- METRICS TAB ----------------------------------------

    with metricsTab:

        # -------------------- PEPARE DATA --------------------

        pfValue = preds["pfVal"].iloc[-1]
        prediction = preds['prediction'].iloc[-1]
        delta = pfValue-pfValueInitial

        assetPerformance = histData.iloc[-1]["Close"]
        hoursRunning = math.floor((totalSecondsRunning)/60/60)

        if len(log)>0:
            totalSecondsSinceTraded = ((dt.datetime.now()+dt.timedelta(hours=3)) - log["transactTime"].max()).seconds

            hoursSinceTraded = math.floor((totalSecondsSinceTraded)/60/60)
            minutesSinceTraded = math.floor((totalSecondsSinceTraded/60)-hoursSinceTraded*60)
            secondsSinceTraded = math.floor(totalSecondsSinceTraded-hoursSinceTraded*60*60-minutesSinceTraded*60)


        # -------------------- RENDER PAGE --------------------

        st.subheader("Metrics")
        
        col1, col2 = st.columns(2)

        with col1:
            col11, col12, col13 = st.columns(3)

            with col11:
                st.markdown(f"**Time running:**<br/>{hoursRunning}h",unsafe_allow_html=True)
                if len(log)>0:
                    st.markdown(f"**Time since last trade:**<br/>{hoursSinceTraded}h {minutesSinceTraded}m {secondsSinceTraded}s",unsafe_allow_html=True)
                    st.markdown(f"**Number of trades:**<br/>{math.floor((len(logTrim))/2)}",unsafe_allow_html=True)
                
                st.markdown(f"**Current prediction ABS:**<br/>{round(prediction,2)} USD",unsafe_allow_html=True)
                st.markdown(f"**Current prediction %:**<br/>{round(((prediction-histData['Close'][-1])/histData['Close'][-1])*100,2)}%",unsafe_allow_html=True)

            with col12:
                st.markdown(f"**Portfolio Initial value:**<br/>{round(pfValueInitial,2)} USD",unsafe_allow_html=True)
                st.markdown(f"**Portfolio Current value:**<br/>{round(pfValue,2)} USD",unsafe_allow_html=True)
                st.markdown(f"**Portfolio Delta:**<br/>{round(delta,2)} USD",unsafe_allow_html=True)
                st.markdown(f"**Portfolio % Delta:**<br/>{round((delta/pfValueInitial)*100,2)}%",unsafe_allow_html=True)

            with col13:
                st.markdown(f"**BTC initial price:**<br/>{assetBeginningPrice} USD",unsafe_allow_html=True)
                st.markdown(f"**BTC current price:**<br/>{assetPerformance} USD",unsafe_allow_html=True)
                st.markdown(f"**BTC price Delta:**<br/>{round(assetPerformance - assetBeginningPrice,2)} USD",unsafe_allow_html=True)
                st.markdown(f"**BTC % price Delta:**<br/>{round((assetPerformance - assetBeginningPrice)/assetBeginningPrice*100,2)}%",unsafe_allow_html=True)
                st.markdown(f"**Portfolio vs. BTC % Delta:**<br/>{round((delta/pfValueInitial)*100-(assetPerformance - assetBeginningPrice)/assetBeginningPrice*100,2)}%",unsafe_allow_html=True)
        
        
        # -------------------- PLOT PORTFOLIO --------------------
        with col2:

            st.markdown(f"**Current portfolio:**",unsafe_allow_html=True)
            pfVal = preds["pfVal"].iloc[-1]
            assetRatio = preds["assetRatio"].iloc[-1]
            inUSD = {"USDT":pfVal*(1-assetRatio),"ASSET":pfVal*assetRatio}
            inUSD = pd.DataFrame(inUSD,index=[0]).T
            fig = px.pie(inUSD,values=0,color=inUSD.index,names=inUSD.index,color_discrete_map={"USDT":"Green","ASSET":"Orange"}, height=500)
            st.plotly_chart(fig,True)

Этот код создает следующую страницу:

В этом коде особо нечего комментировать.

⚙ Вкладка «Настройки»

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

    # ---------------------------------------- SETTINGS TAB ----------------------------------------

    with settingsTab:
        
        if 'checkbox_value' not in st.session_state:
            st.session_state.checkbox_value = True

        autoRefresh = st.checkbox("Auto Refresh", value=st.session_state.checkbox_value)
        st.session_state.checkbox_value = autoRefresh
        
        minVal = 2
        maxVal = 59
        sliderVal = minVal
        sliderVal = st.slider(f"Select auto refresh interval:", min_value=minVal, max_value=maxVal, value=sliderVal)
    
        if autoRefresh:

            st_autorefresh(interval= sliderVal*60*1000, key="dataframerefresh")

Вот как это выглядит:

Часть 3 — Выбор и анализ моделей

Знакомство

В предыдущих статьях мы углубились в класс live-ботов, отвечающий за обработку заявок API на бирже, а также пользовательский интерфейс. Вы можете найти эти статьи здесь и здесь. Не забудьте заглянуть в репозиторий проекта!

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

Параметры и временные рамки, которые следует учитывать

В настоящее время ордера формируются из прогнозов с помощью линейной функции:

exposure = a * prediction + b

  • prediction: значение от 0 до 1 (процент), представляющее процентную разницу между прогнозируемой и текущей скользящей средней.
  • exposure значение от 0 до 1, которое определяет процент портфеля, подверженного торгуемому активу.
  • a: Множитель, который контролирует чувствительность экспозиции по отношению к прогнозу.
  • b: Константа.

Коэффициент «а» определяет чувствительность экспозиции по отношению к прогнозу. Если a установлено на высокое значение, торговый бот возьмет на себя более высокий уровень риска в ответ на прогноз, что приведет к большей подверженности риску и потенциально большей доходности. И наоборот, если a установлено на низкое значение, бот возьмет на себя более низкий уровень риска, что приведет к меньшему риску и потенциально меньшей доходности.

Коэффициент «b» определяет константу, прибавленную к уровню экспозиции. Высокое значение b приведет к более агрессивной торговой стратегии, при которой бот будет иметь более высокий уровень экспозиции, даже если прогноз близок к текущей скользящей средней. С другой стороны, низкое значение b приведет к более консервативной торговой стратегии, при которой бот будет иметь значительные уровни воздействия только тогда, когда прогноз далек от текущей скользящей средней.

Идеальные коэффициенты (a, b) не сразу очевидны. Кроме того, в исходной версии статьи и репозитория использовалось минутное разрешение данных, а во второй версии использовались почасовые данные. Торговая система также может быть применена к дневным или недельным данным, поэтому оптимальный таймфрейм остается неясным.

Более того, горизонт прогнозирования может варьироваться. Прогнозирование 24-часовой скользящей средней через день или неделю — это разные задачи. Чтобы упростить эту оценку, мы всегда будем использовать окно скользящей средней плюс единица в качестве горизонта прогнозирования. То, что представлено в данной статье, представляет собой рабочий процесс для выбора параметров a и b, но такой рабочий процесс может быть легко расширен для выбора других характеристик стратегии, таких как таймфрейм и горизонт прогнозирования.

Рабочий процесс выбора модели

Чтобы оценить историческую эффективность наших торговых стратегий, мы реализуем тестирование на истории. Тестирование на истории — это процесс, который включает в себя оценку эффективности торговой стратегии с использованием исторических данных. Его цель состоит в том, чтобы смоделировать, как бы работала стратегия, если бы она использовалась для торговли в прошлом. Это достигается путем применения правил стратегии к историческим данным и анализа результатов.

Тестирование на истории также позволяет нам оптимизировать стратегию, определяя, какие параметры и правила являются наиболее эффективными. Мы можем протестировать стратегию с различными комбинациями параметров и определить, какие из них дают наилучшие результаты. Затем эта информация может быть использована для уточнения и улучшения стратегии.

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

Обладая достаточной вычислительной мощностью и скоростью, мы могли бы запускать многочисленные итерации обучающих моделей, различные разработки функций, алгоритмы машинного обучения и многое другое. В результате у нас будет несколько обученных моделей в папке «models»:

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

Каждая из этих моделей имеет свою среднюю погрешность. Но как они сравниваются, когда дело доходит до реальной торговли? И какие параметры были бы лучшими (a, b) для каждой модели?

Чтобы ответить на эти вопросы, мы выполним поиск по сетке производительности каждой модели с каждой комбинацией параметров a и b, измеренной коэффициентом резкости бэктеста.

Коэффициент Шарпа рассчитывается путем деления избыточной доходности портфеля (доходность портфеля минус безрисковая ставка) на его стандартное отклонение. Другими словами, он измеряет, сколько прибыли инвестор зарабатывает на единицу принятого риска. Более высокий коэффициент Шарпа указывает на то, что портфель имеет лучшую доходность с поправкой на риск, чем портфель с более низким коэффициентом Шарпа. Мы будем использовать коэффициент Шарпа, встроенный в vectorbt, используемый пакет тестирования на истории.

Внедрение тестирования на истории

Мы будем использовать vectorbt для тестирования наших стратегий. vectorbt — очень удобный пакет Python, и создать бэктест довольно просто.

Первым шагом является генерация вектора экспозиции путем выполнения прогноза с помощью линейной функции, описанной ранее. Для этого я создал следующую функцию:

def genExposure(data,exposureConstant,exposureMultiplier,colname):
    return data.apply(lambda x: min(max(((x[colname]-x["Current_SMA"])/x["Current_SMA"])*exposureMultiplier+exposureConstant,0),1) if ((x["Shifted_Close"] < x[colname])) else 0, axis=1)

Функция вычисляет уровень экспозиции для каждой точки данных в DataFrame, применяя лямбда-функцию. Лямбда-функция вычисляет уровень экспозиции с помощью линейной функции, принимая минимальные и максимальные значения от 0 до 1. Если цена открытия меньше прогнозируемой скользящей средней, рассчитывается уровень воздействия, в противном случае он устанавливается равным 0. Затем функция возвращает объект Series, содержащий уровни экспозиции для каждой точки данных.

«colname» — это имя столбца в DataFrame, в котором хранятся прогнозы.

С помощью «genExposure» мы можем определить функцию, которая запускает бэктест и возвращает объект «portfolio», класс vectorbt с информацией о бэктесте.

def backtestFromExposureSettings(data,exposureConstant,exposureMultiplier,colname,freq):

    exposure = genExposure(data,exposureConstant,exposureMultiplier,colname)
    pf = vbt.Portfolio.from_orders(
            data["Open"],
            exposure,
            size_type='targetpercent',
            freq=freq,
            )
    return pf
  1. Он вызывает функцию genExposure с заданными аргументами для создания уровней экспозиции для каждой точки данных в DataFrame.
  2. Он создает объект vectorbt Portfolio (pf) с помощью vbt.Portfolio.from_orders метод.
  • Для исполнения ордеров используется цена открытия, а не цена закрытия.
  • Параметр size_type назначен «targetpercent», указывает, что размеры ордеров выражены в процентах от общей стоимости портфеля.
  • Данные часто имеют пробелы, поэтому полезно вручную установить частоту, чтобы все метрики вычислялись в бэктесте.

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

Выбор модели с помощью поиска по сетке

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

Функция для запуска всех комбинаций параметров, а также сравнения прогнозов модели с «идеальными» предсказаниями (фактическими SMA, но смещенными назад) выглядит следующим образом:

def searchExposureBacktest(data,exposureConstants,exposureMultipliers,maPeriod,model,modelParamsDict,targetScaler,scalers):
    
    currentSMA = talib.SMA(data["Close"], timeperiod=maPeriod).rename("Current_SMA")

    
    shiftedSMA = pd.DataFrame({f"Shifted_SMA":talib.SMA(data["Close"],maPeriod).shift(-(maPeriod+1))})
    currentShiftedSMA = pd.concat([currentSMA,shiftedSMA["Shifted_SMA"]],axis=1).dropna()

    preds = genPreds(data,model,modelParamsDict,targetScaler,scalers)
    currentPredictedSMA = pd.concat([currentSMA,preds],axis=1).dropna()

    
    pfShiftedSMA_DF = pd.DataFrame()
    pfPred_DF = pd.DataFrame()

    for constant in exposureConstants:
        for multiplier in exposureMultipliers:

            print(f"_{constant}_{multiplier}_", end="\r", flush=True)

            pfShiftedSMA = backtestFromExposureSettings(pd.concat([data["Close"].shift(1).rename("Shifted_Close"),data["Open"],currentShiftedSMA],axis=1).dropna(),constant,multiplier,"Shifted_SMA",modelParamsDict["frequency"])
            pfShiftedSMA_DF.loc[f"{constant}_{multiplier}","Shifted_SMA"] = pfShiftedSMA.sharpe_ratio()

            pfPred = backtestFromExposureSettings(pd.concat([data["Close"].shift(1).rename("Shifted_Close"),data["Open"],currentPredictedSMA],axis=1).dropna(),constant,multiplier,"Prediction",modelParamsDict["frequency"])
            pfPred_DF.loc[f"{constant}_{multiplier}","Pred"] = pfPred.sharpe_ratio()
    
    backtests_DF = pd.concat([pfShiftedSMA_DF,pfPred_DF],axis=1)
    backtests_DF["diff"] = backtests_DF["Pred"]-backtests_DF["Shifted_SMA"]

    return backtests_DF

Функция принимает следующие аргументы:

  • data: DataFrame, содержащий исторические данные.
  • exposureConstants: Список констант воздействия (b), подлежащих тестированию.
  • exposureMultipliers воздействия (a), подлежащих тестированию.
  • maPeriod: период времени для расчета простой скользящей средней.
  • model: модель машинного обучения, используемая для создания прогнозов.
  • modelParamsDict: словарь, содержащий параметры модели.
  • targetScaler: скейлер, используемый для преобразования целевой переменной.
  • scalers, используемых для предварительной обработки объектов.

Функция выполняет следующие действия:

  1. Вычисляет текущую SMA и смещенную SMA, используя заданный maPeriod.
  2. Генерирует прогнозы с помощью функции genPreds и объединяет их с текущим SMA.
  3. Инициализирует пустые кадры данных (pfShiftedSMA_DF и pfPred_DF) для хранения результатов бэктеста для смещенных прогнозов SMA и модели соответственно.
  4. Перебирает все комбинации констант экспозиции и множителей и для каждой комбинации:
    — Вызывает функцию backtestFromExposureSettings для выполнения бэктестов с использованием как смещенной SMA, так и прогнозов модели.
    — Вычисляет коэффициенты Шарпа для обоих бэктестов и сохраняет их в соответствующих кадрах данных.
  5. Объединяет два результирующих DataFrame (pfShiftedSMA_DF и pfPred_DF) в один DataFrame (backtests_DF) и вычисляет разницу между коэффициентами Шарпа прогнозов модели и смещенной SMA.
  6. Возвращает backtests_DF DataFrame, который содержит коэффициенты Шарпа для всех комбинаций параметров экспозиции и различия между прогнозами модели и сдвинутыми тестами SMA назад.

Обратите внимание, что есть некоторые пользовательские функции (например, genPreds), которые здесь не будут обсуждаться. Вы можете ознакомиться с ними в репозитории проекта.

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

Автоматизация тестирования на истории для всех обученных моделей

Функция backtestModels заботится об автоматизации процесса тестирования на истории для всех обученных моделей. Вот функция и разбивка шагов, которые она выполняет:

def backtestModels(maPeriod, exposureConstants, exposureMultipliers, pair, interval):
    
    if not os.path.exists("backtesting_ohlcv_data.csv"):
        start_date = "2017-01-01"
        data = vbt.BinanceData.download(
            pair,
            start=start_date,
            interval=interval).get(["Open", "High", "Low", "Close", "Volume"])
        data.to_csv("backtesting_ohlcv_data.csv")
    else:
        data = pd.read_csv("backtesting_ohlcv_data.csv", index_col=0)
        data.index = pd.to_datetime(data.index)
    
    modelName = f"articleModelSMA{maPeriod}"
    model, modelParamsDict, targetScaler, scalers = loadModel(modelName, data)
    
    backtests_DF = searchExposureBacktest(data, exposureConstants, exposureMultipliers, maPeriod, model, modelParamsDict, targetScaler, scalers)
    
    folder_name = f"modelBacktests/backtest_maPeriod_{maPeriod}"
    if not os.path.exists(folder_name):
        os.makedirs(folder_name)

    # Sort and save the DataFrame ordered by each column
    backtests_DF_sorted_shifted_sma = backtests_DF.sort_values(by="Shifted_SMA",ascending=False)
    backtests_DF_sorted_shifted_sma.to_csv(f"{folder_name}/backtest_sorted_shifted_sma.csv")

    backtests_DF_sorted_pred = backtests_DF.sort_values(by="Pred",ascending=False)
    backtests_DF_sorted_pred.to_csv(f"{folder_name}/backtest_sorted_pred.csv")

    backtests_DF_sorted_diff = backtests_DF.sort_values(by="diff")
    backtests_DF_sorted_diff.to_csv(f"{folder_name}/backtest_sorted_diff.csv")
    
    fig, axs = plt.subplots(2, 1, figsize=(20,6), sharex=True)
    backtests_DF[["Shifted_SMA", "Pred"]].replace([np.inf, -np.inf], np.nan).hist(bins=100, ax=axs)
    plt.savefig(f"{folder_name}/histogram.png")
    plt.close(fig)
  1. Сначала он проверяет, существует ли файл «backtesting_ohlcv_data.csv». Если нет, он загружает исторические данные с Binance с помощью vbt.BinanceData.download и сохраняет его в CSV-файл. Если файл существует, он считывает данные из файла и преобразует индекс в объекты datetime.
  2. Затем он загружает модель, параметры модели, целевое масштабирование и другие средства масштабирования с помощью функции loadModel.
  3. Затем вызывается функция searchExposureBacktest для вычисления результатов бэктеста на основе входных параметров, и результаты сохраняются в переменной backtests_DF.
  4. Создается папка с именем «modelBacktests/backtest_maPeriod_{maPeriod}», если она еще не существует.
  5. Результаты бэктеста сохраняются в CSV-файле в созданной папке.
  6. Наконец, гистограмма смещенных значений SMA и прогнозируемых значений генерируется и сохраняется в виде файла изображения в папке.

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

from botsFactoryLib import backtestModels


models = [3,6,12,18,24,36,48,96,168,336]

exposureConstants = [
    5,
    3.75,
    2.5,
    1.25,
    0,
    -1.25,
    -2.5,
    -3.75,
    -5,
]
exposureMultipliers = [
    1,
    2,
    5,
    10,
    20,
    50,
    100,
    500,
    1000,
]

for model in models:
    backtestModels(model, exposureConstants, exposureMultipliers, "BTCUSDT", "1h")

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

Анализ результатов поиска сетки на истории

Теперь проанализируем результирующие гистограммы для коэффициентов Шарпа каждой модели. Ось X представляет собой коэффициент Шарпа, а ось Y показывает частоту таких соотношений среди всех комбинаций протестированных параметров.

SMA3:

Лучший смещенный SMA Шарпа: 22.37

Лучший прогноз Шарпа: 0.67

Как самая близорукая модель, которую мы обучали, мы ожидали бы лучших результатов, если бы прогнозы были более точными. Однако, поскольку модель пытается предсказать слишком близко к текущему моменту (через 4 точки данных / часы), она имеет дело с большим количеством шума, что приводит в основном к отрицательным результатам.

SMA6:

Лучший смещенный SMA Шарпа: 20.26

Лучший прогноз Шарпа: 1.23

Интересно, что результаты прогнозирования в этой модели немного лучше по сравнению с моделью SMA 3, в то время как смещенная SMA работает хуже. Вероятно, это связано с уменьшением шума на старших таймфреймах — модель извлекает меньше паттернов из младших таймфреймов, в то время как смещенная SMA ведет себя как «идеальный» прогноз, который может предсказать весь случайный шум. Мы должны увидеть, что разница между прогнозом и смещенными SMA коэффициенты Шарпа уменьшаются по мере увеличения таймфрейма.

SМА12:

Лучший смещенный SMA Шарпа: 17.33

Лучший прогноз Шарпа: 2.21

Как и ожидалось, эта тенденция продолжается с лучшими результатами для этой модели.

SМА18:

Лучший смещенный SMA Шарпа: 15.83

Лучший прогноз Шарпа: 3.24

Закономерность сохраняется, и распределение коэффициентов Шарпа из бэктестов предсказаний входит в интересную область. Большинство из них имеют хорошие коэффициенты Шарпа, что должно вызвать некоторый скептицизм.

SМА24:

Лучший смещенный SMA Шарпа: 14.66

Лучший прогноз Шарпа: 3.98

Результаты этой модели продолжают улучшаться.

SМА36:

Лучший смещенный SMA Шарпа: 12.53

Лучший прогноз Шарпа: 5.40

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

SМА48:

Лучший смещенный SMA Шарпа: 11.45

Лучший прогноз Шарпа: 5.47

SМА96:

Лучший смещенный SMA Шарпа: 8.78

Лучший прогноз Шарпа: 5.39

Впервые паттерн нарушен: модель SMA96, похоже, имеет худшие результаты, чем SMA48, которая имеет меньший таймфрейм.

SМА168:

Лучший смещенный SMA Шарпа: 7.06

Лучший прогноз Шарпа: 4.50

Здесь повторяется инверсия паттерна.

SМА336:

Лучший смещенный SMA Шарпа: 5.50

Лучший прогноз Шарпа: 3.51

Опять же, худшие результаты прогнозирования. Интересно отметить, что смещенная SMA продолжала показывать худшие коэффициенты Шарпа, чем дольше таймфрейм. И, несмотря на то, что эта модель дает худшие результаты, чем SMA48, кажется, что гистограмма предсказания имеет меньшую дисперсию. Это может указывать на более стабильные результаты, хотя они, как правило, ниже. Результаты SMA с меньшим смещением указывают на меньший шум в целевой переменной, что указывает на целевую переменную с большей структурой по отношению к шуму, что лучше подходит для моделирования.

Наиболее эффективной моделью, согласно этому анализу, является SMA48 с коэффициентом Шарпа 5,47, протестированным на истории.

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

Исследование выбранной модели

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

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

Заключение

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

Поскольку мы имеем дело с временными рядами, данные должны быть дополнительно разделены заранее, чтобы приспособиться к обучению модели и разделу тестирования на истории, представленному в этой статье. Кроме того, обучение модели берет случайные записи из набора данных для создания тестовых и проверочных наборов поезда, что означает обработку его как данных поперечного сечения. Делать это не рекомендуется. Что-то вроде перекрестной проверки лучше. В целом, эти факторы, скорее всего, объясняют экстраординарные результаты в бэктестах.

Торговые прогнозы с использованием ИИ и Python

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

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

Чтобы продемонстрировать его эффективность, мы разработали доказательство концепции.

Этапы подготовки:

  • Установите последнюю версию Python и PIP.
  • Создайте проект Python с файлом «main.py«.
  • Добавьте в проект каталог «data«.
  • Настройте и активируйте виртуальную среду.
trading-ai-lstm $ python3 -m venv venv
trading-ai-lstm $ source venv/.bin/activate
(venv) trading-ai-lstm $
  • Создайте файл «requirements.txt».
pandas
numpy
scikit-learn
scipy
matplotlib
tensorflow
eodhd
python-dotenv
  • Убедитесь, что вы обновили PIP в виртуальной среде и установили зависимости.
(venv) trading-ai-lstm $ pip install --upgrade pip
(venv) trading-ai-lstm $ python3 -m pip install -r requirements.txt

Мы включили ключ API EODHD в файл .env.

API_TOKEN=<YOUR_API_KEY_GOES_HERE>

Все должно быть готово. Если вы используете VSCode и хотите использовать тот же файл «.vscode/settings.json«, что и наш, то вот он.

{
"python.formatting.provider": "none",
"python.formatting.blackArgs": ["--line-length", "160"],
"python.linting.flake8Args": [
"--max-line-length=160",
"--ignore=E203,E266,E501,W503,F403,F401,C901"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "information",
"reportMissingImports": "none"
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
}
}

Вот репозиторий GitHub для этого проекта на случай, если вам понадобится руководство.

Сборка кода

Первым шагом будет импорт необходимых библиотек.

import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "1"

import pickle
import pandas as pd
import numpy as np
from dotenv import load_dotenv
from sklearn.metrics import mean_squared_error, mean_absolute_error
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.models import load_model
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
from eodhd import APIClient

TensorFlow часто генерирует множество предупреждений и отладочных сведений по умолчанию. Мы предпочитаем более чистый и упорядоченный вывод, поэтому мы подавляем эти уведомления. Это достигается специальной строкой с помощью os.environ, следующей за импортом модуля «os».

Процесс обучения моделей машинного обучения и искусственного интеллекта влечет за собой значительную тонкую настройку, которая в первую очередь управляется с помощью так называемых гиперпараметров. Эта тема сложна, и освоение ее является своего рода искусством. На выбор оптимальных гиперпараметров влияют различные факторы. Основываясь на ежедневных данных S&P 500, которые мы получаем через API EODHD, мы начали с некоторых широко известных настроек. Рекомендуется изменить их, чтобы улучшить результаты. На данный момент рекомендуется поддерживать длину последовательности равной 20.

# Configurable hyperparameters
seq_length = 20
batch_size = 64
lstm_units = 50
epochs = 100

Следующий шаг включает в себя получение API_TOKEN вашего API EODHD из нашего файла «.env».

# Load environment variables from the .env file
load_dotenv()

# Retrieve the API key
API_TOKEN = os.getenv("API_TOKEN")

if API_TOKEN is not None:
print(f"API key loaded: {API_TOKEN[:4]}********")
else:
raise LookupError("Failed to load API key.")

Убедитесь, что у вас есть действительный API_TOKEN API EODHD для успешного доступа к данным.

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

def get_ohlc_data(use_cache: bool = False) -> pd.DataFrame:
ohlcv_file = "data/ohlcv.csv"

if use_cache:
if os.path.exists(ohlcv_file):
return pd.read_csv(ohlcv_file, index_col=None)
else:
api = APIClient(API_TOKEN)
df = api.get_historical_data(
symbol="HSPX.LSE",
interval="d",
iso8601_start="2010-05-17",
iso8601_end="2023-10-04",
)
df.to_csv(ohlcv_file, index=False)
return df
else:
api = APIClient(API_TOKEN)
return api.get_historical_data(
symbol="HSPX.LSE",
interval="d",
iso8601_start="2010-05-17",
iso8601_end="2023-10-04",
)

def create_sequences(data, seq_length):
x, y = [], []
for i in range(len(data) - seq_length):
x.append(data[i : i + seq_length])
y.append(data[i + seq_length, 3]) # The prediction target "close" is the 4th column (index 3)
return np.array(x), np.array(y)

def get_features(df: pd.DataFrame = None, feature_columns: list = ["open", "high", "low", "close", "volume"]) -> list:
return df[feature_columns].values

def get_target(df: pd.DataFrame = None, target_column: str = "close") -> list:
return df[target_column].values

def get_scaler(use_cache: bool = True) -> MinMaxScaler:
scaler_file = "data/scaler.pkl"

if use_cache:
if os.path.exists(scaler_file):
# Load the scaler
with open(scaler_file, "rb") as f:
return pickle.load(f)
else:
scaler = MinMaxScaler(feature_range=(0, 1))
with open(scaler_file, "wb") as f:
pickle.dump(scaler, f)
return scaler
else:
return MinMaxScaler(feature_range=(0, 1))

def scale_features(scaler: MinMaxScaler = None, features: list = []):
return scaler.fit_transform(features)

def get_lstm_model(use_cache: bool = False) -> Sequential:
model_file = "data/lstm_model.h5"

if use_cache:
if os.path.exists(model_file):
# Load the model
return load_model(model_file)
else:
# Train the LSTM model and save it
model = Sequential()
model.add(LSTM(units=lstm_units, activation='tanh', input_shape=(seq_length, 5)))
model.add(Dropout(0.2))
model.add(Dense(units=1))

model.compile(optimizer="adam", loss="mean_squared_error")
model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(x_test, y_test))

# Save the entire model to a HDF5 file
model.save(model_file)

return model

else:
# Train the LSTM model
model = Sequential()
model.add(LSTM(units=lstm_units, activation='tanh', input_shape=(seq_length, 5)))
model.add(Dropout(0.2))
model.add(Dense(units=1))

model.compile(optimizer="adam", loss="mean_squared_error")
model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(x_test, y_test))

return model

def get_predicted_x_test_prices(x_test: np.ndarray = None):
predicted = model.predict(x_test)

# Create a zero-filled matrix to aid in inverse transformation
zero_filled_matrix = np.zeros((predicted.shape[0], 5))

# Replace the 'close' column of zero_filled_matrix with the predicted values
zero_filled_matrix[:, 3] = np.squeeze(predicted)

# Perform inverse transformation
return scaler.inverse_transform(zero_filled_matrix)[:, 3]

def plot_x_test_actual_vs_predicted(actual_close_prices: list = [], predicted_x_test_close_prices = []) -> None:
# Plotting the actual and predicted close prices
plt.figure(figsize=(14, 7))
plt.plot(actual_close_prices, label="Actual Close Prices", color="blue")
plt.plot(predicted_x_test_close_prices, label="Predicted Close Prices", color="red")
plt.title("Actual vs Predicted Close Prices")
plt.xlabel("Time")
plt.ylabel("Price")
plt.legend()
plt.show()

def predict_next_close(df: pd.DataFrame = None, scaler: MinMaxScaler = None) -> float:
# Take the last X days of data and scale it
last_x_days = df.iloc[-seq_length:][["open", "high", "low", "close", "volume"]].values
last_x_days_scaled = scaler.transform(last_x_days)

# Reshape this data to be a single sequence and make the prediction
last_x_days_scaled = np.reshape(last_x_days_scaled, (1, seq_length, 5))

# Predict the future close price
future_close_price = model.predict(last_x_days_scaled)

# Create a zero-filled matrix for the inverse transformation
zero_filled_matrix = np.zeros((1, 5))

# Put the predicted value in the 'close' column (index 3)
zero_filled_matrix[0, 3] = np.squeeze(future_close_price)

# Perform the inverse transformation to get the future price on the original scale
return scaler.inverse_transform(zero_filled_matrix)[0, 3]

def evaluate_model(x_test: list = []) -> None:
# Evaluate the model
y_pred = model.predict(x_test)
mse = mean_squared_error(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mse)

print(f"Mean Squared Error: {mse}")
print(f"Mean Absolute Error: {mae}")
print(f"Root Mean Squared Error: {rmse}")

Одним из аспектов, который мы хотели бы подчеркнуть, является включение переменной «use_cache» в различные функции. Эта стратегия направлена на сокращение ненужных вызовов API к API EODHD и предотвращение избыточного повторного обучения модели с идентичными ежедневными данными. Активация переменной «use_cache» позволяет сохранять данные в файлы в каталоге «data/«. Если данные не существуют, они будут сгенерированы; Если он уже присутствует, он будет загружен. Такой подход значительно повышает эффективность при многократном выполнении скрипта. Чтобы получать свежие данные при каждом запуске, просто отключите опцию «use_cache» при вызове функции или очистите файлы в каталоге «data/«, достигнув того же результата.

Теперь перейдем к сути кода…

if __name__ == "__main__":
# Retrieve 3369 days of S&P 500 data
df = get_ohlc_data(use_cache=True)
print(df)

Первоначально мы получаем данные OHLCV из EODHD API и помещаем их в кадр данных Pandas с именем «df». OHLCV означает Open, High, Low, Close и Volume, которые являются стандартными атрибутами для данных торговых свечей. Как упоминалось ранее, кэширование включено для упрощения процесса. По желанию мы также облегчаем отображение этих данных на экране.

Мы рассмотрим следующий блок кода за один раз…

    features = get_features(df)
target = get_target(df)

scaler = get_scaler(use_cache=True)
scaled_features = scale_features(scaler, features)

x, y = create_sequences(scaled_features, seq_length)

train_size = int(0.8 * len(x)) # Create a train/test split of 80/20%
x_train, x_test = x[:train_size], x[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

# Re-shape input to fit lstm layer
x_train = np.reshape(x_train, (x_train.shape[0], seq_length, 5)) # 5 features
x_test = np.reshape(x_test, (x_test.shape[0], seq_length, 5)) # 5 features
  • «Признаки» включают в себя список входных данных, которые мы будем использовать для прогнозирования нашей цели, а именно «закрытия».
  • «target» включает в себя список целевых значений, таких как «close«.
  • «Scaler» представляет собой метод, используемый для нормализации цифр, делая их сопоставимыми. Например, наш набор данных может начинаться с близкого значения 784 и заканчиваться 3538. Более высокое значение в последней строке по своей сути не означает большую значимость для целей прогнозирования. Нормализация обеспечивает сопоставимость.
  • «scaled_features» — это результаты этого процесса масштабирования, которые мы будем использовать для обучения нашей модели ИИ.
  • «x_train» и «x_test» обозначают наборы данных, которые мы будем использовать для обучения и тестирования нашей модели ИИ, соответственно, при этом разделение 80/20 является обычной практикой. Это означает, что 80% наших торговых данных выделяется для обучения, а 20% зарезервировано для тестирования модели. Символ «x» указывает на то, что это объекты или входные данные.
  • «y_train» и «y_test» функционируют аналогично, но содержат только целевые значения, такие как «close«.
  • Наконец, данные должны быть преобразованы в соответствии с требованиями слоя LSTM.

Мы разработали функцию, которая позволяет либо заново обучать модель, либо загружать ранее обученную модель.

model = get_lstm_model(use_cache=True)

На представленном изображении можно получить представление о последовательности тренировок. Вы заметите, что изначально метрики «потери» и «val_loss» могут не совпадать. Однако ожидается, что по мере продолжения обучения эти цифры будут сближаться, что свидетельствует о прогрессе.

  • Потери: это среднеквадратичная ошибка (MSE), вычисленная для обучающего набора данных. Он отражает «стоимость» или «ошибку» между прогнозируемыми и истинными метками для каждой эпохи обучения. Цель состоит в том, чтобы уменьшить эту цифру через последовательные эпохи.
  • Val_loss. Эта среднеквадратичная ошибка, определяемая на проверочном наборе данных, измеряет производительность модели на данных, с которыми она не сталкивалась во время обучения. Он служит индикатором способности модели обобщать на новые, невиданные данные.

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

    predicted_x_test_close_prices = get_predicted_x_test_prices(x_test)
print("Predicted close prices:", predicted_x_test_close_prices)

Сами по себе данные могут быть не особенно наглядными или простыми для визуализации. Однако, сопоставив фактические цены закрытия с прогнозируемыми ценами закрытия (имея в виду, что это составляет 20% от всего набора данных), мы получаем более четкую картину, как показано ниже.

    # Plot the actual and predicted close prices for the test data
plot_x_test_actual_vs_predicted(df["close"].tail(len(predicted_x_test_close_prices)).values, predicted_x_test_close_prices)

Результаты показывают, что модель показала хорошие результаты в прогнозировании цены закрытия на этапе тестирования.

Теперь перейдем к наиболее ожидаемому аспекту: можем ли мы определить прогнозируемую цену закрытия на завтра?

   # Predict the next close price
predicted_next_close = predict_next_close(df, scaler)
print("Predicted next close price:", predicted_next_close)

Predicted next close price: 3536.906685638428

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

Если вы хотите оценить модель, вы можете включить это.

 # Evaluate the model
evaluate_model(x_test)

Что в нашем сценарии…

Mean Squared Error: 0.00021641664334765608
Mean Absolute Error: 0.01157513692221611
Root Mean Squared Error: 0.014711106122506767

Функции «mean_squared_error» и «mean_absolute_error», взятые из модуля метрик scikit-learn, используются для вычисления среднеквадратичной ошибки (MSE) и средней абсолютной ошибки (MAE) соответственно. Среднеквадратичная ошибка (RMSE) получается путем извлечения квадратного корня из MSE.

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

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

Источник

Источник

Источник