Ошибки, баги, вопросы - страница 105

 
Interesting:

В тестере свой отдельный список инструментов и его нужно сформировать (желательно это сделать при инициализации эксперта).

Спасибо большое! Получилось. Много пропустил...
 
Renat:

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

Вот исправленный вариант:

Я в него добавил пару вызовов Print(), чтобы внести ясность, как оно на самом деле работает:

double CalculateMaxVolume(string symbol)
  {
   double price=0.0;
   double margin=0.0;
//--- select lot size
   if(!SymbolInfoDouble(symbol,SYMBOL_ASK,price))                return(0.0);
   if(!OrderCalcMargin(ORDER_TYPE_BUY,symbol,1.0,price,margin)) return(0.0);
   if(margin<=0.0)                                            return(0.0);

   double lot_pure=AccountInfoDouble(ACCOUNT_FREEMARGIN)/margin;
   double lot=NormalizeDouble(lot_pure,2);
   Print("lot_pure = ", lot_pure, ", lot = ", lot);
//--- normalize and check limits
   double stepvol=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
   if(stepvol>0.0)
     {
      double newlot=stepvol*NormalizeDouble(lot/stepvol,0);
      if(newlot>lot) { Print("Чёрт побери: lot = ", lot, ", newlot = ", newlot);
                       lot=NormalizeDouble(newlot-stepvol,2);
                     }
      else           lot=newlot;
     }

   double minvol=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
   if(lot<minvol) lot=0.0;   // 

   double maxvol=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX);
   if(lot>maxvol) lot=maxvol;
//--- return trading volume
   return(lot);
  }

void OnStart()
{
  Print("CalculateMaxVolume(Symbol()) = ", CalculateMaxVolume(Symbol()));
}

/* Вывод в лог (хронология - сверху вниз)
CO      0       1 (EURUSD,M15)  01:40:33        lot_pure = 7.799703611262773, lot = 7.8
JG      0       1 (EURUSD,M15)  01:40:33        Чёрт побери: lot = 7.8, newlot = 7.800000000000001
MQ      0       1 (EURUSD,M15)  01:40:33        CalculateMaxVolume(Symbol()) = 7.7
*/

Теперь работает правильно. Но с оговоркой - при тех условиях, которые сейчас реально встречаются. Если они будут в будущем расширены, то и этот код начнёт в некоторых случаях при определённых условиях ошибаться. Ниже я поясню.

Вообще-то пришлось провести некоторые исследования, в результате получил преинтереснейшие результаты. Но обо всём по порядку.

Первое, что бросается в глаза: зачем столько плясок с NormalizeDouble()? Ведь, - что делает NormalizeDoubel()? Привязывает свободное значение к сетке. Например, в этом фрагменте кода:

double lot=NormalizeDouble(lot_pure,2);

NormalizeDouble() получает в качестве параметра lot_pure (свободное значение, то есть, такое, какое вычислилось по свободной марже и необходимой марже на 1 лот без всякого округления и прочих привязок) и выдаёт значение, привязанное к ближайшему значению сетки с началом в 0 и шагом 0.01.
Здесь важно заметить: к ближайшему узлу сетки, в том числе и к большему!

Для чего в этом месте кода выполняется эта привязка? И почему именно к сетке 0.01, а не к, скажем, 0.001?

Кстати, по выводу, помеченному в логе символами CO (первая строка приведённого куска лога), видно, что привязка как раз привела к увеличению значения.

Далее, известно, что все торговые функции, принимающие количество лотов в качестве одного из параметров, требуют, чтобы это значение было привязано к сетке: minvol + N * stepvol, где N - целое число от 0 до значения целой части выражения (maxvol - minvol) / stepvol. Соответственно, то свободное значение лота, которое было получено в этом фрагменте:

double lot_pure=AccountInfoDouble(ACCOUNT_FREEMARGIN)/margin;

необходимо привязать к ближайшему узлу указанной сетки: minvol + N * stepvol. Это значит, что нужно сначала от значения lot отнять minvol, прежде чем делить на stepvol (для получения того самого целого N), а после умножения на N - прибавить minvol. Вы же сразу делите на stepvol, неявно полагая, что stepvol является делителем minvol, то есть, укладывается в нём целое число раз, потому что только при соблюдении этого условия можно так "упростить" и не получить побочных эффектов:

double newlot=stepvol*NormalizeDouble(lot/stepvol,0);

И опять Вы используете NormalizeDouble(), на этот раз для привязки к сетке с началом в 0 и шагом 1, то есть к целым числам. Параметры привязки сетки верны, однако инструмент привязки - несколько неудачен: он привязывает к ближайшему узлу сетки, в том числе и к большему, если он окажется ближе. А в нашем случае это приведёт к последующей обязательной отработке корректирующего кода. Почему бы здесь не использовать замечательный инструмент привязки к "сетке целых чисел", используя вместо вызова NormalizeDouble() приведение к типу целых чисел, неувеличивающее приводимое значение, а лишь при необходимости уменьшающее его до ближайшего меньшего целого, то есть, - то, что нужно?

Но в данном месте возникает ещё один преинтереснейший артефакт, доказательство которому обнаруживается во второй строке приведённого куска лога, помеченного символами JG. Выясняется, что выражение "0.1 * NormalizeDouble(7.8 / 0.1)" даёт в качестве результата 7.800000000000001, что вынуждает к отработке корректирующий код! Зачем нужен такой код, который так плохо выполняет свою работу, что к нему нужно дописывать корректирующий?

Ясно, что код привязки к сетке допустимых значений лота нуждается в замене на более качественный.

Конечно, можно и такой код оставить, - ведь корректирующая часть кода, если что, отработает. Вот и третья строчка лога доказывает это: результат в конце концов возвращается верный. Однако, такой код, ко всему прочему, есть показатель уровня профессионализма его создателей и качества кода. В том числе и качества кода самой платформы MT5. И доказательства тому будут мной приведены, ибо на два bug'а в результате проведённого исследования наткнулся.

Кстати, взглянем ещё раз на код первоначальной привязки вычисленного значения lot_pure и на корректирующий код:

double lot=NormalizeDouble(lot_pure,2);
...
lot=NormalizeDouble(newlot-stepvol,2);
В обоих случаях выполняется привязка к сетке с шагом 0.01. Почему именно к этой сетке? Ведь требуется привязка к сетке minvol + N * stepvol, у которой шаг - stepvol. Что будет, если в будущем можно будет встретить и минимальное значение лота, и шаг лота размером в 0.001?

А очень просто - код начнёт в случаях, когда в процессе привязки к сетке с шагом 0.01 свободное значение lot изменяется на величину более, чем 0.001, выдавать неправильные результаты. Это и есть та оговорка, о которой я упомянул вначале.

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

То есть, в коде заложен потенциальный bug - на будущее. Это - к вопросу о профессионализме разработчиков и о качестве их кода.

Теперь, с учётом найденного, предложу свой вариант функции:

double CalculateMaxVolume_New(string symbol)
{
  double stepvol = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
  double minvol  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
  double maxvol  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);

  // Не доверяем значениям, которые вернули функции
  if(stepvol > 0 && minvol > 0 && maxvol > minvol)
  {
    double tmp = 0;

    // Вычисляем цену Ask, Margin на 1 лот, лотов на FreeMargin
    if(SymbolInfoDouble(symbol, SYMBOL_ASK, tmp)            && tmp > 0       &&
       OrderCalcMargin(ORDER_TYPE_BUY, symbol, 1, tmp, tmp) && tmp > 0       &&
       (tmp = AccountInfoDouble(ACCOUNT_FREEMARGIN) / tmp)         >= minvol &&
       tmp < ULONG_MAX * stepvol)
    {
      Print("pure_lot = ", tmp); // Эту строку нужно удалить из рабочего кода

      if(tmp > maxvol) // Здесь в tmp содержится недискретизированное число лотов
        return maxvol;

      // Привязываемся к сетке дискретизации
      return minvol + stepvol * (ulong)((tmp - minvol) / stepvol);
    }
  }

  return 0;
}

void OnStart()
{
  Print("CalculateMaxVolume_New(Symbol()) = ", CalculateMaxVolume_New(Symbol()));
}

/* Вывод в лог (хронология - сверху вниз)
LQ      0       1 (EURUSD,M15)  01:39:07        pure_lot = 7.799095304944626
KD      0       1 (EURUSD,M15)  01:39:07        CalculateMaxVolume_New(Symbol()) = 7.7
*/

Существует несколько случаев, по отношению к значению лотов (хранимому у меня в tmp), вычисленному, но ещё не привязанному к сетке допустимых значений. Привязанное к сетке значение назовём дискретизированным.

1. Случай, когда tmp < minvol. В этом случае неравенство сохранится и после дискретизации tmp, поскольку процесс дискретизации предполагает только уменьшение вычисленного значения (иначе не хватит свободной маржи, ибо вычисленное значение - максимально возможное для данного количества свободной маржи).

Поэтому этот случай можно отсечь ещё на раннем этапе, до дискретизации.

2. Случай, когда tmp > maxvol. В этом случае ограничением служит не свободная маржа, а максимально допустимое значение количества лотов, принимаемое торговыми функциями. В этом случае следует просто вернуть значение maxvol.

Для того, чтобы просто вернуть значение maxvol, никакая дискретизация tmp не требуется, поэтому этот случай также отсекается до кода дискретизации.

3. Случай, когда minvol <= tmp <= maxvol. В этом случае необходимо выполнить дискретизацию, причём в её результате дискретизированное значение останется в рамках неравенства для этого случая, то есть, ничего корректировать после дискретизации не надо.

Код дискретизации прост и эффективен:

return minvol + stepvol * (ulong)((tmp - minvol) / stepvol);

Здесь выражение "(tmp - minvol) / stepvol" вычисляет то самое число N (параметр сетки привязки), но с дробной частью. Поскольку в этом месте выполняется неравенство minvol <= tmp (случай 3), то тем самым гарантируется, что вычисленное значение неотрицательно. Далее происходит явное приведение вычисленного значения к значению типа ulong. Именно к ulong, поскольку имеется гарантия, что приводимое значение неотрицательно.

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

Поскольку целое значение N получено, то далее стандартным образом получается максимальное дискретизированное значение количества лотов, удовлетворяющее требованиям на свободную маржу (то есть, следующее по порядку значение в сетке дискретизации допустимых значений лотов уже не будет удовлетворять требованиям на свободную маржу, а полученное N ещё будет): "minvol + stepvol * N".

Хочу осветить один достаточно важный момент. Дело в том, что числа типа double могут принимать максимальные значения примерно до 1.8e308, а числа типа ulong - только примерно 1.8e19 (это только запись для удобства, сама константа 1.8e19 не ulong типа).

Что, если значение выражения "(tmp - minvol) / stepvol" превысит 1.8e19? Тогда при приведениик типу ulong произойдёт "обрезание" - значением станет остаток от целочисленного деления значения выражения на ULONG_MAX.

Если бы числа не были такими большими, то в терминах MQL5 это выглядело бы как значение "X % ULONG_MAX", где X есть целое, равное "(tmp - minvol) / stepvol".

Случай не типичный, но - зачем bug'и в коде оставлять? Тем более, функциям библиотеки MQL5 доверять нельзя, любую ахинею могут вернуть (я приведу тому доказательства).

Для случаев, когда значение выражения "tmp / stepvol" не укладывается в 1.8e19, специально введена проверка (последняя строчка в условии if):

tmp < ULONG_MAX * stepvol

Конечно, можно было записать как "tmp / stepvol < (double)ULONG_MAX", но, во-первых, стараюсь избегать операции деления когда нет явной надобности, во-вторых, пришлось бы тогда выполнять явное приведение типа, ведь константа ULONG_MAX - типа ulong, операнды при сравнениях неявно к старшему типу не приводятся (по крайней мере это так в C/C++) и в третьих - я бы не наткнулся на замечательный bug уже в самом языке даже, а не в библиотечных функциях, - bug не то, что в ДНК, а буквально молекулах и атомах MQL5.

Левый операнд операции сравнения есть tmp и имеет тип double, а правый - выражение "ULONG_MAX * stepvol" и тоже имеет тип double.

В этом выражении участвуют два операнда, один типа ulong, а другой типа double. Согласно правилам неявного приведения типов, сначала операнд младшего типа приводится к типу операнда старшего типа, выполняется операция и результат имеет тип операнда старшего типа. Тип double - "старше" типа ulong, поэтому значение ULONG_MAX типа ulong неявно приводится к типу double, выполняется умножение и результат имеет тип double.

Однако, здесь имеет место bug, который, кстати, проявляется не всегда, а только в определённых случаях, в том числе и здесь, и заключается bug в том, что результатом выражения "ULONG_MAX * stepvol" является просто значение stepvol.

Поэтому представленная мной функция не работает и не будет работать, пока разработчики MetaQuotes не исправят этот bug.

Для того, чтобы начать пользоваться этой функцией уже сейчас, нужно воспользоваться особенностью bug'а: он исчезает, если выполнить явное приведение к типу:

tmp < (double)ULONG_MAX * stepvol

Возвращаясь к описанной проверке: она гарантирует, что значение выражения "tmp / stepvol" не превысит ULONG_MAX. Но в коде дискретизации используется выражение "(tmp - minvol) / stepvol".

Значение этого выражения также не превысит значение ULONG_MAX, поскольку предыдущие проверки гарантируют, что minvol > 0, а tmp >= minvol, то есть, tmp - minvol < tmp.

Следовательно, гарантия не превышения значения ULOMG_MAX распространяется и на выражение "(tmp - minvol) / stepvol".

Вообще, одно из основных отличий профессионала от, скажем, непрофессионала заключается в том, что профессионал хоть что-то может гарантировать, а вот непрофессионал...

Оба найденных bug'а разобрал в другом сообщении, заодно уточнив, что MetaQuotes смогли, а что - нет.

 

Для чего в этом месте кода выполняется эта привязка? И почему именно к сетке 0.01, а не к, скажем, 0.001?

В системе минимальный лот = 0.01


Замечания:

  1. Ваше исходное условие minvol + N * stepvol не является гарантированно верным, в настройках могут выставить некратное значение minlot и все Ваша логика расчета будет нарушена
  2. Вы зря перешли на ulong - сами себе создали сложности, а потом написали целую страницу размышлений об этом
  3. Хитрая подмена и использование tmp в Вашем коде вынесет мозг многим, в моем варианте все гораздо понятнее по смыслу операций
 

Говорю исключительно за себя (но если кто увидит своё отражение имейте в виду вы не один такой).

За последние месяцы гонки за багами выработалась привычка в первую очередь считать неработающую программу багом МетаТрейдера.

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

Пример: нашёл баг отправил заявку в сервисдеск, отписали что баг исправлен, пишу проверочный код, нифига.

Обращаюсь ещё раз и уже в ожидании ответа нахожу свою криворукость.

Результат, стыдно что отвлёк людей на ровном месте.

Но анализируя поток сообщений понимаю что масса народу пусть и умного народу всё равно подвержена психологии толпы

раз баги есть, то и я напишу что это баг, а уж баг это или нет пусть Ренат разгребает мой код и ткнёт пальцем где у меня ошибка.

Я понимаю что толерантность не позволяет сказать: да ты баран у тебя код кривой.

Но ведь так далеко не уедешь и продожая так дальше весь персонал MQ скоро будет заниматься тем что сидеть над чужими кодами в скорбном раздумье "а нафига нам всё это надо", а тем временем чемпионат на носу, а там и выход на реальные счета не загорами.

Буду закруглятся, девиз сегодняшнего дня вижу такой "Собрался опубликовать баг, проверь не в твоих ли кривых руках проблема".

Общайтесь с разработчиками через Сервисдеск!
Общайтесь с разработчиками через Сервисдеск!
  • www.mql5.com
Ваше сообщение сразу станет доступно нашим отделам тестирования, технической поддержки и разработчикам торговой платформы.
 

Новый билд - новые проблемы. Эксперт, нормально работавший в 306 после компиляции в 314 (компиляция без ошибок), выдаёт в тестере:

2010.08.21 17:03:36    Core 1    disconnected
2010.08.21 17:03:36    Core 1    tester stopped because OnInit failed
2010.08.21 17:03:36    Core 1    2010.01.04 00:00:00   Access violation read to 0x0000000000000014
2010.08.21 17:03:36    Core 1    2010.01.04 00:00:00   Balance=10000.00 Equite=10000.00 Profit=0.00
2010.08.21 17:03:36    Core 1    2010.01.04 00:00:00   Эксперт PriceChannel_multi_Ch_Timer начал работу в 2010.01.04 00:00 на графике EURUSD период H1

В реале также выгружается. Оказалось, что источником ошибки является строка

m_symbol[j].Name(TradeSymbols[i]);

Замена её на пару строк

string curSymbol=TradeSymbols[i];
m_symbol[j].Name(curSymbol);

вернула статус-кво эксперту.

В чём дело ???

Кстати, код скомпилированный в прошлом билде, нормально работает и в этом.

 
Valmars:

В чём дело ???

Кстати, код скомпилированный в прошлом билде, нормально работает и в этом.

Наша ошибка - обязательно исправим.
 
Renat:

В системе минимальный лот = 0.01


Замечания:

  1. Ваше исходное условие minvol + N * stepvol не является гарантированно верным, в настройках могут выставить некратное значение minlot и все Ваша логика расчета будет нарушена
  2. Вы зря перешли на ulong - сами себе создали сложности, а потом написали целую страницу размышлений об этом
  3. Хитрая подмена и использование tmp в Вашем коде вынесет мозг многим, в моем варианте все гораздо понятнее по смыслу операций

Это сейчас в системе минимальный лот = 0.01. А через год? Через два?

1. А какое же условие верно? Какова тогда правильная формула? Скажем, для minvol = 0.15 и stepvol = 0.1 - каковы будут несколько первых допустимых значений лота? а) 0.15, 0.25, 0.35... ? б) 0.15, 0.2, 0.3... ? в) ... ? Я полагал, что имеет место вариант а.

2. На ulong перешёл не зря - имею право выбрать тип с самым широким диапазоном, чтобы хватало на максимально возможный широкий диапазон случаев, потому что такие функции - очень базовые кирпичики. А уж тот факт, что наткнулся при этом на bug, - совсем не значит, что это именно я создал себе сложности. :) Рассуждения писал больше для других, чтобы понятно было максимально широкому кругу, - у нас же здесь не личная переписка.

3. Подмена не хитрая - просто экономия, чтобы не заводить переменных однократного использования. Причём было отслежено-проверено, чтобы переменная передавалась по ссылке при вызове функции максимум один раз, дабы исключить возможные ошибки из-за этого. Если это кому-то "выносит мозг", можно завести переменную под каждое получаемое значение (даже промежуточное, как например, цена Ask), как это сделано у Вас. Этот момент непринципиален.

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

Исходил из того, что базовый кирпичик должен быть максимально надёжным и универсальным, - тогда весь дом, скорее всего, даже землетрясение переживёт.

 
Urain:

Говорю исключительно за себя (но если кто увидит своё отражение имейте в виду вы не один такой).

За последние месяцы гонки за багами выработалась привычка в первую очередь считать неработающую программу багом МетаТрейдера.

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

Тут очень большую роль играет тот факт, что bug'и MQL5 уже больно похожи на свои. И bug'ов MQL5 - много.

Вот было бы bug'ов MQL5 значительно меньше, и были бы они не такими простыми, - труднее значительно было бы перепутать.

Urain:

Но ведь так далеко не уедешь и продожая так дальше весь персонал MQ скоро будет заниматься тем что сидеть над чужими кодами в скорбном раздумье "а нафига нам всё это надо", а тем временем чемпионат на носу, а там и выход на реальные счета не загорами.

Буду закруглятся, девиз сегодняшнего дня вижу такой "Собрался опубликовать баг, проверь не в твоих ли кривых руках проблема".

То, что чемпионат по советникам, написанным ТОЛЬКО на MQL5, - авантюра, было ясно в момент объявления этого решения. Но у руководства - своё видение. Они же сами решили так. Им никто не мешал решать. Так что - ну и что, что чемпионат на носу, - они сами себе устроили такую жизнь.

А здесь - просто: нужно самому проделать некоторую работу по локализации bug'а: начать выбрасывать из кода всё, что не влияет на bug. В конце концов получится что-то типа тестового примера, - достаточно небольшого и при этом, в основном, bug и демонстрирующим. Это уже будет не "чужой код", а "код, bug MQL5 демонстрирующий".

 
Написал скрипт для проверки функции OrderCalcMargin()
void OnStart()
  {
//---
   int total=SymbolsTotal(false);
   double marginbay;
   double marginsell;
   MqlTick last_tick;
   for(int i=0;i<total;i++)
     {

      string symbol=SymbolName(i,false);
      Print("************************************************");
      Print("Инструмент - ",symbol);
      Print("Валюта депозита = ",AccountInfoString(ACCOUNT_CURRENCY));
      Print("Базовая валюта = ",SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE));
      Print("Валюта маржи = ",SymbolInfoString(symbol,SYMBOL_CURRENCY_MARGIN));
      if(SymbolInfoTick(symbol,last_tick))
        {
         OrderCalcMargin(ORDER_TYPE_BUY,symbol,1.0,last_tick.ask,marginbay);
         OrderCalcMargin(ORDER_TYPE_SELL,symbol,1.0,last_tick.bid,marginsell);
         Print("Маржа для покупки = ",marginbay);
         Print("Маржа для продажи = ",marginsell);
        }
      else Print("SymbolInfoTick() failed, error = ",GetLastError());
     }
  }
//+------------------------------------------------------------------+
Для некоторых инструментов функция возвращает ноль, это баг или так задумано?
 
sergey1294:
Написал скрипт для проверки функции OrderCalcMargin()Для некоторых инструментов функция возвращает ноль, это баг или так задумано?

Вероятно, это для тех символов, которые отсутствуют в MarketWatch, так как для SymbolName сказано:

SymbolName

Возвращает наименование указанного символа.

string  SymbolName(
   int   pos,          // номер в списке
   bool  selected      // true – только символы в MarketWatch
   );

Параметры

pos

[in]  Номер символа по порядку.

selected

[in]  Режим запроса. Если значение true, то символ берется из списка выбранных в MarketWatch. Если значение false, то символ берется из общего списка.

Возвращаемое значение

Значение типа string c именем символа.

Выведите имя символа, для которого получен неожиданный результат и сравните со списком в MarketWatch.
Причина обращения: