Разработка торгового советника с нуля (Часть 8): Концептуальный скачок (I)
Введение
Иногда при разработке каких-то проектов мы вдруг видим новые возможности, которые могут оказаться кстати и привнести большое улучшение в создаваемую нами систему. Но возникает вопрос: как максимально просто реализовать новый функционал?
Большая проблема в том, что иногда мы вынуждены забыть всё, что уже было разработано, и начинать с нуля. Это напрочь ДЕМОТИВИРУЕТ. Со временем работа, после более чем 20 лет программирования на C++, я разработал определенную линюю мышления. Да, мы разрабатывает некие концепции, которые помогают планировать вещи и вносить изменения с минимальными усилиями, но тем не менее иногда вещи могут стать намного сложнее, чем мы первоначально предполагали.
До сих пор советник строился таким образом, чтобы он мог получать новый код без потери текущего функционала: классы просто создавались и добавлялись. А теперь нам нужно сделать шаг назад, а затем два шага вперед. А шаг назад позволит нам внедрить в советник новый функционал. Этот функционал представляет собой некий класс окна с какой-то информацией, основанной на шаблонах; это то, что будет здесь первой частью. Мы кардинально изменим код, но при этом сохраним весь функционал, который есть на данный момент, а во второй части займемся IDE.
Планирование
Наш советник в настоящее время структурирован в классе объектов, и это видно на схеме ниже.
При всем при этом имеем текущую работу и отличную стабильность системы. Но нам придется переструктурировать советник как показано ниже. Можно заметить, что был добавлен класс и изменены позиции классов C_TemplateChart и C_SubWindow.
Зачем же нужна такая перестройка? Проблема в том, что способ добавления плавающих окон, содержащих данные активов, был не очень подходящим, и поэтому возникла необходимость пройти через такие изменения. При этом данное изменение было не только эстетическим с точки зрения структуры, но и потребовало радикального изменения кода, поэтому он сильно отличается от тех, которые вы видели раньше.
Итак, приступим к работе — перейдем к написанию кода.
Реализация на практике
1. Изменения во внутреннем коде советника
Первое большое изменение начинается в файле инициализации советника, давайте посмотрим на фрагмент ниже:
input group "Window Indicators" input string user01 = ""; //Индикаторы подокон input group "WallPaper" input string user10 = "Wallpaper_01"; //Используемый BitMap input char user11 = 60; //Прозрачность (от 0 до 100) input C_WallPaper::eTypeImage user12 = C_WallPaper::IMAGEM; //Вид фоного изображении input group "Chart Trader" input int user20 = 1; //Фактор плеча input int user21 = 100; //Take Profit ( ФИНАНСОВЫЙ ) input int user22 = 75; //Stop Loss ( ФИНАНСОВЫЙ ) input color user23 = clrBlue; //Цвет линии цены input color user24 = clrForestGreen; //Цвет линии Take Profit input color user25 = clrFireBrick; //Цвет линии Stop input bool user26 = true; //Day Trade ? input group "Volume At Price" input color user30 = clrBlack; //Цвет бара input char user31 = 20; //Прозрачность (от 0 до 100 ) //+------------------------------------------------------------------+ C_TemplateChart Chart; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { static string memSzUser01 = ""; Terminal.Init(); WallPaper.Init(user10, user12, user11); if (memSzUser01 != user01) { Chart.ClearTemplateChart(); Chart.AddThese(memSzUser01 = user01); } Chart.InitilizeChartTrade(user20, user21, user22, user23, user24, user25, user26); VolumeAtPrice.Init(user24, user25, user30, user31); OnTrade(); EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+
Обратите внимание, что теперь у нас есть только одна переменная, указывающая, какие шаблоны будут загружены. Остальная часть кода выглядит так же, как мы видели, за исключением выделенной части. Возможно будет не совсем понятно, что оно здесь делает, или почему данный выделенный код был помещен здесь при инициализации советника. Когда мы загружаем советник на график, некоторые вещи создаются, и при обычном использовании они могут быть изменены. Ранее не было смысла добавлять выделенный код, потому что всё было ориентировано на совместную работу, где изменения не влияли на поведение или внешний вид советника. Но когда мы добавляем плавающие индикаторы, начинает происходить РАЗДРАЖАЮЩАЯ вещь: каждый раз, когда мы меняем ТАЙМФРЕЙМ, советник ПЕРЕЗАПУСКАЕТСЯ, а окна возвращаются в исходное состояние. Если их не уничтожать, то на графике начинает накапливаться бесполезный хлам, а если их удалять так, как надо, то они перестраиваются затем в прежние места, что жутко раздражает. Если пользователь не изменит нужные шаблоны, выделенный код предотвратит неправильное уничтожение плавающих окон, а если в шаблонах есть изменения, советник просто перезапустится. Все очень просто и чрезвычайно эффективное.
Следующее, на что следует обратить внимание, касается внутренней системы обмена сообщениями. Ранее там была дополнительная переменная, но мы ее убрали, поэтому код стал таким:
void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, NanoEA.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); }
Теперь система более эффективно использует саму систему обмена сообщениями, существующую в MQL5, и обмен сообщениями очень похож на саму функцию OnChartEvent. Это позволяет без какой-либо нагрузки передавать параметры классам объектов, чтобы каждый класс мог обрабатывать сообщения о событиях, сгенерированные системой metaTrader 5, наиболее подходящим образом для самого класса. Таким образом мы дополнительно изолируем каждый класс объектов, и, таким образом, советник может принимать более разнообразные формы для каждого типа пользователя, и делая это каждый раз с меньшими усилиями.
2. Изменения кода поддержки подокна
До этого момента код подокна был очень простым, но у него была проблема: каждый раз по тем или иным причинам советник не мог удалить созданное подокно. При повторном открытии советника создавалось новое подокно, что приводило к потере контроля в самой системе. Но, на удивление, исправить это было очень просто. Сначала обратите внимание на фрагмент файла поддержки, который показан ниже:
int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport"); return INIT_SUCCEEDED; }
Выделенная строка создаст псевдоним для файла поддержки, и этот псевдоним будет видеть советник и тем самым будет проверять, загружена ли система подокна или нет. При этом имя файла не имеет значения — для советника важен псевдоним. Этот же тип кода будет использоваться позже для поддержки других вещей в советнике, мы не будем вдаваться в подробности здесь, но в следующей статье обязательно посмотрим, как воспользоваться преимуществами выделенного кода.
После того, как это сделано, можно рассмотреть код для загрузки и создания подокон. Он показан во фрагменте ниже:
void Init(void) { int i0; if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -1) ChartIndicatorAdd(Terminal.Get_ID(), i0 = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, 0, "::" + def_Resource)); m_IdSubWinEA = i0; }
Как видите, это намного проще, но этот код не является общедоступным, доступ к нему осуществляется через другой код, который является общедоступным, и его можно увидеть ниже:
inline int GetIdSubWinEA(void) { if (m_IdSubWinEA < 0) Init(); return m_IdSubWinEA; }
Но почему мы сделали именно так? Дело в том, что если вы посмотрите на инициализацию советника, то увидите, что может случиться так, что советник не использует никаких индикаторов в подокне, а когда система это осознает, то удаляет подокно из графика и создаст его только в случае необходимости. Но это решение принимает не код советника, а класс C_TemplateChart.
Посмотрите анимацию ниже:
Обратите внимание, что теперь у нас есть вертикальная линия, которая указывает, где мы анализируем. Эти линии независимы друг от друга, но раньше их не было, что затрудняло анализ некоторых точек индикатора в зависимости от графика. Это одно из улучшений, включенных в класс C_TemplateChart. Давайте посмотрим на код внутри класса, чтобы понять еще больше, так как изменения были большими.
Давайте посмотрим на следующий фрагмент, где объявлены переменные:
class C_TemplateChart : public C_SubWindow { #define def_MaxTemplates 8 #define def_NameTemplateRAD "IDE" //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eParameter {TEMPLATE = 0, PERIOD, SCALE, WIDTH, HEIGHT}; //+------------------------------------------------------------------+ struct st { string szObjName, szSymbol, szTemplate, szVLine; int width, scale; ENUM_TIMEFRAMES timeframe; long handle; }m_Info[def_MaxTemplates]; int m_Counter, m_CPre, m_Aggregate; struct st00 { int counter; string Param[HEIGHT + 1]; }m_Params;
Первое, на что следует обратить внимание, это то, что класс C_TemplateChart будет расширять класс C_SubWindow. Этот кусок кода не кажется таким уж особенным, но обратите внимание на выделенную часть: она указывает на внутреннюю систему анализа данных, чтобы можно было создавать и представлять запрошенные пользователем индикаторы соответствующим образом. То есть теперь система для описания того, как пользователь будет указывать вещи, была стандартизирована, и хотя сначала это кажется запутанным, со временем она станет понятной. Чтобы объяснить новый формат, нам нужно проанализировать следующий фрагмент, отвечающий за анализ запроса пользователя:
int GetCommand(int iArg, const string szArg) { for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = ""; m_Params.counter = 0; for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg]) { case ')': case ';': m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++); return iArg + 1; case ' ': c2 += (c1 == iArg ? 0 : 1); c1 = (c1 == iArg ? iArg + 1 : c1); break; case '(': case ',': c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2); m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); c2 = 0; c1 = iArg + 1; break; default: c2++; break; } return -1; }
Первым делом очищаем все данные, которые ранее были в структуре. Затем начинаем анализировать и получать параметр за параметром, если они есть. Они не обязательны, но указывают, как вещи будут представлены пользователю. Эта система является саморасширяемой. То есть, если вы хотите добавить больше информации, вам просто нужно указать это в перечислении eParameter. В настоящее время система имеет 5 параметров, и их надо задавать в порядке, указанном в перечислении eParameters, которое выделено во фрагменте объявления переменной. Ниже показан каждый параметр в правильном порядке и то, что они означают.
Параметр | Результат |
---|---|
1. TEMPLATE или ASSET | Указывает, какой шаблон или актив следует просмотреть |
2. PERIOD | Если указан, зафиксирует индикатор на определенный период, так же, как он использовался ранее |
3. SCALE | Если задано значение, индикатор будет привязан к фиксированной шкале. |
4. WIDTH | Если указан, то будет устанавливать, насколько широким будет индикатор в окне. |
5. HEIGHT | Это новый параметр, будет рассмотрен далее в этой статье — используется для обозначения использования плавающего окна. |
После этого единственная структура, которая не выиграет при параметре №5, это IDE, но это будет исправлено, и в следующей статье я покажу, как это реализовать в IDE. В данной статье речь пойдет только о других системах.
Теперь предположим, что по той или иной причине мы хотим позволить пользователю управлять цветом вертикальной линии индикатора, нам не нужно будет вносить никаких изменений в код разбора параметра, потому что просто сделаем изменение согласно следующему примеру:
class C_TemplateChart : public C_SubWindow { #define def_MaxTemplates 8 #define def_NameTemplateRAD "IDE" //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eParameter {TEMPLATE = 0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT}; //+------------------------------------------------------------------+ // ... Внутренный код .... struct st00 { int counter; string Param[HEIGHT + 1]; }m_Params;
При этом система будет автоматически распознавать, что в одном вызове может быть 6 параметров. Теперь возникла другая проблема. Хотя приведенный выше код GetCommand работает нормально, в нем есть ошибка. Часто эту ошибку мы не замечаем, когда сами создаем систему, которую собираемся использовать. Но когда система становится доступной для использования другими людьми, сбой станет очевидным и может привести к тому, что некоторые менее опытные программисты не будут знать, как решить проблему. Поэтому объектно-ориентированное программирование так высоко ценится, и именно тогда создается наиболее подходящая модель для использования в представляющих наибольший интерес программах. Одна из предпосылок ООП (объектно-ориентированного программирования) заключается в том, чтобы убедиться, что данные и переменные класса инициализированы правильно. Чтобы гарантировать это, нужно все протестировать. И хотя код GetCommand кажется правильным, он содержит ошибку — не проверяет максимальное ограничение параметров. Если исходная модель принимает только 5 параметров, что произойдет, если пользователь поставит 6 параметров? Это то, чего нужно избегать: мы должны не предполагать, что всё будет работать, а гарантировать, что всё будет работать. Поэтому код нужно исправить так, как показано ниже (точки исправления выделены).
int GetCommand(int iArg, const string szArg) { for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = ""; m_Params.counter = 0; for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg]) { case ')': case ';': m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++); return iArg + 1; case ' ': c2 += (c1 == iArg ? 0 : 1); c1 = (c1 == iArg ? iArg + 1 : c1); break; case '(': case ',': if (m_Params.counter == HEIGHT) return StringLen(szArg) + 1; c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2); m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); c2 = 0; c1 = iArg + 1; break; default: c2++; break; } return -1; }
Мы увидим, что просто добавив одну единственную строку, мы предотвратим генерацию системой неожиданного результата, поскольку, если последним ожидаемым параметром является HEIGHT, но он не последний, это логически означает, что что-то не так, поэтому система игнорирует всё, что объявлено впоследствии, тем самым избегая проблем.
Итак, вы могли заметить, как система распознает шаблоны и параметры, но для тех, кто не понимает, синтаксис выглядит так:
Параметр_00 (Параметр_01, Параметр_02, Параметр_03, Параметр_04)
Где параметр_00 указывает шаблон для использования, а остальные разделяются запятыми ( , ) и указывают значения, которые определены в перечислении eParameter, если бы мы хотели изменить только параметр_03, мы можем оставить остальные пустыми, как показано на рисунке ниже. На этом рисунке я показываю, что система работает так, как хочет пользователь.
Обратите внимание, что у нас есть стандартизированная индикация, которая очень напоминает вызовы функций, но это может показаться запутанным. Давайте разберемся, что же произошло на самом деле: обратите внимание, что мы указываем шаблон RSI, затем не указываем ни период, ни масштаб. Эти значения остаются пустые для того, чтобы система понимала, что она должна следовать основному графику. Но мы указываем ширину и высоту, таким образом система понимает, что это должно отображаться в плавающем окне, и вот что происходит: индикатор RSI отображается в плавающем окне. В шаблоне ADX мы сообщаем только ширину, чтобы система отображала ее с шириной, определенной в подокне. Индикатор Stoch займет всё оставшееся подокно, деля место с ADX. Но если пользователь захочет что-то изменить, это не составит труда, посмотрите, что произойдет, когда мы укажем высоту для ADX.
Система немедленно изменяет способ представления ADX, помещая его в плавающее окно, и оставляет всё подокно для Stoch. Мы увидим, что каждое плавающее окно полностью независимо от другого. Но дело выходит за рамки того, что видно, см. следующую анимацию.
Обратите внимание, что подокно было удалено, потому что оно больше не нужно. Но какая функция отвечает за всё это? Мы можем видеть это ниже, и мы понимаем то, как можно сделать вещи действительно интересными с помощью всего нескольких модификаций:
void AddTemplate(void) { ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; string sz0 = m_Params.Param[PERIOD]; int w, h, i; bool bIsSymbol; if (sz0 == "1M") timeframe = PERIOD_M1; else if (sz0 == "2M") timeframe = PERIOD_M2; else if (sz0 == "3M") timeframe = PERIOD_M3; else if (sz0 == "4M") timeframe = PERIOD_M4; else if (sz0 == "5M") timeframe = PERIOD_M5; else if (sz0 == "6M") timeframe = PERIOD_M6; else if (sz0 == "10M") timeframe = PERIOD_M10; else if (sz0 == "12M") timeframe = PERIOD_M12; else if (sz0 == "15M") timeframe = PERIOD_M15; else if (sz0 == "20M") timeframe = PERIOD_M20; else if (sz0 == "30M") timeframe = PERIOD_M30; else if (sz0 == "1H") timeframe = PERIOD_H1; else if (sz0 == "2H") timeframe = PERIOD_H2; else if (sz0 == "3H") timeframe = PERIOD_H3; else if (sz0 == "4H") timeframe = PERIOD_H4; else if (sz0 == "6H") timeframe = PERIOD_H6; else if (sz0 == "8H") timeframe = PERIOD_H8; else if (sz0 == "12H") timeframe = PERIOD_H12; else if (sz0 == "1D") timeframe = PERIOD_D1; else if (sz0 == "1S") timeframe = PERIOD_W1; else if (sz0 == "1MES") timeframe = PERIOD_MN1; if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) return; bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true); w = (m_Params.Param[WIDTH] != "" ? (int)StringToInteger(m_Params.Param[WIDTH]) : 0); h = (m_Params.Param[HEIGHT] != "" ? (int)StringToInteger(m_Params.Param[HEIGHT]) : 0); i = (m_Params.Param[SCALE] != "" ? (int)StringToInteger(m_Params.Param[SCALE]) : -1); i = (i > 5 || i < 0 ? -1 : i); if ((w > 0) && (h > 0)) AddIndicator(m_Params.Param[TEMPLATE], 0, -1, w, h, timeframe, i); else { SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w); if (!ChartApplyTemplate(m_Info[m_Counter - 1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - 1].handle, "Default.tpl"); if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD) { C_Chart_IDE::Create(GetIdSubWinEA()); m_Info[m_Counter - 1].szVLine = ""; }else { m_Info[m_Counter - 1].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand(); ObjectCreate(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJ_VLINE, 0, 0, 0); ObjectSetInteger(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJPROP_COLOR, clrBlack); } ChartRedraw(m_Info[m_Counter - 1].handle); } }
Выделены разделы, которые делают выбор того, как данные будут представлены на экране. Код не сильно отличается от того, который уже существовал, но именно эти тесты гарантируют, что система будет вести себя так, как того хочет пользователь. Всё это до сих пор не требовало реструктуризации кода в новой модели, но когда мы смотрим на функцию, отвечающую за изменение размера подокна, всё меняется.
void Resize(void) { #define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B) int x0, x1, y; if (!ExistSubWin()) return; x0 = 0; y = (int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA())); x1 = (int)((Terminal.GetWidth() - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1)); for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++) { macro_SetInteger(OBJPROP_XDISTANCE, x0); macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1)); macro_SetInteger(OBJPROP_YSIZE, y); if (m_Info[c0].szTemplate == "IDE") C_Chart_IDE::Resize(x0); } ChartRedraw(); #undef macro_SetInteger }
Выделенная строка запрещает построение системы на старой базе, эта строка будет проверять, есть ли подокно, открытое и поддерживаемое советником, а если его нет, то возвращается, и функция больше ничего не делает. Но если такое подокно существует, все вещи, присутствующие в нем, должны будут быть изменены по мере необходимости, и только из-за этого теста система была полностью переделана.
Ниже показана следующая функция, в которую были внесены изменения:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { int mx, my; datetime dt; double p; C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam); switch (id) { case CHARTEVENT_MOUSE_MOVE: mx = (int)lparam; my = (int)dparam; ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (int c0 = 0; c0 < m_Counter; c0++) if (m_Info[c0].szVLine != "") { ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, 0, dt, 0); ChartRedraw(m_Info[c0].handle); } break; case CHARTEVENT_CHART_CHANGE: Resize(); for (int c0 = 0; c0 < m_Counter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe); ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_CHART_SCALE,(m_Info[c0].scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Info[c0].scale)); } break; } }
Выделенная часть действительно заслуживает особого внимания. Что она делает? Именно этот код представит вертикальную линию в нужном месте и в правильном шаблоне. Остальной код будет просто поддерживать и корректировать шаблоны по мере изменения диаграммы.
Существует множество преимуществ, если делать это здесь, в классе объектов, а не в советнике внутри системы OnChartEvent. Главное, что каждый класс может обрабатывать событие, которое МТ5 отправляет в советник, вместо того, чтобы централизовать всё в одной функции, мы позволяем каждому классу выполнять свою работу, и если мы не хотим использовать класс в советнике, мы просто удаляем его без каких-либо побочных эффектов для остального кода.
Программирование это КРАСИВО, не правда ли!? Я ЛЮБЛЮ программировать...
Прежде чем перейти к следующему пункту в рамках этой статьи, мы кратко прокомментируем значения, которые можно использовать в параметрах 1 и 2. Параметру 1 могут быть присвоены следующие значения: 1M, 2M, 3M, 4M, 5M, 6M, 10M, 12M, 15M, 20M, 30M, 1H, 2H, 3H, 4H, 6H, 8H , 12H , 1D, 1S, 1MONTH эти значения не случайны и не были выбраны случайно, они получены из перечисления ENUM_TIMEFRAME и точно воспроизводят то, что было бы сделано при использовании нормального графика. Параметр 2 может принимать значения от 0 до 5, где ноль — самая дальняя шкала, а пять — самая близкая, подробнее см. CHART_SCALE.
3.4 Поддержка плавающего окна
Теперь давайте разберемся, как создаются и поддерживаются плавающие окна, потому что без понимания этого невозможно будет по-настоящему воспользоваться преимуществами системы. Отвечающий за это объектный класс получил название C_ChartFloating, но можно подумать: а почему бы не использовать класс Control из стандартной библиотеки MQL5? Причина проста. Класс управления позволяет нам создавать и поддерживать окно с функциональностью, очень похожей на функциональность операционной системы, присутствующей в машине, но для наших целей это слишком много, нам нужно что-то гораздо более простое. Использование класса управления для выполнения того, что мы хотим, было бы похоже на использование базуки, чтобы убить муху, поэтому был задуман класс C_ChartFloating, который содержит минимум элементов, необходимых для поддержки плавающих окон, при этом позволяя нам управлять ими.
Сам класс не требует особых объяснений, так как единственное, что мы делаем - это создаем 4 графических объекта, но среди внутренних функций у него есть 2, которые заслуживают особого внимания. Давайте начнем с функции, которая создает окно, её код приведен ниже:
bool AddIndicator(string sz0, int x = 0, int y = -1, int w = 300, int h = 200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, int Scale = -1) { m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); if (m_MaxCounter >= def_MaxFloating) return false; y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y); CreateBarTitle(); CreateCaption(sz0); CreateBtnMaxMin(); CreateRegion(TimeFrame, Scale); m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID); ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl"); m_Win[m_MaxCounter].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand(); ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, 0, 0, 0); ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack); m_Win[m_MaxCounter].PosX = -1; m_Win[m_MaxCounter].PosY = -1; m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x; m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y; SetDimension(w, h, true, m_MaxCounter); SetPosition(x, y, m_MaxCounter); ChartRedraw(m_Win[m_MaxCounter].handle); m_MaxCounter++; return true; }
Что делает этот код, так это создает всю необходимую поддержку, чтобы мы могли применить шаблон в объекте CHART, это применяется в выделенной точке кода, обратите внимание, что для вызова этой функции единственный действительно необходимый параметр это имя шаблона. Все остальные значения предварительно инициализируются, но ничто не мешает указать, какие из них нужны. Для каждого нового созданного окна следующее будет немного смещено, чтобы оно, по умолчанию, не перекрывало другие окна. Это показано в следующей строке:
y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);
Следующая интересная функция в этом классе обрабатывает сообщения. Вот ее код:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { int mx, my; datetime dt; double p; static int six = -1, siy = -1, sic = -1; switch (id) { case CHARTEVENT_MOUSE_MOVE: mx = (int)lparam; my = (int)dparam; if ((((int)sparam) & 1) == 1) { if (sic == -1) for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--) sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -1); if (sic >= 0) { if (six < 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false); six = (six < 0 ? mx - m_Win[sic].PosX : six); siy = (siy < 0 ? my - m_Win[sic].PosY : siy); SetPosition(mx - six, my - siy, sic); } }else { if (six > 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true); six = siy = sic = -1; } ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (int c0 = 0; c0 < m_MaxCounter; c0++) ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, 0, dt, 0); break; case CHARTEVENT_OBJECT_CLICK: for (int c0 = 0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin) { SwapMaxMin((bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0); break; } break; case CHARTEVENT_CHART_CHANGE: for(int c0 = 0; c0 < m_MaxCounter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame); ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale)); } m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); break; } for (int c0 = 0; c0 < m_MaxCounter; c0++) ChartRedraw(m_Win[c0].handle); }
Эта функция концентрирует в себе всю обработку событий, которую поддерживает класс C_ChartFloating. Независимо от того, сколько окон присутствует, она будет обрабатывать их все одинаково. Если бы мы сделали это внутри функции OnChartEvent в советнике, функция была бы чрезвычайно сложная и не очень стабильная. А реализуя функционал здесь, в объектном классе, мы гарантируем целостность кода. Получается, что если нам не нужно использовать плавающие окна, то всё, что надо сделать — это удалить файл из класса и точки, в которых к нему можно обращались. Подобная реализация позволяет сделать код намного быстрее и проще.
В приведенном выше коде также есть интересная часть. Она выделена, а ее внутренний код которой ниже:
void SwapMaxMin(const bool IsMax, const int c0) { m_Win[c0].IsMaximized = IsMax; SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : 100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : 0), false, c0); SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0); }
Что делает приведенный выше код? Звучит слишком запутанно? Чтобы понять, давайте внимательно посмотрим на анимацию ниже.
Было сделано следующее: когда создается плавающее окно, оно имеет начальную точку привязки, которую указывает программист или система позиционирования самого класса. Эта точка привязки одинакова как для максимально развернутого окна, так и для момента, когда оно свернуто. Эти значения не являются фиксированными, то есть пользователь может легко изменить эти точки.
Предположим следующее: вам нужно определенное место на чистом графике, тогда вы можете переместить развернутое окно в место, которое легко и быстро читается, затем свернуть это же окно и переместить его в другое место, например в угол экрана. Система запомнит его, и когда вы развернеие окно, оно перейдет к последней точке привязки, в которой оно находилось перед сворачиванием. То же самое касается и обратной ситуации, когда окно свернуто.
Заключение
На этом пока всё, в следующей статье мы расширим этот функционал на класс поддержки IDE.
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/10353
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования