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

Обо мне

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

Введение

В первых двух статьях мы:
1. Создали простейший пример «hello world». Мы собирались собрать данные, сгенерировать модель и измерить наш результат
2. Мы использовали его, чтобы получить «примерную оценку» и, возможно, немного лучше, чем угадывать, и улучшили наши измерения.
3. В в третьем мы заглянули под обложки логистической регрессии, чтобы найти ее ограничения.
4. В этой статье мы рассмотрим проблему нормализации.

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

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

Необходимость нормализации

Нормализация — это процесс, в котором мы помещаем все данные в одну шкалу. Рассмотрим цену AUDUSD за последние 7 лет.

Цена сместилась примерно с 0,55 до 0,90 . Помните из прошлой статьи, что в логистической регрессии используется «вес: (w), умноженный на функцию (x, которая в данном случае является ценой), и поэтому, если цена значительно меняется, значение веса теряет свою точность. Также при «обучении» вес будет масштабироваться по средней цене и терять практическую полезность, когда цена от него отклоняется.

Помните, что наша модель использует цену закрытия за последние 4 часа (на данный момент), чтобы предсказать, что цена вырастет (длинная сделка) на 200 пунктов. Итак, давайте посмотрим на 5 случайных случаев, которые произошли. Мы будем повторно использовать код из предыдущих статей, но с некоторыми изменениями.

Сначала импортируйте наши данные

#
# 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_open'] = df['audusd_open'].shift(4)
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)

# add all future prices to measurement point 
df['y_t-0'] = df['audusd_close']
df['y_t-1'] = df['audusd_close'].shift(-1)
df['y_t-2'] = df['audusd_close'].shift(-2)
df['y_t-3'] = df['audusd_close'].shift(-3)

# 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)

Теперь давайте наметим 5 случайных случаев

#
# Chart data 
#
import random 
import matplotlib.pyplot as plt 

for chart_ix in range(5):

  random_ix = random.randint(0, len(true_events_df))
  event = true_events_df.iloc[random_ix]

  x = [-4, -3, -2, -1, 0, 1, 2, 3]
  y = event[['x_t-4', 'x_t-3', 'x_t-2', 'x_t-1', 'y_t-0', 'y_t-1', 'y_t-2', 'y_t-3']]
  
  event_date = event['date'].strftime('%Y-%b-%d %H')
  plt.plot(x, y, label=event_date)

  #print('{:.5f}, {:.5f}, {:.5f}'.format(event['audusd_open'], event['y_t-3'], event['y_t-3']-event['audusd_open']))

plt.axvline(x = 0, color='black')
plt.legend()
plt.show()

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

Параметры нормализации

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

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

Используя точки, приведенная выше диаграмма становится:

#
# Chart 5 occations where price went up 200 points using points instead of price 
#
import random 
import matplotlib.pyplot as plt 

for chart_ix in range(5):

  random_ix = random.randint(0, len(true_events_df))
  event = true_events_df.iloc[random_ix]

  x = [-4, -3, -2, -1, 0, 1, 2, 3]
  #y = event[['x_t-4', 'x_t-3', 'x_t-2', 'x_t-1', 'y_t-0', 'y_t-1', 'y_t-2', 'y_t-3']]
  
  y_points = (event[['x_t-4', 'x_t-3', 'x_t-2', 'x_t-1']] - event['x_open']) * 100000
  y_points['y_t-0'] = (event['y_t-0'] - event['audusd_open']) * 100000
  y_points['y_t-1'] = (event['y_t-1'] - event['audusd_open']) * 100000
  y_points['y_t-2'] = (event['y_t-2'] - event['audusd_open']) * 100000
  y_points['y_t-3'] = (event['y_t-3'] - event['audusd_open']) * 100000
  
  event_date = event['date'].strftime('%Y-%b-%d %H')
  plt.plot(x, y_points, label=event_date)

plt.axvline(x = 0, color='black')
plt.legend()
plt.show()

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

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

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler, StandardScaler


def normalize_data(df, method='price'):
  
  norm_df = df.copy()
  x_fields = ['x_t-4', 'x_t-3', 'x_t-2', 'x_t-1']
  y_fields = ['y_t-0', 'y_t-1', 'y_t-2', 'y_t-3']

  if method == 'price':
    for field in x_fields:
      norm_df[field + '_norm'] = df[field] 
    for field in y_fields:
      norm_df[field + '_norm'] = df[field] 
    
  if method == 'points': 
    for field in x_fields:
      norm_df[field + '_norm'] = (df[field] - df['x_open']) * 100000 
    for field in y_fields:
      norm_df[field + '_norm'] = (df[field] -  df['audusd_open']) * 100000 

  if method == 'percentage':
    for field in x_fields:
      norm_df[field + '_norm'] = (df[field] - df['x_open']) / df[field] * 100
    for field in y_fields:
      norm_df[field + '_norm'] = (df[field] - df['audusd_open']) / df[field] * 100

  if method == 'minmax':
    scaler = MinMaxScaler()
    scaled = scaler.fit_transform(df[x_fields + y_fields])
    norm_field_names = [x + '_norm' for x in x_fields + y_fields]
    norm_df[norm_field_names] = scaled

  if method == 'stddev':
    scaler = StandardScaler()
    scaled = scaler.fit_transform(df[x_fields + y_fields])
    norm_field_names = [x + '_norm' for x in x_fields + y_fields]
    norm_df[norm_field_names] = scaled

  return norm_df 

def get_class_weights(y_train, display=True):
  
  #
  # 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 
  
  classes = np.unique(y_train)
  class_weights = compute_class_weight(class_weight='balanced', classes=classes, y=y_train)
  class_weights = dict(zip(classes, class_weights))

  if display:
    print('In the training set we have 0s {} ({:.2f}%), 1s {} ({:.2f}%)'.format(num_zeros, num_zeros/len(y_train)*100, num_ones, num_ones/len(y_train)*100))
    print('class weights {}'.format(class_weights))

  return class_weights

def get_train_val(df):
  #
  # Create Train and Val datasets 
  # 

  x = df[['x_t-4_norm', 'x_t-3_norm', 'x_t-2_norm', 'x_t-1_norm']] 
  y = df['y']
  y_points = df['y_change_points']

  # 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]

  x_val = x[no_train_samples:-3]
  y_val = y[no_train_samples:-3]
  y_val_change_points = y_points[no_train_samples:-3]

  return x_train, y_train, x_val, y_val, y_val_change_points

from sklearn.metrics import log_loss, confusion_matrix, precision_score, recall_score, f1_score

def show_metrics(lr, x, y_true, y_change_points, display=True):
  
  # predict from teh val set meas we have predictions and true values as binaries  
  y_pred = lr.predict(x)

  #basic error types 
  log_loss_error = log_loss(y_true, y_pred)
  score = lr.score(x, y_true)
  
  #
  # Customized metrics  
  #
  tp = np.where((y_pred == 1) & (y_change_points >= 0), 1, 0).sum()
  fp = np.where((y_pred == 1) & (y_change_points < 0), 1, 0).sum()
  tn = np.where((y_pred == 0) & (y_change_points < 0), 1, 0).sum()
  fn = np.where((y_pred == 0) & (y_change_points >= 0), 1, 0).sum()

  precision = 0
  if (tp + fp) > 0:
    precision = tp / (tp + fp)

  recall = 0
  if (tp + fn) > 0:
    recall = tp / (tp + fn)

  f1 = 0
  if (precision + recall) > 0:
    f1 = 2 * precision * recall / (precision + recall)

  # output the errors 
  if display:
    print('Errors Loss: {:.4f}'.format(log_loss_error))
    print('Errors Score: {:.2f}%'.format(score*100))
    print('Errors tp: {} ({:.2f}%)'.format(tp, tp/len(y_val)*100))
    print('Errors fp: {} ({:.2f}%)'.format(fp, fp/len(y_val)*100))
    print('Errors tn: {} ({:.2f}%)'.format(tn, tn/len(y_val)*100))
    print('Errors fn: {} ({:.2f}%)'.format(fn, fn/len(y_val)*100))
    print('Errors Precision: {:.2f}%'.format(precision*100))
    print('Errors Recall: {:.2f}%'.format(recall*100))
    print('Errors F1: {:.2f}'.format(f1*100))

  errors = {
      'loss': log_loss_error,
      'score': score, 
      'tp': tp,
      'fp': fp,
      'tn': tn,
      'fn': fn,
      'precision': precision,
      'recall': recall,
      'f1': f1
      }

  return errors

import random
import matplotlib.pyplot as plt

def chart(norm_df, event_ix_to_plot, norm_method, errors):

  fig, ax = plt.subplots()

  for ix in event_ix_to_plot:

    event = norm_df.iloc[ix]

    x = [-4, -3, -2, -1, 0, 1, 2, 3]
    y = event[['x_t-4_norm', 'x_t-3_norm', 'x_t-2_norm', 'x_t-1_norm', 'y_t-0_norm', 'y_t-1_norm', 'y_t-2_norm', 'y_t-3_norm']]
    
    event_date = '{}'.format(ix) + ' - ' + event['date'].strftime('%Y-%b-%d %H')
    ax.plot(x, y, label=event_date)

  ax.axvline(x = 0, color='black')
  ax.legend(loc='lower right')

  ax.set_title('Method: {}'.format(norm_method))

  textstr = 'loss: {:.2f}\nTP: {}\nFP: {}\nPrecision: {:.2f}%\nRecall: {:.2f}%\nF1: {:.2f}%'.format(
      errors['loss'], errors['tp'], errors['fp'], errors['precision']*100, errors['recall']*100, errors['f1']*100)
  props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)
  ax.text(0.05, 0.95, textstr, transform=ax.transAxes, fontsize=8,
          verticalalignment='top', bbox=props)

  plt.show()

import numpy as np
import pandas as pd 
from datetime import datetime 

def load_data():
  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 = df[['date', 'audusd_open', 'audusd_close']].copy()

  # x is the last 4 values so create x for each 
  df['x_open'] = df['audusd_open'].shift(4)
  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)

  # add all future prices to measurement point 
  df['y_t-0'] = df['audusd_close']
  df['y_t-1'] = df['audusd_close'].shift(-1)
  df['y_t-2'] = df['audusd_close'].shift(-2)
  df['y_t-3'] = df['audusd_close'].shift(-3)

  # 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)

  return df 

event_ix_to_plot = None 

for norm_method in ['price', 'points', 'percentage', 'minmax', 'stddev']:

  df = load_data()
  norm_df = normalize_data(df, method=norm_method)

  x_train, y_train, x_val, y_val, y_val_change_points = get_train_val(norm_df)

  if event_ix_to_plot is None:
    valid_events = norm_df[norm_df == 1]
    events_to_plot = valid_events.sample(6)
    event_ix_to_plot = events_to_plot.index 

  class_weights = get_class_weights(y_train, display=False)
  
  lr = LogisticRegression(class_weight=class_weights)
  lr.fit(x_train, y_train)

  errors = show_metrics(lr, x_val, y_val, y_val_change_points, display=False)

  chart(norm_df, event_ix_to_plot, norm_method, errors)

Результаты для каждого метода, отображающие одни и те же события, выглядят следующим образом.

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

Что происходит?

Принципиально есть две возможности.

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

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

Вам нужен этот тип «тупиковой» проверки гипотезы — это нормально.

- Разработайте гипотезу
- проверьте ее
- не подтвердите ее
- двигайтесь дальше

это типичный ход событий, и вы будете делать это много раз, прежде чем найдете что-то, что работает для вас.

Краткое содержание

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

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

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

Рекомендации