Алгоритмическая торговля акциями с 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, а затем расширяя.