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
|