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

Обо мне

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

Введение

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

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

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

Резюме модели

Во-первых, давайте подытожим нашу модель.

Наша гипотеза состоит в том, что что-то в данных OHLC за предыдущие 4 периода предсказывает внезапное изменение на 200 или более пунктов (для упрощения мы измеряем только «вверх» или «вверх») в следующие 4 периода. Мы добавили веса классов, различные методы нормализации и множество функций, но все без особого успеха.

Текущее измерение

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

Хотя метрики хорошие, они измеряют только движение выше 200 пунктов. Однако движение выше 0 означает прибыль. Мы прогнозируем изменение цены на 200 пунктов (в нашем обучении), но если цена движется на 100 пунктов, это все равно выигрыш. Поэтому в статье 2 (опираясь на привет, мир) мы модифицировали наш алгоритм измерения, чтобы учесть это.

Однако это тоже не отражает того, как на самом деле работает торговля на валютном рынке.

Реалии торговли на Форекс

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

На приведенном выше графике выше

Период 0 — цена движется вверх › 200 пунктов (выигрыш с учетом тейк-профита)
Точка 1 — цена идет вниз, примерно туда, где она началась
Период 2 — цена продолжает двигаться вниз
> Период 3 — Цена ‹ -100 пунктов (убыток)

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

Предположим (позже они станут мета-параметрами):
- Мы выходим максимум через 4 периода (наш прогноз был 200 пунктов после четырех периодов, а затем выходим по любой цене, это период 0, 1, 2, 3)
- Тейк-профит 200 пунктов. Если цена поднимается на 200 пунктов в любой момент, мы выходим и фиксируем прибыль
- Стоп-лосс -150 пунктов. Если цена упадет на 150 пунктов в любой момент, мы выйдем из системы, приняв убыток (для защиты от того, что цена может упасть еще ниже)

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

Это влияет на две части нашего алгоритма: измерение И определение y_true, и нам нужно обновить обе.

Определение успеха (у)

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

Обратите внимание, что:
– Мы добавили дату к нашим функциям. Мы не будем использовать это как функцию (пока), но для построения графика ниже.
- При рассмотрении «тейк-профита» мы снова сравниваем «максимальную» цену за период.
- Мы рассматриваем «стоп-лосс». сравниваем с «минимальной» ценой в период
- Теперь у нас есть «пройденные точки (для определения прибыли) и период выхода (0, 1, 2 или 3 — для построения графика)

# create new holder for all values 
    x_values_df = pd.DataFrame()

    #
    # X values look back
    #

    # loop thorugh feature name and "back periods" to go back 
    x_feature_names = []
    for feature in feature_names:
        for period in [1,2,3,4]:
            # create the name (eg 'x_audusd_close_t-1')
            feature_name = 'x_' + feature + '_t-' + str(period)
            x_feature_names.append(feature_name)
            x_values_df[feature_name] = df[feature].shift(period)

    # Add "starting" values when used in normalization and date for reference 
    x_values_df['date'] = df['date']
    x_values_df['x_audusd_open'] = df['audusd_open'].shift(4)
    x_values_df['x_eurusd_open'] = df['eurusd_open'].shift(4)
    x_values_df['audusd_open'] = df['audusd_open']
    x_values_df['eurusd_open'] = df['eurusd_open']

    #
    # Y values look forward
    #

    # add all future y values for future periods
    # set y=1 for >= 200 points
    # set y=-1 for <= -150 (for later use)
    # set y=0 for all else
    # set y_points to actual points finished with (for later calculation of profit)

    x_values_df['y'] = -2       # start wtih -2 to search for not -1,0,1
    x_values_df['y_points'] = 0   # start with no point movement
    for period in [0,1,2,3]:    # loop thorugh each forward period in y (4 forward periods)
        
        # names to store future price and change points 
        name = 'y_t-' + str(period)                         #name of future y value 
        price_name = 'y_change_price_' + str(period)        # name of future y points change 
        points_name = 'y_change_points_' + str(period)      # name of future y points change 

        # add important future values to spreadsheet  
        x_values_df[name] = df['audusd_close'].shift(-period)
        x_values_df[name + '_low'] = df['audusd_low'].shift(-period)
        x_values_df[name + '_high'] = df['audusd_high'].shift(-period)

        # calculate change in points 
        x_values_df[price_name] = x_values_df[name] - df['audusd_open']
        x_values_df[points_name] = x_values_df[price_name] * 100000
        x_values_df[price_name + '_low'] = x_values_df[name + '_low'] - df['audusd_open']
        x_values_df[points_name + '_low'] = x_values_df[price_name + '_low'] * 100000
        x_values_df[price_name + '_high'] = x_values_df[name + '_high'] - df['audusd_open']
        x_values_df[points_name + '_high'] = x_values_df[price_name + '_high'] * 100000

        # get and calculate all "down" values where y isnt already set 
        # down "down" first, for case where bar goes both up and down in the same period assume down first (worst case for profit)
        down_df = x_values_df[(x_values_df['y'] == -2) & (x_values_df[points_name + '_low'] <= -150)]
        x_values_df.loc[down_df.index, 'y']  = -1
        x_values_df.loc[down_df.index, 'y_points'] = -150
        x_values_df.loc[down_df.index, 'y_finish_period'] = period 

        # get and calculate all "up" vales where y isnt already set 
        up_df = x_values_df[(x_values_df['y'] == -2) & (x_values_df[points_name + '_high'] >= 200)]
        x_values_df.loc[up_df.index, 'y'] = 1
        x_values_df.loc[up_df.index, 'y_points'] = 200
        x_values_df.loc[up_df.index, 'y_finish_period'] = period 

    # if no period triggered tp/sl then no movement (y=0) and points are whatever it is at the end of the period 
    none_df = x_values_df[x_values_df['y'] == -2]
    x_values_df.loc[none_df.index, 'y']  = 0
    x_values_df.loc[none_df.index, 'y_points'] = x_values_df[points_name]
    x_values_df.loc[none_df.index, 'y_finish_period'] = 3 

    # set down (currently -1) to 0 since we arent using it (yet - we will later)
    x_values_df.loc[x_values_df[x_values_df['y'] == -1].index, 'y']  = 0
    
    # if points exceeds tp or sl then reset to sl/tp since these limits are fixed in trading
    x_values_df.loc[x_values_df['y_points'] < -150, 'y_points'] = -150
    x_values_df.loc[x_values_df['y_points'] > 200, 'y_points'] = 200

    # and reset df (avoids indexing complications later) and done 
    x_values_df = x_values_df.copy()

    return x_values_df, x_feature_names

Обновленное измерение

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

Несколько заметок:

  • Мы упростили наше деление, используя функцию деления numpy, которая автоматически заботится о делении на 0.
  • Мы добавили показатель «прибыль», который представляет собой накопленные баллы. Помните, что у нас есть тейк-профит в 200 пунктов и стоп-лосс в 150 пунктов. (поэтому наша потенциальная прибыль выше, чем наши потенциальные убытки).
def divide(a, b):
  
  a = np.asarray(a).astype(float)
  b = np.asarray(b).astype(float)
   
  result = np.divide(a, b, out=np.zeros_like(a), where=b != 0)
  return result 

def show_metrics(lr, x, y_true, y_points, display=True):

    x = x.to_numpy()
    y_true = y_true.to_numpy()
    y_points = y_points.to_numpy()

    # 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 to confusion matrix 
    #
    tp = np.where((y_pred == 1) & (y_points >= 0), 1, 0).sum()
    fp = np.where((y_pred == 1) & (y_points < 0), 1, 0).sum()
    tn = np.where((y_pred == 0) & (y_points < 0), 1, 0).sum()
    fn = np.where((y_pred == 0) & (y_points >= 0), 1, 0).sum()

    # derived from confusion matrix 
    precision = float(divide(tp, (tp+fp)))
    recall = float(divide(tp, (tp + fn)))
    f1 = float(divide((precision*recall), (precision + recall)))

    # profit calculation (if predicted use points, otherwise 0)
    profit = np.where(y_pred==1, y_points, 0).sum()

    # 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))
        print('profit: {:.2f} points'.format(profit))

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

    return errors

Полученные результаты

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

def get_trades(lr, x,  y_change_points):
  
  y_pred = lr.predict(x)
  
  trades = [] 
  for ix in range(len(y_pred)):

    if y_pred[ix] == 1:
      won = False
      profit = y_change_points.iloc[ix]

      if profit > 0:
        won = True 

      trades.append([ix, won, profit])

  trades_df = pd.DataFrame(trades, columns=['ix', 'won', 'profit'])

  return trades_df


def chart_trades(trades, features_df, raw_df, number_of_charts):
  
  rows = math.ceil(number_of_charts / 3) 
  fig, axes = plt.subplots(nrows=rows, ncols=3)

  for chart_ix in range(number_of_charts):
    
    # get indexes and ate for chart 
    trade_ix = random.randint(0, len(trades))
    data_ix = trades['ix'].iloc[trade_ix] + 35373
    date = features_df['date'].iloc[data_ix]

    # prepard data for OHLC candles 
    mpf_df = pd.DataFrame()
    mpf_df[['date', 'Open', 'High', 'Low', 'Close', 'Volume']] = \
      raw_df[['date', 'audusd_open', 'audusd_high', 'audusd_low', 'audusd_close', 'audusd_volume']] \
      .iloc[data_ix-4:data_ix+4].to_numpy()
    mpf_df['date'] = pd.to_datetime(mpf_df['date'])
    mpf_df = mpf_df.set_index('date')
    mpf_df = mpf_df[['Open', 'High', 'Low', 'Close', 'Volume']].astype(float)

    # plot the chart 
    ax = axes.flatten()[chart_ix]
    mpf.plot(mpf_df, ax=ax, volume=False, datetime_format='', type='candle')

    # add title  
    ax.set_title(date.strftime('%d-%m-%Y %H:%M'))

    # add marker to seperate history and future 
    ax.plot([4, 4], [mpf_df['High'].iloc[4], ax.get_ylim()[1]],  color='y', marker='o', linewidth=3.0)
    ax.plot([4, 4], [ax.get_ylim()[0], mpf_df['Low'].iloc[4]],  color='y', marker='o', linewidth=3.0)

    # horizontal lines indicator tp and sl 
    open_price = mpf_df['Open'].iloc[4]
    ax.axhline(open_price+(200/100000), color='green', linewidth=1.5)
    ax.axhline(open_price-(150/100000), color='red', linewidth=1.5)

    # plot line from start to close position
    close_period = int(features_df['y_finish_period'].iloc[data_ix]) 
    profit = features_df['y_points'].iloc[data_ix]
    close_price = open_price + (profit / 100000)
    ax.plot([4,4+close_period], [mpf_df['Open'].iloc[4], close_price], color='r', marker='x', linewidth=1.5)

    # text box indicating result 
    txt = 'Win: {}\nProfit: {:.2f}\nExit: {}'.format(trades.iloc[trade_ix]['won'], trades.iloc[trade_ix]['profit'], close_period)
    props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)
    ax.text(0.05, 0.75, txt, style='italic', transform=ax.transAxes, size=10, bbox=props)

  plt.show()
  return 

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

Теперь мы можем запустить наш алгоритм тестирования и обновить метрики.

#
# Main loop to test different normalization techniques
#
for norm_method in ['price', 'points', 'percentage', 'minmax', 'stddev']:
    
  # load raw data
  raw_df = load_data()

  # create features 
  feature_names =['audusd_open', 'audusd_close', 'audusd_high', 'audusd_low', 'audusd_volume', \
                'eurusd_open', 'eurusd_close', 'eurusd_high', 'eurusd_low', 'eurusd_volume']
  df, x_feature_names = create_xy_values(raw_df, feature_names)

  # prepare data for learning (normalize, split and class weights)
  norm_df, x_feature_names_norm = normalize_data(df, x_feature_names, method=norm_method)
  x_train, y_train, x_val, y_val, y_val_change_points, no_train_samples = get_train_val(norm_df, x_feature_names_norm)
  class_weights = get_class_weights(y_train, display=False)

  # train the model 
  lr = LogisticRegression(class_weight=class_weights, max_iter=1000)
  lr.fit(x_train, y_train)

  # if we want to see all actual trades and chart them 
  trades_df = get_trades(lr, x_val, y_val_change_points)
  print('trades {}, wins {}, profit {:.2f}'.format(len(trades_df), trades_df['won'].sum(), trades_df['profit'].sum()))
    
  # if we want to chart trades 
  chart_trades(trades_df, df, raw_df, number_of_charts=9)
    
  # to show standard errors 
  print('Errrors for method {}'.format(norm_method))
  errors = show_metrics(lr, x_val, y_val, y_val_change_points, display=True)

Мы сразу видим, что производительность модели ухудшилась, и торговые потери будут значительными. Это вызывает несколько вопросов:

1. Почему производительность модели ухудшилась?
Точность не совсем низкая (около 43%), что значительно ниже предположений. Мы сделали две вещи. Рассматривая TP/SL, мы добавили еще один шаг, отделяющий результат Y от того, что происходит на самом деле. У нас также есть большое количество функций, с которыми логистическая регрессия не справится.

2. Почему торговые убытки такие большие?
Это легче увидеть, посчитав, сколько баров идет вверх, а сколько вниз.

def check_simultaneous_sl_tp(trades_df, features_df, raw_df):
  
  trade_ixs = trades_df['ix'].tolist()
  results = []

  for ix in range(35373, len(features_df)):

    low = raw_df['audusd_low'].iloc[ix]
    high = raw_df['audusd_high'].iloc[ix]
    open = raw_df['audusd_open'].iloc[ix]
    
    points_high = round((high - open) * 100000, 2)
    points_low = round((low - open) * 100000, 2)

    gone_high, gone_low, during_trade = False, False, False 

    if points_high > 200:
      gone_high = True 
    if points_low < -150:
      gone_low = True 
    if (ix-35373) in trade_ixs:
      during_trade = True 

    results.append([ix, gone_high, gone_low, during_trade])

  results_df = pd.DataFrame(results, columns=['ix', 'high', 'low', 'trade'])

  print('Total Periods {}'.format(len(results_df)))
  print('Highs {}'.format(len(results_df[results_df['high'] == True])))
  print('Lows {}'.format(len(results_df[results_df['low'] == True])))
  print('Trades {}'.format(len(results_df[results_df['trade'] == True])))
  print('Not In Trade Highs AND Lows together {}'.format(len(results_df[(results_df['high'] == True) & (results_df['low'] == True)])))
  print('In Trade Highs AND Lows together {}'.format(len(results_df[(results_df['high'] == True) & (results_df['low'] == True) & (results_df['trade'] == True)])))

  return results_df

Всего периодов 15161
Максимумы 787
Минимумы 1743
Сделки 5087
Не в сделке Максимумы и минимумы вместе 43
В торговле максимумы и минимумы вместе 29

Мы видим, что движения вниз гораздо чаще, чем вверх, и 29 сделок имеют TP и ‹ SL за один и тот же период (мы предполагаем SL, но на самом деле не знаем, куда он пошел).

Примечание о кодировании

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

Крайне важно, чтобы вы проверяли и перепроверяли свои расчеты. Помните: мусор на входе — мусор на выходе! Проверка, двойная проверка и тройная проверка. Если вы ошибетесь в своих входных данных (x или y), вы никогда не заставите их работать, или вы обнаружите, что они работают во время обучения, но не на практике, что приводит к убыткам при торговле.

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

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

Если использовать модель «как есть», наши торговые потери будут значительными.

Куда перейти к следующей статье

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

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

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