Алгоритмическая торговля акциями с XGBoost и фильтрами Калмана — стратегия

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

Выбор модели

В моем предыдущем проекте по созданию алгоритма ставок на футбол я обнаружил, что XGBoost является очень точной и точной моделью машинного обучения, поэтому казалось естественным, что он станет основой моего алгоритма прогнозирования акций. Я также пытался использовать другие алгоритмы, такие как нейронные сети, SVM и EMD, однако они оказались не такими прибыльными при использовании моих симуляций. Желая уменьшить шум в моих обучающих данных, я начал с подгонки линейных моделей к данным, чтобы найти коэффициенты, которые начали бы представлять долю текущей цены, которая определяется предыдущими временными шагами. В этом моделировании казалось, что 99% текущей цены можно определить по предыдущим двум моментам времени. Я назвал этот процесс «марковским» из-за его сходства с цепями Маркова, но использовал два предыдущих пункта для описания текущей цены, а не только предыдущей.

lr = np.linalg.lstsq(data_close[[‘lag_1',’lag_2']],data_close[‘close’], rcond=None)[0]
print("lr")
print(lr)

data_close[‘Markov’] = np.dot(data_close[[‘lag_1',’lag_2']],lr)

fig, ax = plt.subplots()
# Plot the first dataset
ax.plot(data_close['close'] , label='actual')
ax.plot(data_close['Markov'],label='Markov')
ax.legend(loc="upper left")
plt.show()

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

#Define target and features
X_train = train_data[['lag_1','lag_2']]
y_train = train_datax['close']
X_test = test_data[['lag_1','lag_2']]
y_test = test_datax['close']    
#Standard Scaler
X_train = X_train.apply(lambda x: (x - x.mean()) / (x.std()))
X_test = X_test.apply(lambda x: (x - x.mean()) / (x.std()))

Сглаживание

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

import xgboost as xgb
# Define classifier
classifier = xgb.XGBRegressor(random_state=30)

# Train classifier
classifier.fit(X_train, y_train)

# Test classifier
y_pred = classifier.predict(X_test)
y_pred = y_pred.flatten()

combined = pd.DataFrame(dict(actual=y_test, XGBoost=y_pred))

from scipy.signal import savgol_filter

data['smooth'] = savgol_filter(data['XGBoost'],window_length=3, polyorder=2)

#data['smooth'] = savgol_filter(data['smooth'],window_length=5, polyorder=3)
import numpy as np
import numpy as np
from pykalman import KalmanFilter

# Define the observation matrix, which is taken as an identity matrix in this example
observation_matrix = np.identity(1)

# Estimate the initial state mean and initial state covariance based on historical data
initial_state_mean = np.mean(data['smooth'])
initial_state_covariance = np.cov(data['smooth'])

# Define the transition matrix, which assumes a linear relationship between the state at time t and t-1
transition_matrix = np.array([[1]])

# Define the process noise covariance and observation noise covariance, which are assumed to be diagonal matrices with small values in this example
process_noise_covariance = np.array([[1e-5]])
observation_noise_covariance = np.array([[1e-3]])

# Create a KalmanFilter object
kf = KalmanFilter(
    transition_matrices=transition_matrix,
    observation_matrices=observation_matrix,
    initial_state_mean=initial_state_mean,
    initial_state_covariance=initial_state_covariance,
    #process_noise_covariance=process_noise_covariance
)

#Fit the Kalman filter to the financial data
filtered_state_means, filtered_state_covariances = kf.filter(data['smooth'])
data['Kalman'] = pd.DataFrame(filtered_state_means, index=data['smooth'].index)

# Create a figure and an axis
fig, ax = plt.subplots()
# Plot the first dataset
ax.plot(data['close'] , label='actual')

ax2 = ax.twinx()

#ax2.plot(data['ma3XGBoost'],'k',label='ma3XGBoost')
ax2.plot(data['smooth'],'c',label='smooth')

ax3 = ax.twinx()
ax3.plot(data['Kalman'],'m', label='Kalman filter')
#ax2.plot(data['fourier'],'m',label='fourier')

plt.title('Stock Movement')
ax.legend()
ax2.legend(loc='upper left')
ax3.legend(loc='lower left')
plt.show()

Разделение

Когда код запускается, он использует сбор данных за указанный период времени (подробнее об этом я напишу в статье о сборе и очистке данных о запасах). Затем данные о ценах за последний день разбиваются на тестовые данные (10%), а остальные 90% предназначены для обучения модели машинного обучения. Затем мы запускаем алгоритм, указанный выше.

Торговые индикаторы и моделирование

Мои торговые индикаторы были основаны на идее, что общий тренд моего сгенерированного XGBoost графика был полезен, поэтому использовалась модель торгового индикатора производной и второй производной. При определенных значениях (основанных на пробах и ошибках для достижения максимальной прибыли) дериватива у нас будет сигнал на покупку в длинную и короткую позиции, а также дополнительный сигнал на продажу. Используя тестовые данные, я написал симуляцию, чтобы рассчитать прибыль, которая будет получена, торговые издержки, которые я включил, были такими же, как у микро-контракта ES, просто чтобы позволить мне стандартизировать мои симуляции.

#Backtesting

#data[‘Gradient’] = data[‘XGBoost’] - data[‘XGBoost’].shift(1)
data[‘Gradient’] = data[‘Kalman’] - data[‘Kalman’].shift(1)
data[‘2Gradient’] = data[‘Kalman’] - data[‘Kalman’].shift(2)
data[‘SecondDeriv’] = data[‘Gradient’] - data[‘Gradient’].shift(1)

data[‘hour’] = data.index.hour
data[‘Trading’] = np.where(np.logical_and(data[‘hour’] >= 10,data[‘hour’] < 16),1,0)
#buy when gradient > 0.2, sell if gradient < 0 and buy = True

#Long Trades
#data[‘trades_L’] = np.where(np.logical_and(0.13<data[‘Gradient’],data[‘Gradient’]<0.18),1,0)

"""Current best strategies:

#1 - ES
data[‘trades_Buy_L’] = np.where(np.logical_and(data[‘SecondDeriv’]<-0.03,data[‘Gradient’]>0.04),1,0)
data[‘trades_Sell_L’] = np.where(np.logical_and(data[‘SecondDeriv’]>-0.01,data[‘Gradient’]<0.03),-1,0)

"""

data[‘trades_Buy_L’] = np.where(data[‘Trading’]==1, np.where(np.logical_and(data[‘SecondDeriv’]<-0.03, data[‘Gradient’]>0.02),1,0), 0)
data[‘trades_Sell_L’] = np.where(data[‘Trading’]==1, np.where(np.logical_and(data[‘SecondDeriv’]>-0.01, data[‘Gradient’]<0),-1,0), 0)

#Short Trades

data[‘trades_Buy_S’] = np.where(data[‘Trading’]==1, np.where(np.logical_and(data[‘SecondDeriv’]>0.03, data[‘Gradient’]<-0.04),1,0), 0)
data[‘trades_Sell_S’] = np.where(data[‘Trading’]==1, np.where(np.logical_and(data[‘SecondDeriv’]<0.01, data[‘Gradient’]>-0.03),-1,0), 0)


#Bet where we have RF is greater than previous point

data[‘Holding_L’] = np.where(data[‘trades_Buy_L’] == 1, 1, np.where(data[‘trades_Sell_L’] == -1, 0, np.nan))
data[‘Holding_L’].fillna(method=’ffill’, inplace=True)
data[‘prev_holding_L’] = data[‘Holding_L’].shift(1)

data[‘Holding_S’] = np.where(data[‘trades_Buy_S’] == 1, 1, np.where(data[‘trades_Sell_S’] == -1, 0, np.nan))
data[‘Holding_S’].fillna(method=’ffill’, inplace=True)
data[‘prev_holding_S’] = data[‘Holding_S’].shift(1)


#Calculating where trades are made
data[‘change_L’] = np.where((data[‘Holding_L’] == 1) & (data[‘prev_holding_L’] == 0), 1, np.where((data[‘Holding_L’] == 0) & (data[‘prev_holding_L’] == 1), -1, 0))
data[‘change_S’] = np.where((data[‘Holding_S’] == 1) & (data[‘prev_holding_S’] == 0), 1, np.where((data[‘Holding_S’] == 0) & (data[‘prev_holding_S’] == 1), -1, 0))

# Generate trades, we trade if over a sufficient number

hold_mask_L = data[‘change_L’] == 1
hold_mask_S = data[‘change_S’] == 1

# Create a boolean array for when the holding is 0
not_hold_mask_L = data[‘change_L’] == -1
not_hold_mask_S = data[‘change_S’] == -1

""" Plotting """
# Plot the Close values in green when holding is 1
plt.plot(data[hold_mask_L].index, data[hold_mask_L][‘close’], ‘g.’, label=’Bought_L’,markersize=10)

plt.plot(data[hold_mask_S].index, data[hold_mask_S][‘close’], ‘k.’, label=’Bought_S’,markersize=10)

# Plot the Close values in red when holding is 0
plt.plot(data[not_hold_mask_L].index, data[not_hold_mask_L][‘close’], ‘r.’, label=’Sold_L’,markersize=10)

plt.plot(data[not_hold_mask_S].index, data[not_hold_mask_S][‘close’], ‘m.’, label=’Sold_S’,markersize=10)

plt.plot(data[‘close’], label= ‘Close’, dashes=[3, 1])

# Add a legend to the plot
plt.legend()



#Calculation of profit
data[‘profit_L’] = data[‘Holding_L’] * (data[‘close’] - data[‘close’].shift(1))
profit_L = data[‘profit_L’].sum()
   
data[‘profit_S’] = data[‘Holding_S’] * (data[‘close’].shift(1)-data[‘close’])
profit_S = data[‘profit_S’].sum()
data[‘cumprofit’] = data[‘profit_S’].cumsum() + data[‘profit_L’].cumsum()

Long_profit = data[‘profit_L’].sum()*50
Short_profit = data[‘profit_S’].sum()*50

print("Long Profit multiplier",data[‘profit_L’].sum())
print("Short Profit multiplier",data[‘profit_S’].sum())
print("Long Profit",Long_profit)
print("Short Profit",Short_profit)


#data.to_csv("UnFunctioned_V1.1.csv")
Number_of_trades_L = (data[‘trades_Buy_L’].sum())
Number_of_trades_S = (data[‘trades_Buy_S’].sum())
print("Number of trades (Long)",Number_of_trades_L)
print("Number of trades (Short)",Number_of_trades_S)

Trading_costs_micro = (Number_of_trades_L+Number_of_trades_S)*0.25

Total_profit = (Long_profit + Short_profit) - Trading_costs_micro
print("Profit",Total_profit)

На изображении ниже показана динамика прибыли, полученной в результате моделирования в указанные дни. V1 — алгоритм, указанный выше, с фильтром Савголя, но без фильтра Калмана. Значения слева торгуются с акциями Microsoft, а значения справа торгуются с фьючерсами ES. TP представляет собой общую прибыль.

Внешнее моделирование

После этого я подключил свой код к OANDA с ​​помощью бесплатного API для бумажной торговли и собираю результаты за более длительный период, чтобы установить эффективность торгового алгоритма. Фрагмент кода можно увидеть ниже, использование внешнего программного обеспечения для моделирования позволяет мне действительно проверить алгоритм не только на модели, которую я создал.

    #Sell

    if signal == 1:

        #mo is market order

        mo = MarketOrderRequest(instrument="SPX500_USD", units=-1, takeProfitOnFill=TakeProfitDetails(price=TPSell).data, stopLossOnFill=StopLossDetails(price=SLSell).data)

        r = orders.OrderCreate(accountID, data=mo.data)

        rv = client.request(r)

        print(rv) #just to see that order has passed

    #Buy

    elif signal == 2:

        mo = MarketOrderRequest(instrument="SPX500_USD", units=1, takeProfitOnFill=TakeProfitDetails(price=TPBuy).data, stopLossOnFill=StopLossDetails(price=SLBuy).data)

        r = orders.OrderCreate(accountID, data=mo.data)

        rv = client.request(r)

        print(rv)



#trading_job()



scheduler = BlockingScheduler()

scheduler.add_job(trading_job, 'cron', day_of_week='mon-fri', hour='10-17',start_date='2022-02-13 10:00:00', timezone='Europe/London')#minute='1,16,31,46'

scheduler.start()

Как только станет очевидным, что алгоритм кодирования жизнеспособен и не подвержен большим убыткам (из-за разорения игрока и т. д.), настанет время подключить алгоритм к брокеру и торговать реальными средствами, начиная с фьючерсов ES, а затем расширяя.