2026-08-12 19:54:12 +07:00 | | | //+------------------------------------------------------------------+
|
| | | //| CSDPEncoder.mqh |
|
| | | //| Centaur Quant Architecture — Network Module |
|
| | | //| Standardized Data Protocol (SDP) JSON Serializer |
|
| | | //+------------------------------------------------------------------+
|
| | | //| PURPOSE |
|
| | | //| Single source of truth for formatting every outgoing JSON |
|
| | | //| payload from the MT5 EA. Enforces the SDP envelope schema, |
|
| | | //| dot-decimal number formatting, JSON string escaping, and |
|
| | | //| ISO-8601 UTC timestamps. Raw JSON building is FORBIDDEN |
|
| | | //| anywhere else in the MQL5 codebase. |
|
| | | //+------------------------------------------------------------------+
|
| | | #property strict
|
| | |
|
| | | #ifndef CSDPENCODER_MQH
|
| | | #define CSDPENCODER_MQH
|
| | |
|
| | | //--- SDP action types. Wire names are stable contracts for the Python router. ---
|
| | | enum ENUM_SDP_ACTION
|
| | | {
|
| | | SDP_ACTION_HEARTBEAT = 0, // Heartbeat
|
| | | SDP_ACTION_TICK_HARVEST = 1, // Tick_Harvest
|
| | | SDP_ACTION_SETUP_DETECTED = 2, // Setup_Detected
|
| | | SDP_ACTION_TRADE_OPENED = 3, // Trade_Opened
|
| | | SDP_ACTION_TRADE_CLOSED = 4 // Trade_Closed
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
| | | //+------------------------------------------------------------------+
|
| | | class CSDPEncoder
|
| | | {
|
| | | private:
|
| | | string m_sdp_version; // wire SDP protocol version carried by every envelope
|
| | |
|
| | | //--- wire name of an action (single source of truth for the enum) ---
|
| | | string ActionToString(const ENUM_SDP_ACTION action);
|
| | |
|
| | | //--- ISO-8601 UTC "2026-08-12T09:20:00.000Z" from any datetime ---
|
| | | string IsoTimestamp(const datetime t);
|
| | | string IsoTimestampUTC(); // convenience: uses TimeGMT()
|
| | |
|
| | | //--- JSON-safe string escaping (" \ \n \r \t + control chars) ---
|
| | | string EscapeJson(const string value);
|
| | |
|
| | | //--- dot-decimal number formatting; never locale-dependent ---
|
| | | string FormatDouble(const double value, const int digits);
|
| | |
|
| | | //--- wire name of an ENUM_TIMEFRAMES value (e.g., PERIOD_M15 -> "M15") ---
|
| | | string PeriodName(const ENUM_TIMEFRAMES period);
|
| | |
|
| | | //--- resolve empty timeframe to the current chart period ---
|
| | | string ResolveTimeframe(const string timeframe);
|
| | |
|
| | | //--- live digits of a symbol; 8 as a neutral formatting fallback ---
|
| | | int SymbolDigits(const string symbol);
|
| | |
|
| | | //--- clamp confidence into [0.0, 100.0]; logs on out-of-range/NaN ---
|
| | | double ClampConfidence(const double confidence);
|
| | |
|
| | | //--- assemble the full SDP envelope around a prebuilt payload object ---
|
| | | string BuildEnvelope(const ENUM_SDP_ACTION action,
|
| | | const string symbol,
|
| | | const string timeframe,
|
| | | const double confidence,
|
| | | const string payload_json,
|
| | | const string historical_context_json = "");
|
| | |
|
| | | public:
|
| | | CSDPEncoder();
|
| | |
|
| | | //--- SDP protocol version embedded in every envelope ---
|
| | | string Version() const { return m_sdp_version; }
|
| | |
|
| | | //--- action-specific serializers; each returns "" on invalid input ---
|
| | | string EncodeHeartbeat(const string symbol, const string timeframe);
|
| | |
|
| | | string EncodeTickHarvest(const string symbol, const string timeframe,
|
| | | const double bid, const double ask,
|
| | | const datetime tick_time, const long tick_volume);
|
| | |
|
| | | string EncodeSetup(const string symbol, const string timeframe,
|
| | | const string setup_type,
|
| | | const double entry, const double sl, const double tp,
|
| | | const double confidence,
|
| | | const string historical_context_json = "");
|
| | |
|
| | | string EncodeTradeOpened(const string symbol, const string timeframe,
|
| | | const ulong ticket, const ENUM_POSITION_TYPE side,
|
| | | const double lot, const double entry_price,
|
| | | const double sl, const double tp,
|
| | | const double confidence,
|
| | | const string historical_context_json = "");
|
| | |
|
| | | string EncodeTradeClosed(const string symbol, const ulong ticket,
|
| | | const double profit, const double r_multiple,
|
| | | const double initial_ai_score,
|
| | | const string timeframe = "");
|
| | |
|
| | | //--- charter-required historical context array serializer ---
|
| | | string BuildHistoricalContext(const string symbol,
|
| | | const double &swing_high[],
|
| | | const double &swing_low[],
|
| | | const datetime &swing_time[]);
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Constructor — fix the ratified wire version of the SDP protocol. |
|
| | | //+------------------------------------------------------------------+
|
| | | CSDPEncoder::CSDPEncoder()
|
| | | : m_sdp_version("1.0.0")
|
| | | {
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| ActionToString — maps the enum to its stable wire string. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::ActionToString(const ENUM_SDP_ACTION action)
|
| | | {
|
| | | switch(action)
|
| | | {
|
| | | case SDP_ACTION_HEARTBEAT:
|
| | | return "Heartbeat";
|
| | | case SDP_ACTION_TICK_HARVEST:
|
| | | return "Tick_Harvest";
|
| | | case SDP_ACTION_SETUP_DETECTED:
|
| | | return "Setup_Detected";
|
| | | case SDP_ACTION_TRADE_OPENED:
|
| | | return "Trade_Opened";
|
| | | case SDP_ACTION_TRADE_CLOSED:
|
| | | return "Trade_Closed";
|
| | | default:
|
| | | PrintFormat("[CSDPEncoder] ERROR: unknown action %d. Using 'Unknown'.", (int)action);
|
| | | return "Unknown";
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| IsoTimestamp — ISO-8601 UTC with millisecond precision. |
|
| | | //| datetime has 1-second resolution, so the ms field is literal. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::IsoTimestamp(const datetime t)
|
| | | {
|
| | | datetime ts = t;
|
| | | if(ts <= 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] WARNING: invalid timestamp value; using TimeGMT().");
|
| | | ts = TimeGMT();
|
| | | }
|
| | | string stamp = TimeToString(ts, TIME_DATE | TIME_SECONDS);
|
| | | StringReplace(stamp, ".", "-"); // 2026-08-12 09:20:00
|
| | | StringReplace(stamp, " ", "T"); // 2026-08-12T09:20:00
|
| | | return stamp + ".000Z"; // ISO-8601 UTC, millisecond field
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| IsoTimestampUTC — current GMT wall clock as an ISO-8601 string. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::IsoTimestampUTC()
|
| | | {
|
| | | return IsoTimestamp(TimeGMT());
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EscapeJson — escapes every character that would break JSON: |
|
| | | //| quotes, backslashes, control codes, and all bytes < 0x20 |
|
| | | //| (\\b, \\f, \\v etc. are emitted as valid \\uXXXX escapes). |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EscapeJson(const string value)
|
| | | {
|
| | | string result = "";
|
| | | const int len = StringLen(value);
|
| | | for(int i = 0; i < len; i++)
|
| | | {
|
| | | const ushort c = StringGetCharacter(value, i);
|
| | | switch(c)
|
| | | {
|
| | | case '"':
|
| | | result += "\\\"";
|
| | | break;
|
| | | case '\\':
|
| | | result += "\\\\";
|
| | | break;
|
| | | case '\n':
|
| | | result += "\\n";
|
| | | break;
|
| | | case '\r':
|
| | | result += "\\r";
|
| | | break;
|
| | | case '\t':
|
| | | result += "\\t";
|
| | | break;
|
| | | default:
|
| | | if(c < 0x20)
|
| | | result += StringFormat("\\u%04x", c); // backspace, form feed, etc.
|
| | | else
|
| | | result += ShortToString(c);
|
| | | break;
|
| | | }
|
| | | }
|
| | | return result;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| PeriodName — wire name of a timeframe value. |
|
| | | //| PERIOD_CURRENT resolves to the actual chart period. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::PeriodName(const ENUM_TIMEFRAMES period)
|
| | | {
|
| | | ENUM_TIMEFRAMES p = period;
|
| | | if(p == PERIOD_CURRENT)
|
| | | p = (ENUM_TIMEFRAMES)_Period;
|
| | | string name = EnumToString(p); // e.g. "PERIOD_M15"
|
| | | StringReplace(name, "PERIOD_", ""); // "M15"
|
| | | return name;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| FormatDouble — dot-decimal string. DoubleToString already emits a |
|
| | | //| dot; the replace is a defensive guard against any locale quirk. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::FormatDouble(const double value, const int digits)
|
| | | {
|
| | | string s = DoubleToString(value, digits);
|
| | | StringReplace(s, ",", ".");
|
| | | return s;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| ResolveTimeframe — trims the input; empty falls back to the |
|
| | | //| current chart period so the envelope's timeframe key is never |
|
| | | //| blank (the schema mandates it for every action). |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::ResolveTimeframe(const string timeframe)
|
| | | {
|
| | | string tf = timeframe;
|
| | | StringTrimLeft(tf);
|
| | | StringTrimRight(tf);
|
| | | if(StringLen(tf) == 0)
|
| | | {
|
| | | tf = PeriodName((ENUM_TIMEFRAMES)_Period);
|
| | | PrintFormat("[CSDPEncoder] WARNING: empty timeframe; falling back to current chart '%s'.", tf);
|
| | | }
|
| | | return tf;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| SymbolDigits — live SYMBOL_DIGITS for price formatting. 8 is a |
|
| | | //| neutral fallback only (covers crypto-style precision); it is not |
|
| | | //| a per-symbol point buffer. |
|
| | | //+------------------------------------------------------------------+
|
| | | int CSDPEncoder::SymbolDigits(const string symbol)
|
| | | {
|
| | | int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
| | | if(digits <= 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] WARNING: SYMBOL_DIGITS unavailable for %s; using 8.", symbol);
|
| | | digits = 8;
|
| | | }
|
| | | return digits;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| ClampConfidence — enforces the SDP contract 0.0 <= score <= 100.0.|
|
| | | //+------------------------------------------------------------------+
|
| | | double CSDPEncoder::ClampConfidence(const double confidence)
|
| | | {
|
| | | if(!MathIsValidNumber(confidence))
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: confidence is NaN/Inf; using 0.0.");
|
| | | return 0.0;
|
| | | }
|
| | | if(confidence < 0.0 || confidence > 100.0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] WARNING: confidence %G outside [0,100]; clamped.", confidence);
|
| | | return MathMax(0.0, MathMin(confidence, 100.0));
|
| | | }
|
| | | return confidence;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| BuildEnvelope — assembles the mandatory SDP envelope around a |
|
| | | //| prebuilt payload object. Adds the charter-mandated |
|
| | | //| "historical_context" key only when a non-empty array is supplied. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::BuildEnvelope(const ENUM_SDP_ACTION action,
|
| | | const string symbol,
|
| | | const string timeframe,
|
| | | const double confidence,
|
| | | const string payload_json,
|
| | | const string historical_context_json)
|
| | | {
|
| | | const string tf = ResolveTimeframe(timeframe);
|
| | | if(StringLen(symbol) == 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: empty symbol for action %s. Envelope not built.",
|
| | | ActionToString(action));
|
| | | return "";
|
| | | }
|
| | | if(StringLen(payload_json) == 0 || StringGetCharacter(payload_json, 0) != '{')
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid payload object for action %s. Envelope not built.",
|
| | | ActionToString(action));
|
| | | return "";
|
| | | }
|
| | | string env = "{";
|
2026-08-13 08:57:03 +07:00 | | | env += "\"sdp_version\":" + "\"" + m_sdp_version + "\",";
|
2026-08-12 19:54:12 +07:00 | | | env += "\"timestamp\":\"" + IsoTimestampUTC() + "\",";
|
| | | env += "\"symbol\":\"" + EscapeJson(symbol) + "\",";
|
| | | env += "\"timeframe\":\"" + EscapeJson(tf) + "\",";
|
| | | env += "\"action_type\":\"" + ActionToString(action) + "\",";
|
| | | env += "\"algorithmic_confidence_score\":" + FormatDouble(ClampConfidence(confidence), 2);
|
| | | // Charter: recent swing context MUST ride Setup_Detected / Trade_* actions.
|
| | | if(StringLen(historical_context_json) > 0)
|
| | | env += ",\"historical_context\":" + historical_context_json;
|
| | | env += ",\"payload\":" + payload_json;
|
| | | env += "}";
|
| | | return env;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EncodeHeartbeat — lightweight liveness signal. Confidence is not |
|
| | | //| applicable for heartbeats, so the envelope carries 0.0. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EncodeHeartbeat(const string symbol, const string timeframe)
|
| | | {
|
| | | const string payload = "{\"status\":\"alive\"}";
|
| | | return BuildEnvelope(SDP_ACTION_HEARTBEAT, symbol, timeframe, 0.0, payload);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EncodeTickHarvest — one tick for the Data Harvest Core. Raw bid/ |
|
| | | //| ask in price terms plus spread in points and tick volume. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EncodeTickHarvest(const string symbol, const string timeframe,
|
| | | const double bid, const double ask,
|
| | | const datetime tick_time, const long tick_volume)
|
| | | {
|
| | | if(!MathIsValidNumber(bid) || !MathIsValidNumber(ask) || bid <= 0.0 || ask <= 0.0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid bid/ask (%G / %G) for %s. Tick_Harvest dropped.",
|
| | | bid, ask, symbol);
|
| | | return "";
|
| | | }
|
| | | if(ask < bid)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] WARNING: ask %G < bid %G for %s; values forwarded as-is.",
|
| | | ask, bid, symbol);
|
| | | }
|
| | | const int digits = SymbolDigits(symbol);
|
| | | const double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
| | | const long spread_points = (point > 0.0) ? (long)MathRound((ask - bid) / point) : 0;
|
| | | string payload = "{";
|
| | | payload += "\"bid\":" + FormatDouble(bid, digits) + ",";
|
| | | payload += "\"ask\":" + FormatDouble(ask, digits) + ",";
|
| | | payload += "\"spread_points\":" + IntegerToString(spread_points) + ",";
|
| | | payload += "\"tick_time\":\"" + IsoTimestamp(tick_time) + "\",";
|
| | | payload += "\"tick_volume\":" + IntegerToString(tick_volume);
|
| | | payload += "}";
|
| | | return BuildEnvelope(SDP_ACTION_TICK_HARVEST, symbol, timeframe, 0.0, payload);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EncodeSetup — a detected setup proposal. Carries prices, the |
|
| | | //| setup label, and a computed |R:R| for the Python side. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EncodeSetup(const string symbol, const string timeframe,
|
| | | const string setup_type,
|
| | | const double entry, const double sl, const double tp,
|
| | | const double confidence,
|
| | | const string historical_context_json)
|
| | | {
|
| | | if(StringLen(setup_type) == 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: empty setup_type for %s. Setup_Detected dropped.", symbol);
|
| | | return "";
|
| | | }
|
| | | if(!MathIsValidNumber(entry) || !MathIsValidNumber(sl) || !MathIsValidNumber(tp) ||
|
| | | entry <= 0.0 || sl <= 0.0 || tp <= 0.0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid prices (entry=%G sl=%G tp=%G) for %s. Setup_Detected dropped.",
|
| | | entry, sl, tp, symbol);
|
| | | return "";
|
| | | }
|
| | | const int digits = SymbolDigits(symbol);
|
| | | double rr = 0.0;
|
| | | const double risk = entry - sl;
|
| | | if(risk != 0.0)
|
| | | rr = MathAbs((tp - entry) / risk);
|
| | | string payload = "{";
|
| | | payload += "\"setup_type\":\"" + EscapeJson(setup_type) + "\",";
|
| | | payload += "\"entry\":" + FormatDouble(entry, digits) + ",";
|
| | | payload += "\"sl\":" + FormatDouble(sl, digits) + ",";
|
| | | payload += "\"tp\":" + FormatDouble(tp, digits) + ",";
|
| | | payload += "\"risk_reward\":" + FormatDouble(rr, 2);
|
| | | payload += "}";
|
| | | return BuildEnvelope(SDP_ACTION_SETUP_DETECTED, symbol, timeframe, confidence,
|
| | | payload, historical_context_json);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EncodeTradeOpened — actual execution record: ticket, direction, |
|
| | | //| lot, and the live levels. Confidence reflects the score at open. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EncodeTradeOpened(const string symbol, const string timeframe,
|
| | | const ulong ticket, const ENUM_POSITION_TYPE side,
|
| | | const double lot, const double entry_price,
|
| | | const double sl, const double tp,
|
| | | const double confidence,
|
| | | const string historical_context_json)
|
| | | {
|
| | | string side_str = "";
|
| | | if(side == POSITION_TYPE_BUY)
|
| | | side_str = "buy";
|
| | | else
|
| | | if(side == POSITION_TYPE_SELL)
|
| | | side_str = "sell";
|
| | | else
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid position side %d for %s. Trade_Opened dropped.",
|
| | | (int)side, symbol);
|
| | | return "";
|
| | | }
|
| | | if(ticket == 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: zero ticket for %s. Trade_Opened dropped.", symbol);
|
| | | return "";
|
| | | }
|
| | | if(!MathIsValidNumber(lot) || lot <= 0.0 ||
|
| | | !MathIsValidNumber(entry_price) || entry_price <= 0.0 ||
|
| | | !MathIsValidNumber(sl) || sl <= 0.0 ||
|
| | | !MathIsValidNumber(tp) || tp <= 0.0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid trade fields (lot=%G entry=%G sl=%G tp=%G) for %s. Trade_Opened dropped.",
|
| | | lot, entry_price, sl, tp, symbol);
|
| | | return "";
|
| | | }
|
| | | const int digits = SymbolDigits(symbol);
|
| | | string payload = "{";
|
| | | payload += "\"ticket\":" + IntegerToString(ticket) + ",";
|
| | | payload += "\"direction\":\"" + side_str + "\",";
|
| | | payload += "\"lot\":" + FormatDouble(lot, 2) + ",";
|
| | | payload += "\"entry_price\":" + FormatDouble(entry_price, digits) + ",";
|
| | | payload += "\"sl\":" + FormatDouble(sl, digits) + ",";
|
| | | payload += "\"tp\":" + FormatDouble(tp, digits);
|
| | | payload += "}";
|
| | | return BuildEnvelope(SDP_ACTION_TRADE_OPENED, symbol, timeframe, confidence,
|
| | | payload, historical_context_json);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| EncodeTradeClosed — closes the feedback loop: final PnL and |
|
| | | //| R-multiple linked to the original AI score. Envelope confidence |
|
| | | //| carries that original score (schema mandates the field). |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::EncodeTradeClosed(const string symbol, const ulong ticket,
|
| | | const double profit, const double r_multiple,
|
| | | const double initial_ai_score,
|
| | | const string timeframe)
|
| | | {
|
| | | if(StringLen(symbol) == 0 || ticket == 0)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid symbol/ticket ('%s', %I64u). Trade_Closed dropped.",
|
| | | symbol, ticket);
|
| | | return "";
|
| | | }
|
| | | if(!MathIsValidNumber(profit) || !MathIsValidNumber(r_multiple))
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: invalid profit/R-multiple (%G / %G) for ticket %I64u. Trade_Closed dropped.",
|
| | | profit, r_multiple, ticket);
|
| | | return "";
|
| | | }
|
| | | string payload = "{";
|
| | | payload += "\"ticket\":" + IntegerToString(ticket) + ",";
|
| | | payload += "\"profit\":" + FormatDouble(profit, 2) + ",";
|
| | | payload += "\"r_multiple\":" + FormatDouble(r_multiple, 2) + ",";
|
| | | payload += "\"initial_ai_score\":" + FormatDouble(ClampConfidence(initial_ai_score), 2);
|
| | | payload += "}";
|
| | | return BuildEnvelope(SDP_ACTION_TRADE_CLOSED, symbol, timeframe, initial_ai_score, payload);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| BuildHistoricalContext — serializes recent swing structure into a |
|
| | | //| JSON array for the charter-mandated "historical_context" key. |
|
| | | //| Arrays must be equal-length and non-empty; "" returned otherwise. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CSDPEncoder::BuildHistoricalContext(const string symbol,
|
| | | const double &swing_high[],
|
| | | const double &swing_low[],
|
| | | const datetime &swing_time[])
|
| | | {
|
| | | const int n_high = ArraySize(swing_high);
|
| | | const int n_low = ArraySize(swing_low);
|
| | | const int n_time = ArraySize(swing_time);
|
| | | if(n_high == 0 || n_high != n_low || n_high != n_time)
|
| | | {
|
| | | PrintFormat("[CSDPEncoder] ERROR: historical context arrays mismatch (%d/%d/%d). Empty array returned.",
|
| | | n_high, n_low, n_time);
|
| | | return "";
|
| | | }
|
| | | const int digits = SymbolDigits(symbol);
|
| | | string arr = "[";
|
| | | for(int i = 0; i < n_high; i++)
|
| | | {
|
| | | if(i > 0)
|
| | | arr += ",";
|
| | | arr += "{\"swing_high\":" + FormatDouble(swing_high[i], digits) + ",";
|
| | | arr += "\"swing_low\":" + FormatDouble(swing_low[i], digits) + ",";
|
| | | arr += "\"time\":\"" + IsoTimestamp(swing_time[i]) + "\"}";
|
| | | }
|
| | | arr += "]";
|
| | | return arr;
|
| | | }
|
| | |
|
| | | #endif // CSDPENCODER_MQH
|