GitHub で見る

Trade EA Template for News Strategy

Overview

Trade EA Template for News Strategy is a C# conversion of the MetaTrader 4 expert advisor "Trade EA Template for News". The original system paused trading around scheduled economic events downloaded from external websites. This StockSharp port keeps the core ideas while adapting them to the high-level API:

  • Uses completed candles from the configured timeframe (H1 by default).
  • Trades only when the account is flat, exactly like the MQL template that required zero open orders.
  • Applies a manual economic news blackout that blocks entries before and after events depending on their importance.
  • Automatically creates protective stop-loss and take-profit brackets 100 points away from the fill price (converted through the security step).

Trading Logic

  1. Each finished candle triggers a recalculation of the news schedule. The strategy stores the open price of the previous candle so that the next bar can compare its close to the prior open.
  2. If the current time falls inside any configured blackout window the strategy cancels pending orders and does not open new trades.
  3. When no position is open and trading is allowed:
    • A long position is opened if the latest candle closes above the previous candle's open price.
    • A short position is opened if the latest candle closes below the previous candle's open price.
  4. Stop-loss and take-profit levels are expressed in points (TakeProfitPoints and StopLossPoints) and converted into absolute price offsets using the security's Step value.

Manual news schedule

The original expert downloaded data from investing.com or DailyFX. For portability the StockSharp version expects a manually curated calendar supplied through the NewsEventsDefinition parameter. The format accepts a list of entries separated by semicolons or line breaks. Every entry must contain at least three comma-separated fields:

YYYY-MM-DD HH:MM,CURRENCIES,IMPORTANCE[,TITLE]
  • YYYY-MM-DD HH:MM — event start in UTC. The optional TimeZoneOffsetHours parameter shifts all parsed times by the requested amount (for example set 3 for UTC+3).
  • CURRENCIES — currency codes or instrument identifiers such as USD, EUR, EUR/USD. Multiple codes can be separated with /, ,, ;, | or spaces.
  • IMPORTANCE — importance keyword. Recognised values: Low, Medium, Mid, Midle, Moderate, High, NFP, strings containing Nonfarm or Non-farm.
  • TITLE — optional free text description that will be printed in log messages.

Example:

2024-03-01 13:30,USD,High,Nonfarm Payrolls;2024-03-01 15:00,USD,Low,Factory Orders

Blackout windows

  • UseLowNews, UseMediumNews, UseHighNews and UseNfpNews toggle which events are considered.
  • LowMinutesBefore/After, MediumMinutesBefore/After, HighMinutesBefore/After and NfpMinutesBefore/After determine how many minutes around the event trading should be disabled.
  • OnlySymbolNews restricts the blackout to entries whose currency codes match the current security (for example EURUSD results in the pair {EUR, USD}). Disable it to pause trading on every event.
  • The strategy keeps only the highest importance event active at any given time. Informational log messages announce the reason for the current state and the next scheduled release.

Parameters

Parameter Description Default
CandleType Candle data type to subscribe to. Defaults to 1 hour. 1h
UseLowNews Enable low importance events. true
LowMinutesBefore / LowMinutesAfter Minutes before/after low impact news to block entries. 15 / 15
UseMediumNews Enable medium importance events. true
MediumMinutesBefore / MediumMinutesAfter Minutes before/after medium impact news. 30 / 30
UseHighNews Enable high importance events. true
HighMinutesBefore / HighMinutesAfter Minutes before/after high impact news. 60 / 60
UseNfpNews Enable the Non-farm Payrolls flag. true
NfpMinutesBefore / NfpMinutesAfter Minutes before/after NFP events. 180 / 180
OnlySymbolNews Filter the calendar by the current security's currency codes. true
NewsEventsDefinition Manual economic calendar description string. empty
TimeZoneOffsetHours Offset applied to every parsed event (UTC by default). 0
TakeProfitPoints Distance in points for the protective take-profit order. 100
StopLossPoints Distance in points for the protective stop-loss order. 100

Volume is inherited from Strategy and should be set according to the desired position size.

Differences from the MQL version

  • No automatic HTTP download — the user supplies the news list manually, which avoids external dependencies and keeps the conversion deterministic.
  • Chart labels and vertical lines are replaced with log messages that describe the active or upcoming event.
  • The MQL expert opened orders with fixed lot size 0.01; in StockSharp the position size comes from the Volume property.
  • All logic is implemented with the high-level candle subscription API while preserving the template's news-aware behaviour.

Deployment notes

  1. Fill NewsEventsDefinition before starting the strategy or update it, stop and restart to reload the schedule.
  2. Adjust TimeZoneOffsetHours and the minutes-before/after parameters to match your trading session.
  3. Set Volume, portfolio and security in the UI or in code, then start the strategy.
  4. Watch the strategy log for messages such as "Trading paused due to high news" or "Next scheduled news" to confirm the blackout logic.

Python translation is intentionally omitted as requested.

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Trade EA Template for News strategy converted from MQL.
/// </summary>
public class TradeEaTemplateForNewsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useLowNews;
	private readonly StrategyParam<int> _lowMinutesBefore;
	private readonly StrategyParam<int> _lowMinutesAfter;
	private readonly StrategyParam<bool> _useMediumNews;
	private readonly StrategyParam<int> _mediumMinutesBefore;
	private readonly StrategyParam<int> _mediumMinutesAfter;
	private readonly StrategyParam<bool> _useHighNews;
	private readonly StrategyParam<int> _highMinutesBefore;
	private readonly StrategyParam<int> _highMinutesAfter;
	private readonly StrategyParam<bool> _useNfpNews;
	private readonly StrategyParam<int> _nfpMinutesBefore;
	private readonly StrategyParam<int> _nfpMinutesAfter;
	private readonly StrategyParam<bool> _onlySymbolNews;
	private readonly StrategyParam<string> _newsEventsDefinition;
	private readonly StrategyParam<int> _timeZoneOffsetHours;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;

	private readonly List<NewsEvent> _newsEvents = new();
	private readonly HashSet<string> _instrumentCurrencies = new(StringComparer.OrdinalIgnoreCase);

	private decimal? _previousOpenPrice;
	private bool _newsBlocking;
	private string _lastNewsMessage = string.Empty;

	public TradeEaTemplateForNewsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame());
		_useLowNews = Param(nameof(UseLowNews), true);
		_lowMinutesBefore = Param(nameof(LowMinutesBefore), 15);
		_lowMinutesAfter = Param(nameof(LowMinutesAfter), 15);
		_useMediumNews = Param(nameof(UseMediumNews), true);
		_mediumMinutesBefore = Param(nameof(MediumMinutesBefore), 30);
		_mediumMinutesAfter = Param(nameof(MediumMinutesAfter), 30);
		_useHighNews = Param(nameof(UseHighNews), true);
		_highMinutesBefore = Param(nameof(HighMinutesBefore), 60);
		_highMinutesAfter = Param(nameof(HighMinutesAfter), 60);
		_useNfpNews = Param(nameof(UseNfpNews), true);
		_nfpMinutesBefore = Param(nameof(NfpMinutesBefore), 180);
		_nfpMinutesAfter = Param(nameof(NfpMinutesAfter), 180);
		_onlySymbolNews = Param(nameof(OnlySymbolNews), true);
		_newsEventsDefinition = Param(nameof(NewsEventsDefinition), string.Empty);
		_timeZoneOffsetHours = Param(nameof(TimeZoneOffsetHours), 0);
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100);
		_stopLossPoints = Param(nameof(StopLossPoints), 100);
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public bool UseLowNews
	{
		get => _useLowNews.Value;
		set => _useLowNews.Value = value;
	}

	public int LowMinutesBefore
	{
		get => _lowMinutesBefore.Value;
		set => _lowMinutesBefore.Value = value;
	}

	public int LowMinutesAfter
	{
		get => _lowMinutesAfter.Value;
		set => _lowMinutesAfter.Value = value;
	}

	public bool UseMediumNews
	{
		get => _useMediumNews.Value;
		set => _useMediumNews.Value = value;
	}

	public int MediumMinutesBefore
	{
		get => _mediumMinutesBefore.Value;
		set => _mediumMinutesBefore.Value = value;
	}

	public int MediumMinutesAfter
	{
		get => _mediumMinutesAfter.Value;
		set => _mediumMinutesAfter.Value = value;
	}

	public bool UseHighNews
	{
		get => _useHighNews.Value;
		set => _useHighNews.Value = value;
	}

	public int HighMinutesBefore
	{
		get => _highMinutesBefore.Value;
		set => _highMinutesBefore.Value = value;
	}

	public int HighMinutesAfter
	{
		get => _highMinutesAfter.Value;
		set => _highMinutesAfter.Value = value;
	}

	public bool UseNfpNews
	{
		get => _useNfpNews.Value;
		set => _useNfpNews.Value = value;
	}

	public int NfpMinutesBefore
	{
		get => _nfpMinutesBefore.Value;
		set => _nfpMinutesBefore.Value = value;
	}

	public int NfpMinutesAfter
	{
		get => _nfpMinutesAfter.Value;
		set => _nfpMinutesAfter.Value = value;
	}

	public bool OnlySymbolNews
	{
		get => _onlySymbolNews.Value;
		set => _onlySymbolNews.Value = value;
	}

	public string NewsEventsDefinition
	{
		get => _newsEventsDefinition.Value;
		set => _newsEventsDefinition.Value = value;
	}

	public int TimeZoneOffsetHours
	{
		get => _timeZoneOffsetHours.Value;
		set => _timeZoneOffsetHours.Value = value;
	}

	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	private bool HasNewsFilter => UseLowNews || UseMediumNews || UseHighNews || UseNfpNews;

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();

		_previousOpenPrice = null;
		_newsEvents.Clear();
		_newsBlocking = false;
		_lastNewsMessage = string.Empty;
		_instrumentCurrencies.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		BuildInstrumentCurrencies();
		ParseNewsEvents();
		ConfigureProtection();

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ConfigureProtection()
	{
		// Configure stop-loss and take-profit to mirror the 100 point brackets from the template EA.
		var step = Security?.PriceStep ?? 0m;

		if (step <= 0m)
		{
			LogWarning("Security step is zero. Protective orders cannot be configured.");
			return;
		}

		var takeUnit = TakeProfitPoints > 0 ? new Unit(step * TakeProfitPoints, UnitTypes.Absolute) : new Unit();
		var stopUnit = StopLossPoints > 0 ? new Unit(step * StopLossPoints, UnitTypes.Absolute) : new Unit();

		if (TakeProfitPoints > 0 || StopLossPoints > 0)
			StartProtection(takeUnit, stopUnit);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateNewsState(candle.CloseTime);

		// No bound indicators to check readiness for.

		// Abort any signals while a news blackout is active.
		if (_newsBlocking)
		{
			return;
		}

		if (_previousOpenPrice == null)
		{
			// Store the first open price so the next candle can compare against it.
			_previousOpenPrice = candle.OpenPrice;
			return;
		}

		var previousOpen = _previousOpenPrice.Value;
		_previousOpenPrice = candle.OpenPrice;

		if (Volume <= 0)
			return;

		if (Position != 0)
		{
			// The original template trades only when there are no existing positions.
			return;
		}

		if (candle.ClosePrice > previousOpen)
		{
			BuyMarket();
			LogInfo($"Long entry after bullish close at {candle.ClosePrice} compared to prior open {previousOpen}.");
		}
		else if (candle.ClosePrice < previousOpen)
		{
			SellMarket();
			LogInfo($"Short entry after bearish close at {candle.ClosePrice} compared to prior open {previousOpen}.");
		}
	}

	private void UpdateNewsState(DateTimeOffset currentTime)
	{
		// Without configured events the strategy should allow trading freely.
		if (!HasNewsFilter || _newsEvents.Count == 0)
		{
			if (_newsBlocking)
			{
				_newsBlocking = false;
				NotifyNewsMessage("No upcoming news events.");
			}
			return;
		}

		NewsEvent blockingEvent = null;
		NewsEvent upcomingEvent = null;

		for (var i = 0; i < _newsEvents.Count; i++)
		{
			var evt = _newsEvents[i];

			if (!IsImportanceEnabled(evt.Importance))
				continue;

			if (!MatchesSecurity(evt))
				continue;

			if (IsInsideWindow(evt, currentTime))
			{
				if (blockingEvent == null || evt.Importance > blockingEvent.Importance)
					blockingEvent = evt;
			}
			else if (evt.Time > currentTime)
			{
				if (upcomingEvent == null || evt.Time < upcomingEvent.Time)
					upcomingEvent = evt;
			}
		}

		var wasBlocking = _newsBlocking;
		_newsBlocking = blockingEvent != null;

		NotifyNewsMessage(BuildNewsMessage(blockingEvent, upcomingEvent));

		if (_newsBlocking && !wasBlocking)
			CancelActiveOrders();
	}

	private void NotifyNewsMessage(string message)
	{
		if (_lastNewsMessage.EqualsIgnoreCase(message))
			return;

		_lastNewsMessage = message;
		LogInfo(message);
	}

	private bool IsImportanceEnabled(NewsImportances importance)
	=> importance switch
	{
		NewsImportances.Low => UseLowNews,
		NewsImportances.Medium => UseMediumNews,
		NewsImportances.High => UseHighNews,
		NewsImportances.Nfp => UseNfpNews,
		_ => false
	};

	private bool IsInsideWindow(NewsEvent evt, DateTimeOffset currentTime)
	{
		var before = TimeSpan.FromMinutes(GetMinutesBefore(evt.Importance));
		var after = TimeSpan.FromMinutes(GetMinutesAfter(evt.Importance));
		var start = evt.Time - before;
		var end = evt.Time + after;
		return currentTime >= start && currentTime <= end;
	}

	private int GetMinutesBefore(NewsImportances importance)
	=> importance switch
	{
		NewsImportances.Low => Math.Max(0, LowMinutesBefore),
		NewsImportances.Medium => Math.Max(0, MediumMinutesBefore),
		NewsImportances.High => Math.Max(0, HighMinutesBefore),
		NewsImportances.Nfp => Math.Max(0, NfpMinutesBefore),
		_ => 0
	};

	private int GetMinutesAfter(NewsImportances importance)
	=> importance switch
	{
		NewsImportances.Low => Math.Max(0, LowMinutesAfter),
		NewsImportances.Medium => Math.Max(0, MediumMinutesAfter),
		NewsImportances.High => Math.Max(0, HighMinutesAfter),
		NewsImportances.Nfp => Math.Max(0, NfpMinutesAfter),
		_ => 0
	};

	private string BuildNewsMessage(NewsEvent activeEvent, NewsEvent upcomingEvent)
	{
		if (activeEvent != null)
		{
			var label = GetImportanceLabel(activeEvent.Importance);
			var timeText = activeEvent.Time.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
			var currencyPart = activeEvent.Currency.IsEmptyOrWhiteSpace() ? string.Empty : $" [{activeEvent.Currency}]";
			var titlePart = activeEvent.Title.IsEmptyOrWhiteSpace() ? string.Empty : $" - {activeEvent.Title}";
			return $"Trading paused due to {label} news{currencyPart} at {timeText}{titlePart}.";
		}

		if (upcomingEvent != null)
		{
			var label = GetImportanceLabel(upcomingEvent.Importance);
			var timeText = upcomingEvent.Time.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
			var currencyPart = upcomingEvent.Currency.IsEmptyOrWhiteSpace() ? string.Empty : $" [{upcomingEvent.Currency}]";
			var titlePart = upcomingEvent.Title.IsEmptyOrWhiteSpace() ? string.Empty : $" - {upcomingEvent.Title}";
			return $"Next scheduled news: {label}{currencyPart} at {timeText}{titlePart}.";
		}

		return "No upcoming news events.";
	}

	private static string GetImportanceLabel(NewsImportances importance)
	=> importance switch
	{
		NewsImportances.Low => "low",
		NewsImportances.Medium => "medium",
		NewsImportances.High => "high",
		NewsImportances.Nfp => "non-farm payroll",
		_ => "unknown"
	};

	private void ParseNewsEvents()
	{
		// Parse the manual economic calendar description provided in the parameters.
		_newsEvents.Clear();

		var raw = NewsEventsDefinition;

		if (raw.IsEmptyOrWhiteSpace())
		{
			LogInfo("News events list is empty. The filter will allow trading at all times.");
			return;
		}

		var separators = new[] { ';', '\n', '\r' };
		var entries = raw.Split(separators, StringSplitOptions.RemoveEmptyEntries);

		for (var entryIndex = 0; entryIndex < entries.Length; entryIndex++)
		{
			var entry = entries[entryIndex].Trim();

			if (entry.Length == 0)
				continue;

			var rawParts = entry.Split(',');

			if (rawParts.Length < 3)
			{
				LogWarning($"Unable to parse news entry '{entry}'. Expected at least time, currency and importance.");
				continue;
			}

			var parts = new string[rawParts.Length];
			for (var i = 0; i < rawParts.Length; i++)
				parts[i] = rawParts[i].Trim();

			if (!DateTimeOffset.TryParse(parts[0], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var time))
			{
				LogWarning($"Unable to parse time '{parts[0]}' in news entry '{entry}'.");
				continue;
			}

			var currencies = parts[1].ToUpperInvariant();

			if (!TryParseImportance(parts[2], out var importance))
			{
				LogWarning($"Unable to parse importance '{parts[2]}' in news entry '{entry}'.");
				continue;
			}

			var title = string.Empty;
			if (parts.Length > 3)
			{
				var count = parts.Length - 3;
				var combined = string.Join(",", parts, 3, count);
				title = combined.Trim();
			}

			time = time.ToOffset(TimeSpan.FromHours(TimeZoneOffsetHours));

			_newsEvents.Add(new NewsEvent(time, currencies, importance, title));
		}

		_newsEvents.Sort((left, right) => left.Time.CompareTo(right.Time));

		if (_newsEvents.Count > 0)
			LogInfo($"Loaded {_newsEvents.Count} manual news event(s).");
		else
			LogInfo("No valid news events parsed. The filter will remain inactive.");
	}

	private static bool TryParseImportance(string value, out NewsImportances importance)
	{
		if (value.IsEmptyOrWhiteSpace())
		{
			importance = default;
			return false;
		}

		var normalized = value.Trim();

		if (normalized.Equals("LOW", StringComparison.OrdinalIgnoreCase))
		{
			importance = NewsImportances.Low;
			return true;
		}

		if (normalized.Equals("MEDIUM", StringComparison.OrdinalIgnoreCase) ||
				normalized.Equals("MID", StringComparison.OrdinalIgnoreCase) ||
				normalized.Equals("MIDLE", StringComparison.OrdinalIgnoreCase) ||
				normalized.Equals("MODERATE", StringComparison.OrdinalIgnoreCase))
		{
			importance = NewsImportances.Medium;
			return true;
		}

		if (normalized.Equals("HIGH", StringComparison.OrdinalIgnoreCase))
		{
			importance = NewsImportances.High;
			return true;
		}

		if (normalized.Equals("NFP", StringComparison.OrdinalIgnoreCase) ||
				normalized.Contains("NONFARM", StringComparison.OrdinalIgnoreCase) ||
				normalized.Contains("NON-FARM", StringComparison.OrdinalIgnoreCase))
		{
			importance = NewsImportances.Nfp;
			return true;
		}

		importance = default;
		return false;
	}

	private bool MatchesSecurity(NewsEvent evt)
	{
		if (!OnlySymbolNews)
			return true;

		// Match the configured currencies against the current instrument if required.

		if (_instrumentCurrencies.Count == 0)
			return true;

		if (evt.Currency.IsEmptyOrWhiteSpace())
			return true;

		var separators = new[] { '/', ',', '|', ';', ' ' };
		var tokens = evt.Currency.Split(separators, StringSplitOptions.RemoveEmptyEntries);

		for (var i = 0; i < tokens.Length; i++)
		{
			var token = tokens[i].Trim();
			if (token.Length == 0)
				continue;

			if (_instrumentCurrencies.Contains(token))
				return true;
		}

		return false;
	}

	private void BuildInstrumentCurrencies()
	{
		// Extract major currency codes from the security symbol (e.g., EURUSD -> EUR, USD).
		_instrumentCurrencies.Clear();

		var code = Security?.Code;

		if (code.IsEmptyOrWhiteSpace())
			return;

		var trimmed = code.Trim().ToUpperInvariant();

		if (trimmed.Length >= 6)
		{
			_instrumentCurrencies.Add(trimmed.Substring(0, 3));
			_instrumentCurrencies.Add(trimmed.Substring(trimmed.Length - 3, 3));
		}
		else
		{
			_instrumentCurrencies.Add(trimmed);
		}
	}

	private sealed record class NewsEvent(DateTimeOffset Time, string Currency, NewsImportances Importance, string Title);

	private enum NewsImportances
	{
		Low = 1,
		Medium = 2,
		High = 3,
		Nfp = 4
	}
}