| //+------------------------------------------------------------------+
|
| //| LW_SMC_CHOCH_OCO_EA.mq5 |
|
| //| CHoCH -> OCO pending orders + trailing stop |
|
| //| |
|
| //| Структура рынка считается так же, как в индикаторе |
|
| //| LW_MarketStructure_SMC.mq5 (фрактальные swing H/L + BOS + CHoCH). |
|
| //| |
|
| //| Логика: |
|
| //| 1) На новом ЗАКРЫТОМ баре ищем последний CHoCH (Change of |
|
| //| Character = пробой swing-уровня против текущего тренда). |
|
| //| 2) На новый CHoCH выставляем два отложенных ордера (OCO): |
|
| //| - OCO_STOP : BUY STOP выше цены + SELL STOP ниже цены; |
|
| //| - OCO_LIMIT: BUY LIMIT ниже цены + SELL LIMIT выше цены. |
|
| //| 3) Как только один ордер исполнился — второй удаляется (OCO). |
|
| //| 4) Выход — по трейлинг-стопу (опц. безубыток / фикс. TP). |
|
| //| |
|
| //| ВАЖНО: MT5 не принимает отложенный ордер с ценой на "неверной" |
|
| //| стороне рынка (BUY LIMIT выше цены отклоняется сервером). |
|
| //| Поэтому "BUY сверху + SELL снизу" реализовано стоп-ордерами |
|
| //| (OCO_STOP), а лимитные ордера возможны только в зеркальной |
|
| //| геометрии (BUY ниже, SELL выше) — режим OCO_LIMIT. |
|
| //+------------------------------------------------------------------+
|
| #property copyright "Lonely Wolf"
|
| #property version "1.01"
|
| #property description "CHoCH (LW_MarketStructure_SMC) -> OCO pending orders (BUY above / SELL below) + trailing stop."
|
| #property description "Detection uses closed bars only (no repaint)."
|
|
|
| #include <Trade\Trade.mqh>
|
|
|
| //============================== ENUMS ================================
|
| enum ENUM_OCO_MODE
|
| {
|
| OCO_STOP = 0, // Пробой: BUY STOP выше + SELL STOP ниже
|
| OCO_LIMIT = 1 // Откат: BUY LIMIT ниже + SELL LIMIT выше
|
| };
|
|
|
| enum ENUM_SL_MODE
|
| {
|
| SL_STRUCT = 0, // По структуре (swing-экстремум CHoCH)
|
| SL_FIXED = 1 // Фиксированный в пунктах
|
| };
|
|
|
| enum ENUM_SL_ANCHOR
|
| {
|
| SL_ANCHOR_NEAR = 0, // Ближний swing (тейтовый SL)
|
| SL_ANCHOR_FAR = 1 // Дальний swing (широкий SL)
|
| };
|
|
|
| //============================== INPUTS ===============================
|
| input group "=== Идентификация ==="
|
| input long InpMagic = 20261023; // Магический номер
|
| input double InpLot = 0.10; // Объём (лоты), если риск-лот выключен
|
| input bool InpUseRiskPercent = false; // Считать лот от риска (% баланса)
|
| input double InpRiskPercent = 1.0; // Риск на сделку (% баланса)
|
|
|
| input group "=== Детекция CHoCH (как в индикаторе) ==="
|
| input int InpSwingN = 5; // Фрактал: баров с каждой стороны
|
| input bool InpBreakOnClose = true; // Пробой подтверждается по CLOSE свечи
|
| input int InpMaxBars = 1500; // Сколько последних закрытых баров анализировать
|
| input bool InpTradeExistingSignal = false; // Торговать CHoCH, который уже был на старте EA
|
|
|
| input group "=== OCO: отложенные ордера ==="
|
| input ENUM_OCO_MODE InpOCOMode = OCO_STOP; // Тип OCO-сетапа
|
| input double InpOffsetPoints = 100.0; // Отступ ордеров от цены (пункты)
|
| input int InpExpireBars = 0; // Срок жизни ордеров (баров; 0 = не истекают)
|
| input bool InpCancelOnNewSignal = true; // Снимать прошлые ордера при новом CHoCH
|
| input bool InpOneSetupAtATime = true; // Не выставлять новый сетап, пока есть позиция
|
| input int InpMaxSpreadPoints = 0; // Макс. спред (пункты; 0 = без контроля)
|
| input int InpDeviationPoints = 30; // Допустимое отклонение цены (пункты)
|
|
|
| input group "=== Стоп-лосс / тейк-профит ==="
|
| input ENUM_SL_MODE InpSLMode = SL_STRUCT; // Режим Stop Loss
|
| input ENUM_SL_ANCHOR InpSLAnchor = SL_ANCHOR_FAR; // Опора структурного SL (где стоит стоп)
|
| input int InpSLBufferPoints = 30; // Буфер SL за swing-экстремум (пункты)
|
| input double InpSLFallbackPoints = 500.0; // Резервный SL (пункты), если нет валидной структуры (0 = не входить)
|
| input double InpMaxSLPoints = 1500.0; // Макс. ширина SL (пункты; 0 = без ограничения)
|
| input double InpSLPoints = 500.0; // Фикс. SL от входа (пункты) для режима SL_FIXED
|
| input double InpTPPoints = 0.0; // TP от входа (пункты; 0 = без TP)
|
| input bool InpAllowNoSL = false; // ВНИМАНИЕ: разрешить вход без SL (риск)
|
|
|
| input group "=== Трейлинг-стоп ==="
|
| input bool InpUseTrailing = true; // Трейлинг-стоп
|
| input int InpTrailStartPoints = 300; // Старт трейлинга при такой прибыли (пункты)
|
| input int InpTrailDistPoints = 200; // Дистанция трейлинга (пункты)
|
| input int InpTrailStepPoints = 10; // Мин. шаг перемещения SL (пункты)
|
| input bool InpUseBreakEven = false; // Перевод в безубыток
|
| input int InpBETriggerPoints = 200; // Прибыль для перевода в безубыток (пункты)
|
| input int InpBEBufferPoints = 10; // Смещение SL за цену входа (пункты)
|
|
|
| input group "=== Прочее ==="
|
| input bool InpVerboseLog = true; // Подробный лог
|
|
|
| //============================== TYPES ================================
|
| struct SChochEvent
|
| {
|
| datetime time; // время бара подтверждения CHoCH
|
| int dir; // +1 = бычий CHoCH (разворот вверх), -1 = медвежий
|
| double level; // пробитый swing-уровень
|
| double oppLevel; // противоположный swing (вторая опора для SL)
|
| bool oppValid; // противоположный swing найден
|
| };
|
|
|
| struct SSymbolMetrics
|
| {
|
| double point;
|
| int digits;
|
| double tickSize;
|
| double tickValue;
|
| double volMin;
|
| double volMax;
|
| double volStep;
|
| double stopsLevel; // в цене
|
| double freezeLevel; // в цене
|
| bool valid;
|
| };
|
|
|
| //============================== GLOBALS ==============================
|
| CTrade trade;
|
| SSymbolMetrics g_sym;
|
| datetime g_lastBarTime = 0;
|
| datetime g_lastSignalTime = 0;
|
| ulong g_ticketBuy = 0; // тикеты текущего OCO-сетапа
|
| ulong g_ticketSell = 0;
|
|
|
| //============================== INIT =================================
|
| int OnInit()
|
| {
|
| if(InpLot <= 0.0 && !InpUseRiskPercent)
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: InpLot must be > 0 (или включите риск-лот)");
|
| return INIT_PARAMETERS_INCORRECT;
|
| }
|
| if(InpSwingN < 1 || InpMaxBars < InpSwingN * 2 + 10)
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: invalid detection inputs");
|
| return INIT_PARAMETERS_INCORRECT;
|
| }
|
| if(InpSLMode == SL_FIXED && InpSLPoints <= 0.0)
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: InpSLPoints must be > 0 in SL_FIXED mode");
|
| return INIT_PARAMETERS_INCORRECT;
|
| }
|
|
|
| trade.SetExpertMagicNumber(InpMagic);
|
| trade.SetDeviationInPoints(InpDeviationPoints);
|
| trade.SetTypeFillingBySymbol(_Symbol);
|
| trade.SetAsyncMode(false);
|
|
|
| RefreshSymbolMetrics();
|
|
|
| if(InpSLMode == SL_STRUCT && InpAllowNoSL)
|
| Print("LW_SMC_CHOCH_OCO_EA: WARNING - SL по структуре может отсутствовать, вход без SL разрешён (InpAllowNoSL=true).");
|
| if(!InpUseTrailing && !InpUseBreakEven && InpTPPoints <= 0.0)
|
| Print("LW_SMC_CHOCH_OCO_EA: WARNING - выход не управляется: трейлинг/безубыток выключены и TP=0.");
|
| if(InpOCOMode == OCO_LIMIT)
|
| Print("LW_SMC_CHOCH_OCO_EA: OCO_LIMIT - BUY LIMIT ниже цены, SELL LIMIT выше цены (зеркальная геометрия).");
|
|
|
| return INIT_SUCCEEDED;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| |
|
| //+------------------------------------------------------------------+
|
| void OnDeinit(const int reason)
|
| {
|
| // Отложенные ордера намеренно не удаляем: ими управляет OCO и срок жизни.
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| |
|
| //+------------------------------------------------------------------+
|
| void OnTick()
|
| {
|
| ManageOCO(); // OCO: снятие парного ордера + истечение
|
| ManageTrailing(); // трейлинг-стоп / безубыток
|
|
|
| datetime bt = (datetime)SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_LASTBAR_DATE);
|
| if(bt == g_lastBarTime)
|
| return;
|
| g_lastBarTime = bt;
|
|
|
| RefreshSymbolMetrics();
|
| ProcessNewBar();
|
| }
|
|
|
| //============================== SYMBOL METRICS ========================
|
| void RefreshSymbolMetrics()
|
| {
|
| g_sym.point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
| g_sym.digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
| g_sym.tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
| if(g_sym.tickSize <= 0.0)
|
| g_sym.tickSize = g_sym.point;
|
| g_sym.tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
| g_sym.volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
| g_sym.volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
| g_sym.volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
| g_sym.stopsLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * g_sym.point;
|
| g_sym.freezeLevel= (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL) * g_sym.point;
|
| g_sym.valid = (g_sym.point > 0.0);
|
| }
|
|
|
| //============================== MAIN LOGIC ===========================
|
| void ProcessNewBar()
|
| {
|
| if(!g_sym.valid)
|
| return;
|
|
|
| SChochEvent ev;
|
| if(!FindLastCHoCH(ev))
|
| return; // в окне анализа CHoCH нет
|
|
|
| if(g_lastSignalTime == 0)
|
| {
|
| if(!InpTradeExistingSignal)
|
| {
|
| g_lastSignalTime = ev.time; // сигналы до старта EA не торгуем
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: старт, CHoCH %s пропущен (InpTradeExistingSignal=false)",
|
| TimeToString(ev.time, TIME_DATE | TIME_MINUTES));
|
| return;
|
| }
|
| }
|
| else
|
| if(ev.time == g_lastSignalTime)
|
| return; // этот CHoCH уже обработан
|
|
|
| g_lastSignalTime = ev.time;
|
|
|
| if(!IsTradeAllowed())
|
| return;
|
|
|
| if(InpOneSetupAtATime && CountOurPositions() > 0)
|
| {
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: CHoCH %s пропущен - открыта позиция",
|
| TimeToString(ev.time, TIME_DATE | TIME_MINUTES));
|
| return;
|
| }
|
|
|
| if(InpCancelOnNewSignal)
|
| CancelAllOurPendings();
|
|
|
| if(InpMaxSpreadPoints > 0)
|
| {
|
| double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
| double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
| if(ask > 0.0 && bid > 0.0)
|
| {
|
| double spreadPts = (ask - bid) / g_sym.point;
|
| if(spreadPts > InpMaxSpreadPoints)
|
| {
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: CHoCH %s пропущен - спред %.1f > %.1f пт",
|
| TimeToString(ev.time, TIME_DATE | TIME_MINUTES), spreadPts, (double)InpMaxSpreadPoints);
|
| return;
|
| }
|
| }
|
| }
|
|
|
| PlaceOCO(ev);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Поиск последнего CHoCH на закрытых барах |
|
| //| (логика BOS/CHoCH повторяет индикатор LW_MarketStructure_SMC) |
|
| //+------------------------------------------------------------------+
|
| bool FindLastCHoCH(SChochEvent &ev)
|
| {
|
| int need = InpMaxBars + InpSwingN * 2 + 10;
|
|
|
| MqlRates rates[];
|
| ArraySetAsSeries(rates, false);
|
| int n = CopyRates(_Symbol, PERIOD_CURRENT, 0, need, rates);
|
| if(n < InpSwingN * 2 + 10)
|
| return false;
|
|
|
| int last = n - 2; // последний ЗАКРЫТЫЙ бар (без repaint)
|
| if(last < InpSwingN * 2)
|
| return false;
|
|
|
| int start = (int)MathMax((double)InpSwingN, (double)(last - InpMaxBars + 1));
|
|
|
| //--- Pass 1: фрактальные swing-экстремумы (как в индикаторе)
|
| char sh[], sl[];
|
| ArrayResize(sh, n);
|
| ArrayResize(sl, n);
|
| ArrayInitialize(sh, 0);
|
| ArrayInitialize(sl, 0);
|
|
|
| for(int i = start; i <= last - InpSwingN; i++)
|
| {
|
| bool isHigh = true, isLow = true;
|
| for(int k = 1; k <= InpSwingN; k++)
|
| {
|
| if(rates[i].high <= rates[i - k].high || rates[i].high <= rates[i + k].high)
|
| isHigh = false;
|
| if(rates[i].low >= rates[i - k].low || rates[i].low >= rates[i + k].low)
|
| isLow = false;
|
| }
|
| if(isHigh)
|
| sh[i] = 1;
|
| if(isLow)
|
| sl[i] = 1;
|
| }
|
|
|
| //--- Pass 2: BOS / CHoCH
|
| bool found = false;
|
| int trend = 0; // 1 = up, -1 = down, 0 = neutral
|
| double refHigh = 0.0, refLow = 0.0;
|
| int refHighBar = -1, refLowBar = -1;
|
| bool haveHigh = false, haveLow = false;
|
|
|
| for(int i = start + InpSwingN; i <= last; i++)
|
| {
|
| int j = i - InpSwingN; // swing подтверждён на баре j
|
|
|
| if(sh[j] == 1)
|
| {
|
| refHigh = rates[j].high;
|
| refHighBar = j;
|
| haveHigh = true;
|
| }
|
| if(sl[j] == 1)
|
| {
|
| refLow = rates[j].low;
|
| refLowBar = j;
|
| haveLow = true;
|
| }
|
|
|
| double upPx = InpBreakOnClose ? rates[i].close : rates[i].high;
|
| double downPx = InpBreakOnClose ? rates[i].close : rates[i].low;
|
|
|
| if(haveHigh && upPx > refHigh) // бычий пробой
|
| {
|
| bool isBOS = (trend != -1); // против нисходящего тренда = CHoCH
|
| if(!isBOS)
|
| {
|
| ev.time = rates[i].time;
|
| ev.dir = 1;
|
| ev.level = refHigh;
|
| ev.oppLevel = refLow;
|
| ev.oppValid = haveLow;
|
| found = true;
|
| }
|
| trend = 1;
|
| haveHigh = false; // уровень израсходован
|
| }
|
| else
|
| if(haveLow && downPx < refLow) // медвежий пробой
|
| {
|
| bool isBOS = (trend != 1); // против восходящего тренда = CHoCH
|
| if(!isBOS)
|
| {
|
| ev.time = rates[i].time;
|
| ev.dir = -1;
|
| ev.level = refLow;
|
| ev.oppLevel = refHigh;
|
| ev.oppValid = haveHigh;
|
| found = true;
|
| }
|
| trend = -1;
|
| haveLow = false;
|
| }
|
| }
|
|
|
| return found;
|
| }
|
|
|
| //============================== OCO PLACEMENT ========================
|
| void PlaceOCO(const SChochEvent &ev)
|
| {
|
| double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
| double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
| if(ask <= 0.0 || bid <= 0.0)
|
| return;
|
|
|
| g_ticketBuy = 0; // новая группа: старые танкеты недействительны
|
| g_ticketSell = 0;
|
|
|
| double point = g_sym.point;
|
| double minDist = MathMax(g_sym.stopsLevel, g_sym.point);
|
| double off = InpOffsetPoints * point;
|
| if(off < minDist) // соблюдаем Stops Level
|
| off = minDist;
|
|
|
| double buyPx, sellPx;
|
| if(InpOCOMode == OCO_STOP)
|
| {
|
| buyPx = ask + off; // BUY STOP выше цены
|
| sellPx = bid - off; // SELL STOP ниже цены
|
| }
|
| else
|
| {
|
| buyPx = ask - off; // BUY LIMIT ниже цены
|
| sellPx = bid + off; // SELL LIMIT выше цены
|
| }
|
| buyPx = NormalizeDouble(buyPx, g_sym.digits);
|
| sellPx = NormalizeDouble(sellPx, g_sym.digits);
|
|
|
| string cmt = "CHOCH_" + (string)(long)ev.time;
|
|
|
| //--- BUY сторона
|
| double buySL = 0.0;
|
| bool buySLok = BuildSL(1, buyPx, ev, buySL);
|
| if(buySLok || InpAllowNoSL)
|
| {
|
| double buyTP = BuildTP(1, buyPx);
|
| double lot = CalcLot(buyPx, buySL);
|
| if(lot > 0.0)
|
| {
|
| bool ok = (InpOCOMode == OCO_STOP)
|
| ? trade.BuyStop(lot, buyPx, _Symbol, buySL, buyTP, ORDER_TIME_GTC, 0, cmt)
|
| : trade.BuyLimit(lot, buyPx, _Symbol, buySL, buyTP, ORDER_TIME_GTC, 0, cmt);
|
| if(ok && trade.ResultRetcode() == TRADE_RETCODE_DONE)
|
| {
|
| g_ticketBuy = trade.ResultOrder();
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: %s BUY %.2f @ %.5f SL %.5f TP %.5f #%I64u",
|
| (InpOCOMode == OCO_STOP ? "BUY STOP" : "BUY LIMIT"),
|
| lot, buyPx, buySL, buyTP, g_ticketBuy);
|
| }
|
| else
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: BUY pending failed: %d %s",
|
| trade.ResultRetcode(), trade.ResultComment());
|
| }
|
| }
|
| else
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: BUY ордер не выставлен - нет валидного SL (структура и резерв = 0)");
|
|
|
| //--- SELL сторона
|
| double sellSL = 0.0;
|
| bool sellSLok = BuildSL(-1, sellPx, ev, sellSL);
|
| if(sellSLok || InpAllowNoSL)
|
| {
|
| double sellTP = BuildTP(-1, sellPx);
|
| double lot = CalcLot(sellPx, sellSL);
|
| if(lot > 0.0)
|
| {
|
| bool ok = (InpOCOMode == OCO_STOP)
|
| ? trade.SellStop(lot, sellPx, _Symbol, sellSL, sellTP, ORDER_TIME_GTC, 0, cmt)
|
| : trade.SellLimit(lot, sellPx, _Symbol, sellSL, sellTP, ORDER_TIME_GTC, 0, cmt);
|
| if(ok && trade.ResultRetcode() == TRADE_RETCODE_DONE)
|
| {
|
| g_ticketSell = trade.ResultOrder();
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: %s SELL %.2f @ %.5f SL %.5f TP %.5f #%I64u",
|
| (InpOCOMode == OCO_STOP ? "SELL STOP" : "SELL LIMIT"),
|
| lot, sellPx, sellSL, sellTP, g_ticketSell);
|
| }
|
| else
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: SELL pending failed: %d %s",
|
| trade.ResultRetcode(), trade.ResultComment());
|
| }
|
| }
|
| else
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: SELL ордер не выставлен - нет валидного SL (структура и резерв = 0)");
|
|
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: CHoCH %s (%s) level %.5f -> OCO %s",
|
| TimeToString(ev.time, TIME_DATE | TIME_MINUTES),
|
| (ev.dir > 0 ? "bullish" : "bearish"), ev.level,
|
| (InpOCOMode == OCO_STOP ? "stops" : "limits"));
|
| }
|
|
|
| //============================== SL / TP ==============================
|
| //+------------------------------------------------------------------+
|
| //| SL: фиксированный или по структуре. |
|
| //| Для BUY берём опору НИЖЕ входа, для SELL — ВЫШЕ входа; |
|
| //| из валидных опор (пробитый swing и противоположный swing) |
|
| //| выбираем ближнюю (тейтовую) или дальнюю (широкую) по InpSLAnchor. |
|
| //| Если валидной опоры нет — резервный SL в пунктах (если задан). |
|
| //+------------------------------------------------------------------+
|
| //| Ограничение ширины SL: если структурная опора слишком далеко, |
|
| //| SL подрезается до InpMaxSLPoints. |
|
| //+------------------------------------------------------------------+
|
| double CapSL(const int dir, const double entry, const double sl)
|
| {
|
| if(InpMaxSLPoints <= 0.0 || sl <= 0.0)
|
| return sl;
|
|
|
| double maxD = InpMaxSLPoints * g_sym.point;
|
| if(MathAbs(entry - sl) <= maxD)
|
| return sl;
|
|
|
| return (dir > 0) ? (entry - maxD) : (entry + maxD);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| bool BuildSL(const int dir, const double entry, const SChochEvent &ev, double &sl)
|
| {
|
| sl = 0.0;
|
| double point = g_sym.point;
|
| double minDist = MathMax(g_sym.stopsLevel, g_sym.point);
|
|
|
| if(InpSLMode == SL_FIXED)
|
| {
|
| double d = InpSLPoints * point;
|
| if(d < minDist)
|
| d = minDist;
|
| sl = (dir > 0) ? entry - d : entry + d;
|
| sl = NormalizeDouble(sl, g_sym.digits);
|
| return true;
|
| }
|
|
|
| double anchors[2];
|
| int cnt = 0;
|
| if(ev.level > 0.0)
|
| anchors[cnt++] = ev.level;
|
| if(ev.oppValid && ev.oppLevel > 0.0)
|
| anchors[cnt++] = ev.oppLevel;
|
|
|
| double buf = InpSLBufferPoints * point;
|
| bool found = false;
|
|
|
| for(int k = 0; k < cnt; k++)
|
| {
|
| double cand = (dir > 0) ? (anchors[k] - buf) : (anchors[k] + buf);
|
|
|
| if(dir > 0)
|
| {
|
| if(cand > entry - minDist) // опора выше/слишком близко к входу
|
| continue;
|
| if(!found || (InpSLAnchor == SL_ANCHOR_FAR ? (cand < sl) : (cand > sl)))
|
| {
|
| sl = cand;
|
| found = true;
|
| }
|
| }
|
| else
|
| {
|
| if(cand < entry + minDist)
|
| continue;
|
| if(!found || (InpSLAnchor == SL_ANCHOR_FAR ? (cand > sl) : (cand < sl)))
|
| {
|
| sl = cand;
|
| found = true;
|
| }
|
| }
|
| }
|
|
|
| if(!found)
|
| {
|
| // Структурной опоры нет (цена уже ушла от swing): резервный SL в пунктах.
|
| if(InpSLFallbackPoints <= 0.0)
|
| return false;
|
| double d = InpSLFallbackPoints * point;
|
| if(d < minDist)
|
| d = minDist;
|
| sl = CapSL(dir, entry, (dir > 0) ? entry - d : entry + d);
|
| sl = NormalizeDouble(sl, g_sym.digits);
|
| return true;
|
| }
|
|
|
| sl = CapSL(dir, entry, sl);
|
| sl = NormalizeDouble(sl, g_sym.digits);
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| double BuildTP(const int dir, const double entry)
|
| {
|
| if(InpTPPoints <= 0.0)
|
| return 0.0;
|
| double d = InpTPPoints * g_sym.point;
|
| double tp = (dir > 0) ? entry + d : entry - d;
|
| return NormalizeDouble(tp, g_sym.digits);
|
| }
|
|
|
| //============================== SIZING ===============================
|
| double NormalizeLot(double lot)
|
| {
|
| double step = (g_sym.volStep > 0.0) ? g_sym.volStep : 0.01;
|
| lot = MathRound(lot / step) * step;
|
| if(lot < g_sym.volMin)
|
| lot = g_sym.volMin;
|
| if(lot > g_sym.volMax)
|
| lot = g_sym.volMax;
|
| return NormalizeDouble(lot, 3);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| double CalcLot(const double entry, const double sl)
|
| {
|
| if(!InpUseRiskPercent || sl <= 0.0)
|
| return NormalizeLot(InpLot);
|
|
|
| double slDist = MathAbs(entry - sl);
|
| if(slDist <= 0.0 || g_sym.tickSize <= 0.0 || g_sym.tickValue <= 0.0)
|
| return NormalizeLot(InpLot);
|
|
|
| double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double riskMoney = balance * InpRiskPercent / 100.0;
|
| double lossPerLot = (slDist / g_sym.tickSize) * g_sym.tickValue;
|
| if(lossPerLot <= 0.0)
|
| return NormalizeLot(InpLot);
|
|
|
| return NormalizeLot(riskMoney / lossPerLot);
|
| }
|
|
|
| //============================== OCO MANAGEMENT ======================
|
| int CollectOurPendings(ulong &tickets[])
|
| {
|
| ArrayResize(tickets, 0);
|
| int cnt = 0;
|
| for(int i = OrdersTotal() - 1; i >= 0; i--)
|
| {
|
| ulong ticket = OrderGetTicket(i);
|
| if(ticket == 0)
|
| continue;
|
| if(!IsOurPending(ticket))
|
| continue;
|
| cnt++;
|
| ArrayResize(tickets, cnt);
|
| tickets[cnt - 1] = ticket;
|
| }
|
| return cnt;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| bool IsOurPending(const ulong ticket)
|
| {
|
| if(ticket == 0)
|
| return false;
|
| if(!OrderSelect(ticket))
|
| return false;
|
| if(OrderGetString(ORDER_SYMBOL) != _Symbol)
|
| return false;
|
| if(OrderGetInteger(ORDER_MAGIC) != InpMagic)
|
| return false;
|
|
|
| ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
| return (t == ORDER_TYPE_BUY_LIMIT || t == ORDER_TYPE_SELL_LIMIT ||
|
| t == ORDER_TYPE_BUY_STOP || t == ORDER_TYPE_SELL_STOP ||
|
| t == ORDER_TYPE_BUY_STOP_LIMIT || t == ORDER_TYPE_SELL_STOP_LIMIT);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| int CountOurPositions()
|
| {
|
| int cnt = 0;
|
| for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| {
|
| ulong ticket = PositionGetTicket(i);
|
| if(ticket == 0)
|
| continue;
|
| if(PositionGetString(POSITION_SYMBOL) != _Symbol)
|
| continue;
|
| if(PositionGetInteger(POSITION_MAGIC) != InpMagic)
|
| continue;
|
| cnt++;
|
| }
|
| return cnt;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| bool GroupHasPosition(const string grp)
|
| {
|
| if(grp == "")
|
| return false;
|
| for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| {
|
| ulong ticket = PositionGetTicket(i);
|
| if(ticket == 0)
|
| continue;
|
| if(PositionGetString(POSITION_SYMBOL) != _Symbol)
|
| continue;
|
| if(PositionGetInteger(POSITION_MAGIC) != InpMagic)
|
| continue;
|
| if(PositionGetString(POSITION_COMMENT) == grp)
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| void DeletePending(const ulong ticket)
|
| {
|
| if(ticket == 0)
|
| return;
|
| if(!IsOurPending(ticket))
|
| return; // ордер уже исполнен/снят — не трогаем
|
| if(!trade.OrderDelete(ticket))
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: OrderDelete #%I64u failed: %d %s",
|
| ticket, trade.ResultRetcode(), trade.ResultComment());
|
| else
|
| if(InpVerboseLog)
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: снят отложенный ордер #%I64u (OCO)", ticket);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| void CancelAllOurPendings()
|
| {
|
| ulong tickets[];
|
| int cnt = CollectOurPendings(tickets);
|
| for(int k = 0; k < cnt; k++)
|
| DeletePending(tickets[k]);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| void ManageOCO()
|
| {
|
| //--- (a) точная связка по тикетам текущего сетапа: сработал один -> снять второй
|
| if(g_ticketBuy != 0 && g_ticketSell != 0)
|
| {
|
| bool buyFilled = PositionSelectByTicket(g_ticketBuy);
|
| bool sellFilled = PositionSelectByTicket(g_ticketSell);
|
| if(buyFilled && !sellFilled)
|
| {
|
| DeletePending(g_ticketSell);
|
| g_ticketSell = 0;
|
| }
|
| else
|
| if(sellFilled && !buyFilled)
|
| {
|
| DeletePending(g_ticketBuy);
|
| g_ticketBuy = 0;
|
| }
|
| }
|
|
|
| ulong tickets[];
|
| int cnt = CollectOurPendings(tickets);
|
| if(cnt == 0)
|
| return;
|
|
|
| //--- (b)/(c)/(d) страховки и срок жизни
|
| for(int k = 0; k < cnt; k++)
|
| {
|
| ulong ticket = tickets[k];
|
| if(!OrderSelect(ticket))
|
| continue;
|
|
|
| string grp = OrderGetString(ORDER_COMMENT);
|
|
|
| // (b) по группе уже открыта позиция -> это исполненный OCO, снимаем остаток
|
| if(GroupHasPosition(grp))
|
| {
|
| DeletePending(ticket);
|
| continue;
|
| }
|
|
|
| // (c) срок жизни ордера истёк
|
| if(InpExpireBars > 0)
|
| {
|
| datetime setup = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
|
| if(TimeCurrent() - setup >= (long)InpExpireBars * PeriodSeconds())
|
| {
|
| DeletePending(ticket);
|
| continue;
|
| }
|
| }
|
|
|
| // (d) страховка после рестарта EA: позиция есть, а связка неизвестна
|
| if(InpOneSetupAtATime && CountOurPositions() > 0)
|
| DeletePending(ticket);
|
| }
|
| }
|
|
|
| //============================== TRAILING ==============================
|
| void ManageTrailing()
|
| {
|
| if(!InpUseTrailing && !InpUseBreakEven)
|
| return;
|
|
|
| double point = g_sym.point;
|
| int digits = g_sym.digits;
|
| double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
| double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
| if(bid <= 0.0 || ask <= 0.0)
|
| return;
|
|
|
| for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| {
|
| ulong ticket = PositionGetTicket(i);
|
| if(ticket == 0)
|
| continue;
|
| if(PositionGetString(POSITION_SYMBOL) != _Symbol)
|
| continue;
|
| if(PositionGetInteger(POSITION_MAGIC) != InpMagic)
|
| continue;
|
|
|
| ENUM_POSITION_TYPE pt = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
| double entry = PositionGetDouble(POSITION_PRICE_OPEN);
|
| double sl = PositionGetDouble(POSITION_SL);
|
| double tp = PositionGetDouble(POSITION_TP);
|
|
|
| double price = (pt == POSITION_TYPE_BUY) ? bid : ask;
|
| double profitPts = (pt == POSITION_TYPE_BUY) ? (bid - entry) / point
|
| : (entry - ask) / point;
|
| double bestSL = sl;
|
|
|
| if(pt == POSITION_TYPE_BUY)
|
| {
|
| if(InpUseBreakEven && profitPts >= InpBETriggerPoints)
|
| {
|
| double be = NormalizeDouble(entry + InpBEBufferPoints * point, digits);
|
| if(be > bestSL)
|
| bestSL = be;
|
| }
|
| if(InpUseTrailing && profitPts >= InpTrailStartPoints)
|
| {
|
| double tr = NormalizeDouble(price - InpTrailDistPoints * point, digits);
|
| if(tr > bestSL)
|
| bestSL = tr;
|
| }
|
|
|
| double maxSL = NormalizeDouble(price - MathMax(g_sym.stopsLevel, point), digits);
|
| if(bestSL > maxSL)
|
| bestSL = maxSL; // Stops Level
|
| if(tp > 0.0 && bestSL > tp - point)
|
| bestSL = NormalizeDouble(tp - point, digits); // SL ниже TP
|
| if(bestSL <= 0.0)
|
| continue;
|
| if(sl > 0.0 && bestSL <= sl + InpTrailStepPoints * point)
|
| continue; // шаг перемещения
|
| if(sl == 0.0 || bestSL > sl)
|
| if(!trade.PositionModify(ticket, bestSL, tp))
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: BUY SL modify #%I64u failed: %d %s",
|
| ticket, trade.ResultRetcode(), trade.ResultComment());
|
| }
|
| else
|
| {
|
| if(InpUseBreakEven && profitPts >= InpBETriggerPoints)
|
| {
|
| double be = NormalizeDouble(entry - InpBEBufferPoints * point, digits);
|
| if(bestSL == 0.0 || be < bestSL)
|
| bestSL = be;
|
| }
|
| if(InpUseTrailing && profitPts >= InpTrailStartPoints)
|
| {
|
| double tr = NormalizeDouble(price + InpTrailDistPoints * point, digits);
|
| if(bestSL == 0.0 || tr < bestSL)
|
| bestSL = tr;
|
| }
|
|
|
| double minSL = NormalizeDouble(price + MathMax(g_sym.stopsLevel, point), digits);
|
| if(bestSL == 0.0 || bestSL < minSL)
|
| bestSL = minSL; // Stops Level
|
| if(tp > 0.0 && bestSL < tp + point)
|
| bestSL = NormalizeDouble(tp + point, digits); // SL выше TP
|
| if(bestSL <= 0.0)
|
| continue;
|
| if(sl > 0.0 && bestSL >= sl - InpTrailStepPoints * point)
|
| continue; // шаг перемещения
|
| if(sl == 0.0 || bestSL < sl)
|
| if(!trade.PositionModify(ticket, bestSL, tp))
|
| PrintFormat("LW_SMC_CHOCH_OCO_EA: SELL SL modify #%I64u failed: %d %s",
|
| ticket, trade.ResultRetcode(), trade.ResultComment());
|
| }
|
| }
|
| }
|
|
|
| //============================== COMMON ===============================
|
| bool IsTradeAllowed()
|
| {
|
| if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: trading is disabled in the terminal");
|
| return false;
|
| }
|
| if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: trading is disabled for this EA");
|
| return false;
|
| }
|
| if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED))
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: trading is disabled for this account");
|
| return false;
|
| }
|
| if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
|
| {
|
| Print("LW_SMC_CHOCH_OCO_EA: automated trading is disabled for this account");
|
| return false;
|
| }
|
| return true;
|
| }
|
| //+------------------------------------------------------------------+
|