[{"data":1,"prerenderedAt":1522},["ShallowReactive",2],{"\u002Fzh\u002Fguides\u002Fadd-a-news-filter-to-a-ctrader-cbot":3,"\u002Fzh\u002Fguides\u002Fadd-a-news-filter-to-a-ctrader-cbot-surround":1507,"\u002Fzh\u002Fguides\u002Fadd-a-news-filter-to-a-ctrader-cbot-langs":1518},{"id":4,"title":5,"authors":6,"badge":12,"body":14,"date":1495,"description":1496,"draft":1497,"extension":1498,"image":1499,"meta":1501,"navigation":218,"path":1502,"seo":1503,"stem":1504,"updated":1505,"__hash__":1506},"posts\u002Fzh\u002F3.guides\u002F43.add-a-news-filter-to-a-ctrader-cbot.md","如何为 cTrader cBot 添加新闻过滤器（含云端）",[7],{"name":8,"to":9,"avatar":10},"ShowMyTrades Team","https:\u002F\u002Fshowmytrades.com",{"src":11},"\u002Ficon-512.png",{"label":13},"自动化",{"type":15,"value":16,"toc":1486},"minimark",[17,39,52,57,67,151,162,166,173,1272,1294,1298,1332,1336,1353,1357,1386,1390,1396,1409,1430,1441,1445,1471,1474,1482],[18,19,20,21,25,26,29,30,34,35,38],"p",{},"在 cBot 里做新闻过滤，会撞上两堵 MetaTrader 开发者从没见过的墙。第一，cTrader Automate API ",[22,23,24],"strong",{},"没有财经日历","：平台界面上看得到日历，cBot 却读不到。第二，多数人把 cBot 放在 ",[22,27,28],{},"cTrader Cloud"," 上运行，而那里的 cBot 根本发不出 HTTP 请求，",[31,32,33],"code",{},"Http.Get()"," 只会返回状态码 ",[31,36,37],{},"503","。",[18,40,41,42,45,46,51],{},"云端只放行一样东西：",[22,43,44],{},"端口 25345 上的 WebSocket","。免费的 ",[47,48,50],"a",{"href":49},"\u002Fdocs\u002Fapi\u002Feconomic-calendar","ShowMyTrades 财经日历","正是走这条路送到 cBot 手里。本指南给出完整方案：思路、协议，外加一个拿来就能编译的 cBot。",[53,54,56],"h2",{"id":55},"工作原理","🧭 工作原理",[58,59,64],"pre",{"className":60,"code":62,"language":63},[61],"language-text","cBot（桌面端或云端）\n┌──────────────────────────\n│ 每 15 分钟：\n│  打开 socket，发送\n│  \"currency=EUR,USD&\n│   impact=high&days=2&\n│   format=csv\"\n│  解析，关闭\n│ 每个 tick：IsNewsTime()\n└──────────────────────────\n     ws:\u002F\u002F…:25345    CSV 文本\n          │             ▲\n          ▼             │\nShowMyTrades\n┌──────────────────────────\n│ 与以下接口相同的日历：\n│ \u002Fapi\u002Fpublic\u002Fcalendar\u002Fv1\n│ 已缓存、已筛选，\n│ 体积仅几 KB\n└──────────────────────────\n","text",[31,65,62],{"__ignoreMap":66},"",[68,69,70,81],"table",{},[71,72,73],"thead",{},[74,75,76,79],"tr",{},[77,78],"th",{},[77,80],{},[82,83,84,95,116,124,135,143],"tbody",{},[74,85,86,90],{},[87,88,89],"td",{},"地址",[87,91,92],{},[31,93,94],{},"ws:\u002F\u002Fshowmytrades.com:25345\u002F",[74,96,97,100],{},[87,98,99],{},"您发送",[87,101,102,103,106,107,106,110,106,113],{},"一行文本，参数与 HTTP API 相同：",[31,104,105],{},"currency","、",[31,108,109],{},"impact",[31,111,112],{},"days",[31,114,115],{},"format",[74,117,118,121],{},[87,119,120],{},"您收到",[87,122,123],{},"一个文本帧，内容是 CSV（或 JSON）正文",[74,125,126,129],{},[87,127,128],{},"访问权限",[87,130,131,134],{},[31,132,133],{},"AccessRights.None"," 就够了，所以这个 cBot 可以发布",[74,136,137,140],{},[87,138,139],{},"密钥或账户",[87,141,142],{},"都不需要",[74,144,145,148],{},[87,146,147],{},"桌面端备用方案",[87,149,150],{},"桌面端可以直接用 HTTP；如果 socket 连接失败，下面的代码会自动改用 HTTP",[18,152,153,154,157,158,161],{},"这里用的是 ",[31,155,156],{},"ws:\u002F\u002F"," 而不是 ",[31,159,160],{},"wss:\u002F\u002F","，因为 cTrader Cloud 连接时用的就是它。连接上传的只有这四个参数，不涉及您本人或您账户的任何信息。",[53,163,165],{"id":164},"cbot-代码","📄 cBot 代码",[18,167,168,169,172],{},"在 cTrader Automate（自动化）里新建一个 cBot，把代码换成下面这段，然后构建（Build）。桌面端用 NuGet 上的 ",[31,170,171],{},"cTrader.Automate"," 编译；云端原样即可运行。",[58,174,179],{"className":175,"code":176,"filename":177,"language":178,"meta":66,"style":66},"language-csharp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F cTrader 新闻过滤器（桌面端和云端），数据来自 ShowMyTrades，免费，无需密钥。\n\u002F\u002F 文档：https:\u002F\u002Fshowmytrades.com\u002Fdocs\u002Fapi\u002Feconomic-calendar\nusing System;\nusing System.Collections.Generic;\nusing cAlgo.API;\n\nnamespace cAlgo.Robots\n{\n    [Robot(AccessRights = AccessRights.None, TimeZone = TimeZones.UTC)]\n    public class SmtNewsFilterBot : Robot\n    {\n        [Parameter(\"新闻：货币（逗号分隔，留空 = 全部）\", DefaultValue = \"EUR,USD\")]\n        public string NewsCurrencies { get; set; }\n\n        [Parameter(\"新闻：影响级别（high | high,medium | all）\", DefaultValue = \"high\")]\n        public string NewsImpact { get; set; }\n\n        [Parameter(\"新闻：公布前分钟数\", DefaultValue = 30, MinValue = 0)]\n        public int MinutesBefore { get; set; }\n\n        [Parameter(\"新闻：公布后分钟数\", DefaultValue = 30, MinValue = 0)]\n        public int MinutesAfter { get; set; }\n\n        private const string CalendarWs = \"ws:\u002F\u002Fshowmytrades.com:25345\u002F\";\n        private const string CalendarHttp = \"https:\u002F\u002Fshowmytrades.com\u002Fapi\u002Fpublic\u002Fcalendar\u002Fv1?\";\n        private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n\n        private readonly List\u003CKeyValuePair\u003CDateTime, string>> _events = new List\u003CKeyValuePair\u003CDateTime, string>>();\n        private WebSocketClient _ws;\n        private bool _received;\n\n        protected override void OnStart()\n        {\n            DownloadCalendar();\n            Timer.Start(TimeSpan.FromMinutes(15));\n        }\n\n        protected override void OnTimer()\n        {\n            DownloadCalendar();\n        }\n\n        protected override void OnTick()\n        {\n            string reason;\n            if (IsNewsTime(out reason))\n            {\n                \u002F\u002F 新闻窗口内不开新仓。持仓按您自己的方式管理。\n                return;\n            }\n\n            \u002F\u002F ... 您的入场逻辑 ...\n        }\n\n        protected override void OnStop()\n        {\n            CloseSocket();\n        }\n\n        \u002F\u002F 有数据公布处于暂停窗口内时返回 true；reason 说明是哪一条。\n        \u002F\u002F Server.TimeInUtc 是 cTrader 自己的 UTC 时钟：平台知道经纪商的时区偏移，\n        \u002F\u002F 因此可以放心与日历比较（ParseCsv 会用服务器时间核对它）。\n        public bool IsNewsTime(out string reason)\n        {\n            DateTime now = Server.TimeInUtc;\n            foreach (KeyValuePair\u003CDateTime, string> e in _events)\n            {\n                if (now >= e.Key.AddMinutes(-MinutesBefore) && now \u003C= e.Key.AddMinutes(MinutesAfter))\n                {\n                    reason = e.Value;\n                    return true;\n                }\n            }\n            reason = null;\n            return false;\n        }\n\n        private string Query()\n        {\n            string q = \"format=csv&days=2&impact=\" + NewsImpact;\n            if (!string.IsNullOrWhiteSpace(NewsCurrencies)) q += \"&currency=\" + NewsCurrencies.Replace(\" \", \"\");\n            return q;\n        }\n\n        private void DownloadCalendar()\n        {\n            if (RunningMode != RunningMode.RealTime) return; \u002F\u002F 回测器中没有网络\n\n            \u002F\u002F cTrader Cloud 禁止 cBot 发出 HTTP 请求，只允许端口 25345 上的 WebSocket，\n            \u002F\u002F 所以各处都以 socket 为主通道；桌面端以 HTTP 作为备用。\n            try\n            {\n                CloseSocket();\n                _received = false;\n                _ws = new WebSocketClient(new WebSocketClientOptions { Timeout = TimeSpan.FromSeconds(15) });\n                _ws.Connected += OnSocketConnected;\n                _ws.TextReceived += OnSocketText;\n                _ws.Disconnected += OnSocketDisconnected;\n                _ws.Connect(new Uri(CalendarWs));\n            }\n            catch (Exception ex)\n            {\n                Print(\"[SMT] 日历 socket 连接失败：{0}\", ex.Message);\n                DownloadOverHttp();\n            }\n        }\n\n        private void OnSocketConnected(WebSocketClientConnectedEventArgs args)\n        {\n            try { _ws.Send(Query()); }\n            catch (Exception ex) { Print(\"[SMT] 日历请求发送失败：{0}\", ex.Message); }\n        }\n\n        private void OnSocketText(WebSocketClientTextReceivedEventArgs args)\n        {\n            _received = true;\n            if (args.Text.StartsWith(\"Error:\")) Print(\"[SMT] 日历：{0}\", args.Text);\n            else ParseCsv(args.Text);\n            CloseSocket();\n        }\n\n        private void OnSocketDisconnected(WebSocketClientDisconnectEventArgs args)\n        {\n            if (!_received) DownloadOverHttp(); \u002F\u002F 仅限桌面端：云端会返回 503\n        }\n\n        private void DownloadOverHttp()\n        {\n            try\n            {\n                HttpResponse r = Http.Get(CalendarHttp + Query());\n                if (r.IsSuccessful) ParseCsv(r.Body);\n                else Print(\"[SMT] 日历 HTTP 请求失败：{0}\", r.StatusCode);\n            }\n            catch (Exception ex) { Print(\"[SMT] 日历 HTTP 请求失败：{0}\", ex.Message); }\n        }\n\n        private void CloseSocket()\n        {\n            WebSocketClient ws = _ws;\n            _ws = null;\n            if (ws == null) return;\n            try { ws.Connected -= OnSocketConnected; ws.TextReceived -= OnSocketText; ws.Disconnected -= OnSocketDisconnected; } catch (Exception) { }\n            try { if (ws.State == WebSocketClientState.Open) ws.Close(WebSocketClientCloseStatus.NormalClosure, \"done\"); } catch (Exception) { }\n            try { ws.Dispose(); } catch (Exception) { }\n        }\n\n        \u002F\u002F CSV：ts,time,currency,impact,forecast,previous,actual,title（title 在最后，可能带引号），\n        \u002F\u002F 最后一行是 \"# server_time=\u003Cunix> server_time_iso=\u003Ciso>\"，即服务器的 UTC 时钟。\n        private void ParseCsv(string csv)\n        {\n            List\u003CKeyValuePair\u003CDateTime, string>> list = new List\u003CKeyValuePair\u003CDateTime, string>>();\n            foreach (string raw in csv.Split('\\n'))\n            {\n                string line = raw.TrimEnd('\\r');\n                if (line.Length == 0 || line.StartsWith(\"ts,\")) continue;\n                if (line.StartsWith(\"# server_time=\"))\n                {\n                    long serverTime;\n                    if (long.TryParse(line.Substring(14, 10), out serverTime))\n                    {\n                        double skew = (Server.TimeInUtc - Epoch.AddSeconds(serverTime)).TotalSeconds;\n                        if (Math.Abs(skew) > 60) Print(\"[SMT] 警告：cTrader 的 UTC 时钟与日历服务器相差 {0:F0} 秒\", skew);\n                    }\n                    continue;\n                }\n                string[] f = line.Split(',');\n                if (f.Length \u003C 8) continue;\n                long ts;\n                if (!long.TryParse(f[0], out ts)) continue;\n                DateTime at = Epoch.AddSeconds(ts);\n                string title = AfterComma(line, 7).Replace(\"\\\"\\\"\", \"\\\"\");\n                if (title.Length >= 2 && title[0] == '\"') title = title.Substring(1, title.Length - 2);\n                list.Add(new KeyValuePair\u003CDateTime, string>(at, f[2] + \" \" + f[3] + \": \" + title));\n            }\n            _events.Clear();\n            _events.AddRange(list);\n            Print(\"[SMT] 日历：已加载 {0} 个事件\", _events.Count);\n        }\n\n        private static string AfterComma(string line, int n)\n        {\n            int pos = -1;\n            for (int i = 0; i \u003C n; i++)\n            {\n                pos = line.IndexOf(',', pos + 1);\n                if (pos \u003C 0) return \"\";\n            }\n            return line.Substring(pos + 1);\n        }\n    }\n}\n","SmtNewsFilterBot.cs","csharp",[31,180,181,189,195,201,207,213,220,226,232,238,244,250,256,262,267,273,279,284,290,296,301,307,313,318,324,330,336,341,347,353,359,364,370,376,382,388,394,399,405,410,415,420,425,431,436,442,448,454,460,466,472,477,483,488,493,499,504,510,515,520,526,532,538,544,549,555,561,566,572,578,584,590,596,601,607,613,618,623,629,634,640,646,652,657,662,668,673,679,684,690,696,702,707,713,719,725,731,737,743,749,754,760,765,771,777,782,787,792,798,803,809,815,820,825,831,836,842,848,854,859,864,869,875,880,886,891,896,902,907,912,917,923,929,935,940,946,951,956,962,967,973,979,985,991,997,1003,1008,1013,1019,1025,1031,1036,1042,1048,1053,1059,1065,1071,1076,1082,1088,1094,1100,1106,1112,1118,1123,1129,1135,1141,1147,1153,1159,1165,1171,1176,1182,1188,1194,1199,1204,1210,1215,1221,1227,1232,1238,1244,1249,1255,1260,1266],{"__ignoreMap":66},[182,183,186],"span",{"class":184,"line":185},"line",1,[182,187,188],{},"\u002F\u002F cTrader 新闻过滤器（桌面端和云端），数据来自 ShowMyTrades，免费，无需密钥。\n",[182,190,192],{"class":184,"line":191},2,[182,193,194],{},"\u002F\u002F 文档：https:\u002F\u002Fshowmytrades.com\u002Fdocs\u002Fapi\u002Feconomic-calendar\n",[182,196,198],{"class":184,"line":197},3,[182,199,200],{},"using System;\n",[182,202,204],{"class":184,"line":203},4,[182,205,206],{},"using System.Collections.Generic;\n",[182,208,210],{"class":184,"line":209},5,[182,211,212],{},"using cAlgo.API;\n",[182,214,216],{"class":184,"line":215},6,[182,217,219],{"emptyLinePlaceholder":218},true,"\n",[182,221,223],{"class":184,"line":222},7,[182,224,225],{},"namespace cAlgo.Robots\n",[182,227,229],{"class":184,"line":228},8,[182,230,231],{},"{\n",[182,233,235],{"class":184,"line":234},9,[182,236,237],{},"    [Robot(AccessRights = AccessRights.None, TimeZone = TimeZones.UTC)]\n",[182,239,241],{"class":184,"line":240},10,[182,242,243],{},"    public class SmtNewsFilterBot : Robot\n",[182,245,247],{"class":184,"line":246},11,[182,248,249],{},"    {\n",[182,251,253],{"class":184,"line":252},12,[182,254,255],{},"        [Parameter(\"新闻：货币（逗号分隔，留空 = 全部）\", DefaultValue = \"EUR,USD\")]\n",[182,257,259],{"class":184,"line":258},13,[182,260,261],{},"        public string NewsCurrencies { get; set; }\n",[182,263,265],{"class":184,"line":264},14,[182,266,219],{"emptyLinePlaceholder":218},[182,268,270],{"class":184,"line":269},15,[182,271,272],{},"        [Parameter(\"新闻：影响级别（high | high,medium | all）\", DefaultValue = \"high\")]\n",[182,274,276],{"class":184,"line":275},16,[182,277,278],{},"        public string NewsImpact { get; set; }\n",[182,280,282],{"class":184,"line":281},17,[182,283,219],{"emptyLinePlaceholder":218},[182,285,287],{"class":184,"line":286},18,[182,288,289],{},"        [Parameter(\"新闻：公布前分钟数\", DefaultValue = 30, MinValue = 0)]\n",[182,291,293],{"class":184,"line":292},19,[182,294,295],{},"        public int MinutesBefore { get; set; }\n",[182,297,299],{"class":184,"line":298},20,[182,300,219],{"emptyLinePlaceholder":218},[182,302,304],{"class":184,"line":303},21,[182,305,306],{},"        [Parameter(\"新闻：公布后分钟数\", DefaultValue = 30, MinValue = 0)]\n",[182,308,310],{"class":184,"line":309},22,[182,311,312],{},"        public int MinutesAfter { get; set; }\n",[182,314,316],{"class":184,"line":315},23,[182,317,219],{"emptyLinePlaceholder":218},[182,319,321],{"class":184,"line":320},24,[182,322,323],{},"        private const string CalendarWs = \"ws:\u002F\u002Fshowmytrades.com:25345\u002F\";\n",[182,325,327],{"class":184,"line":326},25,[182,328,329],{},"        private const string CalendarHttp = \"https:\u002F\u002Fshowmytrades.com\u002Fapi\u002Fpublic\u002Fcalendar\u002Fv1?\";\n",[182,331,333],{"class":184,"line":332},26,[182,334,335],{},"        private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n",[182,337,339],{"class":184,"line":338},27,[182,340,219],{"emptyLinePlaceholder":218},[182,342,344],{"class":184,"line":343},28,[182,345,346],{},"        private readonly List\u003CKeyValuePair\u003CDateTime, string>> _events = new List\u003CKeyValuePair\u003CDateTime, string>>();\n",[182,348,350],{"class":184,"line":349},29,[182,351,352],{},"        private WebSocketClient _ws;\n",[182,354,356],{"class":184,"line":355},30,[182,357,358],{},"        private bool _received;\n",[182,360,362],{"class":184,"line":361},31,[182,363,219],{"emptyLinePlaceholder":218},[182,365,367],{"class":184,"line":366},32,[182,368,369],{},"        protected override void OnStart()\n",[182,371,373],{"class":184,"line":372},33,[182,374,375],{},"        {\n",[182,377,379],{"class":184,"line":378},34,[182,380,381],{},"            DownloadCalendar();\n",[182,383,385],{"class":184,"line":384},35,[182,386,387],{},"            Timer.Start(TimeSpan.FromMinutes(15));\n",[182,389,391],{"class":184,"line":390},36,[182,392,393],{},"        }\n",[182,395,397],{"class":184,"line":396},37,[182,398,219],{"emptyLinePlaceholder":218},[182,400,402],{"class":184,"line":401},38,[182,403,404],{},"        protected override void OnTimer()\n",[182,406,408],{"class":184,"line":407},39,[182,409,375],{},[182,411,413],{"class":184,"line":412},40,[182,414,381],{},[182,416,418],{"class":184,"line":417},41,[182,419,393],{},[182,421,423],{"class":184,"line":422},42,[182,424,219],{"emptyLinePlaceholder":218},[182,426,428],{"class":184,"line":427},43,[182,429,430],{},"        protected override void OnTick()\n",[182,432,434],{"class":184,"line":433},44,[182,435,375],{},[182,437,439],{"class":184,"line":438},45,[182,440,441],{},"            string reason;\n",[182,443,445],{"class":184,"line":444},46,[182,446,447],{},"            if (IsNewsTime(out reason))\n",[182,449,451],{"class":184,"line":450},47,[182,452,453],{},"            {\n",[182,455,457],{"class":184,"line":456},48,[182,458,459],{},"                \u002F\u002F 新闻窗口内不开新仓。持仓按您自己的方式管理。\n",[182,461,463],{"class":184,"line":462},49,[182,464,465],{},"                return;\n",[182,467,469],{"class":184,"line":468},50,[182,470,471],{},"            }\n",[182,473,475],{"class":184,"line":474},51,[182,476,219],{"emptyLinePlaceholder":218},[182,478,480],{"class":184,"line":479},52,[182,481,482],{},"            \u002F\u002F ... 您的入场逻辑 ...\n",[182,484,486],{"class":184,"line":485},53,[182,487,393],{},[182,489,491],{"class":184,"line":490},54,[182,492,219],{"emptyLinePlaceholder":218},[182,494,496],{"class":184,"line":495},55,[182,497,498],{},"        protected override void OnStop()\n",[182,500,502],{"class":184,"line":501},56,[182,503,375],{},[182,505,507],{"class":184,"line":506},57,[182,508,509],{},"            CloseSocket();\n",[182,511,513],{"class":184,"line":512},58,[182,514,393],{},[182,516,518],{"class":184,"line":517},59,[182,519,219],{"emptyLinePlaceholder":218},[182,521,523],{"class":184,"line":522},60,[182,524,525],{},"        \u002F\u002F 有数据公布处于暂停窗口内时返回 true；reason 说明是哪一条。\n",[182,527,529],{"class":184,"line":528},61,[182,530,531],{},"        \u002F\u002F Server.TimeInUtc 是 cTrader 自己的 UTC 时钟：平台知道经纪商的时区偏移，\n",[182,533,535],{"class":184,"line":534},62,[182,536,537],{},"        \u002F\u002F 因此可以放心与日历比较（ParseCsv 会用服务器时间核对它）。\n",[182,539,541],{"class":184,"line":540},63,[182,542,543],{},"        public bool IsNewsTime(out string reason)\n",[182,545,547],{"class":184,"line":546},64,[182,548,375],{},[182,550,552],{"class":184,"line":551},65,[182,553,554],{},"            DateTime now = Server.TimeInUtc;\n",[182,556,558],{"class":184,"line":557},66,[182,559,560],{},"            foreach (KeyValuePair\u003CDateTime, string> e in _events)\n",[182,562,564],{"class":184,"line":563},67,[182,565,453],{},[182,567,569],{"class":184,"line":568},68,[182,570,571],{},"                if (now >= e.Key.AddMinutes(-MinutesBefore) && now \u003C= e.Key.AddMinutes(MinutesAfter))\n",[182,573,575],{"class":184,"line":574},69,[182,576,577],{},"                {\n",[182,579,581],{"class":184,"line":580},70,[182,582,583],{},"                    reason = e.Value;\n",[182,585,587],{"class":184,"line":586},71,[182,588,589],{},"                    return true;\n",[182,591,593],{"class":184,"line":592},72,[182,594,595],{},"                }\n",[182,597,599],{"class":184,"line":598},73,[182,600,471],{},[182,602,604],{"class":184,"line":603},74,[182,605,606],{},"            reason = null;\n",[182,608,610],{"class":184,"line":609},75,[182,611,612],{},"            return false;\n",[182,614,616],{"class":184,"line":615},76,[182,617,393],{},[182,619,621],{"class":184,"line":620},77,[182,622,219],{"emptyLinePlaceholder":218},[182,624,626],{"class":184,"line":625},78,[182,627,628],{},"        private string Query()\n",[182,630,632],{"class":184,"line":631},79,[182,633,375],{},[182,635,637],{"class":184,"line":636},80,[182,638,639],{},"            string q = \"format=csv&days=2&impact=\" + NewsImpact;\n",[182,641,643],{"class":184,"line":642},81,[182,644,645],{},"            if (!string.IsNullOrWhiteSpace(NewsCurrencies)) q += \"&currency=\" + NewsCurrencies.Replace(\" \", \"\");\n",[182,647,649],{"class":184,"line":648},82,[182,650,651],{},"            return q;\n",[182,653,655],{"class":184,"line":654},83,[182,656,393],{},[182,658,660],{"class":184,"line":659},84,[182,661,219],{"emptyLinePlaceholder":218},[182,663,665],{"class":184,"line":664},85,[182,666,667],{},"        private void DownloadCalendar()\n",[182,669,671],{"class":184,"line":670},86,[182,672,375],{},[182,674,676],{"class":184,"line":675},87,[182,677,678],{},"            if (RunningMode != RunningMode.RealTime) return; \u002F\u002F 回测器中没有网络\n",[182,680,682],{"class":184,"line":681},88,[182,683,219],{"emptyLinePlaceholder":218},[182,685,687],{"class":184,"line":686},89,[182,688,689],{},"            \u002F\u002F cTrader Cloud 禁止 cBot 发出 HTTP 请求，只允许端口 25345 上的 WebSocket，\n",[182,691,693],{"class":184,"line":692},90,[182,694,695],{},"            \u002F\u002F 所以各处都以 socket 为主通道；桌面端以 HTTP 作为备用。\n",[182,697,699],{"class":184,"line":698},91,[182,700,701],{},"            try\n",[182,703,705],{"class":184,"line":704},92,[182,706,453],{},[182,708,710],{"class":184,"line":709},93,[182,711,712],{},"                CloseSocket();\n",[182,714,716],{"class":184,"line":715},94,[182,717,718],{},"                _received = false;\n",[182,720,722],{"class":184,"line":721},95,[182,723,724],{},"                _ws = new WebSocketClient(new WebSocketClientOptions { Timeout = TimeSpan.FromSeconds(15) });\n",[182,726,728],{"class":184,"line":727},96,[182,729,730],{},"                _ws.Connected += OnSocketConnected;\n",[182,732,734],{"class":184,"line":733},97,[182,735,736],{},"                _ws.TextReceived += OnSocketText;\n",[182,738,740],{"class":184,"line":739},98,[182,741,742],{},"                _ws.Disconnected += OnSocketDisconnected;\n",[182,744,746],{"class":184,"line":745},99,[182,747,748],{},"                _ws.Connect(new Uri(CalendarWs));\n",[182,750,752],{"class":184,"line":751},100,[182,753,471],{},[182,755,757],{"class":184,"line":756},101,[182,758,759],{},"            catch (Exception ex)\n",[182,761,763],{"class":184,"line":762},102,[182,764,453],{},[182,766,768],{"class":184,"line":767},103,[182,769,770],{},"                Print(\"[SMT] 日历 socket 连接失败：{0}\", ex.Message);\n",[182,772,774],{"class":184,"line":773},104,[182,775,776],{},"                DownloadOverHttp();\n",[182,778,780],{"class":184,"line":779},105,[182,781,471],{},[182,783,785],{"class":184,"line":784},106,[182,786,393],{},[182,788,790],{"class":184,"line":789},107,[182,791,219],{"emptyLinePlaceholder":218},[182,793,795],{"class":184,"line":794},108,[182,796,797],{},"        private void OnSocketConnected(WebSocketClientConnectedEventArgs args)\n",[182,799,801],{"class":184,"line":800},109,[182,802,375],{},[182,804,806],{"class":184,"line":805},110,[182,807,808],{},"            try { _ws.Send(Query()); }\n",[182,810,812],{"class":184,"line":811},111,[182,813,814],{},"            catch (Exception ex) { Print(\"[SMT] 日历请求发送失败：{0}\", ex.Message); }\n",[182,816,818],{"class":184,"line":817},112,[182,819,393],{},[182,821,823],{"class":184,"line":822},113,[182,824,219],{"emptyLinePlaceholder":218},[182,826,828],{"class":184,"line":827},114,[182,829,830],{},"        private void OnSocketText(WebSocketClientTextReceivedEventArgs args)\n",[182,832,834],{"class":184,"line":833},115,[182,835,375],{},[182,837,839],{"class":184,"line":838},116,[182,840,841],{},"            _received = true;\n",[182,843,845],{"class":184,"line":844},117,[182,846,847],{},"            if (args.Text.StartsWith(\"Error:\")) Print(\"[SMT] 日历：{0}\", args.Text);\n",[182,849,851],{"class":184,"line":850},118,[182,852,853],{},"            else ParseCsv(args.Text);\n",[182,855,857],{"class":184,"line":856},119,[182,858,509],{},[182,860,862],{"class":184,"line":861},120,[182,863,393],{},[182,865,867],{"class":184,"line":866},121,[182,868,219],{"emptyLinePlaceholder":218},[182,870,872],{"class":184,"line":871},122,[182,873,874],{},"        private void OnSocketDisconnected(WebSocketClientDisconnectEventArgs args)\n",[182,876,878],{"class":184,"line":877},123,[182,879,375],{},[182,881,883],{"class":184,"line":882},124,[182,884,885],{},"            if (!_received) DownloadOverHttp(); \u002F\u002F 仅限桌面端：云端会返回 503\n",[182,887,889],{"class":184,"line":888},125,[182,890,393],{},[182,892,894],{"class":184,"line":893},126,[182,895,219],{"emptyLinePlaceholder":218},[182,897,899],{"class":184,"line":898},127,[182,900,901],{},"        private void DownloadOverHttp()\n",[182,903,905],{"class":184,"line":904},128,[182,906,375],{},[182,908,910],{"class":184,"line":909},129,[182,911,701],{},[182,913,915],{"class":184,"line":914},130,[182,916,453],{},[182,918,920],{"class":184,"line":919},131,[182,921,922],{},"                HttpResponse r = Http.Get(CalendarHttp + Query());\n",[182,924,926],{"class":184,"line":925},132,[182,927,928],{},"                if (r.IsSuccessful) ParseCsv(r.Body);\n",[182,930,932],{"class":184,"line":931},133,[182,933,934],{},"                else Print(\"[SMT] 日历 HTTP 请求失败：{0}\", r.StatusCode);\n",[182,936,938],{"class":184,"line":937},134,[182,939,471],{},[182,941,943],{"class":184,"line":942},135,[182,944,945],{},"            catch (Exception ex) { Print(\"[SMT] 日历 HTTP 请求失败：{0}\", ex.Message); }\n",[182,947,949],{"class":184,"line":948},136,[182,950,393],{},[182,952,954],{"class":184,"line":953},137,[182,955,219],{"emptyLinePlaceholder":218},[182,957,959],{"class":184,"line":958},138,[182,960,961],{},"        private void CloseSocket()\n",[182,963,965],{"class":184,"line":964},139,[182,966,375],{},[182,968,970],{"class":184,"line":969},140,[182,971,972],{},"            WebSocketClient ws = _ws;\n",[182,974,976],{"class":184,"line":975},141,[182,977,978],{},"            _ws = null;\n",[182,980,982],{"class":184,"line":981},142,[182,983,984],{},"            if (ws == null) return;\n",[182,986,988],{"class":184,"line":987},143,[182,989,990],{},"            try { ws.Connected -= OnSocketConnected; ws.TextReceived -= OnSocketText; ws.Disconnected -= OnSocketDisconnected; } catch (Exception) { }\n",[182,992,994],{"class":184,"line":993},144,[182,995,996],{},"            try { if (ws.State == WebSocketClientState.Open) ws.Close(WebSocketClientCloseStatus.NormalClosure, \"done\"); } catch (Exception) { }\n",[182,998,1000],{"class":184,"line":999},145,[182,1001,1002],{},"            try { ws.Dispose(); } catch (Exception) { }\n",[182,1004,1006],{"class":184,"line":1005},146,[182,1007,393],{},[182,1009,1011],{"class":184,"line":1010},147,[182,1012,219],{"emptyLinePlaceholder":218},[182,1014,1016],{"class":184,"line":1015},148,[182,1017,1018],{},"        \u002F\u002F CSV：ts,time,currency,impact,forecast,previous,actual,title（title 在最后，可能带引号），\n",[182,1020,1022],{"class":184,"line":1021},149,[182,1023,1024],{},"        \u002F\u002F 最后一行是 \"# server_time=\u003Cunix> server_time_iso=\u003Ciso>\"，即服务器的 UTC 时钟。\n",[182,1026,1028],{"class":184,"line":1027},150,[182,1029,1030],{},"        private void ParseCsv(string csv)\n",[182,1032,1034],{"class":184,"line":1033},151,[182,1035,375],{},[182,1037,1039],{"class":184,"line":1038},152,[182,1040,1041],{},"            List\u003CKeyValuePair\u003CDateTime, string>> list = new List\u003CKeyValuePair\u003CDateTime, string>>();\n",[182,1043,1045],{"class":184,"line":1044},153,[182,1046,1047],{},"            foreach (string raw in csv.Split('\\n'))\n",[182,1049,1051],{"class":184,"line":1050},154,[182,1052,453],{},[182,1054,1056],{"class":184,"line":1055},155,[182,1057,1058],{},"                string line = raw.TrimEnd('\\r');\n",[182,1060,1062],{"class":184,"line":1061},156,[182,1063,1064],{},"                if (line.Length == 0 || line.StartsWith(\"ts,\")) continue;\n",[182,1066,1068],{"class":184,"line":1067},157,[182,1069,1070],{},"                if (line.StartsWith(\"# server_time=\"))\n",[182,1072,1074],{"class":184,"line":1073},158,[182,1075,577],{},[182,1077,1079],{"class":184,"line":1078},159,[182,1080,1081],{},"                    long serverTime;\n",[182,1083,1085],{"class":184,"line":1084},160,[182,1086,1087],{},"                    if (long.TryParse(line.Substring(14, 10), out serverTime))\n",[182,1089,1091],{"class":184,"line":1090},161,[182,1092,1093],{},"                    {\n",[182,1095,1097],{"class":184,"line":1096},162,[182,1098,1099],{},"                        double skew = (Server.TimeInUtc - Epoch.AddSeconds(serverTime)).TotalSeconds;\n",[182,1101,1103],{"class":184,"line":1102},163,[182,1104,1105],{},"                        if (Math.Abs(skew) > 60) Print(\"[SMT] 警告：cTrader 的 UTC 时钟与日历服务器相差 {0:F0} 秒\", skew);\n",[182,1107,1109],{"class":184,"line":1108},164,[182,1110,1111],{},"                    }\n",[182,1113,1115],{"class":184,"line":1114},165,[182,1116,1117],{},"                    continue;\n",[182,1119,1121],{"class":184,"line":1120},166,[182,1122,595],{},[182,1124,1126],{"class":184,"line":1125},167,[182,1127,1128],{},"                string[] f = line.Split(',');\n",[182,1130,1132],{"class":184,"line":1131},168,[182,1133,1134],{},"                if (f.Length \u003C 8) continue;\n",[182,1136,1138],{"class":184,"line":1137},169,[182,1139,1140],{},"                long ts;\n",[182,1142,1144],{"class":184,"line":1143},170,[182,1145,1146],{},"                if (!long.TryParse(f[0], out ts)) continue;\n",[182,1148,1150],{"class":184,"line":1149},171,[182,1151,1152],{},"                DateTime at = Epoch.AddSeconds(ts);\n",[182,1154,1156],{"class":184,"line":1155},172,[182,1157,1158],{},"                string title = AfterComma(line, 7).Replace(\"\\\"\\\"\", \"\\\"\");\n",[182,1160,1162],{"class":184,"line":1161},173,[182,1163,1164],{},"                if (title.Length >= 2 && title[0] == '\"') title = title.Substring(1, title.Length - 2);\n",[182,1166,1168],{"class":184,"line":1167},174,[182,1169,1170],{},"                list.Add(new KeyValuePair\u003CDateTime, string>(at, f[2] + \" \" + f[3] + \": \" + title));\n",[182,1172,1174],{"class":184,"line":1173},175,[182,1175,471],{},[182,1177,1179],{"class":184,"line":1178},176,[182,1180,1181],{},"            _events.Clear();\n",[182,1183,1185],{"class":184,"line":1184},177,[182,1186,1187],{},"            _events.AddRange(list);\n",[182,1189,1191],{"class":184,"line":1190},178,[182,1192,1193],{},"            Print(\"[SMT] 日历：已加载 {0} 个事件\", _events.Count);\n",[182,1195,1197],{"class":184,"line":1196},179,[182,1198,393],{},[182,1200,1202],{"class":184,"line":1201},180,[182,1203,219],{"emptyLinePlaceholder":218},[182,1205,1207],{"class":184,"line":1206},181,[182,1208,1209],{},"        private static string AfterComma(string line, int n)\n",[182,1211,1213],{"class":184,"line":1212},182,[182,1214,375],{},[182,1216,1218],{"class":184,"line":1217},183,[182,1219,1220],{},"            int pos = -1;\n",[182,1222,1224],{"class":184,"line":1223},184,[182,1225,1226],{},"            for (int i = 0; i \u003C n; i++)\n",[182,1228,1230],{"class":184,"line":1229},185,[182,1231,453],{},[182,1233,1235],{"class":184,"line":1234},186,[182,1236,1237],{},"                pos = line.IndexOf(',', pos + 1);\n",[182,1239,1241],{"class":184,"line":1240},187,[182,1242,1243],{},"                if (pos \u003C 0) return \"\";\n",[182,1245,1247],{"class":184,"line":1246},188,[182,1248,471],{},[182,1250,1252],{"class":184,"line":1251},189,[182,1253,1254],{},"            return line.Substring(pos + 1);\n",[182,1256,1258],{"class":184,"line":1257},190,[182,1259,393],{},[182,1261,1263],{"class":184,"line":1262},191,[182,1264,1265],{},"    }\n",[182,1267,1269],{"class":184,"line":1268},192,[182,1270,1271],{},"}\n",[18,1273,1274,1275,1278,1279,1282,1283,1286,1287,1290,1291,38],{},"想把过滤器加进现有的 cBot：把 ",[31,1276,1277],{},"CalendarWs"," 到 ",[31,1280,1281],{},"AfterComma()"," 之间的代码全部复制过去，在 ",[31,1284,1285],{},"OnStart()"," 和一个 15 分钟的定时器里调用 ",[31,1288,1289],{},"DownloadCalendar()","，每次入场前检查 ",[31,1292,1293],{},"IsNewsTime()",[53,1295,1297],{"id":1296},"试运行","🧪 试运行",[1299,1300,1301,1309,1325],"ol",{},[1302,1303,1304,1305,1308],"li",{},"构建 cBot，在图表上添加一个实例。几秒内日志里应出现 ",[31,1306,1307],{},"[SMT] 日历：已加载 N 个事件","。清淡的周末显示为零，属于正常。",[1302,1310,1311,1312,1316,1317,1320,1321,1324],{},"把",[1313,1314,1315],"em",{},"新闻：影响级别","设为 ",[31,1318,1319],{},"all","，",[1313,1322,1323],{},"新闻：公布前分钟数","设成一个很大的数，看到过滤器生效后，再改回实际要用的数值。",[1302,1326,1327,1328,1331],{},"把同一个实例放到",[22,1329,1330],{},"云端","运行，日志里还是那一行。用 socket 图的就是这个。",[53,1333,1335],{"id":1334},"️-时间与回测器","⏱️ 时间与回测器",[18,1337,1338,1339,1342,1343,1346,1347,1349,1350,1352],{},"日历的时间戳都是 UTC，代码拿它和 ",[31,1340,1341],{},"Server.TimeInUtc"," 比较，经纪商的时区和夏令时根本不参与。每次下载的末尾还附有服务器自己的 UTC 时钟（",[31,1344,1345],{},"# server_time=…","）：cBot 会拿它核对 ",[31,1348,1341],{},"，两者相差超过一分钟就打印警告，时钟出了错不会没人发现。回测器和优化器不能联网：",[31,1351,1289],{}," 在那里直接返回，过滤器不起作用。要回测新闻过滤器，得给 cBot 附带一份历史事件文件，那是另一项工作。",[53,1354,1356],{"id":1355},"️-调整","🎛️ 调整",[1358,1359,1360,1369,1380],"ul",{},[1302,1361,1362,1365,1366,1368],{},[22,1363,1364],{},"持仓","不受影响。要怎么处理，可以用 ",[31,1367,1293],{}," 来判断：平仓、追踪止损，或者放着不管。",[1302,1370,1371,1372,1375,1376,1379],{},"加上",[22,1373,1374],{},"中影响","（",[31,1377,1378],{},"high,medium","），事件数量会变成三倍。剥头皮往往用得上，波段系统很少需要。",[1302,1381,1382,1385],{},[22,1383,1384],{},"同一账户跑多个 cBot"," 时，各自单独下载：十个 cBot 每小时合计 40 个小请求，远低于每个 IP 地址每分钟 60 条 socket 消息的上限。",[53,1387,1389],{"id":1388},"常见问题","❓ 常见问题",[18,1391,1392,1395],{},[22,1393,1394],{},"为什么用 WebSocket 而不是 HTTPS？","\n因为 cTrader Cloud 不让 cBot 发 HTTP 请求，WebSocket 连接也只放行端口 25345。在桌面端，同一个 cBot 走 HTTPS 也行，socket 连不上时代码会自动改用 HTTPS。",[18,1397,1398,1404,1405,1408],{},[22,1399,1400,1401,1403],{},"用不加密的 ",[31,1402,156],{}," 发送安全吗？","\n请求里只有四个参数（货币、影响级别、天数、格式），不含您本人或账户的任何信息；返回的是公开的财经日历。没有什么需要保护的。同样的数据也能通过 HTTPS 从 ",[47,1406,1407],{"href":49},"API 端点","获取。",[18,1410,1411,1418,1419,1422,1423,1426,1427,1429],{},[22,1412,1413,1414,1417],{},"需要 ",[31,1415,1416],{},"AccessRights.FullAccess"," 吗？","\n不需要。",[31,1420,1421],{},"WebSocketClient"," 和 ",[31,1424,1425],{},"Http"," 在 ",[31,1428,133],{}," 下就能使用，这也是 cTrader Store 的要求。",[18,1431,1432,1435,1436,1440],{},[22,1433,1434],{},"需要 ShowMyTrades 账户吗？","\n不需要。如果还想公开这个 cBot 的交易结果，",[47,1437,1439],{"href":1438},"\u002Fguides\u002Fshow-ctrader-results-on-your-website","连接 cTrader 账户","是另一回事，只需一次 OAuth 授权。",[53,1442,1444],{"id":1443},"相关内容","🔗 相关内容",[1358,1446,1447,1453,1460,1465],{},[1302,1448,1449,1452],{},[47,1450,1451],{"href":49},"财经日历 API 参考","：WebSocket 协议、HTTP 端点、JSON 和 CSV 格式",[1302,1454,1455,1459],{},[47,1456,1458],{"href":1457},"\u002Fguides\u002Fadd-a-news-filter-to-your-mt4-expert-advisor","MT4 EA（智能交易系统）的新闻过滤器","：用 MQL 实现的同一个过滤器",[1302,1461,1462],{},[47,1463,1464],{"href":1438},"在您的网站上展示 cTrader 交易结果",[1302,1466,1467],{},[47,1468,1470],{"href":1469},"\u002Fglossary\u002Fwhat-is-ctrader","什么是 cTrader",[1472,1473],"hr",{},[18,1475,1476,1477,1481],{},"唯一特别的只有 socket 这部分，而它已经替您写好了。剩下的还是您的策略，只不过在新闻前后少了一种亏钱的方式。",[47,1478,1480],{"href":1479},"\u002Fsignup","免费追踪账户","，在回撤图表上看看差别。",[1483,1484,1485],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":66,"searchDepth":191,"depth":191,"links":1487},[1488,1489,1490,1491,1492,1493,1494],{"id":55,"depth":191,"text":56},{"id":164,"depth":191,"text":165},{"id":1296,"depth":191,"text":1297},{"id":1334,"depth":191,"text":1335},{"id":1355,"depth":191,"text":1356},{"id":1388,"depth":191,"text":1389},{"id":1443,"depth":191,"text":1444},"2026-10-17T00:00:00.000Z","cTrader 没有给 cBot 用的财经日历 API，cTrader Cloud 还会拦截 HTTP 请求。这里给出一个桌面端和云端都能用的新闻过滤器：通过 WebSocket 获取免费财经日历，附完整 C# 代码。",false,"md",{"src":1500},"\u002Fimages\u002Fblog\u002Fadd-a-news-filter-to-a-ctrader-cbot-hero.svg",{},"\u002Fzh\u002Fguides\u002Fadd-a-news-filter-to-a-ctrader-cbot",{"title":5,"description":1496},"zh\u002F3.guides\u002F43.add-a-news-filter-to-a-ctrader-cbot",null,"YvscEGnphApXNuDujLJJ3BHFkmoK0TtPnJg0UEFTYKc",[1508,1513],{"title":1509,"path":1510,"stem":1511,"description":1512,"children":-1},"MT5 新闻过滤器：用内置日历还是 API？","\u002Fzh\u002Fguides\u002Fnews-filter-mt5-expert-advisor-built-in-calendar-or-api","zh\u002F3.guides\u002F42.news-filter-mt5-expert-advisor-built-in-calendar-or-api","MetaTrader 5 自带财经日历，EA 无需下载就能读取。什么时候够用、什么时候不够，以及怎样改用免费的日历 API，两种方式都附 MQL5 代码。",{"title":1514,"path":1515,"stem":1516,"description":1517,"children":-1},"面向交易机器人的免费财经日历 API","\u002Fzh\u002Fguides\u002Ffree-economic-calendar-api-for-trading-bots","zh\u002F3.guides\u002F44.free-economic-calendar-api-for-trading-bots","交易机器人去哪里拿财经日历：常用的几种数据源、各自的坑，以及一个不用密钥的免费 API，专为 EA（智能交易系统）、cBot、Python 和 JavaScript 定时轮询而设计。",[1519,1521],{"path":1520},"\u002Fguides\u002Fadd-a-news-filter-to-a-ctrader-cbot",{"path":1502},1793898039487]