Простой пример построения индикатора с использованием нечеткой логики (Fuzzy Logic)

Максим Востров | 7 октября, 2010

Введение

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

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

1.  Леоненков А. "Нечеткое моделирование в среде MATLAB и fuzzyTECH".
2.  Бочарников В."Fuzzy-технология: Математические основы. Практика моделирования в экономике".


1. Основы нечеткой логики

Как объяснить компьютеру такие, казалось бы, элементарные для человека высказывания вроде "...немного больше...", "...слишком быстро...", "...почти ничего...". Оказывается можно - при  помощи элементов теории нечетких множеств, а точнее, так называемых " функций принадлежности".  Приведу пример из книги А. Леоненкова:

Опишем функцию принадлежности для высказывания "горячий кофе": температуру кофе следует рассматривать в диапазоне от 0 до 100 градусов Цельсия по той простой причине, что при температуре меньше 0 градусов это будет лед, а выше 100 градусов - пар. Очевидно, что чашка кофе с температурой 20 градусов никак не может быть названа горячей, то есть функция принадлежности кофе к категории "горячий" равна 0, а вот чашка кофе с температурой 70 градусов уже однозначно принадлежит к категории "горячий" и соответственно значение функции в этом случае равно 1.

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

Тем не менее примерный вид функции принадлежности мы можем себе представить: она "монотонно возрастающая":


На рисунке, приведенном выше, изображена "кусочно-линейная" функция принадлежности.

Таким образом, функция может быть задана следующим аналитическим выражением:


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


2. Функция принадлежности

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

Для начала нам нужно определиться с граничными условиями. Путь граничным условием для определения «100% Тренд вверх» будет пересечение EMA с периодом 2, построенной по типичной цене (H+L+C)/3, с верхней границей конверта Envelopes  с параметрами 8, 0.08, SMA, Close., а «100% тренд вниз» с нижней границей. Все, что между ними, будем считать флетом.  Для  пущей важности добавим еще один конверт с параметрами 32, 0.15, SMA, Close. 

В итоге мы должны получить две идентичные функции принадлежности.  Сигналом к покупке будет ситуация, когда обе функции будут равны  1, к продаже соответственно -1.  Т.к. графики функции удобно строить в диапазоне от -1 до 1,  результирующий график получим путем среднего арифметического результатов двух функций F(x)= (f1(x)+f2(x))/2.

Вот так это выглядит на чарте:


В таком случае функция принадлежности примет следующее графическое отображение:


Аналитически его можно записать в следующем виде:

,

где a и b верхняя и нижняя линии конверта соответственно, а х - значение EMA(2).

С функцией определились, теперь переходим к написанию кода индикатора.


3. Составляем программный код

Сначала определимся с тем, что и как будем рисовать.

Результаты вычислений функции принадлежности будем выводить линией - красной и синей соответственно.

Среднее арифметическое будем рисовать гистограммой от нулевой линии и раскрашивать в зависимости от значения результирующей функции в пять цветов:

Для этого будем использовать стиль рисования DRAW_COLOR_HISTOGRAM.

Для выдачи сигналов на покупку и продажу нарисуем прямоугольники синего и красного цветов над теми полосками гистограммы, значение которых равно 1 или -1.

Запускаем MetaEditor и приступаем. Создать->Пользовательский индикатор->Далее... Заполняем окошко "параметры":


 Создаем буферы:


Жмем кнопку "Готово", получаем исходный код и начинаем его дорабатывать.

Во-первых, определимся с количеством буферов. Семь у нас уже создал мастер (5 для данных, 2 для цвета). Нам понадобится еще 5.

#property indicator_minimum -1.4 // Ставим дробные значения
#property indicator_maximum 1.4  // мастер экспертов почему-то игнорирует дробные части
#property indicator_buffers 12   // Меняем значение с 7 на 12 (добавилось еще 5 буферов)
Редактируем входные параметры:
input string txt1="----------";
input int                  Period_Fast=8;
input ENUM_MA_METHOD        Method_Fast = MODE_SMA; /*Метод усреднения*/ //метод усреднения скользящей 
input ENUM_APPLIED_PRICE    Price_Fast  = PRICE_CLOSE;
input double               Dev_Fast=0.08;
input string txt2="----------";
input int                  Period_Slow=32;
input ENUM_MA_METHOD        Method_Slow = MODE_SMA;
input ENUM_APPLIED_PRICE    Price_Slow  = PRICE_CLOSE;
input double               Dev_Slow=0.15;  /*Параметр отклонения*/
input string txt3="----------";
input int                  Period_Signal=2;
input ENUM_MA_METHOD        Method_Signal = MODE_EMA;
input ENUM_APPLIED_PRICE    Price_Signal  = PRICE_TYPICAL;
input string txt4="----------";

Очень удобным нововведением является комментарий, стоящий за объявленной переменной, его текст вставляется в окошко параметров индикатора.

Также очень радует возможность создания списков:


Резервируем переменные под хендлы индикаторов и индикаторные буферы:

int Envelopes_Fast;     // Быстрый конверт
int Envelopes_Slow;     // Медленный конверт
int MA_Signal;          // Сигнальная линия

double Env_Fast_Up[];   // Верхняя граница быстрого конверта
double Env_Fast_Dn[];   // Нижняя граница быстрого конверта

double Env_Slow_Up[];   // Верхняя граница медленного конверта
double Env_Slow_Dn[];   // Нижняя граница медленного конверта

double Mov_Sign[];      // Сигнальная линия

Теперь переходим в функцию OnInit().

Наведем немного красоты: зададим имя индикатору и уберем лишние десятичные нули:

IndicatorSetInteger(INDICATOR_DIGITS,1); // зададим точность отображения, большая нам не нужна 
string name;    // имя индикатора 
StringConcatenate(name, "FLE ( ", Period_Fast, " , ", Dev_Fast, " | ", Period_Slow, " , ", Dev_Slow, " | ", Period_Signal, " )"); 
IndicatorSetString(INDICATOR_SHORTNAME,name);

и добавим недостающие буферы:

SetIndexBuffer(7,Env_Fast_Up,INDICATOR_CALCULATIONS);
SetIndexBuffer(8,Env_Fast_Dn,INDICATOR_CALCULATIONS);
SetIndexBuffer(9,Env_Slow_Up,INDICATOR_CALCULATIONS);
SetIndexBuffer(10,Env_Slow_Dn,INDICATOR_CALCULATIONS);
SetIndexBuffer(11,Mov_Sign,INDICATOR_CALCULATIONS); 

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

Обратите внимание на то, как обьявляются индикаторы с буфером цвета:

SetIndexBuffer(4,SignalBuffer1,INDICATOR_DATA);     // Сперва все буферы индикатора 
SetIndexBuffer(5,SignalBuffer2,INDICATOR_DATA);     // т.к это Color Histogram2 то у него 2 буфера данных
SetIndexBuffer(6,SignalColors,INDICATOR_COLOR_INDEX);// а потом идет буфер цвета.

заполняем хендлы:

Envelopes_Fast = iEnvelopes(NULL,0,Period_Fast,0,Method_Fast,Price_Fast,Dev_Fast);
Envelopes_Slow = iEnvelopes(NULL,0,Period_Slow,0,Method_Slow,Price_Slow,Dev_Slow);
MA_Signal      = iMA(NULL,0,Period_Signal,0,Method_Signal,Price_Signal);

С функцией OnInit() все.

Теперь создадим функцию, которая будет рассчитывать значение функции принадлежности:

double Fuzzy(double x,double a, double c)
{
double F;
     if (a<x)          F=1;                // 100% Тренд вверх
else if (x<=a && x>=c)  F=(1-2*(a-x)/(a-c));// Флет
else if (x<c)           F=-1;               // 100% Тренд вниз
return (F);
}

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

Приступаем непосредственно к основной функции OnCalculate().

Для начала запишем в промежуточные буферы значения необходимых нам индикаторов. Воспользуемся функцией CopyBuffer():

CopyBuffer(Envelopes_Fast,  // Хендл индикатора
           UPPER_LINE,      // Буфер индикатора
           0,              // Откуда начинаем 0 - с самого начала
           rates_total,    // Сколько копируем - Все 
           Env_Fast_Up);   // Буфер в который записываются значения
// - остальные по аналогии
CopyBuffer(Envelopes_Fast,LOWER_LINE,0,rates_total,Env_Fast_Dn);
CopyBuffer(Envelopes_Slow,UPPER_LINE,0,rates_total,Env_Slow_Up);
CopyBuffer(Envelopes_Slow,LOWER_LINE,0,rates_total,Env_Slow_Dn);
CopyBuffer(MA_Signal,0,0,rates_total,Mov_Sign);

 Здесь необходимо добавить код для оптимизации вычислений (пересчет только последнего бара)

// объявляем переменную start, в ней будет храниться индекс бара с которого будет осуществляться
// пересчет индикаторных буферов.

int start;              
if (prev_calculated==0// если ни один бар не просчитан
    {
    start = Period_Slow; // до этого значения не все индикаторы просчитаны поэтому исполнять код не имеет смысла
    }
else start=prev_calculated-1;

for (int i=start;i<rates_total;i++)
      {
      // Здесь будет записан весь оставшийся код
      }

Кода осталось совсем чуть-чуть.

Задаем параметры x, a, b, производим расчет значения функции принадлежности и записываем его в соответствующий буфер:

double x = Mov_Sign[i]; // Сигнал
// Зададим параметры первой функции принадлежности:
double a1 = Env_Fast_Up[i]; // Верхняя граница
double b1 = Env_Fast_Dn[i];
// вычислим значение первой функции принадлежности и запишем ее в буфер
Rule1Buffer[i] = Fuzzy(x,a1,b1);
// Зададим параметры второй функции принадлежности:
double a2 = Env_Slow_Up[i]; // Верхняя граница
double b2 = Env_Slow_Dn[i];
// вычислим значение второй функции принадлежности и запишем ее в буфер
Rule2Buffer[i] = Fuzzy(x,a2,b2);

Две индикаторные линии построены.

Теперь рассчитаем результирующее значение.

ResultBuffer[i] = (Rule1Buffer[i]+Rule2Buffer[i])/2;

Далее раскрасим полоски гистограммы соответствующими цветами: т.к. цветов пять, то ResultColors[i] может принимать значение от 0 до 4.

Вообще цветов может быть до 64, так что это невероятная возможность для полета фантазии.

for (int ColorIndex=0;ColorIndex<=4;ColorIndex++) 
    { 
    if (MathAbs(ResultBuffer[i])>0.2*ColorIndex && MathAbs(ResultBuffer[i])<=0.2*(ColorIndex+1)) 
        { 
        ResultColors[i] = ColorIndex; 
        break; 
        } 
    }

Далее нарисуем сигнальные прямоугольники, нам понадобится стиль рисования DRAW_COLOR_HISTOGRAM2.

У него два буфера данных, между которыми строится полоска гистограммы и один буфер цвета.

Значения буферов данных всегда будут постоянными: 1.1 и 1.3 для сигнала на покупку, -1.1 и -1.3 для сигнала на продажу соответственно.

Значение EMPTY_VALUE будет соответствовать отсутствию сигнала.

      if (ResultBuffer[i]==1)
        {
        SignalBuffer1[i]=1.1;
        SignalBuffer2[i]=1.3;
        SignalColors[i]=1;
        }
      else if (ResultBuffer[i]==-1)
        {
        SignalBuffer1[i]=-1.1;
        SignalBuffer2[i]=-1.3;
        SignalColors[i]=0;
        }
      else
        {
        SignalBuffer1[i]=EMPTY_VALUE;
        SignalBuffer2[i]=EMPTY_VALUE;
        SignalColors[i]=EMPTY_VALUE;
        }

Жмем "Компилировать" и вуаля!



Заключение

Что можно сказать в заключении? В своей статье я затронул самый элементарнейший подход к нечеткой логике.

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


Думаю, у вас не составит труда написать для нее аналитическое выражение и найти подходящие условия.

Успехов!