//+------------------------------------------------------------------+ //| 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 //--- Константы истории, маржи и журнала #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>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=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'9') countValid=false; } bool lotsValid=(lotsText!="" && StringToDouble(lotsText)>0.0); int decimalPoints=0; for(int i=0;i1) 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'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.hour0 && 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;i0) { 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) { 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 || volumemaxv) 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(vmaxv) 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_lastAttemptTime0) { 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=tick.bid || tick.bid-firstTp=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(freeMarginNow0 && 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)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-80; } //+------------------------------------------------------------------+ //| Снимок и откат состояния 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;i0 && ArrayResize(g_bricks,g_snapshotCount,512)!=g_snapshotCount) { LogEvent("SNAPSHOT_RESTORE_FAILED","bricks array resize failed"); return; } for(int i=0;i=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;ig_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=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)); }