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

Обо мне

Я торгую на Fx уже 20 лет, используя как традиционный статистический анализ, так и анализ графиков, а также AI/ML последние 5 лет или около того. Имея степень бакалавра технических наук, степень магистра и несколько сертификатов в области машинного обучения, я хотел поделиться некоторыми подводными камнями, на изучение которых у меня ушли годы, и объяснить, почему заставить систему работать действительно сложно.

Введение

Предыдущую статью мы построили на примере «привет, мир», чтобы получить его «на стадионе» и, возможно, немного лучше, чем угадывать. Тем не менее, это все еще довольно бесполезно. Следующий шаг — заглянуть за кулисы логистической регрессии и посмотреть, что происходит, чтобы найти пробелы.

Отказ от ответственности

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

История логистической регрессии

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

Однако на английском языке и специально для Fx Trading он создает модель, основанную на входных переменных, для расчета вероятности события. Мы также вводим входные переменные, а также двоичное значение «да» или «нет», что затем произошло событие. Он учится на этих входных данных, и мы можем затем дать ему любые входные переменные, и он предскажет вероятность события.

В нашем примере мы используем цену закрытия последних 4 периодов в качестве наших входных переменных, а выходом было, если цена поднимется на 200 пунктов (мы проигнорировали движение вниз или короткое движение, чтобы упростить его). Это наши входные переменные x_t1 (цена закрытия сейчас), x_t2 (цена закрытия час назад), x_t3 (цена закрытия 2 часа назад) и x_t4 (цена закрытия 3 часа назад) и y (если цена на 200 пунктов выше за 4 часа). как истина/ложь) выходная переменная.

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

f(x) = a + (w4 * x_t4) + (w3 * x_t3) + (w2 * x_t2) + (w1 * x_t1)

Это линейное уравнение (прямая линия), которое вычисляет алгоритм для наилучшего соответствия набору данных. По умолчанию SciKit использует алгоритм вызова lbfgs (ограниченная память Бройден–Флетчер–Голдфарб–Шанно). Он также поддерживает другие алгоритмы, но в этом сценарии между ними не будет большой разницы.

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

Это берет нашу прямую линию и «изгибает» ее, чтобы задать пределы от 0 до 1 и распределить прогнозы. Это важно, поскольку мы даем «вероятность» (которая должна быть между 0 и 1).

Посмотреть в действии

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

Во-первых, давайте повторим загрузку данных с прошлой недели с некоторыми изменениями, чтобы удалить график и ограничить входные данные (или функции) ценой закрытия за последние 2 периода.

#
# IMPORT DATA From github 
#

import pandas as pd 
from datetime import datetime 

url = 'https://raw.githubusercontent.com/the-ml-bull/Hello_World/main/Fx60.csv'
dateparse = lambda x: datetime.strptime(x, '%d/%m/%Y %H:%M')

df = pd.read_csv(url, parse_dates=['date'], date_parser=dateparse)

df.head(n=10)
#
# Create time shifted data as basis for model 
#

import numpy as np

df = df[['date', 'audusd_open', 'audusd_close']].copy()

# x is the last 4 values so create x for each 
#df['x_t-4'] = df['audusd_close'].shift(4)
#df['x_t-3'] = df['audusd_close'].shift(3)
df['x_t-2'] = df['audusd_close'].shift(2)
df['x_t-1'] = df['audusd_close'].shift(1)

# y is points 4 periods into the future - the open price now (not close)
df['y_future'] = df['audusd_close'].shift(-3)
df['y_change_price'] = df['y_future'] - df['audusd_open']
df['y_change_points'] = df['y_change_price'] * 100000 
df['y'] = np.where(df['y_change_points'] >= 200, 1, 0)
#
# Create Train and Val datasets 
#
from sklearn.linear_model import LogisticRegression   

#x = df[['x_t-4', 'x_t-3', 'x_t-2', 'x_t-1']]
x = df[['x_t-2', 'x_t-1']]
y = df['y']
y_points = df['y_change_points']   # we will use this later    

# Note Fx "follows" (time series) so randomization is NOT a good idea
# create train and val datasets. 
no_train_samples = int(len(x) * 0.7)
x_train = x[4:no_train_samples]
y_train = y[4:no_train_samples]
y_train_change_points = y_points[4:no_train_samples]

x_val = x[no_train_samples:-3]
y_val = y[no_train_samples:-3]
y_val_change_points = y_points[no_train_samples:-3]
#
# Create class weights 
#
from sklearn.utils.class_weight import compute_class_weight

num_ones = np.sum(y_train)
num_zeros = len(y_train) - num_ones 
print('In the training set we have 0s {} ({:.2f}%), 1s {} ({:.2f}%)'.format(num_zeros, num_zeros/len(df)*100, num_ones, num_ones/len(df)*100))

classes = np.unique(y_train)
class_weight = compute_class_weight(class_weight='balanced', classes=classes, y=y_train)
class_weight = dict(zip(classes, class_weight))

print('class weights {}'.format(class_weight))

Далее мы будем вручную запускать алгоритм шаг за шагом и смотреть, что происходит с весами по мере продвижения. В этом примере мы будем использовать 500 точек данных за раз и «переобучать» наш алгоритм на каждом блоке. После каждого тренировочного интервала мы будем

  • отобразить новые веса двух входных (характеристик) переменных.
  • Используя последние x и y блока данных, посмотрим, сможем ли мы рассчитать прогноз и сравнить его с прогнозом вероятности SciKit.

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

#
# fit the model (step by step)
#

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

lr = LogisticRegression(warm_start=True)

start_ix=0
increments=500
x_list, y_list = [], []
while start_ix < (len(x_train) - increments):

  x = x_train.iloc[start_ix:start_ix+increments].to_numpy()
  y = y_train.iloc[start_ix: start_ix+increments].to_numpy() 

  lr.fit(x, y)

  intercept = float(lr.intercept_)
  coef_x1 = float(lr.coef_[0, 0])
  coef_x2 = float(lr.coef_[0, 1])
  x1 = float(x[-1, 0])
  x2 = float(x[-1, 1])

  predicted = float(lr.predict_proba(x[-1].reshape(1, 2))[0, 1])
  calculated = intercept + (coef_x1 * x1) + (coef_x2 * x2)

  print('ix: {}, x1: {:.5f}, x2: {:.5f}, y: {} int: {:.5f}, w1: {:.5f}, w2: {:.5f}, Calc: {:.5f}, CalSig: {:.5f}, Pred: {:.5f}'.format(start_ix+100, 
    x[0,0], x[0, 1], y[0],
    intercept, coef_x1, coef_x2, 
    calculated, sigmoid(calculated), predicted))  

  start_ix += increments

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

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

Обратите внимание, что если вы уменьшите размер блока с 500, вы можете получить некоторые ошибки. Алгоритму lbfgs требуется по крайней мере одна выборка из каждого класса (и 0 и 1), чтобы выполнить определение с несколькими разбросанными единицами, чего может не произойти, если размер блока мал.

Граница решения

Граница решения относится к линии, в которой выборки выше или выше классифицируются как 1 и ниже 0. Из приведенного выше у нас есть две переменные (x1, x2) и y, который на самом деле является да или нет, и формулы для прогнозирования вероятности y от x1 и x2. Следовательно, мы можем наметить их, чтобы увидеть, дает ли это какие-либо новые идеи.

Обратите внимание, что именно поэтому мы ограничили входные данные до 2. Диаграмма с 3 может быть сложной, и если вы можете найти хороший способ сделать 4, пожалуйста, дайте мне знать. В нашей окончательной модели у нас будет около 20 функций!

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

Во-первых, мы вычисляем параметры модели (пересечение, веса и т. д.) и используем их для вычисления x2 (ось y графика) из x1, для которого установлены его минимальное и максимальное значения. Используемые формулы -w1/w2 * x1_values ​​— (b/w2) можно вывести, зная, что граница решения дает вероятность 0,5. Это немного сложно, но есть несколько статей, которые хорошо объясняют это.

# Retrieve the model parameters.

def fit_and_get_parameters(x, y, class_weight):

  lr = LogisticRegression(class_weight=class_weight)
  lr.fit(x, y)

  b = float(lr.intercept_[0])
  w1, w2 = lr.coef_.T
  w1 = float(w1)
  w2 = float(w2)

  # Calculate the intercept and gradient of the decision boundary.
  c = float(-b/w2)
  m = float(-w1/w2)

  # get the min / max values of x1 and use to find decision boundary wtih x2 
  min_x1_value = x['x_t-1'].min()
  max_x1_value = x['x_t-1'].max()
  x1_values = np.array([min_x1_value, max_x1_value])
  x2_values = -w1/w2 * x1_values - (b / w2)

  print('y = {:.2f} + {:.2f} x1 + {:.2f} x2 Intercept(c): {:.2f}, Gradient(m): {:.3f} x1: {}, x2: {}'.format(b, w1, w2, c, m, x1_values, x2_values))

  return x1_values, x2_values

Затем мы можем передать функции «графика» «точки» и начало и конец привязки решения к графику.

def plot_decision_boundary(x, y, x1_values, x2_values, heading):
  
  # put 0's and 1's in two seperate lists for display 
  list_0_x1, list_0_x2, list_1_x1, list_1_x2 = [], [], [], []
  for ix in range(len(y)):
    if y.iloc[ix] == 0:
      list_0_x1.append(x['x_t-1'].iloc[ix])
      list_0_x2.append(x['x_t-2'].iloc[ix])
    else:
      list_1_x1.append(x['x_t-1'].iloc[ix])
      list_1_x2.append(x['x_t-2'].iloc[ix])

  # scaterplot the 0's and 1's
  plt.scatter(list_0_x1, list_0_x2, marker='o', color='blue')
  plt.scatter(list_1_x1, list_1_x2, marker='x', color='red')

  # Draw the decision boundary 
  plt.plot(x1_values, x2_values, linestyle='-', color='black')

  # axis labels 
  plt.xlabel('x1')
  plt.ylabel('x2')
  plt.title(heading)
  
  return 

Затем мы запускаем симуляцию

start_ix, stop_ix = 0, -1
x1_values, x2_values = fit_and_get_parameters(x_train.iloc[start_ix:stop_ix], y_train.iloc[start_ix:stop_ix], class_weight)
plot_decision_boundary(x_train.iloc[start_ix:stop_ix], y_train.iloc[start_ix:stop_ix], x1_values, x2_values, '{} to {} with db {} to {}'.format(start_ix, stop_ix, x1_values, x2_values)).iloc[start_ix:stop_ix], x1_values, x2_values)

Мы можем запустить это для нескольких сценариев данных с разными начальными и конечными значениями. Я наметил некоторые из них ниже.

Некоторые ключевые моменты

  • Меньшие наборы данных могут привести к совершенно бессмысленной информации.
  • Значения точек данных меняются со временем. От 0,55 до 0,85 в зависимости от диапазона. Этого и следовало ожидать, «цена» Fx действительно меняется с течением времени, часто в широком диапазоне. Это основная проблема с нашей моделью на данный момент, так как нам нужно нормализовать данные. В следующей статье мы собираемся сделать именно это.
  • Распределение 1 и 0 (красный и синий) почти равномерное. Следовательно, рисование границы решения выглядит лишь немногим лучше, чем угадывание (она выглядит прямо посередине с равными числами с обеих сторон).

Что все это значит

В нынешнем виде наша модель по-прежнему совершенно бесполезна, но у нас есть некоторые идеи, которые мы можем развивать.

  • граница решения проходит посередине с почти одинаковым количеством единиц с обеих сторон (так что примерно так же хорошо, как угадывать)
  • Цены могут сильно меняться, и, учитывая наше определение «веса» (т.е. y = w1 * t1 и т. д.), изменение t1 может полностью изменить выпуск.
  • Эта модель практически бесполезна

Следующая статья

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

Ссылки и ссылки