Контроль наклона кривой баланса во время работы торгового эксперта
Введение
В статье описывается один из подходов, позволяющий улучшить характеристики торгового эксперта при помощи создания обратной связи. В данном случае обратная связь будет построена на измерении наклона кривой баланса депозита. Контроль наклона осуществляется автоматически, регулированием рабочего лота. Возможна работа эксперта в режимах: торговля урезанным количеством лотов, торговля рабочим количеством лотов (согласно первоначальной установке) и торговля промежуточным количеством лотов. Режим работы переключается автоматически.
При этом используются различные регулировочные характеристики в цепи обратной связи: ступенчатая, ступенчатая с гистерезисом, линейная. Это позволяет настроить систему контроля наклона кривой баланса на характеристики конкретной торговой системы.
Основная идея - это автоматизировать процесс принятия решения трейдером при мониторинге своей торговой системы. Когда наступает неблагоприятный период работы, будет разумным снизить риски. При возвращении к нормальному режиму работы - риски можно вернуть к исходному значению.
Естественно, данная система не панацея и не сделает из убыточного советника прибыльного. Это своего рода надстройка над ММ советника (управлением деньгами), не позволяющая ему получить существенные убытки на счете.
К статье прилагается библиотека, позволяющая встроить в код произвольного торгового эксперта данную функцию.
Принцип работы
Рассмотрим подробней принцип работы системы, контролирующей наклон кривой баланса. Представим, что у нас есть некий торгующий советник. Его гипотетическая кривая баланса выглядит следующим образом:
Рисунок 1. Принцип работы системы, контролируюшей наклон кривой баланса
Вверху - исходная кривая баланса для советника с постоянным объемом торговых операций (далее, размером лота). Красными точками отмечены закрытые сделки. Соединим эти точки между собой - получим ломаную линию, отражающую изменение баланса советника в процессе торговли (толстая черная линия).
Теперь будем непрерывно отслеживать угол наклона этой линии к оси времени (показано тонкими синими линиями). А точнее, перед открытием очередной сделки по сигналу торговой системы, будем рассчитывать угол наклона по предыдущим двум закрытым сделкам (по двум сделкам - для простоты изложения). Если угол наклона стал меньше заданного значения, то начинает работать наша система контроля, которая уменьшает количество лотов в соответствие с рассчитанным значением угла и заданной регулировочной функцией.
Таким образом, если торговля попала в неудачную полосу, объем уменьшается от Лмакс. до Лмин. на интервале торговли Т3...Т5. После точки Т5 торговля ведется минимальным заданным объемом - режим режекции торгового объема. После восстановления профитности работы советника и возрастания угла наклона кривой баланса свыше заданного значения, количество лотов начинает увеличиваться. Это происходит на интервале Т8...Т10. После точки Т10 объем в торговых операциях восстанавливается до исходного значения Лмакс.
Результирующая кривая баланса, получающаяся в результате такого регулирования, показана в нижней части рис.1. Видно, что исходная просадка от Б1 до Б2 уменьшилась от Б1 до Б2*. Также видно, что несколько уменьшилась и прибыль на участке восстановления максимального лота Т8...Т10 - обратная сторона медали.
Зеленым цветом выделен участок кривой баланса, где торговля ведется минимальным заданным объемом. Желтым цветом - участки перехода от максимального объема к минимальному и обратно. Здесь возможно несколько вариантов перехода:
- ступенчатый - объем меняется скачком от максимального до минимального и обратно;
- линейный - объем линейно меняется в зависимости от угла наклона кривой баланса в интервале регулирования;
- ступенчатая с гистерезисом - переход от максимального объема к минимальному и обратный переход происходят при разных значениях угла наклона;
Проиллюстрируем это рисунками:
Рисунок 2. Виды регулировочной характеристики
Регулировочная характеристика влияет на показатели системы контроля - задержку включения/выключения, переходный процесс при переключении от максимального лота к минимальному и обратно. Выбор той или иной характеристики рекомендуется производить экспериментально по достижению наилучших результатов при тестировании.
Таким образом, мы добавляем в торговую систему обратную связь по углу наклона кривой баланса. Здесь надо оговориться, что такое регулирование рабочего лота подходит только для тех торговых систем, в которых размер лота не является частью самой торговой системы. Например, если используется принцип Мартингейла, то использование такой системы не возможно напрямую, без переделок исходного эксперта.
Также, надо отметить несколько важных моментов:
- эффективность управления наклоном кривой баланса напрямую зависит от соотношения рабочего лота в режиме нормальной работы к рабочему лоту в режиме режекции лота. Чем соотношение больше, тем эффективней управление. Поэтому исходный рабочий лот должен быть существенно больше минимально возможного.
- средний период чередования взлетов/падений баланса эксперта должен быть существенно больше, чем время реакции системы контроля. Иначе система просто не будет успевать регулировать наклон кривой баланса. Чем больше отношение среднего периода к времени реакции, тем эффективней работа системы. Данное требование относится практически к любой системе автоматического регулирования.
Реализация на MQL5 с использование объектно-ориентированного программирования
Напишем библиотеку, реализующую описанный выше подход. Для этого используем новые возможности, предоставляемые языком MQL5 - объектно-ориентированный подход. Такой подход позволит в дальнейшем развивать и расширять нашу библиотеку достаточно просто, без переписывания больших участков кода заново.
Класс TradeSymbol
Поскольку в новой платформе MetaTrader 5 предусмотрено мультивалютное тестирование торговых стратегий, то нам требуется класс, инкапсулирующий в себе всю работу с произвольным рабочим инструментом (символом). Это позволит использовать данную библиотеку в мультивалютных экспертах. Прямого отношения к системе контроля данный класс не имеет - он вспомогательный. Итак, первый класс будет использоваться для операций с рабочим инструментом.
//--------------------------------------------------------------------- // Операции с рабочим инструментом: //--------------------------------------------------------------------- class TradeSymbol { private: string trade_symbol; // рабочий инструмент private: double min_trade_volume; // минимально допустимый объем для торговых операций double max_trade_volume; // максимально допустимый объем для торговых операций double min_trade_volume_step; // минимальное изменение объема double max_total_volume; // максимальный совокупный объем по символу double symbol_point; // значение одного пункта double symbol_tick_size; // минимальное изменение цены int symbol_digits; // число знаков после запятой protected: public: void RefreshSymbolInfo( ); // обновить рыночную информацию по рабочему инструменту void SetTradeSymbol( string _symbol ); // установить/изменить рабочий инструмент string GetTradeSymbol( ); // получить рабочий инструмент double GetMaxTotalLots( ); // получить максимальный совокупный объем double GetPoints( double _delta ); // получить изменение цены в пунктах public: double NormalizeLots( double _requied_lot ); // получить нормализованный торговый объем double NormalizePrice( double _org_price ); // получить нормализованную цену с учетом шага изменения котировки public: void TradeSymbol( ); // конструктор void ~TradeSymbol( ); // деструктор };
Структура класса очень простая. Назначение - получение, хранение и обработка текущей рыночной информации по заданному инструменту. Основные методы - TradeSymbol::RefreshSymbolInfo, TradeSymbol::NormalizeLots, TradeSymbol::NormalizePrice. Рассмотрим их последовательно.
Метод TradeSymbol::RefreshSymbolInfo предназначен для обновления рыночной информации по рабочему инструменту.
//--------------------------------------------------------------------- // Обновить рыночную информацию по рабочему инструменту: //--------------------------------------------------------------------- void TradeSymbol::RefreshSymbolInfo() { // Если не задан рабочий инструмент, то дальше ничего не делаем: if(GetTradeSymbol() == NULL) { return; } // Считаем параметры, необходимые для нормализации лота: min_trade_volume = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_VOLUME_MIN ); max_trade_volume = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_VOLUME_MAX ); min_trade_volume_step = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_VOLUME_STEP); max_total_volume = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_VOLUME_LIMIT); symbol_point = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_POINT); symbol_tick_size = SymbolInfoDouble(GetTradeSymbol(), SYMBOL_TRADE_TICK_SIZE); symbol_digits = ( int )SymbolInfoInteger(GetTradeSymbol(), SYMBOL_DIGITS); }
Один важный момент, который встречается в нескольких методах. Поскольку в текущей реализации MQL5 нельзя использовать конструктор с параметрами, то для первоначального задания рабочего инструмента обязательно надо вызвать метод:
void SetTradeSymbol(string _symbol); // установить/изменить рабочий инструмент
Метод TradeSymbol::NormalizeLots используется для получения правильного и нормализованного торгового объема. Известно, что размер позиции не может быть меньше минимально возможной величины, которую разрешает открывать брокер. Дискретность минимального изменения позиции также определяется брокером и может различаться. Данный метод возвращает ближайшее снизу значение лота.
Также проверяется, не выходит ли объем предполагаемой позиции за пределы максимальной величины, разрешенной брокером.
//--------------------------------------------------------------------- // Получить нормализованный торговый объем: //--------------------------------------------------------------------- // - на входе требуемый лот; // - на выходе нормализованный лот; //--------------------------------------------------------------------- double TradeSymbol::NormalizeLots( double _requied_lots ) { double lots, koeff; int nmbr; // Если не задан рабочий инструмент, то дальше ничего не делаем: if( GetTradeSymbol( ) == NULL ) { return( 0.0 ); } if( this.min_trade_volume_step > 0.0 ) { koeff = 1.0 / min_trade_volume_step; nmbr = ( int )MathLog10( koeff ); } else { koeff = 1.0 / min_trade_volume; nmbr = 2; } lots = MathFloor( _requied_lots * koeff ) / koeff; // Ограничение лота снизу: if( lots < min_trade_volume ) { lots = min_trade_volume; } // Ограничение лота сверху: if( lots > max_trade_volume ) { lots = max_trade_volume; } lots = NormalizeDouble( lots, nmbr ); return( lots ); }
Метод TradeSymbol::NormalizePrice используется для получения правильной и нормализованной цены. Поскольку число значимых знаков после запятой (точность представления цены) должна быть определенной для данного рабочего инструмента, то требуется округление цены. Кроме того, некоторые инструменты (например, фьючерсы) имеют минимальную дискретность изменения цены большую одного пункта. Поэтому требуется сделать значение цены кратным минимальной дискретности.
//--------------------------------------------------------------------- // Нормализация цены с учетом шага изменения котировок: //--------------------------------------------------------------------- double TradeSymbol::NormalizePrice( double _org_price ) { // Минимальный размер шага изменения котировок, пунктов: double min_price_step = NormalizeDouble( symbol_tick_size / symbol_point, 0 ); double norm_price = NormalizeDouble( NormalizeDouble(( NormalizeDouble( _org_price / symbol_point, 0 )) / min_price_step, 0 ) * min_price_step * symbol_point, symbol_digits ); return( norm_price ); }
На вход метода подается требуемая ненормализованная цена. Возвращается правильная нормализованная цена, наиболее близкая к требуемой.
Назначение остальных методов понятно из комментариев и какого-либо описания не требуют.
Класс TBalanceHistory
Данный класс, как понятно из его названия, предназначен для выполнения операций с историей баланса счета. Он также является базовым для некоторых классов, описанных ниже. Основное назначение класса - доступ к истории торговли эксперта. При этом возможна фильтрации истории по рабочему символу, по "магическому номеру", по дате начала мониторинга эксперта или по всем трем элементам одновременно.
//--------------------------------------------------------------------- // Операции с историей баланса: //--------------------------------------------------------------------- class TBalanceHistory { private: long current_magic; // значение "магического номера" при доступе к истории сделок ( 0 - любой номер ) long current_type; // тип сделок ( -1 - все ) int current_limit_history; // предельная глубина истории сделок ( 0 - вся история ) datetime monitoring_begin_date; // дата начала мониторинга истории сделок int real_trades; // сколько реально трэйдов уже сделано protected: TradeSymbol trade_symbol; // операции с рабочим инструментом protected: // "Сырые" массивы: double org_datetime_array[]; // дата/время трэйда double org_result_array[]; // результат трэйда // Массивы с сгруппированными по времени данными: double group_datetime_array[]; // дата/время трэйда double group_result_array[]; // результат трэйда double last_result_array[]; // массив для хранения результатов последних трэйдов ( точки по оси OY ) double last_datetime_array[]; // массив для хранения времен последних трэйдов ( точки по оси OX ) private: void SortMasterSlaveArray(double& _m[], double& _s[]); // синхронная сортировка двух массивов по возрастанию public: void SetTradeSymbol(string _symbol); // установить/изменить рабочий инструмент string GetTradeSymbol(); // получить рабочий инструмент void RefreshSymbolInfo(); // обновить рыночную информацию по рабочему инструменту void SetMonitoringBeginDate(datetime _dt); // задать дату начала мониторинга datetime GetMonitoringBeginDate(); // получить дату начала мониторинга void SetFiltrParams(long _magic, long _type = -1, int _limit = 0);// установить параметры фильтрации сделок public: // Получить результаты последних трэйдов: int GetTradeResultsArray(int _max_trades); public: void TBalanceHistory( ); // конструктор void ~TBalanceHistory( ); // деструктор };
Настройки фильтрации при считывании результатов последних сделок из истории задаются с помощью метода TBalanceHistory::SetFiltrParams. Он имеет следующие входные параметры:
- _magic - "магический номер" трейдов, которые
должны считываться из истории. Если задан нулевой номер, то считываются
трейды с любым "магическим номером".
- _type - тип сделок, которые нужно считать. Может принимать значения DEAL_TYPE_BUY (для считывания только длинных трейдов), DEAL_TYPE_SELL (для считывания только коротких трейдов) и значение -1 (для считывания и длинных и коротких трейдов).
- _limit - ограничивает глубину просмотра истории трейдов. Если равна нулю, то просматривается вся доступная история.
По умолчанию, после создания объекта класса TBalanceHistory, значения задаются такие: _magic = 0, _type = -1, _limit = 0.
Основной метод данного класса - TBalanceHistory::GetTradeResultsArray. Предназначен для заполнения массивов - членов класса last_result_array и last_datetime_array результатами последних трейдов. Метод имеет следующие входные параметры:
- _max_trades - максимальное число трейдов, которое нужно считать из истории и записать в выходные массивы. Поскольку, для вычисления угла наклона нужны, по крайней мере, две точки, это значение должно быть не меньше двух. Если это значение равно нулю, то считывается вся доступная история трейдов. На практике здесь задается количество точек, которое требуется для вычисления наклона кривой баланса.
//--------------------------------------------------------------------- // Считывает результаты последних по времени трэйдов в массивы: //--------------------------------------------------------------------- // - возвращает число реально считанных трэйдов, но не более заданного; //--------------------------------------------------------------------- int TBalanceHistory::GetTradeResultsArray(int _max_trades) { int index, limit, count; long deal_type, deal_magic, deal_entry; datetime deal_close_time, current_time; ulong deal_ticket; // тикет сделки double trade_result; string symbol, deal_symbol; real_trades = 0; // Число трэйдов должно быть не меньше двух: if(_max_trades < 2) { return(0); } // Если не задан рабочий инструмент, то дальше ничего не делаем: symbol = trade_symbol.GetTradeSymbol(); if(symbol == NULL) { return(0); } // Запросим историю сделок и ордеров c заданного времени по текущий момент: if(HistorySelect(monitoring_begin_date, TimeCurrent()) != true) { return(0); } // Считаем число трэйдов: count = HistoryDealsTotal(); // Если в истории трэйдов меньше, чем нужно, то на этом все: if(count < _max_trades) { return(0); } // Если в истории трэйдов больше, чем нужно, то ограничим: if(current_limit_history > 0 && count > current_limit_history) { limit = count - current_limit_history; } else { limit = 0; } // Если нужно, подстроим размерности "сырых" массивов на заданное количество трэйдов: if((ArraySize(org_datetime_array)) != (count - limit)) { ArrayResize(org_datetime_array, count - limit); ArrayResize(org_result_array, count - limit); } // Заполним "сырой" массив из базы истории трэйдов: real_trades = 0; for(index = count - 1; index >= limit; index--) { deal_ticket = HistoryDealGetTicket(index); // Если это не закрытие сделки, то дальше не идем: deal_entry = HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); if(deal_entry != DEAL_ENTRY_OUT) { continue; } // Проверим "магический номер" сделки, если требуется: deal_magic = HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); if(current_magic != 0 && deal_magic != current_magic) { continue; } // Проверим символ сделки: deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL ); if( symbol != deal_symbol ) { continue; } // Проверим тип сделки, если требуется: deal_type = HistoryDealGetInteger(deal_ticket, DEAL_TYPE); if(current_type != -1 && deal_type != current_type) { continue; } else if(current_type == -1 && (deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL)) { continue; } // Проверим время закрытия сделки: deal_close_time = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); if(deal_close_time < monitoring_begin_date) { continue; } // Итак, можно считывать очередной трэйд: org_datetime_array[real_trades] = deal_close_time / 60; org_result_array[real_trades] = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT)/HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); real_trades++; } // Если трэйдов меньше, чем нужно, то на этом все: if(real_trades < _max_trades) { return(0); } count = real_trades; // Отсортируем "сырой" массив по дате/времени закрытия ордера: SortMasterSlaveArray(org_datetime_array, org_result_array); // Если нужно, настроим размерности групповых массивов на заданное количество точек: if((ArraySize(group_datetime_array )) != count) { ArrayResize(group_datetime_array, count); ArrayResize(group_result_array, count); } ArrayInitialize(group_datetime_array, 0.0); ArrayInitialize(group_result_array, 0.0); // Заполним выходной массив сгруппированными данными ( группируем по одинаковости даты/времени закрытия позиции ): for(index = 0; index < count; index++) { // Получим очередной трэйд: deal_close_time = (datetime)org_datetime_array[index]; trade_result = org_result_array[index]; // Теперь проверим, нет ли уже такого времени в выходном массиве: current_time = (datetime)group_datetime_array[real_trades]; if(current_time > 0 && MathAbs(current_time - deal_close_time) > 0.0) { real_trades++; // переместим указатель на следующий элемент group_result_array[real_trades] = trade_result; group_datetime_array[real_trades] = deal_close_time; } else { group_result_array[real_trades] += trade_result; group_datetime_array[real_trades] = deal_close_time; } } real_trades++; // теперь это число неповторяющихся элементов // Если трэйдов меньше, чем нужно, то на этом все: if(real_trades < _max_trades) { return(0); } if(ArraySize(last_result_array ) != _max_trades) { ArrayResize(last_result_array, _max_trades); ArrayResize(last_datetime_array, _max_trades); } // Запишем набранные данные в выходные массивы с переворотом индексации: for(index = 0; index < _max_trades; index++) { last_result_array[_max_trades - 1 - index] = group_result_array[index]; last_datetime_array[_max_trades - 1 - index] = group_datetime_array[index]; } // Заменим в выходном массиве результаты отдельных трэйдов на нарастающий итог: for(index = 1; index < _max_trades; index++) { last_result_array[index] += last_result_array[index - 1]; } return( _max_trades ); }
Вначале выполняются обязательные проверки - задан ли рабочий инструмент и корректность входных параметров.
Затем считывается история сделок и ордеров с заданного времени по текущее. Это делает следующий фрагмент кода:
// Запросим историю сделок и ордеров c заданного времени по текущий момент: if(HistorySelect(monitoring_begin_date, TimeCurrent()) != true) { return(0); } // Считаем число трэйдов: count = HistoryDealsTotal(); // Если в истории трэйдов меньше, чем нужно, то на этом все: if(count < _max_trades) { return(0); }
При этом проверяется общее число трейдов в истории. Если оно меньше заданного, то дальнейшие операции не имеют смысла. После подготовки "сырых" массивов идет цикл заполнения их данными из истории трейдов. Это делается так:
// Заполним "сырой" массив из базы истории трэйдов: real_trades = 0; for(index = count - 1; index >= limit; index--) { deal_ticket = HistoryDealGetTicket(index); // Если это не закрытие сделки, то дальше не идем: deal_entry = HistoryDealGetInteger(deal_ticket, DEAL_ENTRY ); if(deal_entry != DEAL_ENTRY_OUT) { continue; } // Проверим "магический номер" сделки, если требуется: deal_magic = HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); if(_magic != 0 && deal_magic != _magic) { continue; } // Проверим символ сделки: deal_symbol = HistoryDealGetString(deal_ticket, DEAL_SYMBOL); if(symbol != deal_symbol) { continue; } // Проверим тип сделки, если требуется: deal_type = HistoryDealGetInteger(deal_ticket, DEAL_TYPE); if(_type != -1 && deal_type != _type) { continue; } else if(_type == -1 && (deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL)) { continue; } // Проверим время закрытия сделки: deal_close_time = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); if(deal_close_time < monitoring_begin_date) { continue; } // Итак, можно считывать очередной трэйд: org_datetime_array[ real_trades ] = deal_close_time / 60; org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME ); real_trades++; } // Если трэйдов меньше, чем нужно, то на этом все: if( real_trades < _max_trades ) { return( 0 ); }
Вначале считывается тикет сделки в истории с помощью функции HistoryDealGetTicket и дальнейшие считывания информации по сделки используют полученный тикет. Поскольку нас интересуют только закрытые сделки (мы будем анализировать баланс), то сначала проверяется тип сделки. Это делается вызовом функции HistoryDealGetInteger с параметром DEAL_ENTRY. Если функция возвращает значение DEAL_ENTRY_OUT, то это закрытие сделки.
После этого проверяется "магический номер" сделки, тип сделки (если задан входной параметр метода) и символ сделки. Если все параметры сделки соответствуют требуемым, то проверяется последний параметр - время закрытия сделки. Это делается так:
// Проверим время закрытия сделки: deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME ); if( deal_close_time < monitoring_begin_date ) { continue; }
Считанные дата/время сделки сравнивается с заданными датой/временем начала мониторинга истории. Если дата/время сделки больше заданной, то переходим к считыванию нашего трейда в массив - считываем результат сделки в пунктах и время сделки в минутах (в данном случае, время закрытия). После чего наращивается счетчик считанных сделок real_trades и цикл продолжается дальше.
После того как "сырые" массивы заполнены необходимым числом данных, надо отсортировать массив, в котором хранятся времена закрытия сделок. При этом должно сохраниться соответствие времени закрытия в массиве org_datetime_array и результата сделки в массиве org_result_array. Это делается специально написанным методом:
TBalanceHistory::SortMasterSlaveArray( double& _master[ ], double& _slave[ ] ). Первый параметр _master - массив, который собственно сортируется по возрастанию. Второй параметр _slave - массив, элементы которого должны перемещаться синхронно с элементами первого. Сортировка проводится методом "пузырька".
После всех, описанных выше, операций у нас есть в распоряжении два массива с временем закрытия сделки и результатом сделки, отсортированных по возрастанию времени. Поскольку каждому времени (фактически, точке на оси OX) может соответствовать только одна точка на кривой баланса (точка на оси OY), то нам надо сгруппировать элементы массива с одинаковым временем закрытия, если такие есть. Это делает следующий участок кода:
// Заполним выходной массив сгруппированными данными ( группируем по одинаковости даты/времени закрытия позиции ): real_trades = 0; for(index = 0; index < count; index++) { // Получим очередной трэйд: deal_close_time = (datetime)org_datetime_array[index]; trade_result = org_result_array[index]; // Теперь проверим, нет ли уже такого времени в выходном массиве: current_time = (datetime)group_datetime_array[real_trades]; if(current_time > 0 && MathAbs(current_time - deal_close_time) > 0.0) { real_trades++; // переместим указатель на следующий элемент group_result_array[real_trades] = trade_result; group_datetime_array[real_trades] = deal_close_time; } else { group_result_array[real_trades] += trade_result; group_datetime_array[real_trades] = deal_close_time; } } real_trades++; // теперь это число неповторяющихся элементов
Фактически, здесь суммируются все трейды с "одинаковым" временем закрытия. Результаты записываются в массивы TBalanceHistory::group_datetime_array (времена закрытия) и TBalanceHistory::group_result_array (результаты трейдов). После этого, мы получаем два отсортированных массива с не повторяющимися элементами. Одинаковость времени, в данном случае, берется в пределах минуты. Это преобразование можно проиллюстрировать графически:
Рисунок 3. Группирование сделок с одинаковым временем
Все сделки в пределах минуты (левая часть рисунка) группируются в одну с округлением времени и суммированием результатов (правая часть рисунка). Это позволяет сгладить "дребезг" по времени закрытия сделок и улучшить стабильность регулирования.
После этого нужно сделать еще два преобразования полученных массивов. Перевернуть порядок элементов, чтобы нулевому элементу соответствовала самая ранняя сделка и заменить результаты отдельных трейдов на нарастающий итог, то есть на баланс. Это делается в следующем фрагменте кода:
// Запишем набранные данные в выходные массивы с переворотом индексации: for(index = 0; index < _max_trades; index++) { last_result_array[_max_trades - 1 - index] = group_result_array[index]; last_datetime_array[_max_trades - 1 - index] = group_datetime_array[index]; } // Заменим в выходном массиве результаты отдельных трэйдов на нарастающий итог: for(index = 1; index < _max_trades; index++ ) { last_result_array[index] += last_result_array[index - 1]; }
Класс TBalanceSlope
Данный класс предназначен для проведения операций с кривой баланса счета. Он порожден от класса TBalanceHistory и наследует все его защищенные и открытые данные и методы. Рассмотрим его структуру подробней:
//--------------------------------------------------------------------- // Операции с кривой баланса: //--------------------------------------------------------------------- class TBalanceSlope : public TBalanceHistory { private: double current_slope; // текущий угол наклона кривой баланса int slope_count_points; // число точек ( трэйдов ) для расчета угла наклона private: double LR_koeff_A, LR_koeff_B; // коэффициенты для уравнения прямой линейной регрессии double LR_points_array[]; // массив точек прямой линейной регрессии private: void CalcLR( double& X[ ], double& Y[ ] ); // вычислить уравнение прямой линейной регрессии public: void SetSlopePoints(int _number); // задать число точек для вычисления угла наклона double CalcSlope(); // вычислить угол наклона public: void TBalanceSlope(); // конструктор void ~TBalanceSlope(); // деструктор };
Мы будем определять угол наклона кривой баланса по углу наклона линии линейной регрессии, построенной для заданного числа последних точек (трейдов) на кривой баланса счета. Таким образом, сначала нужно вычислить уравнение линии регрессии вида A*x + B. Это делает следующий метод:
//--------------------------------------------------------------------- // Вычислить уравнение прямой линейной регрессии: //--------------------------------------------------------------------- // входные параметры: // X[ ] - массив значений числового ряда по оси ОX; // Y[ ] - массив значений числового ряда по оси ОY; //--------------------------------------------------------------------- void TBalanceSlope::CalcLR(double& X[], double& Y[]) { double mo_X = 0, mo_Y = 0, var_0 = 0, var_1 = 0; int i; int size = ArraySize(X); double nmb = (double)size; // Если число точек меньше двух, то прямую вычислить невозможно: if(size < 2) { return; } for(i = 0; i < size; i++) { mo_X += X[i]; mo_Y += Y[i]; } mo_X /= nmb; mo_Y /= nmb; for( i = 0; i < size; i++) { var_0 += (X[i] - mo_X) * (Y[i] - mo_Y); var_1 += (X[i] - mo_X) * (X[i] - mo_X); } // Значение коэффициента A: if( var_1 != 0.0 ) { LR_koeff_A = var_0/var_1; } else { LR_koeff_A = 0.0; } // Значение коэффициента B: LR_koeff_B = mo_Y - LR_koeff_A * mo_X; // Заполним массив точек, лежащих на прямой регрессии: ArrayResize(LR_points_array, size); for(i = 0; i < size; i++) { LR_points_array[i] = LR_koeff_A * X[i] + LR_koeff_B; } }
Здесь используется метод наименьших квадратов для вычисления минимальной погрешности расположения линии регрессии относительно исходных данных. Также, заполняется массив, хранящий значения координат Y, лежащих на вычисленной прямой. Данный массив пока не используется и рассчитан на дальнейшее развитие
Основной метод, который используется в данном классе - TBalanceSlope::CalcSlope. Он возвращает угол наклона кривой баланса, вычисленный по заданному числу последних трейдов. Вот его реализация:
//--------------------------------------------------------------------- // Вычислить угол наклона: //--------------------------------------------------------------------- double TBalanceSlope::CalcSlope() { // Получим результаты торговли из базы истории трейдов: int nmb = GetTradeResultsArray(slope_count_points); if(nmb < slope_count_points) { return( 0.0 ); } // Вычислим линию регрессии по результатам последних трейдов: CalcLR(last_datetime_array, last_result_array); current_slope = LR_koeff_A; return(current_slope); }
Сначала считываются заданное число последних точек кривой баланса. Это делается вызовом метода базового класса TBalanceSlope::GetTradeResultsArray. Если считано точек не меньше заданного числа, то вычисляется прямая регрессии. Это делается вызовом метода TBalanceSlope::CalcLR. В качестве аргументов используются заполненные на предыдущем шаге массивы last_result_array и last_datetime_array, принадлежащие базовому классу.
Остальные методы достаточно просты и не требуют пояснений.
Класс TBalanceSlopeControl
Это основной класс, который, собственно, и управляет наклоном кривой баланса посредством изменения рабочего лота. Он порожден от класса TBalanceSlope и наследует все его открытые и защищенные методы и данные. Единственная задача данного класса: вычисление текущего рабочего лота в зависимости от текущего угла наклона кривой баланса. Рассмотрим его подробней:
//--------------------------------------------------------------------- // Управление наклоном кривой баланса: //--------------------------------------------------------------------- enum LotsState { LOTS_NORMAL = 1, // режим торговли нормальным лотом LOTS_REJECTED = -1, // режим торговли пониженным лотом LOTS_INTERMEDIATE = 0, // режим торговли промежуточным лотом }; //--------------------------------------------------------------------- class TBalanceSlopeControl : public TBalanceSlope { private: double min_slope; // угол наклона, соответствующий режиму режекции лота double max_slope; // угол наклона, соответствующий режиму нормального лота double centr_slope; // угол наклона, соответствующий переключению лота без гистерезиса private: ControlType control_type; // тип регулировочной функции private: double rejected_lots; // размер лота в режиме режекции double normal_lots; // размер лота в нормальном режиме double intermed_lots; // размер лота в промежуточном режиме private: LotsState current_lots_state; // текущий режим лота public: void SetControlType( ControlType _control ); // задать тип регулировочной характеристики void SetControlParams( double _min_slope, double _max_slope, double _centr_slope ); public: double CalcTradeLots( double _min_lots, double _max_lots ); // получить торговый объем protected: double CalcIntermediateLots( double _min_lots, double _max_lots, double _slope ); public: void TBalanceSlopeControl( ); // конструктор void ~TBalanceSlopeControl( ); // деструктор };
Прежде чем вычислять размер текущего лота, необходимо задать начальные параметры. Это делается вызовом следующих методов:
void SetControlType( ControlType _control ); // задать тип регулировочной характеристики
Входной параметр _control - это тип регулировочной характеристики. Может принимать значения:
- STEP_WITH_HYSTERESISH - регулировочная характеристика ступенчатая с гистерезисом;
- STEP_WITHOUT_HYSTERESIS - регулировочная характеристика ступенчатая без гистерезиса;
- LINEAR - регулировочная характеристика линейная;
- NON_LINEAR - регулировочная характеристика не линейная (в данной версии не реализовано);
void SetControlParams( double _min_slope, double _max_slope, double _centr_slope );
Входные параметры следующие:
- _min_slope - угол наклона кривой баланса, соответствующий режиму торговли минимальным лотом;
- _max_slope - угол наклона кривой баланса, соответствующий режиму торговли максимальным лотом;
- _centr_slope - угол наклона кривой баланса, использующийся при задании ступенчатой регулировочной характеристики без гистерезиса;
Размер лота вычисляется вызовом следующего метода:
//--------------------------------------------------------------------- // Получить торговый объем: //--------------------------------------------------------------------- double TBalanceSlopeControl::CalcTradeLots(double _min_lots, double _max_lots) { // Попробуем вычислить наклон кривой баланса: double current_slope = CalcSlope( ); // Если еще не набрано заданное число трэйдов, то торгуем минимальным лотом: if(GetRealTrades() < GetSlopePoints()) { current_lots_state = LOTS_REJECTED; rejected_lots = trade_symbol.NormalizeLots(_min_lots); return(rejected_lots); } // Если функция регулирования ступенчатая без гистерезиса: if(control_type == STEP_WITHOUT_HYSTERESIS) { if(current_slope < centr_slope) { current_lots_state = LOTS_REJECTED; rejected_lots = trade_symbol.NormalizeLots(_min_lots); return(rejected_lots); } else { current_lots_state = LOTS_NORMAL; normal_lots = trade_symbol.NormalizeLots(_max_lots); return(normal_lots); } } // Если наклон ЛР для кривой баланса меньше заданного допустимого: if(current_slope < min_slope) { current_lots_state = LOTS_REJECTED; rejected_lots = trade_symbol.NormalizeLots(_min_lots); return(rejected_lots); } // Если наклон ЛР для кривой баланса больше заданного: if(current_slope > max_slope) { current_lots_state = LOTS_NORMAL; normal_lots = trade_symbol.NormalizeLots(_max_lots); return(normal_lots); } // Наклон ЛР для кривой баланса находится внутри заданных границ ( промежуточное состояние ): current_lots_state = LOTS_INTERMEDIATE; // Вычислим значение промежуточного лота: intermed_lots = CalcIntermediateLots(_min_lots, _max_lots, current_slope); intermed_lots = trade_symbol.NormalizeLots(intermed_lots); return(intermed_lots); }
Основные существенные моменты реализации метода TBalanceSlopeControl::CalcTradeLots следующие:
- Пока не набрано заданное минимальное число трейдов, торговый лот будет минимальным. Это логично, поскольку после постановки эксперта на торговлю, заранее не известно в каком периоде находится торговая система (профитном или убыточном).
- Если задана регулировочная функция - ступенчатая без гистерезиса, то для задания угла переключения режима торговли методом TBalanceSlopeControl::SetControlParams используется только параметр _centr_slope. Параметры _min_slope и _max_slope игнорируются. Это сделано, чтобы можно было корректно проводить оптимизацию в тестере MT5 по этому параметру.
В зависимости от вычисленного угла наклона, торговля идет либо минимальным лотом, либо максимальным, либо - промежуточным. Промежуточный лот вычисляется простым методом TBalanceSlopeControl::CalcIntermediateLots. Данный метод является защищенным и используется внутри класса. Его код приведен ниже:
//--------------------------------------------------------------------- // Вычисление промежуточного лота: //--------------------------------------------------------------------- double TBalanceSlopeControl::CalcIntermediateLots(double _min_lots, double _max_lots, double _slope) { double lots; // Если функция регулирования ступенчатая с гистерезисом: if(control_type == STEP_WITH_HYSTERESISH) { if(current_lots_state == LOTS_REJECTED && _slope > min_slope && _slope < max_slope) { lots = _min_lots; } else if(current_lots_state == LOTS_NORMAL && _slope > min_slope && _slope < max_slope) { lots = _max_lots; } } // Если функция регулирования линейная: else if(control_type == LINEAR) { double a = (_max_lots - _min_lots)/(max_slope - min_slope); double b = normal_lots - a * .max_slope; lots = a * _slope + b; } // Если функция регулирования не линейная ( пока не реализованная ): else if(control_type == NON_LINEAR) { lots = _min_lots; } // Если функция регулирования не известная: else { lots = _min_lots; } return(lots); }
Остальные методы данного класса пояснений не требуют.
Пример встраивания системы в эксперт
Рассмотрим пошагово процесс внедрения системы контроля наклона кривой баланса в эксперт.
Шаг 1 - добавление в советник директивы для подключения разработанной библиотеки:
#include <BalanceSlopeControl.mqh>
Шаг 2 - добавление в советник внешних переменных для задания параметров системы контроля наклона кривой баланса:
//--------------------------------------------------------------------- // Параметры системы контроля наклона кривой баланса; //--------------------------------------------------------------------- enum SetLogic { No = 0, Yes = 1, }; //--------------------------------------------------------------------- input SetLogic UseAutoBalanceControl = No; //--------------------------------------------------------------------- input ControlType BalanceControlType = STEP_WITHOUT_HYSTERESIS; //--------------------------------------------------------------------- // Число последних трэйдов для вычисления ЛР кривой баланса: input int TradesNumberToCalcLR = 3; //--------------------------------------------------------------------- // Наклон ЛР для снижения торгового лота до минимума: input double LRKoeffForRejectLots = -0.030; //--------------------------------------------------------------------- // Наклон ЛР для восстановления нормального режима торговли: input double LRKoeffForRestoreLots = 0.050; //--------------------------------------------------------------------- // Наклон ЛР для работы в промежуточном режиме: input double LRKoeffForIntermedLots = -0.020; //--------------------------------------------------------------------- // Снижать входной лот до заданной величины при наклоне ЛР вниз input double RejectedLots = 0.10; //--------------------------------------------------------------------- // Нормальный рабочий лот в режиме ММ с фиксированным лотом: input double NormalLots = 1.0;
Шаг 3 - добавление в советник объекта типа TBalanceSlopeControl:
TBalanceSlopeControl BalanceControl;
Данное объявление можно добавить в начало советника, перед определениями функций.
Шаг 4 - добавление в функцию советника OnInit кода для инициализации системы контроля наклона кривой баланса:
// Настроим систему контроля наклона кривой баланса: BalanceControl.SetTradeSymbol(Symbol()); BalanceControl.SetControlType(BalanceControlType); BalanceControl.SetControlParams(LRKoeffForRejectLots, LRKoeffForRestoreLots, LRKoeffForIntermedLots); BalanceControl.SetSlopePoints(TradesNumberToCalcLR); BalanceControl.SetFiltrParams(0, -1, 0); BalanceControl.SetMonitoringBeginDate(0);
Шаг 5 - добавление в функцию OnTick советника вызова метода для обновления текущей информации по рынку:
// Обновим рыночную информацию:
BalanceControl.RefreshSymbolInfo();
Вызов данного метода можно добавить в самое начало функции OnTick или после проверки появления нового бара для советников с такой проверкой.
Шаг 6 - добавление в советник, в место перед открытием позиции, кода для вычисления текущего лота:
if(UseAutoBalanceControl == Yes) { current_lots = BalanceControl.CalcTradeLots(RejectedLots, NormalLots); } else { current_lots = NormalLots; }
Если в советнике используется автоматический мани менеджмент, то вместо NormalLots надо передать в метод TBalanceSlopeControl::CalcTradeLots текущий размер лота, вычисленный системой ММ советника.
Тестовый советник BSCS-TestExpert.mq5, в который встроена описанная система, приложен к данной статье. Принцип его работы основан на пересечении уровней индикатора CCI. Данный эксперт разработан в чисто тестовых целях и не пригоден к работе на реальных счетах. Тестировать будем на периоде H4 (2008.07.01 - 2010.09.01) на инcтрументе EURUSD.
Рассмотрим результаты работы данного эксперта. Ниже приведен график изменения баланса при отключенной системе контроля наклона. Для этого установим внешний параметр UseAutoBalanceControl в значение No.
Рисунок 4. Исходный график изменения баланса
Теперь установим внешний параметр UseAutoBalanceControl в значение Yes и протестируем эксперт. Получим график при включенной системе контроля наклона баланса.
Рисунок 5. График изменения баланса при включенной системе контроля
Можно заметить визуально, что большинство периодов просадки на верхнем графике (рис.4) как бы срезаны и имеют почти плоский вид на нижнем графике (рис.5). Это результат действия нашей системы. Можно сравнить основные показатели работы эксперта:
Параметр | UseAutoBalanceControl = No | UseAutoBalanceControl = Yes |
---|---|---|
Чистая прибыль: | 18 378.00 | 17 261.73 |
Прибыльность: | 1.47 | 1.81 |
Фактор восстановления: | 2.66 | 3.74 |
Матожидание выигрыша: | 117.81 | 110.65 |
Абсолютная просадка по балансу: | 1 310.50 | 131.05 |
Абсолютная просадка по средствам: | 1 390.50 | 514.85 |
Максимальная просадка по балансу: | 5 569.50 (5.04%) | 3 762.15 (3.35%) |
Максимальная просадка по средствам: | 6 899.50 (6.19%) | 4 609.60 (4.08%) |
Зеленым цветом выделены лучшие показатели из сравниваемых. Несколько уменьшилась прибыль и матожидание выигрыша, это обратная сторона медали регулирования - из-за задержки переключения состояний рабочего лота. В целом же, есть реальное улучшение показателей работы эксперта. Особенно, по просадке и прибыльности.
Заключение
Просматриваются некоторые пути улучшения данной системы:- Использование виртуального ведения сделок при входе советника в неблагоприятный период работы. Тогда размер нормального рабочего лота уже не будет иметь значения. Это позволит еще уменьшить просадку.
- Использование более сложных алгоритмов для определения текущего состояния работы эксперта (профитное или убыточное). Например, можно попробовать применить нейросеть для такого анализа. Здесь, конечно, нужны дополнительные исследования.
Итак, мы рассмотрели принцип и результат работы одной из систем, помогающих улучшить качественные характеристики торгового эксперта. Совместная работа с системой управления деньгами позволяет в некоторых случаях повышать прибыльность, не увеличивая риски.
Напомню еще раз: никакая вспомогательная система не сможет сделать из убыточного советника прибыльного.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
УРА!!! Я победил проблему! Проблема заключалась в некорректной передаче массива как элемента объекта по ссылке в функцию сортировки.
Если сначала скопировать массив объекта в обычный массив и потом передавать этот временный массив в функцию сортировки, то никаких неожиданных глюков не происходит. Данные в массивах в небеса не улетают и все переключения лота совпадают. Также практически полностью совпадает итоговый баланс на совместном прогоне валютных пар (отличия микроскопичны).
Ниже приведены результаты тестов, а также код, который я использую для ликвидации глюка. (Обратите внимание, что текущие данные отличаются даже при одиночных прогонах на первой паре от данных, которые были получены до исправления кода -
solandr 2012.09.26 23:16 2012.09.26 23:16:04 # )
С включенным контролем лота
34 0 0 6487.33 первая пара
0 36 0 5556,60 вторая пара
0 0 168 4374.44 третья пара
34 36 168 16418.47 все три пары
сумма баланса по трём парам 16418,37 (Разница с совместным прогоном 0.1)
С выключенным контролем лота
0 0 0 6702.44 первая пара
0 0 0 5742.89 вторая пара
0 0 0 4358.22 третья пара
0 0 0 16804.53 все три пары
сумма баланса по трём парам 16803.55 (Разница с совместным прогоном 0.98)
Поменяйте, пожалуйста, инклюдник в статье, чтобы другие люди по нескольку дней не ломали себе голову расходящимися тестами. Спасибо!
ИМХО, здесь надо не инклудник менять, а в сервис-деск писать.
Так не должно быть. А лишнее копирование ни к чему, со всех точек зрения. А вообще, Вы молодец!
Плюсаните себе рейтинга, через сервис-деск))
Сообщение для команды MQ:
Уважаемые разработчики MT5, хотелось бы обратить Ваше внимание на некоторую неожиданную проблему, обнаруженную при тестировании на MT5 Build 695 (6 Sep 2012, Терминал Чемпионата-2012, Счёт: 1101505, Сервер: MetaQuotes-Demo), работающем под операционной системой Windows 7 Enterprise (лицензионной, английской). Проблема заключается в необъяснимом искажении данных (массива как элемента объекта), передаваемых по ссылке в функцию сортировки.
В приложении находятся исходники иклюдников ORIGINAL (с ошибкой) и CORRECTED (без ошибки), а также логфайлы работы эксперта, демонстрирующие работу обоих вариантов кода. Ошибка с искажением данных стабильно воспроизводится при одних и тех же заданных условиях тестирования. Обратите, пожалуйста, внимание на логи за 2012.02.24 08:03:40 (данные массивов перепутаны местами) и за 2012.05.31 14:41:59 (даные "улетели в небеса").
Спасибо!
Сообщение для команды MQ:
Уважаемые разработчики MT5, хотелось бы обратить Ваше внимание на некоторую неожиданную проблему, обнаруженную при тестировании на MT5 Build 695 (6 Sep 2012, Терминал Чемпионата-2012, Счёт: 1101505, Сервер: MetaQuotes-Demo), работающем под операционной системой Windows 7 Enterprise (лицензионной, английской). Проблема заключается в необъяснимом искажении данных (массива как элемента объекта), передаваемых по ссылке в функцию сортировки.
В приложении находятся исходники иклюдников ORIGINAL (с ошибкой) и CORRECTED (без ошибки), а также логфайлы работы эксперта, демонстрирующие работу обоих вариантов кода. Ошибка с искажением данных стабильно воспроизводится при одних и тех же заданных условиях тестирования. Обратите, пожалуйста, внимание на логи за 2012.02.24 08:03:40 (данные массивов перепутаны местами) и за 2012.05.31 14:41:59 (даные "улетели в небеса").
Спасибо!
Ошибка на стороне пользователя в функции GetTradeResultsArray.
Подготавливается динамический массив на X данных, а заполняется на N (при этом N<X), например при наличии сделки с "чужим" magic.
Перед сортировкой выводятся именно N данных, а в сортировке участвует X, естественно что данные X-N это случайные числа в памяти.
В зависимости от значения, при сортировке они "поднимаются" наверx и выводятся после сортировки в лог.
Решение:
1) "Подрезать" массив после заполнения до N
2) Передавать в функцию сортировки N
3) Инициализировать массив X заведомо большими/малыми данными, которые после сортировки останутся "за бортом".
Заключение.