自動売買·

cTrader cBotにニュースフィルターを組み込む方法(Cloud対応)

cTraderにはcBotから読める経済指標カレンダーのAPIがなく、cTrader CloudではHTTPリクエストも送れません。デスクトップでもクラウドでも動くニュースフィルターを、WebSocketで取得する無料のカレンダーとC#の全コードつきで紹介します。

cBotにニュースフィルターを入れようとすると、MetaTraderの開発者なら出会わない壁に2つぶつかります。1つ目は、cTrader Automate APIに経済指標カレンダーがないことです。プラットフォームの画面にはカレンダーが表示されるのに、cBotからは読み取れません。2つ目は、cBotをcTrader Cloudで動かす人が大半なのに、そこではcBotからHTTPリクエストを一切送れないことです。Http.Get()を呼んでも、返ってくるのはステータス503です。

ただし、クラウドでも1つだけ許されている通信があります。ポート25345のWebSocketです。無料のShowMyTradesの経済指標カレンダーは、この経路でcBotにデータを届けます。このガイドでは、考え方、プロトコル、そのままコンパイルできるcBotまで、必要なものをすべてまとめました。

🧭 仕組み

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() │
└──────────────────────────┘
接続先ws://showmytrades.com:25345/
送信するものHTTP APIと同じパラメーター(currency、impact、days、format)を書いた1行のテキスト
返ってくるものCSV(またはJSON)の本文が入ったテキストフレーム1つ
アクセス権限AccessRights.Noneで足りるため、cBotを公開できます
キー・会員登録不要
デスクトップでの代替経路デスクトップでは通常のHTTPも使えます。以下のコードは、ソケットが失敗したときにHTTPへ切り替えます

ソケットはwss://ではなくws://を使います。cTrader Cloudが接続に使うのがws://だからです。この通信でやり取りされるのは4つのパラメーターだけで、利用者や口座に関する情報は一切含まれません。

📄 cBot本体

cTrader AutomateでcBotを新規作成し、コードを以下にまるごと置き換えてビルドします。デスクトップではNuGetのcTrader.Automateを参照してコンパイルされ、クラウドではそのまま動きます。

SmtNewsFilterBot.cs
// cTrader用ニュースフィルター(デスクトップ・クラウド対応)。データはShowMyTrades提供、無料、キー不要。
// ドキュメント: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))
            {
                // ニュースの時間帯は新規エントリーしない。保有ポジションの扱いはお好みで。
                return;
            }

            // ... エントリーのロジック ...
        }

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

        // 指標発表が停止時間帯に入っている間はtrue。reasonにどの指標かが入る。
        // Server.TimeInUtcはcTrader自身のUTC時計。プラットフォームがブローカーの時差を把握しているので、
        // カレンダーとそのまま比較してよい(ParseCsvがサーバーの時刻と照合する)。
        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; // バックテスターではネットワークが使えない

            // cTrader CloudはcBotからのHTTPを遮断し、ポート25345のWebSocketしか許可しない。
            // そのため、どこでもソケットを第一の経路とし、デスクトップではHTTPを予備にする。
            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(); // デスクトップのみ(クラウドは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は最後の列で、引用符付きの場合あり)。
        // 最終行は "# server_time=<unix> server_time_iso=<iso>" で、サーバーのUTC時計を示す。
        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);
        }
    }
}

既存のcBotにフィルターを組み込むときは、CalendarWsからAfterComma()までをすべてコピーして貼り付けます。あとはOnStart()と15分ごとのタイマーからDownloadCalendar()を呼び出し、エントリーの前に毎回IsNewsTime()を確認するだけです。

🧪 動作を確認する

  1. cBotをビルドし、チャートにインスタンスを追加します。数秒以内に、ログへ[SMT] calendar: N events loadedと表示されるはずです。指標の少ない週末なら0件でも正常です。
  2. News: impactをallに、minutes beforeを大きな数値にすると、フィルターが作動する様子を確認できます。確認が済んだら本来の値に戻してください。
  3. 同じインスタンスをクラウドで動かします。ログの出力は同じです。ソケットを使う意味は、まさにここにあります。

⏱️ 時刻の扱いとバックテスト

カレンダーのタイムスタンプはUTCで、コードはこれをServer.TimeInUtcと比べます。そのため、ブローカーのタイムゾーンや夏時間を考える必要はありません。ダウンロードしたデータの末尾には、毎回サーバー自身のUTC時刻(# server_time=…)も付いています。cBotはServer.TimeInUtcをこの時刻と照合し、ずれが1分を超えると警告を出力します。時計が狂っていれば、必ず気付けます。バックテストと最適化ではネットワークが使えないため、DownloadCalendar()はすぐに処理を終え、フィルターは何もしません。ニュースフィルターをバックテストするには、過去の指標をまとめたファイルをcBotに同梱する必要があり、これは別の作業になります。

🎛️ 調整のポイント

  • 保有ポジションには手を付けません。決済する、ストップを追従させる、そのままにする、といった扱いはIsNewsTime()を使って自分で決めてください。
  • 重要度「中」(high,medium)まで含めると、対象の指標は3倍になります。スキャルパーは含めたがることが多く、スイング系のシステムではあまり使いません。
  • 1つの口座で複数のcBotを動かす場合、ダウンロードはcBotごとに行われます。10本動かしても1時間に40回の小さなリクエストで、IPアドレスごとに毎分60メッセージというソケットの上限を大きく下回ります。

❓ よくある質問

なぜHTTPSではなくWebSocketなのですか?

cTrader CloudではcBotからHTTPリクエストを送れず、WebSocket接続もポート25345でしか許可されていないからです。デスクトップなら同じcBotがHTTPSでも動きます。ソケットに接続できなかった場合、コードはHTTPSに切り替えます。

暗号化のないws://で送っても安全ですか?

送るのは4つのパラメーター(通貨、重要度、日数、形式)だけで、利用者や口座の情報は含まれません。返ってくるのも公開されているカレンダーです。守るべきものは何もありません。同じデータはAPIエンドポイントからHTTPSでも取得できます。

AccessRights.FullAccessは必要ですか?

不要です。WebSocketClientとHttpはAccessRights.Noneで使えます。cTrader Storeで求められるのも、この設定です。

ShowMyTradesへの登録は必要ですか?

不要です。cBotの成績も公開したい場合は、別途cTrader口座の連携が必要です。OAuthの認可を1回行うだけで済みます。

🔗 関連ページ


特殊なのはソケットの部分だけで、そこはもう書き上がっています。残りは自分の手法です。ニュースの前後で損をする原因が、これで1つ減ります。口座を無料で記録すると、その違いがドローダウンのチャートに表れます。