mql5/Experts/RenkoGPTBreakoutEA.mq5
2026-11-05 12:53:07 +03:00
//+------------------------------------------------------------------+
//| 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));
}