1984 lines
99 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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));
| |||
}
|