В данной серии статей мы рассмотрим конкретный кейс по прогнозированию стоимости актива с использованием языка программирования 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 бара. А вот крестами отмечено то что получилось предсказать. Конечно данный классификатор не торгуется, он исключительно для знакомства. Готовый результат можно посмотреть в гите