МАШИННОЕ ОБУЧЕНИЕ С PYTHON И SKLEARN
В данной серии статей мы рассмотрим конкретный кейс по прогнозированию стоимости актива с использованием языка программирования Python и простенькой библиотеки к нему sklearn.
Мы будем использовать самый простой и в тоже время самый наглядный алгоритм KNN (k-nearest neighbors) или по-русски k-ближайших соседей. Данный алгоритм относится к категории классификации, в нашем кейсе мы будем предсказывать, будет ли цена выше или ниже актива через заданный промежуток времени.
Ок, создадим проект, виртуальное окружение и активируем его.
mkdir predict && cd predict python -m venv .venv && source .venv/bin/activate
Установим через менеджер пакетов следующие модули, они нам пригодятся для работы
pip install requests pandas numpy matplotlib sklearn
Создадим новый .py файл и подключим все что нам нужно
from datetime import datetime import numpy as np import pandas as pd import matplotlib.pyplot as plt from joblib import dump, load from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import MinMaxScaler from src.Load import LoadBinanceHistoryData
Модуль LoadBinanceHistoryData нам нужен для формирования датасета, похожий класс мы писали в статье Выгрузка данных с биржи Binance, но лучше взять с гита тут.
Допишем вызов LoadBinanceHistoryData и выполним его, для формирования csv файла с данными, это наш будущий датафрейм, из него мы позднее создадим тренировочную и тестовую выборку.
market='SPOT' sym = 'BTCUSDT' tf = '1h' f = '2022-01-01 00:00:00' t = '2022-02-28 23:59:59' lb = LoadBinanceHistoryData(market, sym, tf, f, t) #lb.setProxy("ip address","port","login","password") lb.load()
Если проблемы с доступом к Binance, укажите ваш прокси в методе setProxy().
После запуска у нас должен будет сформироваться в директории нашего проекта новый файл
df = pd.read_csv('S_BTCUSDT_1h__20220101_0000__20220228_2359_.csv') df = df.rename(columns={ "Open time": "ts", "Open": "o", "High": "h", "Low": "l", "Close": "c", "Volume": "v", "Quote asset volume": "qav", "Number of trades": "not", "Taker buy base asset volume": "tbbav", "Taker buy quote asset volume": "tbqav" }) df = df.loc[len(df)-1000:] df["ts"] = [datetime.fromtimestamp(x) for x in df.ts] df = df.set_index('ts') df.drop(columns=['qav','tbbav','tbqav', 'not'], axis=1, inplace=True)
:1 — Подгружаем нашу ранее сформированную выгрузку с Binance
:2 — Переименовываем колонки для удобства
:15 -Ограничим наш датафрейм 1000 строками
:17 — Данные о временных рядах, загруженные с Binance в формате timestamp, для этого мы переводим его в читаемый формат
:18 — Устанавливаем дату и время в качестве индекса нашего датафрейма
:19 — Удаляем лишние колонки из датафрейма
Эксперемента ради, обоготим наш датасет новыми параметрами. Например посчитаем скользящие средние, а так же укажем их направления относительно друг друга. Возьмем скользящие средние по цене закрытия с периодами 5, 8 и 13
df['ema_f'] = df['c'].ewm(span=5, adjust=False).mean() df['ema_m'] = df['c'].ewm(span=8, adjust=False).mean() df['ema_l'] = df['c'].ewm(span=13, adjust=False).mean() df['ema_f_m__bin'] = (df['ema_f'] >= df['ema_m']).astype(int) df['ema_m_l__bin'] = (df['ema_m'] >= df['ema_l']).astype(int) df['ema_f_l__bin'] = (df['ema_f'] >= df['ema_l']).astype(int)
Добавим еще изменение в процентом соотношении между ценами открытия и закрытия и ценами минимума и максимума. Так же запихнем туда процентое соотношение бычьих и меджвежьих фитилей и самого тела свечи
df['p_chg'] = (df.c - df.o) / df.o *100 df['p_cdl_full_size'] = (df.h - df.l) / df.l * 100 df = df.assign(p_cdl_bottom_fitil=(abs((df.c - df.l) / df.l * 100)).where(df.o >= df.c, abs((df.o - df.l) / df.l * 100))) df = df.assign(p_cdl_top_fitil=(abs((df.h - df.o) / df.o * 100)).where(df.o >= df.c, abs((df.h - df.c) / df.c * 100))) df = df.assign(p_cdl_body=(abs((df.o - df.c) / df.c * 100)).where(df.o >= df.c, abs((df.c - df.o) / df.o * 100)))
Самый отвественный момент, отметить на тренировочной выборке ответы для нашей сети
percent = 2.0 df['diff_c3_buy'] = (df.h.shift(-2) - df.c) / df.c * 100 df['diff_c3_sell'] = (df.c - df.l.shift(-2)) / df.l * 100 df['diff_c3_buy_b'] = (((df.h.shift(-2) - df.c) / df.c * 100) > percent).astype(int) df['diff_c3_sell_b'] = (((df.c - df.l.shift(-2)) / df.l * 100) > percent).astype(int) df = df[:-2] y = np.array(list(zip(df['diff_c3_buy_b'].astype(int), df['diff_c3_sell_b'].astype(int)))) df_copy = df.copy() df = df.drop(columns=['diff_c3_buy', 'diff_c3_sell', 'diff_c3_buy_b', 'diff_c3_sell_b'], axis=1)
:1 — Задаем на какой процент должна отклониться цена, чтобы это послужило для нас сигналом
:2-3 — Вычисляем процентное соотношение между ценами закрытия 2 барами ранее и текущей максимальной или минимальной ценой, в зависимости от того на покупку или на продажу мы расчитываем сигнал. Сдвиг цены может быть указан любой, в моем случае это 2 бара.
:4-5 — Аналогичный расчет цены, но сверяем еще с нашим искомым процентом и в случае если условие удовлетворяется, проставляем 1 в качестве сигнала.
:6 — Так как наши расчеты были со смещением в 2 бара, очевидно, что последние 2 свечки мы расчитать не сможем, т.к. сдвигать уже нечего, по-этому мы их удаляем из нашего набора.
:7 — Из сигнальной выборки на покупку и продажу мы формируем новый список
:8 — Делаем копию нашего датафрейма
:9 — Удаляем из нашего основного датафрейма колонки с сигналами и их расчетами
np.set_printoptions(suppress=True) df.reset_index(drop=True, inplace=True) scaller = MinMaxScaler(feature_range=(-1,1)) df = np.array(df) df = scaller.fit_transform(df) X = np.around(df, decimals=3) Xtrain, Xtest, ytrain, ytest = train_test_split(X,y,test_size=.2,random_state=0)
Перед обучением удалим индексы из датафрейма, приведем данные к нужному виду. Нам нужно получить список со значениями от -1 до 1, возьмем для этого scaller из sklearn. И наконец разделим наш список с параметрами для обучения и список с правильными ответами еще на 2 списка, список для тренировки и для тестирования.
Теперь осталось обучить нашу сеть.
В блоке ниже мы создаем экземпляр класса нашей сетки и передаем в нее подготовленные ранее списки для обучения. Результат обучения нашей сети мы сохраняем в файл и в дальнейшем сможем работать с уже обученной сетью.
Так же закинем данные для тестирования и посмотрим на результат что у нас получился. Можно еще вывести информацию по нашим сигналам и размерам нашей тренировочной и тестовой выборках.
knn = KNeighborsClassifier(n_neighbors=5) knn.fit(Xtrain, ytrain) dump(knn, 's_btc_1h_2percent.joblib') classConf = knn.score(Xtest, ytest) print("KNeighborsClassifier confidence: ",classConf) p = knn.predict(X) df_copy['p_buy'], df_copy['p_sell'] = p.T print("Всего сигналов в LONG " + str((df_copy.p_buy == 1).sum())) print("Всего сигналов в SHORT " + str((df_copy.p_sell == 1).sum())) print("Размерность массива X_train: {}".format(Xtrain.shape)) print("Размерность массива у_train: {}".format(ytrain.shape)) print("Размерность массива Х test: {}".format(Xtest.shape)) print("Размерность массива y_test: {}".format(ytest.shape))
Отобразить наш результат можно с помощью графиков и библиотеки matplotlib
df.loc[df.p_buy == 1, 'predict_ii_buy'] = 1 * df.c + (df.c*0.001) df.loc[df.p_sell == 1, 'predict_ii_sell'] = 1 * df.c - (df.c*0.001) df.loc[df.diff_c3_buy_b == 1, 'predict_buy'] = 1 * df.c df.loc[df.diff_c3_sell_b == 1, 'predict_sell'] = 1 * df.c plt.style.use('fivethirtyeight') fig = plt.figure(figsize=(24.,6.)) ax = fig.add_subplot(1,1,1) count = 800 limit = -1 plt.plot(df.predict_buy[count:limit], color='g', marker='o', ms=25.,alpha=.1) plt.plot(df.predict_sell[count:limit], color='r', marker='o', ms=25.,alpha=.1) for t, c, b, s , sigb, sigs in zip(df.index.values[count:limit], df.c[count:limit], df.diff_c3_buy[count:limit],df.diff_c3_sell[count:limit], df.predict_ii_buy[count:limit], df.predict_ii_sell[count:limit]): if sigb > 0: plt.text(x=t, y=c-(c*0.005), s=str(b)[:4], horizontalalignment='center', color='darkgreen') if sigs > 0: plt.text(x=t, y=c+(c*0.005), s=str(s)[:4], horizontalalignment='center', color='red') plt.plot(df.predict_ii_buy[count:limit], color='g', marker='P', ms=10.) plt.plot(df.predict_ii_sell[count:limit], color='r', marker='P', ms=10.) plt.plot(df.c[count:limit], color='black', linewidth=2) plt.title('PREDICT', fontsize=24) plt.xlabel('Date', fontsize=18) plt.ylabel('Close price', fontsize=18) plt.show()
По итогу должен получиться следующий график
Где кружками отмечены потенциальные сигналы, но которые наша сеть не предсказала, это те места где цена изменится на 2 и более процента через 2 бара. А вот крестами отмечено то что получилось предсказать. Конечно данный классификатор не торгуется, он исключительно для знакомства. Готовый результат можно посмотреть в гите
Оригинал тут