How to Add a News Filter to a cTrader cBot (Cloud Included)
A news filter in a cBot runs into two walls that MetaTrader developers never see. First, the cTrader Automate API has no economic calendar: the platform shows one, a cBot cannot read it. Second, most people run their cBots in cTrader Cloud, where a cBot cannot make HTTP requests at all: Http.Get() comes back with status 503.
The cloud does allow one thing: a WebSocket on port 25345. So that is how the free ShowMyTrades economic calendar reaches a cBot. This guide gives you the whole thing: the idea, the protocol, and a cBot you can compile as it is.
๐งญ How It Works
cBot (desktop or cloud) ShowMyTrades
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ every 15 min: โ ws://โฆ:25345 โ same calendar as โ
โ open socket, send โ โโโโโโโโโโโโโโโบ โ /api/public/calendar/v1 โ
โ "currency=EUR,USD& โ โ cached, filtered, โ
โ impact=high&days=2& โ โโโโโโโโโโโโโโโ โ a few kilobytes โ
โ format=csv" โ CSV text โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ parse, close โ
โ every tick: IsNewsTime() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Address | ws://showmytrades.com:25345/ |
| You send | One line of text with the same parameters as the HTTP API: currency, impact, days, format |
| You get | One text frame with the CSV (or JSON) body |
| Access rights | AccessRights.None is enough, so the cBot can be published |
| Key or account | None |
| Desktop fallback | Plain HTTP works there; the code below falls back to it if the socket fails |
The socket uses ws://, not wss://, because that is what cTrader Cloud connects with. Nothing about you or your account travels on it: only the four parameters.
๐ The cBot
Create a new cBot in cTrader Automate, replace its code with this, and build. On the desktop it compiles against cTrader.Automate from NuGet; in the cloud it runs as it is.
// News filter for cTrader (desktop and cloud), data by ShowMyTrades, free, no key.
// Docs: https://showmytrades.com/docs/api/economic-calendar
using System;
using System.Collections.Generic;
using cAlgo.API;
namespace cAlgo.Robots
{
[Robot(AccessRights = AccessRights.None, TimeZone = TimeZones.UTC)]
public class SmtNewsFilterBot : Robot
{
[Parameter("News: currencies (comma-separated, empty = all)", DefaultValue = "EUR,USD")]
public string NewsCurrencies { get; set; }
[Parameter("News: impact (high | high,medium | all)", DefaultValue = "high")]
public string NewsImpact { get; set; }
[Parameter("News: minutes before", DefaultValue = 30, MinValue = 0)]
public int MinutesBefore { get; set; }
[Parameter("News: minutes after", DefaultValue = 30, MinValue = 0)]
public int MinutesAfter { get; set; }
private const string CalendarWs = "ws://showmytrades.com:25345/";
private const string CalendarHttp = "https://showmytrades.com/api/public/calendar/v1?";
private readonly List<KeyValuePair<DateTime, string>> _events = new List<KeyValuePair<DateTime, string>>();
private WebSocketClient _ws;
private bool _received;
protected override void OnStart()
{
DownloadCalendar();
Timer.Start(TimeSpan.FromMinutes(15));
}
protected override void OnTimer()
{
DownloadCalendar();
}
protected override void OnTick()
{
string reason;
if (IsNewsTime(out reason))
{
// No new entries during the news window. Manage open positions as you prefer.
return;
}
// ... your entry logic ...
}
protected override void OnStop()
{
CloseSocket();
}
// True while a release is inside the pause window; reason says which one.
public bool IsNewsTime(out string reason)
{
DateTime now = Server.TimeInUtc;
foreach (KeyValuePair<DateTime, string> e in _events)
{
if (now >= e.Key.AddMinutes(-MinutesBefore) && now <= e.Key.AddMinutes(MinutesAfter))
{
reason = e.Value;
return true;
}
}
reason = null;
return false;
}
private string Query()
{
string q = "format=csv&days=2&impact=" + NewsImpact;
if (!string.IsNullOrWhiteSpace(NewsCurrencies)) q += "¤cy=" + NewsCurrencies.Replace(" ", "");
return q;
}
private void DownloadCalendar()
{
if (RunningMode != RunningMode.RealTime) return; // no network in the backtester
// cTrader Cloud blocks HTTP from cBots and allows only a WebSocket on port 25345,
// so the socket is the primary path everywhere; HTTP is the fallback on the desktop.
try
{
CloseSocket();
_received = false;
_ws = new WebSocketClient(new WebSocketClientOptions { Timeout = TimeSpan.FromSeconds(15) });
_ws.Connected += OnSocketConnected;
_ws.TextReceived += OnSocketText;
_ws.Disconnected += OnSocketDisconnected;
_ws.Connect(new Uri(CalendarWs));
}
catch (Exception ex)
{
Print("[SMT] calendar socket failed: {0}", ex.Message);
DownloadOverHttp();
}
}
private void OnSocketConnected(WebSocketClientConnectedEventArgs args)
{
try { _ws.Send(Query()); }
catch (Exception ex) { Print("[SMT] calendar send failed: {0}", ex.Message); }
}
private void OnSocketText(WebSocketClientTextReceivedEventArgs args)
{
_received = true;
if (args.Text.StartsWith("Error:")) Print("[SMT] calendar: {0}", args.Text);
else ParseCsv(args.Text);
CloseSocket();
}
private void OnSocketDisconnected(WebSocketClientDisconnectEventArgs args)
{
if (!_received) DownloadOverHttp(); // desktop only: the cloud answers 503
}
private void DownloadOverHttp()
{
try
{
HttpResponse r = Http.Get(CalendarHttp + Query());
if (r.IsSuccessful) ParseCsv(r.Body);
else Print("[SMT] calendar HTTP failed: {0}", r.StatusCode);
}
catch (Exception ex) { Print("[SMT] calendar HTTP failed: {0}", ex.Message); }
}
private void CloseSocket()
{
WebSocketClient ws = _ws;
_ws = null;
if (ws == null) return;
try { ws.Connected -= OnSocketConnected; ws.TextReceived -= OnSocketText; ws.Disconnected -= OnSocketDisconnected; } catch (Exception) { }
try { if (ws.State == WebSocketClientState.Open) ws.Close(WebSocketClientCloseStatus.NormalClosure, "done"); } catch (Exception) { }
try { ws.Dispose(); } catch (Exception) { }
}
// CSV: ts,time,currency,impact,forecast,previous,actual,title (title last, may be quoted)
private void ParseCsv(string csv)
{
List<KeyValuePair<DateTime, string>> list = new List<KeyValuePair<DateTime, string>>();
foreach (string raw in csv.Split('\n'))
{
string line = raw.TrimEnd('\r');
if (line.Length == 0 || line.StartsWith("ts,")) continue;
string[] f = line.Split(',');
if (f.Length < 8) continue;
long ts;
if (!long.TryParse(f[0], out ts)) continue;
DateTime at = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(ts);
string title = AfterComma(line, 7).Replace("\"\"", "\"");
if (title.Length >= 2 && title[0] == '"') title = title.Substring(1, title.Length - 2);
list.Add(new KeyValuePair<DateTime, string>(at, f[2] + " " + f[3] + ": " + title));
}
_events.Clear();
_events.AddRange(list);
Print("[SMT] calendar: {0} events loaded", _events.Count);
}
private static string AfterComma(string line, int n)
{
int pos = -1;
for (int i = 0; i < n; i++)
{
pos = line.IndexOf(',', pos + 1);
if (pos < 0) return "";
}
return line.Substring(pos + 1);
}
}
}
To put the filter inside an existing cBot, copy everything from CalendarWs down to AfterComma() into it, call DownloadCalendar() from OnStart() and from a 15-minute timer, and check IsNewsTime() before each entry.
๐งช Trying It
- Build the cBot and add an instance to a chart. The log should show
[SMT] calendar: N events loadedwithin a few seconds. Zero on a quiet weekend is correct. - Set News: impact to
alland minutes before to a large number to see the filter engage, then put the real values back. - Run the same instance in the cloud: the log line is the same. That is the whole point of the socket.
โฑ๏ธ Times and the Backtester
The calendar's timestamps are UTC and the code compares them with Server.TimeInUtc, so the broker's time zone and daylight saving never enter the picture. The backtester and the optimizer have no network: DownloadCalendar() returns early there and the filter is silent. Backtesting a news filter needs a file of past events shipped with the cBot, a separate exercise.
๐๏ธ Tuning
- Open positions are not touched. Use
IsNewsTime()to decide what to do with them: close, trail, or leave alone. - Medium impact (
high,medium) triples the events. Scalpers often want it, swing systems rarely. - Several cBots on one account each download on their own: ten of them are 40 small requests an hour, far below the limit of 60 socket messages per minute per IP address.
โ FAQ
Why a WebSocket and not HTTPS? Because cTrader Cloud does not let a cBot make HTTP requests, and it allows WebSocket connections only on port 25345. On the desktop the same cBot would also work over HTTPS, and the code falls back to it if the socket cannot connect.
Is it safe to send on ws:// without encryption?
The request contains four parameters (currencies, impact, days, format) and nothing about you or your account; the response is a public calendar. There is nothing to protect. The same data is available over HTTPS from the API endpoint.
Does it need AccessRights.FullAccess?
No. WebSocketClient and Http are available with AccessRights.None, which is also what the cTrader Store requires.
Does it need a ShowMyTrades account? No. If you also want to publish the results of the cBot, connecting a cTrader account is a separate thing and takes one OAuth authorisation.
๐ Related
- Economic calendar API reference: the WebSocket protocol, the HTTP endpoint, JSON and CSV formats
- News filter for an MT4 Expert Advisor: the same filter in MQL
- Show your cTrader results on your website
- What is cTrader
The socket is the only unusual part, and it is already written. The rest is your strategy, with one fewer way to lose money around the news. Track the account free to see the difference on the drawdown chart.
News Filter in MT5: the Built-in Calendar or an API?
MetaTrader 5 ships an economic calendar that Expert Advisors can read with no download. When it is enough, when it is not, and how to use a free calendar API instead, with MQL5 code for both.
A Free Economic Calendar API for Trading Bots
Where a trading robot can get the economic calendar: the sources people use, what breaks with each, and a free API with no key built for polling from Expert Advisors, cBots, Python and JavaScript.