如何为 cTrader cBot 添加新闻过滤器(含云端)
在 cBot 里做新闻过滤,会撞上两堵 MetaTrader 开发者从没见过的墙。第一,cTrader Automate API 没有财经日历:平台界面上看得到日历,cBot 却读不到。第二,多数人把 cBot 放在 cTrader Cloud 上运行,而那里的 cBot 根本发不出 HTTP 请求,Http.Get() 只会返回状态码 503。
云端只放行一样东西:端口 25345 上的 WebSocket。免费的 ShowMyTrades 财经日历正是走这条路送到 cBot 手里。本指南给出完整方案:思路、协议,外加一个拿来就能编译的 cBot。
🧭 工作原理
cBot(桌面端或云端)
┌──────────────────────────
│ 每 15 分钟:
│ 打开 socket,发送
│ "currency=EUR,USD&
│ impact=high&days=2&
│ format=csv"
│ 解析,关闭
│ 每个 tick:IsNewsTime()
└──────────────────────────
ws://…:25345 CSV 文本
│ ▲
▼ │
ShowMyTrades
┌──────────────────────────
│ 与以下接口相同的日历:
│ /api/public/calendar/v1
│ 已缓存、已筛选,
│ 体积仅几 KB
└──────────────────────────
| 地址 | ws://showmytrades.com:25345/ |
| 您发送 | 一行文本,参数与 HTTP API 相同:currency、impact、days、format |
| 您收到 | 一个文本帧,内容是 CSV(或 JSON)正文 |
| 访问权限 | AccessRights.None 就够了,所以这个 cBot 可以发布 |
| 密钥或账户 | 都不需要 |
| 桌面端备用方案 | 桌面端可以直接用 HTTP;如果 socket 连接失败,下面的代码会自动改用 HTTP |
这里用的是 ws:// 而不是 wss://,因为 cTrader Cloud 连接时用的就是它。连接上传的只有这四个参数,不涉及您本人或您账户的任何信息。
📄 cBot 代码
在 cTrader Automate(自动化)里新建一个 cBot,把代码换成下面这段,然后构建(Build)。桌面端用 NuGet 上的 cTrader.Automate 编译;云端原样即可运行。
// 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("新闻:货币(逗号分隔,留空 = 全部)", DefaultValue = "EUR,USD")]
public string NewsCurrencies { get; set; }
[Parameter("新闻:影响级别(high | high,medium | all)", DefaultValue = "high")]
public string NewsImpact { get; set; }
[Parameter("新闻:公布前分钟数", DefaultValue = 30, MinValue = 0)]
public int MinutesBefore { get; set; }
[Parameter("新闻:公布后分钟数", 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 += "¤cy=" + NewsCurrencies.Replace(" ", "");
return q;
}
private void DownloadCalendar()
{
if (RunningMode != RunningMode.RealTime) return; // 回测器中没有网络
// cTrader Cloud 禁止 cBot 发出 HTTP 请求,只允许端口 25345 上的 WebSocket,
// 所以各处都以 socket 为主通道;桌面端以 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] 日历 socket 连接失败:{0}", ex.Message);
DownloadOverHttp();
}
}
private void OnSocketConnected(WebSocketClientConnectedEventArgs args)
{
try { _ws.Send(Query()); }
catch (Exception ex) { Print("[SMT] 日历请求发送失败:{0}", ex.Message); }
}
private void OnSocketText(WebSocketClientTextReceivedEventArgs args)
{
_received = true;
if (args.Text.StartsWith("Error:")) Print("[SMT] 日历:{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] 日历 HTTP 请求失败:{0}", r.StatusCode);
}
catch (Exception ex) { Print("[SMT] 日历 HTTP 请求失败:{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] 警告:cTrader 的 UTC 时钟与日历服务器相差 {0:F0} 秒", 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] 日历:已加载 {0} 个事件", _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()。
🧪 试运行
- 构建 cBot,在图表上添加一个实例。几秒内日志里应出现
[SMT] 日历:已加载 N 个事件。清淡的周末显示为零,属于正常。 - 把新闻:影响级别设为
all,新闻:公布前分钟数设成一个很大的数,看到过滤器生效后,再改回实际要用的数值。 - 把同一个实例放到云端运行,日志里还是那一行。用 socket 图的就是这个。
⏱️ 时间与回测器
日历的时间戳都是 UTC,代码拿它和 Server.TimeInUtc 比较,经纪商的时区和夏令时根本不参与。每次下载的末尾还附有服务器自己的 UTC 时钟(# server_time=…):cBot 会拿它核对 Server.TimeInUtc,两者相差超过一分钟就打印警告,时钟出了错不会没人发现。回测器和优化器不能联网:DownloadCalendar() 在那里直接返回,过滤器不起作用。要回测新闻过滤器,得给 cBot 附带一份历史事件文件,那是另一项工作。
🎛️ 调整
- 持仓不受影响。要怎么处理,可以用
IsNewsTime()来判断:平仓、追踪止损,或者放着不管。 - 加上中影响(
high,medium),事件数量会变成三倍。剥头皮往往用得上,波段系统很少需要。 - 同一账户跑多个 cBot 时,各自单独下载:十个 cBot 每小时合计 40 个小请求,远低于每个 IP 地址每分钟 60 条 socket 消息的上限。
❓ 常见问题
为什么用 WebSocket 而不是 HTTPS? 因为 cTrader Cloud 不让 cBot 发 HTTP 请求,WebSocket 连接也只放行端口 25345。在桌面端,同一个 cBot 走 HTTPS 也行,socket 连不上时代码会自动改用 HTTPS。
用不加密的 ws:// 发送安全吗?
请求里只有四个参数(货币、影响级别、天数、格式),不含您本人或账户的任何信息;返回的是公开的财经日历。没有什么需要保护的。同样的数据也能通过 HTTPS 从 API 端点获取。
需要 AccessRights.FullAccess 吗?
不需要。WebSocketClient 和 Http 在 AccessRights.None 下就能使用,这也是 cTrader Store 的要求。
需要 ShowMyTrades 账户吗? 不需要。如果还想公开这个 cBot 的交易结果,连接 cTrader 账户是另一回事,只需一次 OAuth 授权。
🔗 相关内容
- 财经日历 API 参考:WebSocket 协议、HTTP 端点、JSON 和 CSV 格式
- MT4 EA(智能交易系统)的新闻过滤器:用 MQL 实现的同一个过滤器
- 在您的网站上展示 cTrader 交易结果
- 什么是 cTrader
唯一特别的只有 socket 这部分,而它已经替您写好了。剩下的还是您的策略,只不过在新闻前后少了一种亏钱的方式。免费追踪账户,在回撤图表上看看差别。