Trade EA Template for News 策略
概述
Trade EA Template for News 策略是 MetaTrader 4 专家顾问“Trade EA Template for News”的 C# 版本。原始脚本会在重大经济新闻发布前后停止交易,本移植版在 StockSharp 高级 API 中复现这一思路:
- 默认使用 1 小时蜡烛(可配置的
CandleType)。 - 只有在账户没有持仓时才会开仓,与原脚本的
OrdersTotal()<1条件一致。 - 根据新闻重要程度,在事件前后禁用开仓操作。
- 自动设置距离成交价 100 个点的止损/止盈(通过合约的
Security.Step计算实际价格距离)。
交易逻辑
- 每当一根蜡烛收盘,策略就根据最新时间更新新闻日程。上一根蜡烛的开盘价会保存下来,用于下一根蜡烛的比较。
- 若当前时间处于任何禁交易窗口内,策略会取消挂单并禁止开新仓。
- 在允许交易且没有持仓时:
- 若当前蜡烛的收盘价高于上一根蜡烛的开盘价,则买入指定的
Volume数量。 - 若当前蜡烛的收盘价低于上一根蜡烛的开盘价,则卖出(做空)。
- 若当前蜡烛的收盘价高于上一根蜡烛的开盘价,则买入指定的
TakeProfitPoints与StopLossPoints以“点”为单位,运行时会乘以Security.Step转换为绝对价格偏移,随后通过StartProtection下达保护性指令。
手工新闻日历
原 EA 会从 investing.com 或 DailyFX 下载新闻数据。为了提高可移植性,本版本要求手工填充参数 NewsEventsDefinition。列表中的每个事件使用分号或换行分隔,单个事件使用逗号分隔字段:
YYYY-MM-DD HH:MM,CURRENCIES,IMPORTANCE[,TITLE]
YYYY-MM-DD HH:MM:UTC 时间。TimeZoneOffsetHours可以整体平移所有事件(例如设置为3代表 UTC+3)。CURRENCIES:与事件相关的货币代码或品种名称,如USD、EUR/USD。多个代码可通过/、,、;、|或空格分隔。IMPORTANCE:重要程度关键字,支持Low、Medium、Mid、Midle、Moderate、High、NFP以及包含Nonfarm或Non-farm的文本。TITLE:可选描述,将显示在日志中。
示例:
2024-03-01 13:30,USD,High,Nonfarm Payrolls;2024-03-01 15:00,USD,Low,Factory Orders
禁交易窗口
- 通过
UseLowNews、UseMediumNews、UseHighNews、UseNfpNews决定哪些事件会触发过滤器。 LowMinutesBefore/After、MediumMinutesBefore/After、HighMinutesBefore/After、NfpMinutesBefore/After指定在新闻前后需要冻结的分钟数。OnlySymbolNews启用时,只会匹配当前交易品种中的货币(例如EURUSD→{EUR, USD});关闭后所有事件都会触发暂停。- 当同一时间有多个事件时,会选取最高重要度的那一个作为当前状态,并在日志中提示原因以及下一次发布时间。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
CandleType |
订阅的蜡烛类型。 | 1h |
UseLowNews |
是否考虑低重要度新闻。 | true |
LowMinutesBefore / LowMinutesAfter |
低重要度新闻前/后的禁交易时间(分钟)。 | 15 / 15 |
UseMediumNews |
是否考虑中等重要度新闻。 | true |
MediumMinutesBefore / MediumMinutesAfter |
中等重要度新闻前/后的禁交易时间。 | 30 / 30 |
UseHighNews |
是否考虑高重要度新闻。 | true |
HighMinutesBefore / HighMinutesAfter |
高重要度新闻前/后的禁交易时间。 | 60 / 60 |
UseNfpNews |
是否启用非农 (NFP) 事件。 | true |
NfpMinutesBefore / NfpMinutesAfter |
NFP 前/后的禁交易时间。 | 180 / 180 |
OnlySymbolNews |
只针对当前品种中的货币触发过滤。 | true |
NewsEventsDefinition |
手工新闻列表。 | 空 |
TimeZoneOffsetHours |
统一的时区偏移(小时)。 | 0 |
TakeProfitPoints |
止盈距离(点)。 | 100 |
StopLossPoints |
止损距离(点)。 | 100 |
Volume 继承自 Strategy,需要根据账户规模单独设置。
与 MQL 版本的差异
- 不再执行网页请求,避免依赖外部服务,策略完全由手工列表驱动。
- 原来绘制在图表上的标签与竖线改为日志信息,例如 “Trading paused due to high news”。
- MQL 版本固定手数为 0.01,这里改为读取
Volume参数。 - 全部基于高级蜡烛订阅接口实现,没有使用低级价格缓冲区。
使用建议
- 在启动策略前准备好
NewsEventsDefinition,如需修改请停止并重新启动以重新解析列表。 - 根据交易时区调整
TimeZoneOffsetHours与各类新闻的前后缓冲时间。 - 设置好
Volume、投资组合和交易品种,然后启动策略。 - 关注日志输出,确认是否出现“暂停交易”或“下一次新闻”的提示信息。
根据要求未提供 Python 版本。
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
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class trade_ea_template_for_news_strategy(Strategy):
"""News template EA: simple candle direction entries with SL/TP via StartProtection."""
def __init__(self):
super(trade_ea_template_for_news_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for calculations", "General")
self._take_profit_points = self.Param("TakeProfitPoints", 100) \
.SetDisplay("Take Profit Points", "TP distance in price steps", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 100) \
.SetDisplay("Stop Loss Points", "SL distance in price steps", "Risk")
self._previous_open_price = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def TakeProfitPoints(self):
return int(self._take_profit_points.Value)
@property
def StopLossPoints(self):
return int(self._stop_loss_points.Value)
def OnStarted2(self, time):
super(trade_ea_template_for_news_strategy, self).OnStarted2(time)
self._previous_open_price = None
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
if step > 0:
tp_unit = Unit(step * self.TakeProfitPoints, UnitTypes.Absolute) if self.TakeProfitPoints > 0 else None
sl_unit = Unit(step * self.StopLossPoints, UnitTypes.Absolute) if self.StopLossPoints > 0 else None
if tp_unit is not None and sl_unit is not None:
self.StartProtection(takeProfit=tp_unit, stopLoss=sl_unit)
elif tp_unit is not None:
self.StartProtection(takeProfit=tp_unit)
elif sl_unit is not None:
self.StartProtection(stopLoss=sl_unit)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._previous_open_price is None:
self._previous_open_price = float(candle.OpenPrice)
return
previous_open = self._previous_open_price
self._previous_open_price = float(candle.OpenPrice)
if self.Position != 0:
return
close = float(candle.ClosePrice)
if close > previous_open:
self.BuyMarket()
elif close < previous_open:
self.SellMarket()
def OnReseted(self):
super(trade_ea_template_for_news_strategy, self).OnReseted()
self._previous_open_price = None
def CreateClone(self):
return trade_ea_template_for_news_strategy()