Учет тональности новостей для защиты торговой стратегии на MQL5
Содержание
- Проблема: неожиданные новости постоянно нарушают работу моей стратегии
- Основная концепция: тональность новостей как динамический фильтр
- Как это работает на практике
- Реализация: интеграция в советники
- Пример советника
- Заключение
- Важные замечания
Введение
Проблема: неожиданные новости постоянно нарушают работу моей стратегии
В современной автоматизированной торговле даже работу самой надежной торговой стратегии могут нарушить срочные новости. Ситуация на финансовых рынках меняется стремительно, и в нынешних геополитических условиях неожиданные события способны за считанные минуты свести на нет самые продуманные торговые решения. Например, внезапная эскалация напряженности на Ближнем Востоке или резкие изменения макроэкономических перспектив могут мгновенно вызвать волатильность, колебания курсов валют и цепную реакцию на мировых рынках. Внедрение механизма снижения этого риска стало необходимостью.
Основная концепция: тональность новостей как динамический фильтрБольшинство трейдеров пользуются экономическими календарями, чтобы избегать торговли в период выхода важных новостей. Однако направление влияния новостей зачастую имеет большее значение, чем сама по себе волатильность. Используя API, который в режиме реального времени собирает и анализирует мировые финансовые новости, мы можем создать фильтр, который:
- Подтверждает входы в позицию: открывает позицию только в том случае, если её направление согласуется с тональностью новостей.
- Защищает прибыль / минимизирует убытки: закрывает открытую позицию, если тональность новостей меняется на противоположную направлению текущей сделки (обратный сигнал).
Таким образом, основная идея защиты стратегии от сбоев, вызванных поступающими новостями, заключается в том, чтобы использовать новости для дополнительного подтверждения входов в позицию и учитывать их при оценке условий выхода из позиции.
Как это работает на практике
Чтобы понять, как работает фильтр тональности в реальной торговой ситуации, давайте рассмотрим конкретный пример.

Предположим, что у нас есть советник, который выполняет вход в длинные позиции, когда быстрая скользящая средняя пересекает медленную скользящую среднюю снизу вверх, и выход из позиций, когда RSI входит в зону перекупленности. Без фильтра тональности этот советник открывал бы длинную позицию при каждом пересечении скользящих средних, независимо от содержания новостей.
Теперь добавим фильтр тональности. Допустим, что текущая тональность новостей по данному инструменту — медвежья ("bearish"), а оценка уверенности составляет 85%. Даже если условие пересечения скользящих средних выполнено, советник не откроет длинную позицию, поскольку тональность новостей не подтверждает бычью направленность. Эта сделка просто пропускается. Это особенно полезно в периоды, когда сигналы технических индикаторов противоречат сильным негативным новостям.
Теперь рассмотрим обратную ситуацию: предположим, что советник уже удерживает длинную позицию. Рынок движется в нашу пользу, и позиция находится в прибыли. Внезапно происходит важное новостное событие, и тональность новостей по данному инструменту меняется на медвежью ("bearish") с оценкой уверенности 72%. Наш советник обнаруживает это изменение и запускает досрочный выход из позиции, несмотря на то, что RSI ещё не вошёл в зону перекупленности, а технический стоп расположен довольно далеко. В результате мы избегаем более глубокой просадки, которая могла бы возникнуть, если бы советник продолжал слепо удерживать позицию.
Рассмотрим другой сценарий: тональность новостей нейтральная ("neutral") либо оценка уверенности очень низкая. В этом случае в большинстве реализаций тональность новостей просто не будет учитываться при входе в позицию и выходе из неё, и советник будет торговать только по техническим сигналам. Это предотвращает чрезмерную реакцию советника на новостной шум или неоднозначные новости.
Главное заключается в том, что фильтр тональности не заменяет основную торговую логику советника. Вместо этого он служит дополнительным уровнем подтверждения и защиты. Это помогает:
- Избегать входа в позицию, когда тональность новостей явно противоречит предполагаемому направлению сделки.
- Досрочно выходить из позиций, когда фундаментальные новости начинают двигать рынок против открытой позиции.
- Оставаться вне рынка в периоды высокой неопределённости или смешанной тональности новостей.
Такой двухуровневый подход особенно ценен на быстро меняющихся рынках: индикаторы могут отставать от ценового движения, а волатильность, вызванная новостями, может быстро привести к значительным колебаниям. Тональность текста не обязательно соответствует направлению движения цены. Заголовок с бычьей тональностью может не привести к росту цены, если новость уже учтена в цене ("покупай на слухах, продавай на фактах"). В некоторых случаях участники реагируют нелинейно (например, "плохая новость — это хорошая новость"). Поэтому рассматривайте анализ тональности как вспомогательный инструмент. Прежде чем принимать торговые решения, сочетайте анализ тональности с анализом цены, объёма, ликвидности и макроэкономического контекста (времени наступления событий, ожиданий, позиционирования участников рынка).
Реализация: интеграция в советники
Интегрировать такой механизм в советник довольно просто.

Для взаимодействия с REST API используется функция WebRequest языка MQL5. API возвращает данные в формате JSON, содержащие текущую тональность новостей — "bullish", "bearish" или "neutral" — и оценку уверенности.
Подготовка данных
Сначала мы определяем структуру для хранения тональности новостей и объявляем необходимые данные на глобальном уровне, чтобы информация о тональности сохранялась на протяжении всего времени работы советника. Мы создаём структуру SentimentData для хранения заголовка, прогноза тональности со значением "bullish", "bearish" или "neutral", оценки уверенности в диапазоне от 0,0 до 1,0, а также источника каждой новости, который может принимать значение "live" или "cache". Мы также объявляем массив фиксированного размера для хранения как текущей тональности, так и краткой истории предыдущих новостей. Затем определяем инструмент, который хотим проанализировать (например, "EURUSD"), URL-адрес API и ключ API. Эта конфигурация минимальна, но достаточна для взаимодействия с API анализа тональности и структурированного хранения возвращаемых данных.
struct SentimentData { string headline; string prediction; double confidence; string source; }; #define NEWS_ITEM_NUM 10 // this size accommodates both the current news item and historical items SentimentData sentimentHistory[NEWS_ITEM_NUM]; const string newsInstrument = "EURUSD"; const string apiUrl = "https://trading-sentiment-aggregator-ai.p.rapidapi.com/analyze"; const string apiKey = "your-api-key";
Получение данных о тональности
На этом этапе выполняется вызов внешнего API и получение последних данных о тональности новостей по выбранному инструменту. API, возвращающий текущую тональность новостей по интересующему нас инструменту, называется Trading Sentiment Aggregator AI и доступен в рамках бесплатного тарифного плана на портале RapidAPI. Этот API автоматически собирает финансовые новости из различных источников и включает модель искусственного интеллекта, специализированную на финансовой тематике, которая мгновенно прогнозирует, указывают ли заголовки на бычью или медвежью направленность рынка, и выдает оценку уверенности.
Вы можете вызвать API для любого инструмента (валютной пары, индекса, акции и т. д.), если он представлен в основном источнике новостных заголовков — RSS-канале Yahoo Finance. Анализ тональности проводится на основе отдельных заголовков, которые обрабатываются на английском языке по мере поступления из источников. Если заголовок новости по инструменту не изменился с момента последнего запроса, возвращается тот же заголовок. Механизм кэширования на стороне API также обеспечивает быстрые ответы, что позволяет свести к минимуму блокировку при синхронных вызовах.
Мы используем функцию WebRequest в MQL5 для отправки POST-запроса к API, указывая в нём название инструмента и количество предыдущих новостей, которые хотим получить в ответе. API возвращает данные в формате JSON, которые мы разбираем с помощью библиотеки "Сериализация и десериализация JSON (на MQL)" для MetaTrader 5. Если запрос завершился успешно (HTTP 200), мы извлекаем текущую тональность и массив истории, если он присутствует, а затем сохраняем их в нашем глобальном массиве sentimentHistory. Мы также проверяем, поступил ли новый заголовок с момента последнего запроса, и в этом случае выводим сообщение в журнал. WebRequest работает в синхронном режиме, поэтому мы вызываем его через OnTimer с регулярными интервалами (например, каждые 4 часа), чтобы не блокировать советник на каждом тике.
bool fetchSentimentData() { char postData[], result[]; string resultHeaders; string headers = "Content-Type: application/json\r\n" + "x-rapidapi-host: trading-sentiment-aggregator-ai.p.rapidapi.com\r\n" + "x-rapidapi-key: " + apiKey + "\r\n"; CJAVal payload; payload["instrument"] = newsInstrument; payload["lookback"] = NEWS_ITEM_NUM - 1; // max number of past news we want in addition to the current one string jsonPayload = payload.Serialize(); StringToCharArray(jsonPayload, postData, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(postData, ArraySize(postData) - 1); ResetLastError(); int res = WebRequest("POST", apiUrl, headers, 5000, postData, result, resultHeaders); if(res == 200) { string responseText = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); CJAVal response; if(response.Deserialize(responseText)) { string prevHeadline = sentimentHistory[0].headline; //--- reset sentiment history for(int i=0; i < ArraySize(sentimentHistory); i++) { sentimentHistory[i].headline = ""; sentimentHistory[i].prediction = "none"; sentimentHistory[i].confidence = 0.0; sentimentHistory[i].source = ""; } //--- manage current news data sentimentHistory[0].headline = response["current"]["headline"].ToStr(); sentimentHistory[0].prediction = response["current"]["prediction"].ToStr(); sentimentHistory[0].confidence = response["current"]["confidence"].ToDbl(); sentimentHistory[0].source = response["current"]["source"].ToStr(); if(StringCompare(prevHeadline, sentimentHistory[0].headline, false) != 0) { Print(__FUNCTION__, " > New sentiment: \"", sentimentHistory[0].headline, "\". Prediction=", sentimentHistory[0].prediction, "; confidence=", sentimentHistory[0].confidence, "; source=", sentimentHistory[0].source ); } //--- manage historical news data if(response.HasKey("history")) { CJAVal historyArray = response["history"]; int downloadedHistorySize = historyArray.Size(); int sentimentHistoryArrayIdx = 1; for(int i = 0; i < downloadedHistorySize && sentimentHistoryArrayIdx < ArraySize(sentimentHistory); i++, sentimentHistoryArrayIdx++) { sentimentHistory[sentimentHistoryArrayIdx].headline = historyArray[i]["headline"].ToStr(); sentimentHistory[sentimentHistoryArrayIdx].prediction = historyArray[i]["prediction"].ToStr(); sentimentHistory[sentimentHistoryArrayIdx].confidence = historyArray[i]["confidence"].ToDbl(); sentimentHistory[sentimentHistoryArrayIdx].source = historyArray[i]["source"].ToStr(); } } return true; } } else { Print(__FUNCTION__, " > Could not retrieve sentiment for instrument ", newsInstrument, " (last error: ", GetLastError(), ", http code: ", res, ")"); } return false; }
Небольшая вспомогательная функция
Чтобы помочь нашему советнику принимать решения о покупке и продаже, мы хотим передавать ему данные о бычьей или медвежьей тональности новостей. Поскольку загруженные новости могут быть и нейтральными, мы добавляем вспомогательную функцию, которая возвращает первое значимое событие (если оно есть) или пустой элемент
SentimentData getInstrumentSentiment()
{
SentimentData retData = {"", "none", 0.0, ""};
for(int i = 0; i < ArraySize(sentimentHistory); i++)
{
if(sentimentHistory[i].prediction != "neutral")
{
retData = sentimentHistory[i];
break;
}
}
return retData;
} Фильтрация входов в позицию
Получив актуальные данные о тональности, мы можем использовать их как динамический фильтр для входов в позицию. Идея проста: даже если выполнены все технические условия для входа в позицию (например, пересечение скользящих средних, сигналы индикаторов), вход будет выполнен только в том случае, если текущая тональность соответствует намеченному направлению, а оценка уверенности достаточно высока. Таким образом, данные о тональности служат окончательным разрешением на вход в позицию. Например, для ордера на покупку мы требуем, чтобы тональность имела значение "bullish", а оценка уверенности превышала заданный порог (например, 0,7). Для ордера на продажу требуется значение тональности "bearish" с аналогичной минимальной оценкой уверенности. Этот механизм не позволяет советнику входить в позицию, когда тональность новостей явно противоречит техническому сигналу, что может быть особенно полезно во время важных новостных событий или резких изменений на рынке.
bool buyCondition = true; // <-- placeholder for your strategy const double minConfidence = 0.7; SentimentData sentiment = getInstrumentSentiment(); bool newsSentimentOk = (sentiment.prediction == "bullish" && sentiment.confidence >= minConfidence); if(buyCondition && newsSentimentOk) { // Proceed with the trade }
Оценка условий выхода из позиции
Определение подходящего момента для закрытия позиций — одна из самых сложных задач в торговле, будь то ручная или автоматическая. Эти же данные о тональности можно использовать и для улучшения выхода из позиций. Вместо того чтобы полагаться исключительно на фиксированные уровни тейк-профита и стоп-лосса или чисто технические сигналы выхода из позиции, мы можем закрыть открытую позицию, если тональность сменится на противоположную её направлению. Например, если у нас открыта длинная позиция, а тональность внезапно становится выраженно медвежьей, мы можем выйти из позиции досрочно, чтобы ограничить потенциальные убытки. И наоборот, если у нас открыта короткая позиция, а тональность становится выраженно бычьей, мы можем закрыть короткую позицию. Такой подход помогает сохранить прибыль и сократить просадки, когда фундаментальные новости начинают двигать рынок в направлении, противоположном направлению открытой позиции. Для принятия решения о выходе из позиции тональность оценивается с помощью отдельной функции, которая проверяет наличие "смены тональности" относительно текущего типа позиции.
Вот как могла бы выглядеть такая функция.
bool determineSentimentReversal() { SentimentData currentSentiment = getInstrumentSentiment(); if(currentSentiment.prediction == "none" || currentSentiment.prediction == "neutral") { return false; } ulong tkt = 0; bool positionExists = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { tkt = PositionGetTicket(i); if(tkt > 0 && PositionGetInteger(POSITION_MAGIC) == magicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { positionExists = true; break; } } if(!positionExists) return false; const double minConfidenceReversal = 0.55; bool isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); bool isBearishReversal = (isLong && currentSentiment.prediction == "bearish" && currentSentiment.confidence >= minConfidenceReversal); bool isBullishReversal = (!isLong && currentSentiment.prediction == "bullish" && currentSentiment.confidence >= minConfidenceReversal); return (isBearishReversal || isBullishReversal); }
Эту функцию легко использовать следующим образом:
bool exitCondition = true; // <-- placeholder for your strategy bool isSentimentReversal = determineSentimentReversal(); if(exitCondition && isSentimentReversal) { // Exit trade }
Пример советника
А теперь рассмотрим рабочий пример: простой советник, использующий пересечение двух простых скользящих средних как сигнал входа, а RSI — как сигнал выхода. Как для сигналов входа в позицию, так и для сигналов выхода из позиции в качестве дополнительного критерия учитывается текущая тональность новостей.
//+------------------------------------------------------------------+ //| Example EA - News Sentiment.mq5 | //+------------------------------------------------------------------+ #define VERSION "1.00" #property version VERSION #property strict #include <Trade/Trade.mqh> #include <Indicators/Oscilators.mqh> #include <JAson.mqh> // input parameters input group "<--- Trade Management --->"; input double inpLots = 0.1; // Lots input int maFastLen = 20; // Length of Fast SMA input int maSlowLen = 40; // Length of Slow SMA input group "<--- Profit Management --->"; input double tpPricePerc = 0.0; // TP Price % (0: disabled) input int rsiPeriod = 7; // RSI Period (0: disabled) input int rsiOverbought = 80; // RSI Overbought input int rsiOversold = 20; // RSI Oversold input group "<--- Risk Management --->"; input double slPricePerc = 1.0; // SL Price % (0: disabled) input group "<--- Sentiment Settings --->"; input string sentimentInstrument = ""; // Sentiment Instrument (empty: disabled) input ENUM_TIMEFRAMES sentimentUpdateFreq = PERIOD_H4; // Sentiment Update Frequency input double minConfSentEntry = 0.7; // Min. Confidence for Sentiment Entry input double minConfSentExit = 0.5; // Min. Confidence for Sentiment Exit input group "<--- Other Settings --->"; input ulong magicNumber = 0; // Magic Number //--- globals CTrade trade; //--- SMA variables int hFast = INVALID_HANDLE, hSlow = INVALID_HANDLE; double maFasts[], maSlows[]; CiRSI rsi; // RSI indicator providing exit signals //--- data for news sentiment struct SentimentData { string headline; string prediction; double confidence; string source; }; #define NEWS_ITEM_NUM 10 SentimentData sentimentHistory[NEWS_ITEM_NUM]; const string apiUrl = "https://trading-sentiment-aggregator-ai.p.rapidapi.com/analyze"; const string apiKey = "your-api-key"; // Note: protect your API key. Keeping it in source code is not a good security practice //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(magicNumber); hFast = iMA(_Symbol, PERIOD_CURRENT, maFastLen, 0, MODE_SMA, PRICE_CLOSE); if(hFast == INVALID_HANDLE) { Print(__FUNCTION__, " > Could not initialize Fast MA"); return INIT_FAILED; } hSlow = iMA(_Symbol, PERIOD_CURRENT, maSlowLen, 0, MODE_SMA, PRICE_CLOSE); if(hSlow == INVALID_HANDLE) { Print(__FUNCTION__, " > Could not initialize Slow MA"); return INIT_FAILED; } if(rsiPeriod > 0) { if(!rsi.Create(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE)) { Print(__FUNCTION__, " > Could not initialize RSI indicator"); return INIT_FAILED; } } ArraySetAsSeries(maFasts, true); ArraySetAsSeries(maSlows, true); if(StringLen(sentimentInstrument) > 0 && !MQLInfoInteger(MQL_TESTER)) { int timerSeconds = PeriodSeconds(sentimentUpdateFreq); EventSetTimer(timerSeconds); fetchSentimentData(); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(hFast != INVALID_HANDLE) { IndicatorRelease(hFast); hFast = INVALID_HANDLE; } if(hSlow != INVALID_HANDLE) { IndicatorRelease(hSlow); hSlow = INVALID_HANDLE; } if(rsiPeriod > 0) rsi.FullRelease(true); ArrayFree(maFasts); ArrayFree(maSlows); if(StringLen(sentimentInstrument) > 0 && !MQLInfoInteger(MQL_TESTER)) EventKillTimer(); } //+------------------------------------------------------------------+ //| Expert OnTimer function | //+------------------------------------------------------------------+ void OnTimer() { if(!MQLInfoInteger(MQL_TESTER)) fetchSentimentData(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; datetime curBarTime = iTime(_Symbol, PERIOD_M1, 0); if(lastBarTime != curBarTime) { lastBarTime = curBarTime; return; } if(positionsTotal() <= 0) // currently not trading. Manage entry { //--- determine entry conditions bool longCondition = false, shortCondition = false; if(CopyBuffer(hFast, 0, 0, maFastLen, maFasts) != -1 && CopyBuffer(hSlow, 0, 0, maSlowLen, maSlows) != -1) { longCondition = crossover(maFasts, maSlows); shortCondition = crossunder(maFasts, maSlows); } //--- get latest sentiment for configured instrument SentimentData curSentiment = {"", "none", 0.0, ""}; if(StringLen(sentimentInstrument) > 0) { curSentiment = getInstrumentSentiment(); } bool newsSentimentOk = true; if(longCondition) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double slPrice = slPricePerc > 0 ? NormalizeDouble(ask - ask * (slPricePerc / 100), _Digits) : 0; double tpPrice = tpPricePerc > 0 ? NormalizeDouble(ask + ask * (tpPricePerc / 100), _Digits) : 0; if(!MQLInfoInteger(MQL_TESTER) && StringLen(sentimentInstrument)) { newsSentimentOk = curSentiment.prediction == "bullish" && curSentiment.confidence >= minConfSentEntry; } if(newsSentimentOk) trade.Buy(inpLots, _Symbol, ask, slPrice, tpPrice); } else if(shortCondition) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double slPrice = slPricePerc > 0 ? NormalizeDouble(bid + bid * (slPricePerc / 100), _Digits) : 0; double tpPrice = tpPricePerc > 0 ? NormalizeDouble(bid - bid * (tpPricePerc / 100), _Digits) : 0; if(!MQLInfoInteger(MQL_TESTER) && StringLen(sentimentInstrument)) { newsSentimentOk = curSentiment.prediction == "bearish" && curSentiment.confidence >= minConfSentEntry; } if(newsSentimentOk) trade.Sell(inpLots, _Symbol, bid, slPrice, tpPrice); } } else // currently trading. Manage exit { //--- determine exit conditions for RSI-based TP if(rsiPeriod > 0) { rsi.Refresh(); bool exitLongCond = rsi.Main(0) > rsiOverbought; bool exitShortCond = rsi.Main(0) < rsiOversold; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tkt = PositionGetTicket(i); if(tkt > 0 && PositionGetInteger(POSITION_MAGIC) == magicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { if(PositionGetDouble(POSITION_PROFIT) > 0.0) { if(exitLongCond || exitShortCond) { closeOpenPositions(); return; } } } } } //--- check if a sentiment reversal exists if(StringLen(sentimentInstrument) > 0) { if(determineSentimentReversal()) { closeOpenPositions(); } } } } //+------------------------------------------------------------------+ //| Count EA's positions | //+------------------------------------------------------------------+ int positionsTotal() { int totalPos = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetTicket(i) != 0) { if(PositionGetInteger(POSITION_MAGIC) == magicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) totalPos++; } } return totalPos; } //+-------------------------------------------------------------------+ //| Close all EA's positions | //+-------------------------------------------------------------------+ void closeOpenPositions() { CTrade trd; for(int i = PositionsTotal()-1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket > 0) { if(PositionGetInteger(POSITION_MAGIC) == magicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { trd.SetExpertMagicNumber(magicNumber); trd.PositionClose(ticket); } } } } //+----------------------------------------------------------------------+ //| Determine whether a crossover happened between two series of values | //+----------------------------------------------------------------------+ bool crossover(const double &series1[], const double &series2[]) { if(ArraySize(series1) < 2 || ArraySize(series2) < 2) return false; return series1[ArraySize(series1) - 1] > series2[ArraySize(series2) - 1] && series1[ArraySize(series1) - 2] < series2[ArraySize(series2) - 2]; } //+----------------------------------------------------------------------+ //| Determine whether a crossunder happened between two series of values | //+----------------------------------------------------------------------+ bool crossunder(const double &series1[], const double &series2[]) { if(ArraySize(series1) < 2 || ArraySize(series2) < 2) return false; return series1[ArraySize(series1) - 1] < series2[ArraySize(series2) - 1] && series1[ArraySize(series1) - 2] > series2[ArraySize(series2) - 2]; } //+------------------------------------------------------------------+ //| Get news sentiment data | //+------------------------------------------------------------------+ bool fetchSentimentData() { char postData[], result[]; string resultHeaders; string headers = "Content-Type: application/json\r\n" + "x-rapidapi-host: trading-sentiment-aggregator-ai.p.rapidapi.com\r\n" + "x-rapidapi-key: " + apiKey + "\r\n"; CJAVal payload; payload["instrument"] = sentimentInstrument; payload["lookback"] = NEWS_ITEM_NUM - 1; // max number of past news we want in addition to the current one string jsonPayload = payload.Serialize(); StringToCharArray(jsonPayload, postData, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(postData, ArraySize(postData) - 1); ResetLastError(); int res = WebRequest("POST", apiUrl, headers, 5000, postData, result, resultHeaders); if(res == 200) { string responseText = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); CJAVal response; if(response.Deserialize(responseText)) { string prevHeadline = sentimentHistory[0].headline; //--- reset sentiment history for(int i=0; i < ArraySize(sentimentHistory); i++) { sentimentHistory[i].headline = ""; sentimentHistory[i].prediction = "none"; sentimentHistory[i].confidence = 0.0; sentimentHistory[i].source = ""; } //--- manage current news data sentimentHistory[0].headline = response["current"]["headline"].ToStr(); sentimentHistory[0].prediction = response["current"]["prediction"].ToStr(); sentimentHistory[0].confidence = response["current"]["confidence"].ToDbl(); sentimentHistory[0].source = response["current"]["source"].ToStr(); if(prevHeadline != sentimentHistory[0].headline) { Print(__FUNCTION__, " > New sentiment: \"", sentimentHistory[0].headline, "\". Prediction=", sentimentHistory[0].prediction, "; confidence=", sentimentHistory[0].confidence, "; source=", sentimentHistory[0].source ); } //--- manage historical news data if(response.HasKey("history")) { CJAVal historyArray = response["history"]; int downloadedHistorySize = historyArray.Size(); int sentimentHistoryArrayIdx = 1; for(int i = 0; i < downloadedHistorySize && sentimentHistoryArrayIdx < ArraySize(sentimentHistory); i++, sentimentHistoryArrayIdx++) { sentimentHistory[sentimentHistoryArrayIdx].headline = historyArray[i]["headline"].ToStr(); sentimentHistory[sentimentHistoryArrayIdx].prediction = historyArray[i]["prediction"].ToStr(); sentimentHistory[sentimentHistoryArrayIdx].confidence = historyArray[i]["confidence"].ToDbl(); sentimentHistory[sentimentHistoryArrayIdx].source = historyArray[i]["source"].ToStr(); } } return true; } } else { Print(__FUNCTION__, " > Could not retrieve sentiment for instrument ", sentimentInstrument, " (last error: ", GetLastError(), ", http code: ", res, ")"); } return false; } //+------------------------------------------------------------------+ //| Get the first meaningful item from the sentiment array, if any | //+------------------------------------------------------------------+ SentimentData getInstrumentSentiment() { SentimentData retData = {"", "none", 0.0, ""}; for(int i = 0; i < ArraySize(sentimentHistory); i++) { if(sentimentHistory[i].prediction != "neutral") { retData = sentimentHistory[i]; break; } } return retData; } //+---------------------------------------------------------------------------------+ //| Check if the latest sentiment is mismatching direction of the current position | //+---------------------------------------------------------------------------------+ bool determineSentimentReversal() { SentimentData currentSentiment = getInstrumentSentiment(); if(currentSentiment.prediction == "none" || currentSentiment.prediction == "neutral") { return false; } ulong tkt = 0; bool positionExists = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { tkt = PositionGetTicket(i); if(tkt > 0 && PositionGetInteger(POSITION_MAGIC) == magicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { positionExists = true; break; } } if(!positionExists) return false; bool isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); bool isBearishReversal = (isLong && currentSentiment.prediction == "bearish" && currentSentiment.confidence >= minConfSentExit); bool isBullishReversal = (!isLong && currentSentiment.prediction == "bullish" && currentSentiment.confidence >= minConfSentExit); return (isBearishReversal || isBullishReversal); } //+------------------------------------------------------------------+
Обращаем ваше внимание, что данный советник предоставляется исключительно в образовательных целях и не готов к промышленной эксплуатации.
Распространенные ошибки и полезные советы
При интеграции фильтра тональности в собственный советник стоит учитывать распространённые ошибки и следовать практическим советам, которые помогут сэкономить время и избежать непредвиденного поведения.
1. Убедитесь, что инструмент для анализа тональности указан правильноВ приведённом выше примере советника входной параметр sentimentInstrument определяет, для какого инструмента API будет анализировать тональность новостей. Согласно документации Trading Sentiment Aggregator AI, RSS-канал Yahoo Finance является одним из источников новостных заголовков. Чтобы убедиться, что вы указали правильный инструмент, откройте в браузере следующий URL-адрес: https://feeds.finance.yahoo.com/rss/2.0/headline?s= "INSTRUMENT"®ion=US&lang=en-US
заменив "INSTRUMENT" на обозначение вашего инструмента. Например:
https://feeds.finance.yahoo.com/rss/2.0/headline?s=EURUSD=X®ion=US&lang=en-US
Это позволяет убедиться, что пара EURUSD присутствует в RSS-канале Yahoo Finance и будет обрабатываться API для анализа тональности.
Выбор инструмента зависит от торговой пары, с которой вы работаете, а ответ, полученный по этому URL-адресу, также позволяет вам ещё раз убедиться, что общая тематика заголовков соответствует вашей стратегии. Например, если вы торгуете валютной парой USDJPY на рынке Forex, в качестве инструмента можно указать полное обозначение пары или только базовую валюту (в данном случае "USD"), при условии что она представлена на Yahoo Finance. Здесь вы можете найти свой инструмент и проверить, поддерживается ли он: https://finance.yahoo.com/lookup/
Также в приведённом примере советника можно полностью отключить фильтр тональности, оставив входной параметр sentimentInstrument пустым ("").
2. Выберите разумную частоту обновления данных о тональностиПараметр sentimentUpdateFreq определяет, как часто советник обращается к API анализа тональности. Очень короткий интервал между запросами (например, M1 или M5) может вызвать следующие проблемы:
- Слишком большое число вызовов API, быстро исчерпывающее лимит бесплатного тарифа RapidAPI.
- Излишний сетевой трафик без существенной дополнительной пользы, поскольку тональность не меняется настолько быстро.
В примере советника мы вызываем API каждые 4 часа с помощью OnTimer, что обеспечивает хороший баланс между оперативностью и затратами.
3. Разберитесь, как функция WebRequest работает в тестере стратегийСогласно документации по WebRequest, эта функция не может выполняться в тестере стратегий MetaTrader 5. Следовательно:
- Фильтр тональности не будет работать при тестировании на исторических данных.
- При запуске в тестере советник из примера будет торговать только по техническим сигналам, игнорируя тональность новостей.
- Поэтому показатели, полученные в тестере, не отражают результаты комбинированной стратегии (технические сигналы + тональность новостей) и сами по себе не могут подтвердить улучшения за счёт учёта тональности.
Для проведения воспроизводимого, научно обоснованного тестирования (скользящего форвардного тестирования, тестирования методом Монте-Карло и тестирования вне выборки) необходимо моделировать взаимодействие с API внутри тестера. Ниже приведено несколько практических способов создать детерминированный "поток исторических данных о тональности", который сможет считывать советник, работающий в тестере.
Прокси / записанный поток данных (рекомендуется)
- Во время форвардного тестирования записывайте фактические ответы API в файл (CSV или JSON). Включите в файл временные метки, инструмент, оценку/направление тональности и все метаданные, которые использует советник.
- Для работы в тестере измените советник так, чтобы вместо вызова WebRequest он считывал этот файл и возвращал записанное значение тональности для соответствующего бара или момента времени.
Этот подход воспроизводит реальное поведение API и сохраняет временные привязки. Он также позволяет воспроизводить одинаковые сценарии при скользящем форвардном тестировании и тестировании методом Монте-Карло (путём перемешивания записанных событий или формирования выборок из них).
Поток данных, созданный вручную:
- Вручную создайте текстовый файл или файл CSV с синтетическими событиями изменения тональности, например по одной строке на каждую временную метку: YYYY-MM-DD HH:MM, инструмент, sentiment_score, sentiment_label.
- Используйте этот файл для стресс-тестирования советника в контролируемых условиях (последовательности резко выраженных бычьих или медвежьих сигналов, противоречивые сигналы, редкие новости).
Этот вариант полезен для анализа чувствительности и проверки граничных случаев, когда требуется точный контроль над входным потоком данных.
Создание моментальных снимков с помощью Curl:
Используйте скрипт (Bash, Python, PowerShell и т. д.), который с помощью Curl несколько раз обращается к API и добавляет ответы в файл:
curl --request POST \ --url https://trading-sentiment-aggregator-ai.p.rapidapi.com/analyze \ --header 'Content-Type: application/json' \ --header 'x-rapidapi-host: trading-sentiment-aggregator-ai.p.rapidapi.com' \ --header 'x-rapidapi-key: <your-api-key>' \ --data '{"instrument":"USD","lookback": 10}'
Вызывайте API из скрипта в нужные моменты времени, чтобы собрать последние заголовки новостей вместе с результатами анализа тональности, а затем сохраняйте соответствующие поля и временные метки в формате CSV/JSON для последующего воспроизведения в тестере. Это позволяет быстро сформировать более обширный и реалистичный набор данных без проведения реальных торговых операций.
Чтобы реализовать один из этих вариантов, добавьте в советник параметр настройки, например sentimentMode = LIVE|CACHE. В режиме CACHE советник считывает значения тональности из локального файла и не использует WebRequest.
Практические советы для надёжного тестирования:
- Включайте в сохраняемый поток данных временные метки и идентификаторы торговых инструментов, чтобы избежать рассогласования из-за различий в часовых поясах, правилах обозначения символов или переходов на летнее и зимнее время.
- Записывайте дополнительные сведения (текст заголовков, их количество, успешность вызовов API), чтобы воспроизводить граничные случаи и находить причины расхождений.
- Для обеспечения статистической значимости проводите тестирование вне выборки и скользящее форвардное тестирование на данных за несколько записанных периодов. При моделировании методом Монте-Карло используйте случайную выборку записанных событий или перестановку блоков таких событий, при необходимости сохраняя их внутридневной порядок.
- Прежде чем переходить к реальной торговле, проверьте работу советника с кэшированным потоком данных в ходе форвардного тестирования на демо-счёте и сравните решения, принимаемые в реальном времени, с решениями при воспроизведении кэшированных данных, чтобы выявить ошибки реализации.
Добавив простой слой записи и воспроизведения данных, вы сохраняете возможность научно обоснованного и воспроизводимого тестирования стратегий с учётом тональности новостей, оставляя вызовы WebRequest в реальном времени только для демонстрации или работы в реальной торговле.
4. Тщательно настраивайте пороги уверенностиПараметры minConfSentEntry и minConfSentExit определяют, насколько сильным должен быть сигнал тональности, чтобы влиять на входы в позицию и выходы из позиции.
- Если установить слишком высокое значение minConfSentEntry (например, 0,9), советник может крайне редко входить в позицию, даже при сильном техническом сигнале.
- Если установить слишком низкое значение minConfSentExit (например, 0,3), советник может слишком легко выходить из позиции из-за зашумлённых или слабых сигналов тональности.
Начните с умеренных значений (например, 0,6–0,7 для входа в позицию и 0,5–0,6 для выхода из позиции) и корректируйте их с учётом поведения советника в реальных рыночных условиях.
5. Защитите свой ключ APIВ примере кода ключ API хранится непосредственно в исходном коде следующим образом:
const string apiKey = "your-api-key";
Это допустимо для обучения и тестирования, но не рекомендуется при использовании в рабочей среде или распространении. В реальном советнике следует:
- Хранить ключ API вне исходного кода (например, в файле конфигурации или переменной среды).
- Получать ключ с сервера во время выполнения после прохождения аутентификации и авторизации.
- Не распространять исходный код со встроенным ключом (например, через удалённый репозиторий Git).
Также следует помнить, что тональность новостей — это вероятностный сигнал, а не детерминированный предиктор. Не следует:
- Пытаться оптимизировать стратегию так, чтобы она "идеально соответствовала" сигналам тональности новостей.
- Ожидать, что фильтр тональности будет работать безупречно при каждой сделке.
Вместо этого оценивайте эффективность статистически — с учетом динамики во времени и на основе большого количества сделок. Цель заключается в повышении общей доходности с поправкой на риск, а не в том, чтобы исключить все убытки.
Заключение
В этой статье мы подробно рассмотрели механизм, добавляющий дополнительный уровень проверки на основе тональности новостей. Этот механизм дополняет — а не заменяет — основную логику вашей стратегии и любые другие средства, которые вы можете использовать для фильтрации своих торговых решений.
Основная идея проста: получать компактный сигнал о настроении рынка ("bullish", "bearish", "neutral") с указанием уровня уверенности. Обновлять его следует по таймеру, чтобы не блокировать обработку на каждом тике и не перегружать API запросами. Следует применять два практических правила: разрешать вход только тогда, когда тональность подтверждает направление сделки, а оценка уверенности превышает заданный порог, и досрочно выходить из позиции, когда тональность с высокой оценкой уверенности меняется против неё. В прилагаемом примере советника эта идея реализована следующим образом:
- Получение данных через REST и разбор JSON (WebRequest + JAson) с управлением частотой запросов через OnTimer.
- Структура данных sentimentHistory для хранения тональности новостей за заданный ретроспективный период и вспомогательная функция getInstrumentSentiment() для выбора первого события с тональностью, отличной от нейтральной.
- Фильтр входа (направление + minConfidence) и функция determineSentimentReversal() для выхода из позиции.
- Режимы LIVE и CACHE (записанный поток данных) для детерминированных прогонов в тестере стратегий.
- Эксплуатационные настройки: разрешённый URL-адрес для WebRequest, защита ключа API и разумные пороговые значения по умолчанию.
Важные замечания
- Чтобы использовать эту функцию в одном из ваших советников, необходимо добавить URL-адрес https://trading-sentiment-aggregator-ai.p.rapidapi.com/ в список разрешённых URL-адресов для WebRequest в терминале MetaTrader 5 в разделе "Сервис" > "Настройки" > "Советники".
- Ответственность пользователя: информацию, содержащуюся в данной статье, следует рассматривать как инструмент поддержки принятия решений, а не как финансовую рекомендацию. Окончательная ответственность за любое торговое или инвестиционное решение полностью лежит на пользователе. Мы настоятельно рекомендуем использовать эти данные в качестве одного из нескольких источников информации в рамках надёжной стратегии управления рисками.
Отказ от ответственности в отношении искусственного интеллекта
- API, используемый в данной статье, основан на модели искусственного интеллекта для финансовой сферы и должен применяться ответственно.
- Вероятностный характер: модель использует статистические методы для классификации рыночных настроений, при этом результаты носят вероятностный, а не детерминированный характер. Модели искусственного интеллекта прогнозируют закономерности на основе языковых данных и не отражают абсолютную истину о рынке.
- Риск неверной интерпретации: модели искусственного интеллекта могут иногда генерировать "галлюцинации" из-за неверного понимания сложного финансового жаргона, иронии или специфики местного рынка. Хотя модели нацелены на высокую точность, отдельные данные всё же могут содержать ошибки.
- Торговая логика: пользователям следует применять к этим результатам вероятностный подход, принятый в профессиональной торговле. Это означает, что необходимо оценивать эффективность в динамике, а не полагаться на один-единственный прогноз ИИ.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22830
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.