471 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
			
		
		
	
	
			471 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
//+------------------------------------------------------------------+
 | 
						|
//|                                        VirtualSymbolReceiver.mqh |
 | 
						|
//|                                 Copyright 2022-2025, Yuriy Bykov |
 | 
						|
//|                            https://www.mql5.com/ru/users/antekov |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
#property copyright "Copyright 2022-2025, Yuriy Bykov"
 | 
						|
#property link      "https://www.mql5.com/ru/users/antekov"
 | 
						|
#property version   "1.00"
 | 
						|
 | 
						|
#include <Trade\PositionInfo.mqh>
 | 
						|
#include <Trade\SymbolInfo.mqh>
 | 
						|
#include <Trade\Trade.mqh>
 | 
						|
 | 
						|
#include "../Base/Receiver.mqh"
 | 
						|
#include "../Utils/Macros.mqh"
 | 
						|
#include "Money.mqh"
 | 
						|
#include "VirtualOrder.mqh"
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Класс символьного получателя                                     |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
class CVirtualSymbolReceiver : public CReceiver {
 | 
						|
   string            m_symbol;         // Символ
 | 
						|
   CVirtualOrder     *m_orders[];      // Массив открытых виртуальных позиций
 | 
						|
   
 | 
						|
   bool              m_isNetting;      // Это неттинг-счёт?
 | 
						|
 | 
						|
   double            m_minMargin;      // Минимальная маржа для открытия
 | 
						|
 | 
						|
   CPositionInfo     m_position;       // Объект для получения свойств рыночных позиций
 | 
						|
   CSymbolInfo       m_symbolInfo;     // Объект для получения свойств символа
 | 
						|
   CTrade            m_trade;          // Объект для совершения торговых операций
 | 
						|
 | 
						|
   double            MarketVolume();   // Объём открытых рыночных позиций
 | 
						|
   double            VirtualVolume();  // Объём открытых виртуальных позиций
 | 
						|
   bool              IsTradeAllowed(); // Торговля по символу доступна?
 | 
						|
 | 
						|
   // Необходимая разница объёмов
 | 
						|
   double            DiffVolume(double marketVolume, double virtualVolume);
 | 
						|
 | 
						|
   // Коррекция объема на необходимую разницу
 | 
						|
   bool              Correct(double oldVolume, double diffVolume);
 | 
						|
 | 
						|
   // Вспомогательные методы открытия
 | 
						|
   bool              ClearOpen(double diffVolume);
 | 
						|
   bool              AddBuy(double volume);
 | 
						|
   bool              AddSell(double volume);
 | 
						|
 | 
						|
   // Вспомогательные методы закрытия
 | 
						|
   bool              CloseBuyPartial(double volume);
 | 
						|
   bool              CloseSellPartial(double volume);
 | 
						|
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
 | 
						|
   bool              CloseFull();
 | 
						|
 | 
						|
   // Проверка маржинальных требований
 | 
						|
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);
 | 
						|
 | 
						|
public:
 | 
						|
                     CVirtualSymbolReceiver(string p_symbol);  // Конструктор
 | 
						|
   bool              operator==(const string symbol) {// Оператор сравнения по имени символа
 | 
						|
      return m_symbol == symbol;
 | 
						|
   }
 | 
						|
   void              Open(CVirtualOrder *p_order);    // Регистрация открытия виртуальной позиции
 | 
						|
   void              Close(CVirtualOrder *p_order);   // Регистрация закрытия виртуальной позиции
 | 
						|
 | 
						|
   virtual bool      Correct() override;              // Корректировка открытых объёмов
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Конструктор                                                      |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
CVirtualSymbolReceiver::CVirtualSymbolReceiver(string p_symbol) :
 | 
						|
   m_symbol(p_symbol),
 | 
						|
   m_minMargin(100) {
 | 
						|
   if(!m_symbolInfo.Name(m_symbol)) {
 | 
						|
      PrintFormat(__FUNCTION__"#%s | ERROR: This symbol not found. Trade operations are disabled.", m_symbol);
 | 
						|
      m_minMargin = -1;
 | 
						|
   }
 | 
						|
   ArrayResize(m_orders, 0, 128);
 | 
						|
   m_trade.SetExpertMagicNumber(s_magic);
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Регистрация открытия виртуальной позиции                         |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
 | 
						|
   APPEND(m_orders, p_order); // Добавляем позицию в массив
 | 
						|
   m_isChanged = true;        // Устанавливаем флаг изменений
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Регистрация закрытия виртуальной позиции                         |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
 | 
						|
   REMOVE(m_orders, p_order); // Удаляем позицию из массива
 | 
						|
   m_isChanged = true;        // Устанавливаем флаг изменений
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Корректировка открытых объёмов                                   |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::Correct() {
 | 
						|
   bool res = true;
 | 
						|
   if(m_isChanged && IsTradeAllowed()) {
 | 
						|
      double marketVolume = MarketVolume();
 | 
						|
      double virtualVolume = VirtualVolume();
 | 
						|
      double diffVolume = DiffVolume(marketVolume, virtualVolume);
 | 
						|
 | 
						|
      // Если есть необходимость коррекции объема, то выполняем её
 | 
						|
      if(MathAbs(diffVolume) > 0.001) {
 | 
						|
         res = Correct(marketVolume, diffVolume);
 | 
						|
         if(res) {
 | 
						|
            PrintFormat(__FUNCTION__"#%s | CORRECTED %.2f -> %.2f", m_symbol, marketVolume, virtualVolume);
 | 
						|
         }
 | 
						|
      }
 | 
						|
      m_isChanged = !res;
 | 
						|
   }
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Объём открытых рыночных позиций                                  |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
double CVirtualSymbolReceiver::MarketVolume() {
 | 
						|
   double volume = 0;
 | 
						|
   string symbol;
 | 
						|
   ulong magic;
 | 
						|
   int type;
 | 
						|
 | 
						|
   CPositionInfo p;
 | 
						|
 | 
						|
   for(int i = 0; i < PositionsTotal(); i++) {
 | 
						|
      if(p.SelectByIndex(i)) {
 | 
						|
         symbol = p.Symbol();
 | 
						|
         magic = p.Magic();
 | 
						|
         type = (int) p.PositionType();
 | 
						|
 | 
						|
         if(magic == s_magic && symbol == m_symbol) {
 | 
						|
            volume += p.Volume() * (-(type) * 2 + 1);
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
   return volume;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Объём открытых витруальных позиций                               |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
double CVirtualSymbolReceiver::VirtualVolume() {
 | 
						|
   double volume = 0;
 | 
						|
   FOREACH(m_orders) volume += CMoney::Volume(m_orders[i]);
 | 
						|
   return volume;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Торговля по символу доступна?                                    |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::IsTradeAllowed() {
 | 
						|
   return (true
 | 
						|
           && m_minMargin > 0
 | 
						|
           && m_symbolInfo.TradeMode() == SYMBOL_TRADE_MODE_FULL
 | 
						|
          );
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Необходимая разница объёмов                                      |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
double CVirtualSymbolReceiver::DiffVolume(double marketVolume, double virtualVolume) {
 | 
						|
// Получаем граничные значения допустимых объмов
 | 
						|
   double minLot = MathMax(0.01, m_symbolInfo.LotsMin());
 | 
						|
   double maxLot = m_symbolInfo.LotsMax();
 | 
						|
   double lotStep = MathMax(0.01, m_symbolInfo.LotsStep());
 | 
						|
 | 
						|
// Находим, на сколько надо изменить объём открытых позиций по символу
 | 
						|
   double oldVolume = marketVolume;
 | 
						|
   double newVolume = virtualVolume;
 | 
						|
   int ratio = 0;
 | 
						|
 | 
						|
// Проверяем, что новый объем укладывается в допустимые рамки
 | 
						|
   if(MathAbs(newVolume) > maxLot) {
 | 
						|
      newVolume = maxLot * MathAbs(newVolume) / newVolume;
 | 
						|
   }
 | 
						|
 | 
						|
   if(MathAbs(newVolume) < minLot && MathAbs(newVolume) > 0) {
 | 
						|
      if(MathAbs(newVolume) < 0.5 * minLot) {
 | 
						|
         newVolume = 0;
 | 
						|
      } else {
 | 
						|
         newVolume = minLot * MathAbs(newVolume) / newVolume;
 | 
						|
      }
 | 
						|
   }
 | 
						|
// На сколько надо изменить открытый объем
 | 
						|
   double diffVolume = newVolume - oldVolume;
 | 
						|
   int digits = 2;
 | 
						|
 | 
						|
   if (lotStep >= 0.1 && lotStep < 1.0) {
 | 
						|
      digits = 1;
 | 
						|
   } else if (lotStep >= 1.0) {
 | 
						|
      digits = 0;
 | 
						|
   }
 | 
						|
 | 
						|
   if(oldVolume == 0) {
 | 
						|
      if (minLot >= 0.1 && lotStep < 1.0) {
 | 
						|
         digits = 1;
 | 
						|
      } else if (minLot >= 1.0) {
 | 
						|
         digits = 0;
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   diffVolume = NormalizeDouble(diffVolume, digits);
 | 
						|
 | 
						|
   ratio = (int) MathRound(MathAbs(diffVolume) / lotStep);
 | 
						|
   if(MathAbs(ratio * lotStep - MathAbs(diffVolume)) > 0.0000001) {
 | 
						|
      diffVolume = ratio * lotStep * MathAbs(diffVolume) / diffVolume;
 | 
						|
   }
 | 
						|
 | 
						|
   return diffVolume;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Коррекция объема на необходимую разницу                          |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::Correct(double oldVolume, double diffVolume) {
 | 
						|
   bool res = false;
 | 
						|
 | 
						|
   double volume = MathAbs(diffVolume);
 | 
						|
 | 
						|
   if(oldVolume > 0) { // Have BUY position
 | 
						|
      if(diffVolume > 0) { // New BUY position
 | 
						|
         res = AddBuy(volume);
 | 
						|
      } else if(diffVolume < 0) { // New SELL position
 | 
						|
         if(volume < oldVolume) {
 | 
						|
            res = CloseBuyPartial(volume);
 | 
						|
         } else {
 | 
						|
            res = CloseFull();
 | 
						|
 | 
						|
            if(res && volume > oldVolume) {
 | 
						|
               res = AddSell(volume - oldVolume);
 | 
						|
            }
 | 
						|
         }
 | 
						|
      }
 | 
						|
   } else if(oldVolume < 0) { // Have SELL position
 | 
						|
      if(diffVolume < 0) { // New SELL position
 | 
						|
         res = AddSell(volume);
 | 
						|
      } else if(diffVolume > 0) { // New BUY position
 | 
						|
         if(volume < -oldVolume) {
 | 
						|
            res = CloseSellPartial(volume);
 | 
						|
         } else {
 | 
						|
            res = CloseFull();
 | 
						|
 | 
						|
            if(res && volume > -oldVolume) {
 | 
						|
               res = AddBuy(volume + oldVolume);
 | 
						|
            }
 | 
						|
         }
 | 
						|
      }
 | 
						|
   } else { // No old position
 | 
						|
      res = ClearOpen(diffVolume);
 | 
						|
   }
 | 
						|
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Открытие рыночной позиции BUY или SELL                           |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::ClearOpen(double diffVolume) {
 | 
						|
   double volume = MathAbs(diffVolume);
 | 
						|
   double minLot = MathAbs(m_symbolInfo.LotsMin());
 | 
						|
 | 
						|
   if(minLot < 1e-12 || volume < minLot) {
 | 
						|
      return true;
 | 
						|
   }
 | 
						|
 | 
						|
   bool res = true;
 | 
						|
   ENUM_ORDER_TYPE type = (diffVolume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
 | 
						|
 | 
						|
   if(FreeMarginCheck(volume, type)) {
 | 
						|
      PrintFormat(__FUNCTION__"#%s | OPEN %s %.2f", m_symbol, (diffVolume > 0 ? "BUY" : "SELL"), volume);
 | 
						|
 | 
						|
      if(diffVolume > 0) {
 | 
						|
         res = m_trade.Buy(volume, m_symbol);
 | 
						|
      } else {
 | 
						|
         res = m_trade.Sell(volume, m_symbol);
 | 
						|
      }
 | 
						|
 | 
						|
      if(!res) {
 | 
						|
         PrintFormat(__FUNCTION__"#%s | ERROR: %d, Result Code: %d", m_symbol, _LastError, m_trade.ResultRetcode());
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Открытие дополнительного объёма BUY                              |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::AddBuy(double volume) {
 | 
						|
   return ClearOpen(volume);
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Открытие дополнительного объёма SELL                                                                 |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::AddSell(double volume) {
 | 
						|
   return ClearOpen(-volume);
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Частичное закрытие объёма BUY по символу                         |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::CloseBuyPartial(double volume) {
 | 
						|
   bool res = true;
 | 
						|
 | 
						|
   PrintFormat(__FUNCTION__"#%s | CLOSE BUY partial | volume = %.2f", m_symbol, volume);
 | 
						|
 | 
						|
   if(volume > 0) {
 | 
						|
      if(m_isNetting) {
 | 
						|
         res = m_trade.Sell(volume, m_symbol, 0, 0, 0);
 | 
						|
      } else {
 | 
						|
         res = CloseHedgingPartial(volume, POSITION_TYPE_BUY);
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   if(!res) {
 | 
						|
      PrintFormat(__FUNCTION__"#%s | ERROR: %d, Result Code: %d", m_symbol, _LastError, m_trade.ResultRetcode());
 | 
						|
      ResetLastError();
 | 
						|
   }
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Частичное закрытие объёма SELL по символу                        |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::CloseSellPartial(double volume) {
 | 
						|
   bool res = true;
 | 
						|
 | 
						|
   PrintFormat(__FUNCTION__"#%s | CLOSE SELL partial | volume = %.2f", m_symbol, volume);
 | 
						|
 | 
						|
   if(volume > 0) {
 | 
						|
      if(m_isNetting) {
 | 
						|
         res &= m_trade.Buy(volume, m_symbol, 0, 0, 0);
 | 
						|
      } else {
 | 
						|
         res &= CloseHedgingPartial(volume, POSITION_TYPE_SELL);
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   if(!res) {
 | 
						|
      PrintFormat(__FUNCTION__"#%s | ERROR: %d, Result Code: %d", m_symbol, _LastError, m_trade.ResultRetcode());
 | 
						|
      ResetLastError();
 | 
						|
   }
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Частичное закрытие BUY или SELL по символу на счете Hedge        |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type) {
 | 
						|
   bool res = true;
 | 
						|
 | 
						|
   ulong ticket;
 | 
						|
   double positionVolume;
 | 
						|
 | 
						|
   for(int i = 0; i < PositionsTotal(); i++) {
 | 
						|
      if (m_position.SelectByIndex(i)) {
 | 
						|
         ticket = m_position.Ticket();
 | 
						|
 | 
						|
         if(m_position.Magic() == s_magic && m_position.Symbol() == m_symbol && m_position.PositionType() == type) {
 | 
						|
            positionVolume = m_position.Volume();
 | 
						|
 | 
						|
            if(volume > 0) {
 | 
						|
               if(positionVolume <= volume) {
 | 
						|
                  res &= m_trade.PositionClose(ticket);
 | 
						|
                  volume -= positionVolume;
 | 
						|
               } else {
 | 
						|
                  res &= m_trade.PositionClosePartial(ticket, volume);
 | 
						|
                  volume = 0;
 | 
						|
                  break;
 | 
						|
               }
 | 
						|
            } else {
 | 
						|
               break;
 | 
						|
            }
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   if(volume > 0) {
 | 
						|
      res = false;
 | 
						|
   }
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Полное закрытие объёма по символу                                |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::CloseFull() {
 | 
						|
   bool res = true;
 | 
						|
 | 
						|
   ulong ticket;
 | 
						|
   bool found = true;
 | 
						|
 | 
						|
   while(found && !IsStopped()) {
 | 
						|
      found = false;
 | 
						|
      for(int i = 0; i < PositionsTotal(); i++) {
 | 
						|
         if (m_position.SelectByIndex(i)) {
 | 
						|
            if(m_position.Magic() == s_magic && (m_position.Symbol() == m_symbol)) {
 | 
						|
               found = true;
 | 
						|
               ticket = m_position.Ticket();
 | 
						|
               res &= m_trade.PositionClose(ticket);
 | 
						|
               break;
 | 
						|
            }
 | 
						|
         }
 | 
						|
      }
 | 
						|
      if(!res) {
 | 
						|
         found = false;
 | 
						|
      }
 | 
						|
   }
 | 
						|
   return res;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Проверка маржинальных требований                                 |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
bool CVirtualSymbolReceiver::FreeMarginCheck(double volume, ENUM_ORDER_TYPE type) {
 | 
						|
   double freeMarginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
 | 
						|
 | 
						|
   if (freeMarginLevel != 0 && freeMarginLevel < m_minMargin) {
 | 
						|
      PrintFormat(__FUNCTION__" | Margin level (%.2f) is less than minimum required (%.2f)",
 | 
						|
                  freeMarginLevel, m_minMargin);
 | 
						|
      return false;
 | 
						|
   }
 | 
						|
 | 
						|
#ifdef __MQL4__
 | 
						|
   double free_margin = AccountFreeMarginCheck(m_symbol, type, volume);
 | 
						|
//-- если денег не хватает
 | 
						|
   if(free_margin < 0) {
 | 
						|
      PrintFormat(__FUNCTION__" | ERROR: Not enough money for %s %.2f, %s",
 | 
						|
                  (type == OP_BUY) ? "BUY" : "SELL", volume, m_symbol);
 | 
						|
      return(false);
 | 
						|
   }
 | 
						|
//-- проверка прошла успешно
 | 
						|
   return(true);
 | 
						|
#endif
 | 
						|
 | 
						|
#ifdef __MQL5__
 | 
						|
//--- получим цену открытия
 | 
						|
   MqlTick mqltick;
 | 
						|
   SymbolInfoTick(m_symbol, mqltick);
 | 
						|
   double price = mqltick.ask;
 | 
						|
   if(type == ORDER_TYPE_SELL) {
 | 
						|
      price = mqltick.bid;
 | 
						|
   }
 | 
						|
//--- значения необходимой и свободной маржи
 | 
						|
   double margin, free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
 | 
						|
//--- вызовем функцию проверки
 | 
						|
   if(!OrderCalcMargin(type, m_symbol, volume, price, margin)) {
 | 
						|
      //--- что-то пошло не так, сообщим и вернем false
 | 
						|
      PrintFormat(__FUNCTION__" | ERROR: Can't calc margin via OrderCalcMargin(), code=%d", GetLastError());
 | 
						|
      return(false);
 | 
						|
   }
 | 
						|
//--- если не хватает средств на проведение операции
 | 
						|
   if(margin > free_margin) {
 | 
						|
      //--- сообщим об ошибке и вернем false
 | 
						|
      PrintFormat(__FUNCTION__" | ERROR: Not enough money for %s %.2f, %s",
 | 
						|
                  (type == ORDER_TYPE_BUY) ? "BUY" : "SELL", volume, m_symbol);
 | 
						|
      return(false);
 | 
						|
   }
 | 
						|
//--- проверка прошла успешно
 | 
						|
   return(true);
 | 
						|
#endif
 | 
						|
}
 | 
						|
//+------------------------------------------------------------------+
 |