Adwizard/Virtual/VirtualAdvisor.mqh
2025-04-11 13:28:40 +03:00

600 lines
48 KiB
MQL5

//+------------------------------------------------------------------+
//| VirtualAdvisor.mqh |
//| Copyright 2019-2025, Yuriy Bykov |
//| https://www.mql5.com/ru/users/antekov |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019-2025, Yuriy Bykov"
#property link "https://www.mql5.com/ru/users/antekov"
#property version "1.11"
class CVirtualStrategyGroup;
#include "../Base/Advisor.mqh"
#include "../Utils/NewBarEvent.mqh"
#include "../Utils/SymbolsMonitor.mqh"
#include "VirtualRiskManager.mqh"
#include "VirtualInterface.mqh"
#include "VirtualReceiver.mqh"
#include "VirtualStrategyGroup.mqh"
#include "TesterHandler.mqh"
//+------------------------------------------------------------------+
//| Класс эксперта, работающего с виртуальными позициями (ордерами) |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
CSymbolsMonitor *m_symbols; // Объект монитора символов
CVirtualReceiver *m_receiver; // Объект получателя, выводящий позиции на рынок
CVirtualInterface *m_interface; // Объект интерфейса для показа состояния пользователю
CVirtualRiskManager *m_riskManager; // Объект риск-менеджера
string m_fileName; // Название файла с базой данных эксперта
datetime m_lastSaveTime; // Время последнего сохранения
bool m_useOnlyNewBar; // Обрабатывать только тики нового бара
datetime m_fromDate; // Дата начала работы
string m_paramsNorm; // Параметры группы стратегий после нормировки
virtual void Add(CVirtualStrategyGroup *p_group); // Метод добавления группы стратегий
static int s_groupId; // ID загруженной из базы данных группы стратегий
CVirtualAdvisor(string p_param); // Конструктор
public:
STATIC_CONSTRUCTOR(CVirtualAdvisor);
~CVirtualAdvisor(); // Деструктор
virtual string operator~() override; // Преобразование объекта в строку
virtual void Tick() override; // Обработчик события OnTick
virtual double Tester() override; // Обработчик события OnTester
// Обработчик события OnChartEvent (пока не используется)
virtual void ChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam);
virtual bool Save(); // Сохранение состояния
virtual bool Load(); // Загрузка состояния
// Замена названий символов
bool SymbolsReplace(const string p_symbolsReplace);
// Проверка наличия новой группы стратегий в базе данных эксперта
bool CheckUpdate();
// Экспорт текущей группы стратегий в заданную базу данных эксперта
void Export(string p_groupName, string p_advFileName);
// Обработчик события OnTesterInit
static int TesterInit(ulong p_idTask = 0, string p_fileName = NULL);
static void TesterPass(); // Обработчик события OnTesterDeinit
static void TesterDeinit(); // Обработчик события OnTesterDeinit
// Имя файла с базой данных эксперта
static string FileName(string p_name, ulong p_magic = 1);
// Получение строки инициализации группы стратегий
// из базы данных эксперта с заданным идентификатором
static string Import(string p_fileName, int p_groupId = 0);
};
int CVirtualAdvisor::s_groupId = 0;
REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);
//+------------------------------------------------------------------+
//| Метод добавления группы стратегий |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Add(CVirtualStrategyGroup *p_group) {
// Если в этой группе содержатся другие группы, то добавляем каждую из них
FOREACH(p_group.m_groups) {
CVirtualAdvisor::Add(p_group.m_groups[i]);
delete p_group.m_groups[i];
}
// Если в этой группе содержатся стратегии, то добавляем каждую из них
FOREACH(p_group.m_strategies) CAdvisor::Add(p_group.m_strategies[i]);
}
//+------------------------------------------------------------------+
//| Конструктор |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Запоминаем строку инициализации
m_params = p_params;
// Читаем строку инициализации объекта группы стратегий
string groupParams = ReadObject(p_params);
// Читаем строку инициализации объекта риск-менеджера
string riskManagerParams = ReadObject(p_params);
// Читаем магический номер
ulong p_magic = ReadLong(p_params);
// Читаем название эксперта
string p_name = ReadString(p_params);
// Читаем признак работы на только на открытии бара
m_useOnlyNewBar = (bool) ReadLong(p_params);
// Если нет ошибок чтения, то
if(IsValid()) {
// Создаём группу стратегий
CREATE(CVirtualStrategyGroup, p_group, groupParams);
// Инициализируем монитор символов статическим монитором символов
m_symbols = CSymbolsMonitor::Instance();
// Инициализируем получателя статическим получателем
m_receiver = CVirtualReceiver::Instance(p_magic);
// Инициализируем интерфейс статическим интерфейсом
m_interface = CVirtualInterface::Instance(p_magic);
// Формируем из имени эксперта и параметров имя файла базы данных эксперта для сохранения состояния
m_fileName = FileName(p_name, p_magic);
// Запоминаем время начала работы (тестирования)
m_fromDate = TimeCurrent();
// Сбрасываем время последнего сохранения
m_lastSaveTime = 0;
// Добавляем к эксперту содержимое группы
Add(p_group);
// Удаляем объект группы
delete p_group;
// Создаём объект риск-менеджера
m_riskManager = NEW(riskManagerParams);
}
}
//+------------------------------------------------------------------+
//| Деструктор |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
if(!!m_symbols) delete m_symbols; // Удаляем монитор символов
if(!!m_receiver) delete m_receiver; // Удаляем получатель
if(!!m_interface) delete m_interface; // Удаляем интерфейс
if(!!m_riskManager) delete m_riskManager; // Удаляем риск-менеджер
DestroyNewBar(); // Удаляем объекты отслеживания нового бара
}
//+------------------------------------------------------------------+
//| Преобразование объекта в строку |
//+------------------------------------------------------------------+
string CVirtualAdvisor::operator~() {
return StringFormat("%s(%s)", typename(this), m_params);
}
//+------------------------------------------------------------------+
//| Обработчик события OnTick |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Определяем новый бар по всем нужным символам и таймфреймам
bool isNewBar = UpdateNewBar();
// Если нигде нового бара нет, а мы работаем только по новым барам, то выходим
if(!isNewBar && m_useOnlyNewBar) {
return;
}
// Монитор символов обновляет котировки
m_symbols.Tick();
// Получатель обрабатывает виртуальные позиции
m_receiver.Tick();
// Запуск обработки в стратегиях
CAdvisor::Tick();
// Риск-менеджер обрабатывает виртуальные позиции
m_riskManager.Tick();
// Корректировка рыночных объемов
m_receiver.Correct();
// Сохранение состояния
Save();
// Отрисовка интерфейса
m_interface.Redraw();
}
//+------------------------------------------------------------------+
//| Обработчик события OnTester |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Максимальная абсолютная просадка
double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);
// Прибыль
double profit = TesterStatistics(STAT_PROFIT);
// Фиксированный баланс для торговли из настроек
double fixedBalance = CMoney::FixedBalance();
// Коэффициент возможного увеличения размеров позиций для просадки 10% от fixedBalance_
double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown);
// Пресчитываем прибыль в годовую
long totalSeconds = TimeCurrent() - m_fromDate;
double totalYears = totalSeconds / (365.0 * 24 * 3600);
double fittedProfit = profit * coeff / totalYears;
// Если он не указан, то берём начальный баланс (хотя это будет давать искажённый результат)
if(fixedBalance < 1) {
fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT);
balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
coeff = 0.1 / MathMax(1, balanceDrawdown);
fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears);
}
// Воссоздаём группу использованных стратегий для последующей нормировки
CVirtualStrategyGroup* group = NEW(ReadObject(m_params));
if(!!group) {
// Строка инициализации нормированной группы
m_paramsNorm = group.ToStringNorm(coeff);
FOREACH(m_strategies) ((CVirtualStrategy*)m_strategies[i]).Scale(coeff);
// Выполняем формирование фрейма данных на агенте тестирования
CTesterHandler::Tester(fittedProfit, // Нормированная прибыль
m_paramsNorm // Строка инициализации нормированной группы
);
PrintFormat(__FUNCTION__" | Scale = %.2f\nParams:\n%s", coeff, m_paramsNorm);
PrintFormat(__FUNCTION__" | Scale = %.2f", coeff);
delete group;
}
PrintFormat(__FUNCTION__" |\n%s = %.2f\n%s = %.2f\n%s = %.2f\n%s = %.2f\n%s = %.2f\n",
EnumToString(STAT_BALANCE_DD), TesterStatistics(STAT_BALANCE_DD),
EnumToString(STAT_BALANCE_DD_RELATIVE), TesterStatistics(STAT_BALANCE_DD_RELATIVE),
EnumToString(STAT_EQUITY_DD), TesterStatistics(STAT_EQUITY_DD),
EnumToString(STAT_EQUITY_DD_RELATIVE), TesterStatistics(STAT_EQUITY_DD_RELATIVE),
EnumToString(STAT_EQUITY_DDREL_PERCENT), TesterStatistics(STAT_EQUITY_DDREL_PERCENT)
);
return fittedProfit;
}
//+------------------------------------------------------------------+
//| Экспорт текущей группы стратегий в заданную базу данных эксперта |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Export(string p_groupName, string p_advFileName) {
CTesterHandler::Export(m_strategies, p_groupName, p_advFileName);
}
//+------------------------------------------------------------------+
//| Обработчик событий ChartEvent |
//+------------------------------------------------------------------+
void CVirtualAdvisor::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
m_interface.ChartEvent(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
//| Инициализация перед началом оптимизации |
//+------------------------------------------------------------------+
int CVirtualAdvisor::TesterInit(ulong p_idTask, string p_fileName) {
return CTesterHandler::TesterInit(p_idTask, p_fileName);
}
//+------------------------------------------------------------------+
//| Действия после завершения очередного прохода при оптимизации |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterPass() {
CTesterHandler::TesterPass();
}
//+------------------------------------------------------------------+
//| Действия после завершения оптимизации |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterDeinit() {
CTesterHandler::TesterDeinit();
}
//+------------------------------------------------------------------+
//| Сохранение состояния |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
// Сохраняем состояние, если:
if(true
// появились более поздние изменения
&& m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// и сейчас не оптимизация
&& !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
&& (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
) {
// Если подключение к базе данных эксперта установлено
if(CStorage::Connect(m_fileName)) {
// Сохраняем время последних изменений
CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime);
CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId);
// Сохраняем все стратегии
FOREACH(m_strategies) ((CVirtualStrategy*) m_strategies[i]).Save();
// Сохраняем риск-менеджер
m_riskManager.Save();
// Обновляем время последнего сохранения
m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
PrintFormat(__FUNCTION__" | OK at %s to %s",
TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
m_fileName);
// Закрываем соединение
CStorage::Close();
// Возвращаем результат
return CStorage::Res();
} else {
PrintFormat(__FUNCTION__" | ERROR: Can't open database [%s], LastError=%d",
m_fileName, GetLastError());
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| Загрузка состояния |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
bool res = true;
ulong groupId = 0;
// Загружаем состояние, если:
if(true
// файл существует
&& FileIsExist(m_fileName, FILE_COMMON)
// и сейчас не оптимизация
&& !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
&& (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
) {
// Если подключение к базе данных эксперта установлено
if(CStorage::Connect(m_fileName)) {
// Если время последних изменений загружено и меньше текущего времени
if(CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime)
&& m_lastSaveTime <= TimeCurrent()) {
PrintFormat(__FUNCTION__" | LAST SAVE at %s",
TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
// Если идентификатор сохранённой группы стратегий загружен
if(CStorage::Get("CVirtualAdvisor::s_groupId", groupId)) {
// Загружаем все стратегии, игнорируя возможные ошибки
FOREACH(m_strategies) {
res &= ((CVirtualStrategy*) m_strategies[i]).Load();
}
if(groupId != s_groupId) {
// Действия при запуске эксперта с новой группой стратегий.
PrintFormat(__FUNCTION__" | UPDATE Group ID: %I64u -> %I64u", groupId, s_groupId);
// Сбрасываем возможный признак ошибки при загрузке стратегий
res = true;
string symbols[]; // Массив для названий символоа
// Получаем список всех используемых предыдущей группой символов
CStorage::GetSymbols(symbols);
// Для всех символов создаём символьный получатель.
// Это нужно для корректного закрытия виртуальных позиций
// старой группы стратегий сразу после загрузки новой
FOREACH(symbols) m_receiver[symbols[i]];
}
if(res) {
// Загружаем риск-менеджер
res &= m_riskManager.Load();
if(!res) {
PrintFormat(__FUNCTION__" | ERROR loading risk manager from DB [%s]", m_fileName);
}
} else {
PrintFormat(__FUNCTION__" | ERROR loading strategies from DB [%s]", m_fileName);
}
}
} else {
// Если время последних изменений не найдено или находится в будущем,
// то начинаем работу с чистого листа
PrintFormat(__FUNCTION__" | NO LAST SAVE [%s] - Clear Storage",
TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
CStorage::Clear();
m_lastSaveTime = 0;
}
// Закрываем соединение
CStorage::Close();
}
}
return res;
}
//+------------------------------------------------------------------+
//| Замена названий символов |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) {
// Избавляемся от пробелов в строке замен
StringReplace(p_symbolsReplace, " ", "");
// Если строка замен пустая, то ничего не делаем
if(p_symbolsReplace == "") {
return true;
}
// Переменная для результата
bool res = true;
string symbolKeyValuePairs[]; // Массив для отдельных замен
string symbolPair[]; // Массив для двух имён в одной замене
// Делим строку замен на части, представляющие одну отдельную замену
StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs);
// Словарь для соответствия целевого символа исходному символу
CHashMap<string, string> symbolsMap;
// Для всех отдельных замен
FOREACH(symbolKeyValuePairs) {
// Получаем исходный и целевой символы как два элемента массива
StringSplit(symbolKeyValuePairs[i], '=', symbolPair);
// Проверяем наличие целевого символа в списке доступных символов (не кастомных)
bool custom = false;
res &= SymbolExist(symbolPair[1], custom);
// Если целевой символ не найден, то сообщаем об ошибке и выходим
if(!res) {
PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]);
return res;
}
// Добавляем в словарь новый элемент: ключ - исходный символ, значение - целевой символ
res &= symbolsMap.Add(symbolPair[0], symbolPair[1]);
// Если целевой символ не удалось добавить в словарь, то сообщаем об ошибке и выходим
if(!res) {
PrintFormat(__FUNCTION__" | ERROR: Can't add symbol map pair %s to HashMap. Check your parameter:\n%s",
symbolKeyValuePairs[i], p_symbolsReplace);
return res;
}
}
// Если ошибок не возникло, то для всех стратегий вызываем соответствующий метод замены
FOREACH(m_strategies) res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap);
return res;
}
//+------------------------------------------------------------------+
//| Проверка наличия новой группы стратегий в базе данных эксперта |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::CheckUpdate() {
// Запрос на получение стратегий заданной группы либо последней группы
string query = StringFormat("SELECT MAX(id_group) FROM strategy_groups"
" WHERE to_date <= '%s'",
TimeToString(TimeCurrent(), TIME_DATE));
// Открываем базу данных эксперта
if(DB::Connect(m_fileName, DB_TYPE_ADV)) {
// Выполняем запрос
int request = DatabasePrepare(DB::Id(), query);
// Если нет ошибки
if(request != INVALID_HANDLE) {
// Структура данных для чтения одной строки результата запроса
struct Row {
int groupId;
} row;
// Читаем данные из первой строки результата
while(DatabaseReadBind(request, row)) {
// Запоминаем идентификатор группы стратегий
// в статическом свойстве класса эксперта
return s_groupId < row.groupId;
}
} else {
// Сообщаем об ошибке при необходимости
PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
}
// Закрываем базу данных эксперта
DB::Close();
}
return false;
}
//+------------------------------------------------------------------+
//| Получение строки инициализации группы стратегий |
//| из базы данных эксперта с заданным идентификатором |
//+------------------------------------------------------------------+
string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) {
string params[]; // Массив для строк инициализации стратегий
// Запрос на получение стратегий заданной группы либо последней группы
string query = StringFormat("SELECT id_group, params "
" FROM strategies"
" WHERE id_group = %s;",
(p_groupId > 0 ? (string) p_groupId
: "(SELECT MAX(id_group) FROM strategy_groups WHERE to_date <= '"
+ TimeToString(TimeCurrent(), TIME_DATE) +
"')"));
// Открываем базу данных эксперта
if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
// Выполняем запрос
int request = DatabasePrepare(DB::Id(), query);
// Если нет ошибки
if(request != INVALID_HANDLE) {
// Структура данных для чтения одной строки результата запроса
struct Row {
int groupId;
string params;
} row;
// Читаем данные из первой строки результата
while(DatabaseReadBind(request, row)) {
// Запоминаем идентификатор группы стратегий
// в статическом свойстве класса эксперта
s_groupId = row.groupId;
// Добавляем очередную строку инициализации стратегии в массив
APPEND(params, row.params);
}
} else {
// Сообщаем об ошибке при необходимости
PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
}
// Закрываем базу данных эксперта
DB::Close();
}
// Строка инициализации группы стратегий
string groupParams = NULL;
// Общее количество стратегий в группе
int totalStrategies = ArraySize(params);
// Если стратегии есть, то
if(totalStrategies > 0) {
// Соединяем их строки инициализации через запятую
JOIN(params, groupParams, ",");
// Создаём строку инициализации группы стратегий
groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)",
groupParams,
totalStrategies);
}
// Возвращаем строку инициализации группы стратегий
return groupParams;
}
//+------------------------------------------------------------------+
//| Имя файла с базой данных эксперта |
//+------------------------------------------------------------------+
string CVirtualAdvisor::FileName(string p_name, ulong p_magic = 1) {
return StringFormat("%s-%d%s.db.sqlite",
(p_name != "" ? p_name : "Expert"),
p_magic,
(MQLInfoInteger(MQL_TESTER) ? ".test" : "")
);
}
//+------------------------------------------------------------------+