Automatisierung·

News-Filter in einen cTrader-cBot einbauen (Cloud inklusive)

cTrader bietet cBots keine Kalender-API, und die cTrader Cloud blockiert HTTP-Anfragen. Dieser News-Filter funktioniert am Desktop und in der Cloud: mit einem kostenlosen Kalender über WebSocket und dem vollständigen C#-Code.

Wer einen cBot mit einem News-Filter ausstatten will, stößt auf zwei Hürden, die MetaTrader-Entwickler gar nicht kennen. Erstens hat die cTrader-Automate-API keinen Wirtschaftskalender: Die Plattform zeigt zwar einen an, ein cBot kann ihn aber nicht auslesen. Zweitens laufen die meisten cBots in der cTrader Cloud, und dort darf ein cBot überhaupt keine HTTP-Anfragen stellen: Http.Get() kommt mit dem Status 503 zurück.

Eines lässt die Cloud allerdings zu: einen WebSocket auf Port 25345. Genau auf diesem Weg erreicht der kostenlose Wirtschaftskalender von ShowMyTrades einen cBot. Diese Anleitung liefert dir alles auf einmal: das Prinzip, das Protokoll und einen cBot, den du unverändert kompilieren kannst.

🧭 So funktioniert es

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() │
└──────────────────────────┘
Adressews://showmytrades.com:25345/
Du sendestEine Textzeile mit denselben Parametern wie bei der HTTP-API: currency, impact, days, format
Du bekommstEinen einzigen Text-Frame mit dem Inhalt als CSV (oder JSON)
ZugriffsrechteAccessRights.None genügt, der cBot lässt sich also veröffentlichen
Schlüssel oder KontoNicht nötig
Ausweichweg am DesktopDort funktioniert normales HTTP; der Code unten weicht darauf aus, wenn der Socket nicht klappt

Der Socket nutzt ws:// und nicht wss://, weil sich die cTrader Cloud genau so verbindet. Über die Verbindung läuft nichts, was dich oder dein Konto betrifft, sondern nur die vier Parameter.

📄 Der cBot

Lege in cTrader Automate einen neuen cBot an, ersetze seinen Code durch den folgenden und starte den Build. Am Desktop wird er gegen cTrader.Automate aus NuGet kompiliert, in der Cloud läuft er so, wie er ist.

SmtNewsFilterBot.cs
// News-Filter für cTrader (Desktop und Cloud), Daten von ShowMyTrades, kostenlos, ohne Schlüssel.
// Doku: 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 static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        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))
            {
                // Im News-Fenster keine neuen Einstiege. Offene Positionen verwaltest du nach Belieben.
                return;
            }

            // ... deine Einstiegslogik ...
        }

        protected override void OnStop()
        {
            CloseSocket();
        }

        // True, solange eine Veröffentlichung im Pausenfenster liegt; reason sagt, welche.
        // Server.TimeInUtc ist die eigene UTC-Uhr von cTrader: Die Plattform kennt den Zeitversatz des Brokers,
        // der Vergleich mit dem Kalender ist also sicher (ParseCsv gleicht sie mit dem Server ab).
        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 += "&currency=" + NewsCurrencies.Replace(" ", "");
            return q;
        }

        private void DownloadCalendar()
        {
            if (RunningMode != RunningMode.RealTime) return; // im Backtester gibt es kein Netzwerk

            // Die cTrader Cloud sperrt HTTP aus cBots und erlaubt nur einen WebSocket auf Port 25345,
            // deshalb ist der Socket überall der Hauptweg; HTTP ist am Desktop der Ausweichweg.
            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(); // nur am Desktop: Die Cloud antwortet mit 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 zuletzt, evtl. in Anführungszeichen),
        // danach eine letzte Zeile "# server_time=<unix> server_time_iso=<iso>" mit der UTC-Uhr des Servers.
        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;
                if (line.StartsWith("# server_time="))
                {
                    long serverTime;
                    if (long.TryParse(line.Substring(14, 10), out serverTime))
                    {
                        double skew = (Server.TimeInUtc - Epoch.AddSeconds(serverTime)).TotalSeconds;
                        if (Math.Abs(skew) > 60) Print("[SMT] warning: cTrader's UTC clock is {0:F0} s away from the calendar server", skew);
                    }
                    continue;
                }
                string[] f = line.Split(',');
                if (f.Length < 8) continue;
                long ts;
                if (!long.TryParse(f[0], out ts)) continue;
                DateTime at = Epoch.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);
        }
    }
}

Willst du den Filter in einen bestehenden cBot einbauen, kopierst du alles von CalendarWs bis AfterComma() hinein, rufst DownloadCalendar() in OnStart() und über einen 15-Minuten-Timer auf und prüfst vor jedem Einstieg IsNewsTime().

🧪 Ausprobieren

  1. Baue den cBot und füge einem Chart eine Instanz hinzu. Nach wenigen Sekunden sollte im Log [SMT] calendar: N events loaded stehen. An einem ruhigen Wochenende ist null völlig korrekt.
  2. Setze News: impact auf all und minutes before auf einen großen Wert: So siehst du, wie der Filter greift. Danach stellst du die echten Werte wieder ein.
  3. Starte dieselbe Instanz in der Cloud: Die Log-Zeile ist dieselbe. Genau dafür gibt es den Socket.

⏱️ Uhrzeiten und Backtester

Die Zeitstempel des Kalenders sind in UTC, und der Code vergleicht sie mit Server.TimeInUtc. Zeitzone und Sommerzeit des Brokers spielen dadurch keine Rolle. Jeder Download endet außerdem mit der UTC-Uhr des Servers (# server_time=…): Der cBot gleicht Server.TimeInUtc damit ab und schreibt eine Warnung ins Log, wenn beide um mehr als eine Minute auseinanderliegen. Eine falsch gehende Uhr fällt so in jedem Fall auf. Backtester und Optimierer haben kein Netzwerk: DownloadCalendar() kehrt dort sofort zurück, und der Filter bleibt stumm. Wer einen News-Filter backtesten will, braucht eine Datei mit vergangenen Ereignissen, die zusammen mit dem cBot ausgeliefert wird. Das ist ein eigenes Thema.

🎛️ Feinabstimmung

  • Offene Positionen bleiben unberührt. Mit IsNewsTime() entscheidest du selbst, was mit ihnen passiert: schließen, den Stop nachziehen oder in Ruhe lassen.
  • Mittlere Wichtigkeit (high,medium) verdreifacht die Zahl der Termine. Scalper wollen sie oft dabeihaben, Swing-Systeme eher selten.
  • Mehrere cBots auf einem Konto laden ihren Kalender jeweils selbst herunter: Zehn davon kommen auf 40 kleine Anfragen pro Stunde, weit unter dem Limit von 60 Socket-Nachrichten pro Minute und IP-Adresse.

❓ Häufige Fragen

Warum ein WebSocket und nicht HTTPS? Weil die cTrader Cloud einem cBot keine HTTP-Anfragen erlaubt und WebSocket-Verbindungen nur auf Port 25345 zulässt. Am Desktop würde derselbe cBot auch über HTTPS laufen, und der Code weicht darauf aus, wenn der Socket keine Verbindung bekommt.

Ist es sicher, unverschlüsselt über ws:// zu senden? Die Anfrage enthält vier Parameter (Währungen, Wichtigkeit, Tage, Format) und nichts über dich oder dein Konto; die Antwort ist ein öffentlicher Kalender. Es gibt also nichts zu schützen. Dieselben Daten bekommst du auch per HTTPS über den API-Endpunkt.

Braucht der cBot AccessRights.FullAccess? Nein. WebSocketClient und Http stehen schon mit AccessRights.None zur Verfügung, und genau das verlangt auch der cTrader Store.

Brauche ich ein ShowMyTrades-Konto? Nein. Wenn du zusätzlich die Ergebnisse des cBots veröffentlichen willst, ist das Verbinden eines cTrader-Kontos eine eigene Sache, die eine einzige OAuth-Autorisierung erfordert.

🔗 Weiterführende Artikel


Der Socket ist der einzige ungewöhnliche Teil, und der ist schon geschrieben. Der Rest ist deine Strategie, mit einer Möglichkeit weniger, rund um die News Geld zu verlieren. Behalte das Konto kostenlos im Blick, dann siehst du den Unterschied im Drawdown-Chart.