C#

Руководство по программному расчету значений индикатора RSI на C#

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

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

История

RSI — это индикатор индекса относительной силы. Широко используемый индикатор моментума. Он измеряет силу изменения цены, чтобы оценить состояния перекупленности или перепроданности на рынке.

RSI может иметь иметь значения от 0 до 100. Изначально разработанный Дж. Уэллсом Уайлдером-младшим, американским инженером-механиком. Уайлдер изобрел еще несколько технических индикаторов, которые теперь входят в стандартную коллекцию большинства программ для технического анализа и торговых терминалов. В этот список входят: средний истинный диапазон (ATR), индекс относительной силы (RSI), средний индекс направления (ADX) и Parabolic SAR (параболический стоп и разворот).

Расчеты значения RSI не так просты, как индикатор Simple Moving Average (SMA), который просто вычисляет среднее значение ряда Open, High, Low или Close. Помимо основной формулы есть ряд мелких, часто упускаемых из виду деталей, которые в итоге играют важную роль.

Основная логика

Шаг 1.1 — Отличия

Допустим, у нас есть стандартный RSI 14, основанный на ценах закрытия. Сначала он вычисляет разницу между двумя последовательными ценами закрытия (Close — предыдущее закрытие), затем возвращается в историю дальше и вычисляет разницу между предыдущей ценой закрытия и ценой закрытия до этого, вплоть до (минимум) 14 значений, требующих в общей сложности 15 цен закрытия, чтобы иметь по крайней мере 14 различий для основных уравнений, но на самом деле вам абсолютно необходимы все бары, которые вы можете получить (ниже я покажу вам причину), упрощенная запись (разница = последнее закрытие – предыдущее закрытие): DIFF = C – PC

Шаг 1.2 — Разделение на положительную и отрицательную разницу

Теперь нам нужно получить 2 набора значений из разницы, которые мы получили на шаге 1. Список положительных значений и список отрицательных значений, мы должны хранить отрицательные значения различий как абсолютные значения без знака минус :

PD = DIFF > 0 ? DIFF : 0

И для отрицательного:

ND = DIFF < 0 ? DIFF * -1 : 0

Шаг 1.3 — Вычисление средней положительной и средней отрицательной разности

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

APD = (PD1 + PD2 + PD3 + PD4 + PD5 + PD6 + PD7 + PD8 + PD9 + PD10 + PD11 + PD12 + PD13 + PD14) / 14

И средняя отрицательная разница:

AND = (ND1 + ND2 + ND3 + ND4 + ND5 + ND6 + ND7 + ND8 + ND9 + ND10 + ND11 + ND12 + ND13 + ND14) / 14

Поскольку у нас уже есть первые значения APD и AND, каждый последующий (15-й, 16-й и т. д.) расчет APD и AND будет учитывать предыдущие значения APD по следующей формуле:

APD = (PREVIOUS_APD * 13 + PD1) / 14

То же самое касается AND:

AND = (PREVIOUS_AND * 13 + ND1) / 14

Шаг 1.4 — расчет относительной силы (RS)

RS = APD / AND

Шаг 1.5 — расчет индекса относительной силы (RSI)

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

100-(100/(1+RS))

Основная часть

Шаг 2.1 — обязательно к прочтению:

Дело в том, что у вас не будет точных значений RSI, если у вас нет точно такого же количества доступных исторических данных. Если торговый терминал имеет индикатор RSI 14, прикрепленный к графику EURUSD H1 с сотнями тысяч баров на нем, то вам понадобится точно такое же количество баров, чтобы получить точно такое же значение. И исправить это нельзя.

По мере того, как количество свечей или цен закрытия в вашем распоряжении увеличивается, значения будут становиться все ближе к значению терминала. Если у вас всего 14 или даже 50 свечей, RSI может вернуть значение 53 на последней свече, а терминал может показать 50. Разница легко может составлять 2, 4 или достигать 6.

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

Вот скриншот версии Excel, которая генерирует почти такое же окончательное значение RSI после 102 свечей, как и торговый терминал Metatrader 5 (разница составляет всего 0,01, график EURUSD H1):

MT5 RSI(14) EURUSD H1 — Final Value: 53.23
Excel RSI(14) EURUSD H1 — Final Value: 53.24

Шаг – 2.2

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

public static double CalculateDifference(double current_price, double previous_price)
{
    return current_price - previous_price;
}

Вычисление положительных и отрицательных значений:

public static double CalculatePositiveChange(double difference)
{
    return difference > 0 ? difference : 0;
}
public static double CalculateNegativeChange(double difference)
{
    return difference < 0 ? difference * -1 : 0;
}

Пора создавать глобальные массивы положительных и отрицательных изменений для экономии ресурсов. И массивы среднего прироста и убывания, а также RSI.

На самом деле нам не нужен массив RS (Relative Strength), потому что это всего лишь средний шаг в расчете RSI (индекс относительной силы).

public static double[] positiveChanges;
public static double[] negativeChanges;
public static double[] averageGain;
public static double[] averageLoss;
public static double[] rsi;

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

positiveChanges = new double[DataManager.data.Rows.Count];
negativeChanges = new double[DataManager.data.Rows.Count];
averageGain = new double[DataManager.data.Rows.Count];
averageLoss = new double[DataManager.data.Rows.Count];
rsi = new double[DataManager.data.Rows.Count];

DataManager.data — это объект DataTable, в котором хранятся данные о ценах, экспортированные из терминала MT5. Количество записей DataTable используется для определения размеров массива. Теперь мы можем создать следующую функцию, которая будет вычислять среднюю прибыль и средний убыток. Взгляните на все функции и массивы, объединенные ниже:

public static double CalculateDifference(double current_price, double previous_price)
    {
        return current_price - previous_price;
    }

public static double CalculatePositiveChange(double difference)
    {
        return difference > 0 ? difference : 0;
    }

public static double CalculateNegativeChange(double difference)
    {
        return difference < 0 ? difference * -1 : 0;
    }


public static void CalculateAverageGainsAndLosses(int rsi_period, int price_index = 5)
    {
        for(int i = 0; i < DataManager.data.Rows.Count; i++)
        {
            double current_difference = 0.0;
            if (i > 0)
            {
                double previous_close = Convert.ToDouble(DataManager.data.Rows[i-1].Field<string>(price_index));
                double current_close = Convert.ToDouble(DataManager.data.Rows[i].Field<string>(price_index));
                current_difference = CalculateDifference(current_close, previous_close);
            }
            
            PriceEngine.positiveChanges[i] = CalculatePositiveChange(current_difference);
            PriceEngine.negativeChanges[i] = CalculateNegativeChange(current_difference);

            if(i == Math.Max(1,rsi_period))
            {
                double gain_sum = 0.0;
                double loss_sum = 0.0;
                for(int x = Math.Max(1,rsi_period); x > 0; x--)
                {
                    gain_sum += PriceEngine.positiveChanges[x];
                    loss_sum += PriceEngine.negativeChanges[x];
                }
                PriceEngine.averageGain[i] = gain_sum / Math.Max(1,rsi_period);
                PriceEngine.averageLoss[i] = loss_sum / Math.Max(1,rsi_period);
            }else if (i > Math.Max(1,rsi_period))
            {
                PriceEngine.averageGain[i] = ( PriceEngine.averageGain[i-1]*(rsi_period-1) + PriceEngine.positiveChanges[i]) / Math.Max(1, rsi_period);
                PriceEngine.averageLoss[i] = ( PriceEngine.averageLoss[i-1]*(rsi_period-1) + PriceEngine.negativeChanges[i]) / Math.Max(1, rsi_period);
            }
        }
    }

Давайте добавим последнюю строку в функцию CalculateAverageGainsAndLosses и переименуем ее:

public static double CalculateDifference(double current_price, double previous_price)
    {
        return current_price - previous_price;
    }

public static double CalculatePositiveChange(double difference)
    {
        return difference > 0 ? difference : 0;
    }

public static double CalculateNegativeChange(double difference)
    {
        return difference < 0 ? difference * -1 : 0;
    }


public static void CalculateRSI(int rsi_period, int price_index = 5)
    {
        for(int i = 0; i < DataManager.data.Rows.Count; i++)
        {
            double current_difference = 0.0;
            if (i > 0)
            {
                double previous_close = Convert.ToDouble(DataManager.data.Rows[i-1].Field<string>(price_index));
                double current_close = Convert.ToDouble(DataManager.data.Rows[i].Field<string>(price_index));
                current_difference = CalculateDifference(current_close, previous_close);
            }
            PriceEngine.positiveChanges[i] = CalculatePositiveChange(current_difference);
            PriceEngine.negativeChanges[i] = CalculateNegativeChange(current_difference);

            if(i == Math.Max(1,rsi_period))
            {
                double gain_sum = 0.0;
                double loss_sum = 0.0;
                for(int x = Math.Max(1,rsi_period); x > 0; x--)
                {
                    gain_sum += PriceEngine.positiveChanges[x];
                    loss_sum += PriceEngine.negativeChanges[x];
                }

                PriceEngine.averageGain[i] = gain_sum / Math.Max(1,rsi_period);
                PriceEngine.averageLoss[i] = loss_sum / Math.Max(1,rsi_period);

            }else if (i > Math.Max(1,rsi_period))
            {
                PriceEngine.averageGain[i] = ( PriceEngine.averageGain[i-1]*(rsi_period-1) + PriceEngine.positiveChanges[i]) / Math.Max(1, rsi_period);
                PriceEngine.averageLoss[i] = ( PriceEngine.averageLoss[i-1]*(rsi_period-1) + PriceEngine.negativeChanges[i]) / Math.Max(1, rsi_period);

                PriceEngine.rsi[i] = PriceEngine.averageLoss[i] == 0 ? 100 : PriceEngine.averageGain[i] == 0 ? 0 : Math.Round(100 - (100 / (1 + PriceEngine.averageGain[i] / PriceEngine.averageLoss[i])), 5);
            }
        }
    }

Поскольку объект DataEngine.data содержит точно такое же количество свечей, экспортированных из самого терминала МТ5, то мы получаем в этой программе точно такое же значение, как и в МТ5!

Полный класс PriceEngine.cs

using System;
using System.Data;
using System.Globalization;

namespace YourNameSpace
  {
   class PriceEngine
      {
        public static DataTable data;
        public static double[] positiveChanges;
        public static double[] negativeChanges;
        public static double[] averageGain;
        public static double[] averageLoss;
        public static double[] rsi;
        
        public static double CalculateDifference(double current_price, double previous_price)
          {
              return current_price - previous_price;
          }

        public static double CalculatePositiveChange(double difference)
          {
              return difference > 0 ? difference : 0;
          }

        public static double CalculateNegativeChange(double difference)
          {
              return difference < 0 ? difference * -1 : 0;
          }

        public static void CalculateRSI(int rsi_period, int price_index = 5)
          {
              for(int i = 0; i < PriceEngine.data.Rows.Count; i++)
              {
                  double current_difference = 0.0;
                  if (i > 0)
                  {
                      double previous_close = Convert.ToDouble(PriceEngine.data.Rows[i-1].Field<string>(price_index));
                      double current_close = Convert.ToDouble(PriceEngine.data.Rows[i].Field<string>(price_index));
                      current_difference = CalculateDifference(current_close, previous_close);
                  }
                  PriceEngine.positiveChanges[i] = CalculatePositiveChange(current_difference);
                  PriceEngine.negativeChanges[i] = CalculateNegativeChange(current_difference);

                  if(i == Math.Max(1,rsi_period))
                  {
                      double gain_sum = 0.0;
                      double loss_sum = 0.0;
                      for(int x = Math.Max(1,rsi_period); x > 0; x--)
                      {
                          gain_sum += PriceEngine.positiveChanges[x];
                          loss_sum += PriceEngine.negativeChanges[x];
                      }

                      PriceEngine.averageGain[i] = gain_sum / Math.Max(1,rsi_period);
                      PriceEngine.averageLoss[i] = loss_sum / Math.Max(1,rsi_period);

                  }else if (i > Math.Max(1,rsi_period))
                  {
                      PriceEngine.averageGain[i] = ( PriceEngine.averageGain[i-1]*(rsi_period-1) + PriceEngine.positiveChanges[i]) / Math.Max(1, rsi_period);
                      PriceEngine.averageLoss[i] = ( PriceEngine.averageLoss[i-1]*(rsi_period-1) + PriceEngine.negativeChanges[i]) / Math.Max(1, rsi_period);
                      PriceEngine.rsi[i] = PriceEngine.averageLoss[i] == 0 ? 100 : PriceEngine.averageGain[i] == 0 ? 0 : Math.Round(100 - (100 / (1 + PriceEngine.averageGain[i] / PriceEngine.averageLoss[i])), 5);
                  }
              }
          }
          
        public static void Launch()
          {
            PriceEngine.data = new DataTable();            
            //load {date, time, open, high, low, close} values in PriceEngine.data (6th column (index #5) = close price) here
            
            positiveChanges = new double[PriceEngine.data.Rows.Count];
            negativeChanges = new double[PriceEngine.data.Rows.Count];
            averageGain = new double[PriceEngine.data.Rows.Count];
            averageLoss = new double[PriceEngine.data.Rows.Count];
            rsi = new double[PriceEngine.data.Rows.Count];
            
            CalculateRSI(14);
          }
          
      }
  }

Спасибо за прочтение и удачной торговли!

To top