From 12b60a012c1f7f21b12f0f1de46f1d7c4b230699 Mon Sep 17 00:00:00 2001 From: biohazardxxx Date: Sat, 4 Jul 2026 00:04:37 +0200 Subject: [PATCH 1/7] MtApi5: add custom symbols command ids 340-350 Co-Authored-By: Claude Fable 5 --- MtApi5/MtProtocol/Mt5CommandType.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/MtApi5/MtProtocol/Mt5CommandType.cs b/MtApi5/MtProtocol/Mt5CommandType.cs index 4af91bdb..174d6823 100755 --- a/MtApi5/MtProtocol/Mt5CommandType.cs +++ b/MtApi5/MtProtocol/Mt5CommandType.cs @@ -260,6 +260,18 @@ internal enum Mt5CommandType OrderCheck = 303, Buy = 304, Sell = 305, - GetSymbols = 306 + GetSymbols = 306, + + CustomSymbolCreate = 340, + CustomSymbolDelete = 341, + CustomSymbolSetDouble = 342, + CustomSymbolSetInteger = 343, + CustomSymbolSetString = 344, + CustomRatesDelete = 345, + CustomRatesReplace = 346, + CustomRatesUpdate = 347, + CustomTicksAdd = 348, + CustomTicksDelete = 349, + CustomTicksReplace = 350 } } From 388cc68a30cc1eebd0c0b8b2fbdf498f46786fe6 Mon Sep 17 00:00:00 2001 From: biohazardxxx Date: Sat, 4 Jul 2026 00:04:38 +0200 Subject: [PATCH 2/7] MtApi5: add 11 CustomSymbol/CustomRates/CustomTicks client methods MqlRates[]/MqlTick[] are serialized into the command payload as JSON arrays using the same field names the EA emits for the reverse direction. MqlTick gains a time_msc field (falls back to MtTime*1000 when unset). Int-returning functions propagate the native -1 failure value. Co-Authored-By: Claude Fable 5 --- MtApi5/MqlTick.cs | 1 + MtApi5/MtApi5Client.cs | 205 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) diff --git a/MtApi5/MqlTick.cs b/MtApi5/MqlTick.cs index 5e908c88..6423d44d 100755 --- a/MtApi5/MqlTick.cs +++ b/MtApi5/MqlTick.cs @@ -39,6 +39,7 @@ internal MqlTick(MtTick? tick) public long MtTime { get; set; } // Time of the last prices update + public long time_msc { get; set; } // Time of the last prices update in milliseconds (0 = derive from MtTime) public double bid { get; set; } // Current Bid price public double ask { get; set; } // Current Ask price public double last { get; set; } // Price of the last deal (Last) diff --git a/MtApi5/MtApi5Client.cs b/MtApi5/MtApi5Client.cs index b75bd2a0..f5d38421 100755 --- a/MtApi5/MtApi5Client.cs +++ b/MtApi5/MtApi5Client.cs @@ -1645,6 +1645,211 @@ public bool SymbolInfoTick(string symbol, out MqlTick? tick) return null; } + #region Custom Symbols + + /// + ///Creates a custom symbol with the specified name in the specified group. + /// + ///Custom symbol name. It must not contain groups or subgroups the symbol is located in. + ///The group name a symbol is located in. + ///Name of a symbol the properties of the created custom symbol are to be copied from. + ///true if successful, otherwise false. + public bool CustomSymbolCreate(string symbolName, string symbolPath = "", string symbolOrigin = "") + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "SymbolPath", symbolPath ?? string.Empty }, { "SymbolOrigin", symbolOrigin ?? string.Empty } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomSymbolCreate, cmdParams); + } + + /// + ///Deletes a custom symbol with the specified name. + /// + ///Custom symbol name. + ///true if successful, otherwise false. + public bool CustomSymbolDelete(string symbolName) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomSymbolDelete, cmdParams); + } + + /// + ///Sets the double type property value for a custom symbol. + /// + ///Custom symbol name. + ///Symbol property ID. + ///A double type property value. + ///true if successful, otherwise false. + public bool CustomSymbolSetDouble(string symbolName, ENUM_SYMBOL_INFO_DOUBLE propertyId, double value) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "PropertyId", (int)propertyId }, { "PropertyValue", value } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomSymbolSetDouble, cmdParams); + } + + /// + ///Sets the integer type property value for a custom symbol. + /// + ///Custom symbol name. + ///Symbol property ID. + ///A long type property value. + ///true if successful, otherwise false. + public bool CustomSymbolSetInteger(string symbolName, ENUM_SYMBOL_INFO_INTEGER propertyId, long value) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "PropertyId", (int)propertyId }, { "PropertyValue", value } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomSymbolSetInteger, cmdParams); + } + + /// + ///Sets the string type property value for a custom symbol. + /// + ///Custom symbol name. + ///Symbol property ID. + ///A string type property value. + ///true if successful, otherwise false. + public bool CustomSymbolSetString(string symbolName, ENUM_SYMBOL_INFO_STRING propertyId, string value) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "PropertyId", (int)propertyId }, { "PropertyValue", value ?? string.Empty } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomSymbolSetString, cmdParams); + } + + /// + ///Deletes all bars from the price history of the custom symbol in the specified time interval. + /// + ///Custom symbol name. + ///Time of the first bar in the price history within the specified range to be removed. + ///Time of the last bar in the price history within the specified range to be removed. + ///Number of deleted bars or -1 in case of an error. + public int CustomRatesDelete(string symbolName, DateTime from, DateTime to) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "FromDate", Mt5TimeConverter.ConvertToMtTime(from) }, { "ToDate", Mt5TimeConverter.ConvertToMtTime(to) } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomRatesDelete, cmdParams); + } + + /// + ///Fully replaces the price history of the custom symbol within the specified time interval with the data from the MqlRates type array. + /// + ///Custom symbol name. + ///Time of the first bar in the price history within the specified range to be updated. + ///Time of the last bar in the price history within the specified range to be updated. + ///Array of the MqlRates type history data for M1. + ///Number of updated bars or -1 in case of an error. + public int CustomRatesReplace(string symbolName, DateTime from, DateTime to, MqlRates[] rates) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "FromDate", Mt5TimeConverter.ConvertToMtTime(from) }, { "ToDate", Mt5TimeConverter.ConvertToMtTime(to) }, + { "Rates", MqlRatesArrayToPayload(rates) } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomRatesReplace, cmdParams); + } + + /// + ///Adds missing bars to the custom symbol history and replaces existing data with the ones from the MqlRates type array. + /// + ///Custom symbol name. + ///Array of the MqlRates type history data for M1. + ///Number of updated bars or -1 in case of an error. + public int CustomRatesUpdate(string symbolName, MqlRates[] rates) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "Rates", MqlRatesArrayToPayload(rates) } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomRatesUpdate, cmdParams); + } + + /// + ///Adds data from an array of the MqlTick type to the price history of a custom symbol. The custom symbol must be selected in the Market Watch window. + /// + ///Custom symbol name. + ///Array of the MqlTick type tick data arranged in order of time from the oldest to the newest. + ///Number of added ticks or -1 in case of an error. + public int CustomTicksAdd(string symbolName, MqlTick[] ticks) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "Ticks", MqlTickArrayToPayload(ticks) } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomTicksAdd, cmdParams); + } + + /// + ///Deletes all ticks from the price history of the custom symbol in the specified time interval. + /// + ///Custom symbol name. + ///Time of the first tick in the price history within the specified range to be removed, in milliseconds since 1970.01.01. + ///Time of the last tick in the price history within the specified range to be removed, in milliseconds since 1970.01.01. + ///Number of deleted ticks or -1 in case of an error. + public int CustomTicksDelete(string symbolName, long fromMsc, long toMsc) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "FromMsc", fromMsc }, { "ToMsc", toMsc } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomTicksDelete, cmdParams); + } + + /// + ///Fully replaces the price history of the custom symbol within the specified time interval with the data from the MqlTick type array. + /// + ///Custom symbol name. + ///Time of the first tick in the price history within the specified range to be replaced, in milliseconds since 1970.01.01. + ///Time of the last tick in the price history within the specified range to be replaced, in milliseconds since 1970.01.01. + ///Array of the MqlTick type tick data arranged in order of time from the oldest to the newest. + ///Number of replaced ticks or -1 in case of an error. + public int CustomTicksReplace(string symbolName, long fromMsc, long toMsc, MqlTick[] ticks) + { + Dictionary cmdParams = new() { { "SymbolName", symbolName ?? string.Empty }, + { "FromMsc", fromMsc }, { "ToMsc", toMsc }, { "Ticks", MqlTickArrayToPayload(ticks) } }; + return SendCommand(ExecutorHandle, Mt5CommandType.CustomTicksReplace, cmdParams); + } + + // Serializes MqlRates[] for the command payload using the same field names + // the EA uses in MqlRatesToJson (the EA parses the reverse direction). + private static List> MqlRatesArrayToPayload(MqlRates[]? rates) + { + List> payload = new(rates?.Length ?? 0); + if (rates == null) + return payload; + foreach (var r in rates) + { + payload.Add(new Dictionary + { + { "mt_time", r.mt_time }, + { "open", r.open }, + { "high", r.high }, + { "low", r.low }, + { "close", r.close }, + { "tick_volume", r.tick_volume }, + { "spread", r.spread }, + { "real_volume", r.real_volume } + }); + } + return payload; + } + + // Serializes MqlTick[] for the command payload using the same field names + // the EA uses in MqlTickToJson (the EA parses the reverse direction). + // TimeMsc falls back to MtTime * 1000 when time_msc is not set. + private static List> MqlTickArrayToPayload(MqlTick[]? ticks) + { + List> payload = new(ticks?.Length ?? 0); + if (ticks == null) + return payload; + foreach (var t in ticks) + { + payload.Add(new Dictionary + { + { "Time", t.MtTime }, + { "TimeMsc", t.time_msc != 0 ? t.time_msc : t.MtTime * 1000 }, + { "Bid", t.bid }, + { "Ask", t.ask }, + { "Last", t.last }, + { "Volume", t.volume }, + { "VolumeReal", t.volume_real }, + { "Flags", (uint)t.flags } + }); + } + return payload; + } + + #endregion + /// ///Allows receiving time of beginning and end of the specified quoting sessions for a specified symbol and weekday. /// From b7febd17a2f9e94f1a35df2d556ca6dacc54d2d1 Mon Sep 17 00:00:00 2001 From: biohazardxxx Date: Sat, 4 Jul 2026 00:04:48 +0200 Subject: [PATCH 3/7] MQL5: add custom symbols command handlers and executor registration (340-350) Adds JsonToMqlRates/JsonToMqlTick (and array variants) to parse struct arrays from the command payload - the reverse of MqlRatesToJson / MqlTickToJson. Tick time is reconciled between seconds and time_msc. Co-Authored-By: Claude Fable 5 --- mq5/MtApi5.mq5 | 216 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 1 deletion(-) diff --git a/mq5/MtApi5.mq5 b/mq5/MtApi5.mq5 index 18c0b55e..eb88e413 100644 --- a/mq5/MtApi5.mq5 +++ b/mq5/MtApi5.mq5 @@ -381,7 +381,19 @@ int preinit() ADD_EXECUTOR(304, Buy); ADD_EXECUTOR(305, Sell); ADD_EXECUTOR(306, GetSymbols); - + + ADD_EXECUTOR(340, CustomSymbolCreate); + ADD_EXECUTOR(341, CustomSymbolDelete); + ADD_EXECUTOR(342, CustomSymbolSetDouble); + ADD_EXECUTOR(343, CustomSymbolSetInteger); + ADD_EXECUTOR(344, CustomSymbolSetString); + ADD_EXECUTOR(345, CustomRatesDelete); + ADD_EXECUTOR(346, CustomRatesReplace); + ADD_EXECUTOR(347, CustomRatesUpdate); + ADD_EXECUTOR(348, CustomTicksAdd); + ADD_EXECUTOR(349, CustomTicksDelete); + ADD_EXECUTOR(350, CustomTicksReplace); + return (0); } @@ -3474,6 +3486,130 @@ string Execute_GetSymbols() return CreateSuccessResponse(jaSymbols); } +//+------------------------------------------------------------------+ +//| Custom symbols command handlers | +//+------------------------------------------------------------------+ +string Execute_CustomSymbolCreate() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_STRING_JSON_VALUE(jo, "SymbolPath", symbol_path); + GET_STRING_JSON_VALUE(jo, "SymbolOrigin", symbol_origin); + if(StringLen(symbol_origin) == 0) symbol_origin = NULL; + bool ok = CustomSymbolCreate(symbol_name, symbol_path, symbol_origin); + return CreateSuccessResponse(new JSONBool(ok)); +} + +string Execute_CustomSymbolDelete() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + bool ok = CustomSymbolDelete(symbol_name); + return CreateSuccessResponse(new JSONBool(ok)); +} + +string Execute_CustomSymbolSetDouble() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_INT_JSON_VALUE(jo, "PropertyId", property_id); + GET_DOUBLE_JSON_VALUE(jo, "PropertyValue", property_value); + bool ok = CustomSymbolSetDouble(symbol_name, (ENUM_SYMBOL_INFO_DOUBLE)property_id, property_value); + return CreateSuccessResponse(new JSONBool(ok)); +} + +string Execute_CustomSymbolSetInteger() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_INT_JSON_VALUE(jo, "PropertyId", property_id); + GET_LONG_JSON_VALUE(jo, "PropertyValue", property_value); + bool ok = CustomSymbolSetInteger(symbol_name, (ENUM_SYMBOL_INFO_INTEGER)property_id, property_value); + return CreateSuccessResponse(new JSONBool(ok)); +} + +string Execute_CustomSymbolSetString() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_INT_JSON_VALUE(jo, "PropertyId", property_id); + GET_STRING_JSON_VALUE(jo, "PropertyValue", property_value); + bool ok = CustomSymbolSetString(symbol_name, (ENUM_SYMBOL_INFO_STRING)property_id, property_value); + return CreateSuccessResponse(new JSONBool(ok)); +} + +string Execute_CustomRatesDelete() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_LONG_JSON_VALUE(jo, "FromDate", from_date); + GET_LONG_JSON_VALUE(jo, "ToDate", to_date); + int result = CustomRatesDelete(symbol_name, (datetime)from_date, (datetime)to_date); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + +string Execute_CustomRatesReplace() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_LONG_JSON_VALUE(jo, "FromDate", from_date); + GET_LONG_JSON_VALUE(jo, "ToDate", to_date); + CHECK_JSON_VALUE(jo, "Rates"); + MqlRates rates[]; + if(JsonToMqlRatesArray(jo.p.getArray("Rates"), rates) < 0) + return CreateErrorResponse(-1, "Failed to parse parameter Rates"); + int result = CustomRatesReplace(symbol_name, (datetime)from_date, (datetime)to_date, rates); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + +string Execute_CustomRatesUpdate() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + CHECK_JSON_VALUE(jo, "Rates"); + MqlRates rates[]; + if(JsonToMqlRatesArray(jo.p.getArray("Rates"), rates) < 0) + return CreateErrorResponse(-1, "Failed to parse parameter Rates"); + int result = CustomRatesUpdate(symbol_name, rates); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + +string Execute_CustomTicksAdd() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + CHECK_JSON_VALUE(jo, "Ticks"); + MqlTick ticks[]; + if(JsonToMqlTickArray(jo.p.getArray("Ticks"), ticks) < 0) + return CreateErrorResponse(-1, "Failed to parse parameter Ticks"); + int result = CustomTicksAdd(symbol_name, ticks); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + +string Execute_CustomTicksDelete() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_LONG_JSON_VALUE(jo, "FromMsc", from_msc); + GET_LONG_JSON_VALUE(jo, "ToMsc", to_msc); + int result = CustomTicksDelete(symbol_name, from_msc, to_msc); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + +string Execute_CustomTicksReplace() +{ + GET_JSON_PAYLOAD(jo); + GET_STRING_JSON_VALUE(jo, "SymbolName", symbol_name); + GET_LONG_JSON_VALUE(jo, "FromMsc", from_msc); + GET_LONG_JSON_VALUE(jo, "ToMsc", to_msc); + CHECK_JSON_VALUE(jo, "Ticks"); + MqlTick ticks[]; + if(JsonToMqlTickArray(jo.p.getArray("Ticks"), ticks) < 0) + return CreateErrorResponse(-1, "Failed to parse parameter Ticks"); + int result = CustomTicksReplace(symbol_name, from_msc, to_msc, ticks); + return CreateSuccessResponse(new JSONNumber((long)result)); +} + int PositionCloseAll() { CTrade trade; @@ -3915,4 +4051,82 @@ JSONObject* MqlRatesToJson(const MqlRates& rates) jo.put("spread", new JSONNumber(rates.spread)); jo.put("real_volume", new JSONNumber(rates.real_volume)); return jo; +} + +bool JsonToMqlRates(JSONObject* jo, MqlRates& rates) +{ + if (jo == NULL) return false; + + long time; + if (!jo.getLong("mt_time", time)) return false; + rates.time = (datetime)time; + + if (!jo.getDouble("open", rates.open)) return false; + if (!jo.getDouble("high", rates.high)) return false; + if (!jo.getDouble("low", rates.low)) return false; + if (!jo.getDouble("close", rates.close)) return false; + if (!jo.getLong("tick_volume", rates.tick_volume)) return false; + if (!jo.getInt("spread", rates.spread)) return false; + if (!jo.getLong("real_volume", rates.real_volume)) return false; + + return true; +} + +int JsonToMqlRatesArray(JSONArray* ja, MqlRates& rates[]) +{ + if (ja == NULL) return -1; + + int size = ja.size(); + ArrayResize(rates, size); + for(int i = 0; i < size; i++) + { + ZeroMemory(rates[i]); + if (!JsonToMqlRates(ja.getObject(i), rates[i])) + return -1; + } + return size; +} + +bool JsonToMqlTick(JSONObject* jo, MqlTick& tick) +{ + if (jo == NULL) return false; + + long time; + if (!jo.getLong("Time", time)) return false; + long time_msc; + if (!jo.getLong("TimeMsc", time_msc)) return false; + if (time_msc == 0 && time != 0) time_msc = time * 1000; + if (time == 0 && time_msc != 0) time = time_msc / 1000; + tick.time = (datetime)time; + tick.time_msc = time_msc; + + if (!jo.getDouble("Bid", tick.bid)) return false; + if (!jo.getDouble("Ask", tick.ask)) return false; + if (!jo.getDouble("Last", tick.last)) return false; + if (!jo.getDouble("VolumeReal", tick.volume_real)) return false; + + long volume; + if (!jo.getLong("Volume", volume)) return false; + tick.volume = (ulong)volume; + + long flags; + if (!jo.getLong("Flags", flags)) return false; + tick.flags = (uint)flags; + + return true; +} + +int JsonToMqlTickArray(JSONArray* ja, MqlTick& ticks[]) +{ + if (ja == NULL) return -1; + + int size = ja.size(); + ArrayResize(ticks, size); + for(int i = 0; i < size; i++) + { + ZeroMemory(ticks[i]); + if (!JsonToMqlTick(ja.getObject(i), ticks[i])) + return -1; + } + return size; } \ No newline at end of file From cebc76afa6500e1a637072151378eea60e4046bf Mon Sep 17 00:00:00 2001 From: biohazardxxx Date: Sat, 4 Jul 2026 00:04:48 +0200 Subject: [PATCH 4/7] MtApi5TestClient: add manual CustomSymbolTest action Co-Authored-By: Claude Fable 5 --- TestClients/MtApi5TestClient/MainWindow.xaml | 2 + TestClients/MtApi5TestClient/ViewModel.cs | 44 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/TestClients/MtApi5TestClient/MainWindow.xaml b/TestClients/MtApi5TestClient/MainWindow.xaml index c30fe43e..7c28e99a 100755 --- a/TestClients/MtApi5TestClient/MainWindow.xaml +++ b/TestClients/MtApi5TestClient/MainWindow.xaml @@ -421,6 +421,8 @@