| //+------------------------------------------------------------------+
|
| //| RenkoGPTBreakoutEA.mq5 |
|
| //| Самостоятельный советник для торговли пробоев структуры Renko |
|
| //+------------------------------------------------------------------+
|
| //
|
| // НАЗНАЧЕНИЕ
|
| // 1. Самостоятельно строит внутреннюю последовательность кирпичей Renko
|
| // по потоку котировок; внешний индикатор и подключаемый ресурс не требуются.
|
| // 2. Отображает торговую панель с кнопками BUY/SELL, статусом торговли,
|
| // диагностикой, размером кирпича и предполагаемыми уровнями входа.
|
| //
|
| // РУЧНАЯ ПОДГОТОВКА ВХОДА
|
| // 3. Нажатие BUY или SELL фиксирует цену пробоя и структурный Stop Loss
|
| // по доступной на этот момент структуре Renko. Уровни должны оставаться
|
| // неизменными, пока подготовка активна; новые экстремумы их не сдвигают.
|
| // 4. BUY ожидает пробой фиксированной цены вверх; SELL — пробой фиксированной
|
| // цены вниз. На панели отображается именно сохранённая цена, а не новый
|
| // уровень, пересчитанный из текущего экстремума.
|
| // 5. Если цена достигает/пересекает зафиксированный SL до входа, подготовка
|
| // соответствующего направления отменяется, кнопка выключается, а в ячейке
|
| // цены показывается «нет цены входа». Пользователь также может отменить
|
| // подготовку повторным нажатием активной кнопки.
|
| // 6. Если цена входа уже пробита в момент нажатия кнопки, подготовка не должна
|
| // активироваться задним числом: требуется новое пересечение уровня.
|
| //
|
| // ОТКРЫТИЕ СДЕЛКИ И РИСКИ
|
| // 7. После пересечения уровня советник проверяет разрешение торговли,
|
| // торговое время, спред и остальные предусмотренные ограничения. На счёте
|
| // Hedging открывает отдельную корзину позиций; неоднозначный ответ не вызывает
|
| // слепого повтора. Неполная корзина фиксируется в журнале.
|
| // 8. Stop Loss основан на зафиксированном структурном уровне с буфером и общий
|
| // для корзины. Для каждой позиции TP рассчитывается от фактической цены сделки
|
| // с нарастающим шагом. Значения *_Points измеряются в points инструмента.
|
| // 9. InpSignalMaxAgeSeconds — защитный максимальный срок жизни возникшего
|
| // сигнала после пробоя; он не заменяет событийную отмену подготовки до входа.
|
| // 10. Часы торгового окна задаются по Москве. Проверьте соответствие часового
|
| // пояса и поведения закрытия позиций настройкам брокера и терминала.
|
| //
|
| // ДАННЫЕ, ДИАГНОСТИКА И ПРОВЕРКИ
|
| // 11. Для восстановления структуры используется ограниченная история тиков
|
| // за InpHistoryDays; результат зависит от доступности истории у брокера.
|
| // 12. Диагностические события записываются в журнал/CSV, если запись доступна.
|
| // В журнале ищите причины подготовки, отмены, попытки и результата ордера.
|
| // 13. Кодовые статические проверки не заменяют компиляцию в MetaEditor,
|
| // визуальную проверку панели и тестирование в Strategy Tester/на демо-счёте.
|
| // До таких проверок нельзя считать исполнение и прибыльность подтверждёнными.
|
| //
|
| // ВАЖНО: советник может отправлять реальные торговые заявки при разрешённой
|
| // торговле. Сначала проверяйте его на исторических данных и демо-счёте.
|
| //+------------------------------------------------------------------+
|
| // MQL4-директива #property strict в MQL5 не используется и удалена
|
| #property version "1.08"
|
| #property description "Self-contained Renko breakout EA; no external indicator resource dependency"
|
|
|
| #include <Trade/Trade.mqh>
|
| //--- Константы истории, маржи и журнала
|
| #define HISTORY_CHUNK_SECONDS (6*3600) // Размер чанка истории тиков
|
| #define HISTORY_PROBE_SECONDS (15*60) // Проверка свежей истории перед пересборкой
|
| #define HISTORY_INIT_BUDGET_MS 15000 // Бюджет первичной пересборки в OnInit
|
| #define HISTORY_RETRY_MIN_MS 10000 // Минимальная пауза между попытками
|
| #define HISTORY_RETRY_MAX_MS 60000 // Максимальная пауза между попытками
|
| #define MARGIN_SAFETY_RATIO 1.20 // Запас свободной маржи к требуемой
|
| #define PANEL_MIN_INTERVAL_MS 200 // Минимальный интервал обновления панели
|
|
|
|
|
| input int InpBrickPoints = 10; // Размер кирпича Renko, points
|
| input int InpPositionsCount = 5; // Количество отдельных позиций в корзине
|
| input double InpPositionLots = 0.01; // Объём каждой позиции
|
| input int InpTakeProfitStepPoints = 500; // Шаг ступенчатого TP, points
|
| input int InpSLBufferPoints = 3; // Буфер SL: BUY ниже минимума, SELL выше максимума, points
|
| input int InpRetrySeconds = 5; // Интервал повторной попытки
|
| input int InpSignalMaxAgeSeconds = 120; // Максимальный возраст сигнала; 0 не допускается
|
| input int InpMaxSpreadPoints = 30; // Максимальный спред, points; 0 = без фильтра (по умолчанию фильтр включён)
|
| input int InpDeviationPoints = 20; // Допустимое отклонение, points
|
| input ulong InpMagicNumber = 6211026; // Magic Number
|
| input string InpInstanceId = "default"; // Постоянный уникальный ID настроек для этого символа/Magic
|
| input int InpHistoryDays = 7; // История тиков для восстановления структуры, дней (30 — заметно медленнее старт)
|
| input int InpTradeStartHourMSK = 3; // Начало торговли, Москва
|
| input int InpTradeEndHourMSK = 23; // Конец торговли, Москва (не включая)
|
| input bool InpCloseAt23MSK = false; // Включить автозакрытие по московскому времени
|
| input int InpCloseHourMSK = 23; // Час автозакрытия по Москве: 0–23
|
| input bool InpCloseAllSymbolPositions = true; // Совместимость настроек: при автозакрытии закрываются все позиции символа
|
| input string InpErrorLogFile = "RenkoGPTBreakoutEA_errors.csv";
|
|
|
| input bool InpBuyEnabledInitially = false; // BUY-режим при запуске
|
| input bool InpSellEnabledInitially = false; // SELL-режим при запуске
|
| input color InpButtonActiveColor = clrForestGreen; // Цвет активных кнопок
|
| input color InpButtonInactiveColor = clrFireBrick; // Цвет неактивных кнопок
|
| input color InpTradeOnColor = clrLimeGreen; // Индикатор TRADE ON
|
| input color InpTradeOffColor = clrTomato; // Индикатор TRADE OFF
|
|
|
| int g_panelYOffset=0;
|
| string g_instanceTag="", g_buyButton="", g_sellButton="", g_brickEdit="", g_brickApply="", g_positionsEdit="", g_lotsEdit="", g_basketApply="", g_fivePositionsButton="", g_positionsLabel="", g_lotsLabel="", g_statusLabel="", g_terminalTradeLabel="", g_terminalTradeDot="";
|
| int g_runtimePositionsCount=5;
|
| double g_runtimePositionLots=0.01;
|
| string g_diagLabels[10];
|
| string g_lastCloseInfo="Закрытие позиции: данных пока нет";
|
| string g_lastDecision="Ожидание сигнала";
|
| bool g_buyEnabled=false, g_sellEnabled=false;
|
| int g_runtimeBrickPoints=0;
|
| bool g_restoreBuyEnabled=false, g_restoreSellEnabled=false;
|
| bool g_restoreModesPending=false;
|
| bool g_persistenceReady=false;
|
|
|
| struct Brick
|
| {
|
| datetime time;
|
| double open;
|
| double high;
|
| double low;
|
| double close;
|
| int direction;
|
| };
|
|
|
| CTrade g_trade;
|
| Brick g_bricks[];
|
| int g_count=0;
|
| double g_brickSize=0.0, g_lastClose=0.0, g_lastBid=0.0;
|
| int g_direction=0;
|
| bool g_haveAnchor=false, g_historyReady=false;
|
| bool g_processBidGuardExceeded=false;
|
| long g_lastTickMsc=0;
|
| int g_ticksProcessedAtLastMsc=0; // Все записи на последней миллисекунде, включая записи без Bid.
|
| bool g_liveTickRangePending=false;
|
| datetime g_lastLiveTickRangeLog=0;
|
| double g_lastSwingHigh=0.0, g_lastSwingLow=0.0;
|
| double g_prevDownLow=0.0, g_latestDownLow=0.0;
|
| double g_prevUpHigh=0.0, g_latestUpHigh=0.0;
|
| double g_downHighs[], g_upLows[];
|
| bool g_swingHighBreakoutTriggered=false, g_swingLowBreakoutTriggered=false;
|
| double g_buyBreakLevel=0.0, g_sellBreakLevel=0.0;
|
| double g_buyEntryLevel=0.0, g_sellEntryLevel=0.0;
|
| double g_buySetupStop=0.0, g_sellSetupStop=0.0;
|
| double g_pendingStopLevel=0.0; // Структурный уровень SL, зафиксированный при возникновении сигнала
|
|
|
| //--- Восстановление истории: частичная готовность, задержка повторов, снимок состояния
|
| bool g_historyPartial=false; // История собрана частично (часть чанков недоступна)
|
| ulong g_historyRetryDelayMs=10000; // Текущая пауза между попытками пересборки
|
|
|
| //--- Снимок состояния Renko: неудачная пересборка не должна стирать кирпичи и режимы
|
| bool g_snapshotValid=false;
|
| Brick g_snapshotBricks[];
|
| int g_snapshotCount=0;
|
| double g_snapshotBrickSize=0.0;
|
| double g_snapshotLastClose=0.0;
|
| double g_snapshotLastBid=0.0;
|
| int g_snapshotDirection=0;
|
| bool g_snapshotHaveAnchor=false;
|
| long g_snapshotLastTickMsc=0;
|
| int g_snapshotTicksAtLastMsc=0;
|
| double g_snapshotLastSwingHigh=0.0;
|
| double g_snapshotLastSwingLow=0.0;
|
| double g_snapshotPrevDownLow=0.0;
|
| double g_snapshotLatestDownLow=0.0;
|
| double g_snapshotPrevUpHigh=0.0;
|
| double g_snapshotLatestUpHigh=0.0;
|
| bool g_snapshotSwingHighTriggered=false;
|
| bool g_snapshotSwingLowTriggered=false;
|
| double g_snapshotBuyEntry=0.0;
|
| double g_snapshotSellEntry=0.0;
|
| double g_snapshotBuyStop=0.0;
|
| double g_snapshotSellStop=0.0;
|
| bool g_snapshotBuyEnabled=false;
|
| bool g_snapshotSellEnabled=false;
|
| int g_snapshotPendingSignal=0;
|
| bool g_snapshotHistoryReady=false;
|
| bool g_snapshotHistoryPartial=false;
|
|
|
| //--- Журнал событий: файл держится открытым между записями
|
| int g_logHandle=INVALID_HANDLE;
|
| datetime g_lastSignalTime=0, g_lastAttemptTime=0;
|
| int g_attempts=0;
|
| int g_pendingSignal=0; // 1 BUY, -1 SELL
|
| datetime g_lastCloseCheckDay=0;
|
| ulong g_lastCloseAttemptMs=0; // Монотонный таймер: не отправлять повторное закрытие каждый тик/секунду.
|
| ulong g_emergencyCloseTicket=0;
|
| ulong g_emergencyCloseLastAttemptMs=0;
|
| ulong g_lastHistoryAttemptMs=0;
|
| string g_logFile="";
|
|
|
| string PersistentSettingKey(const string settingName)
|
| {
|
| // Stable across chart timeframe changes and terminal restarts.
|
| // Two independent 32-bit hashes keep Global Variable names short.
|
| string identity=AccountInfoString(ACCOUNT_SERVER)+"|"+
|
| IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))+"|"+
|
| _Symbol+"|"+IntegerToString((long)InpMagicNumber)+"|"+InpInstanceId;
|
| uint hashA=2166136261;
|
| uint hashB=2246822519;
|
| for(int i=0;i<StringLen(identity);i++)
|
| {
|
| uint ch=(uint)StringGetCharacter(identity,i);
|
| hashA=(hashA^ch)*16777619;
|
| hashB=(hashB+ch)*3266489917;
|
| hashB^=(hashB>>13);
|
| }
|
| return StringFormat("RGPT.%u.%u.%s",hashA,hashB,settingName);
|
| }
|
|
|
| bool SavePersistentValue(const string settingName,const double value)
|
| {
|
| string key=PersistentSettingKey(settingName);
|
| ResetLastError();
|
| datetime written=GlobalVariableSet(key,value);
|
| if(written==0)
|
| {
|
| LogEvent("SETTING_SAVE_FAILED",StringFormat("name=%s error=%d",settingName,GetLastError()));
|
| return false;
|
| }
|
| GlobalVariablesFlush();
|
| if(!GlobalVariableCheck(key))
|
| {
|
| LogEvent("SETTING_VERIFY_FAILED",StringFormat("name=%s reason=key_missing",settingName));
|
| return false;
|
| }
|
| double stored=GlobalVariableGet(key);
|
| if(stored!=value)
|
| {
|
| LogEvent("SETTING_VERIFY_FAILED",StringFormat("name=%s expected=%s actual=%s",settingName,DoubleToString(value,8),DoubleToString(stored,8)));
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| bool LoadPersistentValue(const string settingName,double &value)
|
| {
|
| string key=PersistentSettingKey(settingName);
|
| if(!GlobalVariableCheck(key)) return false;
|
| ResetLastError();
|
| double stored=GlobalVariableGet(key);
|
| int error=GetLastError();
|
| if(error!=0 || !MathIsValidNumber(stored))
|
| {
|
| LogEvent("SETTING_LOAD_INVALID",StringFormat("name=%s error=%d",settingName,error));
|
| return false;
|
| }
|
| value=stored;
|
| return true;
|
| }
|
|
|
| void LoadPersistentSettings()
|
| {
|
| g_runtimeBrickPoints=InpBrickPoints;
|
| g_runtimePositionsCount=InpPositionsCount;
|
| g_runtimePositionLots=InpPositionLots;
|
| g_restoreBuyEnabled=InpBuyEnabledInitially;
|
| g_restoreSellEnabled=InpSellEnabledInitially;
|
| double value=0.0;
|
| if(LoadPersistentValue("BrickPoints",value))
|
| {
|
| if(value>=1.0 && value<=100000000.0 && MathFloor(value)==value)
|
| g_runtimeBrickPoints=(int)value;
|
| else
|
| LogEvent("SETTING_LOAD_INVALID","name=BrickPoints value="+DoubleToString(value,2));
|
| }
|
| if(LoadPersistentValue("PositionsCount",value))
|
| {
|
| if(value>=1.0 && value<=100.0 && MathFloor(value)==value) g_runtimePositionsCount=(int)value;
|
| else LogEvent("SETTING_LOAD_INVALID","name=PositionsCount value="+DoubleToString(value,2));
|
| }
|
| if(LoadPersistentValue("PositionLots",value))
|
| {
|
| double valid=ValidVolume(value);
|
| if(value>0.0 && valid>0.0 && MathAbs(valid-value)<1e-8) g_runtimePositionLots=valid;
|
| else LogEvent("SETTING_LOAD_INVALID","name=PositionLots value="+DoubleToString(value,4));
|
| }
|
| if(LoadPersistentValue("BuyEnabled",value))
|
| {
|
| if(value==0.0 || value==1.0) g_restoreBuyEnabled=(value==1.0);
|
| else LogEvent("SETTING_LOAD_INVALID","name=BuyEnabled value="+DoubleToString(value,2));
|
| }
|
| if(LoadPersistentValue("SellEnabled",value))
|
| {
|
| if(value==0.0 || value==1.0) g_restoreSellEnabled=(value==1.0);
|
| else LogEvent("SETTING_LOAD_INVALID","name=SellEnabled value="+DoubleToString(value,2));
|
| }
|
| g_brickSize=(double)g_runtimeBrickPoints*_Point;
|
| }
|
|
|
| bool PersistEmergencyCloseTicket()
|
| {
|
| // Persist the pending retry so a terminal restart cannot silently drop it.
|
| // MT5 Global Variables store doubles; reject tickets outside exact integer range.
|
| if(g_emergencyCloseTicket>9007199254740991)
|
| {
|
| LogEvent("EMERGENCY_CLOSE_PERSIST_FAILED","ticket exceeds exact double integer range");
|
| return false;
|
| }
|
| return SavePersistentValue("EmergencyCloseTicket",(double)g_emergencyCloseTicket);
|
| }
|
|
|
| bool SavePersistentSettings()
|
| {
|
| if(!g_persistenceReady) return false;
|
| // If history is temporarily unavailable, preserve the last persisted mode preferences
|
| // instead of overwriting them with the fail-closed runtime state.
|
| bool buyToSave=(g_historyReady && !g_restoreModesPending)?g_buyEnabled:g_restoreBuyEnabled;
|
| bool sellToSave=(g_historyReady && !g_restoreModesPending)?g_sellEnabled:g_restoreSellEnabled;
|
| bool brickOk=SavePersistentValue("BrickPoints",(double)g_runtimeBrickPoints);
|
| bool countOk=SavePersistentValue("PositionsCount",(double)g_runtimePositionsCount);
|
| bool lotsOk=SavePersistentValue("PositionLots",g_runtimePositionLots);
|
| bool buyOk=SavePersistentValue("BuyEnabled",buyToSave?1.0:0.0);
|
| bool sellOk=SavePersistentValue("SellEnabled",sellToSave?1.0:0.0);
|
| bool ok=brickOk && countOk && lotsOk && buyOk && sellOk;
|
| if(ok)
|
| {
|
| g_restoreBuyEnabled=buyToSave;
|
| g_restoreSellEnabled=sellToSave;
|
| }
|
| return ok;
|
| }
|
|
|
| void SetButtonStyle(const string name,const bool enabled)
|
| {
|
| if(ObjectFind(0,name)<0) return;
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,enabled?InpButtonActiveColor:InpButtonInactiveColor);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,enabled?InpButtonActiveColor:InpButtonInactiveColor);
|
| ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| }
|
|
|
| void UpdateTerminalTradeIndicator()
|
| {
|
| bool allowed=(bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED);
|
| if(ObjectFind(0,g_terminalTradeLabel)>=0)
|
| {
|
| ObjectSetString(0,g_terminalTradeLabel,OBJPROP_TEXT,allowed?"TRADE ON":"TRADE OFF");
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_FONTSIZE,11);
|
| ObjectSetString(0,g_terminalTradeLabel,OBJPROP_FONT,"Arial Bold");
|
| }
|
| if(ObjectFind(0,g_terminalTradeDot)>=0)
|
| {
|
| ObjectSetString(0,g_terminalTradeDot,OBJPROP_TEXT,"●");
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_COLOR,allowed?InpTradeOnColor:InpTradeOffColor);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_FONTSIZE,14);
|
| ObjectSetString(0,g_terminalTradeDot,OBJPROP_FONT,"Arial");
|
| }
|
| }
|
|
|
| void ClearEntrySetup(const int side)
|
| {
|
| if(side>0)
|
| {
|
| g_buyEnabled=false; g_buyEntryLevel=0.0; g_buySetupStop=0.0;
|
| if(g_pendingSignal>0) { g_pendingSignal=0; g_pendingStopLevel=0.0; }
|
| }
|
| else
|
| {
|
| g_sellEnabled=false; g_sellEntryLevel=0.0; g_sellSetupStop=0.0;
|
| if(g_pendingSignal<0) { g_pendingSignal=0; g_pendingStopLevel=0.0; }
|
| }
|
| }
|
|
|
| bool ArmEntrySetup(const int side)
|
| {
|
| if(!g_historyReady || g_liveTickRangePending)
|
| {
|
| g_lastDecision="Нельзя установить вход: история тиков не синхронизирована";
|
| return false;
|
| }
|
| MqlTick tick;
|
| if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0 || g_brickSize<=0)
|
| {
|
| g_lastDecision="Нельзя установить вход: нет актуальной котировки или размера Renko";
|
| return false;
|
| }
|
| if(side>0)
|
| {
|
| if(g_lastSwingHigh<=0 || g_lastSwingLow<=0)
|
| { g_lastDecision="BUY не активирован: нет подтверждённой вершины/впадины для входа и SL"; return false; }
|
| double entry=NormalizePrice(g_lastSwingHigh+g_brickSize);
|
| double stop=NormalizeStopLoss(g_lastSwingLow-InpSLBufferPoints*_Point,true);
|
| if(tick.bid>=entry)
|
| { g_lastDecision="BUY не активирован: цена входа уже пробита"; return false; }
|
| if(stop>=tick.bid)
|
| { g_lastDecision="BUY не активирован: цена уже достигла предполагаемого SL"; return false; }
|
| g_buyEntryLevel=entry; g_buySetupStop=stop; g_buyEnabled=true;
|
| g_lastDecision="BUY подготовлен: цена входа и SL зафиксированы";
|
| LogEvent("BUY_SETUP_ARMED",StringFormat("entry=%s sl=%s",DoubleToString(entry,_Digits),DoubleToString(stop,_Digits)));
|
| return true;
|
| }
|
| if(g_lastSwingLow<=0 || g_lastSwingHigh<=0)
|
| { g_lastDecision="SELL не активирован: нет подтверждённой впадины/вершины для входа и SL"; return false; }
|
| double entry=NormalizePrice(g_lastSwingLow-g_brickSize);
|
| double stop=NormalizeStopLoss(g_lastSwingHigh+InpSLBufferPoints*_Point,false);
|
| if(tick.bid<=entry)
|
| { g_lastDecision="SELL не активирован: цена входа уже пробита"; return false; }
|
| if(stop<=tick.bid)
|
| { g_lastDecision="SELL не активирован: цена уже достигла предполагаемого SL"; return false; }
|
| g_sellEntryLevel=entry; g_sellSetupStop=stop; g_sellEnabled=true;
|
| g_lastDecision="SELL подготовлен: цена входа и SL зафиксированы";
|
| LogEvent("SELL_SETUP_ARMED",StringFormat("entry=%s sl=%s",DoubleToString(entry,_Digits),DoubleToString(stop,_Digits)));
|
| return true;
|
| }
|
|
|
| void RestoreSavedModesIfReady()
|
| {
|
| if(!g_restoreModesPending || !g_historyReady || g_liveTickRangePending)
|
| return;
|
|
|
| // Existing positions may be an incomplete basket left by a failed order or restart.
|
| // Never reactivate entry modes while this instance still owns an open position.
|
| g_restoreModesPending=false;
|
| if(HasOurPosition())
|
| {
|
| g_buyEnabled=false;
|
| g_sellEnabled=false;
|
| g_buyEntryLevel=0.0;
|
| g_sellEntryLevel=0.0;
|
| g_buySetupStop=0.0;
|
| g_sellSetupStop=0.0;
|
| g_restoreBuyEnabled=false;
|
| g_restoreSellEnabled=false;
|
| LogEvent("RESTORED_MODES_BLOCKED_POSITION_EXISTS",
|
| "saved entry modes cleared because an owned position already exists");
|
| SavePersistentSettings();
|
| return;
|
| }
|
| // Only restore after HISTORY → LIVE synchronization and a no-position check.
|
| if(g_restoreBuyEnabled && !ArmEntrySetup(1))
|
| {
|
| g_restoreBuyEnabled=false;
|
| LogEvent("RESTORED_BUY_SETUP_NOT_ARMED",g_lastDecision);
|
| }
|
| if(g_restoreSellEnabled && !ArmEntrySetup(-1))
|
| {
|
| g_restoreSellEnabled=false;
|
| LogEvent("RESTORED_SELL_SETUP_NOT_ARMED",g_lastDecision);
|
| }
|
| SavePersistentSettings();
|
| LogEvent("RESTORED_MODES_FINALIZED",StringFormat("buy=%s sell=%s",g_buyEnabled?"true":"false",g_sellEnabled?"true":"false"));
|
| }
|
|
|
| void UpdatePanel()
|
| {
|
| // Троттлинг: панель не перерисовывается чаще PANEL_MIN_INTERVAL_MS.
|
| static ulong lastPanelMs=0;
|
| ulong nowPanelMs=GetTickCount64();
|
| if(lastPanelMs!=0 && nowPanelMs-lastPanelMs<PANEL_MIN_INTERVAL_MS) return;
|
| lastPanelMs=nowPanelMs;
|
| if(ObjectFind(0,g_buyButton)>=0)
|
| {
|
| ObjectSetString(0,g_buyButton,OBJPROP_TEXT,"BUY");
|
| SetButtonStyle(g_buyButton,g_buyEnabled);
|
| }
|
| if(ObjectFind(0,g_sellButton)>=0)
|
| {
|
| ObjectSetString(0,g_sellButton,OBJPROP_TEXT,"SELL");
|
| SetButtonStyle(g_sellButton,g_sellEnabled);
|
| }
|
| UpdateTerminalTradeIndicator();
|
| if(ObjectFind(0,g_brickEdit)>=0 && !ObjectGetInteger(0,g_brickEdit,OBJPROP_SELECTED))
|
| ObjectSetString(0,g_brickEdit,OBJPROP_TEXT,IntegerToString(g_runtimeBrickPoints));
|
| if(ObjectFind(0,g_positionsEdit)>=0 && !ObjectGetInteger(0,g_positionsEdit,OBJPROP_SELECTED))
|
| ObjectSetString(0,g_positionsEdit,OBJPROP_TEXT,IntegerToString(g_runtimePositionsCount));
|
| if(ObjectFind(0,g_lotsEdit)>=0 && !ObjectGetInteger(0,g_lotsEdit,OBJPROP_SELECTED))
|
| ObjectSetString(0,g_lotsEdit,OBJPROP_TEXT,DoubleToString(g_runtimePositionLots,4));
|
| if(ObjectFind(0,g_statusLabel)>=0)
|
| ObjectSetString(0,g_statusLabel,OBJPROP_TEXT,
|
| StringFormat("Renko EA | кирпич: %d points | BUY %s | SELL %s",
|
| g_runtimeBrickPoints,g_buyEnabled?"АКТИВЕН":"НЕАКТИВЕН",g_sellEnabled?"АКТИВЕН":"НЕАКТИВЕН"));
|
| string side=(g_pendingSignal>0?"BUY":(g_pendingSignal<0?"SELL":"нет"));
|
| string lines[10];
|
| lines[0]=StringFormat("Автоторговля терминала: %s | BUY: %s | SELL: %s",(bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)?"РАЗРЕШЕНА":"ЗАПРЕЩЕНА",g_buyEnabled?"АКТИВЕН":"НЕАКТИВЕН",g_sellEnabled?"АКТИВЕН":"НЕАКТИВЕН");
|
| lines[1]=StringFormat("Сигнал: %s | учтённых попыток: %d",side,g_attempts);
|
| datetime serverNow=TimeTradeServer();
|
| if(serverNow<=0) serverNow=TimeCurrent();
|
| lines[2]=StringFormat("Время терминала: %s | Москва: %s | окно: %s",
|
| TimeToString(serverNow,TIME_DATE|TIME_SECONDS),
|
| TimeToString(MoscowTime(),TIME_DATE|TIME_SECONDS),
|
| TradingHours()?"ОТКРЫТО":"ЗАКРЫТО");
|
| lines[3]=StringFormat("Позиция по символу: %s",HasAnyPositionOnSymbol()?"ЕСТЬ":"НЕТ");
|
| if(!g_historyReady)
|
| lines[4]="Решение: история тиков не готова; торговля и подготовка входа заблокированы";
|
| else if(g_liveTickRangePending)
|
| lines[4]="Решение: ожидание синхронизации тиков; исполнение сигналов заблокировано";
|
| else
|
| lines[4]="Решение: "+g_lastDecision;
|
| MqlTick diagTick; double spreadPoints=-1;
|
| if(SymbolInfoTick(_Symbol,diagTick) && diagTick.ask>0 && diagTick.bid>0) spreadPoints=(diagTick.ask-diagTick.bid)/_Point;
|
| lines[5]=StringFormat("Спред: %s | лимит: %s | SL-buffer: %d | корзина: %d x %s лота | шаг TP: %d | retry: %dс",spreadPoints<0?"нет котировки":DoubleToString(spreadPoints,1),InpMaxSpreadPoints>0?IntegerToString(InpMaxSpreadPoints):"без лимита",InpSLBufferPoints,g_runtimePositionsCount,DoubleToString(g_runtimePositionLots,4),InpTakeProfitStepPoints,InpRetrySeconds);
|
| lines[6]=StringFormat("Цена входа BUY: %s | SELL: %s",
|
| g_buyEntryLevel>0?DoubleToString(g_buyEntryLevel,_Digits):"нет цены входа",
|
| g_sellEntryLevel>0?DoubleToString(g_sellEntryLevel,_Digits):"нет цены входа");
|
| lines[7]=StringFormat("Последняя вершина: %s | последняя впадина: %s",
|
| g_lastSwingHigh>0?DoubleToString(g_lastSwingHigh,_Digits):"не определена",
|
| g_lastSwingLow>0?DoubleToString(g_lastSwingLow,_Digits):"не определена");
|
| lines[8]=StringFormat("Автозакрытие %02d:00 MSK: %s | охват: все позиции символа",
|
| InpCloseHourMSK,InpCloseAt23MSK?"ВКЛ":"ВЫКЛ");
|
| lines[9]=g_lastCloseInfo;
|
| for(int i=0;i<10;i++) if(ObjectFind(0,g_diagLabels[i])>=0) ObjectSetString(0,g_diagLabels[i],OBJPROP_TEXT,lines[i]);
|
| ChartRedraw(0);
|
| }
|
|
|
| bool CreatePanel()
|
| {
|
| string basePrefix=StringFormat("RenkoEA.%I64d.",ChartID());
|
| g_panelYOffset=0;
|
| int existingPanels=0;
|
| for(int objectIndex=ObjectsTotal(0,-1,-1)-1;objectIndex>=0;--objectIndex)
|
| {
|
| string existingName=ObjectName(0,objectIndex,-1,-1);
|
| if(StringFind(existingName,basePrefix)!=0 || StringLen(existingName)<7) continue;
|
| if(StringSubstr(existingName,StringLen(existingName)-7)==".Status")
|
| existingPanels++;
|
| }
|
| g_panelYOffset=existingPanels*270;
|
| string prefix=StringFormat("RenkoEA.%I64d.%s.",ChartID(),g_instanceTag);
|
| g_buyButton=prefix+"BUY";
|
| g_sellButton=prefix+"SELL";
|
| g_brickEdit=prefix+"BrickEdit";
|
| g_brickApply=prefix+"BrickApply";
|
| g_positionsEdit=prefix+"PositionsEdit";
|
| g_lotsEdit=prefix+"LotsEdit";
|
| g_basketApply=prefix+"BasketApply";
|
| g_fivePositionsButton=prefix+"FivePositions";
|
| g_positionsLabel=prefix+"PositionsLabel";
|
| g_lotsLabel=prefix+"LotsLabel";
|
| g_statusLabel=prefix+"Status";
|
| g_terminalTradeDot=prefix+"TerminalTradeDot";
|
| for(int i=0;i<10;i++) g_diagLabels[i]=prefix+"Diag"+IntegerToString(i);
|
| if(!ObjectCreate(0,g_buyButton,OBJ_BUTTON,0,0,0) &&
|
| ObjectFind(0,g_buyButton)<0) return false;
|
| if(!ObjectCreate(0,g_sellButton,OBJ_BUTTON,0,0,0) &&
|
| ObjectFind(0,g_sellButton)<0) return false;
|
| g_terminalTradeLabel=prefix+"TerminalTradeStatus";
|
| if(!ObjectCreate(0,g_terminalTradeLabel,OBJ_LABEL,0,0,0) && ObjectFind(0,g_terminalTradeLabel)<0) return false;
|
| if(!ObjectCreate(0,g_terminalTradeDot,OBJ_LABEL,0,0,0) && ObjectFind(0,g_terminalTradeDot)<0) return false;
|
| if(!ObjectCreate(0,g_brickEdit,OBJ_EDIT,0,0,0) &&
|
| ObjectFind(0,g_brickEdit)<0) return false;
|
| if(!ObjectCreate(0,g_brickApply,OBJ_BUTTON,0,0,0) &&
|
| ObjectFind(0,g_brickApply)<0) return false;
|
| if(!ObjectCreate(0,g_positionsEdit,OBJ_EDIT,0,0,0) && ObjectFind(0,g_positionsEdit)<0) return false;
|
| if(!ObjectCreate(0,g_lotsEdit,OBJ_EDIT,0,0,0) && ObjectFind(0,g_lotsEdit)<0) return false;
|
| if(!ObjectCreate(0,g_basketApply,OBJ_BUTTON,0,0,0) && ObjectFind(0,g_basketApply)<0) return false;
|
| if(!ObjectCreate(0,g_fivePositionsButton,OBJ_BUTTON,0,0,0) && ObjectFind(0,g_fivePositionsButton)<0) return false;
|
| if(!ObjectCreate(0,g_positionsLabel,OBJ_LABEL,0,0,0) && ObjectFind(0,g_positionsLabel)<0) return false;
|
| if(!ObjectCreate(0,g_lotsLabel,OBJ_LABEL,0,0,0) && ObjectFind(0,g_lotsLabel)<0) return false;
|
| if(!ObjectCreate(0,g_statusLabel,OBJ_LABEL,0,0,0) &&
|
| ObjectFind(0,g_statusLabel)<0) return false;
|
|
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_XDISTANCE,10);
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_YDISTANCE,20+g_panelYOffset);
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_XSIZE,100);
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_YSIZE,26);
|
| ObjectSetInteger(0,g_buyButton,OBJPROP_FONTSIZE,10);
|
|
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_XDISTANCE,116);
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_YDISTANCE,20+g_panelYOffset);
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_XSIZE,100);
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_YSIZE,26);
|
| ObjectSetInteger(0,g_sellButton,OBJPROP_FONTSIZE,10);
|
|
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_XDISTANCE,430);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_YDISTANCE,25+g_panelYOffset);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_FONTSIZE,11);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,g_terminalTradeLabel,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_XDISTANCE,410);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_YDISTANCE,23+g_panelYOffset);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,g_terminalTradeDot,OBJPROP_HIDDEN,true);
|
|
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_XDISTANCE,222);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_YDISTANCE,20+g_panelYOffset);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_XSIZE,75);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_YSIZE,26);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_FONTSIZE,10);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_ALIGN,ALIGN_CENTER);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_COLOR,clrBlack);
|
| ObjectSetInteger(0,g_brickEdit,OBJPROP_BGCOLOR,clrWhite);
|
|
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_XDISTANCE,302);
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_YDISTANCE,20+g_panelYOffset);
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_XSIZE,90);
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_YSIZE,26);
|
| ObjectSetInteger(0,g_brickApply,OBJPROP_FONTSIZE,9);
|
| ObjectSetString(0,g_brickApply,OBJPROP_TEXT,"Применить");
|
|
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_XDISTANCE,10);
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_YDISTANCE,52+g_panelYOffset);
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_XSIZE,88);
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_YSIZE,22);
|
| ObjectSetInteger(0,g_fivePositionsButton,OBJPROP_FONTSIZE,8);
|
| ObjectSetString(0,g_fivePositionsButton,OBJPROP_TEXT,"5 позиций");
|
|
|
| ObjectSetInteger(0,g_positionsLabel,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_positionsLabel,OBJPROP_XDISTANCE,105);
|
| ObjectSetInteger(0,g_positionsLabel,OBJPROP_YDISTANCE,57+g_panelYOffset);
|
| ObjectSetInteger(0,g_positionsLabel,OBJPROP_FONTSIZE,8);
|
| ObjectSetInteger(0,g_positionsLabel,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetString(0,g_positionsLabel,OBJPROP_TEXT,"Позиций");
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_XDISTANCE,153);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_YDISTANCE,52+g_panelYOffset);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_XSIZE,42);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_YSIZE,22);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_FONTSIZE,9);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_ALIGN,ALIGN_CENTER);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_COLOR,clrBlack);
|
| ObjectSetInteger(0,g_positionsEdit,OBJPROP_BGCOLOR,clrWhite);
|
| ObjectSetString(0,g_positionsEdit,OBJPROP_TEXT,IntegerToString(g_runtimePositionsCount));
|
|
|
| ObjectSetInteger(0,g_lotsLabel,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_lotsLabel,OBJPROP_XDISTANCE,205);
|
| ObjectSetInteger(0,g_lotsLabel,OBJPROP_YDISTANCE,57+g_panelYOffset);
|
| ObjectSetInteger(0,g_lotsLabel,OBJPROP_FONTSIZE,8);
|
| ObjectSetInteger(0,g_lotsLabel,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetString(0,g_lotsLabel,OBJPROP_TEXT,"Лот");
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_XDISTANCE,230);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_YDISTANCE,52+g_panelYOffset);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_XSIZE,62);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_YSIZE,22);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_FONTSIZE,9);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_ALIGN,ALIGN_CENTER);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_COLOR,clrBlack);
|
| ObjectSetInteger(0,g_lotsEdit,OBJPROP_BGCOLOR,clrWhite);
|
| ObjectSetString(0,g_lotsEdit,OBJPROP_TEXT,DoubleToString(g_runtimePositionLots,4));
|
|
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_XDISTANCE,300);
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_YDISTANCE,52+g_panelYOffset);
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_XSIZE,90);
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_YSIZE,22);
|
| ObjectSetInteger(0,g_basketApply,OBJPROP_FONTSIZE,8);
|
| ObjectSetString(0,g_basketApply,OBJPROP_TEXT,"Применить");
|
|
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_XDISTANCE,10);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_YDISTANCE,76+g_panelYOffset);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_FONTSIZE,9);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,g_statusLabel,OBJPROP_HIDDEN,true);
|
| for(int i=0;i<10;i++)
|
| {
|
| if(!ObjectCreate(0,g_diagLabels[i],OBJ_LABEL,0,0,0) && ObjectFind(0,g_diagLabels[i])<0) return false;
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_XDISTANCE,10);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_YDISTANCE,96+g_panelYOffset+i*18);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_FONTSIZE,9);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_COLOR,clrDimGray);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_BACK,false);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,g_diagLabels[i],OBJPROP_HIDDEN,true);
|
| }
|
| UpdatePanel();
|
| return true;
|
| }
|
|
|
| void ApplyRuntimeBasketSettings()
|
| {
|
| if(ObjectFind(0,g_positionsEdit)<0 || ObjectFind(0,g_lotsEdit)<0) return;
|
| string countText=ObjectGetString(0,g_positionsEdit,OBJPROP_TEXT);
|
| string lotsText=ObjectGetString(0,g_lotsEdit,OBJPROP_TEXT);
|
| StringTrimLeft(countText); StringTrimRight(countText);
|
| StringTrimLeft(lotsText); StringTrimRight(lotsText);
|
| StringReplace(lotsText,",",".");
|
| long countParsed=StringToInteger(countText);
|
| bool countValid=(countText!="" && countParsed>=1 && countParsed<=100);
|
| for(int i=0;i<StringLen(countText) && countValid;i++)
|
| {
|
| ushort ch=StringGetCharacter(countText,i);
|
| if(ch<'0' || ch>'9') countValid=false;
|
| }
|
| bool lotsValid=(lotsText!="" && StringToDouble(lotsText)>0.0);
|
| int decimalPoints=0;
|
| for(int i=0;i<StringLen(lotsText) && lotsValid;i++)
|
| {
|
| ushort ch=StringGetCharacter(lotsText,i);
|
| if(ch=='.') { decimalPoints++; if(decimalPoints>1) lotsValid=false; }
|
| else if(ch<'0' || ch>'9') lotsValid=false;
|
| }
|
| double parsedLots=StringToDouble(lotsText);
|
| double validLots=ValidVolume(parsedLots);
|
| if(!countValid || !lotsValid || validLots<=0.0 || MathAbs(validLots-parsedLots)>1e-8)
|
| {
|
| g_lastDecision="Параметры корзины не применены: проверьте число позиций и шаг лота брокера";
|
| LogEvent("INVALID_RUNTIME_BASKET_SETTINGS",StringFormat("count=%s lots=%s valid_count=%s valid_lots=%s",countText,lotsText,countValid?"true":"false",DoubleToString(validLots,4)));
|
| UpdatePanel();
|
| return;
|
| }
|
| g_runtimePositionsCount=(int)countParsed;
|
| g_runtimePositionLots=validLots;
|
| bool saved=SavePersistentSettings();
|
| g_lastDecision=StringFormat("Корзина применена: %d позиций x %s лота%s",g_runtimePositionsCount,DoubleToString(g_runtimePositionLots,4),saved?"":"; ошибка сохранения настроек");
|
| LogEvent("RUNTIME_BASKET_SETTINGS_APPLIED",StringFormat("count=%d lots=%s persisted=%s",g_runtimePositionsCount,DoubleToString(g_runtimePositionLots,4),saved?"true":"false"));
|
| UpdatePanel();
|
| }
|
|
|
| void ApplyFivePositionsPreset()
|
| {
|
| g_runtimePositionsCount=5;
|
| if(ObjectFind(0,g_positionsEdit)>=0)
|
| ObjectSetString(0,g_positionsEdit,OBJPROP_TEXT,"5");
|
| bool saved=SavePersistentSettings();
|
| g_lastDecision=StringFormat("Установлено 5 позиций по %s лота%s",DoubleToString(g_runtimePositionLots,4),saved?"":"; ошибка сохранения настроек");
|
| LogEvent("RUNTIME_BASKET_PRESET_APPLIED",StringFormat("count=5 lots=%s persisted=%s",DoubleToString(g_runtimePositionLots,4),saved?"true":"false"));
|
| UpdatePanel();
|
| }
|
|
|
| void ApplyRuntimeBrickSize()
|
| {
|
| if(ObjectFind(0,g_brickEdit)<0) return;
|
| string value=ObjectGetString(0,g_brickEdit,OBJPROP_TEXT);
|
| StringTrimLeft(value); StringTrimRight(value);
|
| long parsed=StringToInteger(value);
|
| bool valid=(value!="" && parsed>=1 && parsed<=100000000);
|
| for(int i=0;i<StringLen(value) && valid;i++)
|
| {
|
| ushort ch=StringGetCharacter(value,i);
|
| if(ch<'0'||ch>'9') valid=false;
|
| }
|
| if(!valid)
|
| {
|
| LogEvent("INVALID_BRICK_SIZE","value="+value);
|
| UpdatePanel();
|
| return;
|
| }
|
| // Сохраняем рабочие параметры до попытки перестройки: при сбое
|
| // новый размер не должен расходиться со старой Renko-структурой.
|
| int previousRuntimeBrickPoints=g_runtimeBrickPoints;
|
| double previousBrickSize=g_brickSize;
|
| bool previousBuyEnabled=g_buyEnabled;
|
| bool previousSellEnabled=g_sellEnabled;
|
| bool previousRestoreBuyEnabled=g_restoreBuyEnabled;
|
| bool previousRestoreSellEnabled=g_restoreSellEnabled;
|
| bool previousRestoreModesPending=g_restoreModesPending;
|
| double previousBuyEntryLevel=g_buyEntryLevel;
|
| double previousSellEntryLevel=g_sellEntryLevel;
|
| double previousBuySetupStop=g_buySetupStop;
|
| double previousSellSetupStop=g_sellSetupStop;
|
| int requestedBrickPoints=(int)parsed;
|
| g_runtimeBrickPoints=requestedBrickPoints;
|
| g_brickSize=(double)g_runtimeBrickPoints*_Point;
|
| // После успешной перестройки повторно активируем только те режимы,
|
| // которые были включены до изменения размера; уровни входа будут рассчитаны заново.
|
| g_restoreBuyEnabled=previousBuyEnabled || previousRestoreBuyEnabled;
|
| g_restoreSellEnabled=previousSellEnabled || previousRestoreSellEnabled;
|
| g_restoreModesPending=(g_restoreBuyEnabled || g_restoreSellEnabled);
|
| ClearEntrySetup(1); ClearEntrySetup(-1);
|
| g_pendingSignal=0; g_pendingStopLevel=0.0;
|
| RebuildHistory();
|
| if(!g_historyReady || g_historyPartial || g_snapshotValid)
|
| {
|
| g_runtimeBrickPoints=previousRuntimeBrickPoints;
|
| g_brickSize=previousBrickSize;
|
| g_buyEnabled=previousBuyEnabled;
|
| g_sellEnabled=previousSellEnabled;
|
| g_restoreBuyEnabled=previousRestoreBuyEnabled;
|
| g_restoreSellEnabled=previousRestoreSellEnabled;
|
| g_restoreModesPending=previousRestoreModesPending;
|
| g_buyEntryLevel=previousBuyEntryLevel;
|
| g_sellEntryLevel=previousSellEntryLevel;
|
| g_buySetupStop=previousBuySetupStop;
|
| g_sellSetupStop=previousSellSetupStop;
|
| g_lastDecision="Размер Renko не применён: история не перестроена полностью";
|
| LogEvent("BRICK_SIZE_CHANGE_ROLLED_BACK",StringFormat("requested=%d restored=%d history_ready=%s partial=%s",
|
| requestedBrickPoints,previousRuntimeBrickPoints,g_historyReady?"true":"false",g_historyPartial?"true":"false"));
|
| UpdatePanel();
|
| return;
|
| }
|
| RestoreSavedModesIfReady();
|
| UpdatePanel();
|
| bool saved=SavePersistentSettings();
|
| if(saved)
|
| LogEvent("BRICK_SIZE_CHANGED",IntegerToString(g_runtimeBrickPoints));
|
| else
|
| LogEvent("BRICK_SIZE_PERSIST_FAILED",IntegerToString(g_runtimeBrickPoints));
|
| }
|
|
|
| datetime MoscowTime()
|
| {
|
| // Москва UTC+3 без сезонного перевода времени.
|
| return (datetime)(TimeGMT()+3*3600);
|
| }
|
|
|
| datetime MoscowDayStart(datetime t)
|
| {
|
| MqlDateTime d;
|
| if(!TimeToStruct(t,d)) return 0;
|
| d.hour=0; d.min=0; d.sec=0;
|
| return StructToTime(d);
|
| }
|
|
|
| bool EnsureLogHandle()
|
| {
|
| if(g_logHandle!=INVALID_HANDLE) return true;
|
| g_logHandle=FileOpen(g_logFile,FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,',');
|
| if(g_logHandle==INVALID_HANDLE) return false;
|
| if(FileSize(g_logHandle)==0)
|
| FileWrite(g_logHandle,"server_time","symbol","event","details");
|
| FileSeek(g_logHandle,0,SEEK_END);
|
| return true;
|
| }
|
|
|
| void CloseLogHandle()
|
| {
|
| if(g_logHandle==INVALID_HANDLE) return;
|
| FileFlush(g_logHandle);
|
| FileClose(g_logHandle);
|
| g_logHandle=INVALID_HANDLE;
|
| }
|
|
|
| // Журнал держит файл открытым: открытие на каждое событие давало лишний I/O.
|
| void LogEvent(string eventName,string details="")
|
| {
|
| string line=TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)+","+_Symbol+","+eventName+","+details;
|
| Print("RenkoGPTBreakoutEA: ",line);
|
| if(!EnsureLogHandle())
|
| {
|
| static ulong lastOpenFailMs=0;
|
| ulong nowMs=GetTickCount64();
|
| if(lastOpenFailMs==0 || nowMs-lastOpenFailMs>=60000)
|
| {
|
| Print("RenkoGPTBreakoutEA: не удалось открыть CSV-журнал; error=",GetLastError());
|
| lastOpenFailMs=nowMs;
|
| }
|
| return;
|
| }
|
| int h=g_logHandle;
|
| if(h==INVALID_HANDLE)
|
| {
|
| Print("RenkoGPTBreakoutEA: не удалось открыть CSV-журнал; error=",GetLastError());
|
| return;
|
| }
|
| if(FileSize(h)==0) FileWrite(h,"server_time","symbol","event","details");
|
| FileSeek(h,0,SEEK_END);
|
| FileWrite(h,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS),_Symbol,eventName,details);
|
| // Файл остаётся открытым; закрытие выполняется в CloseLogHandle().
|
| }
|
|
|
| double NormalizePrice(double price)
|
| {
|
| double tickSize=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
|
| if(tickSize<=0) tickSize=_Point;
|
| return NormalizeDouble(MathRound(price/tickSize)*tickSize,_Digits);
|
| }
|
|
|
| // Для защитного SL округляем только наружу от структурного экстремума:
|
| // BUY — вниз, SELL — вверх. Округление к ближайшему тику могло уменьшать
|
| // фактический буфер InpSLBufferPoints.
|
| double NormalizeStopLoss(const double price,const bool isBuy)
|
| {
|
| double tickSize=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
|
| if(tickSize<=0) tickSize=_Point;
|
| double ticks=price/tickSize;
|
| double rounded=(isBuy ? MathFloor(ticks+1e-10) : MathCeil(ticks-1e-10))*tickSize;
|
| return NormalizeDouble(rounded,_Digits);
|
| }
|
|
|
| bool HasOurPosition()
|
| {
|
| for(int i=PositionsTotal()-1;i>=0;--i)
|
| {
|
| ulong ticket=PositionGetTicket(i);
|
| if(ticket==0) continue;
|
| if(PositionGetString(POSITION_SYMBOL)==_Symbol &&
|
| (ulong)PositionGetInteger(POSITION_MAGIC)==InpMagicNumber)
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
| bool HasAnyPositionOnSymbol()
|
| {
|
| for(int i=PositionsTotal()-1;i>=0;--i)
|
| {
|
| ulong ticket=PositionGetTicket(i);
|
| if(ticket==0) continue;
|
| if(PositionGetString(POSITION_SYMBOL)==_Symbol)
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
| bool TradingHours()
|
| {
|
| MqlDateTime d;
|
| if(!TimeToStruct(MoscowTime(),d)) return false;
|
| return (d.hour>=InpTradeStartHourMSK && d.hour<InpTradeEndHourMSK);
|
| }
|
|
|
| void CloseAtTimeIfRequired()
|
| {
|
| if(!InpCloseAt23MSK) return;
|
| MqlDateTime d;
|
| datetime now=MoscowTime();
|
| if(!TimeToStruct(now,d) || d.hour<InpCloseHourMSK) return;
|
| datetime day=MoscowDayStart(now);
|
| if(day==g_lastCloseCheckDay) return;
|
|
|
| // CloseAtTimeIfRequired вызывается и из OnTick, и из OnTimer.
|
| // Ограничиваем повторы после отказа, не отменяя повторное закрытие открытых позиций.
|
| ulong attemptMs=GetTickCount64();
|
| if(g_lastCloseAttemptMs>0 && attemptMs-g_lastCloseAttemptMs<5000)
|
| return;
|
| g_lastCloseAttemptMs=attemptMs;
|
|
|
| for(int i=PositionsTotal()-1;i>=0;--i)
|
| {
|
| ulong ticket=PositionGetTicket(i);
|
| // По инварианту автозакрытие охватывает все позиции текущего символа,
|
| // независимо от Magic Number.
|
| if(ticket==0 || PositionGetString(POSITION_SYMBOL)!=_Symbol) continue;
|
| ResetLastError();
|
| bool closeSent=g_trade.PositionClose(ticket);
|
| uint closeRc=g_trade.ResultRetcode();
|
| if(!closeSent || (closeRc!=TRADE_RETCODE_DONE && closeRc!=TRADE_RETCODE_DONE_PARTIAL))
|
| LogEvent("CLOSE_23_FAILED",StringFormat("ticket=%I64u sent=%s retcode=%u %s error=%d",
|
| ticket,closeSent?"true":"false",closeRc,g_trade.ResultRetcodeDescription(),GetLastError()));
|
| else
|
| {
|
| bool stillOpen=PositionSelectByTicket(ticket);
|
| LogEvent(stillOpen?"CLOSE_23_NOT_CONFIRMED":"CLOSE_23_CONFIRMED",
|
| StringFormat("ticket=%I64u retcode=%u still_open=%s",ticket,closeRc,stillOpen?"true":"false"));
|
| }
|
| }
|
|
|
| // Не считаем закрытие за день завершённым, пока на символе остаётся
|
| // любая позиция — независимо от Magic Number и источника открытия.
|
| // При временном отказе следующий OnTimer/OnTick повторит попытку.
|
| bool closeTargetRemaining=HasAnyPositionOnSymbol();
|
| if(!closeTargetRemaining)
|
| g_lastCloseCheckDay=day;
|
| }
|
|
|
| void AddLevel(double &arr[],double value)
|
| {
|
| int n=ArraySize(arr);
|
| if(ArrayResize(arr,n+1,32)==n+1) arr[n]=value;
|
| }
|
|
|
| bool AddBrick(datetime t,double o,double c,int dir)
|
| {
|
| // Для торговой структуры достаточно последних кирпичей и агрегированных swings.
|
| // Ограничиваем логическую длину массива, чтобы длинная работа EA не росла бесконечно.
|
| const int MAX_RETAINED_BRICKS=5000;
|
| if(g_count>=MAX_RETAINED_BRICKS)
|
| {
|
| for(int i=1;i<g_count;i++) g_bricks[i-1]=g_bricks[i];
|
| g_count--;
|
| }
|
| // Сначала резервируем память. При ошибке нельзя обновлять swing-состояние
|
| // без соответствующего кирпича: это создало бы фиктивный торговый уровень.
|
| int n=g_count+1;
|
| if(ArrayResize(g_bricks,n,512)!=n)
|
| {
|
| LogEvent("MEMORY_ERROR","brick array resize failed");
|
| return false;
|
| }
|
| bool confirmedHigh=false, confirmedLow=false;
|
| double swingHigh=0.0, swingLow=0.0;
|
| if(g_count>0)
|
| {
|
| Brick prev=g_bricks[g_count-1];
|
| if(prev.direction>0 && dir<0) { confirmedHigh=true; swingHigh=prev.high; }
|
| else if(prev.direction<0 && dir>0) { confirmedLow=true; swingLow=prev.low; }
|
| }
|
| g_bricks[g_count].time=t;
|
| g_bricks[g_count].open=o;
|
| g_bricks[g_count].high=MathMax(o,c);
|
| g_bricks[g_count].low=MathMin(o,c);
|
| g_bricks[g_count].close=c;
|
| g_bricks[g_count].direction=dir;
|
| g_count=n;
|
| if(confirmedHigh)
|
| {
|
| g_lastSwingHigh=swingHigh;
|
| g_latestUpHigh=swingHigh;
|
| g_swingHighBreakoutTriggered=false;
|
| g_prevUpHigh=g_latestUpHigh;
|
| }
|
| else if(confirmedLow)
|
| {
|
| g_lastSwingLow=swingLow;
|
| g_latestDownLow=swingLow;
|
| g_swingLowBreakoutTriggered=false;
|
| g_prevDownLow=g_latestDownLow;
|
| }
|
| return true;
|
| }
|
|
|
| void ProcessBid(datetime t,double bid,bool allowSignal)
|
| {
|
| if(bid<=0 || g_brickSize<=0) return;
|
| if(!g_haveAnchor)
|
| {
|
| g_lastClose=NormalizePrice(bid);
|
| g_lastBid=bid;
|
| g_haveAnchor=true;
|
| return;
|
| }
|
| double previousBid=g_lastBid;
|
| bool changed=true;
|
| int guard=0;
|
| while(changed && guard++<10000)
|
| {
|
| changed=false;
|
| if(g_direction==0)
|
| {
|
| if(bid>=g_lastClose+g_brickSize)
|
| {
|
| double c=g_lastClose+g_brickSize; if(!AddBrick(t,g_lastClose,c,1)) return;
|
| g_lastClose=c; g_direction=1; changed=true;
|
| }
|
| else if(bid<=g_lastClose-g_brickSize)
|
| {
|
| double c=g_lastClose-g_brickSize; if(!AddBrick(t,g_lastClose,c,-1)) return;
|
| g_lastClose=c; g_direction=-1; changed=true;
|
| }
|
| }
|
| else if(g_direction>0)
|
| {
|
| if(bid>=g_lastClose+g_brickSize)
|
| {
|
| double c=g_lastClose+g_brickSize; if(!AddBrick(t,g_lastClose,c,1)) return;
|
| g_lastClose=c; changed=true;
|
| }
|
| else if(bid<=g_lastClose-2.0*g_brickSize)
|
| {
|
| double o=g_lastClose-g_brickSize, c=g_lastClose-2.0*g_brickSize;
|
| if(!AddBrick(t,o,c,-1)) return;
|
| g_lastClose=c; g_direction=-1; changed=true;
|
| }
|
| }
|
| else
|
| {
|
| if(bid<=g_lastClose-g_brickSize)
|
| {
|
| double c=g_lastClose-g_brickSize; if(!AddBrick(t,g_lastClose,c,-1)) return;
|
| g_lastClose=c; changed=true;
|
| }
|
| else if(bid>=g_lastClose+2.0*g_brickSize)
|
| {
|
| double o=g_lastClose+g_brickSize, c=g_lastClose+2.0*g_brickSize;
|
| if(!AddBrick(t,o,c,1)) return;
|
| g_lastClose=c; g_direction=1; changed=true;
|
| }
|
| }
|
| }
|
|
|
| if(changed)
|
| {
|
| g_processBidGuardExceeded=true;
|
| LogEvent("RENKO_PROCESS_GUARD_EXCEEDED",StringFormat("guard=%d bid=%s last_close=%s brick_size=%s allow_signal=%s",guard,DoubleToString(bid,_Digits),DoubleToString(g_lastClose,_Digits),DoubleToString(g_brickSize,_Digits),allowSignal?"true":"false"));
|
| return;
|
| }
|
|
|
| // Уровни входа и SL фиксируются при нажатии кнопки и не следуют за новыми swings.
|
| if(allowSignal && previousBid>0)
|
| {
|
| if(g_buyEnabled && g_buyEntryLevel>0 && bid<=g_buySetupStop)
|
| {
|
| LogEvent("BUY_SETUP_CANCELLED_SL",StringFormat("bid=%s entry=%s sl=%s",DoubleToString(bid,_Digits),DoubleToString(g_buyEntryLevel,_Digits),DoubleToString(g_buySetupStop,_Digits)));
|
| ClearEntrySetup(1);
|
| g_lastDecision="BUY отменён: цена пробила предполагаемый SL до входа";
|
| UpdatePanel();
|
| }
|
| if(g_sellEnabled && g_sellEntryLevel>0 && bid>=g_sellSetupStop)
|
| {
|
| LogEvent("SELL_SETUP_CANCELLED_SL",StringFormat("bid=%s entry=%s sl=%s",DoubleToString(bid,_Digits),DoubleToString(g_sellEntryLevel,_Digits),DoubleToString(g_sellSetupStop,_Digits)));
|
| ClearEntrySetup(-1);
|
| g_lastDecision="SELL отменён: цена пробила предполагаемый SL до входа";
|
| UpdatePanel();
|
| }
|
| if(g_pendingSignal==0 && g_buyEnabled && g_buyEntryLevel>0 && previousBid<g_buyEntryLevel && bid>=g_buyEntryLevel)
|
| {
|
| g_buyBreakLevel=g_buyEntryLevel; g_pendingSignal=1;
|
| g_lastDecision="BUY-пробой зафиксированной цены входа; проверка условий открытия";
|
| g_lastSignalTime=TimeCurrent(); g_pendingStopLevel=g_buySetupStop; // SL фиксируется в момент сигнала
|
| g_lastAttemptTime=0; g_attempts=0; g_swingHighBreakoutTriggered=true;
|
| LogEvent("BUY_SIGNAL",StringFormat("entry=%s sl=%s",DoubleToString(g_buyEntryLevel,_Digits),DoubleToString(g_buySetupStop,_Digits)));
|
| UpdatePanel();
|
| }
|
| if(g_pendingSignal==0 && g_sellEnabled && g_sellEntryLevel>0 && previousBid>g_sellEntryLevel && bid<=g_sellEntryLevel)
|
| {
|
| g_sellBreakLevel=g_sellEntryLevel; g_pendingSignal=-1;
|
| g_lastDecision="SELL-пробой зафиксированной цены входа; проверка условий открытия";
|
| g_lastSignalTime=TimeCurrent(); g_pendingStopLevel=g_sellSetupStop; // SL фиксируется в момент сигнала
|
| g_lastAttemptTime=0; g_attempts=0; g_swingLowBreakoutTriggered=true;
|
| LogEvent("SELL_SIGNAL",StringFormat("entry=%s sl=%s",DoubleToString(g_sellEntryLevel,_Digits),DoubleToString(g_sellSetupStop,_Digits)));
|
| UpdatePanel();
|
| }
|
| }
|
| g_lastBid=bid;
|
| }
|
|
|
| double ValidVolume(double volume)
|
| {
|
| double minv=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
|
| double maxv=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
|
| double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
|
| if(step<=0 || volume<minv || volume>maxv) return 0.0;
|
| // Не подгоняем пользовательский объём вниз: несовместимый шаг
|
| // должен приводить к отказу, а не к скрытому уменьшению позиции.
|
| double steps=volume/step;
|
| double nearestSteps=MathRound(steps);
|
| double v=nearestSteps*step;
|
| if(MathAbs(volume-v)>MathMax(1e-8,step*1e-7)) return 0.0;
|
| if(v<minv || v>maxv) return 0.0;
|
| int digits=2;
|
| if(step<0.01) digits=3;
|
| if(step<0.001) digits=4;
|
| return NormalizeDouble(v,digits);
|
| }
|
|
|
| bool ExecutePendingSignal()
|
| {
|
| if(!g_historyReady || g_liveTickRangePending) return false;
|
| if(g_pendingSignal==0) return false;
|
| bool isBuy=(g_pendingSignal>0);
|
| datetime signalNow=TimeCurrent();
|
| if(g_lastSignalTime<=0 || signalNow-g_lastSignalTime>=InpSignalMaxAgeSeconds)
|
| {
|
| LogEvent("SIGNAL_EXPIRED",StringFormat("side=%s age_seconds=%I64d max_age=%d",
|
| isBuy?"BUY":"SELL",g_lastSignalTime>0?(long)(signalNow-g_lastSignalTime):-1,InpSignalMaxAgeSeconds));
|
| ClearEntrySetup(isBuy?1:-1);
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| g_lastDecision="Сигнал отменён: истёк срок действия";
|
| UpdatePanel();
|
| return false;
|
| }
|
| if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { g_lastDecision="БЛОК: нет соединения с торговым сервером"; UpdatePanel(); return false; }
|
| if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { g_lastDecision="БЛОК: автоторговля терминала MT5 выключена"; UpdatePanel(); return false; }
|
| if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { g_lastDecision="БЛОК: торговля запрещена для этого советника"; UpdatePanel(); return false; }
|
| if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) { g_lastDecision="БЛОК: торговля запрещена для счёта или соединение недоступно"; UpdatePanel(); return false; }
|
| if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) { g_lastDecision="БЛОК: сервер запрещает торговлю советниками на этом счёте"; UpdatePanel(); return false; }
|
| if(g_pendingSignal>0 && !g_buyEnabled)
|
| {
|
| g_lastDecision="BUY-сигнал отменён: BUY-режим выключен";
|
| LogEvent("SIGNAL_CANCELLED_MODE_OFF","BUY");
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| UpdatePanel();
|
| return false;
|
| }
|
| if(g_pendingSignal<0 && !g_sellEnabled)
|
| {
|
| g_lastDecision="SELL-сигнал отменён: SELL-режим выключен";
|
| LogEvent("SIGNAL_CANCELLED_MODE_OFF","SELL");
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| UpdatePanel();
|
| return false;
|
| }
|
| if(!TradingHours())
|
| {
|
| g_lastDecision="Сигнал отменён: вне торгового окна по Москве";
|
| LogEvent("SIGNAL_CANCELLED_OUTSIDE_HOURS",g_pendingSignal>0?"BUY":"SELL");
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| UpdatePanel();
|
| return false;
|
| }
|
| // Повторный вход блокирует только позиция, принадлежащая этому EA
|
| // (совпадают символ и InpMagicNumber), независимо от направления.
|
| if(HasOurPosition())
|
| {
|
| g_lastDecision="Сигнал отменён: у RenkoGPTBreakoutEA уже есть собственная позиция по символу";
|
| LogEvent("SIGNAL_CANCELLED_POSITION_EXISTS",isBuy?"BUY":"SELL");
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| UpdatePanel();
|
| return false;
|
| }
|
| datetime now=TimeCurrent();
|
| if(g_lastAttemptTime>0 && now-g_lastAttemptTime<InpRetrySeconds) { g_lastDecision="Ожидание интервала между попытками"; UpdatePanel(); return false; }
|
| if(InpMaxSpreadPoints>0)
|
| {
|
| MqlTick tick;
|
| if(!SymbolInfoTick(_Symbol,tick)) { g_lastDecision="БЛОК: котировка недоступна"; UpdatePanel(); return false; }
|
| if((tick.ask-tick.bid)/_Point>InpMaxSpreadPoints) { g_lastDecision=StringFormat("БЛОК: спред %.1f > лимита %d points",(tick.ask-tick.bid)/_Point,InpMaxSpreadPoints); UpdatePanel(); return false; }
|
| }
|
| double volume=ValidVolume(g_runtimePositionLots);
|
| if(g_runtimePositionsCount<1 || g_runtimePositionsCount>100 || volume<=0)
|
| {
|
| g_lastDecision="ОТКАЗ: количество позиций или объём не соответствует ограничениям";
|
| LogEvent("INVALID_BASKET_PARAMETERS",StringFormat("count=%d lots=%s",g_runtimePositionsCount,DoubleToString(g_runtimePositionLots,4)));
|
| g_pendingSignal=0; g_pendingStopLevel=0.0; UpdatePanel(); return false;
|
| }
|
| if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
|
| {
|
| g_lastDecision="БЛОК: для отдельных позиций нужен счёт MT5 Hedging";
|
| LogEvent("BASKET_BLOCKED_ACCOUNT_MODE",IntegerToString((int)AccountInfoInteger(ACCOUNT_MARGIN_MODE)));
|
| g_pendingSignal=0; UpdatePanel(); return false;
|
| }
|
|
|
| MqlTick tick;
|
| if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0 || tick.ask<=0) { g_lastDecision="БЛОК: котировка не получена перед отправкой"; UpdatePanel(); return false; }
|
| double sl=0.0;
|
| if(isBuy)
|
| {
|
| if(g_buySetupStop<=0) { g_lastDecision="BUY отменён: сохранённый SL отсутствует"; LogEvent("NO_SNAPSHOT_LOW","BUY cancelled"); ClearEntrySetup(1); g_pendingSignal=0; UpdatePanel(); return false; }
|
| sl=(g_pendingStopLevel>0.0)?g_pendingStopLevel:g_buySetupStop; // SL, зафиксированный при сигнале
|
| }
|
| else
|
| {
|
| if(g_sellSetupStop<=0) { g_lastDecision="SELL отменён: сохранённый SL отсутствует"; LogEvent("NO_STRUCTURAL_HIGH","SELL cancelled"); ClearEntrySetup(-1); g_pendingSignal=0; UpdatePanel(); return false; }
|
| sl=(g_pendingStopLevel>0.0)?g_pendingStopLevel:g_sellSetupStop; // SL, зафиксированный при сигнале
|
| }
|
|
|
| long stops=SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
|
| long freeze=SymbolInfoInteger(_Symbol,SYMBOL_TRADE_FREEZE_LEVEL);
|
| // Freeze level запрещает правку SL/TP вблизи рынка: берём консервативный максимум.
|
| double minDistance=(double)MathMax(stops,freeze)*_Point;
|
| // minDistance рассчитан выше как максимум из stops level и freeze level.
|
| double basePrice=isBuy?tick.ask:tick.bid;
|
| double firstTp=NormalizePrice(basePrice+(isBuy?1.0:-1.0)*InpTakeProfitStepPoints*_Point);
|
| if((isBuy && (sl>=tick.bid || tick.bid-sl<minDistance || firstTp<=tick.ask || firstTp-tick.ask<minDistance)) ||
|
| (!isBuy && (sl<=tick.ask || sl-tick.ask<minDistance || firstTp>=tick.bid || tick.bid-firstTp<minDistance)))
|
| {
|
| g_lastDecision=StringFormat("ОТКАЗ: недопустимы SL/TP; SL=%s первый TP=%s stops=%d",DoubleToString(sl,_Digits),DoubleToString(firstTp,_Digits),(int)stops);
|
| LogEvent("INVALID_SL_TP",StringFormat("side=%s sl=%s first_tp=%s stops=%d",isBuy?"BUY":"SELL",DoubleToString(sl,_Digits),DoubleToString(firstTp,_Digits),(int)stops));
|
| g_lastAttemptTime=now; g_attempts++;
|
| if(g_attempts>=10) { g_pendingSignal=0; LogEvent("SIGNAL_ABANDONED","invalid SL/TP after 10 attempts"); }
|
| UpdatePanel(); return false;
|
| }
|
|
|
| g_trade.SetExpertMagicNumber(InpMagicNumber);
|
| g_trade.SetDeviationInPoints(InpDeviationPoints);
|
| // Маржа: проверяем всю корзину до первой заявки, иначе позиции откроются частично.
|
| ENUM_ORDER_TYPE marginType=isBuy?ORDER_TYPE_BUY:ORDER_TYPE_SELL;
|
| double basketVolume=volume*(double)g_runtimePositionsCount;
|
| double basketMargin=0.0;
|
| if(!OrderCalcMargin(marginType,_Symbol,basketVolume,basePrice,basketMargin) || basketMargin<=0.0)
|
| {
|
| g_lastDecision="БЛОК: не удалось оценить требуемую маржу";
|
| LogEvent("MARGIN_CALC_FAILED",StringFormat("volume=%s price=%s error=%d",DoubleToString(basketVolume,4),DoubleToString(basePrice,_Digits),GetLastError()));
|
| // Ограничиваем повторные расчёты интервалом InpRetrySeconds.
|
| g_lastAttemptTime=now;
|
| g_attempts++;
|
| UpdatePanel(); return false;
|
| }
|
| double freeMarginNow=AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
| double marginNeeded=basketMargin*MARGIN_SAFETY_RATIO;
|
| if(freeMarginNow<marginNeeded)
|
| {
|
| g_lastDecision=StringFormat("БЛОК: свободной маржи %s меньше требуемой %s (корзина %d x %s лота)",
|
| DoubleToString(freeMarginNow,2),DoubleToString(marginNeeded,2),
|
| g_runtimePositionsCount,DoubleToString(volume,4));
|
| LogEvent("MARGIN_INSUFFICIENT",StringFormat("free=%s required=%s volume=%s count=%d",
|
| DoubleToString(freeMarginNow,2),DoubleToString(marginNeeded,2),
|
| DoubleToString(basketVolume,4),g_runtimePositionsCount));
|
| g_lastAttemptTime=now; g_attempts++;
|
| UpdatePanel(); return false;
|
| }
|
| g_trade.SetTypeFillingBySymbol(_Symbol);
|
| int opened=0;
|
| bool basketComplete=true;
|
| for(int basketIndex=0;basketIndex<g_runtimePositionsCount;basketIndex++)
|
| {
|
| if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0 || tick.ask<=0)
|
| {
|
| basketComplete=false;
|
| LogEvent("BASKET_STOPPED_NO_QUOTE",StringFormat("opened=%d requested=%d",opened,g_runtimePositionsCount));
|
| break;
|
| }
|
| // Повторная проверка маржи: свободная маржа может измениться между заявками.
|
| double provisionalMarketPrice=isBuy?tick.ask:tick.bid;
|
| double orderMargin=0.0;
|
| if(!OrderCalcMargin(marginType,_Symbol,volume,provisionalMarketPrice,orderMargin) || orderMargin<=0.0 ||
|
| AccountInfoDouble(ACCOUNT_MARGIN_FREE)<orderMargin*MARGIN_SAFETY_RATIO)
|
| {
|
| basketComplete=false;
|
| LogEvent("BASKET_STOPPED_MARGIN",StringFormat("opened=%d requested=%d free=%s order_margin=%s",
|
| opened,g_runtimePositionsCount,DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE),2),DoubleToString(orderMargin,2)));
|
| break;
|
| }
|
| double provisionalPrice=isBuy?tick.ask:tick.bid;
|
| double provisionalTp=NormalizePrice(provisionalPrice+(isBuy?1.0:-1.0)*InpTakeProfitStepPoints*(basketIndex+1)*_Point);
|
| ResetLastError();
|
| bool sent=isBuy ? g_trade.Buy(volume,_Symbol,0.0,sl,provisionalTp,"RenkoGPTBreakoutEA BUY")
|
| : g_trade.Sell(volume,_Symbol,0.0,sl,provisionalTp,"RenkoGPTBreakoutEA SELL");
|
| uint rc=g_trade.ResultRetcode();
|
| g_lastAttemptTime=TimeCurrent();
|
| g_attempts++;
|
| if(!sent || (rc!=TRADE_RETCODE_DONE && rc!=TRADE_RETCODE_DONE_PARTIAL))
|
| {
|
| LogEvent("BASKET_ORDER_UNCONFIRMED",StringFormat("index=%d requested=%d sent=%s retcode=%u order=%I64u deal=%I64u description=%s error=%d",
|
| basketIndex+1,g_runtimePositionsCount,sent?"true":"false",rc,g_trade.ResultOrder(),g_trade.ResultDeal(),g_trade.ResultRetcodeDescription(),GetLastError()));
|
| basketComplete=false;
|
| break; // Не повторяем заявку вслепую при неоднозначном результате.
|
| }
|
|
|
| ulong deal=g_trade.ResultDeal();
|
| double fillPrice=g_trade.ResultPrice();
|
| double filledVolume=g_trade.ResultVolume();
|
| ulong positionId=0;
|
| if(deal>0 && HistoryDealSelect(deal))
|
| positionId=(ulong)HistoryDealGetInteger(deal,DEAL_POSITION_ID);
|
| ulong positionTicket=0;
|
| for(int p=PositionsTotal()-1;p>=0;--p)
|
| {
|
| ulong candidate=PositionGetTicket(p);
|
| if(candidate==0) continue;
|
| if(PositionGetString(POSITION_SYMBOL)==_Symbol &&
|
| (ulong)PositionGetInteger(POSITION_MAGIC)==InpMagicNumber &&
|
| (ulong)PositionGetInteger(POSITION_IDENTIFIER)==positionId)
|
| { positionTicket=candidate; break; }
|
| }
|
| if(fillPrice<=0.0 || positionId==0 || positionTicket==0)
|
| {
|
| LogEvent("BASKET_FILL_RECONCILE_REQUIRED",StringFormat("index=%d deal=%I64u position_id=%I64u ticket=%I64u fill=%s",
|
| basketIndex+1,deal,positionId,positionTicket,DoubleToString(fillPrice,_Digits)));
|
| basketComplete=false;
|
| break;
|
| }
|
| double actualTp=NormalizePrice(fillPrice+(isBuy?1.0:-1.0)*InpTakeProfitStepPoints*(basketIndex+1)*_Point);
|
| bool modifyRequired=true;
|
| double freezeDistance=(double)freeze*_Point;
|
| double currentMarket=isBuy?SymbolInfoDouble(_Symbol,SYMBOL_BID):SymbolInfoDouble(_Symbol,SYMBOL_ASK);
|
| if(freezeDistance>0.0 && currentMarket>0.0 && MathAbs(actualTp-currentMarket)<freezeDistance)
|
| {
|
| // Freeze level запрещает правку TP вблизи рынка: оставляем TP, выставленный
|
| // вместе с позицией, вместо рискованного принудительного закрытия.
|
| modifyRequired=false;
|
| LogEvent("BASKET_TP_MODIFY_SKIPPED_FREEZE",StringFormat("index=%d ticket=%I64u tp=%s market=%s freeze=%d",
|
| basketIndex+1,positionTicket,DoubleToString(actualTp,_Digits),DoubleToString(currentMarket,_Digits),(int)freeze));
|
| }
|
| ResetLastError();
|
| if(modifyRequired &&
|
| ((!g_trade.PositionModify(positionTicket,sl,actualTp) &&
|
| g_trade.ResultRetcode()!=TRADE_RETCODE_NO_CHANGES) ||
|
| (g_trade.ResultRetcode()!=TRADE_RETCODE_DONE &&
|
| g_trade.ResultRetcode()!=TRADE_RETCODE_NO_CHANGES)))
|
| {
|
| LogEvent("BASKET_TP_MODIFY_FAILED",StringFormat("index=%d ticket=%I64u fill=%s tp=%s retcode=%u description=%s",
|
| basketIndex+1,positionTicket,DoubleToString(fillPrice,_Digits),DoubleToString(actualTp,_Digits),g_trade.ResultRetcode(),g_trade.ResultRetcodeDescription()));
|
| // Не оставляем позицию с предварительным TP намеренно: пытаемся закрыть её.
|
| ResetLastError();
|
| bool emergencyCloseSent=g_trade.PositionClose(positionTicket);
|
| uint emergencyCloseRc=g_trade.ResultRetcode();
|
| bool emergencyStillOpen=PositionSelectByTicket(positionTicket);
|
| LogEvent("BASKET_TP_MODIFY_EMERGENCY_CLOSE",StringFormat("ticket=%I64u sent=%s retcode=%u description=%s still_open=%s",
|
| positionTicket,emergencyCloseSent?"true":"false",emergencyCloseRc,g_trade.ResultRetcodeDescription(),
|
| emergencyStillOpen?"true":"false"));
|
| if(emergencyStillOpen && PositionGetString(POSITION_SYMBOL)==_Symbol &&
|
| (ulong)PositionGetInteger(POSITION_MAGIC)==InpMagicNumber)
|
| {
|
| g_emergencyCloseTicket=positionTicket;
|
| g_emergencyCloseLastAttemptMs=GetTickCount64();
|
| PersistEmergencyCloseTicket();
|
| LogEvent("EMERGENCY_CLOSE_QUEUED",StringFormat("ticket=%I64u retry_interval_ms=5000",positionTicket));
|
| }
|
| basketComplete=false;
|
| break;
|
| }
|
| // Подтверждение retcode не гарантирует, что сервер сохранил защитные SL и TP.
|
| bool protectionOk=false;
|
| double confirmedSl=0.0, confirmedTp=0.0;
|
| if(PositionSelectByTicket(positionTicket))
|
| {
|
| confirmedSl=PositionGetDouble(POSITION_SL);
|
| confirmedTp=PositionGetDouble(POSITION_TP);
|
| double tickSize=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
|
| if(tickSize<=0.0) tickSize=_Point;
|
| bool slOk=(confirmedSl>0.0 && MathAbs(confirmedSl-sl)<=tickSize*0.51);
|
| bool tpOk=(confirmedTp>0.0 && (!modifyRequired || MathAbs(confirmedTp-actualTp)<=tickSize*0.51));
|
| protectionOk=(slOk && tpOk);
|
| }
|
| if(!protectionOk)
|
| {
|
| LogEvent("BASKET_PROTECTION_VERIFY_FAILED",StringFormat("index=%d ticket=%I64u expected_sl=%s actual_sl=%s expected_tp=%s actual_tp=%s modify_required=%s",
|
| basketIndex+1,positionTicket,DoubleToString(sl,_Digits),DoubleToString(confirmedSl,_Digits),
|
| DoubleToString(actualTp,_Digits),DoubleToString(confirmedTp,_Digits),modifyRequired?"true":"false"));
|
| ResetLastError();
|
| bool closeSent=g_trade.PositionClose(positionTicket);
|
| uint closeRc=g_trade.ResultRetcode();
|
| bool stillOpen=PositionSelectByTicket(positionTicket);
|
| LogEvent("BASKET_PROTECTION_EMERGENCY_CLOSE",StringFormat("ticket=%I64u sent=%s retcode=%u still_open=%s",
|
| positionTicket,closeSent?"true":"false",closeRc,stillOpen?"true":"false"));
|
| if(stillOpen && PositionGetString(POSITION_SYMBOL)==_Symbol &&
|
| (ulong)PositionGetInteger(POSITION_MAGIC)==InpMagicNumber)
|
| {
|
| g_emergencyCloseTicket=positionTicket;
|
| g_emergencyCloseLastAttemptMs=GetTickCount64();
|
| PersistEmergencyCloseTicket();
|
| LogEvent("EMERGENCY_CLOSE_QUEUED",StringFormat("ticket=%I64u reason=protection_verify_failed retry_interval_ms=5000",positionTicket));
|
| }
|
| basketComplete=false;
|
| break;
|
| }
|
| opened++;
|
| LogEvent("BASKET_POSITION_OPENED",StringFormat("index=%d/%d ticket=%I64u deal=%I64u volume=%s fill=%s sl=%s tp=%s retcode=%u",
|
| basketIndex+1,g_runtimePositionsCount,positionTicket,deal,DoubleToString(filledVolume,4),
|
| DoubleToString(fillPrice,_Digits),DoubleToString(sl,_Digits),DoubleToString(actualTp,_Digits),rc));
|
| if(rc==TRADE_RETCODE_DONE_PARTIAL || filledVolume+1e-8<volume)
|
| {
|
| basketComplete=false;
|
| LogEvent("BASKET_PARTIAL_FILL",StringFormat("index=%d requested_volume=%s actual_volume=%s; further orders stopped",
|
| basketIndex+1,DoubleToString(volume,4),DoubleToString(filledVolume,4)));
|
| break;
|
| }
|
| }
|
|
|
| g_lastDecision=StringFormat("%s: открыто %d из %d отдельных позиций%s",
|
| isBuy?"BUY":"SELL",opened,g_runtimePositionsCount,basketComplete?"":"; корзина неполная, автоматический повтор запрещён");
|
| LogEvent(basketComplete?"BASKET_OPENED":"BASKET_INCOMPLETE",StringFormat("side=%s opened=%d requested=%d",isBuy?"BUY":"SELL",opened,g_runtimePositionsCount));
|
| ClearEntrySetup(isBuy?1:-1);
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| UpdatePanel();
|
| return opened>0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Снимок и откат состояния Renko |
|
| //| Неудачная пересборка истории не должна стирать уже построенные |
|
| //| кирпичи, уровни и активные режимы BUY/SELL. |
|
| //+------------------------------------------------------------------+
|
| void SaveHistorySnapshot()
|
| {
|
| g_snapshotValid=false;
|
| g_snapshotCount=g_count;
|
| if(g_count>0 && ArrayResize(g_snapshotBricks,g_count,512)!=g_count)
|
| {
|
| LogEvent("SNAPSHOT_FAILED","bricks array resize failed");
|
| g_snapshotCount=0;
|
| return;
|
| }
|
| for(int i=0;i<g_count;i++) g_snapshotBricks[i]=g_bricks[i];
|
| g_snapshotBrickSize=g_brickSize;
|
| g_snapshotLastClose=g_lastClose;
|
| g_snapshotLastBid=g_lastBid;
|
| g_snapshotDirection=g_direction;
|
| g_snapshotHaveAnchor=g_haveAnchor;
|
| g_snapshotLastTickMsc=g_lastTickMsc;
|
| g_snapshotTicksAtLastMsc=g_ticksProcessedAtLastMsc;
|
| g_snapshotLastSwingHigh=g_lastSwingHigh;
|
| g_snapshotLastSwingLow=g_lastSwingLow;
|
| g_snapshotPrevDownLow=g_prevDownLow;
|
| g_snapshotLatestDownLow=g_latestDownLow;
|
| g_snapshotPrevUpHigh=g_prevUpHigh;
|
| g_snapshotLatestUpHigh=g_latestUpHigh;
|
| g_snapshotSwingHighTriggered=g_swingHighBreakoutTriggered;
|
| g_snapshotSwingLowTriggered=g_swingLowBreakoutTriggered;
|
| g_snapshotBuyEntry=g_buyEntryLevel;
|
| g_snapshotSellEntry=g_sellEntryLevel;
|
| g_snapshotBuyStop=g_buySetupStop;
|
| g_snapshotSellStop=g_sellSetupStop;
|
| g_snapshotBuyEnabled=g_buyEnabled;
|
| g_snapshotSellEnabled=g_sellEnabled;
|
| g_snapshotPendingSignal=g_pendingSignal;
|
| g_snapshotHistoryReady=g_historyReady;
|
| g_snapshotHistoryPartial=g_historyPartial;
|
| g_snapshotValid=true;
|
| }
|
|
|
| void RestoreHistorySnapshot()
|
| {
|
| if(!g_snapshotValid) return;
|
| if(g_snapshotCount>0 && ArrayResize(g_bricks,g_snapshotCount,512)!=g_snapshotCount)
|
| {
|
| LogEvent("SNAPSHOT_RESTORE_FAILED","bricks array resize failed");
|
| return;
|
| }
|
| for(int i=0;i<g_snapshotCount;i++) g_bricks[i]=g_snapshotBricks[i];
|
| g_count=g_snapshotCount;
|
| g_brickSize=g_snapshotBrickSize;
|
| g_lastClose=g_snapshotLastClose;
|
| g_lastBid=g_snapshotLastBid;
|
| g_direction=g_snapshotDirection;
|
| g_haveAnchor=g_snapshotHaveAnchor;
|
| g_lastTickMsc=g_snapshotLastTickMsc;
|
| g_ticksProcessedAtLastMsc=g_snapshotTicksAtLastMsc;
|
| g_lastSwingHigh=g_snapshotLastSwingHigh;
|
| g_lastSwingLow=g_snapshotLastSwingLow;
|
| g_prevDownLow=g_snapshotPrevDownLow;
|
| g_latestDownLow=g_snapshotLatestDownLow;
|
| g_prevUpHigh=g_snapshotPrevUpHigh;
|
| g_latestUpHigh=g_snapshotLatestUpHigh;
|
| g_swingHighBreakoutTriggered=g_snapshotSwingHighTriggered;
|
| g_swingLowBreakoutTriggered=g_snapshotSwingLowTriggered;
|
| g_buyEntryLevel=g_snapshotBuyEntry;
|
| g_sellEntryLevel=g_snapshotSellEntry;
|
| g_buySetupStop=g_snapshotBuyStop;
|
| g_sellSetupStop=g_snapshotSellStop;
|
| g_buyEnabled=g_snapshotBuyEnabled;
|
| g_sellEnabled=g_snapshotSellEnabled;
|
| g_pendingSignal=g_snapshotPendingSignal;
|
| g_historyReady=g_snapshotHistoryReady;
|
| g_historyPartial=g_snapshotHistoryPartial;
|
| g_liveTickRangePending=false;
|
| LogEvent("HISTORY_SNAPSHOT_RESTORED",StringFormat("bricks=%d ready=%s",g_count,g_historyReady?"true":"false"));
|
| }
|
|
|
| // Проверка свежей истории до разрушения текущего состояния:
|
| // если терминал ещё не синхронизировал тики (4403), пересборку откладываем.
|
| bool ProbeRecentTickHistory()
|
| {
|
| datetime probeEnd=TimeCurrent();
|
| if(probeEnd<=0) probeEnd=TimeTradeServer();
|
| datetime probeFrom=probeEnd-HISTORY_PROBE_SECONDS;
|
| MqlTick probe[];
|
| ResetLastError();
|
| int copied=CopyTicksRange(_Symbol,probe,COPY_TICKS_INFO,(ulong)probeFrom*1000,(ulong)probeEnd*1000);
|
| int error=GetLastError();
|
| if(copied<0 || error!=0)
|
| {
|
| LogEvent(error==4403?"HISTORY_PROBE_TIMEOUT":"HISTORY_PROBE_FAILED",
|
| StringFormat("from=%s to=%s copied=%d error=%d",
|
| TimeToString(probeFrom,TIME_DATE|TIME_MINUTES),
|
| TimeToString(probeEnd,TIME_DATE|TIME_MINUTES),copied,error));
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| void RebuildHistory()
|
| {
|
| g_lastHistoryAttemptMs=GetTickCount64();
|
| g_processBidGuardExceeded=false;
|
| // 1) Снимок текущего состояния: при неудаче откатываемся, а не теряем кирпичи и режимы.
|
| SaveHistorySnapshot();
|
| // 2) Проверка свежей истории до разрушения состояния: терминал может отдавать 4403.
|
| if(!ProbeRecentTickHistory())
|
| {
|
| RestoreHistorySnapshot();
|
| g_historyRetryDelayMs=(ulong)MathMin((ulong)HISTORY_RETRY_MAX_MS,g_historyRetryDelayMs+g_historyRetryDelayMs);
|
| g_lastDecision="История тиков ещё не синхронизирована; торговля заблокирована, повтор позже";
|
| return;
|
| }
|
| // Fail closed: после начала перестроения старая готовность и курсор тиков недействительны.
|
| g_historyReady=false;
|
| g_liveTickRangePending=false;
|
| g_lastTickMsc=0;
|
| g_ticksProcessedAtLastMsc=0;
|
| g_lastBid=0.0;
|
| ArrayResize(g_bricks,0); g_count=0;
|
| g_lastClose=0; g_lastBid=0; g_direction=0; g_haveAnchor=false;
|
| g_lastSwingHigh=0; g_lastSwingLow=0; g_prevDownLow=0; g_latestDownLow=0;
|
| g_prevUpHigh=0; g_latestUpHigh=0;
|
| ArrayResize(g_downHighs,0); ArrayResize(g_upLows,0);
|
| g_swingHighBreakoutTriggered=false; g_swingLowBreakoutTriggered=false;
|
| g_buyEnabled=false; g_sellEnabled=false;
|
| g_buyEntryLevel=0.0; g_sellEntryLevel=0.0;
|
| g_buySetupStop=0.0; g_sellSetupStop=0.0;
|
| g_pendingSignal=0; g_pendingStopLevel=0.0;
|
| int days=MathMax(1,MathMin(InpHistoryDays,365));
|
| int chunkSeconds=(int)HISTORY_CHUNK_SECONDS;
|
| int chunks=(int)(((long)days*86400+chunkSeconds-1)/chunkSeconds);
|
| ulong buildStartedMs=GetTickCount64();
|
| bool budgetExhausted=false;
|
| datetime historyEnd=TimeCurrent();
|
| int totalTicksCopied=0;
|
| int failedChunks=0;
|
| // Загружаем историю по одному дню, а не одним массивом на 365 дней:
|
| // это ограничивает пиковое потребление памяти.
|
| for(int day=chunks-1;day>=0;--day) // чанки по HISTORY_CHUNK_SECONDS, от старых к новым
|
| {
|
| // Защита OnInit от слишком долгой пересборки истории.
|
| if(IsStopped() || GetTickCount64()-buildStartedMs>=HISTORY_INIT_BUDGET_MS)
|
| {
|
| budgetExhausted=true;
|
| LogEvent("HISTORY_BUILD_BUDGET","rebuild stopped by time budget; will retry later");
|
| break;
|
| }
|
| datetime from=historyEnd-(datetime)((day+1)*chunkSeconds);
|
| datetime to=from+chunkSeconds;
|
| if(to>historyEnd) to=historyEnd;
|
| MqlTick ticks[];
|
| ResetLastError();
|
| int copied=CopyTicksRange(_Symbol,ticks,COPY_TICKS_INFO,
|
| (ulong)from*1000,(ulong)to*1000-1);
|
| int error=GetLastError();
|
| // Любой код ошибки означает, что полнота диапазона не подтверждена.
|
| // При 4403 API может вернуть доступную часть; её нельзя считать полным днём.
|
| if(copied<0 || error!=0)
|
| {
|
| LogEvent(error==4403 ? "HISTORY_RANGE_TIMEOUT" : "HISTORY_RANGE_FAILED",
|
| StringFormat("from=%s to=%s copied=%d error=%d",
|
| TimeToString(from,TIME_DATE|TIME_MINUTES),
|
| TimeToString(to,TIME_DATE|TIME_MINUTES),copied,error));
|
| // Не подтверждаем готовность истории, если хотя бы один диапазон
|
| // не был получен полностью. Повторное восстановление начнётся с нуля.
|
| failedChunks++;
|
| continue;
|
| }
|
| totalTicksCopied+=copied;
|
| for(int i=0;i<copied;i++)
|
| {
|
| if(ticks[i].time_msc>g_lastTickMsc)
|
| {
|
| g_lastTickMsc=ticks[i].time_msc;
|
| g_ticksProcessedAtLastMsc=1;
|
| }
|
| else if(ticks[i].time_msc==g_lastTickMsc)
|
| g_ticksProcessedAtLastMsc++;
|
| if(ticks[i].bid<=0) continue;
|
| ProcessBid((datetime)ticks[i].time,ticks[i].bid,false);
|
| if(g_processBidGuardExceeded)
|
| {
|
| failedChunks++;
|
| // Не используем общий budget-rollback: он мог бы восстановить
|
| // старый флаг historyReady и разрешить торговлю на неполной структуре.
|
| LogEvent("HISTORY_BUILD_RENKO_GUARD","history rebuild aborted because one tick exceeded the brick processing guard");
|
| break;
|
| }
|
| }
|
| if(g_processBidGuardExceeded) break;
|
| }
|
| if(totalTicksCopied<=0 || budgetExhausted)
|
| {
|
| LogEvent("HISTORY_UNAVAILABLE",StringFormat("ticks=%d failed_chunks=%d budget_exhausted=%s",totalTicksCopied,failedChunks,budgetExhausted?"true":"false"));
|
| // Откат: ранее построенная структура и режимы сохраняются, торговля остаётся заблокированной.
|
| RestoreHistorySnapshot();
|
| g_historyRetryDelayMs=(ulong)MathMin((ulong)HISTORY_RETRY_MAX_MS,g_historyRetryDelayMs+g_historyRetryDelayMs);
|
| g_lastDecision="История тиков недоступна или неполна; торговля заблокирована, повтор позже";
|
| return;
|
| }
|
| // Неполная история не является готовой: не разрешаем торговлю и
|
| // повторяем полную загрузку по таймеру, сохраняя желаемые режимы отдельно.
|
| if(failedChunks>0)
|
| {
|
| g_historyReady=false;
|
| g_historyPartial=true;
|
| g_buyEnabled=false;
|
| g_sellEnabled=false;
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| g_historyRetryDelayMs=(ulong)HISTORY_RETRY_MIN_MS;
|
| g_lastDecision="История неполна; торговля заблокирована до полной пересборки";
|
| LogEvent("HISTORY_REBUILT_PARTIAL",
|
| StringFormat("ticks=%d bricks=%d failed_chunks=%d; trading blocked",totalTicksCopied,g_count,failedChunks));
|
| return;
|
| }
|
| // Сигналы из истории не торгуются: оставляем последний исторический Bid
|
| // как предыдущую котировку, чтобы не создавать искусственный переход через ноль.
|
| g_historyReady=true;
|
| g_historyPartial=false;
|
| g_historyRetryDelayMs=(ulong)HISTORY_RETRY_MIN_MS;
|
| LogEvent("HISTORY_REBUILT",
|
| StringFormat("ticks=%d bricks=%d failed_chunks=0",totalTicksCopied,g_count));
|
| g_snapshotValid=false; // Пересборка успешна: откат больше не нужен.
|
| }
|
|
|
| int OnInit()
|
| {
|
| if(InpBrickPoints<1 || InpPositionsCount<1 || InpPositionsCount>100 || InpPositionLots<=0 || InpTakeProfitStepPoints<1 || InpSLBufferPoints<0 ||
|
| InpRetrySeconds<1 || InpSignalMaxAgeSeconds<1 || InpHistoryDays<1 || InpDeviationPoints<0 || InpMaxSpreadPoints<0 || InpTradeStartHourMSK<0 ||
|
| InpTradeStartHourMSK>23 || InpTradeEndHourMSK<1 || InpTradeEndHourMSK>24 ||
|
| InpCloseHourMSK<0 || InpCloseHourMSK>23 || InpTradeStartHourMSK>=InpTradeEndHourMSK)
|
| return INIT_PARAMETERS_INCORRECT;
|
| g_runtimeBrickPoints=InpBrickPoints;
|
| g_brickSize=g_runtimeBrickPoints*_Point;
|
| // Сначала восстановить структуру Renko; режим нельзя включать без валидных уровней.
|
| g_buyEnabled=false;
|
| g_sellEnabled=false;
|
| g_instanceTag=StringFormat("%I64d",(long)GetMicrosecondCount());
|
| g_logFile=InpErrorLogFile;
|
| if(StringLen(InpInstanceId)<1 || StringLen(InpInstanceId)>32)
|
| {
|
| Print("RenkoGPTBreakoutEA: InpInstanceId должен содержать от 1 до 32 символов");
|
| return INIT_PARAMETERS_INCORRECT;
|
| }
|
| LoadPersistentSettings();
|
| double emergencyTicketValue=0.0;
|
| if(LoadPersistentValue("EmergencyCloseTicket",emergencyTicketValue) &&
|
| emergencyTicketValue>=0.0 && emergencyTicketValue<=9007199254740991.0 &&
|
| MathFloor(emergencyTicketValue)==emergencyTicketValue)
|
| {
|
| g_emergencyCloseTicket=(ulong)emergencyTicketValue;
|
| if(g_emergencyCloseTicket>0)
|
| LogEvent("EMERGENCY_CLOSE_RESTORED",StringFormat("ticket=%I64u",g_emergencyCloseTicket));
|
| }
|
| else if(emergencyTicketValue!=0.0)
|
| LogEvent("EMERGENCY_CLOSE_PERSIST_INVALID",DoubleToString(emergencyTicketValue,0));
|
| g_restoreModesPending=true;
|
| g_persistenceReady=true;
|
| g_trade.SetExpertMagicNumber(InpMagicNumber);
|
| g_trade.SetDeviationInPoints(InpDeviationPoints);
|
| ResetLastError();
|
| if(!EventSetTimer(1))
|
| {
|
| LogEvent("TIMER_START_FAILED",StringFormat("error=%d",GetLastError()));
|
| return INIT_FAILED;
|
| }
|
| if(!CreatePanel())
|
| LogEvent("PANEL_CREATE_FAILED","one or more chart controls could not be created");
|
| RebuildHistory();
|
| if(g_historyReady)
|
| {
|
| MqlTick initialTick;
|
| if(SymbolInfoTick(_Symbol,initialTick) && initialTick.bid>0 && ProcessLiveTickRange(initialTick))
|
| RestoreSavedModesIfReady();
|
| }
|
| if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
|
| LogEvent("ACCOUNT_MODE_WARNING","Для корзины из отдельных позиций требуется режим MT5 Hedging");
|
| LogEvent("EA_BUILD_INFO","version=1.08; renko_engine=self-contained; basket_positions=enabled; hedging_required=true; stepped_tp_from_fill=true; external_indicator=not-used; history_chunked=true; history_snapshot_rollback=true; margin_and_freeze_checks=true; close_scope=all_symbol_positions");
|
| LogEvent("EA_INITIALIZED",StringFormat("brick_points=%d terminal_trade=%s",InpBrickPoints,(bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)?"ON":"OFF"));
|
| return INIT_SUCCEEDED;
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| SavePersistentSettings();
|
| EventKillTimer();
|
| ObjectDelete(0,g_buyButton); ObjectDelete(0,g_sellButton); ObjectDelete(0,g_terminalTradeLabel); ObjectDelete(0,g_terminalTradeDot);
|
| ObjectDelete(0,g_brickEdit); ObjectDelete(0,g_brickApply); ObjectDelete(0,g_positionsEdit); ObjectDelete(0,g_lotsEdit); ObjectDelete(0,g_basketApply); ObjectDelete(0,g_fivePositionsButton); ObjectDelete(0,g_positionsLabel); ObjectDelete(0,g_lotsLabel); ObjectDelete(0,g_statusLabel);
|
| for(int i=0;i<10;i++) ObjectDelete(0,g_diagLabels[i]);
|
| LogEvent("EA_STOPPED",StringFormat("reason=%d",reason));
|
| CloseLogHandle();
|
| }
|
|
|
| void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
|
| {
|
| if(id!=CHARTEVENT_OBJECT_CLICK) return;
|
| if(sparam==g_buyButton)
|
| {
|
| if(g_buyEnabled) { ClearEntrySetup(1); g_lastDecision="BUY-подготовка отменена пользователем"; LogEvent("BUY_SETUP_CANCELLED_USER","manual button click"); }
|
| else if(!ArmEntrySetup(1)) g_buyEnabled=false;
|
| SavePersistentSettings();
|
| UpdatePanel();
|
| }
|
| else if(sparam==g_sellButton)
|
| {
|
| if(g_sellEnabled) { ClearEntrySetup(-1); g_lastDecision="SELL-подготовка отменена пользователем"; LogEvent("SELL_SETUP_CANCELLED_USER","manual button click"); }
|
| else if(!ArmEntrySetup(-1)) g_sellEnabled=false;
|
| SavePersistentSettings();
|
| UpdatePanel();
|
| }
|
| else if(sparam==g_brickApply) ApplyRuntimeBrickSize();
|
| else if(sparam==g_basketApply) ApplyRuntimeBasketSettings();
|
| else if(sparam==g_fivePositionsButton) ApplyFivePositionsPreset();
|
| }
|
|
|
| bool ProcessLiveTickRange(const MqlTick &tick)
|
| {
|
| bool wasPending=g_liveTickRangePending;
|
| if(g_lastTickMsc<=0)
|
| {
|
| g_liveTickRangePending=true;
|
| return false;
|
| }
|
|
|
| // Читаем курсорную миллисекунду включительно: в ней могут быть разные Bid.
|
| // Пропускается только префикс, уже обработанный ранее.
|
| ulong fromMsc=(ulong)g_lastTickMsc;
|
| ulong toMsc=tick.time_msc;
|
| if(fromMsc>toMsc)
|
| {
|
| g_liveTickRangePending=true;
|
| datetime now=TimeCurrent();
|
| if(g_lastLiveTickRangeLog==0 || now-g_lastLiveTickRangeLog>=5)
|
| {
|
| LogEvent("LIVE_TICK_CURSOR_MISMATCH",StringFormat("from_msc=%I64u to_msc=%I64u last_tick_msc=%I64d current_bid=%s last_bid=%s pending=true",
|
| fromMsc,toMsc,g_lastTickMsc,DoubleToString(tick.bid,_Digits),DoubleToString(g_lastBid,_Digits)));
|
| g_lastLiveTickRangeLog=now;
|
| }
|
| return false;
|
| }
|
|
|
| MqlTick newTicks[];
|
| ResetLastError();
|
| int copied=CopyTicksRange(_Symbol,newTicks,COPY_TICKS_INFO,fromMsc,toMsc);
|
| int error=GetLastError();
|
|
|
| // При любом коде ошибки непрерывность диапазона не подтверждена.
|
| // Не обрабатываем возможный частичный ответ и не двигаем курсор.
|
| if(copied<0 || error!=0)
|
| {
|
| g_liveTickRangePending=true;
|
| datetime now=TimeCurrent();
|
| if(g_lastLiveTickRangeLog==0 || now-g_lastLiveTickRangeLog>=5)
|
| {
|
| LogEvent("LIVE_TICK_RANGE_UNAVAILABLE",StringFormat("from_msc=%I64u to_msc=%I64u copied=%d error=%d current_tick_msc=%I64u current_bid=%s last_tick_msc=%I64d last_bid=%s pending=true",
|
| fromMsc,toMsc,copied,error,tick.time_msc,DoubleToString(tick.bid,_Digits),g_lastTickMsc,DoubleToString(g_lastBid,_Digits)));
|
| g_lastLiveTickRangeLog=now;
|
| }
|
| return false;
|
| }
|
|
|
| if(copied==0)
|
| {
|
| if(wasPending || tick.bid!=g_lastBid || tick.time_msc>g_lastTickMsc)
|
| {
|
| g_liveTickRangePending=true;
|
| datetime now=TimeCurrent();
|
| if(g_lastLiveTickRangeLog==0 || now-g_lastLiveTickRangeLog>=5)
|
| {
|
| LogEvent("LIVE_TICK_RANGE_UNAVAILABLE",StringFormat("from_msc=%I64u to_msc=%I64u copied=0 error=%d current_tick_msc=%I64u current_bid=%s last_tick_msc=%I64d last_bid=%s pending=true",
|
| fromMsc,toMsc,error,tick.time_msc,DoubleToString(tick.bid,_Digits),g_lastTickMsc,DoubleToString(g_lastBid,_Digits)));
|
| g_lastLiveTickRangeLog=now;
|
| }
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| g_processBidGuardExceeded=false;
|
| int seenAtCursorMsc=0;
|
| int processedAtCursorMsc=g_ticksProcessedAtLastMsc;
|
| for(int i=0;i<copied;i++)
|
| {
|
| long tickMsc=(long)newTicks[i].time_msc;
|
| if(tickMsc<g_lastTickMsc)
|
| continue;
|
|
|
| if(tickMsc==g_lastTickMsc)
|
| {
|
| ++seenAtCursorMsc;
|
| if(seenAtCursorMsc<=processedAtCursorMsc)
|
| continue;
|
| ++g_ticksProcessedAtLastMsc;
|
| }
|
| else
|
| {
|
| g_lastTickMsc=tickMsc;
|
| g_ticksProcessedAtLastMsc=1;
|
| seenAtCursorMsc=0;
|
| processedAtCursorMsc=0;
|
| }
|
|
|
| // Тик без Bid учитывается курсором, но не создаёт кирпич или сигнал.
|
| if(newTicks[i].bid<=0)
|
| continue;
|
| ProcessBid((datetime)newTicks[i].time,newTicks[i].bid,true);
|
| if(g_processBidGuardExceeded)
|
| {
|
| g_historyReady=false;
|
| g_historyPartial=true;
|
| g_buyEnabled=false;
|
| g_sellEnabled=false;
|
| g_pendingSignal=0;
|
| g_pendingStopLevel=0.0;
|
| g_liveTickRangePending=true;
|
| g_historyRetryDelayMs=(ulong)HISTORY_RETRY_MIN_MS;
|
| LogEvent("LIVE_RENKO_GUARD_RECOVERY","live Renko processing exceeded guard; trading disabled until full history rebuild");
|
| g_lastDecision="Превышен предел обработки Renko; торговля заблокирована до пересборки истории";
|
| return false;
|
| }
|
| }
|
|
|
| if(tick.bid!=g_lastBid)
|
| {
|
| g_liveTickRangePending=true;
|
| datetime now=TimeCurrent();
|
| if(g_lastLiveTickRangeLog==0 || now-g_lastLiveTickRangeLog>=5)
|
| {
|
| LogEvent("LIVE_TICK_RANGE_UNAVAILABLE",StringFormat("from_msc=%I64u to_msc=%I64u copied=%d error=%d current_tick_msc=%I64u current_bid=%s last_tick_msc=%I64d last_bid=%s pending=true reason=bid_not_reconciled",
|
| fromMsc,toMsc,copied,error,tick.time_msc,DoubleToString(tick.bid,_Digits),g_lastTickMsc,DoubleToString(g_lastBid,_Digits)));
|
| g_lastLiveTickRangeLog=now;
|
| }
|
| return false;
|
| }
|
|
|
| g_liveTickRangePending=false;
|
| if(wasPending)
|
| LogEvent("LIVE_TICK_RANGE_RECOVERED",StringFormat("copied=%d current_tick_msc=%I64u cursor_msc=%I64d bid=%s",
|
| copied,tick.time_msc,g_lastTickMsc,DoubleToString(g_lastBid,_Digits)));
|
| return true;
|
| }
|
| void OnTick()
|
| {
|
| CloseAtTimeIfRequired();
|
| MqlTick tick;
|
| if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0) return;
|
| if(!g_historyReady)
|
| {
|
| if(g_lastHistoryAttemptMs==0 || GetTickCount64()-g_lastHistoryAttemptMs>=g_historyRetryDelayMs)
|
| RebuildHistory();
|
| return;
|
| }
|
| if(!ProcessLiveTickRange(tick))
|
| {
|
| UpdatePanel();
|
| return;
|
| }
|
| RestoreSavedModesIfReady();
|
| ExecutePendingSignal();
|
| }
|
|
|
| void RetryEmergencyClose()
|
| {
|
| if(g_emergencyCloseTicket==0) return;
|
| ulong nowMs=GetTickCount64();
|
| if(g_emergencyCloseLastAttemptMs>0 && nowMs-g_emergencyCloseLastAttemptMs<5000) return;
|
| ulong ticket=g_emergencyCloseTicket;
|
| if(!PositionSelectByTicket(ticket))
|
| {
|
| LogEvent("EMERGENCY_CLOSE_RESOLVED",StringFormat("ticket=%I64u reason=position_not_open",ticket));
|
| g_emergencyCloseTicket=0;
|
| g_emergencyCloseLastAttemptMs=0;
|
| PersistEmergencyCloseTicket();
|
| return;
|
| }
|
| if(PositionGetString(POSITION_SYMBOL)!=_Symbol ||
|
| (ulong)PositionGetInteger(POSITION_MAGIC)!=InpMagicNumber)
|
| {
|
| LogEvent("EMERGENCY_CLOSE_CANCELLED",StringFormat("ticket=%I64u reason=position_identity_changed",ticket));
|
| g_emergencyCloseTicket=0;
|
| g_emergencyCloseLastAttemptMs=0;
|
| PersistEmergencyCloseTicket();
|
| return;
|
| }
|
| g_emergencyCloseLastAttemptMs=nowMs;
|
| ResetLastError();
|
| bool sent=g_trade.PositionClose(ticket);
|
| uint rc=g_trade.ResultRetcode();
|
| bool stillOpen=PositionSelectByTicket(ticket);
|
| LogEvent("EMERGENCY_CLOSE_RETRY",StringFormat("ticket=%I64u sent=%s retcode=%u description=%s still_open=%s error=%d",
|
| ticket,sent?"true":"false",rc,g_trade.ResultRetcodeDescription(),stillOpen?"true":"false",GetLastError()));
|
| if(!stillOpen)
|
| {
|
| g_emergencyCloseTicket=0;
|
| g_emergencyCloseLastAttemptMs=0;
|
| PersistEmergencyCloseTicket();
|
| }
|
| }
|
|
|
| void OnTimer()
|
| {
|
| UpdatePanel(); // Обновляем время, вершины/впадины и статус каждую секунду, даже без новых тиков.
|
| CloseAtTimeIfRequired();
|
| RetryEmergencyClose();
|
| if(!g_historyReady && (g_lastHistoryAttemptMs==0 || GetTickCount64()-g_lastHistoryAttemptMs>=g_historyRetryDelayMs))
|
| RebuildHistory();
|
| if(g_historyReady)
|
| {
|
| MqlTick tick;
|
| if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0)
|
| {
|
| UpdatePanel();
|
| return;
|
| }
|
| if(!ProcessLiveTickRange(tick))
|
| {
|
| UpdatePanel();
|
| return;
|
| }
|
| RestoreSavedModesIfReady();
|
| }
|
| ExecutePendingSignal();
|
| UpdatePanel();
|
| }
|
|
|
| void OnTradeTransaction(const MqlTradeTransaction &trans,
|
| const MqlTradeRequest &request,
|
| const MqlTradeResult &result)
|
| {
|
| if(trans.type==TRADE_TRANSACTION_DEAL_ADD && trans.deal>0 && HistoryDealSelect(trans.deal))
|
| {
|
| string dealSymbol=HistoryDealGetString(trans.deal,DEAL_SYMBOL);
|
| long dealMagic=HistoryDealGetInteger(trans.deal,DEAL_MAGIC);
|
| ENUM_DEAL_ENTRY entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal,DEAL_ENTRY);
|
| if(dealSymbol==_Symbol && (ulong)dealMagic==InpMagicNumber &&
|
| (entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY || entry==DEAL_ENTRY_INOUT))
|
| {
|
| datetime closeTime=(datetime)HistoryDealGetInteger(trans.deal,DEAL_TIME);
|
| long serverToUtcOffset=(long)TimeGMT()-(long)TimeCurrent();
|
| datetime closeMsk=(datetime)((long)closeTime+serverToUtcOffset+3*3600);
|
| double closePrice=HistoryDealGetDouble(trans.deal,DEAL_PRICE);
|
| double closeVolume=HistoryDealGetDouble(trans.deal,DEAL_VOLUME);
|
| long closeReason=HistoryDealGetInteger(trans.deal,DEAL_REASON);
|
| g_lastCloseInfo=StringFormat("Последнее закрытие: сервер %s | MSK %s | %s | объём %.2f | цена %s",
|
| TimeToString(closeTime,TIME_DATE|TIME_MINUTES),TimeToString(closeMsk,TIME_DATE|TIME_MINUTES),
|
| closeReason==DEAL_REASON_TP?"TP":(closeReason==DEAL_REASON_SL?"SL":(closeReason==DEAL_REASON_CLIENT?"вручную":"прочее")),
|
| closeVolume,DoubleToString(closePrice,_Digits));
|
| UpdatePanel();
|
| LogEvent("POSITION_CLOSED",StringFormat("deal=%I64u server=%s msk=%s volume=%.2f price=%s reason=%d",
|
| trans.deal,TimeToString(closeTime,TIME_DATE|TIME_SECONDS),TimeToString(closeMsk,TIME_DATE|TIME_SECONDS),
|
| closeVolume,DoubleToString(closePrice,_Digits),(int)closeReason));
|
| }
|
| }
|
| if(trans.symbol!=_Symbol) return;
|
| if(result.retcode!=0 && result.retcode!=TRADE_RETCODE_DONE &&
|
| result.retcode!=TRADE_RETCODE_DONE_PARTIAL && result.retcode!=TRADE_RETCODE_PLACED)
|
| LogEvent("TRADE_TRANSACTION_ERROR",StringFormat("type=%d retcode=%u comment=%s order=%I64u deal=%I64u",
|
| (int)trans.type,result.retcode,result.comment,result.order,result.deal));
|
| }
|