Реализация индикаторов в виде классов на примере Zigzag и ATR

Aleksandr Chugunov | 31 января, 2011


Для чего это может понадобиться?

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

Но работа одного алгоритма так и осталась неизменна. При разрывах связи с сервером или существенной синхронизации истории значение prev_calculated (или IndicatorCounted() для MetaTrader 4) обнуляется, что влечёт за собой полный пересчёт индикатора по всей истории (это сделано разработчиками терминала специально, что бы при любых изменениях истории гарантировать правильные значения индикаторов). Есть несколько моментов, которые могут отражаться на скорости вычислений индикаторов:

Чем больше вышеперечисленных пунктов применимо к вашей ситуации, тем более актуальна для вас проблема пересчёта индикаторов по всей истории. Дополнительно усугубляет ситуацию плохой канал передачи данных.

Конечно же, вы можете через дополнительный input-параметр ограничить глубину расчета индикатора, но здесь есть один нюанс при использовании iCustom индикаторов. Максимальное количество баров, с которыми работает любой график или любой пользовательский индикатор, задаётся на глобальном уровне для всего терминала. Под пользовательский индикатор для каждого буфера выделяется оперативная память на всю глубину доступной истории, ограниченной только TERMINAL_MAXBARS.

Правда здесь есть серьёзное уточнение -  если в алгоритме расчёта индикатора принудительно ограничить максимальное количество рассчитываемых баров (например через input-параметр или жёстко в коде), то память будет при этом выделяться динамически на каждом новом баре (постепенно увеличиваться до отведённого лимита в TERMINAL_MAXBARS (или чуть больше - этот алгоритм уже полностью зависит от разработчиков, в следующих билдах программы они могут его изменить)).


Какими способами можно уйти от пересчета индикатора по всей истории

На сегодняшний момент я вижу следующие пути решения данной проблемы:
  1. Попросить MetaQuotes доработать этот момент на уровне платформы
  2. Создать отдельный класс для реализации аналога prev_calculated

Был ещё вариант-предположение, что можно непосредственно в индикатор встроить алгоритм вычисления prev_calculated, но как оказалось, MetaTrader 5, в отличие от MetaTrader 4, при обнулении prev_calculated "чистит" все индикаторные буферы (т.е. в принудительном порядке производит полное обнуление всех индикаторных массивов; управлять этим нельзя, так как данная реализация на уровне платформы).

Рассмотрим каждый вариант в отдельности.


Плюсы и минусы второго варианта решения проблемы

Плюсы:
Минусы:


Создание класса CCustPrevCalculated для реализации аналога prev_calculated

В самой реализации класса нет особо интересных мест для описания. Алгоритм учитывает как расширение истории в обе стороны, так и её теоретически возможную «урезку» слева. Так же алгоритм умеет обрабатывать вставку истории внутрь просчитанных данных (для MetaTrader 4 это актуально, в МetaTrader 5 на сегодняшний момент я с этим ещё не сталкивался). Код класса находится в файле CustPrevCalculated.mqh

Расскажу о ключевых моментах.


Создание кольцевого доступа к элементам массива

При создании данного класса мы будем применять нестандартный приём – кольцевой доступ к элементам массива для однократного распределения памяти под массив и для исключения излишних процедур копирования массивов. Рассмотри этот момент подробнее на массиве из 5-ти элементов:



 
Изначально мы работаем с массивом, нумерация элементов которого начинается с 0. Но что делать, если требуется добавить следующее значение, сохранив при этом размер массива (добавление нового бара)? Есть два варианта:

Для реализации второго варианта нам понадобится переменная, назовём её DataStartInd, которая будет хранить позицию нулевого индекса массива. Для удобства дальнейших вычислений её нумерация будет соответствовать привычной индексации в массиве (т.е. начинаться с нуля). В переменной BarsLimit будет храниться количество элементов массива. Тогда реальный адрес элемента массива для виртуального индекса I будет вычисляться по простой формуле:

В переменной DataBarsCount хранится реально используемое количество ячеек памяти (мы ведь можем использовать и 3 ячейки из 5).


Алгоритмы синхронизации истории

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

Сам механизм синхронизации строится на ещё одном, задаваемом программистом, параметре – HSMinute (хранится как HistorySynchSecond). Мы делаем предположение, что ДЦ можетподкорректировать историю только за последние HSMinute минут. Если при синхронизации локальной истории не найдены разночтения за этот временной период, то истории считаются идентичными и сравнение прерывается. Если же будет найдено хоть одно расхождение – проверится и исправится вся локальная история.

Кроме того, алгоритм позволяет проверять только заданные при инициализации цены/спреды/объёмы из структуры MqlRates. К примеру, для построения ZigZag необходимы только цены High и Low.


Использование класса CCustPrevCalculated на практике

Для инициализации класса CCustPrevCalculated необходимо однократно вызвать функцию InitData(), которая вернёт истину в случае успеха:
CCustPrevCalculated CustPrevCalculated;
CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
Для синхронизации истории необходимо по необходимости вызывать функцию PrepareData():
CPCPrepareDataResultCode resData;
resData = CustPrevCalculated.PrepareData();

Варианты возврата значений функцией PrepareData():

enum CPCPrepareDataResultCode
  {
   CPCPDRC_NoData,                     // Возвращается, когда нет данных для расчета (не подготовлены сервером)
   CPCPDRC_FullInitialization,         // Была произведена полная инициализация массива
   CPCPDRC_Synch,                      // Была произведена синхронизация с добавлением новых баров
   CPCPDRC_SynchOnlyLastBar,           // Была произведена синхронизация только последнего бара (возможно с урезкой истории)
   CPCPDRC_NoRecountNotRequired        // Перерасчета не было, так как данные не изменились
  };


Функции класса CCustPrevCalculated для доступа к данным

Примечание: для ускорения расчетов исключены проверки на выход за пределы массива. Точнее, при неверном индексе будут возвращаться неверные значения.

Наименование
Применение
 uint GetDataBarsCount()
 Возвращает количество доступных баров
 uint GetDataBarsCalculated()
 Возвращает количество не изменённых баров
 uint GetDataStartInd()
 Возвращает индекс для кольцевого доступа (для пользовательских индикаторов)
 bool GetDataBarsCuttingLeft()
 Возвращает результат урезки слева баров
 double GetDataOpen(int shift, bool AsSeries)
 Возвращает Open для shift-бара
 double GetDataHigh(int shift, bool AsSeries)
 Возвращает High для shift-бара
 double GetDataLow(int shift, bool AsSeries)
 Возвращает Low для shift-бара
 double GetDataClose(int shift, bool AsSeries)
 Возвращает Close для shift-бара
 datetime GetDataTime(int shift, bool AsSeries)
 Возвращает Time для shift-бара
 long GetDataTick_volume(int shift, bool AsSeries)
 Возвращает Tick_volume для shift-бара
 long GetDataReal_volume(int shift, bool AsSeries)
 Возвращает Real_volume для shift-бара
 int GetDataSpread(int shift, bool AsSeries)
 Возвращает Spread для shift-бара


Примеры дальнейшей оптимизации класса CCustPrevCalculated


Создание класса CCustZigZagPPC для расчета пользовательского индикатора ZigZag на основе данных класса CCustPrevCalculated

Данный алгоритм основывается на пользовательском индикаторе Профессиональный ZigZag. Код класса находится в файле ZigZags.mqh, дополнительно используется библиотека OutsideBar.mqh для работы с внешним баром.

Создадим отдельную структуру для описание одного бара нашего индикатора:

struct ZZBar
  {
   double UP, DN;                      // Буферы индикатора ЗигЗаг
   OrderFormationBarHighLow OB;       // Буфер для кэширования внешнего бара
  };

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

enum CPCZZResultCode
  {
   CPCZZRC_NotInitialized,             // Класс не инициализирован
   CPCZZRC_NoData,                     // Не смогли получить данные (в том числе и для внешнего бара)
   CPCZZRC_NotChanged,                 // Изменений лучей ZZ не было
   CPCZZRC_Changed                     // Были изменения ZZ
  };

Для инициализации класса CCustZigZagPPC необходимо однократно вызвать функцию Init(), которая вернёт истину в случае успеха:

CCustZigZagPPC ZZ1;
ZZ1.Init(CustPrevCalculated, _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);

Для вычислений индикатора необходимо сначала вызывать обновление данных на основе ранее просчитанных данных класса CCustPrevCalculated:

CPCPrepareDataResultCode resZZ1;
resZZ1 = ZZ1.PrepareData(resData);

А затем процедуру Calculate():

if ( (resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired) )
   ZZ1.Calculate();

Полный пример использования одного класса CCustPrevCalculated совместно с несколькими классами CCustZigZagPPC находится в файле ScriptSample_CustZigZagPPC.mq5


Функции класса CCustZigZagPPC для доступа к данным

Наименование
Применение
 uint GetBarsCount()
 Возвращает количество доступных баров
 uint GetBarsCalculated()  Возвращает количество рассчитанных баров
 double GetUP(uint shift, bool AsSeries)
 Возвращает значение вершины ZigZag для бара
 double GetDN(uint shift, bool AsSeries)
 Возвращает значение впадины ZigZag для бара
 OrderFormationBarHighLow GetOB(uint shift, bool AsSeries)  Возвращает значение Outside бара для бара


Визуальная и программная проверка

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

Красный - оригинальный, синий - наш, рассчитанный на 100 последних барах.

Так же мы можем сравнить их и в эксперте - будет ли разница? В тестовом эксперте Expert_CustZigZagPPC_test.mq5 на каждом тике используется сравнение расчетов, полученных с помощью iCustom("AlexSTAL_ZigZagProf") и класса CCustZigZagPPC. В лог выводится информация о расчетах (возможно отсутствие расчета на первых барах из-за недостаточности истории для алгоритма):

(EURUSD,M1)                1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 1.35971; // это нормально
(EURUSD,M1) Тик обработан: 1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 
(EURUSD,M1) Расхождение на баре: 7 

Рассмотрим данный эксперт чуть по-подробнее. Определим глобальные переменные для работы:

#include <ZigZags.mqh>

CCustPrevCalculated CustPrevCalculated;
CCustZigZagPPC ZZ1;
int HandleZZ;

Проинициализируем переменные:

int OnInit()
  {
   // Создание нового класса и его инициализация
   CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
   
   // Инициализация класса ZZ
   ZZ1.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);
   
   // Получение хэндла для пользовательского индикатора
   HandleZZ = iCustom(_Symbol, _Period, "AlexSTAL_ZigZagProf", 12, 10, 0 , true);
   Print("ZZ_handle = ", HandleZZ, "  error = ", GetLastError());

   return(0);
  }
Обработка тиков в эксперте:
void OnTick()
  {
   // Расчет данных
   CPCPrepareDataResultCode resData, resZZ1;
   resData = CustPrevCalculated.PrepareData();
   
   // Для каждого индикатора вызов пересчета! PrepareData в обязательном порядке!
   resZZ1 = ZZ1.PrepareData(resData);
   
   // Расчет данных ZZ1
   if ( !((resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired)) )
      return;

   // Получим результаты расчета
   ZZ1.Calculate();

Теперь у нас есть ZZ1.GetBarsCalculated() просчитанных баров классом CCustZigZagPPC. Добавим код сравнения данных индикатора iCustom("AlexSTAL_ZigZagProf") и класса CCustZigZagPPC:

   int tmpBars = (int)ZZ1.GetBarsCalculated();
   double zzUP[], zzDN[];
   CopyBuffer(HandleZZ, 0, 0, tmpBars, zzUP);
   CopyBuffer(HandleZZ, 1, 0, tmpBars, zzDN);
   
   // Произведём сравнение
   string tmpSt1 = "", tmpSt2 = "";
   for (int i = (tmpBars-1); i >= 0; i--)
     {
      double tmpUP = ZZ1.GetUP(i, false);
      double tmpDN = ZZ1.GetDN(i, false);
      if (tmpUP != zzUP[i])
         Print("Расхождение на баре: ", i);
      if (tmpDN != zzDN[i])
         Print("Расхождение на баре: ", i);
      if (tmpUP != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpUP, _Digits) + "; ";
      if (tmpDN != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpDN, _Digits) + "; ";

      if (zzUP[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzUP[i], _Digits) + "; ";
      if (zzDN[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzDN[i], _Digits) + "; ";
     }
  Print("Тик обработан: ", tmpSt1);
  Print("                              ", tmpSt2);
  }

Вот, собственно говоря, и простое практическое применение класса CCustZigZagPPC в эксперте или скрипте. Вместо CopyBuffer() функции прямого обращения GetUP(), GetDN(), GetOB().


Переносим свой индикатор в отдельный класс (на примере iATR)

На основе файла ZigZags.mqh я изготовил шаблон MyIndicator.mqh для самостоятельного быстрого построения пользовательских индикатор по описанному принципу.

Общий план действий:

1. Подготовительный этап.

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

В моём примере в индикаторе ATR один внешний параметр:
input int InpAtrPeriod=14;  // ATR period
class CCustATR
  {
protected:
   ...
   uchar iAtrPeriod;
   ...
public:
   ...
   bool Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod);
bool CCustATR::Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod)
{
      ...
      BarsLimit = Limit;
      iAtrPeriod = AtrPeriod;
      ...

3. Определим необходимое количество буферов в исходном индикаторе, объявим их в своём классе. Так же объявим функции возврата буферов INDICATOR_DATA.

struct ATRBar
  {
   double Val;                          // Буферы индикатора
  };

на свою структуру:

struct ATRBar
  {
   double ATR;
   double TR;
  };
CPCPrepareDataResultCode CCustATR::PrepareData(CPCPrepareDataResultCode resData)
{
   ...
   for (uint i = (DataBarsCalculated == 0)?0:(DataBarsCalculated+1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].ATR = EMPTY_VALUE;
      Buf[PInd(i, false)].TR = EMPTY_VALUE;
     }
   ...

изменяем (если буфер один, то можно и не изменять)

class CCustATR
  {
   ...
   double GetVal(uint shift, bool AsSeries);                      // Возвращает значение буфера Val для бара
   ...

на

class CCustATR
  {
   ...
   double GetATR(uint shift, bool AsSeries);                      // Возвращает значение буфера ATR для бара
   ...

и меняем код соответствующих функций:

double CCustATR::GetATR(uint shift, bool AsSeries)
{
   if ( shift > (DataBarsCount-1) )
      return(EMPTY_VALUE);
   return(Buf[PInd(shift, AsSeries)].ATR);
}
Примечание: можно вместо нескольких функций возврата значений буферов использовать одну, у которой будет дополнительный параметр - номер или наименование буфера.


4. Перенесём логику функции OnCalculate() исходного индикатора в соответствующую функцию класса

CPCATRResultCode CCustATR::Calculate()
{
   ...
   // Проверим - достаточно ли количество баров для расчета
   if (DataBarsCount <= iAtrPeriod)
      return(CPCATRRC_NoData);
   ...
   if ( DataBarsCalculated != 0 )
      BarsForRecalculation = DataBarsCount - ATRDataBarsCalculated - 1;
   else
     {
      Buf[PInd(0, false)].TR = 0.0;
      Buf[PInd(0, false)].ATR = 0.0;
      //--- filling out the array of True Range values for each period
      for (uint i = 1; i < DataBarsCount; i++)
         Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                                  MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      //--- first AtrPeriod values of the indicator are not calculated
      double firstValue = 0.0;
      for (uint i = 1; i <= iAtrPeriod; i++)
        {
         Buf[PInd(i, false)].ATR = 0;
         firstValue += Buf[PInd(i, false)].TR;
        }
      //--- calculating the first value of the indicator
      firstValue /= iAtrPeriod;
      Buf[PInd(iAtrPeriod, false)].ATR = firstValue;
      
      BarsForRecalculation = DataBarsCount - iAtrPeriod - 2;
     }
   for (uint i = (DataBarsCount - BarsForRecalculation - 1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                               MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      Buf[PInd(i, false)].ATR = Buf[PInd(i-1, false)].ATR + (Buf[PInd(i, false)].TR-Buf[PInd(i-iAtrPeriod, false)].TR) / iAtrPeriod;
      ...

Всё. Наш класс создан. Для визуальной проверки можно создать тестовый индикатор (в моём примере это Indicator_ATRsample.mq5):



Во время корректировки статьи мне пришла в голову мысль, что при использовании класса CCustPrevCalculated в связке только с одним пользовательским индикатором, можно создание, инициализацию и синхронизацию данного класса интегрировать в пользовательский класс индикатора (в моих примерах это CCustZigZagPPC и CCustATR). При вызове функции инициализации пользовательских индикаторов для этого необходимо использовать нулевой указатель на объект:

   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);

При этом общая конструкция

#include <CustPrevCalculated.mqh>
#include <ATRsample.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustATR ATR;

int OnInit()
  {
   CustPrevCalculated.InitData(_Symbol, _Period, iBars, CPCHSM_Normal, 0, 30);
   ATR.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   CPCPrepareDataResultCode resData = CustPrevCalculated.PrepareData();
   CPCPrepareDataResultCode resATR = ATR.PrepareData(resData);
   if ( (resATR != CPCPDRC_NoData) && (resATR != CPCPDRC_NoRecountNotRequired) )
      ATR.Calculate();
  }

упростится до:

#include <ATRsample.mqh>
CCustATR ATR;

int OnInit()
  {
   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   ATR.Calculate();
  }
Практический пример показан в файле Indicator_ATRsample2.mq5.

Влияние описанной технологии на производительность в тестере стратегий

Для проверки был создан простой советник (TestSpeed_IndPrevCalculated.mq5), который на каждом тике получает значение индикатора нулевого бара, по одному из трёх вариантов:

enum eTestVariant
  {
   BuiltIn,    // Встроенный индикатор iATR
   Custom,     // Пользовательский индикатор iCustom("ATR")
   IndClass    // Расчет в классе
  };

Данный советник был прогнан по 10 раз на 1 запущенном агенте со следующими параметрами оптимизации:

Было измерено время оптимизации при использовании каждого из трех вариантов индикатора. Результаты представлены в виде линейной гистограммы.


Код эксперта, на котором измерялось время оптимизации:

//+------------------------------------------------------------------+
//|                                  TestSpeed_IndPrevCalculated.mq5 |
//|                                         Copyright 2011, AlexSTAL |
//|                                           http://www.alexstal.ru |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, AlexSTAL"
#property link      "http://www.alexstal.ru"
#property version   "1.00"
//--- подключим включаемый файл с классом CustATR
#include <ATRsample.mqh>
//--- перечислением лучше задавать выбор параметра
enum eTestVariant
  {
   BuiltIn,    // Встроенный индикатор iATR
   Custom,     // Пользовательский индикатор iCustom("ATR")
   IndClass    // Расчет в классе
  };
//--- входные переменные
input eTestVariant TestVariant;
input int          FalseParameter = 0;
//--- период индикатора ATR
const uchar        InpAtrPeriod = 14;
//--- хэндл встроенного или пользовательского индикатора
int                Handle;
//--- индикатор на базе класса 
CCustATR           *ATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //---
   switch(TestVariant)
     {
      case BuiltIn:
         Handle = iATR(_Symbol, _Period, InpAtrPeriod);
         break;
      case Custom:
         Handle = iCustom(_Symbol, _Period, "Examples\ATR", InpAtrPeriod);
         break;
      case IndClass:
         ATR = new CCustATR;
         ATR.Init(NULL, _Symbol, _Period, 100, CPCHSM_Normal, 0, 30, InpAtrPeriod);
         break;
     };
   //---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   switch(TestVariant)
     {
      case IndClass:
         delete ATR;
         break;
     };
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   double tmpValue[1];
   switch(TestVariant)
     {
      case BuiltIn:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case Custom:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case IndClass:
         ATR.Calculate();
         tmpValue[0] = ATR.GetATR(0, true);
         break;
     };
  }
//+------------------------------------------------------------------+

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


Замечания по практическому применению данной технологии


Заключение

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

P.S. Кто не работает, тот не ошибается! Если Вы найдёте ошибку – сообщите, пожалуйста, мне об этом.