Centaur_Quant_Architecture/MQL5/Include/Network/CSDPEncoder.mqh

498 lines
22 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 = "{";
env += "\"sdp_version\":" + "\"" + m_sdp_version + "\",";
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