Торговля акциями с фильтрами XGBoost и Калмана

Алгоритм бумажной торговли на OANDA
  • Торговля акциями с фильтрами 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, а затем расширяясь.

Раскрытие возможностей фильтра Калмана

Как специалист по обработке и анализу данных, мы иногда сталкиваемся с ситуациями, когда нам нужно смоделировать тренд для прогнозирования будущих значений. Несмотря на то, что существует искушение сосредоточиться на статистических алгоритмах или алгоритмах, основанных на машинном обучении, я здесь, чтобы представить другой вариант: фильтр Калмана (KF).

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

В этом блоге мы подробно рассмотрим, как работает фильтр Калмана, показав примеры на Python, которые подчеркнут истинную мощь этого метода. Начав с простого 2D-примера, мы увидим, как мы можем модифицировать код, чтобы адаптировать его к более сложным 4D-пространствам, и закончим рассмотрением расширенного фильтра Калмана (сложного предшественника). Присоединяйтесь ко мне в этом путешествии, когда мы отправимся в мир алгоритмов прогнозирования и фильтров.

Основы работы фильтра Калмана

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

Шаг прогнозирования:

Эта фаза посвящена прогнозированию следующего будущего состояния системы на основе известных апостериорных оценок модели и шага во времени Δk. Математически мы представляем оценки пространства состояний в виде:

где, F, наша матрица переходов состояний моделирует эволюцию состояний от шага к шагу независимо от управляющего входа и шума процесса. Наша матрица B моделирует управляющий вход, uk, влияние на состояние.

Наряду с нашими оценками следующего состояния, алгоритм также вычисляет неопределенность оценки, представленной ковариационной матрицей P:

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

Шаг обновления:

На этапе обновления алгоритм обновляет коэффициент усиления Калмана, оценки состояния и ковариационную матрицу. Коэффициент усиления Калмана определяет, какое влияние новое измерение должно оказать на оценки штата. Расчет включает в себя матрицу модели наблюдения, H, которая связывает состояние с измерением, которое мы ожидаем получить, и R — ковариационную матрицу шума измерений ошибок измерений:

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

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

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

Фильтр Калмана в действии

Уравнения, лежащие в основе фильтра Калмана, могут сильно увлечься, и чтобы полностью оценить, как он работает, исходя только из математики, потребуется понимание пространства состояний (выходящее за рамки этого блога), но я попытаюсь воплотить его в жизнь на нескольких примерах Python. В простейшей форме мы можем определить объект фильтра Калмана как:import numpy as np

class KalmanFilter:
«»»
An implementation of the classic Kalman Filter for linear dynamic systems.
The Kalman Filter is an optimal recursive data processing algorithm which
aims to estimate the state of a system from noisy observations.

Attributes:
F (np.ndarray): The state transition matrix.
B (np.ndarray): The control input marix.
H (np.ndarray): The observation matrix.
u (np.ndarray): the control input.
Q (np.ndarray): The process noise covariance matrix.
R (np.ndarray): The measurement noise covariance matrix.
x (np.ndarray): The mean state estimate of the previous step (k-1).
P (np.ndarray): The state covariance of previous step (k-1).
«»»
def __init__(self, F, B, u, H, Q, R, x0, P0):
«»»
Initializes the Kalman Filter with the necessary matrices and initial state.

Parameters:
F (np.ndarray): The state transition matrix.
B (np.ndarray): The control input marix.
H (np.ndarray): The observation matrix.
u (np.ndarray): the control input.
Q (np.ndarray): The process noise covariance matrix.
R (np.ndarray): The measurement noise covariance matrix.
x0 (np.ndarray): The initial state estimate.
P0 (np.ndarray): The initial state covariance matrix.
«»»
self.F = F # State transition matrix
self.B = B # Control input matrix
self.u = u # Control vector
self.H = H # Observation matrix
self.Q = Q # Process noise covariance
self.R = R # Measurement noise covariance
self.x = x0 # Initial state estimate
self.P = P0 # Initial estimate covariance

def predict(self):
«»»
Predicts the state and the state covariance for the next time step.
«»»
self.x = self.F @ self.x + self.B @ self.u
self.P = self.F @ self.P @ self.F.T + self.Q
return self.x

def update(self, z):
«»»
Updates the state estimate with the latest measurement.

Parameters:
z (np.ndarray): The measurement at the current step.
«»»
y = z — self.H @ self.x
S = self.H @ self.P @ self.H.T + self.R
K = self.P @ self.H.T @ np.linalg.inv(S)
self.x = self.x + K @ y
I = np.eye(self.P.shape[0])
self.P = (I — K @ self.H) @ self.P

return self.xChallenges with Non-linear Systems

Мы будем использовать функции predict()() и update() для итерации шагов, описанных ранее. Приведенная выше конструкция фильтра будет работать для любого временного ряда, и чтобы показать, как наши оценки соотносятся с фактическими, построим простой пример:import numpy as np
import matplotlib.pyplot as plt

# Set the random seed for reproducibility
np.random.seed(42)

# Simulate the ground truth position of the object
true_velocity = 0.5 # units per time step
num_steps = 50
time_steps = np.linspace(0, num_steps, num_steps)
true_positions = true_velocity * time_steps

# Simulate the measurements with noise
measurement_noise = 10 # increase this value to make measurements noisier
noisy_measurements = true_positions + np.random.normal(0, measurement_noise, num_steps)

# Plot the true positions and the noisy measurements
plt.figure(figsize=(10, 6))
plt.plot(time_steps, true_positions, label=’True Position’, color=’green’)
plt.scatter(time_steps, noisy_measurements, label=’Noisy Measurements’, color=’red’, marker=’o’)

plt.xlabel(‘Time Step’)
plt.ylabel(‘Position’)
plt.title(‘True Position and Noisy Measurements Over Time’)
plt.legend()
plt.show()

В действительности «Истинное положение» было бы неизвестно, но мы построим его здесь для справки, «Измерения шума» — это точки наблюдения, которые подаются в наш фильтр Калмана. Мы выполним очень простое создание экземпляров матриц, и в какой-то степени это не имеет значения, так как модель Калмана будет быстро сходиться благодаря применению коэффициента усиления Калмана, но при определенных обстоятельствах может быть разумно выполнить теплое начало модели.# Kalman Filter Initialization
F = np.array([[1, 1], [0, 1]]) # State transition matrix
B = np.array([[0], [0]]) # No control input
u = np.array([[0]]) # No control input
H = np.array([[1, 0]]) # Measurement function
Q = np.array([[1, 0], [0, 3]]) # Process noise covariance
R = np.array([[measurement_noise**2]]) # Measurement noise covariance
x0 = np.array([[0], [0]]) # Initial state estimate
P0 = np.array([[1, 0], [0, 1]]) # Initial estimate covariance

kf = KalmanFilter(F, B, u, H, Q, R, x0, P0)

# Allocate space for estimated positions and velocities
estimated_positions = np.zeros(num_steps)
estimated_velocities = np.zeros(num_steps)

# Kalman Filter Loop
for t in range(num_steps):
# Predict
kf.predict()

# Update
measurement = np.array([[noisy_measurements[t]]])
kf.update(measurement)

# Store the filtered position and velocity
estimated_positions[t] = kf.x[0]
estimated_velocities[t] = kf.x[1]

# Plot the true positions, noisy measurements, and the Kalman filter estimates
plt.figure(figsize=(10, 6))
plt.plot(time_steps, true_positions, label=’True Position’, color=’green’)
plt.scatter(time_steps, noisy_measurements, label=’Noisy Measurements’, color=’red’, marker=’o’)
plt.plot(time_steps, estimated_positions, label=’Kalman Filter Estimate’, color=’blue’)

plt.xlabel(‘Time Step’)
plt.ylabel(‘Position’)
plt.title(‘Kalman Filter Estimation Over Time’)
plt.legend()
plt.show()

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

Слежение за движущимся объектом в 4D

Предположим, что мы хотим проследить движение объекта в пространстве и времени, и чтобы сделать этот пример еще более реалистичным, мы смоделируем некую силу, действующую на него, приводящую к угловому вращению. Чтобы показать приспособляемость этого алгоритма к представлениям в пространстве состояний более высокой размерности, мы будем принимать линейную силу, хотя на самом деле это не так (после этого мы рассмотрим более реалистичный пример). В приведенном ниже коде показан пример того, как мы могли бы изменить наш фильтр Калмана для этого конкретного сценария.import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class KalmanFilter:
«»»
An implementation of the classic Kalman Filter for linear dynamic systems.
The Kalman Filter is an optimal recursive data processing algorithm which
aims to estimate the state of a system from noisy observations.

Attributes:
F (np.ndarray): The state transition matrix.
B (np.ndarray): The control input marix.
H (np.ndarray): The observation matrix.
u (np.ndarray): the control input.
Q (np.ndarray): The process noise covariance matrix.
R (np.ndarray): The measurement noise covariance matrix.
x (np.ndarray): The mean state estimate of the previous step (k-1).
P (np.ndarray): The state covariance of previous step (k-1).
«»»
def __init__(self, F=None, B=None, u=None, H=None, Q=None, R=None, x0=None, P0=None):
«»»
Initializes the Kalman Filter with the necessary matrices and initial state.

Parameters:
F (np.ndarray): The state transition matrix.
B (np.ndarray): The control input marix.
H (np.ndarray): The observation matrix.
u (np.ndarray): the control input.
Q (np.ndarray): The process noise covariance matrix.
R (np.ndarray): The measurement noise covariance matrix.
x0 (np.ndarray): The initial state estimate.
P0 (np.ndarray): The initial state covariance matrix.
«»»
self.F = F # State transition matrix
self.B = B # Control input matrix
self.u = u # Control input
self.H = H # Observation matrix
self.Q = Q # Process noise covariance
self.R = R # Measurement noise covariance
self.x = x0 # Initial state estimate
self.P = P0 # Initial estimate covariance

def predict(self):
«»»
Predicts the state and the state covariance for the next time step.
«»»
self.x = np.dot(self.F, self.x) + np.dot(self.B, self.u)
self.P = np.dot(np.dot(self.F, self.P), self.F.T) + self.Q

def update(self, z):
«»»
Updates the state estimate with the latest measurement.

Parameters:
z (np.ndarray): The measurement at the current step.
«»»
y = z — np.dot(self.H, self.x)
S = np.dot(self.H, np.dot(self.P, self.H.T)) + self.R
K = np.dot(np.dot(self.P, self.H.T), np.linalg.inv(S))
self.x = self.x + np.dot(K, y)
self.P = self.P — np.dot(np.dot(K, self.H), self.P)

# Parameters for simulation
true_angular_velocity = 0.1 # radians per time step
radius = 20
num_steps = 100
dt = 1 # time step

# Create time steps
time_steps = np.arange(0, num_steps*dt, dt)

# Ground truth state
true_x_positions = radius * np.cos(true_angular_velocity * time_steps)
true_y_positions = radius * np.sin(true_angular_velocity * time_steps)
true_z_positions = 0.5 * time_steps # constant velocity in z

# Create noisy measurements
measurement_noise = 1.0
noisy_x_measurements = true_x_positions + np.random.normal(0, measurement_noise, num_steps)
noisy_y_measurements = true_y_positions + np.random.normal(0, measurement_noise, num_steps)
noisy_z_measurements = true_z_positions + np.random.normal(0, measurement_noise, num_steps)

# Kalman Filter initialization
F = np.array([[1, 0, 0, -radius*dt*np.sin(true_angular_velocity*dt)],
[0, 1, 0, radius*dt*np.cos(true_angular_velocity*dt)],
[0, 0, 1, 0],
[0, 0, 0, 1]])
B = np.zeros((4, 1))
u = np.zeros((1, 1))
H = np.array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0]])
Q = np.eye(4) * 0.1 # Small process noise
R = measurement_noise**2 * np.eye(3) # Measurement noise
x0 = np.array([[0], [radius], [0], [true_angular_velocity]])
P0 = np.eye(4) * 1.0

kf = KalmanFilter(F, B, u, H, Q, R, x0, P0)

# Allocate space for estimated states
estimated_states = np.zeros((num_steps, 4))

# Kalman Filter Loop
for t in range(num_steps):
# Predict
kf.predict()

# Update
z = np.array([[noisy_x_measurements[t]],
[noisy_y_measurements[t]],
[noisy_z_measurements[t]]])
kf.update(z)

# Store the state
estimated_states[t, :] = kf.x.ravel()

# Extract estimated positions
estimated_x_positions = estimated_states[:, 0]
estimated_y_positions = estimated_states[:, 1]
estimated_z_positions = estimated_states[:, 2]

# Plotting
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection=’3d’)

# Plot the true trajectory
ax.plot(true_x_positions, true_y_positions, true_z_positions, label=’True Trajectory’, color=’g’)
# Plot the start and end markers for the true trajectory
ax.scatter(true_x_positions[0], true_y_positions[0], true_z_positions[0], label=’Start (Actual)’, c=’green’, marker=’x’, s=100)
ax.scatter(true_x_positions[-1], true_y_positions[-1], true_z_positions[-1], label=’End (Actual)’, c=’red’, marker=’x’, s=100)

# Plot the noisy measurements
ax.scatter(noisy_x_measurements, noisy_y_measurements, noisy_z_measurements, label=’Noisy Measurements’, color=’r’)

# Plot the estimated trajectory
ax.plot(estimated_x_positions, estimated_y_positions, estimated_z_positions, label=’Estimated Trajectory’, color=’b’)

# Plot settings
ax.set_xlabel(‘X position’)
ax.set_ylabel(‘Y position’)
ax.set_zlabel(‘Z position’)
ax.set_title(‘3D Trajectory Estimation with Kalman Filter’)
ax.legend()

plt.show()

Здесь следует отметить несколько интересных моментов: на графике выше мы можем увидеть, как модель быстро корректируется до предполагаемого истинного состояния, когда мы начинаем итерацию по наблюдениям. Модель также достаточно хорошо справляется с определением истинного состояния системы, при этом оценки пересекаются с истинными состояниями («истинная траектория»). Такая конструкция может подойти для некоторых реальных приложений, но не для тех, где сила, действующая на систему, нелинейна. Вместо этого нам нужно рассмотреть другое применение фильтра Калмана: расширенный фильтр Калмана, предшественник того, что мы исследовали до сих пор, который линеаризирует нелинейность поступающих наблюдений.

Расширенный фильтр Калмана

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

для предсказания состояния, где f — наша нелинейная функция перехода состояния, примененная к оценке предыдущего состояния, а u — управляющий вход на предыдущем временном шаге.

для предсказания ковариации ошибок, где F – якобиан функции перехода состояния относительно P – ковариации предыдущей ошибки, а Q – ковариационной матрицы шума процесса.

наблюдение нашего измерения, z, на временном шаге k, где h — нелинейная функция наблюдения, применяемая к предсказанию состояния с некоторым аддитивным наблюдательным шумом v.

Мы обновили расчет коэффициента усиления Калмана, где H — якобиан функции наблюдения по отношению к состоянию, а R — ковариационная матрица шума измерения.

Модифицированное вычисление для оценки состояния, которое включает в себя коэффициент усиления Калмана и нелинейную функцию наблюдения, и, наконец, наше уравнение для обновления ковариации ошибки:

В последнем примере мы будем использовать Jocabian для линеаризации нелинейного влияния углового вращения на наш объект, соответствующим образом модифицируя код. Проектирование EKF является более сложной задачей, чем KF, поскольку наше предположение о линейных аппроксимациях первого порядка может непреднамеренно внести ошибки в наши оценки состояния. Кроме того, вычисление Якоби может стать сложным, ресурсоемким и трудным для определения в определенных ситуациях, что также может привести к ошибкам. Однако при правильном проектировании EKF часто превосходит реализацию KF.

Основываясь на нашем последнем примере на Python, я представил реализацию EKF:import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class ExtendedKalmanFilter:
«»»
An implementation of the Extended Kalman Filter (EKF).
This filter is suitable for systems with non-linear dynamics by linearising
the system model at each time step using the Jacobian.

Attributes:
state_transition (callable): The state transition function for the system.
jacobian_F (callable): Function to compute the Jacobian of the state transition.
H (np.ndarray): The observation matrix.
jacobian_H (callable): Function to compute the Jacobian of the observation model.
Q (np.ndarray): The process noise covariance matrix.
R (np.ndarray): The measurement noise covariance matrix.
x (np.ndarray): The initial state estimate.
P (np.ndarray): The initial estimate covariance.
«»»
def __init__(self, state_transition, jacobian_F, observation_matrix, jacobian_H, Q, R, x, P):
«»»
Constructs the Extended Kalman Filter.

Parameters:
state_transition (callable): The state transition function.
jacobian_F (callable): Function to compute the Jacobian of F.
observation_matrix (np.ndarray): Observation matrix.
jacobian_H (callable): Function to compute the Jacobian of H.
Q (np.ndarray): Process noise covariance.
R (np.ndarray): Measurement noise covariance.
x (np.ndarray): Initial state estimate.
P (np.ndarray): Initial estimate covariance.
«»»
self.state_transition = state_transition # Non-linear state transition function
self.jacobian_F = jacobian_F # Function to compute Jacobian of F
self.H = observation_matrix # Observation matrix
self.jacobian_H = jacobian_H # Function to compute Jacobian of H
self.Q = Q # Process noise covariance
self.R = R # Measurement noise covariance
self.x = x # Initial state estimate
self.P = P # Initial estimate covariance

def predict(self, u):
«»»
Predicts the state at the next time step.

Parameters:
u (np.ndarray): The control input vector.
«»»
self.x = self.state_transition(self.x, u)
F = self.jacobian_F(self.x, u)
self.P = F @ self.P @ F.T + self.Q

def update(self, z):
«»»
Updates the state estimate with a new measurement.

Parameters:
z (np.ndarray): The measurement vector.
«»»
H = self.jacobian_H()
y = z — self.H @ self.x
S = H @ self.P @ H.T + self.R
K = self.P @ H.T @ np.linalg.inv(S)
self.x = self.x + K @ y
self.P = (np.eye(len(self.x)) — K @ H) @ self.P

# Define the non-linear transition and Jacobian functions
def state_transition(x, u):
«»»
Defines the state transition function for the system with non-linear dynamics.

Parameters:
x (np.ndarray): The current state vector.
u (np.ndarray): The control input vector containing time step and rate of change of angular velocity.

Returns:
np.ndarray: The next state vector as predicted by the state transition function.
«»»
dt = u[0]
alpha = u[1]
x_next = np.array([
x[0] — x[3] * x[1] * dt,
x[1] + x[3] * x[0] * dt,
x[2] + x[3] * dt,
x[3],
x[4] + alpha * dt
])
return x_next

def jacobian_F(x, u):
«»»
Computes the Jacobian matrix of the state transition function.

Parameters:
x (np.ndarray): The current state vector.
u (np.ndarray): The control input vector containing time step and rate of change of angular velocity.

Returns:
np.ndarray: The Jacobian matrix of the state transition function at the current state.
«»»
dt = u[0]
# Compute the Jacobian matrix of the state transition function
F = np.array([
[1, -x[3]*dt, 0, -x[1]*dt, 0],
[x[3]*dt, 1, 0, x[0]*dt, 0],
[0, 0, 1, dt, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]
])
return F

def jacobian_H():
# Jacobian matrix for the observation function is simply the observation matrix
return H

# Simulation parameters
num_steps = 100
dt = 1.0
alpha = 0.01 # Rate of change of angular velocity

# Observation matrix, assuming we can directly observe the x, y, and z position
H = np.eye(3, 5)

# Process noise covariance matrix
Q = np.diag([0.1, 0.1, 0.1, 0.1, 0.01])

# Measurement noise covariance matrix
R = np.diag([0.5, 0.5, 0.5])

# Initial state estimate and covariance
x0 = np.array([0, 20, 0, 0.5, 0.1]) # [x, y, z, v, omega]
P0 = np.eye(5)

# Instantiate the EKF
ekf = ExtendedKalmanFilter(state_transition, jacobian_F, H, jacobian_H, Q, R, x0, P0)

# Generate true trajectory and measurements
true_states = []
measurements = []
for t in range(num_steps):
u = np.array([dt, alpha])
true_state = state_transition(x0, u) # This would be your true system model
true_states.append(true_state)
measurement = true_state[:3] + np.random.multivariate_normal(np.zeros(3), R) # Simulate measurement noise
measurements.append(measurement)
x0 = true_state # Update the true state

# Now we run the EKF over the measurements
estimated_states = []
for z in measurements:
ekf.predict(u=np.array([dt, alpha]))
ekf.update(z=np.array(z))
estimated_states.append(ekf.x)

# Convert lists to arrays for plotting
true_states = np.array(true_states)
measurements = np.array(measurements)
estimated_states = np.array(estimated_states)

# Plotting the results
fig = plt.figure(figsize=(12, 9))
ax = fig.add_subplot(111, projection=’3d’)

# Plot the true trajectory
ax.plot(true_states[:, 0], true_states[:, 1], true_states[:, 2], label=’True Trajectory’)
# Increase the size of the start and end markers for the true trajectory
ax.scatter(true_states[0, 0], true_states[0, 1], true_states[0, 2], label=’Start (Actual)’, c=’green’, marker=’x’, s=100)
ax.scatter(true_states[-1, 0], true_states[-1, 1], true_states[-1, 2], label=’End (Actual)’, c=’red’, marker=’x’, s=100)

# Plot the measurements
ax.scatter(measurements[:, 0], measurements[:, 1], measurements[:, 2], label=’Measurements’, alpha=0.6)
# Plot the start and end markers for the measurements
ax.scatter(measurements[0, 0], measurements[0, 1], measurements[0, 2], c=’green’, marker=’o’, s=100)
ax.scatter(measurements[-1, 0], measurements[-1, 1], measurements[-1, 2], c=’red’, marker=’x’, s=100)

# Plot the EKF estimate
ax.plot(estimated_states[:, 0], estimated_states[:, 1], estimated_states[:, 2], label=’EKF Estimate’)

ax.set_xlabel(‘X’)
ax.set_ylabel(‘Y’)
ax.set_zlabel(‘Z’)
ax.legend()

plt.show()

Простое резюме

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

КФ: Эта модель применима к линейным системам, где можно предположить, что матрицы переходов состояний и сохранения состояния являются линейными функциями состояния (с некоторым гауссовским шумом). Этот алгоритм можно применить для:

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

Ключевое преимущество для КФ заключается в том, что (если следовать матричным вычислениям) алгоритм достаточно прост, менее вычислительно менее емок, чем другие подходы, и может давать очень точные прогнозы и оценки истинного состояния системы во времени. Недостатком является предположение о линейности, которое, как правило, не выполняется в сложных сценариях реального мира.

EKF: По существу, мы можем рассматривать EKF как нелинейный эквивалент KF, поддерживаемый использованием якобиана. Вам следует рассмотреть EKF, если вы имеете дело с:

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

EKF часто дает более точные оценки, чем KF, особенно для нелинейных систем, однако он может стать гораздо более ресурсоемким из-за вычисления матрицы Якоби. Этот подход также опирается на линейные приближения первого порядка из разложения рядов Тейлора, что может быть неподходящим предположением для сильно нелинейных систем. Добавление Якобиана также может усложнить проектирование модели, и поэтому, несмотря на ее преимущества, может быть более целесообразным реализовать KF для простоты и совместимости.

Парная торговля с фильтром Калмана и скрытой марковской моделью

Кванты любят статистический арбитраж. Парная торговля — отличный способ начать работу с алгоритмической торговлей. — Новости PyQuant

Tего блог представляет собой краткое изложение очень интересного статистического исследования торговой стратегии арбитража, в котором исследуется стратегия парной торговли с использованием надежного фильтра Калмана и скрытой модели Маркова. Исследование направлено на воспроизведение и уточнение новой алгоритмической торговой стратегии, предложенной Johnson-Skinner et al. (2021), которая использует прогнозы волатильности инноваций на основе данных для одновременного прогнозирования коэффициента хеджирования и волатильности спреда. Стратегия также использует скрытую марковскую модель для оптимизации пороговых значений торговых сигналов в разных рыночных режимах. В посте обсуждается методология и представлены результаты исследования, а также причины, по которым стратегия не дала удовлетворительных результатов.

Пожалуйста, найдите полный набор кодов и исследовательское эссе по ссылке ниже.

Знакомство

Статистические арбитражные стратегии, как и парная торговля, в последнее время стали суперпопулярными. В этом исследовании основное внимание уделяется стратегии парной торговли, в которой применяется надежный фильтр Калмана с использованием прогнозов волатильности инноваций на основе данных для одновременного прогнозирования коэффициента хеджирования и волатильности спреда. Стратегия также использует скрытую марковскую модель для оптимизации пороговых значений торговых сигналов в разных рыночных режимах. Исследование направлено на воспроизведение и уточнение стратегии, предложенной Johnson-Skinner et al. (2021), с использованием более строгих математических рассуждений и обширных торговых симуляций.

Методология

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

Торговля коинтеграционными парами и подбор пар

Ключевая идея торговли коинтеграционными парами заключается в применении стратегии возврата к среднему к стационарному спреду, сформированному как портфель из коинтегрированной пары акций с нестационарными ценами. Двухэтапный подход Энгла-Грейнджера используется для идентификации коинтегрированных пар и определения позиций каждой акции в портфеле. Торговые сигналы генерируются путем вычисления z-оценки на основе спреда и предполагаемой волатильности спреда. Коинтегрированные пары акций выбираются на основе достаточно небольшого p-значения, полученного с помощью двухэтапного подхода Энгла-Грейнджера.

def linear_OLS(x_arr, y_arr):
x_avg = x_arr.mean()
y_avg = y_arr.mean()
s_xy = (x_arr-x_avg)*(y_arr-y_avg).T
s_x = (x_arr-x_avg)*(x_arr-x_avg).T
beta_1 = s_xy.sum()/s_x.sum()
beta_0 = y_avg-beta_1*x_avg
resid = y_arr - beta_0 - beta_1 * x_arr
return beta_1, beta_0, resid

# Select pairs with the most significant cointegration (ADF p-value < 0.1)

p_values = []

for pair in itertools.combinations(tickers[:-1], 2):
beta_1, beta_0, spread = linear_OLS(np.log(data_train[pair[0]]), np.log(data_train[pair[1]]))
p_value = test_stationarity(spread).iloc[1, 0]
p_values.append(p_value)

pair_p_value = pd.DataFrame({'p-value': p_values}, index=itertools.combinations(tickers[:-1], 2))
selected_pairs = pair_p_value[pair_p_value['p-value'] < 0.1]
selected_pairs.index

Фильтр Калмана и прогнозирование волатильности инноваций на основе данных

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

Модель прогнозирования волатильности инноваций на основе данных (DDIVF) адаптирована из модели прогнозирования волатильности на основе обобщенной экспоненциальной взвешенной скользящей средней (DD-EWMA) на основе данных. Он обеспечивает более надежный прогноз волатильности, чем традиционный метод. Алгоритм DDIVF использует прошлые инновации для расчета корреляции знаков выборки и оценки волатильности. Оценка сглаженной волатильности рассчитывается рекурсивно, при этом оптимальная константа сглаживания определяется путем минимизации суммы квадратов ошибки прогноза на один шаг вперед.

# Initialization

beta_0, beta_1 = initialize_beta(df_demo_train)

df_demo_train['Predicted Beta 0'] = beta_0
df_demo_train['Predicted Beta 1'] = beta_1
df_demo_train['Innovation'] = 0.0
df_demo_train['DDIVF'] = 0.0

R = np.array([0.001]).reshape(1, 1)
# Initializing R (constant Measurement Uncertainty)

P = np.zeros((2, 2))
# Initializing P (Estimate Uncertainty)

delta = 0.0001

Q = delta / (1 - delta) * np.diag([1, 1])
# Initializing Q (Constant Process Noise Uncertainty)

I = np.identity(2)

for t in range(j, df_demo_train.shape[0]-1):
z = np.array([np.log(df_demo_train.iloc[:,1][t])]).reshape(1, 1)
H = np.array([1, np.log(df_demo_train.iloc[:,0][t])]).reshape(1, 2)
# Prediction
## 1. Extrapolate the state
beta_old = np.array([df_demo_train['Predicted Beta 0'][t], df_demo_train['Predicted Beta 1'][t]]).reshape(2, 1)
## 2. Extrapolate uncertainty
P = P + Q
## 3. Compute innovation
prediction = np.matmul(H, beta_old)
innovation = z - prediction
df_demo_train['Innovation'][t] = innovation[0][0]
## 4. Compute ddivf
k = 100
l = 20
if t > j + k:
w = df_demo_train['Innovation'].iloc[t-k:t].values
rho = np.corrcoef(w - w.mean(), np.sign(w - w.mean()))[0, 1]
V = abs(w - w.mean()) / rho
S = np.zeros(k)
S[0] = V[:l].mean()
alpha_values = np.arange(0, 0.5, 0.01)
min_fess = 999999999.9
alpha_opt = None
for alpha in alpha_values:
for i in range(1, k):
S[i] = alpha * V[i] + (1 - alpha) * S[i-1]
fess = np.sum(np.square(np.subtract(V[l:], S[l-1:-1])))
if fess < min_fess:
min_fess = fess
alpha_opt = alpha
for i in range(1, k):
S[i] = alpha_opt * V[i] + (1 - alpha_opt) * S[i-1]
ddivf = S[-1]
df_demo_train['DDIVF'][t] = ddivf

# Update
## 5. Compute the Kalman Gain
K = np.matmul(P, H.T) / (np.matmul(np.matmul(H, P), H.T) + R)
## 6. Update estimate with measurement
beta_predict = beta_old + np.matmul(K, innovation)
df_demo_train['Predicted Beta 0'][t+1], df_demo_train['Predicted Beta 1'][t+1] = beta_predict[0][0], beta_predict[1][0]
## 7. Update the estimate uncertainty
P = np.matmul(np.matmul((I - np.matmul(K, H)), P), (I - np.matmul(K, H)).T) + np.matmul(np.matmul(K, R), K.T)
Innovations and Forecasted Volatility with KF-DDIVF (AOS & DUK)
Инновации и прогнозируемая волатильность с KFDDIVF (AOS & DUK)

Скрытая марковская модель и режимно-осведомленные пороги

Скрытая марковская модель (HMM) используется для моделирования рыночного режима с двумя возможными состояниями: возможно, «нормальным» и «экстремальным». Модель использует вероятности для оценки вероятности базовой последовательности состояний на основе наблюдаемой последовательности. HMM используется для определения текущего рыночного режима. Далее формируется торговый сигнал на основе наблюдаемого нововведения, предполагаемой волатильности и текущего режима. Верхняя и нижняя торговые полосы рассчитываются с использованием оптимальных пороговых значений, определенных на основе обучающего набора данных путем оптимизации различной статистики.

import sklearn.mixture as mix
import scipy.stats as scs

df_demo_train['H Return'] = np.log1p(df_demo_train.iloc[:,0].pct_change())
df_demo_train['z Return'] = np.log1p(df_demo_train.iloc[:,1].pct_change())

X = df_demo_train.iloc[j+5:-1][['Innovation', 'H Return', 'z Return']].values
hmm = mix.GaussianMixture(n_components=2, covariance_type="full", n_init=100, random_state=7).fit(X)
Инновации, отмеченные государствами с HMM (AOS & DUK)

Торговая стратегия и симуляция

Торговая стратегия включает в себя несколько шагов. Во-первых, мы используем двухэтапный подход Энгла-Грейнджера для определения коинтегрированной пары акций. После периода инициализации начинается торговый горизонт. В конце каждого дня на торговом горизонте мы наблюдаем цены закрытия акций. Используя KF, мы получаем инновацию и прогнозируемый коэффициент хеджирования. Мы оцениваем волатильность инновации с помощью DDIVF и используем установленный HMM для определения текущего рыночного режима. Далее мы генерируем торговый сигнал на основе наблюдаемого нововведения, предполагаемой волатильности и текущего режима. Мы генерируем сигнал на продажу, когда инновация пересекает верхнюю торговую полосу снизу, и сигнал на покупку, когда инновация пересекает нижнюю торговую полосу сверху. Если инновация не пересекает эти пороги, мы не генерируем сигнал. После генерации торгового сигнала мы торгуем портфелем, который подвергает нас спреду. В частности, мы открываем длинную позицию по акциям 2 и короткую позицию по акциям 1 с размером позиции, равным текущему коэффициенту хеджирования, умноженному на наши инвестиции в акции 1. Наконец, мы получаем прибыль на основе ежедневной доходности акций и нашей позиции в предыдущий день.

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

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

# Let me first demostrate the trading strategy and fix the thresholds to be 2.

p_S0 = p_S1 = 2

df_demo_train['Investment in H'] = 0.0
df_demo_train['Investment in z'] = 0.0
df_demo_train['Signal'] = 0
df_demo_train['Strategy Daily PnL'] = 0.0
df_demo_train['Strategy'] = E0
df_demo_train['Strategy Daily Return'] = 0.0
df_demo_train['Cash'] = W0 - E0

number_of_successes = 0
number_of_failures = 0
total_win = 0.0
total_loss = 0.0

for t in range(j+k+5, df_demo_train.shape[0]-1):
# Starts after the fifth iteration to make the KF stablized.

## GENERATING SIGNALS
if df_demo_train['State'][t] == 0:
p_now = p_S0
elif df_demo_train['State'][t] == 1:
p_now = p_S1
if df_demo_train['State'][t-1] == 0:
p_past = p_S0
elif df_demo_train['State'][t-1] == 1:
p_past = p_S1
# Note the position would close in one day
if df_demo_train['Innovation'][t] > p_now * df_demo_train['DDIVF'][t] and df_demo_train['Innovation'][t-1] < p_past * df_demo_train['DDIVF'][t-1]:
signal = -1
elif df_demo_train['Innovation'][t] < -p_now * df_demo_train['DDIVF'][t] and df_demo_train['Innovation'][t-1] > p_past * df_demo_train['DDIVF'][t-1]:
signal = 1
else:
signal = 0
df_demo_train['Signal'][t] = signal

## GENERATE DAILY PNL
E = df_demo_train['Strategy'][t]
# Refill if falls below zero
if E < 0:
refill = min(E0 - E, df_demo_train['Cash'][t])
df_demo_train['Cash'][t] = df_demo_train['Cash'][t] - refill
E = df_demo_train['Strategy'][t] + refill
df_demo_train['Cash'][t+1] = df_demo_train['Cash'][t]
r = df_demo_train['Daily Fed Funds Rate'][t]
hedge_ratio = df_demo_train['Predicted Beta 1'][t]
R_z = (df_demo_train.iloc[:,1][t] / df_demo_train.iloc[:,1][t-1]) - 1
R_H = (df_demo_train.iloc[:,0][t] / df_demo_train.iloc[:,0][t-1]) - 1
# Trade the minimum of $200,000 and maximum leverage
Q_z_new = min(200000.0, Delta_max * E) / (1 + hedge_ratio) * signal
Q_H_new = -Q_z_new * hedge_ratio
df_demo_train['Investment in z'][t] = Q_z_new
df_demo_train['Investment in H'][t] = Q_H_new
Q_z = df_demo_train['Investment in z'][t-1]
Q_H = df_demo_train['Investment in H'][t-1]
daily_pnl = r * (E - Q_z - Q_H) + (Q_z * R_z + Q_H * R_H) - delta_r * (abs(Q_z) + abs(Q_H)) - epsilon * (abs(Q_z_new - Q_z) + abs(Q_H_new - Q_H))
df_demo_train['Strategy Daily PnL'][t] = daily_pnl
if df_demo_train['Signal'][t-1] != 0:
if daily_pnl > 0:
number_of_successes += 1
total_win += daily_pnl
else:
number_of_failures += 1
total_loss -= daily_pnl

## GAIN PNL
df_demo_train['Strategy'][t+1] = E + daily_pnl
df_demo_train['Strategy Daily Return'][t+1] = daily_pnl / E

df_demo_train['Strategy'] = df_demo_train['Strategy'] + df_demo_train['Cash'] - W0 + E0
df_demo_train = df_demo_train.drop(['Investment in H', 'Investment in z', 'Cash'], axis=1)

## Calculate Trading Statistics
number_of_trades = number_of_successes + number_of_failures
average_win = total_win / number_of_successes
average_loss = total_loss / number_of_failures
hit_rate = number_of_successes / number_of_trades
average_return = df_demo_train['Strategy Daily Return'][j+k+5:].sum() / (df_demo_train[j+k+5:].shape[0] / 252)
volatility = ((df_demo_train['Strategy Daily Return'][j+k+5:] - (average_return/252)) ** 2).sum() / (df_demo_train[j+k+5:].shape[0] / 252)
sharpe = (df_demo_train['Strategy Daily Return'][j+k+5:] - df_demo_train['Daily Fed Funds Rate'][j+k+5:]).sum() / (df_demo_train[j+k+5:].shape[0] / 252) / volatility

print('Number of Trades:', number_of_trades)
print('Hit Rate:', hit_rate)
print('Average Win:', average_win)
print('Average Loss:', average_loss)
print('Average Return:', average_return)
print('Sharpe Ratio:', sharpe)

df_demo_train['Strategy'].plot(figsize=(20,10))
plt.show()

Результаты

Я применил предложенную стратегию к основным акциям в коммунальном секторе, а именно к NEE (NextEra Energy Inc.), D (Dominion Energy Inc.), DUK (Duke Energy Corporation), ED (Consolidated Edison Inc.), AWK (American Water Works Company Inc.), AOS (A.O. Smith Corporation), XEL (Xcel Energy Inc.), SO (Southern), AEP (American Electric Power Company) и SRE (Sempra Energy). Ежедневная безрисковая ставка рассчитывается с использованием федеральных 13-недельных ставок казначейских векселей.

Двухэтапный подход Энгла-Грейнджера используется для расчета p-значений для каждой пары акций. Двенадцать пар с p-значениями ниже 10% отбираются для тестирования на истории. Этими парами являются AOS & DUK (8,77%), AWK & ED (2,71%), AWK & XEL (1,50%), AWK & AEP (0,87%), AWK & SRE (6,18%), D & ED (3,31%), ED & SRE (4,14%), NEE & SRE (8,74%), XEL & AEP (0,01%), XEL & SRE (0,39%), SO & SRE (0,43%) и AEP & SRE (0,76%). Стратегия продемонстрирована с использованием пары AOS и DUK, которая была самой эффективной парой в исследовании.

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

Производительность тренировочного набора (AOS и DUK)
Производительность тестового набора (AOS и DUK)

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

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

Ссылка

[1] Джонсон-Скиннер Э., Лян Ю., Ю Н. и Морариу А. (2021, июль). Новая алгоритмическая торговая стратегия с использованием скрытой марковской модели для инноваций фильтрации Калмана. В 2021 году 45-я ежегодная конференция IEEE по компьютерам, программному обеспечению и приложениям (COMPSAC) (стр. 1766–1771). ИЭЭЭ.

[2] Лян Ю., Таванесваран А. и Хок М. Е. (2020, декабрь). Новая алгоритмическая торговая стратегия с использованием волатильности инноваций, основанной на данных. В 2020 году серия симпозиумов IEEE по вычислительному интеллекту (SSCI) (стр. 1107–1114). ИЭЭЭ.

[3] Энгл, Р. Ф., и Грейнджер, К. В. (1987). Коинтеграция и исправление ошибок: представление, оценка и тестирование. Эконометрика: журнал Эконометрического общества, 251–276.

[4] Эллиотт, Р. Дж., Ван дер Хук *, Дж., и Малкольм, В.. (2005). Парная торговля. Количественные финансы5 (3), 271–276.

[5] Де Моура, К.Э., Пиццинга, А., и Зубелли, Дж. (2016). Стратегия парной торговли, основанная на моделях линейного пространства состояний и фильтре Калмана. Количественные финансы16 (10), 1559–1573.

[6] Беккер, А. (без даты). Онлайн-учебник по фильтру Калмана. Проверено в 2023 году с KalmanFilter.Net года.

[7] Таванесваран А., Пасека А. и Фрэнк Дж. (2020). Обобщенное прогнозирование стоимости при риске. Коммуникации в теории и методах статистики49 (20), 4988–4995.

Реализация торговой стратегии на основе фильтра Калмана

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

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

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

Что такое фильтр Калмана?

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

  1. Шаг прогнозирования: На этом шаге фильтр Калмана использует оценку текущего состояния для прогнозирования следующего состояния системы. Этот прогноз основан на математической модели динамики системы, которая описывает, как состояние развивается с течением времени. Прогнозируемое состояние сопровождается оценкой неопределенности (или ковариации ошибок), связанной с прогнозом.
  2. Шаг обновления: На этом шаге фильтр Калмана включает новые измерения для обновления оценки состояния. Он вычисляет средневзвешенное значение прогнозируемого состояния и нового измерения, при этом весовые коэффициенты определяются соответствующими неопределенностями. Обновленная оценка состояния является более точной, так как она учитывает как прогноз, так и новую информацию. Фильтр также обновляет оценку неопределенности, чтобы отразить уменьшение ошибки после включения нового измерения.

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

Фильтр Калмана в трейдинге

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

Для реализации торговой стратегии на основе фильтра Калмана необходимо выполнить следующие действия:

  1. Оценить скользящую среднюю: используйте фильтр Калмана для оценки скользящей средней цены акции. Эта скользящая средняя представляет собой основной тренд, сглаженный шумом.
  2. Вычислить Z-оценку: Вычислить z-оценку, которая измеряет, насколько далеко текущая цена находится от предполагаемой скользящей средней с точки зрения стандартных отклонений. Z-оценка помогает выявить существенные отклонения от тренда.
  3. Генерировать торговые сигналы: определение пороговых значений для входа и выхода из позиций на основе z-оценки. Например, входить в длинную позицию, когда z-оценка ниже определенного порога, и выходить, когда она возвращается к более высокому порогу. Точно так же входите в короткую позицию, когда z-оценка превышает определенный порог, и выходите, когда она возвращается к более низкому пороговому значению.
  4. Реализуйте механизм стоп-лосс: Чтобы управлять рисками, реализуйте механизм стоп-лосс на основе цены. Это гарантирует, что позиции будут закрыты, если цена выйдет за пределы заданного процента от цены входа, ограничивая потенциальные потери.

Торговая стратегия

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

Правила стратегии:

  1. Вход в длинную позицию: вход в длинную позицию, когда z-оценка ниже определенного порога.
  2. Вход в короткую позицию: вход в короткую позицию, когда z-оценка превышает определенный порог.
  3. Exit Long: Выход из длинной позиции, когда z-оценка вернется к более высокому пороговому значению.
  4. Exit Short: Закрыть короткую позицию, когда z-оценка вернется к более низкому пороговому значению.
  5. Стоп-лосс: выход из позиции, если цена выходит за пределы указанного процента от цены входа.

Эволюция стратегии

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

  1. Первоначальная концепция: Первоначальная концепция включала в себя использование фильтра Калмана для сглаживания данных о ценах на акции и генерации торговых сигналов на основе отклонений от расчетной скользящей средней. Мы начали с базовых правил входа и выхода, основанных на z-оценке.
  2. Добавление стоп-лосса: Для управления рисками мы внедрили механизм стоп-лосс, основанный на z-оценке. Однако у этого подхода были ограничения, особенно на волатильных рынках, где z-оценка могла колебаться в широких пределах.
  3. Оптимизация параметров: Мы изучили различные пороговые значения для входа, выхода и стоп-лосса, чтобы оптимизировать эффективность стратегии. Это потребовало обширного тестирования на истории и настройки параметров.
  4. Стоп-лосс на основе цены: Чтобы повысить надежность стратегии, мы перешли на механизм стоп-лосса на основе цены. Это изменение обеспечило более интуитивно понятный и эффективный способ ограничения потенциальных потерь.
  5. Окончательные доработки: После нескольких раундов тестирования и доработок мы пришли к текущей версии стратегии, которая уравновешивает правила входа и выхода с эффективным механизмом стоп-лосса.

Вот как вы можете инициировать объект KalmanFilter, который предоставляется библиотекой Python под названием pykalman

# Initialize Kalman filter parameters for the moving average estimation
kf = KalmanFilter(transition_matrices=[1],
observation_matrices=[1],
initial_state_mean=0,
initial_state_covariance=1,
observation_covariance=1,
transition_covariance=0.01)

Параметры:

  • transition_matrices=[1]: Матрица переходов определяет, как состояние эволюционирует от одного шага к другому. В этом случае [1] указывает на то, что состояние (которое является скользящей средней в данном контексте) остается неизменным от шага к шагу, то есть предполагает постоянную модель для скользящей средней.
  • observation_matrices=[1]: Матрица наблюдений связывает наблюдаемые значения с состоянием. Здесь [1] указывает на то, что наблюдаемые цены акций напрямую соответствуют состоянию без какого-либо масштабирования или трансформации.
  • initial_state_mean=0: Этот параметр задает начальное среднее значение оценки состояния. Установка его в 0 означает, что начальная оценка скользящей средней начинается с 0.
  • initial_state_covariance=1: Этот параметр определяет начальную неопределенность оценки состояния. Значение 1 указывает на умеренный уровень неопределенности в отношении начального состояния.
  • observation_covariance=1: Этот параметр представляет собой неопределенность в наблюдениях (т.е. в ценах акций). Значение 1 указывает на то, что в наблюдаемых ценах акций присутствует некоторый шум.
  • transition_covariance=0.01: Ковариация перехода представляет собой неопределенность в процессе перехода состояния. Значение 0.01 указывает на то, что в предположении, что состояние остается постоянным с течением времени, очень мало шума.

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

# Use the Kalman filter to estimate the moving average
state_means, state_covariances = kf.filter(close_prices.values)
kalman_avg = state_means.flatten()

Результаты

После внедрения и оптимизации торговой стратегии на основе фильтра Калмана оценим ее эффективность на исторических данных для Apple Inc. (AAPL). Результаты показывают лучшие пороговые значения и показатели эффективности стратегии.

Лучшие пороги:

  • Порог входа в длинную позицию: -3
  • Порог входа в шорт: 3
  • Порог выхода из длинной позиции: 0.25
  • Порог выхода из короткой позиции: -0.5
  • Порог стоп-лосса: 0,02

Показатели производительности:

  • Коэффициент Шарпа: 0.6404972725290812
  • Оптимизированный коэффициент Шарпа: 0,6404972725290812
  • Оптимизированная максимальная просадка: 0.1802083236506904

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

График кумулятивной доходности показывает эффективность стратегии за период тестирования на истории. График торговых сигналов иллюстрирует цену акции, скользящую среднюю фильтра Калмана, а также длинные и короткие позиции, занятые стратегией. График z-оценки выделяет точки входа и выхода на основе оптимизированных пороговых значений.

Вы можете найти полную стратегию, разработанную с помощью Python, здесь:

https://quantitativepy.substack.com/p/implementing-a-kalman-filter-based

Источник

Источник

Источник

Источник