Automatización·

Cómo agregar un filtro de noticias a un cBot de cTrader (también en la nube)

cTrader no ofrece a los cBots ninguna API de calendario, y cTrader Cloud bloquea las solicitudes HTTP. Aquí tienes un filtro de noticias que funciona en el escritorio y en la nube, con un calendario gratuito por WebSocket y el código C# completo.

Quien programa un filtro de noticias para un cBot choca con dos muros que en MetaTrader no existen. El primero: la API de cTrader Automate no tiene calendario económico. La plataforma muestra uno, pero un cBot no puede leerlo. El segundo: la mayoría de la gente ejecuta sus cBots en cTrader Cloud, donde un cBot no puede hacer ninguna solicitud HTTP: Http.Get() devuelve el estado 503.

La nube sí permite una cosa: un WebSocket en el puerto 25345. Por ahí llega a un cBot el calendario económico de ShowMyTrades, que es gratuito. En esta guía tienes todo: la idea, el protocolo y un cBot que puedes compilar tal cual.

🧭 Cómo funciona

cBot (escritorio o nube)                     ShowMyTrades
┌──────────────────────────┐                 ┌─────────────────────────┐
│ cada 15 min:             │  ws://…:25345   │ el mismo calendario que │
│  abre el socket, envía   │ ──────────────► │ /api/public/calendar/v1 │
│  "currency=EUR,USD&      │                 │ en caché, filtrado,     │
│   impact=high&days=2&    │ ◄────────────── │ unos pocos kilobytes    │
│   format=csv"            │   texto CSV     └─────────────────────────┘
│  analiza, cierra         │
│ cada tick: IsNewsTime()  │
└──────────────────────────┘
Direcciónws://showmytrades.com:25345/
Qué envíasUna línea de texto con los mismos parámetros que la API HTTP: currency, impact, days, format
Qué recibesUn frame de texto con el cuerpo en CSV (o JSON)
Permisos de accesoBasta con AccessRights.None, así que el cBot se puede publicar
Clave o cuentaNinguna
Plan B en el escritorioAhí el HTTP normal funciona; el código de abajo recurre a él si el socket falla

El socket usa ws:// y no wss:// porque es lo que acepta cTrader Cloud. Por él no viaja nada sobre ti ni sobre tu cuenta: solo los cuatro parámetros.

📄 El cBot

Crea un cBot nuevo en cTrader Automate, sustituye su código por este y compílalo. En el escritorio se compila contra el paquete cTrader.Automate de NuGet; en la nube funciona tal cual.

SmtNewsFilterBot.cs
// Filtro de noticias para cTrader (escritorio y nube), datos de ShowMyTrades, gratis, sin clave.
// Documentación: 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))
            {
                // Sin entradas nuevas durante la ventana de noticias. Gestiona las posiciones abiertas como prefieras.
                return;
            }

            // ... tu lógica de entrada ...
        }

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

        // True mientras una publicación cae dentro de la ventana de pausa; reason indica cuál.
        // Server.TimeInUtc es el reloj UTC del propio cTrader: la plataforma conoce el desfase del bróker,
        // así que se puede comparar con el calendario sin problema (ParseCsv lo contrasta con el servidor).
        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; // el backtester no tiene red

            // cTrader Cloud bloquea el HTTP de los cBots y solo permite un WebSocket en el puerto 25345,
            // así que el socket es la vía principal en todas partes; HTTP es el plan B en el escritorio.
            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(); // solo en el escritorio: la nube responde 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 al final, puede ir entre comillas),
        // y una última línea "# server_time=<unix> server_time_iso=<iso>" con el reloj UTC del servidor.
        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);
        }
    }
}

Para incorporar el filtro a un cBot que ya tienes, copia en él todo lo que va de CalendarWs hasta AfterComma(), llama a DownloadCalendar() desde OnStart() y desde un temporizador de 15 min, y comprueba IsNewsTime() antes de cada entrada.

🧪 Cómo probarlo

  1. Compila el cBot y agrega una instancia a un gráfico (Add instance). En pocos segundos el registro debería mostrar [SMT] calendar: N events loaded. Un cero en un fin de semana tranquilo es lo normal.
  2. Pon News: impact en all y minutes before en un número alto para ver cómo actúa el filtro; después vuelve a los valores reales.
  3. Ejecuta la misma instancia en la nube: la línea del registro es la misma. Para eso existe el socket.

⏱️ Los horarios y el backtester

Las marcas de tiempo del calendario están en UTC y el código las compara con Server.TimeInUtc, así que ni la zona horaria del bróker ni el horario de verano entran en juego. Además, cada descarga termina con el reloj UTC del propio servidor (# server_time=…): el cBot compara Server.TimeInUtc con ese valor y escribe un aviso si los dos difieren en más de un minuto, de modo que un reloj desajustado no pasa inadvertido. El backtester y el optimizador no tienen red: ahí DownloadCalendar() sale antes de tiempo y el filtro no hace nada. Para hacer el backtest de un filtro de noticias hace falta un archivo de eventos pasados que viaje con el cBot, y eso es otro trabajo.

🎛️ Ajuste fino

  • Las posiciones abiertas no se tocan. Usa IsNewsTime() para decidir qué hacer con ellas: cerrarlas, mover el stop o dejarlas como están.
  • El impacto medio (high,medium) triplica los eventos. Los scalpers suelen quererlo; los sistemas swing, rara vez.
  • Varios cBots en una misma cuenta descargan cada uno por su lado: diez cBots son 40 solicitudes pequeñas por hora, muy por debajo del límite de 60 mensajes por minuto y por dirección IP en el socket.

❓ Preguntas frecuentes

¿Por qué un WebSocket y no HTTPS? Porque cTrader Cloud no deja que un cBot haga solicitudes HTTP y solo permite conexiones WebSocket en el puerto 25345. En el escritorio el mismo cBot también funcionaría por HTTPS, y el código recurre a él si el socket no logra conectarse.

¿Es seguro enviar por ws:// sin cifrado? La solicitud lleva cuatro parámetros (divisas, impacto, días, formato) y nada sobre ti ni sobre tu cuenta; la respuesta es un calendario público. No hay nada que proteger. Los mismos datos están disponibles por HTTPS en el endpoint de la API.

¿Necesita AccessRights.FullAccess? No. WebSocketClient y Http están disponibles con AccessRights.None, que además es lo que exige la cTrader Store.

¿Hace falta una cuenta de ShowMyTrades? No. Si además quieres publicar los resultados del cBot, conectar una cuenta de cTrader es otra cosa y requiere una sola autorización OAuth.

🔗 Artículos relacionados


El socket es la única parte fuera de lo común, y ya está escrita. El resto es tu estrategia, con una forma menos de perder dinero alrededor de las noticias. Haz el seguimiento gratuito de la cuenta y verás la diferencia en el gráfico de drawdown.