471 lines
34 KiB
MQL5
471 lines
34 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
|
|
}
|
|
//+------------------------------------------------------------------+
|