// Live account sync for cTrader — the cAlgo cBot equivalent of app/mt5_scripts/KBAlgoLabSync.mq5. // Attach to any chart. Reads this account's own trade history and open positions (read-only, // never touches credentials) and pushes them to your KB Algo Lab account report every 15s, // same JSON schema as the MT5 EA — no server-side changes needed. // // Verified working end-to-end 2026-07-24: built, ran live against a real demo account in // cTrader Desktop, confirmed "HTTP 200: OK" from https://kbalgolab.co.uk/api/account/sync // and the account showed up live on the account report page. // // SETUP IN CTRADER: // 1. cTrader > Algo > cBots > New > name it "KBAlgoLabSync" > Create. // 2. Replace the generated .cs with this file's contents. // 3. Edit config.json in the same folder (Documents\cAlgo\Sources\Robots\KBAlgoLabSync\KBAlgoLabSync\) // and set "AccessRights" to "FullAccess" — default "None" silently blocks the HTTP call. // Do NOT add/rename/remove entries in config.json's "Parameters" array beyond this — every // attempt to add a second parameter (even just one extra string) reproducibly broke the // build with "CT0002: Assembly must contain algo type" in this cAlgo version (5.9.5). Root // cause not fully pinned down; work around it by keeping the single "Message" parameter and // reusing it as shown below, rather than declaring more [Parameter] properties in code. // 4. Build. Set the "Message" parameter (this IS the email field — see note above) to the // email on your KB Algo Lab account, then start it on any chart. // // KNOWN GOTCHA: naming your own HttpClient field "Http" collides with a member the Algo base // class already exposes (cAlgo.API's Robot/Algo has a built-in Http-related member) and breaks // the build the same "CT0002" way — hence "HttpClientInstance" below, not "Http". using System.Net.Http; using System.Text; namespace cAlgo.Robots; public partial class KBAlgoLabSync : Robot { private static readonly HttpClient HttpClientInstance = new HttpClient(); protected override void OnStart() { if (string.IsNullOrEmpty(Message) || !Message.Contains("@")) Print("KBAlgoLabSync: set the email address on your KB Algo Lab account in the 'Message' parameter before running."); Timer.Start(15); SendOnce(); } protected override void OnTick() { } protected override void OnTimer() { SendOnce(); } protected override void OnStop() { } private static string JsonEsc(string s) { if (string.IsNullOrEmpty(s)) return ""; var sb = new StringBuilder(); foreach (var c in s) { switch (c) { case '\\': sb.Append("\\\\"); break; case '"': sb.Append("\\\""); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: if (c < 0x20) continue; sb.Append(c); break; } } return sb.ToString(); } private static double SafeNum(double v) => double.IsNaN(v) || double.IsInfinity(v) ? 0.0 : v; // Converts raw volume-in-units to lots using the symbol's own conversion when available, // falling back to the common 100,000-unit lot only if the symbol can't be looked up. private double UnitsToLots(string symbolName, double units) { try { var sym = Symbols.GetSymbol(symbolName); return sym != null ? sym.VolumeInUnitsToQuantity(units) : units / 100000.0; } catch { return units / 100000.0; } } private async void SendOnce() { if (string.IsNullOrEmpty(Message) || !Message.Contains("@")) return; try { var deals = new StringBuilder(); var sentDeals = 0; foreach (var trade in History) { var typeStr = trade.TradeType == TradeType.Buy ? "buy" : "sell"; var profit = SafeNum(trade.NetProfit); var lots = UnitsToLots(trade.SymbolName, trade.VolumeInUnits); if (sentDeals > 0) deals.Append(","); deals.Append("{\"time\":\"").Append(trade.ClosingTime.ToString("yyyy.MM.dd HH:mm:ss")) .Append("\",\"symbol\":\"").Append(JsonEsc(trade.SymbolName)) .Append("\",\"type\":\"").Append(typeStr) .Append("\",\"volume\":").Append(SafeNum(lots).ToString("F2")) .Append(",\"profit\":").Append(profit.ToString("F2")) .Append("}"); sentDeals++; } var positions = new StringBuilder(); var posCount = 0; foreach (var pos in Positions) { var typeStr = pos.TradeType == TradeType.Buy ? "buy" : "sell"; var lots = UnitsToLots(pos.SymbolName, pos.VolumeInUnits); if (posCount > 0) positions.Append(","); positions.Append("{\"symbol\":\"").Append(JsonEsc(pos.SymbolName)) .Append("\",\"type\":\"").Append(typeStr) .Append("\",\"volume\":").Append(SafeNum(lots).ToString("F2")) .Append(",\"openPrice\":").Append(SafeNum(pos.EntryPrice).ToString("F5")) .Append(",\"currentPrice\":").Append(SafeNum(pos.CurrentPrice).ToString("F5")) .Append(",\"profit\":").Append(SafeNum(pos.NetProfit).ToString("F2")) .Append(",\"openTime\":\"").Append(pos.EntryTime.ToString("yyyy.MM.dd HH:mm:ss")) .Append("\"}"); posCount++; } // NOTE: "Message" is the parameter cTrader shows in its Parameters panel — it holds // the account's KB Algo Lab email, not a literal message. See the setup note above // for why this wasn't renamed/split into its own dedicated parameter. var payload = "{\"email\":\"" + JsonEsc(Message) + "\"," + "\"accountLogin\":" + Account.Number + "," + "\"broker\":\"" + JsonEsc(Account.BrokerName) + "\"," + "\"server\":\"" + JsonEsc(Account.BrokerName) + "\"," + "\"currency\":\"" + JsonEsc(Account.Asset.Name) + "\"," + "\"leverage\":" + (int)Account.PreciseLeverage + "," + "\"balance\":" + SafeNum(Account.Balance).ToString("F2") + "," + "\"equity\":" + SafeNum(Account.Equity).ToString("F2") + "," + "\"isDemo\":" + (Account.IsLive ? "false" : "true") + "," + "\"deals\":[" + deals + "]," + "\"openPositions\":[" + positions + "]}"; var content = new StringContent(payload, Encoding.UTF8, "application/json"); var resp = await HttpClientInstance.PostAsync("https://kbalgolab.co.uk/api/account/sync", content); if (resp.IsSuccessStatusCode) Print("KBAlgoLabSync: synced {0} deals OK", sentDeals); else Print("KBAlgoLabSync: server replied {0}: {1}", (int)resp.StatusCode, await resp.Content.ReadAsStringAsync()); } catch (Exception ex) { Print("KBAlgoLabSync: sync failed: {0}", ex.Message); } } }