From d39aa8a405bcedecd522b68048e2d1521af867da Mon Sep 17 00:00:00 2001 From: ghwmelite-dotcom Date: Wed, 1 Jul 2026 12:45:43 +0000 Subject: [PATCH 1/3] feat(downloads): offer compilable MQL5 source ZIP per EA Add a "Source (.mq5)" button to each EA card (Master, Follower, Journal) in Downloads & Setup. Each button downloads a self-contained, MT5-structured ZIP: the EA plus the transitive closure of the .mqh includes it needs to compile, laid out as Experts/ + Include/ with a README. - New GET /v1/accounts/ea-source/:type route builds the ZIP from an EA_BUNDLES manifest. Authenticated but no per-account check (source carries no per-user credentials). - src/lib/zip.ts: dependency-free STORE-method ZIP writer for the Workers runtime (no node:zlib). Verified end-to-end via independent unzip. - scripts/embed-ea-source.mjs generates src/generated/ea-source.ts from the canonical apps/ea sources (single source of truth), wired into prebuild/predev/predeploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/pages/DownloadsPage.tsx | 58 ++++++- workers/api-gateway/package.json | 4 + .../api-gateway/scripts/embed-ea-source.mjs | 65 ++++++++ .../api-gateway/src/generated/ea-source.ts | 21 +++ workers/api-gateway/src/lib/zip.ts | 141 ++++++++++++++++++ workers/api-gateway/src/routes/accounts.ts | 133 +++++++++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 workers/api-gateway/scripts/embed-ea-source.mjs create mode 100644 workers/api-gateway/src/generated/ea-source.ts create mode 100644 workers/api-gateway/src/lib/zip.ts diff --git a/apps/web/src/pages/DownloadsPage.tsx b/apps/web/src/pages/DownloadsPage.tsx index 28c66ac..99f23c4 100644 --- a/apps/web/src/pages/DownloadsPage.tsx +++ b/apps/web/src/pages/DownloadsPage.tsx @@ -1,11 +1,19 @@ import { useState, useEffect } from 'react'; -import { Upload, Download, BookOpen, ChevronDown, ChevronRight, AlertTriangle, CheckCircle, Package } from 'lucide-react'; +import { Upload, Download, BookOpen, ChevronDown, ChevronRight, AlertTriangle, CheckCircle, Package, FileCode2 } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; +import { API_BASE } from '@/lib/constants'; import { useAccountsStore, type Account } from '@/stores/accounts'; import { useAuthStore } from '@/stores/auth'; +/** Server-side ZIP filenames, keyed by EA card type. */ +const SOURCE_ZIP_NAMES: Record<'master' | 'follower' | 'journal', string> = { + master: 'EdgeRelay_Master_Source.zip', + follower: 'EdgeRelay_Follower_Source.zip', + journal: 'TradeJournal_Sync_Source.zip', +}; + /* ------------------------------------------------------------------ */ /* Setup Guide Data */ /* ------------------------------------------------------------------ */ @@ -286,6 +294,7 @@ function EADownloadCard({ accounts: Account[]; }) { const [downloading, setDownloading] = useState(false); + const [sourceDownloading, setSourceDownloading] = useState(false); const [error, setError] = useState(null); const token = useAuthStore((s) => s.token); @@ -341,6 +350,40 @@ function EADownloadCard({ } }; + // Source ZIP is generic (no per-user credentials) — available to any + // signed-in user without needing a matching account created first. + const handleSourceDownload = async () => { + setError(null); + setSourceDownloading(true); + + try { + const res = await fetch(`${API_BASE}/accounts/ea-source/${type}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + const json = await res.json().catch(() => null); + const msg = (json as { error?: { message?: string } })?.error?.message || 'Source download failed'; + setError(msg); + return; + } + + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = SOURCE_ZIP_NAMES[type]; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch { + setError('Network error. Please try again.'); + } finally { + setSourceDownloading(false); + } + }; + return (
@@ -366,7 +409,7 @@ function EADownloadCard({ {isJournal ? '~38 KB' : isMaster ? '~45 KB' : '~52 KB'}
-
+
+
{error && ( diff --git a/workers/api-gateway/package.json b/workers/api-gateway/package.json index 5b6451f..f1eab33 100644 --- a/workers/api-gateway/package.json +++ b/workers/api-gateway/package.json @@ -4,6 +4,10 @@ "private": true, "type": "module", "scripts": { + "embed:ea": "node scripts/embed-ea-source.mjs", + "prebuild": "npm run embed:ea", + "predev": "npm run embed:ea", + "predeploy": "npm run embed:ea", "dev": "wrangler dev", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit" diff --git a/workers/api-gateway/scripts/embed-ea-source.mjs b/workers/api-gateway/scripts/embed-ea-source.mjs new file mode 100644 index 0000000..6586764 --- /dev/null +++ b/workers/api-gateway/scripts/embed-ea-source.mjs @@ -0,0 +1,65 @@ +// embed-ea-source.mjs +// --------------------------------------------------------------------------- +// Reads the canonical MQL5 source files from apps/ea/ and generates a bundled +// TypeScript module the Worker can serve at runtime (Cloudflare Workers cannot +// read the filesystem, so the source is embedded at build time). +// +// The single source of truth stays the real .mq5 / .mqh files — run this +// whenever they change (wired into `prebuild` / predeploy in package.json). +// +// node scripts/embed-ea-source.mjs +// --------------------------------------------------------------------------- + +import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs'; +import { dirname, join, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const EA_DIR = join(__dirname, '..', '..', '..', 'apps', 'ea'); +const INCLUDE_DIR = join(EA_DIR, 'Include'); +const OUT_FILE = join(__dirname, '..', 'src', 'generated', 'ea-source.ts'); + +// The three top-level Expert Advisors (Scripts/ setup EA is bundled separately). +const EA_FILES = ['EdgeRelay_Master.mq5', 'EdgeRelay_Follower.mq5', 'TradeJournal_Sync.mq5']; + +/** Collect { basename: contents } for a fixed list of EA files + every .mqh include. */ +function collectSources() { + const sources = {}; + + for (const file of EA_FILES) { + sources[file] = readFileSync(join(EA_DIR, file), 'utf8'); + } + + for (const file of readdirSync(INCLUDE_DIR)) { + if (file.endsWith('.mqh')) { + sources[basename(file)] = readFileSync(join(INCLUDE_DIR, file), 'utf8'); + } + } + + return sources; +} + +function generate() { + const sources = collectSources(); + const keys = Object.keys(sources).sort(); + + const entries = keys + .map((key) => ` ${JSON.stringify(key)}: ${JSON.stringify(sources[key])},`) + .join('\n'); + + const banner = `// AUTO-GENERATED by scripts/embed-ea-source.mjs — DO NOT EDIT BY HAND.\n` + + `// Regenerate with: npm run embed:ea\n` + + `// Source of truth: apps/ea/**/*.mq5 and apps/ea/Include/**/*.mqh\n`; + + const body = + `${banner}\n` + + `/** Canonical MQL5 source, keyed by file basename (EAs + include headers). */\n` + + `export const EA_SOURCE: Record = {\n${entries}\n};\n`; + + mkdirSync(dirname(OUT_FILE), { recursive: true }); + writeFileSync(OUT_FILE, body, 'utf8'); + + console.log(`[embed-ea-source] wrote ${keys.length} files to ${OUT_FILE}`); +} + +generate(); diff --git a/workers/api-gateway/src/generated/ea-source.ts b/workers/api-gateway/src/generated/ea-source.ts new file mode 100644 index 0000000..38ad939 --- /dev/null +++ b/workers/api-gateway/src/generated/ea-source.ts @@ -0,0 +1,21 @@ +// AUTO-GENERATED by scripts/embed-ea-source.mjs — DO NOT EDIT BY HAND. +// Regenerate with: npm run embed:ea +// Source of truth: apps/ea/**/*.mq5 and apps/ea/Include/**/*.mqh + +/** Canonical MQL5 source, keyed by file basename (EAs + include headers). */ +export const EA_SOURCE: Record = { + "EdgeRelay_Common.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Common.mqh |\r\n//| EdgeRelay Master EA - Shared Types |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_COMMON_MQH\r\n#define EDGERELAY_COMMON_MQH\r\n\r\n//--- Signal action enum\r\nenum ENUM_SIGNAL_ACTION\r\n {\r\n SIGNAL_OPEN = 0,\r\n SIGNAL_MODIFY = 1,\r\n SIGNAL_PARTIAL_CLOSE = 2,\r\n SIGNAL_CLOSE = 3,\r\n SIGNAL_PENDING = 4,\r\n SIGNAL_CANCEL_PENDING = 5\r\n };\r\n\r\n//--- Connection status enum\r\nenum ENUM_CONNECTION_STATUS\r\n {\r\n STATUS_CONNECTED = 0,\r\n STATUS_DISCONNECTED = 1,\r\n STATUS_CONNECTING = 2,\r\n STATUS_ERROR = 3\r\n };\r\n\r\n//--- Constants\r\n#define MAX_QUEUE_SIZE 1000\r\n#define HEARTBEAT_INTERVAL_MS 5000\r\n#define MAX_SIGNAL_AGE_S 30\r\n\r\n//--- Signal struct\r\nstruct Signal\r\n {\r\n string signal_id;\r\n string account_id;\r\n int sequence_num;\r\n ENUM_SIGNAL_ACTION action;\r\n ENUM_ORDER_TYPE order_type;\r\n string symbol;\r\n double volume;\r\n double price;\r\n double sl;\r\n double tp;\r\n long magic_number;\r\n long ticket;\r\n string comment;\r\n datetime timestamp;\r\n string hmac_signature;\r\n };\r\n\r\n//--- Heartbeat struct\r\nstruct Heartbeat\r\n {\r\n string account_id;\r\n datetime timestamp;\r\n string hmac_signature;\r\n int queue_size;\r\n int signals_sent;\r\n };\r\n\r\n//--- PropGuard drawdown type enum\r\nenum ENUM_DD_TYPE\r\n {\r\n DD_STATIC = 0, // Static (from initial balance)\r\n DD_TRAILING = 1, // Trailing (from equity high water mark)\r\n DD_EOD_TRAILING = 2 // EOD Trailing (updates at end of day only)\r\n };\r\n\r\n//--- PropGuard daily loss calculation enum\r\nenum ENUM_DAILY_LOSS_CALC\r\n {\r\n DL_BALANCE_START_OF_DAY = 0, // From balance at day start\r\n DL_EQUITY_HIGH_OF_DAY = 1, // From highest equity today (strictest)\r\n DL_PREVIOUS_DAY_BALANCE = 2 // From yesterday's closing balance\r\n };\r\n\r\n//--- PropGuard evaluation verdict\r\nstruct PropGuardVerdict\r\n {\r\n bool allowed;\r\n string blocked_rule;\r\n string blocked_reason;\r\n double current_daily_loss_pct;\r\n double current_drawdown_pct;\r\n double projected_daily_loss_pct;\r\n double projected_drawdown_pct;\r\n };\r\n\r\n//--- PropGuard equity state\r\nstruct EquityState\r\n {\r\n double balance;\r\n double equity;\r\n double floating_pnl;\r\n double daily_pnl;\r\n double daily_pnl_percent;\r\n double high_water_mark;\r\n double total_drawdown_pct;\r\n double balance_start_of_day;\r\n double equity_high_of_day;\r\n datetime day_start_time;\r\n int trades_today;\r\n int positions_open;\r\n };\r\n\r\n//--- PropGuard rule configuration (loaded from cloud or local inputs)\r\nstruct PropGuardRules\r\n {\r\n double initial_balance;\r\n double profit_target_percent;\r\n double max_daily_loss_percent;\r\n ENUM_DAILY_LOSS_CALC daily_loss_calculation;\r\n double max_total_drawdown_percent;\r\n ENUM_DD_TYPE drawdown_type;\r\n bool trailing_dd_lock_at_breakeven;\r\n double max_lot_size;\r\n int max_open_positions;\r\n int max_daily_trades;\r\n int min_trading_days;\r\n bool consistency_rule_enabled;\r\n double max_profit_single_day_pct;\r\n string allowed_trading_start;\r\n string allowed_trading_end;\r\n bool block_weekend_holding;\r\n bool block_during_news;\r\n int news_minutes_before;\r\n int news_minutes_after;\r\n double warning_threshold_pct;\r\n double critical_threshold_pct;\r\n bool auto_close_at_critical;\r\n };\r\n\r\n//--- PropGuard status enum\r\nenum ENUM_PROPGUARD_STATUS\r\n {\r\n PG_PROTECTED = 0,\r\n PG_WARNING = 1,\r\n PG_CRITICAL = 2,\r\n PG_LOCKED = 3,\r\n PG_DISABLED = 4\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\n//| Convert signal action to string |\r\n//+------------------------------------------------------------------+\r\nstring ActionToString(ENUM_SIGNAL_ACTION action)\r\n {\r\n switch(action)\r\n {\r\n case SIGNAL_OPEN: return \"open\";\r\n case SIGNAL_MODIFY: return \"modify\";\r\n case SIGNAL_PARTIAL_CLOSE: return \"partial_close\";\r\n case SIGNAL_CLOSE: return \"close\";\r\n case SIGNAL_PENDING: return \"pending\";\r\n case SIGNAL_CANCEL_PENDING: return \"cancel_pending\";\r\n }\r\n return \"unknown\";\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Convert order type to string |\r\n//+------------------------------------------------------------------+\r\nstring OrderTypeToStr(ENUM_ORDER_TYPE type)\r\n {\r\n switch(type)\r\n {\r\n case ORDER_TYPE_BUY: return \"buy\";\r\n case ORDER_TYPE_SELL: return \"sell\";\r\n case ORDER_TYPE_BUY_LIMIT: return \"buy_limit\";\r\n case ORDER_TYPE_SELL_LIMIT: return \"sell_limit\";\r\n case ORDER_TYPE_BUY_STOP: return \"buy_stop\";\r\n case ORDER_TYPE_SELL_STOP: return \"sell_stop\";\r\n case ORDER_TYPE_BUY_STOP_LIMIT: return \"buy_stop_limit\";\r\n case ORDER_TYPE_SELL_STOP_LIMIT: return \"sell_stop_limit\";\r\n case ORDER_TYPE_CLOSE_BY: return \"close_by\";\r\n }\r\n return \"unknown\";\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Escape a string for JSON output |\r\n//+------------------------------------------------------------------+\r\n//+------------------------------------------------------------------+\r\n//| Format double like JavaScript's JSON.stringify (strip trailing 0) |\r\n//+------------------------------------------------------------------+\r\nstring JsDouble(double value, int digits)\r\n {\r\n if(value == 0.0)\r\n return \"0\";\r\n string s = DoubleToString(value, digits);\r\n //--- Strip trailing zeros after decimal point\r\n if(StringFind(s, \".\") >= 0)\r\n {\r\n while(StringLen(s) > 1 && StringGetCharacter(s, StringLen(s) - 1) == '0')\r\n s = StringSubstr(s, 0, StringLen(s) - 1);\r\n //--- Remove trailing decimal point\r\n if(StringLen(s) > 0 && StringGetCharacter(s, StringLen(s) - 1) == '.')\r\n s = StringSubstr(s, 0, StringLen(s) - 1);\r\n }\r\n return s;\r\n }\r\n\r\nstring JsonEscape(string text)\r\n {\r\n string result = text;\r\n StringReplace(result, \"\\\\\", \"\\\\\\\\\");\r\n StringReplace(result, \"\\\"\", \"\\\\\\\"\");\r\n StringReplace(result, \"\\n\", \"\\\\n\");\r\n StringReplace(result, \"\\r\", \"\\\\r\");\r\n StringReplace(result, \"\\t\", \"\\\\t\");\r\n return result;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Convert Signal struct to JSON string |\r\n//+------------------------------------------------------------------+\r\nstring SignalToJson(Signal &signal)\r\n {\r\n string json = \"{\";\r\n\r\n json += \"\\\"signal_id\\\":\\\"\" + JsonEscape(signal.signal_id) + \"\\\",\";\r\n json += \"\\\"account_id\\\":\\\"\" + JsonEscape(signal.account_id) + \"\\\",\";\r\n json += \"\\\"sequence_num\\\":\" + IntegerToString(signal.sequence_num) + \",\";\r\n json += \"\\\"action\\\":\\\"\" + ActionToString(signal.action) + \"\\\",\";\r\n json += \"\\\"order_type\\\":\\\"\" + OrderTypeToStr(signal.order_type) + \"\\\",\";\r\n json += \"\\\"symbol\\\":\\\"\" + JsonEscape(signal.symbol) + \"\\\",\";\r\n int digits = (int)SymbolInfoInteger(signal.symbol, SYMBOL_DIGITS);\r\n if(digits <= 0)\r\n digits = 5;\r\n\r\n json += \"\\\"volume\\\":\" + JsDouble(signal.volume, 8) + \",\";\r\n json += \"\\\"price\\\":\" + JsDouble(signal.price, digits) + \",\";\r\n json += \"\\\"sl\\\":\" + JsDouble(signal.sl, digits) + \",\";\r\n json += \"\\\"tp\\\":\" + JsDouble(signal.tp, digits) + \",\";\r\n json += \"\\\"magic_number\\\":\" + IntegerToString(signal.magic_number) + \",\";\r\n json += \"\\\"ticket\\\":\" + IntegerToString(signal.ticket) + \",\";\r\n json += \"\\\"comment\\\":\\\"\" + JsonEscape(signal.comment) + \"\\\",\";\r\n json += \"\\\"timestamp\\\":\" + IntegerToString((long)signal.timestamp) + \",\";\r\n json += \"\\\"hmac_signature\\\":\\\"\" + JsonEscape(signal.hmac_signature) + \"\\\"\";\r\n\r\n json += \"}\";\r\n return json;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Generate a unique signal ID |\r\n//+------------------------------------------------------------------+\r\nstring GenerateSignalId(string accountId, int sequenceNum)\r\n {\r\n return accountId + \"-\" + IntegerToString(sequenceNum) + \"-\" + IntegerToString(GetTickCount());\r\n }\r\n\r\n#endif // EDGERELAY_COMMON_MQH\r\n", + "EdgeRelay_Crypto.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Crypto.mqh |\r\n//| EdgeRelay Master EA - HMAC-SHA256 Auth |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_CRYPTO_MQH\r\n#define EDGERELAY_CRYPTO_MQH\r\n\r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| Convert byte array to hex string |\r\n//+------------------------------------------------------------------+\r\nstring BytesToHex(uchar &bytes[], int len)\r\n {\r\n string hex = \"\";\r\n for(int i = 0; i < len; i++)\r\n {\r\n string h = StringFormat(\"%02x\", bytes[i]);\r\n hex += h;\r\n }\r\n return hex;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| SHA256 hash wrapper |\r\n//+------------------------------------------------------------------+\r\nbool Sha256(uchar &data[], uchar &hash[])\r\n {\r\n uchar key[]; // empty key for plain hashing\r\n return CryptEncode(CRYPT_HASH_SHA256, data, key, hash) > 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| HMAC-SHA256 implementation |\r\n//| RFC 2104: HMAC(K, m) = H((K' ^ opad) || H((K' ^ ipad) || m)) |\r\n//+------------------------------------------------------------------+\r\nstring HmacSha256(string message, string key)\r\n {\r\n int blockSize = 64; // SHA256 block size\r\n\r\n //--- Convert key and message to byte arrays\r\n uchar keyBytes[];\r\n uchar msgBytes[];\r\n StringToCharArray(key, keyBytes, 0, StringLen(key));\r\n StringToCharArray(message, msgBytes, 0, StringLen(message));\r\n\r\n int keyLen = ArraySize(keyBytes);\r\n\r\n //--- If key is longer than block size, hash it\r\n if(keyLen > blockSize)\r\n {\r\n uchar hashedKey[];\r\n if(!Sha256(keyBytes, hashedKey))\r\n return \"\";\r\n ArrayResize(keyBytes, ArraySize(hashedKey));\r\n ArrayCopy(keyBytes, hashedKey);\r\n keyLen = ArraySize(keyBytes);\r\n }\r\n\r\n //--- Pad key to block size with zeros\r\n uchar keyPadded[];\r\n ArrayResize(keyPadded, blockSize);\r\n ArrayInitialize(keyPadded, 0);\r\n for(int i = 0; i < keyLen; i++)\r\n keyPadded[i] = keyBytes[i];\r\n\r\n //--- Create inner and outer padded keys\r\n uchar ipad[];\r\n uchar opad[];\r\n ArrayResize(ipad, blockSize);\r\n ArrayResize(opad, blockSize);\r\n\r\n for(int i = 0; i < blockSize; i++)\r\n {\r\n ipad[i] = (uchar)(keyPadded[i] ^ 0x36);\r\n opad[i] = (uchar)(keyPadded[i] ^ 0x5C);\r\n }\r\n\r\n //--- Inner hash: SHA256(ipad || message)\r\n int msgLen = ArraySize(msgBytes);\r\n uchar innerData[];\r\n ArrayResize(innerData, blockSize + msgLen);\r\n ArrayCopy(innerData, ipad, 0, 0, blockSize);\r\n ArrayCopy(innerData, msgBytes, blockSize, 0, msgLen);\r\n\r\n uchar innerHash[];\r\n if(!Sha256(innerData, innerHash))\r\n return \"\";\r\n\r\n //--- Outer hash: SHA256(opad || innerHash)\r\n int innerHashLen = ArraySize(innerHash);\r\n uchar outerData[];\r\n ArrayResize(outerData, blockSize + innerHashLen);\r\n ArrayCopy(outerData, opad, 0, 0, blockSize);\r\n ArrayCopy(outerData, innerHash, blockSize, 0, innerHashLen);\r\n\r\n uchar outerHash[];\r\n if(!Sha256(outerData, outerHash))\r\n return \"\";\r\n\r\n return BytesToHex(outerHash, ArraySize(outerHash));\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Sign a signal payload with HMAC-SHA256 |\r\n//| Sorts fields alphabetically, concatenates key=value pairs |\r\n//+------------------------------------------------------------------+\r\nstring SignPayload(Signal &signal, string apiSecret)\r\n {\r\n //--- Build sorted JSON (must match worker's verifyHmacSignature)\r\n //--- Keys must be in alphabetical order, matching JSON.stringify output\r\n int digits = (int)SymbolInfoInteger(signal.symbol, SYMBOL_DIGITS);\r\n if(digits <= 0)\r\n digits = 5;\r\n\r\n string payload = \"{\";\r\n payload += \"\\\"account_id\\\":\\\"\" + JsonEscape(signal.account_id) + \"\\\",\";\r\n payload += \"\\\"action\\\":\\\"\" + ActionToString(signal.action) + \"\\\",\";\r\n payload += \"\\\"comment\\\":\\\"\" + JsonEscape(signal.comment) + \"\\\",\";\r\n payload += \"\\\"magic_number\\\":\" + IntegerToString(signal.magic_number) + \",\";\r\n payload += \"\\\"order_type\\\":\\\"\" + OrderTypeToStr(signal.order_type) + \"\\\",\";\r\n payload += \"\\\"price\\\":\" + JsDouble(signal.price, digits) + \",\";\r\n payload += \"\\\"sequence_num\\\":\" + IntegerToString(signal.sequence_num) + \",\";\r\n payload += \"\\\"signal_id\\\":\\\"\" + JsonEscape(signal.signal_id) + \"\\\",\";\r\n payload += \"\\\"sl\\\":\" + JsDouble(signal.sl, digits) + \",\";\r\n payload += \"\\\"symbol\\\":\\\"\" + signal.symbol + \"\\\",\";\r\n payload += \"\\\"ticket\\\":\" + IntegerToString(signal.ticket) + \",\";\r\n payload += \"\\\"timestamp\\\":\" + IntegerToString((long)signal.timestamp) + \",\";\r\n payload += \"\\\"tp\\\":\" + JsDouble(signal.tp, digits) + \",\";\r\n payload += \"\\\"volume\\\":\" + JsDouble(signal.volume, 8);\r\n payload += \"}\";\r\n\r\n return HmacSha256(payload, apiSecret);\r\n }\r\n\r\n#endif // EDGERELAY_CRYPTO_MQH\r\n", + "EdgeRelay_Display.mqh": "//+------------------------------------------------------------------+\n//| EdgeRelay_Display.mqh |\n//| EdgeRelay Master EA - Chart Status Panel |\n//| https://www.edgerelay.io |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property strict\n\n#ifndef EDGERELAY_DISPLAY_MQH\n#define EDGERELAY_DISPLAY_MQH\n\n#include \n\n//--- Object name constants\n#define OBJ_PREFIX \"EdgeRelay_\"\n#define OBJ_PANEL_BG OBJ_PREFIX + \"PanelBG\"\n#define OBJ_STATUS_DOT OBJ_PREFIX + \"StatusDot\"\n#define OBJ_TITLE OBJ_PREFIX + \"Title\"\n#define OBJ_STATUS_TEXT OBJ_PREFIX + \"StatusText\"\n#define OBJ_SIGNALS_TEXT OBJ_PREFIX + \"SignalsText\"\n#define OBJ_QUEUE_TEXT OBJ_PREFIX + \"QueueText\"\n#define OBJ_LATENCY_TEXT OBJ_PREFIX + \"LatencyText\"\n#define OBJ_LAST_SIGNAL OBJ_PREFIX + \"LastSignalText\"\n\n//--- Panel dimensions\n#define PANEL_WIDTH 220\n#define PANEL_HEIGHT 155\n#define PANEL_X_OFFSET 10\n#define PANEL_Y_OFFSET 25\n#define TEXT_X_OFFSET 25\n#define LINE_HEIGHT 20\n\n//+------------------------------------------------------------------+\n//| On-chart status display class |\n//+------------------------------------------------------------------+\nclass CEdgeRelayDisplay\n {\nprivate:\n bool m_initialized;\n\n void CreatePanelLabel(string name, int x, int y, string text,\n color clr, int fontSize = 9, string font = \"Consolas\");\n void UpdatePanelLabel(string name, string text, color clr = clrNONE);\n\npublic:\n CEdgeRelayDisplay();\n ~CEdgeRelayDisplay();\n\n void Init();\n void Update(int status, int signalCount, int queueSize,\n double lastLatencyMs, datetime lastSignalTime);\n void Deinit();\n };\n\n//+------------------------------------------------------------------+\n//| Constructor |\n//+------------------------------------------------------------------+\nCEdgeRelayDisplay::CEdgeRelayDisplay()\n {\n m_initialized = false;\n }\n\n//+------------------------------------------------------------------+\n//| Destructor |\n//+------------------------------------------------------------------+\nCEdgeRelayDisplay::~CEdgeRelayDisplay()\n {\n if(m_initialized)\n Deinit();\n }\n\n//+------------------------------------------------------------------+\n//| Initialize chart display objects |\n//+------------------------------------------------------------------+\nvoid CEdgeRelayDisplay::Init()\n {\n //--- Background panel\n ObjectCreate(0, OBJ_PANEL_BG, OBJ_RECTANGLE_LABEL, 0, 0, 0);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_CORNER, CORNER_RIGHT_UPPER);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_XDISTANCE, PANEL_WIDTH + PANEL_X_OFFSET);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_YDISTANCE, PANEL_Y_OFFSET);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_XSIZE, PANEL_WIDTH);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_YSIZE, PANEL_HEIGHT);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_BGCOLOR, C'25,25,35');\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_BORDER_COLOR, C'60,60,80');\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_BORDER_TYPE, BORDER_FLAT);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_WIDTH, 1);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_BACK, false);\n ObjectSetInteger(0, OBJ_PANEL_BG, OBJPROP_SELECTABLE, false);\n\n int xBase = PANEL_WIDTH + PANEL_X_OFFSET - TEXT_X_OFFSET;\n int yBase = PANEL_Y_OFFSET + 8;\n\n //--- Status dot (Wingdings circle character 0x6C = 108)\n CreatePanelLabel(OBJ_STATUS_DOT, xBase + 12, yBase, \"l\", clrGray, 12, \"Wingdings\");\n\n //--- Title\n CreatePanelLabel(OBJ_TITLE, xBase - 15, yBase, \"EdgeRelay Master v1.0\", C'200,200,220', 9, \"Consolas\");\n\n //--- Status text\n CreatePanelLabel(OBJ_STATUS_TEXT, xBase, yBase + LINE_HEIGHT, \"Initializing...\", clrGray);\n\n //--- Signals sent today\n CreatePanelLabel(OBJ_SIGNALS_TEXT, xBase, yBase + LINE_HEIGHT * 2, \"Signals sent: 0\", C'160,160,180');\n\n //--- Queue size\n CreatePanelLabel(OBJ_QUEUE_TEXT, xBase, yBase + LINE_HEIGHT * 3, \"Queue size: 0\", C'160,160,180');\n\n //--- Latency\n CreatePanelLabel(OBJ_LATENCY_TEXT, xBase, yBase + LINE_HEIGHT * 4, \"Last latency: --\", C'160,160,180');\n\n //--- Last signal time\n CreatePanelLabel(OBJ_LAST_SIGNAL, xBase, yBase + LINE_HEIGHT * 5, \"Last signal: --:--:--\", C'160,160,180');\n\n m_initialized = true;\n ChartRedraw(0);\n }\n\n//+------------------------------------------------------------------+\n//| Update the display panel |\n//+------------------------------------------------------------------+\nvoid CEdgeRelayDisplay::Update(int status, int signalCount, int queueSize,\n double lastLatencyMs, datetime lastSignalTime)\n {\n if(!m_initialized)\n return;\n\n //--- Update status dot and text based on connection status\n string statusText = \"\";\n color dotColor = clrGray;\n\n switch((ENUM_CONNECTION_STATUS)status)\n {\n case STATUS_CONNECTED:\n statusText = \"Connected\";\n dotColor = clrLime;\n break;\n case STATUS_DISCONNECTED:\n statusText = \"Disconnected\";\n dotColor = clrRed;\n break;\n case STATUS_CONNECTING:\n statusText = \"Connecting...\";\n dotColor = clrYellow;\n break;\n case STATUS_ERROR:\n statusText = \"Error\";\n dotColor = clrOrangeRed;\n break;\n }\n\n UpdatePanelLabel(OBJ_STATUS_DOT, \"l\", dotColor);\n UpdatePanelLabel(OBJ_STATUS_TEXT, statusText);\n\n //--- Signals sent\n UpdatePanelLabel(OBJ_SIGNALS_TEXT, \"Signals sent: \" + IntegerToString(signalCount));\n\n //--- Queue size\n color queueColor = (queueSize > 0) ? clrOrange : C'160,160,180';\n UpdatePanelLabel(OBJ_QUEUE_TEXT, \"Queue size: \" + IntegerToString(queueSize), queueColor);\n\n //--- Latency\n string latencyStr = (lastLatencyMs >= 0)\n ? DoubleToString(lastLatencyMs, 0) + \"ms\"\n : \"--\";\n UpdatePanelLabel(OBJ_LATENCY_TEXT, \"Last latency: \" + latencyStr);\n\n //--- Last signal time\n string timeStr = (lastSignalTime > 0)\n ? TimeToString(lastSignalTime, TIME_SECONDS)\n : \"--:--:--\";\n //--- Show only HH:MM:SS\n int spacePos = StringFind(timeStr, \" \");\n if(spacePos >= 0)\n timeStr = StringSubstr(timeStr, spacePos + 1);\n UpdatePanelLabel(OBJ_LAST_SIGNAL, \"Last signal: \" + timeStr);\n\n ChartRedraw(0);\n }\n\n//+------------------------------------------------------------------+\n//| Remove all chart objects |\n//+------------------------------------------------------------------+\nvoid CEdgeRelayDisplay::Deinit()\n {\n ObjectDelete(0, OBJ_PANEL_BG);\n ObjectDelete(0, OBJ_STATUS_DOT);\n ObjectDelete(0, OBJ_TITLE);\n ObjectDelete(0, OBJ_STATUS_TEXT);\n ObjectDelete(0, OBJ_SIGNALS_TEXT);\n ObjectDelete(0, OBJ_QUEUE_TEXT);\n ObjectDelete(0, OBJ_LATENCY_TEXT);\n ObjectDelete(0, OBJ_LAST_SIGNAL);\n\n m_initialized = false;\n ChartRedraw(0);\n }\n\n//+------------------------------------------------------------------+\n//| Create a text label on the chart |\n//+------------------------------------------------------------------+\nvoid CEdgeRelayDisplay::CreatePanelLabel(string name, int x, int y, string text,\n color clr, int fontSize, string font)\n {\n ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);\n ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_UPPER);\n ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);\n ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);\n ObjectSetInteger(0, name, OBJPROP_COLOR, clr);\n ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);\n ObjectSetString(0, name, OBJPROP_FONT, font);\n ObjectSetString(0, name, OBJPROP_TEXT, text);\n ObjectSetInteger(0, name, OBJPROP_BACK, false);\n ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);\n }\n\n//+------------------------------------------------------------------+\n//| Update a label's text and optionally its color |\n//+------------------------------------------------------------------+\nvoid CEdgeRelayDisplay::UpdatePanelLabel(string name, string text, color clr)\n {\n ObjectSetString(0, name, OBJPROP_TEXT, text);\n if(clr != clrNONE)\n ObjectSetInteger(0, name, OBJPROP_COLOR, clr);\n }\n\n#endif // EDGERELAY_DISPLAY_MQH\n", + "EdgeRelay_Equity.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Equity.mqh |\r\n//| EdgeRelay PropGuard - Equity State Tracker |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_EQUITY_MQH\r\n#define EDGERELAY_EQUITY_MQH\r\n\r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| Equity tracker class |\r\n//| Updates every 1 second via OnTimer, syncs to cloud every 30s |\r\n//+------------------------------------------------------------------+\r\nclass CEquityTracker\r\n {\r\nprivate:\r\n EquityState m_state;\r\n PropGuardRules m_rules;\r\n bool m_initialized;\r\n datetime m_lastSyncTime;\r\n datetime m_lastDayCheck;\r\n double m_previousDayBalance;\r\n double m_eodHighWaterMark;\r\n bool m_hwmLockedAtBreakeven;\r\n\r\n void ResetDailyCounters();\r\n double GetDailyLossReference();\r\n\r\npublic:\r\n CEquityTracker();\r\n ~CEquityTracker();\r\n\r\n void Init(const PropGuardRules &rules);\r\n void SetRules(const PropGuardRules &rules);\r\n\r\n EquityState GetState() const { return m_state; }\r\n double GetDailyPnlPercent() const { return m_state.daily_pnl_percent; }\r\n double GetTotalDrawdownPercent() const { return m_state.total_drawdown_pct; }\r\n double GetHighWaterMark() const { return m_state.high_water_mark; }\r\n int GetTradesToday() const { return m_state.trades_today; }\r\n int GetPositionsOpen() const { return m_state.positions_open; }\r\n double GetBalanceStartOfDay() const { return m_state.balance_start_of_day; }\r\n double GetEquityHighOfDay() const { return m_state.equity_high_of_day; }\r\n ENUM_PROPGUARD_STATUS GetStatus();\r\n\r\n void Update();\r\n void OnTradeExecuted();\r\n void OnDayEnd();\r\n\r\n bool ShouldSync();\r\n string ToJson();\r\n\r\n double CalculateMaxSafeLot(string symbol);\r\n double GetProfitPercent();\r\n double GetProfitTarget();\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\nCEquityTracker::CEquityTracker()\r\n {\r\n m_initialized = false;\r\n m_lastSyncTime = 0;\r\n m_lastDayCheck = 0;\r\n m_previousDayBalance = 0;\r\n m_eodHighWaterMark = 0;\r\n m_hwmLockedAtBreakeven = false;\r\n ZeroMemory(m_state);\r\n ZeroMemory(m_rules);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nCEquityTracker::~CEquityTracker() {}\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::Init(const PropGuardRules &rules)\r\n {\r\n m_rules = rules;\r\n\r\n double balance = AccountInfoDouble(ACCOUNT_BALANCE);\r\n double equity = AccountInfoDouble(ACCOUNT_EQUITY);\r\n\r\n m_state.balance = balance;\r\n m_state.equity = equity;\r\n m_state.floating_pnl = equity - balance;\r\n m_state.daily_pnl = 0;\r\n m_state.daily_pnl_percent = 0;\r\n m_state.balance_start_of_day = balance;\r\n m_state.equity_high_of_day = equity;\r\n m_state.day_start_time = TimeCurrent();\r\n m_state.trades_today = 0;\r\n m_state.positions_open = PositionsTotal();\r\n\r\n double initialBal = (m_rules.initial_balance > 0) ? m_rules.initial_balance : balance;\r\n m_state.high_water_mark = MathMax(initialBal, equity);\r\n m_eodHighWaterMark = m_state.high_water_mark;\r\n m_previousDayBalance = balance;\r\n\r\n if(m_rules.drawdown_type == DD_STATIC)\r\n {\r\n double ddBase = (m_rules.initial_balance > 0) ? m_rules.initial_balance : balance;\r\n m_state.total_drawdown_pct = (ddBase > 0) ? ((ddBase - equity) / ddBase) * 100.0 : 0;\r\n }\r\n else\r\n {\r\n m_state.total_drawdown_pct = (m_state.high_water_mark > 0)\r\n ? ((m_state.high_water_mark - equity) / m_state.high_water_mark) * 100.0 : 0;\r\n }\r\n\r\n if(m_state.total_drawdown_pct < 0) m_state.total_drawdown_pct = 0;\r\n\r\n m_lastSyncTime = TimeCurrent();\r\n m_lastDayCheck = TimeCurrent();\r\n m_initialized = true;\r\n\r\n Print(\"[PropGuard] Equity tracker initialized. Balance=\", DoubleToString(balance, 2),\r\n \" HWM=\", DoubleToString(m_state.high_water_mark, 2),\r\n \" DD%=\", DoubleToString(m_state.total_drawdown_pct, 2));\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::SetRules(const PropGuardRules &rules)\r\n {\r\n m_rules = rules;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::Update()\r\n {\r\n if(!m_initialized) return;\r\n\r\n MqlDateTime dtNow, dtLast;\r\n TimeCurrent(dtNow);\r\n TimeToStruct(m_lastDayCheck, dtLast);\r\n\r\n if(dtNow.day != dtLast.day)\r\n {\r\n OnDayEnd();\r\n ResetDailyCounters();\r\n m_lastDayCheck = TimeCurrent();\r\n }\r\n\r\n double balance = AccountInfoDouble(ACCOUNT_BALANCE);\r\n double equity = AccountInfoDouble(ACCOUNT_EQUITY);\r\n\r\n m_state.balance = balance;\r\n m_state.equity = equity;\r\n m_state.floating_pnl = equity - balance;\r\n m_state.positions_open = PositionsTotal();\r\n\r\n //--- Update equity high of day\r\n if(equity > m_state.equity_high_of_day)\r\n m_state.equity_high_of_day = equity;\r\n\r\n //--- Update high water mark for ALL drawdown types (not just trailing)\r\n if(m_rules.drawdown_type == DD_TRAILING)\r\n {\r\n if(equity > m_state.high_water_mark)\r\n {\r\n double initialBal = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_state.balance_start_of_day;\r\n if(m_rules.trailing_dd_lock_at_breakeven && m_hwmLockedAtBreakeven)\r\n {\r\n // HWM locked, don't update\r\n }\r\n else if(m_rules.trailing_dd_lock_at_breakeven && equity >= initialBal)\r\n {\r\n m_state.high_water_mark = initialBal;\r\n m_hwmLockedAtBreakeven = true;\r\n }\r\n else\r\n {\r\n m_state.high_water_mark = equity;\r\n }\r\n }\r\n }\r\n else\r\n {\r\n //--- For static/EOD trailing: still track HWM for reporting purposes\r\n if(equity > m_state.high_water_mark)\r\n m_state.high_water_mark = equity;\r\n }\r\n\r\n //--- Calculate daily P&L based on BALANCE change from start of day (not equity)\r\n //--- This avoids floating P&L distortion\r\n double dailyBalancePnl = balance - m_state.balance_start_of_day;\r\n double dailyFloatingPnl = equity - balance;\r\n m_state.daily_pnl = dailyBalancePnl + dailyFloatingPnl; // realized + unrealized today\r\n\r\n //--- Daily loss reference for PropGuard threshold checks\r\n double dailyRef = GetDailyLossReference();\r\n m_state.daily_pnl_percent = (dailyRef > 0) ? (m_state.daily_pnl / dailyRef) * 100.0 : 0;\r\n\r\n if(m_rules.drawdown_type == DD_STATIC)\r\n {\r\n double ddBase = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_state.balance_start_of_day;\r\n m_state.total_drawdown_pct = (ddBase > 0) ? ((ddBase - equity) / ddBase) * 100.0 : 0;\r\n }\r\n else if(m_rules.drawdown_type == DD_TRAILING)\r\n {\r\n m_state.total_drawdown_pct = (m_state.high_water_mark > 0)\r\n ? ((m_state.high_water_mark - equity) / m_state.high_water_mark) * 100.0 : 0;\r\n }\r\n else // DD_EOD_TRAILING\r\n {\r\n m_state.total_drawdown_pct = (m_eodHighWaterMark > 0)\r\n ? ((m_eodHighWaterMark - equity) / m_eodHighWaterMark) * 100.0 : 0;\r\n }\r\n\r\n if(m_state.total_drawdown_pct < 0) m_state.total_drawdown_pct = 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ndouble CEquityTracker::GetDailyLossReference()\r\n {\r\n switch(m_rules.daily_loss_calculation)\r\n {\r\n case DL_BALANCE_START_OF_DAY:\r\n return m_state.balance_start_of_day;\r\n case DL_EQUITY_HIGH_OF_DAY:\r\n return m_state.equity_high_of_day;\r\n case DL_PREVIOUS_DAY_BALANCE:\r\n return m_previousDayBalance;\r\n }\r\n return m_state.balance_start_of_day;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::ResetDailyCounters()\r\n {\r\n m_previousDayBalance = m_state.balance;\r\n m_state.balance_start_of_day = m_state.balance;\r\n m_state.equity_high_of_day = m_state.equity;\r\n m_state.daily_pnl = 0;\r\n m_state.daily_pnl_percent = 0;\r\n m_state.trades_today = 0;\r\n m_state.day_start_time = TimeCurrent();\r\n\r\n Print(\"[PropGuard] Daily counters reset. New balance_start=\",\r\n DoubleToString(m_state.balance_start_of_day, 2));\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::OnDayEnd()\r\n {\r\n if(m_rules.drawdown_type == DD_EOD_TRAILING)\r\n {\r\n if(m_state.equity > m_eodHighWaterMark)\r\n {\r\n double initialBal = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_state.balance_start_of_day;\r\n if(m_rules.trailing_dd_lock_at_breakeven && m_state.equity >= initialBal)\r\n {\r\n m_eodHighWaterMark = initialBal;\r\n m_hwmLockedAtBreakeven = true;\r\n }\r\n else if(!m_hwmLockedAtBreakeven)\r\n {\r\n m_eodHighWaterMark = m_state.equity;\r\n }\r\n }\r\n m_state.high_water_mark = m_eodHighWaterMark;\r\n }\r\n\r\n Print(\"[PropGuard] Day end. HWM=\", DoubleToString(m_state.high_water_mark, 2),\r\n \" Daily PnL=\", DoubleToString(m_state.daily_pnl, 2));\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CEquityTracker::OnTradeExecuted()\r\n {\r\n m_state.trades_today++;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nENUM_PROPGUARD_STATUS CEquityTracker::GetStatus()\r\n {\r\n double dailyLossPct = MathAbs(MathMin(m_state.daily_pnl_percent, 0));\r\n double dailyRatio = (m_rules.max_daily_loss_percent > 0)\r\n ? (dailyLossPct / m_rules.max_daily_loss_percent) * 100.0 : 0;\r\n\r\n double ddRatio = (m_rules.max_total_drawdown_percent > 0)\r\n ? (m_state.total_drawdown_pct / m_rules.max_total_drawdown_percent) * 100.0 : 0;\r\n\r\n double worstRatio = MathMax(dailyRatio, ddRatio);\r\n\r\n if(worstRatio >= m_rules.critical_threshold_pct)\r\n return PG_CRITICAL;\r\n if(worstRatio >= m_rules.warning_threshold_pct)\r\n return PG_WARNING;\r\n\r\n return PG_PROTECTED;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CEquityTracker::ShouldSync()\r\n {\r\n return (TimeCurrent() - m_lastSyncTime >= 30);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nstring CEquityTracker::ToJson()\r\n {\r\n m_lastSyncTime = TimeCurrent();\r\n\r\n string json = \"{\";\r\n json += \"\\\"balance\\\":\" + DoubleToString(m_state.balance, 2) + \",\";\r\n json += \"\\\"equity\\\":\" + DoubleToString(m_state.equity, 2) + \",\";\r\n json += \"\\\"floating_pnl\\\":\" + DoubleToString(m_state.floating_pnl, 2) + \",\";\r\n json += \"\\\"daily_pnl\\\":\" + DoubleToString(m_state.daily_pnl, 2) + \",\";\r\n json += \"\\\"daily_pnl_percent\\\":\" + DoubleToString(m_state.daily_pnl_percent, 4) + \",\";\r\n json += \"\\\"high_water_mark\\\":\" + DoubleToString(m_state.high_water_mark, 2) + \",\";\r\n json += \"\\\"total_drawdown_percent\\\":\" + DoubleToString(m_state.total_drawdown_pct, 4) + \",\";\r\n json += \"\\\"balance_start_of_day\\\":\" + DoubleToString(m_state.balance_start_of_day, 2) + \",\";\r\n json += \"\\\"equity_high_of_day\\\":\" + DoubleToString(m_state.equity_high_of_day, 2) + \",\";\r\n json += \"\\\"trades_today\\\":\" + IntegerToString(m_state.trades_today) + \",\";\r\n json += \"\\\"positions_open\\\":\" + IntegerToString(m_state.positions_open);\r\n json += \"}\";\r\n return json;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ndouble CEquityTracker::CalculateMaxSafeLot(string symbol)\r\n {\r\n double dailyRef = GetDailyLossReference();\r\n double dailyLossRemaining = (m_rules.max_daily_loss_percent / 100.0 * dailyRef)\r\n + m_state.daily_pnl;\r\n if(dailyLossRemaining < 0) dailyLossRemaining = 0;\r\n\r\n double ddRemaining = 0;\r\n if(m_rules.drawdown_type == DD_STATIC)\r\n {\r\n double ddBase = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_state.balance_start_of_day;\r\n double maxDDAmount = m_rules.max_total_drawdown_percent / 100.0 * ddBase;\r\n double currentDD = ddBase - m_state.equity;\r\n ddRemaining = maxDDAmount - currentDD;\r\n }\r\n else\r\n {\r\n double hwm = (m_rules.drawdown_type == DD_EOD_TRAILING) ? m_eodHighWaterMark : m_state.high_water_mark;\r\n double maxDDAmount = m_rules.max_total_drawdown_percent / 100.0 * hwm;\r\n double currentDD = hwm - m_state.equity;\r\n ddRemaining = maxDDAmount - currentDD;\r\n }\r\n if(ddRemaining < 0) ddRemaining = 0;\r\n\r\n double buffer = MathMin(dailyLossRemaining, ddRemaining);\r\n if(buffer <= 0) return 0;\r\n\r\n double pointSize = SymbolInfoDouble(symbol, SYMBOL_POINT);\r\n double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);\r\n double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);\r\n\r\n if(pointSize <= 0 || tickValue <= 0 || tickSize <= 0)\r\n return 0;\r\n\r\n double slDistancePoints = 100.0;\r\n double lossPerLot = slDistancePoints * (tickValue / tickSize) * pointSize;\r\n\r\n if(lossPerLot <= 0) return 0;\r\n\r\n double maxLot = buffer / lossPerLot;\r\n\r\n double lotMin = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);\r\n double lotMax = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);\r\n double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);\r\n\r\n if(m_rules.max_lot_size > 0)\r\n lotMax = MathMin(lotMax, m_rules.max_lot_size);\r\n\r\n maxLot = MathMin(maxLot, lotMax);\r\n\r\n if(lotStep > 0)\r\n maxLot = MathFloor(maxLot / lotStep) * lotStep;\r\n\r\n if(maxLot < lotMin) maxLot = 0;\r\n\r\n return NormalizeDouble(maxLot, 2);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ndouble CEquityTracker::GetProfitPercent()\r\n {\r\n double initialBal = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_state.balance_start_of_day;\r\n if(initialBal <= 0) return 0;\r\n double profit = m_state.equity - initialBal;\r\n return (profit / initialBal) * 100.0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ndouble CEquityTracker::GetProfitTarget()\r\n {\r\n return m_rules.profit_target_percent;\r\n }\r\n\r\n#endif // EDGERELAY_EQUITY_MQH\r\n", + "EdgeRelay_Follower.mq5": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Follower.mq5 |\r\n//| EdgeRelay - Trade Signal Receiver & Executor |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property version \"1.00\"\r\n#property description \"EdgeRelay Follower EA - Receives and executes copied trades from a master account.\"\r\n#property strict\r\n\r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| Lot sizing mode |\r\n//+------------------------------------------------------------------+\r\nenum ENUM_LOT_MODE\r\n {\r\n LOT_MIRROR = 0, // Mirror master lot size exactly\r\n LOT_FIXED = 1, // Use fixed lot size\r\n LOT_MULTIPLIER = 2, // Multiply master lot by factor\r\n LOT_RISK_PERCENT = 3 // Risk % of balance per trade\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\n//| Input parameters |\r\n//+------------------------------------------------------------------+\r\ninput string API_Key = \"\"; // API Key\r\ninput string API_Secret = \"\"; // API Secret (HMAC)\r\ninput string API_Endpoint = \"https://edgerelay-signal-ingestion.ghwmelite.workers.dev\"; // Signal Endpoint\r\ninput string API_Gateway = \"https://edgerelay-api.ghwmelite.workers.dev\"; // API Gateway (PropGuard)\r\ninput string AccountID = \"\"; // This follower account ID\r\ninput string MasterAccountID = \"\"; // Master account to follow\r\ninput ENUM_LOT_MODE LotMode = LOT_MIRROR; // Lot sizing mode\r\ninput double LotValue = 1.0; // Lot value (fixed/multiplier/risk%)\r\ninput double MaxDailyLossPercent = 5.0; // Max daily loss (% of equity)\r\ninput double MaxTotalDrawdownPercent = 10.0; // Max total drawdown (% of equity)\r\ninput bool RespectNewsFilter = true; // Respect news filter\r\ninput int MaxSlippagePoints = 30; // Max slippage in points\r\ninput bool AutoReconnect = true; // Auto-reconnect on failure\r\ninput string SymbolSuffix = \"\"; // Symbol suffix (e.g. \".m\")\r\ninput int PollIntervalMs = 500; // Poll interval (ms)\r\ninput bool CopyBuys = true; // Copy BUY orders\r\ninput bool CopySells = true; // Copy SELL orders\r\ninput bool CopyPendings = true; // Copy pending orders\r\ninput bool InvertDirection = false; // Invert trade direction\r\n\r\n//--- PropGuard Settings\r\ninput bool PropGuard_Enabled = true; // Enable PropGuard protection\r\ninput bool PropGuard_UseCloudRules = true; // Fetch rules from dashboard\r\ninput string PropGuard_Preset = \"Custom\"; // Preset name\r\ninput double PropGuard_InitialBalance = 0; // 0 = auto-detect\r\ninput double PropGuard_MaxDailyLoss = 5.0; // % max daily loss\r\ninput double PropGuard_MaxDrawdown = 10.0; // % max total drawdown\r\ninput double PropGuard_ProfitTarget = 10.0; // % profit target\r\ninput ENUM_DD_TYPE PropGuard_DDType = DD_STATIC; // Drawdown type\r\ninput double PropGuard_MaxLotSize = 100.0; // Max lot size\r\ninput int PropGuard_MaxPositions = 50; // Max open positions\r\ninput int PropGuard_MaxDailyTrades = 0; // 0 = unlimited\r\ninput bool PropGuard_BlockNews = false; // Block during news\r\ninput int PropGuard_NewsMinBefore = 5; // Minutes before news\r\ninput int PropGuard_NewsMinAfter = 5; // Minutes after news\r\ninput bool PropGuard_BlockWeekend = false; // Block weekend holding\r\ninput double PropGuard_WarnThreshold = 80.0; // Warning threshold %\r\ninput double PropGuard_CritThreshold = 95.0; // Critical threshold %\r\ninput bool PropGuard_AutoClose = true; // Auto-close at critical\r\ninput bool PropGuard_ShowPanel = true; // Show PropGuard panel\r\ninput int PropGuard_PanelX = 10; // Panel X position\r\ninput int PropGuard_PanelY = 30; // Panel Y position\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execution result structure |\r\n//+------------------------------------------------------------------+\r\nstruct ExecutionResult\r\n {\r\n string signal_id;\r\n bool success;\r\n ulong ticket;\r\n double executed_price;\r\n double executed_volume;\r\n int slippage;\r\n string error_message;\r\n uint retcode;\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\n//| Equity guard result |\r\n//+------------------------------------------------------------------+\r\nstruct EquityGuardResult\r\n {\r\n bool allowed;\r\n string reason;\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\n//| Constants |\r\n//+------------------------------------------------------------------+\r\n#define MAX_SIGNALS_PER_POLL 50\r\n#define HTTP_TIMEOUT 5000\r\n#define COMMENT_PREFIX \"ER:\"\r\n\r\n//+------------------------------------------------------------------+\r\n//| Global state |\r\n//+------------------------------------------------------------------+\r\ndouble g_startingEquityToday = 0.0;\r\ndouble g_startingEquityTotal = 0.0;\r\ndatetime g_lastDayReset = 0;\r\nint g_signalsProcessed = 0;\r\nint g_signalsFailed = 0;\r\nint g_consecutiveErrors = 0;\r\nENUM_CONNECTION_STATUS g_connStatus = STATUS_DISCONNECTED;\r\n\r\nCTrade g_trade;\r\n\r\n//--- PropGuard globals\r\nCEquityTracker g_equityTracker;\r\nCPropGuard g_propGuard;\r\nCPropGuardDisplay g_pgDisplay;\r\nbool g_propGuardReady = false;\r\n\r\n//+------------------------------------------------------------------+\r\n//| Expert initialization |\r\n//+------------------------------------------------------------------+\r\nint OnInit()\r\n {\r\n //--- Validate required inputs\r\n if(API_Key == \"\")\r\n {\r\n Print(\"[EdgeRelay] ERROR: API_Key is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n if(AccountID == \"\")\r\n {\r\n Print(\"[EdgeRelay] ERROR: AccountID is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n if(MasterAccountID == \"\")\r\n {\r\n Print(\"[EdgeRelay] ERROR: MasterAccountID is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n\r\n //--- Configure trade object\r\n g_trade.SetExpertMagicNumber(0);\r\n g_trade.SetDeviationInPoints(MaxSlippagePoints);\r\n g_trade.SetTypeFilling(ORDER_FILLING_IOC);\r\n g_trade.SetAsyncMode(false);\r\n\r\n //--- Record starting equity\r\n g_startingEquityToday = AccountInfoDouble(ACCOUNT_EQUITY);\r\n g_startingEquityTotal = g_startingEquityToday;\r\n g_lastDayReset = TimeCurrent();\r\n g_signalsProcessed = 0;\r\n g_signalsFailed = 0;\r\n g_consecutiveErrors = 0;\r\n g_connStatus = STATUS_CONNECTING;\r\n\r\n //--- Initialize display panel\r\n InitDisplayPanel();\r\n\r\n //--- Set poll timer\r\n if(!EventSetMillisecondTimer(PollIntervalMs))\r\n {\r\n Print(\"[EdgeRelay] ERROR: Failed to set timer. Interval=\", PollIntervalMs);\r\n return INIT_FAILED;\r\n }\r\n\r\n Print(\"[EdgeRelay] Follower EA initialized. Account=\", AccountID,\r\n \" Master=\", MasterAccountID, \" Endpoint=\", API_Endpoint);\r\n Print(\"[EdgeRelay] LotMode=\", EnumToString(LotMode),\r\n \" LotValue=\", DoubleToString(LotValue, 2),\r\n \" PollMs=\", PollIntervalMs);\r\n\r\n //--- Initialize PropGuard\r\n if(PropGuard_Enabled)\r\n {\r\n PropGuardRules pgRules;\r\n pgRules.initial_balance = (PropGuard_InitialBalance > 0)\r\n ? PropGuard_InitialBalance\r\n : AccountInfoDouble(ACCOUNT_BALANCE);\r\n pgRules.profit_target_percent = PropGuard_ProfitTarget;\r\n pgRules.max_daily_loss_percent = PropGuard_MaxDailyLoss;\r\n pgRules.daily_loss_calculation = DL_BALANCE_START_OF_DAY;\r\n pgRules.max_total_drawdown_percent = PropGuard_MaxDrawdown;\r\n pgRules.drawdown_type = PropGuard_DDType;\r\n pgRules.trailing_dd_lock_at_breakeven = false;\r\n pgRules.max_lot_size = PropGuard_MaxLotSize;\r\n pgRules.max_open_positions = PropGuard_MaxPositions;\r\n pgRules.max_daily_trades = PropGuard_MaxDailyTrades;\r\n pgRules.min_trading_days = 0;\r\n pgRules.consistency_rule_enabled = false;\r\n pgRules.max_profit_single_day_pct = 30.0;\r\n pgRules.allowed_trading_start = \"00:00\";\r\n pgRules.allowed_trading_end = \"23:59\";\r\n pgRules.block_weekend_holding = PropGuard_BlockWeekend;\r\n pgRules.block_during_news = PropGuard_BlockNews;\r\n pgRules.news_minutes_before = PropGuard_NewsMinBefore;\r\n pgRules.news_minutes_after = PropGuard_NewsMinAfter;\r\n pgRules.warning_threshold_pct = PropGuard_WarnThreshold;\r\n pgRules.critical_threshold_pct = PropGuard_CritThreshold;\r\n pgRules.auto_close_at_critical = PropGuard_AutoClose;\r\n\r\n g_equityTracker.Init(pgRules);\r\n g_propGuard.Init(pgRules, GetPointer(g_equityTracker));\r\n g_propGuardReady = true;\r\n\r\n if(PropGuard_ShowPanel)\r\n g_pgDisplay.Init(PropGuard_PanelX, PropGuard_PanelY, C'26,26,46');\r\n\r\n if(PropGuard_UseCloudRules)\r\n {\r\n string rulesResponse;\r\n int rc = FetchPropGuardRules(API_Gateway, API_Key, AccountID, rulesResponse);\r\n if(rc == 200 && StringLen(rulesResponse) > 5)\r\n Print(\"[PropGuard] Cloud rules fetched successfully.\");\r\n else\r\n Print(\"[PropGuard] Using local fallback rules.\");\r\n }\r\n\r\n Print(\"[PropGuard] Initialized. Preset=\", PropGuard_Preset);\r\n }\r\n\r\n return INIT_SUCCEEDED;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Expert deinitialization |\r\n//+------------------------------------------------------------------+\r\nvoid OnDeinit(const int reason)\r\n {\r\n if(PropGuard_ShowPanel)\r\n g_pgDisplay.Deinit();\r\n EventKillTimer();\r\n CleanupDisplayPanel();\r\n Print(\"[EdgeRelay] Follower EA shutdown. Reason=\", reason,\r\n \" Processed=\", g_signalsProcessed, \" Failed=\", g_signalsFailed);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Timer event - main poll loop |\r\n//+------------------------------------------------------------------+\r\nvoid OnTimer()\r\n {\r\n //--- Reset daily equity at day boundary\r\n MqlDateTime dt;\r\n TimeCurrent(dt);\r\n MqlDateTime dtLast;\r\n TimeToStruct(g_lastDayReset, dtLast);\r\n if(dt.day != dtLast.day)\r\n {\r\n g_startingEquityToday = AccountInfoDouble(ACCOUNT_EQUITY);\r\n g_lastDayReset = TimeCurrent();\r\n Print(\"[EdgeRelay] Daily equity reset: \", DoubleToString(g_startingEquityToday, 2));\r\n }\r\n\r\n //--- Update PropGuard equity state\r\n if(g_propGuardReady)\r\n {\r\n g_equityTracker.Update();\r\n\r\n //--- Check for critical threshold — emergency close\r\n ENUM_PROPGUARD_STATUS pgStatus = g_equityTracker.GetStatus();\r\n if(pgStatus == PG_CRITICAL && PropGuard_AutoClose && !g_propGuard.IsLocked())\r\n {\r\n string reason = \"Critical threshold breached. DD=\" +\r\n DoubleToString(g_equityTracker.GetTotalDrawdownPercent(), 2) + \"%\";\r\n int closed = g_propGuard.EmergencyCloseAll(reason);\r\n\r\n PostEmergencyClose(API_Gateway, API_Key, AccountID, reason,\r\n AccountInfoDouble(ACCOUNT_EQUITY), closed);\r\n\r\n PlaySound(\"alert2.wav\");\r\n Alert(\"[PropGuard] EMERGENCY CLOSE: \", reason);\r\n }\r\n\r\n //--- Cloud sync every 30 seconds\r\n if(g_equityTracker.ShouldSync())\r\n {\r\n string eqJson = g_equityTracker.ToJson();\r\n SyncEquityToCloud(API_Gateway, API_Key, AccountID, eqJson);\r\n }\r\n\r\n //--- Refresh news cache if needed\r\n if(g_propGuard.ShouldRefreshNews())\r\n {\r\n string newsResponse;\r\n int rc = FetchNewsEvents(API_Gateway, API_Key, \"USD,EUR,GBP,JPY,CHF,AUD,NZD,CAD\", newsResponse);\r\n if(rc == 200 && StringLen(newsResponse) > 5)\r\n g_propGuard.UpdateNewsCache(newsResponse);\r\n }\r\n\r\n //--- Update PropGuard display\r\n if(PropGuard_ShowPanel)\r\n g_pgDisplay.Update(GetPointer(g_equityTracker), GetPointer(g_propGuard),\r\n PropGuard_Preset, (int)g_connStatus, -1);\r\n }\r\n\r\n //--- Poll for signals\r\n Signal signals[];\r\n int count = 0;\r\n if(!PollForSignals(signals, count))\r\n {\r\n g_consecutiveErrors++;\r\n g_connStatus = STATUS_ERROR;\r\n\r\n if(g_consecutiveErrors > 10 && !AutoReconnect)\r\n {\r\n Print(\"[EdgeRelay] Too many consecutive errors. AutoReconnect=false. Stopping.\");\r\n EventKillTimer();\r\n return;\r\n }\r\n\r\n UpdateDisplayPanel();\r\n return;\r\n }\r\n\r\n //--- Connected successfully\r\n g_connStatus = STATUS_CONNECTED;\r\n g_consecutiveErrors = 0;\r\n\r\n //--- Process each signal\r\n for(int i = 0; i < count; i++)\r\n {\r\n //--- Map symbol with suffix\r\n string mappedSymbol = signals[i].symbol + SymbolSuffix;\r\n signals[i].symbol = mappedSymbol;\r\n\r\n //--- Check if symbol exists\r\n if(!SymbolSelect(mappedSymbol, true))\r\n {\r\n Print(\"[EdgeRelay] Symbol not found: \", mappedSymbol);\r\n ExecutionResult exRes;\r\n exRes.signal_id = signals[i].signal_id;\r\n exRes.success = false;\r\n exRes.error_message = \"Symbol not found: \" + mappedSymbol;\r\n exRes.retcode = 0;\r\n exRes.ticket = 0;\r\n exRes.executed_price = 0;\r\n exRes.executed_volume = 0;\r\n exRes.slippage = 0;\r\n ReportExecution(exRes);\r\n g_signalsFailed++;\r\n continue;\r\n }\r\n\r\n //--- Filter by direction\r\n if(!ShouldCopySignal(signals[i]))\r\n {\r\n Print(\"[EdgeRelay] Signal filtered: \", signals[i].signal_id);\r\n continue;\r\n }\r\n\r\n //--- Calculate lot size (needed before PropGuard check)\r\n double lot = CalculateLotSize(signals[i]);\r\n\r\n //--- Check PropGuard\r\n if(g_propGuardReady)\r\n {\r\n double evalPrice = (signals[i].price > 0) ? signals[i].price : SymbolInfoDouble(mappedSymbol, SYMBOL_ASK);\r\n PropGuardVerdict pgVerdict = g_propGuard.EvaluateTrade(\r\n mappedSymbol, signals[i].order_type, lot, evalPrice, signals[i].sl, signals[i].tp);\r\n\r\n if(!pgVerdict.allowed)\r\n {\r\n Print(\"[PropGuard] BLOCKED: \", pgVerdict.blocked_rule, \" - \", pgVerdict.blocked_reason);\r\n\r\n PostBlockedTrade(API_Gateway, API_Key, AccountID,\r\n pgVerdict.blocked_rule, pgVerdict.blocked_reason,\r\n ActionToString(signals[i].action), mappedSymbol, lot, evalPrice,\r\n pgVerdict.current_daily_loss_pct, pgVerdict.current_drawdown_pct,\r\n AccountInfoDouble(ACCOUNT_EQUITY));\r\n\r\n ExecutionResult exRes;\r\n exRes.signal_id = signals[i].signal_id;\r\n exRes.success = false;\r\n exRes.error_message = \"PropGuard: \" + pgVerdict.blocked_reason;\r\n exRes.retcode = 0;\r\n exRes.ticket = 0;\r\n exRes.executed_price = 0;\r\n exRes.executed_volume = 0;\r\n exRes.slippage = 0;\r\n ReportExecution(exRes);\r\n g_signalsFailed++;\r\n continue;\r\n }\r\n }\r\n else\r\n {\r\n //--- Fallback to simple equity guard if PropGuard not initialized\r\n EquityGuardResult guard = CheckEquityGuard();\r\n if(!guard.allowed)\r\n {\r\n Print(\"[EdgeRelay] Equity guard blocked: \", guard.reason);\r\n ExecutionResult exRes;\r\n exRes.signal_id = signals[i].signal_id;\r\n exRes.success = false;\r\n exRes.error_message = \"Equity guard: \" + guard.reason;\r\n exRes.retcode = 0;\r\n exRes.ticket = 0;\r\n exRes.executed_price = 0;\r\n exRes.executed_volume = 0;\r\n exRes.slippage = 0;\r\n ReportExecution(exRes);\r\n g_signalsFailed++;\r\n continue;\r\n }\r\n }\r\n\r\n //--- Invert direction if enabled\r\n if(InvertDirection)\r\n signals[i].order_type = InvertOrderType(signals[i].order_type);\r\n\r\n //--- Execute the signal\r\n ExecutionResult execResult = ExecuteSignal(signals[i], lot);\r\n\r\n //--- Report result\r\n ReportExecution(execResult);\r\n\r\n if(execResult.success)\r\n g_signalsProcessed++;\r\n else\r\n g_signalsFailed++;\r\n }\r\n\r\n //--- Update display\r\n UpdateDisplayPanel();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Trade transaction handler — detect manual trades |\r\n//+------------------------------------------------------------------+\r\nvoid OnTradeTransaction(const MqlTradeTransaction &trans,\r\n const MqlTradeRequest &request,\r\n const MqlTradeResult &result)\r\n {\r\n if(!g_propGuardReady || !PropGuard_Enabled)\r\n return;\r\n\r\n if(trans.type == TRADE_TRANSACTION_DEAL_ADD)\r\n {\r\n g_equityTracker.Update();\r\n g_equityTracker.OnTradeExecuted();\r\n\r\n ENUM_PROPGUARD_STATUS status = g_equityTracker.GetStatus();\r\n if(status == PG_CRITICAL && PropGuard_AutoClose && !g_propGuard.IsLocked())\r\n {\r\n string reason = \"Manual trade triggered critical threshold. DD=\" +\r\n DoubleToString(g_equityTracker.GetTotalDrawdownPercent(), 2) + \"%\";\r\n int closed = g_propGuard.EmergencyCloseAll(reason);\r\n\r\n PostEmergencyClose(API_Gateway, API_Key, AccountID, reason,\r\n AccountInfoDouble(ACCOUNT_EQUITY), closed);\r\n\r\n PlaySound(\"alert2.wav\");\r\n Alert(\"[PropGuard] EMERGENCY: \", reason);\r\n }\r\n }\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Poll for signals via HTTP GET |\r\n//+------------------------------------------------------------------+\r\nbool PollForSignals(Signal &signals[], int &count)\r\n {\r\n count = 0;\r\n\r\n string url = API_Endpoint + \"/v1/poll/\" + AccountID;\r\n string headers = \"Content-Type: application/json\\r\\nX-API-Key: \" + API_Key + \"\\r\\n\";\r\n int timeout = HTTP_TIMEOUT;\r\n\r\n char postData[];\r\n char resultData[];\r\n string resultHeaders;\r\n\r\n int res = WebRequest(\"GET\", url, headers, timeout, postData, resultData, resultHeaders);\r\n\r\n if(res == -1)\r\n {\r\n int err = GetLastError();\r\n if(err == 4060)\r\n Print(\"[EdgeRelay] WebRequest failed: Add \", API_Endpoint,\r\n \" to Tools > Options > Expert Advisors > Allow WebRequest for listed URL\");\r\n else\r\n Print(\"[EdgeRelay] WebRequest failed. Error=\", err);\r\n return false;\r\n }\r\n\r\n if(res != 200)\r\n {\r\n Print(\"[EdgeRelay] Poll returned HTTP \", res);\r\n return false;\r\n }\r\n\r\n string response = CharArrayToString(resultData, 0, WHOLE_ARRAY, CP_UTF8);\r\n\r\n if(StringLen(response) < 5)\r\n return true; // Empty or minimal response, no signals\r\n\r\n //--- Parse the response\r\n return ParsePollResponse(response, signals, count);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Parse poll response JSON into signal array |\r\n//+------------------------------------------------------------------+\r\nbool ParsePollResponse(const string &json, Signal &signals[], int &count)\r\n {\r\n count = 0;\r\n\r\n CJsonParser parser;\r\n if(!parser.Parse(json))\r\n {\r\n Print(\"[EdgeRelay] Failed to parse poll response JSON\");\r\n return false;\r\n }\r\n\r\n int arrSize = parser.GetArraySize(\"signals\");\r\n if(arrSize <= 0)\r\n return true; // No signals\r\n\r\n if(arrSize > MAX_SIGNALS_PER_POLL)\r\n arrSize = MAX_SIGNALS_PER_POLL;\r\n\r\n ArrayResize(signals, arrSize);\r\n\r\n for(int i = 0; i < arrSize; i++)\r\n {\r\n string elemJson = parser.GetArrayElementByKey(\"signals\", i);\r\n if(elemJson == \"\")\r\n continue;\r\n\r\n CJsonParser ep;\r\n if(!ep.Parse(elemJson))\r\n continue;\r\n\r\n signals[count].signal_id = ep.GetString(\"signal_id\");\r\n signals[count].account_id = ep.GetString(\"account_id\");\r\n signals[count].sequence_num = ep.GetInt(\"sequence_num\");\r\n signals[count].symbol = ep.GetString(\"symbol\");\r\n signals[count].volume = ep.GetDouble(\"volume\");\r\n signals[count].price = ep.GetDouble(\"price\");\r\n signals[count].sl = ep.GetDouble(\"sl\");\r\n signals[count].tp = ep.GetDouble(\"tp\");\r\n signals[count].magic_number = ep.GetLong(\"magic_number\");\r\n signals[count].ticket = ep.GetLong(\"ticket\");\r\n signals[count].comment = ep.GetString(\"comment\");\r\n signals[count].timestamp = (datetime)ep.GetLong(\"timestamp\");\r\n signals[count].hmac_signature = ep.GetString(\"hmac_signature\");\r\n\r\n //--- Parse action\r\n string actionStr = ep.GetString(\"action\");\r\n signals[count].action = ParseAction(actionStr);\r\n\r\n //--- Parse order type\r\n string typeStr = ep.GetString(\"order_type\");\r\n signals[count].order_type = ParseOrderType(typeStr);\r\n\r\n count++;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Parse action string to enum |\r\n//+------------------------------------------------------------------+\r\nENUM_SIGNAL_ACTION ParseAction(const string &actionStr)\r\n {\r\n if(actionStr == \"OPEN\" || actionStr == \"open\") return SIGNAL_OPEN;\r\n if(actionStr == \"MODIFY\" || actionStr == \"modify\") return SIGNAL_MODIFY;\r\n if(actionStr == \"PARTIAL_CLOSE\" || actionStr == \"partial_close\") return SIGNAL_PARTIAL_CLOSE;\r\n if(actionStr == \"CLOSE\" || actionStr == \"close\") return SIGNAL_CLOSE;\r\n if(actionStr == \"PENDING\" || actionStr == \"pending\") return SIGNAL_PENDING;\r\n if(actionStr == \"CANCEL_PENDING\" || actionStr == \"cancel_pending\") return SIGNAL_CANCEL_PENDING;\r\n return SIGNAL_OPEN;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Parse order type string to enum |\r\n//+------------------------------------------------------------------+\r\nENUM_ORDER_TYPE ParseOrderType(const string &typeStr)\r\n {\r\n if(typeStr == \"BUY\" || typeStr == \"buy\") return ORDER_TYPE_BUY;\r\n if(typeStr == \"SELL\" || typeStr == \"sell\") return ORDER_TYPE_SELL;\r\n if(typeStr == \"BUY_LIMIT\" || typeStr == \"buy_limit\") return ORDER_TYPE_BUY_LIMIT;\r\n if(typeStr == \"SELL_LIMIT\" || typeStr == \"sell_limit\") return ORDER_TYPE_SELL_LIMIT;\r\n if(typeStr == \"BUY_STOP\" || typeStr == \"buy_stop\") return ORDER_TYPE_BUY_STOP;\r\n if(typeStr == \"SELL_STOP\" || typeStr == \"sell_stop\") return ORDER_TYPE_SELL_STOP;\r\n if(typeStr == \"BUY_STOP_LIMIT\" || typeStr == \"buy_stop_limit\") return ORDER_TYPE_BUY_STOP_LIMIT;\r\n if(typeStr == \"SELL_STOP_LIMIT\" || typeStr == \"sell_stop_limit\") return ORDER_TYPE_SELL_STOP_LIMIT;\r\n return ORDER_TYPE_BUY;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute a signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecuteSignal(Signal &signal, double lot)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n string comment = COMMENT_PREFIX + IntegerToString(signal.ticket) + \":\" + signal.signal_id;\r\n\r\n switch(signal.action)\r\n {\r\n case SIGNAL_OPEN:\r\n execResult = ExecuteOpen(signal, lot, comment);\r\n break;\r\n\r\n case SIGNAL_MODIFY:\r\n execResult = ExecuteModify(signal);\r\n break;\r\n\r\n case SIGNAL_CLOSE:\r\n execResult = ExecuteClose(signal);\r\n break;\r\n\r\n case SIGNAL_PARTIAL_CLOSE:\r\n execResult = ExecutePartialClose(signal);\r\n break;\r\n\r\n case SIGNAL_PENDING:\r\n execResult = ExecutePending(signal, lot, comment);\r\n break;\r\n\r\n case SIGNAL_CANCEL_PENDING:\r\n execResult = ExecuteCancelPending(signal);\r\n break;\r\n\r\n default:\r\n execResult.error_message = \"Unknown action\";\r\n break;\r\n }\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute OPEN signal (market order) |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecuteOpen(Signal &signal, double lot, string comment)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n //--- Normalize lot\r\n lot = NormalizeLot(signal.symbol, lot);\r\n if(lot <= 0.0)\r\n {\r\n execResult.error_message = \"Invalid lot size after normalization\";\r\n return execResult;\r\n }\r\n\r\n //--- Get current price\r\n double price = 0.0;\r\n if(signal.order_type == ORDER_TYPE_BUY)\r\n price = SymbolInfoDouble(signal.symbol, SYMBOL_ASK);\r\n else if(signal.order_type == ORDER_TYPE_SELL)\r\n price = SymbolInfoDouble(signal.symbol, SYMBOL_BID);\r\n else\r\n {\r\n execResult.error_message = \"Invalid order type for market open: \" + OrderTypeToStr(signal.order_type);\r\n return execResult;\r\n }\r\n\r\n //--- Normalize SL/TP\r\n int digits = (int)SymbolInfoInteger(signal.symbol, SYMBOL_DIGITS);\r\n double sl = NormalizeDouble(signal.sl, digits);\r\n double tp = NormalizeDouble(signal.tp, digits);\r\n\r\n //--- Send order\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_DEAL;\r\n tradeRequest.symbol = signal.symbol;\r\n tradeRequest.volume = lot;\r\n tradeRequest.type = signal.order_type;\r\n tradeRequest.price = price;\r\n tradeRequest.sl = sl;\r\n tradeRequest.tp = tp;\r\n tradeRequest.deviation = (ulong)MaxSlippagePoints;\r\n tradeRequest.magic = (ulong)signal.magic_number;\r\n tradeRequest.comment = comment;\r\n tradeRequest.type_filling = ORDER_FILLING_IOC;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"OrderSend failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] OPEN failed: \", execResult.error_message,\r\n \" Symbol=\", signal.symbol, \" Lot=\", lot);\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE && tradeResult.retcode != TRADE_RETCODE_PLACED)\r\n {\r\n execResult.error_message = \"OrderSend retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] OPEN retcode: \", tradeResult.retcode, \" Signal=\", signal.signal_id);\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = tradeResult.order;\r\n execResult.executed_price = tradeResult.price;\r\n execResult.executed_volume = tradeResult.volume;\r\n execResult.retcode = tradeResult.retcode;\r\n\r\n //--- Calculate slippage\r\n double pointSize = SymbolInfoDouble(signal.symbol, SYMBOL_POINT);\r\n if(pointSize > 0)\r\n execResult.slippage = (int)MathRound(MathAbs(tradeResult.price - price) / pointSize);\r\n\r\n Print(\"[EdgeRelay] OPEN success: Ticket=\", execResult.ticket,\r\n \" \", OrderTypeToStr(signal.order_type),\r\n \" \", signal.symbol, \" Lot=\", lot,\r\n \" Price=\", tradeResult.price);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute MODIFY signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecuteModify(Signal &signal)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n //--- Find position: try magic → master ticket in comment → signal_id in comment\r\n ulong posTicket = FindPositionByMagic(signal.magic_number);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByMasterTicket(signal.ticket);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByComment(COMMENT_PREFIX + signal.signal_id);\r\n\r\n if(posTicket == 0)\r\n {\r\n execResult.error_message = \"Position not found for modify. Magic=\" +\r\n IntegerToString(signal.magic_number);\r\n Print(\"[EdgeRelay] MODIFY failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n //--- Normalize SL/TP\r\n int digits = (int)SymbolInfoInteger(signal.symbol, SYMBOL_DIGITS);\r\n double sl = NormalizeDouble(signal.sl, digits);\r\n double tp = NormalizeDouble(signal.tp, digits);\r\n\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_SLTP;\r\n tradeRequest.position = posTicket;\r\n tradeRequest.symbol = signal.symbol;\r\n tradeRequest.sl = sl;\r\n tradeRequest.tp = tp;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"Modify failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] MODIFY failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE)\r\n {\r\n execResult.error_message = \"Modify retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = posTicket;\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] MODIFY success: Ticket=\", posTicket,\r\n \" SL=\", sl, \" TP=\", tp);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute CLOSE signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecuteClose(Signal &signal)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n //--- Find position: try magic → master ticket in comment → signal_id in comment\r\n ulong posTicket = FindPositionByMagic(signal.magic_number);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByMasterTicket(signal.ticket);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByComment(COMMENT_PREFIX + signal.signal_id);\r\n\r\n if(posTicket == 0)\r\n {\r\n execResult.error_message = \"Position not found for close. Magic=\" +\r\n IntegerToString(signal.magic_number);\r\n Print(\"[EdgeRelay] CLOSE failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n //--- Select position and get details\r\n if(!PositionSelectByTicket(posTicket))\r\n {\r\n execResult.error_message = \"Cannot select position: \" + IntegerToString((long)posTicket);\r\n return execResult;\r\n }\r\n\r\n double volume = PositionGetDouble(POSITION_VOLUME);\r\n ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);\r\n string symbol = PositionGetString(POSITION_SYMBOL);\r\n\r\n ENUM_ORDER_TYPE closeType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;\r\n double price = (closeType == ORDER_TYPE_SELL)\r\n ? SymbolInfoDouble(symbol, SYMBOL_BID)\r\n : SymbolInfoDouble(symbol, SYMBOL_ASK);\r\n\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_DEAL;\r\n tradeRequest.position = posTicket;\r\n tradeRequest.symbol = symbol;\r\n tradeRequest.volume = volume;\r\n tradeRequest.type = closeType;\r\n tradeRequest.price = price;\r\n tradeRequest.deviation = (ulong)MaxSlippagePoints;\r\n tradeRequest.type_filling = ORDER_FILLING_IOC;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"Close OrderSend failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] CLOSE failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE)\r\n {\r\n execResult.error_message = \"Close retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = posTicket;\r\n execResult.executed_price = tradeResult.price;\r\n execResult.executed_volume = volume;\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] CLOSE success: Ticket=\", posTicket,\r\n \" Vol=\", volume, \" Price=\", tradeResult.price);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute PARTIAL_CLOSE signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecutePartialClose(Signal &signal)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n //--- Find position: try magic → master ticket in comment → signal_id in comment\r\n ulong posTicket = FindPositionByMagic(signal.magic_number);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByMasterTicket(signal.ticket);\r\n if(posTicket == 0)\r\n posTicket = FindPositionByComment(COMMENT_PREFIX + signal.signal_id);\r\n\r\n if(posTicket == 0)\r\n {\r\n execResult.error_message = \"Position not found for partial close.\";\r\n Print(\"[EdgeRelay] PARTIAL_CLOSE failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n if(!PositionSelectByTicket(posTicket))\r\n {\r\n execResult.error_message = \"Cannot select position: \" + IntegerToString((long)posTicket);\r\n return execResult;\r\n }\r\n\r\n double currentVolume = PositionGetDouble(POSITION_VOLUME);\r\n ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);\r\n string symbol = PositionGetString(POSITION_SYMBOL);\r\n\r\n //--- Determine close volume\r\n double closeVol = signal.volume;\r\n if(closeVol <= 0 || closeVol > currentVolume)\r\n closeVol = currentVolume;\r\n\r\n closeVol = NormalizeLot(symbol, closeVol);\r\n if(closeVol <= 0)\r\n {\r\n execResult.error_message = \"Invalid partial close volume\";\r\n return execResult;\r\n }\r\n\r\n ENUM_ORDER_TYPE closeType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;\r\n double price = (closeType == ORDER_TYPE_SELL)\r\n ? SymbolInfoDouble(symbol, SYMBOL_BID)\r\n : SymbolInfoDouble(symbol, SYMBOL_ASK);\r\n\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_DEAL;\r\n tradeRequest.position = posTicket;\r\n tradeRequest.symbol = symbol;\r\n tradeRequest.volume = closeVol;\r\n tradeRequest.type = closeType;\r\n tradeRequest.price = price;\r\n tradeRequest.deviation = (ulong)MaxSlippagePoints;\r\n tradeRequest.type_filling = ORDER_FILLING_IOC;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"Partial close failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE)\r\n {\r\n execResult.error_message = \"Partial close retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = posTicket;\r\n execResult.executed_price = tradeResult.price;\r\n execResult.executed_volume = closeVol;\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] PARTIAL_CLOSE success: Ticket=\", posTicket,\r\n \" ClosedVol=\", closeVol, \" Remaining=\", currentVolume - closeVol);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute PENDING order signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecutePending(Signal &signal, double lot, string comment)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n lot = NormalizeLot(signal.symbol, lot);\r\n if(lot <= 0.0)\r\n {\r\n execResult.error_message = \"Invalid lot for pending order\";\r\n return execResult;\r\n }\r\n\r\n int digits = (int)SymbolInfoInteger(signal.symbol, SYMBOL_DIGITS);\r\n\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_PENDING;\r\n tradeRequest.symbol = signal.symbol;\r\n tradeRequest.volume = lot;\r\n tradeRequest.type = signal.order_type;\r\n tradeRequest.price = NormalizeDouble(signal.price, digits);\r\n tradeRequest.sl = NormalizeDouble(signal.sl, digits);\r\n tradeRequest.tp = NormalizeDouble(signal.tp, digits);\r\n tradeRequest.magic = (ulong)signal.magic_number;\r\n tradeRequest.comment = comment;\r\n tradeRequest.type_filling = ORDER_FILLING_IOC;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"Pending OrderSend failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] PENDING failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE && tradeResult.retcode != TRADE_RETCODE_PLACED)\r\n {\r\n execResult.error_message = \"Pending retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = tradeResult.order;\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] PENDING success: Order=\", execResult.ticket,\r\n \" Type=\", OrderTypeToStr(signal.order_type),\r\n \" Price=\", signal.price, \" Lot=\", lot);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Execute CANCEL_PENDING signal |\r\n//+------------------------------------------------------------------+\r\nExecutionResult ExecuteCancelPending(Signal &signal)\r\n {\r\n ExecutionResult execResult;\r\n execResult.signal_id = signal.signal_id;\r\n execResult.success = false;\r\n execResult.ticket = 0;\r\n execResult.executed_price = 0.0;\r\n execResult.executed_volume = 0.0;\r\n execResult.slippage = 0;\r\n execResult.error_message = \"\";\r\n execResult.retcode = 0;\r\n\r\n //--- Find the pending order by magic or comment\r\n ulong orderTicket = FindPendingOrderByMagic(signal.magic_number);\r\n if(orderTicket == 0)\r\n orderTicket = FindPendingOrderByComment(COMMENT_PREFIX + IntegerToString(signal.ticket));\r\n\r\n if(orderTicket == 0)\r\n {\r\n execResult.error_message = \"Pending order not found for cancel.\";\r\n Print(\"[EdgeRelay] CANCEL_PENDING failed: \", execResult.error_message);\r\n return execResult;\r\n }\r\n\r\n MqlTradeRequest tradeRequest = {};\r\n MqlTradeResult tradeResult = {};\r\n\r\n tradeRequest.action = TRADE_ACTION_REMOVE;\r\n tradeRequest.order = orderTicket;\r\n\r\n if(!OrderSend(tradeRequest, tradeResult))\r\n {\r\n execResult.error_message = \"Cancel pending failed: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n if(tradeResult.retcode != TRADE_RETCODE_DONE)\r\n {\r\n execResult.error_message = \"Cancel retcode: \" + IntegerToString(tradeResult.retcode);\r\n execResult.retcode = tradeResult.retcode;\r\n return execResult;\r\n }\r\n\r\n execResult.success = true;\r\n execResult.ticket = orderTicket;\r\n execResult.retcode = tradeResult.retcode;\r\n Print(\"[EdgeRelay] CANCEL_PENDING success: Order=\", orderTicket);\r\n\r\n return execResult;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check equity guard |\r\n//+------------------------------------------------------------------+\r\nEquityGuardResult CheckEquityGuard()\r\n {\r\n EquityGuardResult guard;\r\n guard.allowed = true;\r\n guard.reason = \"\";\r\n\r\n double equity = AccountInfoDouble(ACCOUNT_EQUITY);\r\n\r\n //--- Daily loss check\r\n if(g_startingEquityToday > 0)\r\n {\r\n double dailyLoss = g_startingEquityToday - equity;\r\n double dailyPct = (dailyLoss / g_startingEquityToday) * 100.0;\r\n\r\n if(dailyPct >= MaxDailyLossPercent)\r\n {\r\n guard.allowed = false;\r\n guard.reason = StringFormat(\"Daily loss %.2f%% exceeds max %.2f%%\",\r\n dailyPct, MaxDailyLossPercent);\r\n return guard;\r\n }\r\n }\r\n\r\n //--- Total drawdown check\r\n if(g_startingEquityTotal > 0)\r\n {\r\n double totalDrawdown = g_startingEquityTotal - equity;\r\n double totalPct = (totalDrawdown / g_startingEquityTotal) * 100.0;\r\n\r\n if(totalPct >= MaxTotalDrawdownPercent)\r\n {\r\n guard.allowed = false;\r\n guard.reason = StringFormat(\"Total drawdown %.2f%% exceeds max %.2f%%\",\r\n totalPct, MaxTotalDrawdownPercent);\r\n return guard;\r\n }\r\n }\r\n\r\n return guard;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Report execution result via HTTP POST |\r\n//+------------------------------------------------------------------+\r\nvoid ReportExecution(ExecutionResult &exResult)\r\n {\r\n string url = API_Endpoint + \"/v1/execution\";\r\n string headers = \"Content-Type: application/json\\r\\nX-API-Key: \" + API_Key + \"\\r\\n\";\r\n\r\n //--- Build JSON body\r\n string body = \"{\";\r\n body += \"\\\"signal_id\\\":\\\"\" + JsonEscape(exResult.signal_id) + \"\\\",\";\r\n body += \"\\\"account_id\\\":\\\"\" + JsonEscape(AccountID) + \"\\\",\";\r\n body += \"\\\"success\\\":\" + (exResult.success ? \"true\" : \"false\") + \",\";\r\n body += \"\\\"ticket\\\":\" + IntegerToString((long)exResult.ticket) + \",\";\r\n body += \"\\\"executed_price\\\":\" + DoubleToString(exResult.executed_price, 5) + \",\";\r\n body += \"\\\"executed_volume\\\":\" + DoubleToString(exResult.executed_volume, 8) + \",\";\r\n body += \"\\\"slippage\\\":\" + IntegerToString(exResult.slippage) + \",\";\r\n body += \"\\\"retcode\\\":\" + IntegerToString(exResult.retcode) + \",\";\r\n body += \"\\\"error_message\\\":\\\"\" + JsonEscape(exResult.error_message) + \"\\\",\";\r\n body += \"\\\"timestamp\\\":\" + IntegerToString((long)TimeCurrent());\r\n body += \"}\";\r\n\r\n char postData[];\r\n StringToCharArray(body, postData, 0, StringLen(body));\r\n\r\n char resultData[];\r\n string resultHeaders;\r\n\r\n //--- Fire and forget - log errors but don't block\r\n int res = WebRequest(\"POST\", url, headers, HTTP_TIMEOUT, postData, resultData, resultHeaders);\r\n if(res != 200 && res != 201 && res != -1)\r\n {\r\n Print(\"[EdgeRelay] Report execution HTTP \", res, \" for signal=\", exResult.signal_id);\r\n }\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Calculate lot size based on LotMode |\r\n//+------------------------------------------------------------------+\r\ndouble CalculateLotSize(Signal &signal)\r\n {\r\n double lot = 0.0;\r\n\r\n switch(LotMode)\r\n {\r\n case LOT_MIRROR:\r\n lot = signal.volume;\r\n break;\r\n\r\n case LOT_FIXED:\r\n lot = LotValue;\r\n break;\r\n\r\n case LOT_MULTIPLIER:\r\n lot = signal.volume * LotValue;\r\n break;\r\n\r\n case LOT_RISK_PERCENT:\r\n {\r\n //--- Risk-based: risk LotValue% of balance per trade\r\n double balance = AccountInfoDouble(ACCOUNT_BALANCE);\r\n double riskMoney = balance * (LotValue / 100.0);\r\n\r\n //--- Calculate risk in money per lot from SL distance\r\n double point = SymbolInfoDouble(signal.symbol, SYMBOL_POINT);\r\n double tickValue = SymbolInfoDouble(signal.symbol, SYMBOL_TRADE_TICK_VALUE);\r\n double tickSize = SymbolInfoDouble(signal.symbol, SYMBOL_TRADE_TICK_SIZE);\r\n\r\n if(signal.sl > 0 && point > 0 && tickValue > 0 && tickSize > 0)\r\n {\r\n double price = signal.price;\r\n if(price <= 0)\r\n {\r\n if(signal.order_type == ORDER_TYPE_BUY)\r\n price = SymbolInfoDouble(signal.symbol, SYMBOL_ASK);\r\n else\r\n price = SymbolInfoDouble(signal.symbol, SYMBOL_BID);\r\n }\r\n\r\n double slDistance = MathAbs(price - signal.sl);\r\n double riskPerLot = (slDistance / tickSize) * tickValue;\r\n\r\n if(riskPerLot > 0)\r\n lot = riskMoney / riskPerLot;\r\n else\r\n lot = signal.volume; // Fallback to mirror\r\n }\r\n else\r\n {\r\n //--- No SL provided, fallback to mirror\r\n lot = signal.volume;\r\n Print(\"[EdgeRelay] RISK_PERCENT: No SL in signal, falling back to mirror. Signal=\",\r\n signal.signal_id);\r\n }\r\n }\r\n break;\r\n\r\n default:\r\n lot = signal.volume;\r\n break;\r\n }\r\n\r\n return lot;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Normalize lot to symbol constraints |\r\n//+------------------------------------------------------------------+\r\ndouble NormalizeLot(string symbol, double lot)\r\n {\r\n double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);\r\n double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);\r\n double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);\r\n\r\n if(lotStep <= 0)\r\n lotStep = 0.01;\r\n\r\n //--- Round to lot step\r\n lot = MathFloor(lot / lotStep) * lotStep;\r\n\r\n //--- Clamp to min/max\r\n if(lot < minLot)\r\n lot = minLot;\r\n if(lot > maxLot)\r\n lot = maxLot;\r\n\r\n //--- Normalize decimal places\r\n int lotDigits = (int)MathMax(-MathLog10(lotStep), 0);\r\n lot = NormalizeDouble(lot, lotDigits);\r\n\r\n return lot;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Invert order type (buy<->sell) |\r\n//+------------------------------------------------------------------+\r\nENUM_ORDER_TYPE InvertOrderType(ENUM_ORDER_TYPE type)\r\n {\r\n switch(type)\r\n {\r\n case ORDER_TYPE_BUY: return ORDER_TYPE_SELL;\r\n case ORDER_TYPE_SELL: return ORDER_TYPE_BUY;\r\n case ORDER_TYPE_BUY_LIMIT: return ORDER_TYPE_SELL_LIMIT;\r\n case ORDER_TYPE_SELL_LIMIT: return ORDER_TYPE_BUY_LIMIT;\r\n case ORDER_TYPE_BUY_STOP: return ORDER_TYPE_SELL_STOP;\r\n case ORDER_TYPE_SELL_STOP: return ORDER_TYPE_BUY_STOP;\r\n case ORDER_TYPE_BUY_STOP_LIMIT: return ORDER_TYPE_SELL_STOP_LIMIT;\r\n case ORDER_TYPE_SELL_STOP_LIMIT: return ORDER_TYPE_BUY_STOP_LIMIT;\r\n }\r\n return type;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check if signal should be copied based on direction filters |\r\n//+------------------------------------------------------------------+\r\nbool ShouldCopySignal(Signal &signal)\r\n {\r\n //--- Close/modify/cancel always allowed\r\n if(signal.action == SIGNAL_CLOSE || signal.action == SIGNAL_MODIFY ||\r\n signal.action == SIGNAL_PARTIAL_CLOSE || signal.action == SIGNAL_CANCEL_PENDING)\r\n return true;\r\n\r\n //--- Pending filter\r\n if(signal.action == SIGNAL_PENDING && !CopyPendings)\r\n return false;\r\n\r\n //--- Direction filter (consider inversion)\r\n ENUM_ORDER_TYPE type = signal.order_type;\r\n if(InvertDirection)\r\n type = InvertOrderType(type);\r\n\r\n bool isBuy = (type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT ||\r\n type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_BUY_STOP_LIMIT);\r\n bool isSell = (type == ORDER_TYPE_SELL || type == ORDER_TYPE_SELL_LIMIT ||\r\n type == ORDER_TYPE_SELL_STOP || type == ORDER_TYPE_SELL_STOP_LIMIT);\r\n\r\n if(isBuy && !CopyBuys) return false;\r\n if(isSell && !CopySells) return false;\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Find open position by magic number |\r\n//+------------------------------------------------------------------+\r\nulong FindPositionByMagic(long magicNumber)\r\n {\r\n if(magicNumber == 0)\r\n return 0;\r\n\r\n for(int i = PositionsTotal() - 1; i >= 0; i--)\r\n {\r\n ulong ticket = PositionGetTicket(i);\r\n if(ticket == 0)\r\n continue;\r\n\r\n if(PositionGetInteger(POSITION_MAGIC) == magicNumber)\r\n return ticket;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Find open position by comment tag |\r\n//+------------------------------------------------------------------+\r\nulong FindPositionByComment(string tag)\r\n {\r\n if(tag == \"\")\r\n return 0;\r\n\r\n for(int i = PositionsTotal() - 1; i >= 0; i--)\r\n {\r\n ulong ticket = PositionGetTicket(i);\r\n if(ticket == 0)\r\n continue;\r\n\r\n string comment = PositionGetString(POSITION_COMMENT);\r\n if(StringFind(comment, tag) >= 0)\r\n return ticket;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Find position by master ticket embedded in comment (ER:ticket:…) |\r\n//+------------------------------------------------------------------+\r\nulong FindPositionByMasterTicket(long masterTicket)\r\n {\r\n if(masterTicket == 0)\r\n return 0;\r\n\r\n string tag = COMMENT_PREFIX + IntegerToString(masterTicket) + \":\";\r\n\r\n for(int i = PositionsTotal() - 1; i >= 0; i--)\r\n {\r\n ulong ticket = PositionGetTicket(i);\r\n if(ticket == 0)\r\n continue;\r\n\r\n string comment = PositionGetString(POSITION_COMMENT);\r\n if(StringFind(comment, tag) == 0)\r\n return ticket;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Find pending order by magic number |\r\n//+------------------------------------------------------------------+\r\nulong FindPendingOrderByMagic(long magicNumber)\r\n {\r\n if(magicNumber == 0)\r\n return 0;\r\n\r\n for(int i = OrdersTotal() - 1; i >= 0; i--)\r\n {\r\n ulong ticket = OrderGetTicket(i);\r\n if(ticket == 0)\r\n continue;\r\n\r\n if(OrderGetInteger(ORDER_MAGIC) == magicNumber)\r\n return ticket;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Find pending order by comment tag |\r\n//+------------------------------------------------------------------+\r\nulong FindPendingOrderByComment(string tag)\r\n {\r\n if(tag == \"\")\r\n return 0;\r\n\r\n for(int i = OrdersTotal() - 1; i >= 0; i--)\r\n {\r\n ulong ticket = OrderGetTicket(i);\r\n if(ticket == 0)\r\n continue;\r\n\r\n string comment = OrderGetString(ORDER_COMMENT);\r\n if(StringFind(comment, tag) >= 0)\r\n return ticket;\r\n }\r\n\r\n return 0;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Display panel - Initialization |\r\n//+------------------------------------------------------------------+\r\nvoid InitDisplayPanel()\r\n {\r\n string prefix = \"ER_\";\r\n\r\n ObjectCreate(0, prefix + \"bg\", OBJ_RECTANGLE_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_XDISTANCE, 10);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_YDISTANCE, 25);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_XSIZE, 280);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_YSIZE, 180);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_BGCOLOR, clrBlack);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_COLOR, clrDodgerBlue);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_BORDER_TYPE, BORDER_FLAT);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetInteger(0, prefix + \"bg\", OBJPROP_BACK, false);\r\n\r\n CreateFollowerLabel(prefix + \"title\", 15, 30, \"EdgeRelay Follower\", clrDodgerBlue, 10);\r\n CreateFollowerLabel(prefix + \"status\", 15, 50, \"Status: Connecting...\", clrYellow, 9);\r\n CreateFollowerLabel(prefix + \"master\", 15, 70, \"Master: \" + MasterAccountID, clrWhite, 9);\r\n CreateFollowerLabel(prefix + \"lotmode\", 15, 90, \"LotMode: \" + EnumToString(LotMode), clrWhite, 9);\r\n CreateFollowerLabel(prefix + \"signals\", 15, 110, \"Signals: 0 | Failed: 0\", clrWhite, 9);\r\n CreateFollowerLabel(prefix + \"equity\", 15, 130, \"Equity: \" + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), clrWhite, 9);\r\n CreateFollowerLabel(prefix + \"drawdown\", 15, 150, \"Drawdown: 0.00%\", clrWhite, 9);\r\n CreateFollowerLabel(prefix + \"poll\", 15, 170, \"Poll: \" + IntegerToString(PollIntervalMs) + \"ms\", clrGray, 8);\r\n\r\n ChartRedraw();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Create a text label on chart |\r\n//+------------------------------------------------------------------+\r\nvoid CreateFollowerLabel(string name, int x, int y, string text, color clr, int fontSize)\r\n {\r\n ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);\r\n ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);\r\n ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetString(0, name, OBJPROP_TEXT, text);\r\n ObjectSetString(0, name, OBJPROP_FONT, \"Consolas\");\r\n ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);\r\n ObjectSetInteger(0, name, OBJPROP_COLOR, clr);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Update display panel |\r\n//+------------------------------------------------------------------+\r\nvoid UpdateDisplayPanel()\r\n {\r\n string prefix = \"ER_\";\r\n\r\n //--- Status\r\n string statusText = \"Status: \";\r\n color statusClr = clrWhite;\r\n switch(g_connStatus)\r\n {\r\n case STATUS_CONNECTED: statusText += \"Connected\"; statusClr = clrLime; break;\r\n case STATUS_DISCONNECTED: statusText += \"Disconnected\"; statusClr = clrRed; break;\r\n case STATUS_CONNECTING: statusText += \"Connecting...\";statusClr = clrYellow; break;\r\n case STATUS_ERROR: statusText += \"Error (\" + IntegerToString(g_consecutiveErrors) + \")\";\r\n statusClr = clrOrangeRed; break;\r\n }\r\n ObjectSetString(0, prefix + \"status\", OBJPROP_TEXT, statusText);\r\n ObjectSetInteger(0, prefix + \"status\", OBJPROP_COLOR, statusClr);\r\n\r\n //--- Signals\r\n ObjectSetString(0, prefix + \"signals\", OBJPROP_TEXT,\r\n \"Signals: \" + IntegerToString(g_signalsProcessed) +\r\n \" | Failed: \" + IntegerToString(g_signalsFailed));\r\n\r\n //--- Equity & Drawdown\r\n double equity = AccountInfoDouble(ACCOUNT_EQUITY);\r\n ObjectSetString(0, prefix + \"equity\", OBJPROP_TEXT,\r\n \"Equity: \" + DoubleToString(equity, 2));\r\n\r\n double ddPct = 0;\r\n if(g_startingEquityTotal > 0)\r\n ddPct = ((g_startingEquityTotal - equity) / g_startingEquityTotal) * 100.0;\r\n\r\n color ddClr = clrWhite;\r\n if(ddPct > MaxTotalDrawdownPercent * 0.75) ddClr = clrOrangeRed;\r\n else if(ddPct > MaxTotalDrawdownPercent * 0.5) ddClr = clrYellow;\r\n\r\n ObjectSetString(0, prefix + \"drawdown\", OBJPROP_TEXT,\r\n \"Drawdown: \" + DoubleToString(ddPct, 2) + \"%\");\r\n ObjectSetInteger(0, prefix + \"drawdown\", OBJPROP_COLOR, ddClr);\r\n\r\n ChartRedraw();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Cleanup display panel |\r\n//+------------------------------------------------------------------+\r\nvoid CleanupDisplayPanel()\r\n {\r\n string prefix = \"ER_\";\r\n ObjectDelete(0, prefix + \"bg\");\r\n ObjectDelete(0, prefix + \"title\");\r\n ObjectDelete(0, prefix + \"status\");\r\n ObjectDelete(0, prefix + \"master\");\r\n ObjectDelete(0, prefix + \"lotmode\");\r\n ObjectDelete(0, prefix + \"signals\");\r\n ObjectDelete(0, prefix + \"equity\");\r\n ObjectDelete(0, prefix + \"drawdown\");\r\n ObjectDelete(0, prefix + \"poll\");\r\n ChartRedraw();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| OnTick - not used, polling is timer-based |\r\n//+------------------------------------------------------------------+\r\nvoid OnTick()\r\n {\r\n // No action - all polling handled in OnTimer\r\n }\r\n//+------------------------------------------------------------------+\r\n", + "EdgeRelay_Http.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Http.mqh |\r\n//| EdgeRelay Master EA - HTTP Communication |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_HTTP_MQH\r\n#define EDGERELAY_HTTP_MQH\r\n\r\n#include \r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| Send a trade signal via HTTP POST |\r\n//| Returns HTTP status code, or -1 on failure |\r\n//+------------------------------------------------------------------+\r\nint SendSignal(string endpoint, string apiKey, Signal &signal)\r\n {\r\n string url = endpoint + \"/v1/ingest\";\r\n string headers = \"Content-Type: application/json\\r\\n\"\r\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n //--- Build JSON body\r\n string jsonBody = SignalToJson(signal);\r\n\r\n //--- Convert to char array for WebRequest\r\n char postData[];\r\n StringToCharArray(jsonBody, postData, 0, StringLen(jsonBody));\r\n\r\n //--- Response buffers\r\n char result[];\r\n string resultHeaders;\r\n\r\n //--- Reset error\r\n ResetLastError();\r\n\r\n int statusCode = WebRequest(\r\n \"POST\",\r\n url,\r\n headers,\r\n 5000, // 5 second timeout\r\n postData,\r\n result,\r\n resultHeaders\r\n );\r\n\r\n if(statusCode == -1)\r\n {\r\n int err = GetLastError();\r\n PrintFormat(\"[EdgeRelay] SendSignal failed: error=%d, url=%s. \"\r\n \"Ensure URL is added to Tools > Options > Expert Advisors > Allow WebRequest.\",\r\n err, url);\r\n return -1;\r\n }\r\n\r\n if(statusCode != 200 && statusCode != 201)\r\n {\r\n string responseBody = CharArrayToString(result);\r\n PrintFormat(\"[EdgeRelay] SendSignal HTTP %d: %s\", statusCode, responseBody);\r\n }\r\n\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Send heartbeat to server |\r\n//| Returns HTTP status code, or -1 on failure |\r\n//+------------------------------------------------------------------+\r\nint SendHeartbeat(string endpoint, string apiKey, string accountId, string apiSecret)\r\n {\r\n string url = endpoint + \"/v1/heartbeat\";\r\n string headers = \"Content-Type: application/json\\r\\n\"\r\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n //--- Build heartbeat payload\r\n long ts = (long)TimeCurrent();\r\n string tsStr = IntegerToString(ts);\r\n\r\n //--- Sign the heartbeat: HMAC of sorted JSON (must match worker's verifyHmacSignature)\r\n string sigPayload = \"{\\\"account_id\\\":\\\"\" + accountId + \"\\\",\\\"timestamp\\\":\" + tsStr + \"}\";\r\n string hmac = HmacSha256(sigPayload, apiSecret);\r\n\r\n //--- Build JSON\r\n string jsonBody = \"{\";\r\n jsonBody += \"\\\"account_id\\\":\\\"\" + JsonEscape(accountId) + \"\\\",\";\r\n jsonBody += \"\\\"timestamp\\\":\" + tsStr + \",\";\r\n jsonBody += \"\\\"hmac_signature\\\":\\\"\" + hmac + \"\\\"\";\r\n jsonBody += \"}\";\r\n\r\n //--- Convert to char array\r\n char postData[];\r\n StringToCharArray(jsonBody, postData, 0, StringLen(jsonBody));\r\n\r\n //--- Response buffers\r\n char result[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n\r\n int statusCode = WebRequest(\r\n \"POST\",\r\n url,\r\n headers,\r\n 5000,\r\n postData,\r\n result,\r\n resultHeaders\r\n );\r\n\r\n if(statusCode == -1)\r\n {\r\n int err = GetLastError();\r\n PrintFormat(\"[EdgeRelay] SendHeartbeat failed: error=%d\", err);\r\n return -1;\r\n }\r\n\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check if the EdgeRelay server is reachable |\r\n//| Quick GET to /v1/health, returns true if HTTP 200 |\r\n//+------------------------------------------------------------------+\r\nbool IsServerReachable(string endpoint)\r\n {\r\n string url = endpoint + \"/v1/health\";\r\n string headers = \"\";\r\n\r\n char postData[];\r\n char result[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n\r\n int statusCode = WebRequest(\r\n \"GET\",\r\n url,\r\n headers,\r\n 3000, // 3 second timeout for health check\r\n postData,\r\n result,\r\n resultHeaders\r\n );\r\n\r\n return (statusCode == 200);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Sync equity state to cloud |\r\n//| POST /v1/propguard/equity/:accountId |\r\n//+------------------------------------------------------------------+\r\nint SyncEquityToCloud(string endpoint, string apiKey, string accountId, string equityJson)\r\n {\r\n string url = endpoint + \"/v1/propguard/equity/\" + accountId;\r\n string headers = \"Content-Type: application/json\\r\\n\"\r\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n char postData[];\r\n StringToCharArray(equityJson, postData, 0, StringLen(equityJson));\r\n\r\n char result[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n int statusCode = WebRequest(\"POST\", url, headers, 3000, postData, result, resultHeaders);\r\n\r\n if(statusCode == -1)\r\n {\r\n int err = GetLastError();\r\n PrintFormat(\"[PropGuard] SyncEquity failed: error=%d\", err);\r\n }\r\n\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Post blocked trade to cloud |\r\n//| POST /v1/propguard/blocked/:accountId |\r\n//+------------------------------------------------------------------+\r\nint PostBlockedTrade(string endpoint, string apiKey, string accountId,\r\n string ruleViolated, string ruleDetails,\r\n string action, string symbol, double volume, double price,\r\n double dailyLossPct, double totalDDPct, double equity)\r\n {\r\n string url = endpoint + \"/v1/propguard/blocked/\" + accountId;\r\n string headers = \"Content-Type: application/json\\r\\n\"\r\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n string json = \"{\";\r\n json += \"\\\"rule_violated\\\":\\\"\" + JsonEscape(ruleViolated) + \"\\\",\";\r\n json += \"\\\"rule_details\\\":\\\"\" + JsonEscape(ruleDetails) + \"\\\",\";\r\n json += \"\\\"attempted_action\\\":\\\"\" + JsonEscape(action) + \"\\\",\";\r\n json += \"\\\"attempted_symbol\\\":\\\"\" + JsonEscape(symbol) + \"\\\",\";\r\n json += \"\\\"attempted_volume\\\":\" + DoubleToString(volume, 2) + \",\";\r\n json += \"\\\"attempted_price\\\":\" + DoubleToString(price, 5) + \",\";\r\n json += \"\\\"current_daily_loss_percent\\\":\" + DoubleToString(dailyLossPct, 4) + \",\";\r\n json += \"\\\"current_total_drawdown_percent\\\":\" + DoubleToString(totalDDPct, 4) + \",\";\r\n json += \"\\\"current_equity\\\":\" + DoubleToString(equity, 2);\r\n json += \"}\";\r\n\r\n char postData[];\r\n StringToCharArray(json, postData, 0, StringLen(json));\r\n\r\n char result[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n int statusCode = WebRequest(\"POST\", url, headers, 3000, postData, result, resultHeaders);\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Post emergency close event to cloud |\r\n//| POST /v1/propguard/emergency/:accountId |\r\n//+------------------------------------------------------------------+\r\nint PostEmergencyClose(string endpoint, string apiKey, string accountId,\r\n string reason, double equityAtClose, int positionsClosed)\r\n {\r\n string url = endpoint + \"/v1/propguard/emergency/\" + accountId;\r\n string headers = \"Content-Type: application/json\\r\\n\"\r\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n string json = \"{\";\r\n json += \"\\\"reason\\\":\\\"\" + JsonEscape(reason) + \"\\\",\";\r\n json += \"\\\"equity_at_close\\\":\" + DoubleToString(equityAtClose, 2) + \",\";\r\n json += \"\\\"positions_closed\\\":\" + IntegerToString(positionsClosed);\r\n json += \"}\";\r\n\r\n char postData[];\r\n StringToCharArray(json, postData, 0, StringLen(json));\r\n\r\n char result[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n int statusCode = WebRequest(\"POST\", url, headers, 3000, postData, result, resultHeaders);\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Fetch PropGuard rules from cloud |\r\n//| GET /v1/propguard/rules/:accountId |\r\n//+------------------------------------------------------------------+\r\nint FetchPropGuardRules(string endpoint, string apiKey, string accountId, string &response)\r\n {\r\n string url = endpoint + \"/v1/propguard/rules/\" + accountId;\r\n string headers = \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n char postData[];\r\n char resultData[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n int statusCode = WebRequest(\"GET\", url, headers, 5000, postData, resultData, resultHeaders);\r\n\r\n if(statusCode == 200)\r\n response = CharArrayToString(resultData, 0, WHOLE_ARRAY, CP_UTF8);\r\n else\r\n response = \"\";\r\n\r\n return statusCode;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Fetch news events from cloud |\r\n//| GET /v1/news/calendar?currency=USD,EUR,GBP |\r\n//+------------------------------------------------------------------+\r\nint FetchNewsEvents(string endpoint, string apiKey, string currencies, string &response)\r\n {\r\n string url = endpoint + \"/v1/news/calendar?currency=\" + currencies;\r\n string headers = \"X-API-Key: \" + apiKey + \"\\r\\n\";\r\n\r\n char postData[];\r\n char resultData[];\r\n string resultHeaders;\r\n\r\n ResetLastError();\r\n int statusCode = WebRequest(\"GET\", url, headers, 5000, postData, resultData, resultHeaders);\r\n\r\n if(statusCode == 200)\r\n response = CharArrayToString(resultData, 0, WHOLE_ARRAY, CP_UTF8);\r\n else\r\n response = \"\";\r\n\r\n return statusCode;\r\n }\r\n\r\n#endif // EDGERELAY_HTTP_MQH\r\n", + "EdgeRelay_JournalQueue.mqh": "//+------------------------------------------------------------------+\n//| EdgeRelay_JournalQueue.mqh |\n//| File-backed journal trade queue — no expiry |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property strict\n\n#ifndef EDGERELAY_JOURNAL_QUEUE_MQH\n#define EDGERELAY_JOURNAL_QUEUE_MQH\n\n#include \n\n#define JOURNAL_MAX_QUEUE_SIZE 5000\n#define JOURNAL_BATCH_SIZE 10\n\n//+------------------------------------------------------------------+\n//| Journal queue class |\n//+------------------------------------------------------------------+\nclass CJournalQueue\n {\nprivate:\n string m_filename;\n int m_count;\n\n void Recount();\n\npublic:\n CJournalQueue() { m_filename = \"\"; m_count = 0; }\n\n void Init(string filename);\n void Enqueue(JournalTrade &trade);\n int Count() { return m_count; }\n bool IsEmpty() { return m_count == 0; }\n void Clear();\n\n //--- Flush up to JOURNAL_BATCH_SIZE trades to the endpoint.\n //--- Returns number successfully sent.\n int Flush(string endpoint, string apiKey, string apiSecret, string accountId);\n };\n\n//+------------------------------------------------------------------+\nvoid CJournalQueue::Init(string filename)\n {\n m_filename = filename;\n Recount();\n if(m_count > 0)\n PrintFormat(\"[Journal] Queue initialized: %s (%d trades pending)\", m_filename, m_count);\n }\n\n//+------------------------------------------------------------------+\nvoid CJournalQueue::Recount()\n {\n m_count = 0;\n int handle = FileOpen(m_filename, FILE_READ | FILE_TXT | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE) return;\n\n while(!FileIsEnding(handle))\n {\n string line = FileReadString(handle);\n if(StringLen(line) > 0) m_count++;\n }\n FileClose(handle);\n }\n\n//+------------------------------------------------------------------+\nvoid CJournalQueue::Enqueue(JournalTrade &trade)\n {\n if(m_count >= JOURNAL_MAX_QUEUE_SIZE)\n {\n PrintFormat(\"[Journal] Queue full (%d), dropping trade %d\", m_count, trade.deal_ticket);\n return;\n }\n\n string json = JournalTradeToJson(trade);\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_WRITE | FILE_TXT | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n {\n PrintFormat(\"[Journal] Failed to open queue file for write: %d\", GetLastError());\n return;\n }\n\n FileSeek(handle, 0, SEEK_END);\n FileWriteString(handle, json + \"\\n\");\n FileClose(handle);\n\n m_count++;\n PrintFormat(\"[Journal] Queued trade %d (%s %s %s) queue=%d\",\n trade.deal_ticket, trade.deal_entry, trade.direction, trade.symbol, m_count);\n }\n\n//+------------------------------------------------------------------+\nvoid CJournalQueue::Clear()\n {\n int handle = FileOpen(m_filename, FILE_WRITE | FILE_TXT | FILE_ANSI);\n if(handle != INVALID_HANDLE)\n FileClose(handle);\n m_count = 0;\n }\n\n//+------------------------------------------------------------------+\nint CJournalQueue::Flush(string endpoint, string apiKey, string apiSecret, string accountId)\n {\n if(m_count == 0) return 0;\n\n //--- Read all lines\n string lines[];\n int lineCount = 0;\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_TXT | FILE_SHARE_READ | FILE_ANSI);\n if(handle == INVALID_HANDLE) return 0;\n\n while(!FileIsEnding(handle))\n {\n string line = FileReadString(handle);\n if(StringLen(line) > 0)\n {\n ArrayResize(lines, lineCount + 1);\n lines[lineCount] = line;\n lineCount++;\n }\n }\n FileClose(handle);\n\n if(lineCount == 0) { m_count = 0; return 0; }\n\n //--- Take up to JOURNAL_BATCH_SIZE trades\n int batchSize = MathMin(lineCount, JOURNAL_BATCH_SIZE);\n JournalTrade batch[];\n ArrayResize(batch, batchSize);\n\n //--- Parse JSON lines back to JournalTrade structs (simplified — just send raw JSON)\n //--- Actually, we stored them as JSON, so we can build the batch JSON directly\n string tradesJson = \"[\";\n ulong dealTickets[];\n ArrayResize(dealTickets, batchSize);\n\n for(int i = 0; i < batchSize; i++)\n {\n if(i > 0) tradesJson += \",\";\n tradesJson += lines[i];\n\n //--- Extract deal_ticket from the JSON line for HMAC\n //--- \"deal_ticket\": is 14 chars, value starts at dtPos + 14\n int dtPos = StringFind(lines[i], \"\\\"deal_ticket\\\":\");\n if(dtPos >= 0)\n {\n string sub = StringSubstr(lines[i], dtPos + 14, 20);\n int commaPos = StringFind(sub, \",\");\n if(commaPos > 0) sub = StringSubstr(sub, 0, commaPos);\n dealTickets[i] = (ulong)StringToInteger(sub);\n }\n }\n tradesJson += \"]\";\n\n //--- Build HMAC\n long ts = (long)TimeCurrent();\n string canonical = BuildJournalHmacCanonical(accountId, batchSize, dealTickets, ts);\n string hmac = HmacSha256(canonical, apiSecret);\n\n //--- Build full payload\n string json = \"{\";\n json += \"\\\"account_id\\\":\\\"\" + JsonEscape(accountId) + \"\\\",\";\n json += \"\\\"timestamp\\\":\" + IntegerToString(ts) + \",\";\n json += \"\\\"trades\\\":\" + tradesJson + \",\";\n json += \"\\\"hmac_signature\\\":\\\"\" + hmac + \"\\\"\";\n json += \"}\";\n\n //--- Send\n string url = endpoint + \"/v1/journal/sync\";\n string headers = \"Content-Type: application/json\\r\\n\"\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\n\n char postData[];\n StringToCharArray(json, postData, 0, StringLen(json));\n\n char result[];\n string resultHeaders;\n\n ResetLastError();\n int statusCode = WebRequest(\"POST\", url, headers, 10000, postData, result, resultHeaders);\n\n if(statusCode == 200 || statusCode == 201)\n {\n PrintFormat(\"[Journal] Batch synced: %d trades\", batchSize);\n\n //--- Remove sent lines from queue file\n int wHandle = FileOpen(m_filename, FILE_WRITE | FILE_TXT | FILE_ANSI);\n if(wHandle != INVALID_HANDLE)\n {\n for(int i = batchSize; i < lineCount; i++)\n FileWriteString(wHandle, lines[i] + \"\\n\");\n FileClose(wHandle);\n }\n m_count = lineCount - batchSize;\n return batchSize;\n }\n else\n {\n if(statusCode == -1)\n PrintFormat(\"[Journal] Batch send failed: network error %d\", GetLastError());\n else\n {\n string responseBody = CharArrayToString(result);\n PrintFormat(\"[Journal] Batch send HTTP %d: %s\", statusCode, responseBody);\n }\n return 0;\n }\n }\n\n#endif // EDGERELAY_JOURNAL_QUEUE_MQH\n", + "EdgeRelay_JournalSync.mqh": "//+------------------------------------------------------------------+\n//| EdgeRelay_JournalSync.mqh |\n//| Shared journal sync logic: capture & enrichment |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property strict\n\n#ifndef EDGERELAY_JOURNAL_SYNC_MQH\n#define EDGERELAY_JOURNAL_SYNC_MQH\n\n#include \n#include \n#include \n\n//+------------------------------------------------------------------+\n//| Journal trade struct |\n//+------------------------------------------------------------------+\nstruct JournalTrade\n {\n ulong deal_ticket;\n ulong order_ticket;\n ulong position_id;\n string symbol;\n string direction; // \"buy\" or \"sell\"\n string deal_entry; // \"in\", \"out\", \"inout\"\n double volume;\n double price;\n double sl;\n double tp;\n datetime time;\n double profit;\n double commission;\n double swap;\n long magic_number;\n string comment;\n double balance_at_trade;\n double equity_at_trade;\n int spread_at_entry;\n double atr_at_entry;\n string session_tag; // \"asian\", \"london\", \"new_york\", \"off_hours\"\n int duration_seconds; // -1 = null\n double pips; // 0 with hasPips=false = null\n double risk_reward_ratio; // 0 with hasRR=false = null\n bool has_duration;\n bool has_pips;\n bool has_rr;\n };\n\n//+------------------------------------------------------------------+\n//| Determine session tag from hour (UTC) |\n//+------------------------------------------------------------------+\nstring GetSessionTag(datetime tradeTime)\n {\n MqlDateTime dt;\n TimeToStruct(tradeTime, dt);\n int hour = dt.hour;\n\n // NY takes priority in 13-16 overlap with London\n if(hour >= 13 && hour < 21) return \"new_york\";\n if(hour >= 8 && hour < 13) return \"london\";\n if(hour >= 0 && hour < 8) return \"asian\";\n return \"off_hours\";\n }\n\n//+------------------------------------------------------------------+\n//| Get ATR value using MQL5 indicator handle pattern |\n//+------------------------------------------------------------------+\ndouble GetATR(string symbol, ENUM_TIMEFRAMES tf, int period)\n {\n int handle = iATR(symbol, tf, period);\n if(handle == INVALID_HANDLE)\n return 0.0;\n\n double buffer[];\n ArraySetAsSeries(buffer, true);\n int copied = CopyBuffer(handle, 0, 0, 1, buffer);\n IndicatorRelease(handle);\n\n if(copied <= 0)\n return 0.0;\n\n return buffer[0];\n }\n\n//+------------------------------------------------------------------+\n//| Capture and enrich a deal into a JournalTrade |\n//+------------------------------------------------------------------+\nbool CaptureDeal(ulong dealTicket, JournalTrade &trade)\n {\n //--- Ensure history is loaded (needed when called from OnTradeTransaction)\n HistorySelect(0, TimeCurrent());\n\n if(!HistoryDealSelect(dealTicket))\n return false;\n\n ENUM_DEAL_TYPE dealType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);\n if(dealType != DEAL_TYPE_BUY && dealType != DEAL_TYPE_SELL)\n return false;\n\n ENUM_DEAL_ENTRY dealEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);\n\n //--- Core fields\n trade.deal_ticket = dealTicket;\n trade.order_ticket = (ulong)HistoryDealGetInteger(dealTicket, DEAL_ORDER);\n trade.position_id = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);\n trade.symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);\n trade.direction = (dealType == DEAL_TYPE_BUY) ? \"buy\" : \"sell\";\n trade.volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);\n trade.price = HistoryDealGetDouble(dealTicket, DEAL_PRICE);\n trade.time = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);\n trade.profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);\n trade.commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);\n trade.swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP);\n trade.magic_number = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);\n trade.comment = HistoryDealGetString(dealTicket, DEAL_COMMENT);\n\n //--- Deal entry\n if(dealEntry == DEAL_ENTRY_IN) trade.deal_entry = \"in\";\n else if(dealEntry == DEAL_ENTRY_OUT) trade.deal_entry = \"out\";\n else if(dealEntry == DEAL_ENTRY_INOUT) trade.deal_entry = \"inout\";\n else trade.deal_entry = \"in\";\n\n //--- SL/TP from position if available\n trade.sl = 0;\n trade.tp = 0;\n if(PositionSelectByTicket(trade.position_id))\n {\n trade.sl = PositionGetDouble(POSITION_SL);\n trade.tp = PositionGetDouble(POSITION_TP);\n }\n\n //--- Enrichment: balance, equity\n trade.balance_at_trade = AccountInfoDouble(ACCOUNT_BALANCE);\n trade.equity_at_trade = AccountInfoDouble(ACCOUNT_EQUITY);\n\n //--- Enrichment: spread (integer, in points)\n trade.spread_at_entry = (int)SymbolInfoInteger(trade.symbol, SYMBOL_SPREAD);\n\n //--- Enrichment: ATR(14, H1)\n trade.atr_at_entry = GetATR(trade.symbol, PERIOD_H1, 14);\n\n //--- Enrichment: session tag\n trade.session_tag = GetSessionTag(trade.time);\n\n //--- Enrichment: duration, pips, R:R (only for closing deals)\n trade.has_duration = false;\n trade.has_pips = false;\n trade.has_rr = false;\n trade.duration_seconds = 0;\n trade.pips = 0;\n trade.risk_reward_ratio = 0;\n\n if(dealEntry == DEAL_ENTRY_OUT || dealEntry == DEAL_ENTRY_INOUT)\n {\n //--- Look up the entry deal for this position\n if(HistorySelectByPosition(trade.position_id))\n {\n int total = HistoryDealsTotal();\n for(int i = 0; i < total; i++)\n {\n ulong entryTicket = HistoryDealGetTicket(i);\n if(entryTicket == dealTicket) continue;\n ENUM_DEAL_ENTRY eEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(entryTicket, DEAL_ENTRY);\n if(eEntry == DEAL_ENTRY_IN)\n {\n datetime entryTime = (datetime)HistoryDealGetInteger(entryTicket, DEAL_TIME);\n double entryPrice = HistoryDealGetDouble(entryTicket, DEAL_PRICE);\n\n trade.duration_seconds = (int)(trade.time - entryTime);\n trade.has_duration = true;\n\n //--- Pips\n double point = SymbolInfoDouble(trade.symbol, SYMBOL_POINT);\n if(point > 0)\n {\n double rawPips = (trade.price - entryPrice) / point;\n trade.pips = (trade.direction == \"sell\") ? -rawPips : rawPips;\n trade.has_pips = true;\n }\n\n //--- R:R from the order's SL/TP (deals don't have SL/TP in MQL5)\n double entrySL = 0;\n double entryTP = 0;\n ulong entryOrder = (ulong)HistoryDealGetInteger(entryTicket, DEAL_ORDER);\n if(entryOrder > 0 && HistoryOrderSelect(entryOrder))\n {\n entrySL = HistoryOrderGetDouble(entryOrder, ORDER_SL);\n entryTP = HistoryOrderGetDouble(entryOrder, ORDER_TP);\n }\n // Fallback to position SL/TP if order didn't have them\n if(entrySL == 0 && trade.sl != 0) entrySL = trade.sl;\n if(entryTP == 0 && trade.tp != 0) entryTP = trade.tp;\n\n if(entrySL != 0 && entryTP != 0)\n {\n double risk = MathAbs(entryPrice - entrySL);\n double reward = MathAbs(entryTP - entryPrice);\n if(risk > 0)\n {\n trade.risk_reward_ratio = reward / risk;\n trade.has_rr = true;\n }\n }\n break; // Found the entry deal\n }\n }\n }\n }\n\n return true;\n }\n\n//+------------------------------------------------------------------+\n//| Convert a JournalTrade to JSON string |\n//+------------------------------------------------------------------+\nstring JournalTradeToJson(JournalTrade &trade)\n {\n int digits = (int)SymbolInfoInteger(trade.symbol, SYMBOL_DIGITS);\n if(digits <= 0) digits = 5;\n\n string json = \"{\";\n json += \"\\\"deal_ticket\\\":\" + IntegerToString((long)trade.deal_ticket) + \",\";\n json += \"\\\"order_ticket\\\":\" + IntegerToString((long)trade.order_ticket) + \",\";\n json += \"\\\"position_id\\\":\" + IntegerToString((long)trade.position_id) + \",\";\n json += \"\\\"symbol\\\":\\\"\" + JsonEscape(trade.symbol) + \"\\\",\";\n json += \"\\\"direction\\\":\\\"\" + trade.direction + \"\\\",\";\n json += \"\\\"deal_entry\\\":\\\"\" + trade.deal_entry + \"\\\",\";\n json += \"\\\"volume\\\":\" + DoubleToString(trade.volume, 8) + \",\";\n json += \"\\\"price\\\":\" + DoubleToString(trade.price, digits) + \",\";\n json += \"\\\"sl\\\":\" + DoubleToString(trade.sl, digits) + \",\";\n json += \"\\\"tp\\\":\" + DoubleToString(trade.tp, digits) + \",\";\n json += \"\\\"time\\\":\" + IntegerToString((long)trade.time) + \",\";\n json += \"\\\"profit\\\":\" + DoubleToString(trade.profit, 2) + \",\";\n json += \"\\\"commission\\\":\" + DoubleToString(trade.commission, 2) + \",\";\n json += \"\\\"swap\\\":\" + DoubleToString(trade.swap, 2) + \",\";\n json += \"\\\"magic_number\\\":\" + IntegerToString(trade.magic_number) + \",\";\n json += \"\\\"comment\\\":\\\"\" + JsonEscape(trade.comment) + \"\\\",\";\n json += \"\\\"balance_at_trade\\\":\" + DoubleToString(trade.balance_at_trade, 2) + \",\";\n json += \"\\\"equity_at_trade\\\":\" + DoubleToString(trade.equity_at_trade, 2) + \",\";\n json += \"\\\"spread_at_entry\\\":\" + IntegerToString(trade.spread_at_entry) + \",\";\n json += \"\\\"atr_at_entry\\\":\" + DoubleToString(trade.atr_at_entry, 6) + \",\";\n json += \"\\\"session_tag\\\":\\\"\" + trade.session_tag + \"\\\",\";\n\n if(trade.has_duration)\n json += \"\\\"duration_seconds\\\":\" + IntegerToString(trade.duration_seconds) + \",\";\n else\n json += \"\\\"duration_seconds\\\":null,\";\n\n if(trade.has_pips)\n json += \"\\\"pips\\\":\" + DoubleToString(trade.pips, 1) + \",\";\n else\n json += \"\\\"pips\\\":null,\";\n\n if(trade.has_rr)\n json += \"\\\"risk_reward_ratio\\\":\" + DoubleToString(trade.risk_reward_ratio, 2);\n else\n json += \"\\\"risk_reward_ratio\\\":null\";\n\n json += \"}\";\n return json;\n }\n\n//+------------------------------------------------------------------+\n//| Build HMAC canonical string for a batch of trades |\n//+------------------------------------------------------------------+\nstring BuildJournalHmacCanonical(string accountId, int tradeCount, ulong &dealTickets[], long timestamp)\n {\n //--- Sort deal tickets ascending\n for(int i = 0; i < tradeCount - 1; i++)\n for(int j = i + 1; j < tradeCount; j++)\n if(dealTickets[j] < dealTickets[i])\n {\n ulong tmp = dealTickets[i];\n dealTickets[i] = dealTickets[j];\n dealTickets[j] = tmp;\n }\n\n //--- Join with commas\n string ticketStr = \"\";\n for(int i = 0; i < tradeCount; i++)\n {\n if(i > 0) ticketStr += \",\";\n ticketStr += IntegerToString((long)dealTickets[i]);\n }\n\n return \"account_id:\" + accountId + \":count:\" + IntegerToString(tradeCount)\n + \":deals:\" + ticketStr + \":ts:\" + IntegerToString(timestamp);\n }\n\n//+------------------------------------------------------------------+\n//| Send journal heartbeat (uses /v1/journal/heartbeat path) |\n//+------------------------------------------------------------------+\nint SendJournalHeartbeat(string endpoint, string apiKey, string accountId, string apiSecret)\n {\n string url = endpoint + \"/v1/journal/heartbeat\";\n string headers = \"Content-Type: application/json\\r\\n\"\n \"X-API-Key: \" + apiKey + \"\\r\\n\";\n\n long ts = (long)TimeCurrent();\n string tsStr = IntegerToString(ts);\n\n string sigPayload = \"{\\\"account_id\\\":\\\"\" + accountId + \"\\\",\\\"timestamp\\\":\" + tsStr + \"}\";\n string hmac = HmacSha256(sigPayload, apiSecret);\n\n string jsonBody = \"{\";\n jsonBody += \"\\\"account_id\\\":\\\"\" + JsonEscape(accountId) + \"\\\",\";\n jsonBody += \"\\\"timestamp\\\":\" + tsStr + \",\";\n jsonBody += \"\\\"hmac_signature\\\":\\\"\" + hmac + \"\\\"\";\n jsonBody += \"}\";\n\n char postData[];\n StringToCharArray(jsonBody, postData, 0, StringLen(jsonBody));\n\n char result[];\n string resultHeaders;\n\n ResetLastError();\n int statusCode = WebRequest(\"POST\", url, headers, 5000, postData, result, resultHeaders);\n\n if(statusCode == -1)\n {\n int err = GetLastError();\n PrintFormat(\"[Journal] Heartbeat failed: error=%d\", err);\n }\n\n return statusCode;\n }\n\n#endif // EDGERELAY_JOURNAL_SYNC_MQH\n", + "EdgeRelay_JsonParser.mqh": "//+------------------------------------------------------------------+\n//| EdgeRelay_JsonParser.mqh |\n//| EdgeRelay - Lightweight MQL5 JSON Parser |\n//| https://www.edgerelay.io |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property strict\n\n#ifndef EDGERELAY_JSONPARSER_MQH\n#define EDGERELAY_JSONPARSER_MQH\n\n//+------------------------------------------------------------------+\n//| CJsonParser - Simple string-based JSON parser for MQL5 |\n//| Handles flat objects and arrays of objects. |\n//| Not a full spec parser - tuned for EdgeRelay poll responses. |\n//+------------------------------------------------------------------+\nclass CJsonParser\n {\nprivate:\n string m_json; // Raw JSON string\n int m_len; // Length cache\n\n int SkipWhitespace(int pos);\n int FindMatchingBrace(int openPos, ushort openChar, ushort closeChar);\n int FindKeyValue(const string &key, int searchStart, int searchEnd);\n string ExtractStringValue(int valueStart, int &valueEnd);\n string ExtractRawValue(int valueStart, int &valueEnd);\n\npublic:\n CJsonParser();\n ~CJsonParser();\n\n //--- Core interface\n bool Parse(const string &json);\n string GetString(const string &key);\n double GetDouble(const string &key);\n int GetInt(const string &key);\n long GetLong(const string &key);\n bool GetBool(const string &key);\n\n //--- Array support\n int GetArraySize(const string &key);\n string GetArrayElement(int index);\n string GetArrayElementByKey(const string &key, int index);\n\n //--- Utility\n string GetRawJson() { return m_json; }\n };\n\n//+------------------------------------------------------------------+\n//| Constructor |\n//+------------------------------------------------------------------+\nCJsonParser::CJsonParser()\n {\n m_json = \"\";\n m_len = 0;\n }\n\n//+------------------------------------------------------------------+\n//| Destructor |\n//+------------------------------------------------------------------+\nCJsonParser::~CJsonParser()\n {\n }\n\n//+------------------------------------------------------------------+\n//| Parse - Load a JSON string |\n//+------------------------------------------------------------------+\nbool CJsonParser::Parse(const string &json)\n {\n m_json = json;\n m_len = StringLen(json);\n if(m_len < 2)\n return false;\n\n // Basic sanity: should start with { or [\n int start = SkipWhitespace(0);\n if(start >= m_len)\n return false;\n\n ushort ch = StringGetCharacter(m_json, start);\n if(ch != '{' && ch != '[')\n return false;\n\n return true;\n }\n\n//+------------------------------------------------------------------+\n//| Skip whitespace characters, return next non-WS position |\n//+------------------------------------------------------------------+\nint CJsonParser::SkipWhitespace(int pos)\n {\n while(pos < m_len)\n {\n ushort ch = StringGetCharacter(m_json, pos);\n if(ch != ' ' && ch != '\\t' && ch != '\\r' && ch != '\\n')\n break;\n pos++;\n }\n return pos;\n }\n\n//+------------------------------------------------------------------+\n//| Find matching close brace/bracket respecting nesting & strings |\n//+------------------------------------------------------------------+\nint CJsonParser::FindMatchingBrace(int openPos, ushort openChar, ushort closeChar)\n {\n int depth = 0;\n bool inString = false;\n for(int i = openPos; i < m_len; i++)\n {\n ushort ch = StringGetCharacter(m_json, i);\n\n if(inString)\n {\n if(ch == '\\\\')\n {\n i++; // skip escaped char\n continue;\n }\n if(ch == '\"')\n inString = false;\n continue;\n }\n\n if(ch == '\"')\n {\n inString = true;\n continue;\n }\n if(ch == openChar)\n depth++;\n if(ch == closeChar)\n {\n depth--;\n if(depth == 0)\n return i;\n }\n }\n return -1;\n }\n\n//+------------------------------------------------------------------+\n//| Find the value-start position for a given key within a range |\n//| Returns position right after the colon (at value start) |\n//+------------------------------------------------------------------+\nint CJsonParser::FindKeyValue(const string &key, int searchStart, int searchEnd)\n {\n string needle = \"\\\"\" + key + \"\\\"\";\n int keyPos = searchStart;\n\n while(keyPos < searchEnd)\n {\n int found = StringFind(m_json, needle, keyPos);\n if(found < 0 || found >= searchEnd)\n return -1;\n\n // Move past the key and find the colon\n int afterKey = found + StringLen(needle);\n int colonPos = SkipWhitespace(afterKey);\n if(colonPos < searchEnd && StringGetCharacter(m_json, colonPos) == ':')\n {\n return SkipWhitespace(colonPos + 1);\n }\n\n // Not a key:value pair, keep searching\n keyPos = afterKey;\n }\n return -1;\n }\n\n//+------------------------------------------------------------------+\n//| Extract a JSON string value (assumes pos is at opening quote) |\n//+------------------------------------------------------------------+\nstring CJsonParser::ExtractStringValue(int valueStart, int &valueEnd)\n {\n if(valueStart >= m_len || StringGetCharacter(m_json, valueStart) != '\"')\n {\n valueEnd = valueStart;\n return \"\";\n }\n\n int i = valueStart + 1;\n string result = \"\";\n\n while(i < m_len)\n {\n ushort ch = StringGetCharacter(m_json, i);\n\n if(ch == '\\\\' && i + 1 < m_len)\n {\n ushort next = StringGetCharacter(m_json, i + 1);\n switch(next)\n {\n case '\"': result += \"\\\"\"; break;\n case '\\\\': result += \"\\\\\"; break;\n case '/': result += \"/\"; break;\n case 'n': result += \"\\n\"; break;\n case 'r': result += \"\\r\"; break;\n case 't': result += \"\\t\"; break;\n default: result += StringSubstr(m_json, i, 2); break;\n }\n i += 2;\n continue;\n }\n\n if(ch == '\"')\n {\n valueEnd = i + 1;\n return result;\n }\n\n result += ShortToString(ch);\n i++;\n }\n\n valueEnd = i;\n return result;\n }\n\n//+------------------------------------------------------------------+\n//| Extract a raw JSON value (number, bool, null, object, array) |\n//+------------------------------------------------------------------+\nstring CJsonParser::ExtractRawValue(int valueStart, int &valueEnd)\n {\n if(valueStart >= m_len)\n {\n valueEnd = valueStart;\n return \"\";\n }\n\n ushort ch = StringGetCharacter(m_json, valueStart);\n\n // String\n if(ch == '\"')\n return ExtractStringValue(valueStart, valueEnd);\n\n // Object\n if(ch == '{')\n {\n int close = FindMatchingBrace(valueStart, '{', '}');\n if(close < 0) { valueEnd = m_len; return \"\"; }\n valueEnd = close + 1;\n return StringSubstr(m_json, valueStart, valueEnd - valueStart);\n }\n\n // Array\n if(ch == '[')\n {\n int close = FindMatchingBrace(valueStart, '[', ']');\n if(close < 0) { valueEnd = m_len; return \"\"; }\n valueEnd = close + 1;\n return StringSubstr(m_json, valueStart, valueEnd - valueStart);\n }\n\n // Primitive (number, true, false, null)\n int i = valueStart;\n while(i < m_len)\n {\n ushort c = StringGetCharacter(m_json, i);\n if(c == ',' || c == '}' || c == ']' || c == ' ' || c == '\\r' || c == '\\n' || c == '\\t')\n break;\n i++;\n }\n valueEnd = i;\n return StringSubstr(m_json, valueStart, i - valueStart);\n }\n\n//+------------------------------------------------------------------+\n//| GetString - Get a string value by key |\n//+------------------------------------------------------------------+\nstring CJsonParser::GetString(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return \"\";\n\n int endPos = 0;\n return ExtractStringValue(valPos, endPos);\n }\n\n//+------------------------------------------------------------------+\n//| GetDouble - Get a numeric value as double |\n//+------------------------------------------------------------------+\ndouble CJsonParser::GetDouble(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return 0.0;\n\n int endPos = 0;\n string raw = ExtractRawValue(valPos, endPos);\n return StringToDouble(raw);\n }\n\n//+------------------------------------------------------------------+\n//| GetInt - Get a numeric value as int |\n//+------------------------------------------------------------------+\nint CJsonParser::GetInt(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return 0;\n\n int endPos = 0;\n string raw = ExtractRawValue(valPos, endPos);\n return (int)StringToInteger(raw);\n }\n\n//+------------------------------------------------------------------+\n//| GetLong - Get a numeric value as long |\n//+------------------------------------------------------------------+\nlong CJsonParser::GetLong(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return 0;\n\n int endPos = 0;\n string raw = ExtractRawValue(valPos, endPos);\n return StringToInteger(raw);\n }\n\n//+------------------------------------------------------------------+\n//| GetBool - Get a boolean value |\n//+------------------------------------------------------------------+\nbool CJsonParser::GetBool(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return false;\n\n int endPos = 0;\n string raw = ExtractRawValue(valPos, endPos);\n return (raw == \"true\");\n }\n\n//+------------------------------------------------------------------+\n//| GetArraySize - Count elements in a JSON array value |\n//+------------------------------------------------------------------+\nint CJsonParser::GetArraySize(const string &key)\n {\n int valPos = FindKeyValue(key, 0, m_len);\n if(valPos < 0)\n return 0;\n\n if(StringGetCharacter(m_json, valPos) != '[')\n return 0;\n\n int closePos = FindMatchingBrace(valPos, '[', ']');\n if(closePos < 0)\n return 0;\n\n // Check for empty array\n int inner = SkipWhitespace(valPos + 1);\n if(inner >= closePos)\n return 0;\n\n // Count top-level elements by counting commas at depth 0\n int count = 1;\n bool inStr = false;\n int depth = 0;\n for(int i = valPos + 1; i < closePos; i++)\n {\n ushort ch = StringGetCharacter(m_json, i);\n\n if(inStr)\n {\n if(ch == '\\\\') { i++; continue; }\n if(ch == '\"') inStr = false;\n continue;\n }\n\n if(ch == '\"') { inStr = true; continue; }\n if(ch == '{' || ch == '[') { depth++; continue; }\n if(ch == '}' || ch == ']') { depth--; continue; }\n if(ch == ',' && depth == 0) count++;\n }\n\n return count;\n }\n\n//+------------------------------------------------------------------+\n//| GetArrayElement - Get nth element from root-level array |\n//+------------------------------------------------------------------+\nstring CJsonParser::GetArrayElement(int index)\n {\n if(m_len < 2)\n return \"\";\n\n int start = SkipWhitespace(0);\n if(StringGetCharacter(m_json, start) != '[')\n return \"\";\n\n return GetArrayElementByKey(\"\", index);\n }\n\n//+------------------------------------------------------------------+\n//| GetArrayElementByKey - Get nth element of a named array |\n//+------------------------------------------------------------------+\nstring CJsonParser::GetArrayElementByKey(const string &key, int index)\n {\n int arrStart;\n\n if(key == \"\")\n {\n arrStart = SkipWhitespace(0);\n }\n else\n {\n arrStart = FindKeyValue(key, 0, m_len);\n }\n\n if(arrStart < 0 || arrStart >= m_len)\n return \"\";\n\n if(StringGetCharacter(m_json, arrStart) != '[')\n return \"\";\n\n int closeArr = FindMatchingBrace(arrStart, '[', ']');\n if(closeArr < 0)\n return \"\";\n\n // Walk through elements\n int elemIdx = 0;\n int pos = SkipWhitespace(arrStart + 1);\n\n while(pos < closeArr)\n {\n int endPos = 0;\n string val = ExtractRawValue(pos, endPos);\n\n if(elemIdx == index)\n return val;\n\n elemIdx++;\n\n // Skip to next element\n pos = SkipWhitespace(endPos);\n if(pos < closeArr && StringGetCharacter(m_json, pos) == ',')\n pos = SkipWhitespace(pos + 1);\n }\n\n return \"\";\n }\n\n#endif // EDGERELAY_JSONPARSER_MQH\n", + "EdgeRelay_Master.mq5": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_Master.mq5 |\r\n//| EdgeRelay Master EA - Trade Sender |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property version \"1.00\"\r\n#property description \"EdgeRelay Master EA - sends trade signals to the EdgeRelay relay server.\"\r\n#property strict\r\n\r\n//--- Includes\r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| Input parameters |\r\n//+------------------------------------------------------------------+\r\ninput string API_Key = \"\"; // API Key\r\ninput string API_Secret = \"\"; // API Secret\r\ninput string API_Endpoint = \"https://edgerelay-signal-ingestion.ghwmelite.workers.dev\"; // API Endpoint URL\r\ninput string AccountID = \"\"; // Account ID\r\ninput int HeartbeatIntervalMs = 5000; // Heartbeat interval (ms)\r\ninput bool CopyBuys = true; // Copy BUY trades\r\ninput bool CopySells = true; // Copy SELL trades\r\ninput string SymbolFilter = \"\"; // Symbol filter (empty = all)\r\ninput double MinLotFilter = 0.0; // Minimum lot filter\r\ninput bool CopyPendings = true; // Copy pending orders\r\ninput bool CopyModifications = true; // Copy trade modifications\r\ninput bool CopyCloses = true; // Copy trade closes\r\n\r\n//--- Journal sync settings\r\ninput bool EnableJournal = false; // Enable trade journaling\r\ninput string JournalEndpoint = \"https://edgerelay-journal-sync.ghwmelite.workers.dev\"; // Journal endpoint\r\n\r\n//--- Global variables\r\nCSignalQueue g_queue;\r\nCEdgeRelayDisplay g_display;\r\nCJournalQueue g_journalQueue;\r\nulong g_journalSyncedDeals[];\r\nint g_journalSyncedCount = 0;\r\n\r\nint g_sequenceNum = 0;\r\nint g_signalsSentToday = 0;\r\ndouble g_lastLatencyMs = -1.0;\r\ndatetime g_lastSignalTime = 0;\r\nENUM_CONNECTION_STATUS g_connStatus = STATUS_DISCONNECTED;\r\ndatetime g_lastDay = 0;\r\n\r\n//--- GlobalVariable name for sequence_num persistence\r\nstring g_gvSeqName = \"\";\r\n\r\n//+------------------------------------------------------------------+\r\n//| Expert initialization function |\r\n//+------------------------------------------------------------------+\r\nint OnInit()\r\n {\r\n //--- Validate inputs\r\n if(StringLen(API_Key) == 0)\r\n {\r\n Alert(\"[EdgeRelay] API_Key is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n if(StringLen(API_Secret) == 0)\r\n {\r\n Alert(\"[EdgeRelay] API_Secret is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n if(StringLen(AccountID) == 0)\r\n {\r\n Alert(\"[EdgeRelay] AccountID is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n if(StringLen(API_Endpoint) == 0)\r\n {\r\n Alert(\"[EdgeRelay] API_Endpoint is required.\");\r\n return INIT_PARAMETERS_INCORRECT;\r\n }\r\n\r\n //--- Initialize display\r\n g_display.Init();\r\n\r\n //--- Initialize queue\r\n string queueFile = \"EdgeRelay_Queue_\" + AccountID + \".txt\";\r\n g_queue.Init(queueFile);\r\n\r\n //--- Restore sequence_num from GlobalVariable for crash recovery\r\n g_gvSeqName = \"EdgeRelay_SeqNum_\" + AccountID;\r\n if(GlobalVariableCheck(g_gvSeqName))\r\n {\r\n g_sequenceNum = (int)GlobalVariableGet(g_gvSeqName);\r\n PrintFormat(\"[EdgeRelay] Restored sequence_num: %d\", g_sequenceNum);\r\n }\r\n else\r\n {\r\n g_sequenceNum = 0;\r\n GlobalVariableSet(g_gvSeqName, 0);\r\n }\r\n\r\n //--- Reset daily counter\r\n g_lastDay = StringToTime(TimeToString(TimeCurrent(), TIME_DATE));\r\n g_signalsSentToday = 0;\r\n\r\n //--- Set timer for heartbeat\r\n int timerMs = MathMax(HeartbeatIntervalMs, 1000);\r\n if(!EventSetMillisecondTimer(timerMs))\r\n {\r\n PrintFormat(\"[EdgeRelay] Failed to set timer: %d\", GetLastError());\r\n EventSetTimer(MathMax(timerMs / 1000, 1));\r\n }\r\n\r\n //--- Initial connection check and heartbeat\r\n g_connStatus = STATUS_CONNECTING;\r\n g_display.Update(g_connStatus, g_signalsSentToday, g_queue.Count(), g_lastLatencyMs, g_lastSignalTime);\r\n\r\n if(IsServerReachable(API_Endpoint))\r\n {\r\n int hbResult = SendHeartbeat(API_Endpoint, API_Key, AccountID, API_Secret);\r\n g_connStatus = (hbResult == 200 || hbResult == 201) ? STATUS_CONNECTED : STATUS_ERROR;\r\n }\r\n else\r\n {\r\n g_connStatus = STATUS_DISCONNECTED;\r\n }\r\n\r\n g_display.Update(g_connStatus, g_signalsSentToday, g_queue.Count(), g_lastLatencyMs, g_lastSignalTime);\r\n\r\n //--- Initialize journal if enabled\r\n if(EnableJournal)\r\n {\r\n string jQueueFile = \"JournalSync_Queue_\" + AccountID + \".txt\";\r\n g_journalQueue.Init(jQueueFile);\r\n PrintFormat(\"[EdgeRelay] Journal sync enabled. Endpoint: %s\", JournalEndpoint);\r\n }\r\n\r\n PrintFormat(\"[EdgeRelay] Master EA initialized. Account: %s, Endpoint: %s\", AccountID, API_Endpoint);\r\n return INIT_SUCCEEDED;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Expert deinitialization function |\r\n//+------------------------------------------------------------------+\r\nvoid OnDeinit(const int reason)\r\n {\r\n //--- Flush remaining queue\r\n if(!g_queue.IsEmpty())\r\n {\r\n PrintFormat(\"[EdgeRelay] Flushing queue on shutdown (%d signals)...\", g_queue.Count());\r\n g_queue.Flush(API_Endpoint, API_Key);\r\n }\r\n\r\n //--- Persist sequence_num\r\n GlobalVariableSet(g_gvSeqName, (double)g_sequenceNum);\r\n\r\n //--- Clean up timer and display\r\n EventKillTimer();\r\n g_display.Deinit();\r\n\r\n Print(\"[EdgeRelay] Master EA deinitialized.\");\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Timer event handler |\r\n//+------------------------------------------------------------------+\r\nvoid OnTimer()\r\n {\r\n //--- Reset daily counter at day change\r\n datetime today = StringToTime(TimeToString(TimeCurrent(), TIME_DATE));\r\n if(today != g_lastDay)\r\n {\r\n g_lastDay = today;\r\n g_signalsSentToday = 0;\r\n }\r\n\r\n //--- Send heartbeat\r\n uint startTick = GetTickCount();\r\n int hbResult = SendHeartbeat(API_Endpoint, API_Key, AccountID, API_Secret);\r\n uint elapsed = GetTickCount() - startTick;\r\n\r\n if(hbResult == 200 || hbResult == 201)\r\n {\r\n g_connStatus = STATUS_CONNECTED;\r\n g_lastLatencyMs = (double)elapsed;\r\n }\r\n else if(hbResult == -1)\r\n {\r\n g_connStatus = STATUS_DISCONNECTED;\r\n }\r\n else\r\n {\r\n g_connStatus = STATUS_ERROR;\r\n PrintFormat(\"[EdgeRelay] Heartbeat returned HTTP %d\", hbResult);\r\n }\r\n\r\n //--- Attempt to flush queue if not empty\r\n if(!g_queue.IsEmpty() && g_connStatus == STATUS_CONNECTED)\r\n g_queue.Flush(API_Endpoint, API_Key);\r\n\r\n //--- Flush journal queue if enabled\r\n if(EnableJournal && !g_journalQueue.IsEmpty() && g_connStatus == STATUS_CONNECTED)\r\n g_journalQueue.Flush(JournalEndpoint, API_Key, API_Secret, AccountID);\r\n\r\n //--- Persist sequence_num periodically\r\n GlobalVariableSet(g_gvSeqName, (double)g_sequenceNum);\r\n\r\n //--- Update display\r\n g_display.Update(g_connStatus, g_signalsSentToday, g_queue.Count(), g_lastLatencyMs, g_lastSignalTime);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check if a symbol passes the symbol filter |\r\n//+------------------------------------------------------------------+\r\nbool PassesSymbolFilter(string sym)\r\n {\r\n if(StringLen(SymbolFilter) == 0)\r\n return true;\r\n\r\n //--- Support comma-separated symbol list\r\n string filters[];\r\n int count = StringSplit(SymbolFilter, ',', filters);\r\n\r\n for(int i = 0; i < count; i++)\r\n {\r\n string f = filters[i];\r\n StringTrimLeft(f);\r\n StringTrimRight(f);\r\n if(StringLen(f) > 0 && StringFind(sym, f) >= 0)\r\n return true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check if an order type passes the direction filter |\r\n//+------------------------------------------------------------------+\r\nbool PassesDirectionFilter(ENUM_ORDER_TYPE orderType)\r\n {\r\n switch(orderType)\r\n {\r\n case ORDER_TYPE_BUY:\r\n case ORDER_TYPE_BUY_LIMIT:\r\n case ORDER_TYPE_BUY_STOP:\r\n case ORDER_TYPE_BUY_STOP_LIMIT:\r\n return CopyBuys;\r\n\r\n case ORDER_TYPE_SELL:\r\n case ORDER_TYPE_SELL_LIMIT:\r\n case ORDER_TYPE_SELL_STOP:\r\n case ORDER_TYPE_SELL_STOP_LIMIT:\r\n return CopySells;\r\n }\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Check if an order type is a pending order |\r\n//+------------------------------------------------------------------+\r\nbool IsPendingOrderType(ENUM_ORDER_TYPE orderType)\r\n {\r\n return (orderType == ORDER_TYPE_BUY_LIMIT ||\r\n orderType == ORDER_TYPE_SELL_LIMIT ||\r\n orderType == ORDER_TYPE_BUY_STOP ||\r\n orderType == ORDER_TYPE_SELL_STOP ||\r\n orderType == ORDER_TYPE_BUY_STOP_LIMIT ||\r\n orderType == ORDER_TYPE_SELL_STOP_LIMIT);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Build, sign, and dispatch a signal |\r\n//+------------------------------------------------------------------+\r\nvoid DispatchSignal(Signal &signal)\r\n {\r\n //--- Set common fields\r\n signal.account_id = AccountID;\r\n signal.timestamp = TimeCurrent();\r\n signal.sequence_num = ++g_sequenceNum;\r\n signal.signal_id = GenerateSignalId(AccountID, g_sequenceNum);\r\n\r\n //--- Sign the signal\r\n signal.hmac_signature = SignPayload(signal, API_Secret);\r\n\r\n //--- Attempt to send\r\n uint startTick = GetTickCount();\r\n int httpCode = SendSignal(API_Endpoint, API_Key, signal);\r\n uint elapsed = GetTickCount() - startTick;\r\n\r\n if(httpCode == 200 || httpCode == 201)\r\n {\r\n g_connStatus = STATUS_CONNECTED;\r\n g_lastLatencyMs = (double)elapsed;\r\n g_lastSignalTime = TimeCurrent();\r\n g_signalsSentToday++;\r\n\r\n PrintFormat(\"[EdgeRelay] Signal sent: %s %s %s vol=%.2f (seq=%d, %dms)\",\r\n ActionToString(signal.action),\r\n OrderTypeToStr(signal.order_type),\r\n signal.symbol,\r\n signal.volume,\r\n signal.sequence_num,\r\n elapsed);\r\n }\r\n else\r\n {\r\n //--- Enqueue on failure\r\n if(httpCode == -1)\r\n g_connStatus = STATUS_DISCONNECTED;\r\n else\r\n g_connStatus = STATUS_ERROR;\r\n\r\n g_queue.Enqueue(signal);\r\n PrintFormat(\"[EdgeRelay] Signal queued (HTTP %d): %s %s %s\",\r\n httpCode,\r\n ActionToString(signal.action),\r\n OrderTypeToStr(signal.order_type),\r\n signal.symbol);\r\n }\r\n\r\n //--- Persist sequence_num\r\n GlobalVariableSet(g_gvSeqName, (double)g_sequenceNum);\r\n\r\n //--- Update display\r\n g_display.Update(g_connStatus, g_signalsSentToday, g_queue.Count(), g_lastLatencyMs, g_lastSignalTime);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| TradeTransaction event handler |\r\n//+------------------------------------------------------------------+\r\nvoid OnTradeTransaction(const MqlTradeTransaction &trans,\r\n const MqlTradeRequest &request,\r\n const MqlTradeResult &result)\r\n {\r\n //--- We process TRADE_TRANSACTION_DEAL_ADD for position opens/closes\r\n //--- and TRADE_TRANSACTION_ORDER_ADD for pending orders\r\n //--- and TRADE_TRANSACTION_POSITION for modifications\r\n\r\n Signal signal;\r\n signal.magic_number = 0;\r\n signal.ticket = 0;\r\n signal.volume = 0;\r\n signal.price = 0;\r\n signal.sl = 0;\r\n signal.tp = 0;\r\n signal.symbol = \"\";\r\n signal.comment = \"\";\r\n signal.order_type = ORDER_TYPE_BUY;\r\n signal.action = SIGNAL_OPEN;\r\n\r\n //--- Handle deal additions (position open, close, partial close)\r\n if(trans.type == TRADE_TRANSACTION_DEAL_ADD)\r\n {\r\n //--- Get deal info\r\n ulong dealTicket = trans.deal;\r\n if(dealTicket == 0)\r\n return;\r\n\r\n if(!HistoryDealSelect(dealTicket))\r\n return;\r\n\r\n ENUM_DEAL_TYPE dealType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);\r\n ENUM_DEAL_ENTRY dealEntry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);\r\n\r\n //--- Only process buy/sell deals\r\n if(dealType != DEAL_TYPE_BUY && dealType != DEAL_TYPE_SELL)\r\n return;\r\n\r\n string sym = HistoryDealGetString(dealTicket, DEAL_SYMBOL);\r\n double vol = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);\r\n double dealPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);\r\n long magic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);\r\n long posId = HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);\r\n string dealComment = HistoryDealGetString(dealTicket, DEAL_COMMENT);\r\n\r\n //--- Apply filters\r\n if(!PassesSymbolFilter(sym))\r\n return;\r\n if(vol < MinLotFilter)\r\n return;\r\n\r\n //--- Map deal type to order type\r\n ENUM_ORDER_TYPE ordType = (dealType == DEAL_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;\r\n\r\n if(!PassesDirectionFilter(ordType))\r\n return;\r\n\r\n signal.symbol = sym;\r\n signal.volume = vol;\r\n signal.price = dealPrice;\r\n signal.magic_number = magic;\r\n signal.ticket = (long)posId;\r\n signal.comment = dealComment;\r\n signal.order_type = ordType;\r\n\r\n //--- Determine action based on deal entry\r\n if(dealEntry == DEAL_ENTRY_IN)\r\n {\r\n //--- New position opened\r\n signal.action = SIGNAL_OPEN;\r\n\r\n //--- Try to get SL/TP from the position\r\n if(PositionSelectByTicket((ulong)posId))\r\n {\r\n signal.sl = PositionGetDouble(POSITION_SL);\r\n signal.tp = PositionGetDouble(POSITION_TP);\r\n }\r\n }\r\n else if(dealEntry == DEAL_ENTRY_OUT)\r\n {\r\n //--- Position closed (full or partial)\r\n if(!CopyCloses)\r\n return;\r\n\r\n //--- Check if this is a partial close by seeing if position still exists\r\n if(PositionSelectByTicket((ulong)posId))\r\n {\r\n signal.action = SIGNAL_PARTIAL_CLOSE;\r\n signal.sl = PositionGetDouble(POSITION_SL);\r\n signal.tp = PositionGetDouble(POSITION_TP);\r\n }\r\n else\r\n {\r\n signal.action = SIGNAL_CLOSE;\r\n }\r\n }\r\n else if(dealEntry == DEAL_ENTRY_INOUT)\r\n {\r\n //--- Close-and-reverse: treat as close followed by open\r\n if(!CopyCloses)\r\n return;\r\n signal.action = SIGNAL_CLOSE;\r\n }\r\n else\r\n {\r\n return; // DEAL_ENTRY_STATE or unknown\r\n }\r\n\r\n DispatchSignal(signal);\r\n\r\n //--- Journal capture (if enabled)\r\n if(EnableJournal)\r\n {\r\n ulong jDeal = trans.deal;\r\n if(jDeal != 0)\r\n {\r\n bool jAlreadySynced = false;\r\n for(int ji = 0; ji < g_journalSyncedCount; ji++)\r\n if(g_journalSyncedDeals[ji] == jDeal) { jAlreadySynced = true; break; }\r\n\r\n if(!jAlreadySynced)\r\n {\r\n JournalTrade jTrade;\r\n if(CaptureDeal(jDeal, jTrade))\r\n {\r\n g_journalQueue.Enqueue(jTrade);\r\n ArrayResize(g_journalSyncedDeals, g_journalSyncedCount + 1);\r\n g_journalSyncedDeals[g_journalSyncedCount] = jDeal;\r\n g_journalSyncedCount++;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return;\r\n }\r\n\r\n //--- Handle pending order placement\r\n if(trans.type == TRADE_TRANSACTION_ORDER_ADD)\r\n {\r\n if(!CopyPendings)\r\n return;\r\n\r\n ulong orderTicket = trans.order;\r\n if(orderTicket == 0)\r\n return;\r\n\r\n //--- Check if this is a pending order (not a market order)\r\n ENUM_ORDER_TYPE ordType = trans.order_type;\r\n if(!IsPendingOrderType(ordType))\r\n return;\r\n\r\n string sym = trans.symbol;\r\n if(!PassesSymbolFilter(sym))\r\n return;\r\n if(!PassesDirectionFilter(ordType))\r\n return;\r\n\r\n signal.symbol = sym;\r\n signal.order_type = ordType;\r\n signal.volume = trans.volume;\r\n signal.price = trans.price;\r\n signal.sl = trans.price_sl;\r\n signal.tp = trans.price_tp;\r\n signal.ticket = (long)orderTicket;\r\n signal.action = SIGNAL_PENDING;\r\n\r\n if(signal.volume < MinLotFilter)\r\n return;\r\n\r\n DispatchSignal(signal);\r\n return;\r\n }\r\n\r\n //--- Handle position modification (SL/TP changes)\r\n if(trans.type == TRADE_TRANSACTION_POSITION)\r\n {\r\n if(!CopyModifications)\r\n return;\r\n\r\n ulong posTicket = trans.position;\r\n if(posTicket == 0)\r\n return;\r\n\r\n string sym = trans.symbol;\r\n if(!PassesSymbolFilter(sym))\r\n return;\r\n\r\n //--- Get current position details\r\n if(!PositionSelectByTicket(posTicket))\r\n return;\r\n\r\n ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);\r\n ENUM_ORDER_TYPE ordType = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;\r\n\r\n if(!PassesDirectionFilter(ordType))\r\n return;\r\n\r\n signal.symbol = sym;\r\n signal.order_type = ordType;\r\n signal.volume = PositionGetDouble(POSITION_VOLUME);\r\n signal.price = PositionGetDouble(POSITION_PRICE_OPEN);\r\n signal.sl = trans.price_sl;\r\n signal.tp = trans.price_tp;\r\n signal.magic_number = PositionGetInteger(POSITION_MAGIC);\r\n signal.ticket = (long)posTicket;\r\n signal.action = SIGNAL_MODIFY;\r\n\r\n DispatchSignal(signal);\r\n return;\r\n }\r\n\r\n //--- Handle pending order deletion\r\n if(trans.type == TRADE_TRANSACTION_ORDER_DELETE)\r\n {\r\n if(!CopyPendings)\r\n return;\r\n\r\n ulong orderTicket = trans.order;\r\n if(orderTicket == 0)\r\n return;\r\n\r\n //--- Only cancel if this was a pending order that was removed (not filled)\r\n ENUM_ORDER_TYPE ordType = trans.order_type;\r\n if(!IsPendingOrderType(ordType))\r\n return;\r\n\r\n string sym = trans.symbol;\r\n if(!PassesSymbolFilter(sym))\r\n return;\r\n if(!PassesDirectionFilter(ordType))\r\n return;\r\n\r\n signal.symbol = sym;\r\n signal.order_type = ordType;\r\n signal.volume = trans.volume;\r\n signal.price = trans.price;\r\n signal.ticket = (long)orderTicket;\r\n signal.action = SIGNAL_CANCEL_PENDING;\r\n\r\n DispatchSignal(signal);\r\n return;\r\n }\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\n//| Tick handler - not used for trade detection, but can be used |\r\n//| for additional monitoring if needed |\r\n//+------------------------------------------------------------------+\r\nvoid OnTick()\r\n {\r\n // Trade detection is handled by OnTradeTransaction.\r\n // OnTick is reserved for future use (e.g., price-based alerts).\r\n }\r\n//+------------------------------------------------------------------+\r\n", + "EdgeRelay_PropGuard.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_PropGuard.mqh |\r\n//| EdgeRelay PropGuard - Trade Rule Enforcement |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_PROPGUARD_MQH\r\n#define EDGERELAY_PROPGUARD_MQH\r\n\r\n#include \r\n#include \r\n\r\n//+------------------------------------------------------------------+\r\n//| PropGuard rule engine class |\r\n//| All checks use cached data — NO WebRequest calls. |\r\n//| Must complete in <1ms. |\r\n//+------------------------------------------------------------------+\r\nclass CPropGuard\r\n {\r\nprivate:\r\n PropGuardRules m_rules;\r\n CEquityTracker *m_equity;\r\n bool m_enabled;\r\n bool m_locked;\r\n\r\n //--- Cached news events\r\n struct NewsEvent\r\n {\r\n string currency;\r\n datetime event_time;\r\n string event_name;\r\n };\r\n NewsEvent m_newsEvents[];\r\n int m_newsCount;\r\n datetime m_newsLastFetch;\r\n\r\n //--- Blocked trade counters\r\n int m_blockedToday;\r\n string m_lastBlockedRule;\r\n string m_lastBlockedSymbol;\r\n datetime m_lastBlockedTime;\r\n\r\n //--- Individual checks (return true if ALLOWED)\r\n bool CheckInstrument(string symbol, PropGuardVerdict &verdict);\r\n bool CheckTradingHours(PropGuardVerdict &verdict);\r\n bool CheckNewsBlackout(string symbol, PropGuardVerdict &verdict);\r\n bool CheckMaxPositions(PropGuardVerdict &verdict);\r\n bool CheckLotSize(double volume, PropGuardVerdict &verdict);\r\n bool CheckDailyTradeCount(PropGuardVerdict &verdict);\r\n bool CheckDailyLoss(double volume, double sl, double price,\r\n string symbol, ENUM_ORDER_TYPE type, PropGuardVerdict &verdict);\r\n bool CheckTotalDrawdown(double volume, double sl, double price,\r\n string symbol, ENUM_ORDER_TYPE type, PropGuardVerdict &verdict);\r\n bool CheckConsistency(PropGuardVerdict &verdict);\r\n bool CheckWeekendHolding(PropGuardVerdict &verdict);\r\n\r\n //--- Helpers\r\n double CalculateWorstCaseLoss(string symbol, double volume, double sl,\r\n double price, ENUM_ORDER_TYPE type);\r\n void RecordBlock(PropGuardVerdict &verdict, string symbol);\r\n\r\npublic:\r\n CPropGuard();\r\n ~CPropGuard();\r\n\r\n void Init(const PropGuardRules &rules, CEquityTracker *equity);\r\n void SetRules(const PropGuardRules &rules);\r\n void SetEnabled(bool enabled) { m_enabled = enabled; }\r\n bool IsEnabled() const { return m_enabled; }\r\n bool IsLocked() const { return m_locked; }\r\n void Unlock() { m_locked = false; }\r\n\r\n PropGuardVerdict EvaluateTrade(string symbol, ENUM_ORDER_TYPE type,\r\n double volume, double price, double sl, double tp);\r\n\r\n int EmergencyCloseAll(string reason);\r\n\r\n void UpdateNewsCache(const string &jsonEvents);\r\n bool ShouldRefreshNews();\r\n\r\n int GetBlockedToday() const { return m_blockedToday; }\r\n string GetLastBlockedRule() const { return m_lastBlockedRule; }\r\n string GetLastBlockedSymbol() const { return m_lastBlockedSymbol; }\r\n datetime GetLastBlockedTime() const { return m_lastBlockedTime; }\r\n void ResetDailyBlockCount() { m_blockedToday = 0; }\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\nCPropGuard::CPropGuard()\r\n {\r\n m_enabled = false;\r\n m_locked = false;\r\n m_equity = NULL;\r\n m_newsCount = 0;\r\n m_newsLastFetch = 0;\r\n m_blockedToday = 0;\r\n m_lastBlockedRule = \"\";\r\n m_lastBlockedSymbol = \"\";\r\n m_lastBlockedTime = 0;\r\n ZeroMemory(m_rules);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nCPropGuard::~CPropGuard() {}\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuard::Init(const PropGuardRules &rules, CEquityTracker *equity)\r\n {\r\n m_rules = rules;\r\n m_equity = equity;\r\n m_enabled = true;\r\n m_locked = false;\r\n m_blockedToday = 0;\r\n Print(\"[PropGuard] Rule engine initialized. MaxDD=\",\r\n DoubleToString(rules.max_total_drawdown_percent, 1), \"%\",\r\n \" DailyLoss=\", DoubleToString(rules.max_daily_loss_percent, 1), \"%\",\r\n \" DDType=\", EnumToString(rules.drawdown_type));\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuard::SetRules(const PropGuardRules &rules)\r\n {\r\n m_rules = rules;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nPropGuardVerdict CPropGuard::EvaluateTrade(string symbol, ENUM_ORDER_TYPE type,\r\n double volume, double price, double sl, double tp)\r\n {\r\n PropGuardVerdict verdict;\r\n verdict.allowed = false;\r\n verdict.blocked_rule = \"\";\r\n verdict.blocked_reason = \"\";\r\n\r\n if(m_equity != NULL)\r\n {\r\n verdict.current_daily_loss_pct = MathAbs(MathMin(m_equity.GetDailyPnlPercent(), 0));\r\n verdict.current_drawdown_pct = m_equity.GetTotalDrawdownPercent();\r\n }\r\n else\r\n {\r\n verdict.current_daily_loss_pct = 0;\r\n verdict.current_drawdown_pct = 0;\r\n }\r\n verdict.projected_daily_loss_pct = verdict.current_daily_loss_pct;\r\n verdict.projected_drawdown_pct = verdict.current_drawdown_pct;\r\n\r\n if(!m_enabled)\r\n {\r\n verdict.allowed = true;\r\n return verdict;\r\n }\r\n\r\n if(m_locked)\r\n {\r\n verdict.blocked_rule = \"session_locked\";\r\n verdict.blocked_reason = \"Session locked after critical event. Restart EA to unlock.\";\r\n return verdict;\r\n }\r\n\r\n // 1. Instrument check\r\n if(!CheckInstrument(symbol, verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 2. Trading hours\r\n if(!CheckTradingHours(verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 3. News blackout\r\n if(!CheckNewsBlackout(symbol, verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 4. Max open positions\r\n if(!CheckMaxPositions(verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 5. Lot size\r\n if(!CheckLotSize(volume, verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 6. Daily trade count\r\n if(!CheckDailyTradeCount(verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 7. Daily loss projection\r\n if(!CheckDailyLoss(volume, sl, price, symbol, type, verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 8. Total drawdown projection\r\n if(!CheckTotalDrawdown(volume, sl, price, symbol, type, verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n // 9. Consistency check (warn only)\r\n CheckConsistency(verdict);\r\n // 10. Weekend holding\r\n if(!CheckWeekendHolding(verdict)) { RecordBlock(verdict, symbol); return verdict; }\r\n\r\n verdict.allowed = true;\r\n return verdict;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuard::RecordBlock(PropGuardVerdict &verdict, string symbol)\r\n {\r\n m_blockedToday++;\r\n m_lastBlockedRule = verdict.blocked_rule;\r\n m_lastBlockedSymbol = symbol;\r\n m_lastBlockedTime = TimeCurrent();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckInstrument(string symbol, PropGuardVerdict &verdict)\r\n {\r\n // Instrument filter — placeholder for symbol allow/block lists\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckTradingHours(PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.allowed_trading_start == \"00:00\" && m_rules.allowed_trading_end == \"23:59\")\r\n return true;\r\n\r\n MqlDateTime dt;\r\n TimeCurrent(dt);\r\n string currentTime = StringFormat(\"%02d:%02d\", dt.hour, dt.min);\r\n\r\n if(currentTime < m_rules.allowed_trading_start || currentTime > m_rules.allowed_trading_end)\r\n {\r\n verdict.blocked_rule = \"trading_hours\";\r\n verdict.blocked_reason = \"Outside trading hours (\" + m_rules.allowed_trading_start +\r\n \" - \" + m_rules.allowed_trading_end + \"). Current: \" + currentTime;\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckNewsBlackout(string symbol, PropGuardVerdict &verdict)\r\n {\r\n if(!m_rules.block_during_news || m_newsCount == 0)\r\n return true;\r\n\r\n datetime now = TimeCurrent();\r\n\r\n string base = StringSubstr(symbol, 0, 3);\r\n string quote = (StringLen(symbol) >= 6) ? StringSubstr(symbol, 3, 3) : \"\";\r\n\r\n for(int i = 0; i < m_newsCount; i++)\r\n {\r\n if(m_newsEvents[i].currency != base && m_newsEvents[i].currency != quote)\r\n continue;\r\n\r\n datetime eventTime = m_newsEvents[i].event_time;\r\n datetime blackoutStart = eventTime - m_rules.news_minutes_before * 60;\r\n datetime blackoutEnd = eventTime + m_rules.news_minutes_after * 60;\r\n\r\n if(now >= blackoutStart && now <= blackoutEnd)\r\n {\r\n verdict.blocked_rule = \"news_blackout\";\r\n verdict.blocked_reason = \"News blackout: \" + m_newsEvents[i].event_name +\r\n \" (\" + m_newsEvents[i].currency + \") at \" +\r\n TimeToString(eventTime, TIME_MINUTES);\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckMaxPositions(PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.max_open_positions <= 0)\r\n return true;\r\n\r\n int currentPositions = (m_equity != NULL) ? m_equity.GetPositionsOpen() : PositionsTotal();\r\n\r\n if(currentPositions >= m_rules.max_open_positions)\r\n {\r\n verdict.blocked_rule = \"max_positions\";\r\n verdict.blocked_reason = \"Max positions reached: \" + IntegerToString(currentPositions) +\r\n \"/\" + IntegerToString(m_rules.max_open_positions);\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckLotSize(double volume, PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.max_lot_size <= 0)\r\n return true;\r\n\r\n if(volume > m_rules.max_lot_size)\r\n {\r\n verdict.blocked_rule = \"max_lot_size\";\r\n verdict.blocked_reason = \"Lot size \" + DoubleToString(volume, 2) +\r\n \" exceeds max \" + DoubleToString(m_rules.max_lot_size, 2);\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckDailyTradeCount(PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.max_daily_trades <= 0)\r\n return true;\r\n\r\n int tradesToday = (m_equity != NULL) ? m_equity.GetTradesToday() : 0;\r\n\r\n if(tradesToday >= m_rules.max_daily_trades)\r\n {\r\n verdict.blocked_rule = \"max_daily_trades\";\r\n verdict.blocked_reason = \"Daily trade limit reached: \" + IntegerToString(tradesToday) +\r\n \"/\" + IntegerToString(m_rules.max_daily_trades);\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckDailyLoss(double volume, double sl, double price,\r\n string symbol, ENUM_ORDER_TYPE type, PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.max_daily_loss_percent <= 0 || m_equity == NULL)\r\n return true;\r\n\r\n double worstCase = CalculateWorstCaseLoss(symbol, volume, sl, price, type);\r\n double dailyRef = m_equity.GetBalanceStartOfDay();\r\n\r\n if(m_rules.daily_loss_calculation == DL_EQUITY_HIGH_OF_DAY)\r\n dailyRef = m_equity.GetEquityHighOfDay();\r\n\r\n double currentLoss = MathAbs(MathMin(m_equity.GetState().daily_pnl, 0));\r\n double projectedLoss = currentLoss + worstCase;\r\n double projectedPct = (dailyRef > 0) ? (projectedLoss / dailyRef) * 100.0 : 0;\r\n\r\n verdict.projected_daily_loss_pct = projectedPct;\r\n\r\n if(projectedPct >= m_rules.max_daily_loss_percent)\r\n {\r\n verdict.blocked_rule = \"daily_loss\";\r\n verdict.blocked_reason = \"Would breach daily loss: \" + DoubleToString(projectedPct, 2) +\r\n \"% (limit \" + DoubleToString(m_rules.max_daily_loss_percent, 1) + \"%)\";\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckTotalDrawdown(double volume, double sl, double price,\r\n string symbol, ENUM_ORDER_TYPE type, PropGuardVerdict &verdict)\r\n {\r\n if(m_rules.max_total_drawdown_percent <= 0 || m_equity == NULL)\r\n return true;\r\n\r\n double worstCase = CalculateWorstCaseLoss(symbol, volume, sl, price, type);\r\n double equity = m_equity.GetState().equity;\r\n double projectedEquity = equity - worstCase;\r\n\r\n double ddBase;\r\n if(m_rules.drawdown_type == DD_STATIC)\r\n {\r\n ddBase = (m_rules.initial_balance > 0) ? m_rules.initial_balance : m_equity.GetBalanceStartOfDay();\r\n }\r\n else\r\n {\r\n ddBase = m_equity.GetHighWaterMark();\r\n }\r\n\r\n double projectedDD = (ddBase > 0) ? ((ddBase - projectedEquity) / ddBase) * 100.0 : 0;\r\n if(projectedDD < 0) projectedDD = 0;\r\n\r\n verdict.projected_drawdown_pct = projectedDD;\r\n\r\n if(projectedDD >= m_rules.max_total_drawdown_percent)\r\n {\r\n verdict.blocked_rule = \"max_drawdown\";\r\n verdict.blocked_reason = \"Would breach drawdown: \" + DoubleToString(projectedDD, 2) +\r\n \"% (limit \" + DoubleToString(m_rules.max_total_drawdown_percent, 1) + \"%)\";\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckConsistency(PropGuardVerdict &verdict)\r\n {\r\n if(!m_rules.consistency_rule_enabled || m_equity == NULL)\r\n return true;\r\n\r\n double todayPnl = m_equity.GetState().daily_pnl;\r\n if(todayPnl <= 0) return true;\r\n\r\n double totalProfit = m_equity.GetState().equity -\r\n ((m_rules.initial_balance > 0) ? m_rules.initial_balance : m_equity.GetBalanceStartOfDay());\r\n if(totalProfit <= 0) return true;\r\n\r\n double dayRatio = (todayPnl / totalProfit) * 100.0;\r\n if(dayRatio > m_rules.max_profit_single_day_pct)\r\n {\r\n Print(\"[PropGuard] CONSISTENCY WARNING: Today's profit is \",\r\n DoubleToString(dayRatio, 1), \"% of total (max \",\r\n DoubleToString(m_rules.max_profit_single_day_pct, 1), \"%)\");\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::CheckWeekendHolding(PropGuardVerdict &verdict)\r\n {\r\n if(!m_rules.block_weekend_holding)\r\n return true;\r\n\r\n MqlDateTime dt;\r\n TimeCurrent(dt);\r\n\r\n if(dt.day_of_week != 5)\r\n return true;\r\n\r\n string currentTime = StringFormat(\"%02d:%02d\", dt.hour, dt.min);\r\n\r\n if(currentTime >= \"22:00\")\r\n {\r\n verdict.blocked_rule = \"weekend_holding\";\r\n verdict.blocked_reason = \"No new positions after Friday 22:00 (weekend holding block)\";\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ndouble CPropGuard::CalculateWorstCaseLoss(string symbol, double volume, double sl,\r\n double price, ENUM_ORDER_TYPE type)\r\n {\r\n double pointSize = SymbolInfoDouble(symbol, SYMBOL_POINT);\r\n double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);\r\n double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);\r\n\r\n if(pointSize <= 0 || tickValue <= 0 || tickSize <= 0)\r\n return volume * 100.0;\r\n\r\n double slDistance = 0;\r\n\r\n if(sl > 0 && price > 0)\r\n {\r\n slDistance = MathAbs(price - sl);\r\n }\r\n else\r\n {\r\n slDistance = 100.0 * pointSize;\r\n }\r\n\r\n double lossPerPoint = (tickValue / tickSize);\r\n double worstCase = volume * slDistance * lossPerPoint;\r\n\r\n return MathAbs(worstCase);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nint CPropGuard::EmergencyCloseAll(string reason)\r\n {\r\n int closed = 0;\r\n int total = PositionsTotal();\r\n\r\n Print(\"[PropGuard] EMERGENCY CLOSE ALL: \", reason, \" Positions=\", total);\r\n\r\n for(int i = total - 1; i >= 0; i--)\r\n {\r\n ulong ticket = PositionGetTicket(i);\r\n if(ticket <= 0) continue;\r\n\r\n string symbol = PositionGetString(POSITION_SYMBOL);\r\n double volume = PositionGetDouble(POSITION_VOLUME);\r\n ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);\r\n\r\n MqlTradeRequest request = {};\r\n MqlTradeResult result = {};\r\n\r\n request.action = TRADE_ACTION_DEAL;\r\n request.position = ticket;\r\n request.symbol = symbol;\r\n request.volume = volume;\r\n request.type = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;\r\n request.price = (posType == POSITION_TYPE_BUY)\r\n ? SymbolInfoDouble(symbol, SYMBOL_BID)\r\n : SymbolInfoDouble(symbol, SYMBOL_ASK);\r\n request.deviation = 50;\r\n request.type_filling = ORDER_FILLING_IOC;\r\n request.comment = \"PG:EMERGENCY\";\r\n\r\n if(OrderSend(request, result))\r\n {\r\n if(result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED)\r\n closed++;\r\n }\r\n else\r\n {\r\n Print(\"[PropGuard] Failed to close ticket \", ticket, \" retcode=\", result.retcode);\r\n }\r\n }\r\n\r\n m_locked = true;\r\n\r\n Print(\"[PropGuard] Emergency close complete. Closed=\", closed, \"/\", total);\r\n return closed;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuard::UpdateNewsCache(const string &jsonEvents)\r\n {\r\n m_newsCount = 0;\r\n ArrayResize(m_newsEvents, 50);\r\n\r\n int pos = 0;\r\n int len = StringLen(jsonEvents);\r\n\r\n while(pos < len && m_newsCount < 50)\r\n {\r\n int objStart = StringFind(jsonEvents, \"{\", pos);\r\n if(objStart < 0) break;\r\n int objEnd = StringFind(jsonEvents, \"}\", objStart);\r\n if(objEnd < 0) break;\r\n\r\n string obj = StringSubstr(jsonEvents, objStart, objEnd - objStart + 1);\r\n\r\n int curPos = StringFind(obj, \"\\\"currency\\\":\\\"\");\r\n if(curPos >= 0)\r\n {\r\n curPos += 12;\r\n int curEnd = StringFind(obj, \"\\\"\", curPos);\r\n m_newsEvents[m_newsCount].currency = StringSubstr(obj, curPos, curEnd - curPos);\r\n }\r\n\r\n int timePos = StringFind(obj, \"\\\"event_time\\\":\\\"\");\r\n if(timePos >= 0)\r\n {\r\n timePos += 14;\r\n int timeEnd = StringFind(obj, \"\\\"\", timePos);\r\n string timeStr = StringSubstr(obj, timePos, timeEnd - timePos);\r\n StringReplace(timeStr, \"T\", \" \");\r\n m_newsEvents[m_newsCount].event_time = StringToTime(timeStr);\r\n }\r\n\r\n int namePos = StringFind(obj, \"\\\"event_name\\\":\\\"\");\r\n if(namePos >= 0)\r\n {\r\n namePos += 14;\r\n int nameEnd = StringFind(obj, \"\\\"\", namePos);\r\n m_newsEvents[m_newsCount].event_name = StringSubstr(obj, namePos, nameEnd - namePos);\r\n }\r\n\r\n m_newsCount++;\r\n pos = objEnd + 1;\r\n }\r\n\r\n ArrayResize(m_newsEvents, m_newsCount);\r\n m_newsLastFetch = TimeCurrent();\r\n\r\n Print(\"[PropGuard] News cache updated. Events=\", m_newsCount);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nbool CPropGuard::ShouldRefreshNews()\r\n {\r\n if(!m_rules.block_during_news)\r\n return false;\r\n return (TimeCurrent() - m_newsLastFetch >= 4 * 3600);\r\n }\r\n\r\n#endif // EDGERELAY_PROPGUARD_MQH\r\n", + "EdgeRelay_PropGuardDisplay.mqh": "//+------------------------------------------------------------------+\r\n//| EdgeRelay_PropGuardDisplay.mqh |\r\n//| EdgeRelay PropGuard - On-Chart Status Panel |\r\n//| https://www.edgerelay.io |\r\n//+------------------------------------------------------------------+\r\n#property copyright \"EdgeRelay\"\r\n#property link \"https://www.edgerelay.io\"\r\n#property strict\r\n\r\n#ifndef EDGERELAY_PROPGUARD_DISPLAY_MQH\r\n#define EDGERELAY_PROPGUARD_DISPLAY_MQH\r\n\r\n#include \r\n#include \r\n#include \r\n\r\n//--- Object name constants\r\n#define PG_PREFIX \"PG_\"\r\n#define PG_PANEL_BG PG_PREFIX + \"BG\"\r\n#define PG_TITLE PG_PREFIX + \"Title\"\r\n#define PG_STATUS_DOT PG_PREFIX + \"StatusDot\"\r\n#define PG_STATUS_TEXT PG_PREFIX + \"StatusText\"\r\n#define PG_PRESET_TEXT PG_PREFIX + \"PresetText\"\r\n#define PG_PROFIT_LABEL PG_PREFIX + \"ProfitLabel\"\r\n#define PG_PROFIT_BAR_BG PG_PREFIX + \"ProfitBarBG\"\r\n#define PG_PROFIT_BAR PG_PREFIX + \"ProfitBar\"\r\n#define PG_PROFIT_TEXT PG_PREFIX + \"ProfitText\"\r\n#define PG_DAILY_DD_LABEL PG_PREFIX + \"DailyDDLabel\"\r\n#define PG_DAILY_DD_BAR_BG PG_PREFIX + \"DailyDDBarBG\"\r\n#define PG_DAILY_DD_BAR PG_PREFIX + \"DailyDDBar\"\r\n#define PG_DAILY_DD_TEXT PG_PREFIX + \"DailyDDText\"\r\n#define PG_TOTAL_DD_LABEL PG_PREFIX + \"TotalDDLabel\"\r\n#define PG_TOTAL_DD_BAR_BG PG_PREFIX + \"TotalDDBarBG\"\r\n#define PG_TOTAL_DD_BAR PG_PREFIX + \"TotalDDBar\"\r\n#define PG_TOTAL_DD_TEXT PG_PREFIX + \"TotalDDText\"\r\n#define PG_TRADES_TEXT PG_PREFIX + \"TradesText\"\r\n#define PG_MAX_LOT_TEXT PG_PREFIX + \"MaxLotText\"\r\n#define PG_LAST_BLOCK_TEXT PG_PREFIX + \"LastBlockText\"\r\n#define PG_COPIER_TEXT PG_PREFIX + \"CopierText\"\r\n\r\n//--- Panel dimensions\r\n#define PG_WIDTH 300\r\n#define PG_HEIGHT 220\r\n#define PG_LINE_H 18\r\n#define PG_BAR_WIDTH 120\r\n#define PG_BAR_HEIGHT 10\r\n\r\n//--- Colors\r\n#define CLR_SAFE C'0,200,83'\r\n#define CLR_WARN C'255,179,0'\r\n#define CLR_DANGER C'255,23,68'\r\n#define CLR_PANEL_BG C'26,26,46'\r\n#define CLR_PANEL_BORDER C'60,60,80'\r\n#define CLR_TEXT_DIM C'140,140,160'\r\n#define CLR_TEXT_BRIGHT C'220,220,240'\r\n#define CLR_BAR_BG C'40,40,60'\r\n\r\n//+------------------------------------------------------------------+\r\nclass CPropGuardDisplay\r\n {\r\nprivate:\r\n bool m_initialized;\r\n int m_x;\r\n int m_y;\r\n\r\n void CreateLabel(string name, int x, int y, string text,\r\n color clr, int fontSize = 8, string font = \"Consolas\");\r\n void CreateBar(string bgName, string barName, int x, int y,\r\n int width, int height);\r\n void UpdateLabel(string name, string text, color clr = clrNONE);\r\n void UpdateBar(string barName, double percent, color clr);\r\n color GetThresholdColor(double value, double limit, double warnPct, double critPct);\r\n\r\npublic:\r\n CPropGuardDisplay();\r\n ~CPropGuardDisplay();\r\n\r\n void Init(int x, int y, color bgColor);\r\n void Update(CEquityTracker *equity, CPropGuard *guard, string presetName,\r\n int connStatus, double latencyMs);\r\n void Deinit();\r\n };\r\n\r\n//+------------------------------------------------------------------+\r\nCPropGuardDisplay::CPropGuardDisplay()\r\n {\r\n m_initialized = false;\r\n m_x = 10;\r\n m_y = 30;\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nCPropGuardDisplay::~CPropGuardDisplay()\r\n {\r\n if(m_initialized) Deinit();\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::Init(int x, int y, color bgColor)\r\n {\r\n m_x = x;\r\n m_y = y;\r\n\r\n //--- Background panel\r\n ObjectCreate(0, PG_PANEL_BG, OBJ_RECTANGLE_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_XDISTANCE, m_x);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_YDISTANCE, m_y);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_XSIZE, PG_WIDTH);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_YSIZE, PG_HEIGHT);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_BGCOLOR, bgColor);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_BORDER_COLOR, CLR_PANEL_BORDER);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_BORDER_TYPE, BORDER_FLAT);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_WIDTH, 1);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_BACK, false);\r\n ObjectSetInteger(0, PG_PANEL_BG, OBJPROP_SELECTABLE, false);\r\n\r\n int cx = m_x + 12;\r\n int cy = m_y + 8;\r\n\r\n CreateLabel(PG_STATUS_DOT, cx, cy, \"l\", clrGray, 10, \"Wingdings\");\r\n CreateLabel(PG_TITLE, cx + 16, cy, \"PropGuard\", CLR_TEXT_BRIGHT, 9, \"Consolas\");\r\n CreateLabel(PG_STATUS_TEXT, cx + 90, cy, \"INITIALIZING\", clrGray, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_PRESET_TEXT, cx, cy, \"Loading preset...\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H + 2;\r\n\r\n CreateLabel(PG_PROFIT_LABEL, cx, cy, \"Profit:\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n CreateBar(PG_PROFIT_BAR_BG, PG_PROFIT_BAR, cx + 55, cy + 2, PG_BAR_WIDTH, PG_BAR_HEIGHT);\r\n CreateLabel(PG_PROFIT_TEXT, cx + 55 + PG_BAR_WIDTH + 5, cy, \"0%\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_DAILY_DD_LABEL, cx, cy, \"Daily DD:\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n CreateBar(PG_DAILY_DD_BAR_BG, PG_DAILY_DD_BAR, cx + 55, cy + 2, PG_BAR_WIDTH, PG_BAR_HEIGHT);\r\n CreateLabel(PG_DAILY_DD_TEXT, cx + 55 + PG_BAR_WIDTH + 5, cy, \"0%\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_TOTAL_DD_LABEL, cx, cy, \"Total DD:\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n CreateBar(PG_TOTAL_DD_BAR_BG, PG_TOTAL_DD_BAR, cx + 55, cy + 2, PG_BAR_WIDTH, PG_BAR_HEIGHT);\r\n CreateLabel(PG_TOTAL_DD_TEXT, cx + 55 + PG_BAR_WIDTH + 5, cy, \"0%\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_TRADES_TEXT, cx, cy, \"Trades: 0 today | 0 open\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H + 2;\r\n\r\n CreateLabel(PG_MAX_LOT_TEXT, cx, cy, \"Max safe lot: --\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_LAST_BLOCK_TEXT, cx, cy, \"Last block: none\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n cy += PG_LINE_H;\r\n\r\n CreateLabel(PG_COPIER_TEXT, cx, cy, \"Copier: -- | --ms\", CLR_TEXT_DIM, 8, \"Consolas\");\r\n\r\n m_initialized = true;\r\n ChartRedraw(0);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::Update(CEquityTracker *equity, CPropGuard *guard,\r\n string presetName, int connStatus, double latencyMs)\r\n {\r\n if(!m_initialized || equity == NULL || guard == NULL) return;\r\n\r\n EquityState state = equity.GetState();\r\n ENUM_PROPGUARD_STATUS pgStatus = equity.GetStatus();\r\n\r\n //--- Status dot + text\r\n string statusStr = \"\";\r\n color dotColor = clrGray;\r\n switch(pgStatus)\r\n {\r\n case PG_PROTECTED: statusStr = \"PROTECTED\"; dotColor = CLR_SAFE; break;\r\n case PG_WARNING: statusStr = \"WARNING\"; dotColor = CLR_WARN; break;\r\n case PG_CRITICAL: statusStr = \"CRITICAL\"; dotColor = CLR_DANGER; break;\r\n case PG_LOCKED: statusStr = \"LOCKED\"; dotColor = CLR_DANGER; break;\r\n case PG_DISABLED: statusStr = \"DISABLED\"; dotColor = clrGray; break;\r\n }\r\n if(guard.IsLocked()) { statusStr = \"LOCKED\"; dotColor = CLR_DANGER; }\r\n\r\n UpdateLabel(PG_STATUS_DOT, \"l\", dotColor);\r\n UpdateLabel(PG_STATUS_TEXT, statusStr, dotColor);\r\n UpdateLabel(PG_PRESET_TEXT, presetName, CLR_TEXT_DIM);\r\n\r\n //--- Profit progress\r\n double profitPct = equity.GetProfitPercent();\r\n double profitTarget = equity.GetProfitTarget();\r\n double profitRatio = (profitTarget > 0) ? MathMin(profitPct / profitTarget, 1.0) : 0;\r\n if(profitRatio < 0) profitRatio = 0;\r\n string profitStr = DoubleToString(profitPct, 1) + \"/\" + DoubleToString(profitTarget, 0) + \"%\";\r\n UpdateBar(PG_PROFIT_BAR, profitRatio * 100.0, CLR_SAFE);\r\n UpdateLabel(PG_PROFIT_TEXT, profitStr, CLR_TEXT_BRIGHT);\r\n\r\n //--- Daily DD (use dynamic thresholds from rules)\r\n double dailyLossPct = MathAbs(MathMin(state.daily_pnl_percent, 0));\r\n // Bar shows % of limit consumed\r\n double dailyBarPct = 0;\r\n double maxDailyLoss = equity.GetProfitTarget(); // placeholder — we need rules\r\n // Since we can't access rules directly, use the ratio approach from GetStatus\r\n color dailyClr = CLR_SAFE;\r\n if(pgStatus == PG_CRITICAL) dailyClr = CLR_DANGER;\r\n else if(pgStatus == PG_WARNING) dailyClr = CLR_WARN;\r\n // Simple bar: show as percentage of 5% (common limit)\r\n dailyBarPct = MathMin(dailyLossPct * 20.0, 100.0);\r\n string dailyStr = DoubleToString(dailyLossPct, 2) + \"%\";\r\n UpdateBar(PG_DAILY_DD_BAR, dailyBarPct, dailyClr);\r\n UpdateLabel(PG_DAILY_DD_TEXT, dailyStr, dailyClr);\r\n\r\n //--- Total DD\r\n double totalDD = state.total_drawdown_pct;\r\n double totalBarPct = MathMin(totalDD * 10.0, 100.0);\r\n color totalClr = CLR_SAFE;\r\n if(pgStatus == PG_CRITICAL) totalClr = CLR_DANGER;\r\n else if(pgStatus == PG_WARNING) totalClr = CLR_WARN;\r\n string totalStr = DoubleToString(totalDD, 2) + \"%\";\r\n UpdateBar(PG_TOTAL_DD_BAR, totalBarPct, totalClr);\r\n UpdateLabel(PG_TOTAL_DD_TEXT, totalStr, totalClr);\r\n\r\n //--- Trades\r\n string tradesStr = \"Trades: \" + IntegerToString(state.trades_today) +\r\n \" today | \" + IntegerToString(state.positions_open) + \" open\";\r\n UpdateLabel(PG_TRADES_TEXT, tradesStr, CLR_TEXT_DIM);\r\n\r\n //--- Max safe lot\r\n string currentSymbol = Symbol();\r\n double maxLot = equity.CalculateMaxSafeLot(currentSymbol);\r\n string maxLotStr = \"Max safe lot: \" + DoubleToString(maxLot, 2) + \" (\" + currentSymbol + \")\";\r\n UpdateLabel(PG_MAX_LOT_TEXT, maxLotStr, CLR_TEXT_DIM);\r\n\r\n //--- Last block\r\n if(guard.GetBlockedToday() > 0)\r\n {\r\n MqlDateTime blockDt;\r\n TimeToStruct(guard.GetLastBlockedTime(), blockDt);\r\n string blockTimeStr = StringFormat(\"%02d:%02d\", blockDt.hour, blockDt.min);\r\n string blockStr = \"Last block: \" + guard.GetLastBlockedSymbol() +\r\n \" @ \" + blockTimeStr + \" - \" + guard.GetLastBlockedRule();\r\n UpdateLabel(PG_LAST_BLOCK_TEXT, blockStr, CLR_WARN);\r\n }\r\n\r\n //--- Copier status\r\n string connStr = (connStatus == STATUS_CONNECTED) ? \"Connected\" : \"Disconnected\";\r\n color connClr = (connStatus == STATUS_CONNECTED) ? CLR_SAFE : CLR_DANGER;\r\n string latStr = (latencyMs >= 0) ? DoubleToString(latencyMs, 0) + \"ms\" : \"--\";\r\n UpdateLabel(PG_COPIER_TEXT, \"Copier: \" + connStr + \" | \" + latStr, connClr);\r\n\r\n ChartRedraw(0);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::Deinit()\r\n {\r\n ObjectsDeleteAll(0, PG_PREFIX);\r\n m_initialized = false;\r\n ChartRedraw(0);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::CreateLabel(string name, int x, int y, string text,\r\n color clr, int fontSize, string font)\r\n {\r\n ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);\r\n ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);\r\n ObjectSetInteger(0, name, OBJPROP_COLOR, clr);\r\n ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);\r\n ObjectSetString(0, name, OBJPROP_FONT, font);\r\n ObjectSetString(0, name, OBJPROP_TEXT, text);\r\n ObjectSetInteger(0, name, OBJPROP_BACK, false);\r\n ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::CreateBar(string bgName, string barName, int x, int y,\r\n int width, int height)\r\n {\r\n ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, bgName, OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, x);\r\n ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, y);\r\n ObjectSetInteger(0, bgName, OBJPROP_XSIZE, width);\r\n ObjectSetInteger(0, bgName, OBJPROP_YSIZE, height);\r\n ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, CLR_BAR_BG);\r\n ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, BORDER_FLAT);\r\n ObjectSetInteger(0, bgName, OBJPROP_BORDER_COLOR, CLR_BAR_BG);\r\n ObjectSetInteger(0, bgName, OBJPROP_BACK, false);\r\n ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);\r\n\r\n ObjectCreate(0, barName, OBJ_RECTANGLE_LABEL, 0, 0, 0);\r\n ObjectSetInteger(0, barName, OBJPROP_CORNER, CORNER_LEFT_UPPER);\r\n ObjectSetInteger(0, barName, OBJPROP_XDISTANCE, x);\r\n ObjectSetInteger(0, barName, OBJPROP_YDISTANCE, y);\r\n ObjectSetInteger(0, barName, OBJPROP_XSIZE, 1);\r\n ObjectSetInteger(0, barName, OBJPROP_YSIZE, height);\r\n ObjectSetInteger(0, barName, OBJPROP_BGCOLOR, CLR_SAFE);\r\n ObjectSetInteger(0, barName, OBJPROP_BORDER_TYPE, BORDER_FLAT);\r\n ObjectSetInteger(0, barName, OBJPROP_BORDER_COLOR, CLR_SAFE);\r\n ObjectSetInteger(0, barName, OBJPROP_BACK, false);\r\n ObjectSetInteger(0, barName, OBJPROP_SELECTABLE, false);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::UpdateLabel(string name, string text, color clr)\r\n {\r\n ObjectSetString(0, name, OBJPROP_TEXT, text);\r\n if(clr != clrNONE)\r\n ObjectSetInteger(0, name, OBJPROP_COLOR, clr);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\nvoid CPropGuardDisplay::UpdateBar(string barName, double percent, color clr)\r\n {\r\n int width = (int)MathRound(PG_BAR_WIDTH * MathMin(percent, 100.0) / 100.0);\r\n if(width < 1) width = 1;\r\n ObjectSetInteger(0, barName, OBJPROP_XSIZE, width);\r\n ObjectSetInteger(0, barName, OBJPROP_BGCOLOR, clr);\r\n ObjectSetInteger(0, barName, OBJPROP_BORDER_COLOR, clr);\r\n }\r\n\r\n//+------------------------------------------------------------------+\r\ncolor CPropGuardDisplay::GetThresholdColor(double value, double limit, double warnPct, double critPct)\r\n {\r\n if(limit <= 0) return CLR_SAFE;\r\n double ratio = (value / limit) * 100.0;\r\n if(ratio >= critPct) return CLR_DANGER;\r\n if(ratio >= warnPct) return CLR_WARN;\r\n return CLR_SAFE;\r\n }\r\n\r\n#endif // EDGERELAY_PROPGUARD_DISPLAY_MQH\r\n", + "EdgeRelay_Queue.mqh": "//+------------------------------------------------------------------+\n//| EdgeRelay_Queue.mqh |\n//| EdgeRelay Master EA - Offline Queue |\n//| https://www.edgerelay.io |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property strict\n\n#ifndef EDGERELAY_QUEUE_MQH\n#define EDGERELAY_QUEUE_MQH\n\n#include \n#include \n\n//--- Forward declarations for helper functions\nENUM_SIGNAL_ACTION StringToAction(string action);\nENUM_ORDER_TYPE StringToOrderType(string orderType);\n\n//+------------------------------------------------------------------+\n//| Signal queue class - file-backed for crash resilience |\n//| File format: one JSON signal per line |\n//+------------------------------------------------------------------+\nclass CSignalQueue\n {\nprivate:\n string m_filename;\n int m_count;\n\n //--- Re-count lines in the queue file\n void Recount();\n\n //--- Parse a JSON line back into a Signal struct\n bool ParseSignalLine(string line, Signal &signal);\n\n //--- Extract a JSON string value by key\n string ExtractJsonString(string &json, string key);\n\n //--- Extract a JSON numeric value by key (as string)\n string ExtractJsonNumber(string &json, string key);\n\npublic:\n CSignalQueue();\n ~CSignalQueue();\n\n void Init(string filename);\n bool Enqueue(Signal &signal);\n bool Dequeue(Signal &signal);\n int Count();\n void Clear();\n bool IsEmpty();\n void Flush(string endpoint, string apiKey);\n };\n\n//+------------------------------------------------------------------+\n//| Constructor |\n//+------------------------------------------------------------------+\nCSignalQueue::CSignalQueue()\n {\n m_filename = \"\";\n m_count = 0;\n }\n\n//+------------------------------------------------------------------+\n//| Destructor |\n//+------------------------------------------------------------------+\nCSignalQueue::~CSignalQueue()\n {\n }\n\n//+------------------------------------------------------------------+\n//| Initialize queue with file path |\n//+------------------------------------------------------------------+\nvoid CSignalQueue::Init(string filename)\n {\n m_filename = filename;\n\n //--- Create file if it doesn't exist\n int handle = FileOpen(m_filename, FILE_READ | FILE_WRITE | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle != INVALID_HANDLE)\n FileClose(handle);\n\n Recount();\n PrintFormat(\"[EdgeRelay] Queue initialized: %s (%d signals pending)\", m_filename, m_count);\n }\n\n//+------------------------------------------------------------------+\n//| Append a signal to the queue file |\n//+------------------------------------------------------------------+\nbool CSignalQueue::Enqueue(Signal &signal)\n {\n if(m_count >= MAX_QUEUE_SIZE)\n {\n PrintFormat(\"[EdgeRelay] Queue full (%d/%d), dropping signal %s\",\n m_count, MAX_QUEUE_SIZE, signal.signal_id);\n return false;\n }\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_WRITE | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n {\n PrintFormat(\"[EdgeRelay] Failed to open queue file for write: %d\", GetLastError());\n return false;\n }\n\n //--- Seek to end of file\n FileSeek(handle, 0, SEEK_END);\n\n //--- Write JSON line\n string json = SignalToJson(signal);\n FileWriteString(handle, json + \"\\n\");\n\n FileClose(handle);\n\n m_count++;\n PrintFormat(\"[EdgeRelay] Signal enqueued: %s (queue size: %d)\", signal.signal_id, m_count);\n return true;\n }\n\n//+------------------------------------------------------------------+\n//| Read and remove the oldest signal from the queue |\n//+------------------------------------------------------------------+\nbool CSignalQueue::Dequeue(Signal &signal)\n {\n if(m_count <= 0)\n return false;\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n return false;\n\n //--- Read all lines\n string lines[];\n int lineCount = 0;\n\n while(!FileIsEnding(handle))\n {\n string line = FileReadString(handle);\n if(StringLen(line) > 0)\n {\n ArrayResize(lines, lineCount + 1);\n lines[lineCount] = line;\n lineCount++;\n }\n }\n FileClose(handle);\n\n if(lineCount == 0)\n {\n m_count = 0;\n return false;\n }\n\n //--- Parse first line into signal\n if(!ParseSignalLine(lines[0], signal))\n return false;\n\n //--- Rewrite file without the first line\n handle = FileOpen(m_filename, FILE_WRITE | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n return false;\n\n for(int i = 1; i < lineCount; i++)\n FileWriteString(handle, lines[i] + \"\\n\");\n\n FileClose(handle);\n\n m_count = lineCount - 1;\n return true;\n }\n\n//+------------------------------------------------------------------+\n//| Return number of queued signals |\n//+------------------------------------------------------------------+\nint CSignalQueue::Count()\n {\n return m_count;\n }\n\n//+------------------------------------------------------------------+\n//| Clear all queued signals |\n//+------------------------------------------------------------------+\nvoid CSignalQueue::Clear()\n {\n int handle = FileOpen(m_filename, FILE_WRITE | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle != INVALID_HANDLE)\n FileClose(handle);\n\n m_count = 0;\n Print(\"[EdgeRelay] Queue cleared\");\n }\n\n//+------------------------------------------------------------------+\n//| Check if queue is empty |\n//+------------------------------------------------------------------+\nbool CSignalQueue::IsEmpty()\n {\n return (m_count <= 0);\n }\n\n//+------------------------------------------------------------------+\n//| Flush queue - attempt to send all queued signals |\n//| Successfully sent signals are removed |\n//+------------------------------------------------------------------+\nvoid CSignalQueue::Flush(string endpoint, string apiKey)\n {\n if(m_count <= 0)\n return;\n\n PrintFormat(\"[EdgeRelay] Flushing queue (%d signals)...\", m_count);\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n return;\n\n //--- Read all lines\n string lines[];\n int lineCount = 0;\n\n while(!FileIsEnding(handle))\n {\n string line = FileReadString(handle);\n if(StringLen(line) > 0)\n {\n ArrayResize(lines, lineCount + 1);\n lines[lineCount] = line;\n lineCount++;\n }\n }\n FileClose(handle);\n\n //--- Try to send each signal, keep failed ones\n string failedLines[];\n int failedCount = 0;\n int sentCount = 0;\n\n for(int i = 0; i < lineCount; i++)\n {\n Signal sig;\n if(!ParseSignalLine(lines[i], sig))\n {\n //--- Skip unparseable lines\n PrintFormat(\"[EdgeRelay] Skipping unparseable queue line %d\", i);\n continue;\n }\n\n //--- Check signal age - drop signals older than MAX_SIGNAL_AGE_S\n if((long)TimeCurrent() - (long)sig.timestamp > MAX_SIGNAL_AGE_S)\n {\n PrintFormat(\"[EdgeRelay] Dropping expired signal: %s (age > %ds)\",\n sig.signal_id, MAX_SIGNAL_AGE_S);\n continue;\n }\n\n int httpCode = SendSignal(endpoint, apiKey, sig);\n if(httpCode == 200 || httpCode == 201)\n {\n sentCount++;\n }\n else\n {\n //--- Keep for retry\n ArrayResize(failedLines, failedCount + 1);\n failedLines[failedCount] = lines[i];\n failedCount++;\n //--- Stop trying if server is unreachable\n if(httpCode == -1)\n {\n //--- Keep remaining lines too\n for(int j = i + 1; j < lineCount; j++)\n {\n ArrayResize(failedLines, failedCount + 1);\n failedLines[failedCount] = lines[j];\n failedCount++;\n }\n break;\n }\n }\n }\n\n //--- Rewrite file with only failed lines\n handle = FileOpen(m_filename, FILE_WRITE | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle != INVALID_HANDLE)\n {\n for(int i = 0; i < failedCount; i++)\n FileWriteString(handle, failedLines[i] + \"\\n\");\n FileClose(handle);\n }\n\n m_count = failedCount;\n PrintFormat(\"[EdgeRelay] Queue flush complete: %d sent, %d remaining\", sentCount, failedCount);\n }\n\n//+------------------------------------------------------------------+\n//| Re-count lines in the queue file |\n//+------------------------------------------------------------------+\nvoid CSignalQueue::Recount()\n {\n m_count = 0;\n\n int handle = FileOpen(m_filename, FILE_READ | FILE_TXT |\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI);\n if(handle == INVALID_HANDLE)\n return;\n\n while(!FileIsEnding(handle))\n {\n string line = FileReadString(handle);\n if(StringLen(line) > 0)\n m_count++;\n }\n\n FileClose(handle);\n }\n\n//+------------------------------------------------------------------+\n//| Extract a JSON string value by key |\n//+------------------------------------------------------------------+\nstring CSignalQueue::ExtractJsonString(string &json, string key)\n {\n string searchKey = \"\\\"\" + key + \"\\\":\\\"\";\n int pos = StringFind(json, searchKey);\n if(pos < 0)\n return \"\";\n\n int start = pos + StringLen(searchKey);\n int end = StringFind(json, \"\\\"\", start);\n if(end < 0)\n return \"\";\n\n return StringSubstr(json, start, end - start);\n }\n\n//+------------------------------------------------------------------+\n//| Extract a JSON numeric value by key |\n//+------------------------------------------------------------------+\nstring CSignalQueue::ExtractJsonNumber(string &json, string key)\n {\n string searchKey = \"\\\"\" + key + \"\\\":\";\n int pos = StringFind(json, searchKey);\n if(pos < 0)\n return \"\";\n\n int start = pos + StringLen(searchKey);\n\n //--- Find end of number (comma, closing brace, or end of string)\n int end = start;\n int len = StringLen(json);\n while(end < len)\n {\n ushort ch = StringGetCharacter(json, end);\n if(ch == ',' || ch == '}' || ch == ' ' || ch == '\\n' || ch == '\\r')\n break;\n end++;\n }\n\n return StringSubstr(json, start, end - start);\n }\n\n//+------------------------------------------------------------------+\n//| Parse action string back to enum |\n//+------------------------------------------------------------------+\nENUM_SIGNAL_ACTION StringToAction(string action)\n {\n if(action == \"OPEN\") return SIGNAL_OPEN;\n if(action == \"MODIFY\") return SIGNAL_MODIFY;\n if(action == \"PARTIAL_CLOSE\") return SIGNAL_PARTIAL_CLOSE;\n if(action == \"CLOSE\") return SIGNAL_CLOSE;\n if(action == \"PENDING\") return SIGNAL_PENDING;\n if(action == \"CANCEL_PENDING\") return SIGNAL_CANCEL_PENDING;\n return SIGNAL_OPEN;\n }\n\n//+------------------------------------------------------------------+\n//| Parse order type string back to enum |\n//+------------------------------------------------------------------+\nENUM_ORDER_TYPE StringToOrderType(string orderType)\n {\n if(orderType == \"BUY\") return ORDER_TYPE_BUY;\n if(orderType == \"SELL\") return ORDER_TYPE_SELL;\n if(orderType == \"BUY_LIMIT\") return ORDER_TYPE_BUY_LIMIT;\n if(orderType == \"SELL_LIMIT\") return ORDER_TYPE_SELL_LIMIT;\n if(orderType == \"BUY_STOP\") return ORDER_TYPE_BUY_STOP;\n if(orderType == \"SELL_STOP\") return ORDER_TYPE_SELL_STOP;\n if(orderType == \"BUY_STOP_LIMIT\") return ORDER_TYPE_BUY_STOP_LIMIT;\n if(orderType == \"SELL_STOP_LIMIT\") return ORDER_TYPE_SELL_STOP_LIMIT;\n if(orderType == \"CLOSE_BY\") return ORDER_TYPE_CLOSE_BY;\n return ORDER_TYPE_BUY;\n }\n\n//+------------------------------------------------------------------+\n//| Parse a JSON line back into a Signal struct |\n//+------------------------------------------------------------------+\nbool CSignalQueue::ParseSignalLine(string line, Signal &signal)\n {\n if(StringLen(line) < 10)\n return false;\n\n signal.signal_id = ExtractJsonString(line, \"signal_id\");\n signal.account_id = ExtractJsonString(line, \"account_id\");\n signal.sequence_num = (int)StringToInteger(ExtractJsonNumber(line, \"sequence_num\"));\n signal.action = StringToAction(ExtractJsonString(line, \"action\"));\n signal.order_type = StringToOrderType(ExtractJsonString(line, \"order_type\"));\n signal.symbol = ExtractJsonString(line, \"symbol\");\n signal.volume = StringToDouble(ExtractJsonNumber(line, \"volume\"));\n signal.price = StringToDouble(ExtractJsonNumber(line, \"price\"));\n signal.sl = StringToDouble(ExtractJsonNumber(line, \"sl\"));\n signal.tp = StringToDouble(ExtractJsonNumber(line, \"tp\"));\n signal.magic_number = StringToInteger(ExtractJsonNumber(line, \"magic_number\"));\n signal.ticket = StringToInteger(ExtractJsonNumber(line, \"ticket\"));\n signal.comment = ExtractJsonString(line, \"comment\");\n signal.timestamp = (datetime)StringToInteger(ExtractJsonNumber(line, \"timestamp\"));\n signal.hmac_signature = ExtractJsonString(line, \"hmac_signature\");\n\n return (StringLen(signal.signal_id) > 0);\n }\n\n#endif // EDGERELAY_QUEUE_MQH\n", + "TradeJournal_Sync.mq5": "//+------------------------------------------------------------------+\n//| TradeJournal_Sync.mq5 |\n//| EdgeRelay — Zero-Drop Trade Journal Sync |\n//+------------------------------------------------------------------+\n#property copyright \"EdgeRelay\"\n#property link \"https://www.edgerelay.io\"\n#property version \"1.00\"\n#property description \"Syncs every trade to your EdgeRelay journal — zero drops guaranteed.\"\n#property strict\n\n#include \n#include \n#include \n#include \n#include \n\n//+------------------------------------------------------------------+\n//| Input parameters |\n//+------------------------------------------------------------------+\ninput string API_Key = \"\"; // API Key\ninput string API_Secret = \"\"; // API Secret\ninput string API_Endpoint = \"https://edgerelay-journal-sync.ghwmelite.workers.dev\"; // Journal Endpoint\ninput string AccountID = \"\"; // Account ID\ninput int SyncIntervalSeconds = 60; // History scan interval (s)\ninput int HeartbeatIntervalMs = 30000; // Heartbeat interval (ms)\n\n//--- Global variables\nCJournalQueue g_journalQueue;\nENUM_CONNECTION_STATUS g_connStatus = STATUS_DISCONNECTED;\n\n//--- Synced deal tracking\nulong g_syncedDeals[];\nint g_syncedCount = 0;\nstring g_gvLastDeal = \"\";\ndatetime g_lastHistoryScan = 0;\n\n//+------------------------------------------------------------------+\n//| Check if a deal ticket has already been synced |\n//+------------------------------------------------------------------+\nbool IsDealSynced(ulong dealTicket)\n {\n for(int i = 0; i < g_syncedCount; i++)\n if(g_syncedDeals[i] == dealTicket)\n return true;\n return false;\n }\n\n//+------------------------------------------------------------------+\n//| Mark a deal ticket as synced |\n//+------------------------------------------------------------------+\nvoid MarkDealSynced(ulong dealTicket)\n {\n ArrayResize(g_syncedDeals, g_syncedCount + 1);\n g_syncedDeals[g_syncedCount] = dealTicket;\n g_syncedCount++;\n }\n\n//+------------------------------------------------------------------+\n//| Expert initialization |\n//+------------------------------------------------------------------+\nint OnInit()\n {\n if(StringLen(API_Key) == 0 || StringLen(API_Secret) == 0 || StringLen(AccountID) == 0)\n {\n Alert(\"[Journal] API_Key, API_Secret, and AccountID are required.\");\n return INIT_PARAMETERS_INCORRECT;\n }\n\n //--- Initialize queue\n string queueFile = \"JournalSync_Queue_\" + AccountID + \".txt\";\n g_journalQueue.Init(queueFile);\n\n //--- Restore last synced deal from GlobalVariable\n g_gvLastDeal = \"JournalSync_LastDeal_\" + AccountID;\n\n //--- Set timer (use the shorter of heartbeat and sync interval)\n int timerMs = MathMin(HeartbeatIntervalMs, SyncIntervalSeconds * 1000);\n timerMs = MathMax(timerMs, 1000);\n if(!EventSetMillisecondTimer(timerMs))\n EventSetTimer(MathMax(timerMs / 1000, 1));\n\n //--- Initial heartbeat\n g_connStatus = STATUS_CONNECTING;\n if(IsServerReachable(API_Endpoint))\n {\n int hbResult = SendJournalHeartbeat(API_Endpoint, API_Key, AccountID, API_Secret);\n g_connStatus = (hbResult == 200 || hbResult == 201) ? STATUS_CONNECTED : STATUS_ERROR;\n }\n else\n g_connStatus = STATUS_DISCONNECTED;\n\n //--- Initial history scan\n g_lastHistoryScan = TimeCurrent();\n ScanHistory();\n\n PrintFormat(\"[Journal] TradeJournal_Sync initialized. Account=%s Endpoint=%s\", AccountID, API_Endpoint);\n return INIT_SUCCEEDED;\n }\n\n//+------------------------------------------------------------------+\n//| Expert deinitialization |\n//+------------------------------------------------------------------+\nvoid OnDeinit(const int reason)\n {\n //--- Flush remaining queue\n if(!g_journalQueue.IsEmpty())\n {\n PrintFormat(\"[Journal] Flushing queue on shutdown (%d trades)...\", g_journalQueue.Count());\n g_journalQueue.Flush(API_Endpoint, API_Key, API_Secret, AccountID);\n }\n\n EventKillTimer();\n Print(\"[Journal] TradeJournal_Sync deinitialized.\");\n }\n\n//+------------------------------------------------------------------+\n//| Process a deal — capture, enrich, queue |\n//+------------------------------------------------------------------+\nvoid ProcessDeal(ulong dealTicket)\n {\n if(IsDealSynced(dealTicket))\n return;\n\n JournalTrade trade;\n if(!CaptureDeal(dealTicket, trade))\n return;\n\n g_journalQueue.Enqueue(trade);\n MarkDealSynced(dealTicket);\n }\n\n//+------------------------------------------------------------------+\n//| Real-time trade detection |\n//+------------------------------------------------------------------+\nvoid OnTradeTransaction(const MqlTradeTransaction &trans,\n const MqlTradeRequest &request,\n const MqlTradeResult &result)\n {\n if(trans.type != TRADE_TRANSACTION_DEAL_ADD)\n return;\n\n ulong dealTicket = trans.deal;\n if(dealTicket == 0)\n return;\n\n ProcessDeal(dealTicket);\n }\n\n//+------------------------------------------------------------------+\n//| History scan catch-up |\n//+------------------------------------------------------------------+\nvoid ScanHistory()\n {\n //--- Select history for the last 7 days (covers any missed deals)\n datetime from = TimeCurrent() - 7 * 24 * 60 * 60;\n datetime to = TimeCurrent();\n if(!HistorySelect(from, to))\n return;\n\n int total = HistoryDealsTotal();\n for(int i = 0; i < total; i++)\n {\n ulong dealTicket = HistoryDealGetTicket(i);\n if(dealTicket == 0) continue;\n\n //--- Only BUY/SELL deals\n ENUM_DEAL_TYPE dealType = (ENUM_DEAL_TYPE)HistoryDealGetInteger(dealTicket, DEAL_TYPE);\n if(dealType != DEAL_TYPE_BUY && dealType != DEAL_TYPE_SELL)\n continue;\n\n ProcessDeal(dealTicket);\n }\n\n g_lastHistoryScan = TimeCurrent();\n }\n\n//+------------------------------------------------------------------+\n//| Timer handler |\n//+------------------------------------------------------------------+\nvoid OnTimer()\n {\n //--- Heartbeat\n static datetime lastHeartbeat = 0;\n if((TimeCurrent() - lastHeartbeat) >= HeartbeatIntervalMs / 1000)\n {\n int hbResult = SendJournalHeartbeat(API_Endpoint, API_Key, AccountID, API_Secret);\n g_connStatus = (hbResult == 200 || hbResult == 201) ? STATUS_CONNECTED : STATUS_ERROR;\n lastHeartbeat = TimeCurrent();\n }\n\n //--- History scan catch-up\n if((TimeCurrent() - g_lastHistoryScan) >= SyncIntervalSeconds)\n ScanHistory();\n\n //--- Flush queue\n if(!g_journalQueue.IsEmpty() && g_connStatus == STATUS_CONNECTED)\n g_journalQueue.Flush(API_Endpoint, API_Key, API_Secret, AccountID);\n }\n\n//+------------------------------------------------------------------+\nvoid OnTick()\n {\n // Trade detection is handled by OnTradeTransaction.\n }\n//+------------------------------------------------------------------+\n", +}; diff --git a/workers/api-gateway/src/lib/zip.ts b/workers/api-gateway/src/lib/zip.ts new file mode 100644 index 0000000..12b10d3 --- /dev/null +++ b/workers/api-gateway/src/lib/zip.ts @@ -0,0 +1,141 @@ +// zip.ts +// --------------------------------------------------------------------------- +// Minimal, dependency-free ZIP writer for the Cloudflare Workers runtime. +// +// Uses the STORE method (no compression, method 0) — the files we bundle are +// small MQL5 text sources (tens of KB), so compression buys nothing and STORE +// keeps the implementation tiny and fully auditable. No `node:zlib` needed. +// +// Produces a spec-compliant ZIP: for each entry a local file header + data, +// followed by the central directory and end-of-central-directory record. +// --------------------------------------------------------------------------- + +export interface ZipEntry { + /** Path within the archive, e.g. "Experts/EdgeRelay_Master.mq5". Use forward slashes. */ + name: string; + /** File contents. */ + data: string | Uint8Array; +} + +// Precomputed CRC-32 lookup table (IEEE 802.3 polynomial). +const CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[n] = c >>> 0; + } + return table; +})(); + +function crc32(bytes: Uint8Array): number { + let crc = 0xffffffff; + for (let i = 0; i < bytes.length; i++) { + crc = CRC_TABLE[(crc ^ bytes[i]!) & 0xff]! ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +/** DOS date/time. We use a fixed timestamp so archives are deterministic. */ +const DOS_TIME = 0; // 00:00:00 +const DOS_DATE = ((2024 - 1980) << 9) | (1 << 5) | 1; // 2024-01-01 + +/** + * Build a ZIP archive from the given entries. + * @returns the raw archive bytes. + */ +export function createZip(entries: ZipEntry[]): Uint8Array { + const encoder = new TextEncoder(); + + interface Prepared { + nameBytes: Uint8Array; + dataBytes: Uint8Array; + crc: number; + offset: number; + } + + const prepared: Prepared[] = []; + const localParts: Uint8Array[] = []; + let offset = 0; + + // ── Local file headers + file data ── + for (const entry of entries) { + const nameBytes = encoder.encode(entry.name); + const dataBytes = typeof entry.data === 'string' ? encoder.encode(entry.data) : entry.data; + const crc = crc32(dataBytes); + + const header = new Uint8Array(30 + nameBytes.length); + const view = new DataView(header.buffer); + view.setUint32(0, 0x04034b50, true); // local file header signature + view.setUint16(4, 20, true); // version needed to extract (2.0) + view.setUint16(6, 0, true); // general purpose flag + view.setUint16(8, 0, true); // compression method: STORE + view.setUint16(10, DOS_TIME, true); + view.setUint16(12, DOS_DATE, true); + view.setUint32(14, crc, true); + view.setUint32(18, dataBytes.length, true); // compressed size + view.setUint32(22, dataBytes.length, true); // uncompressed size + view.setUint16(26, nameBytes.length, true); + view.setUint16(28, 0, true); // extra field length + header.set(nameBytes, 30); + + prepared.push({ nameBytes, dataBytes, crc, offset }); + localParts.push(header, dataBytes); + offset += header.length + dataBytes.length; + } + + // ── Central directory ── + const centralParts: Uint8Array[] = []; + const centralStart = offset; + let centralSize = 0; + + for (const p of prepared) { + const record = new Uint8Array(46 + p.nameBytes.length); + const view = new DataView(record.buffer); + view.setUint32(0, 0x02014b50, true); // central directory header signature + view.setUint16(4, 20, true); // version made by + view.setUint16(6, 20, true); // version needed to extract + view.setUint16(8, 0, true); // general purpose flag + view.setUint16(10, 0, true); // compression method: STORE + view.setUint16(12, DOS_TIME, true); + view.setUint16(14, DOS_DATE, true); + view.setUint32(16, p.crc, true); + view.setUint32(20, p.dataBytes.length, true); // compressed size + view.setUint32(24, p.dataBytes.length, true); // uncompressed size + view.setUint16(28, p.nameBytes.length, true); + view.setUint16(30, 0, true); // extra field length + view.setUint16(32, 0, true); // file comment length + view.setUint16(34, 0, true); // disk number start + view.setUint16(36, 0, true); // internal file attributes + view.setUint32(38, 0, true); // external file attributes + view.setUint32(42, p.offset, true); // relative offset of local header + record.set(p.nameBytes, 46); + + centralParts.push(record); + centralSize += record.length; + } + + // ── End of central directory record ── + const eocd = new Uint8Array(22); + const eocdView = new DataView(eocd.buffer); + eocdView.setUint32(0, 0x06054b50, true); // EOCD signature + eocdView.setUint16(4, 0, true); // number of this disk + eocdView.setUint16(6, 0, true); // disk where central directory starts + eocdView.setUint16(8, prepared.length, true); // central dir records on this disk + eocdView.setUint16(10, prepared.length, true); // total central dir records + eocdView.setUint32(12, centralSize, true); // size of central directory + eocdView.setUint32(16, centralStart, true); // offset of central directory + eocdView.setUint16(20, 0, true); // comment length + + // ── Concatenate everything ── + const total = offset + centralSize + eocd.length; + const out = new Uint8Array(total); + let cursor = 0; + for (const part of [...localParts, ...centralParts, eocd]) { + out.set(part, cursor); + cursor += part.length; + } + return out; +} diff --git a/workers/api-gateway/src/routes/accounts.ts b/workers/api-gateway/src/routes/accounts.ts index 0bd4362..3b7d260 100644 --- a/workers/api-gateway/src/routes/accounts.ts +++ b/workers/api-gateway/src/routes/accounts.ts @@ -2,6 +2,8 @@ import { Hono } from 'hono'; import type { ApiResponse } from '@edgerelay/shared'; import type { Env } from '../types.js'; import { authMiddleware } from '../middleware/auth.js'; +import { EA_SOURCE } from '../generated/ea-source.js'; +import { createZip, type ZipEntry } from '../lib/zip.js'; const accounts = new Hono<{ Bindings: Env }>(); @@ -660,4 +662,135 @@ accounts.get('/ea-package', async (c) => { }); }); +// ── EA source bundles ─────────────────────────────────────────── +// Each bundle ships the EA plus the transitive closure of the .mqh +// includes it needs to compile (from the #include graph). MT5's own +// Trade\Trade.mqh ships with the terminal and is intentionally omitted. +type EaSourceType = 'master' | 'follower' | 'journal'; + +const EA_BUNDLES: Record = { + master: { + ea: 'EdgeRelay_Master.mq5', + title: 'TradeMetrics Master EA', + includes: [ + 'EdgeRelay_Common.mqh', + 'EdgeRelay_Crypto.mqh', + 'EdgeRelay_Http.mqh', + 'EdgeRelay_Queue.mqh', + 'EdgeRelay_Display.mqh', + 'EdgeRelay_JournalSync.mqh', + 'EdgeRelay_JournalQueue.mqh', + ], + }, + follower: { + ea: 'EdgeRelay_Follower.mq5', + title: 'TradeMetrics Follower EA', + includes: [ + 'EdgeRelay_Common.mqh', + 'EdgeRelay_Crypto.mqh', + 'EdgeRelay_Http.mqh', + 'EdgeRelay_Equity.mqh', + 'EdgeRelay_PropGuard.mqh', + 'EdgeRelay_PropGuardDisplay.mqh', + 'EdgeRelay_JsonParser.mqh', + ], + }, + journal: { + ea: 'TradeJournal_Sync.mq5', + title: 'TradeJournal Sync EA', + includes: [ + 'EdgeRelay_Common.mqh', + 'EdgeRelay_Crypto.mqh', + 'EdgeRelay_Http.mqh', + 'EdgeRelay_JournalSync.mqh', + 'EdgeRelay_JournalQueue.mqh', + ], + }, +}; + +function buildSourceReadme(bundle: { ea: string; includes: string[]; title: string }): string { + return [ + `${bundle.title} — MQL5 Source`, + '='.repeat(bundle.title.length + 15), + '', + 'This archive contains the full MQL5 source so you can inspect, audit, or', + 'modify the EA and compile it yourself in MetaEditor.', + '', + 'INSTALL', + '-------', + 'In MT5: File -> Open Data Folder, then copy:', + '', + ` Experts\\${bundle.ea}`, + ' -> MQL5\\Experts\\', + '', + ' Include\\*.mqh', + ' -> MQL5\\Include\\', + '', + 'COMPILE', + '-------', + '1. Restart MT5 or right-click the Navigator -> Refresh.', + `2. Open ${bundle.ea} in MetaEditor and press F7 (Compile).`, + '3. Drag the compiled EA from the Navigator onto a chart.', + '', + 'The Include (.mqh) files are REQUIRED — without them in MQL5\\Include\\', + 'the EA will not compile.', + '', + 'Configuration parameters and WebRequest whitelist URLs are documented on', + 'the Downloads & Setup page in TradeMetrics Pro.', + '', + ].join('\n'); +} + +// ── GET /accounts/ea-source/:type — Download compilable .mq5 source ── +// Ships the EA source + its required includes as a MT5-structured ZIP. +// Authenticated (source carries no per-user credentials), so no account +// ownership check is required. +accounts.get('/ea-source/:type', (c) => { + const type = c.req.param('type') as EaSourceType; + const bundle = EA_BUNDLES[type]; + + if (!bundle) { + return c.json( + { data: null, error: { code: 'VALIDATION_ERROR', message: 'Type must be master, follower or journal' } }, + 400, + ); + } + + const entries: ZipEntry[] = []; + + const eaSource = EA_SOURCE[bundle.ea]; + if (eaSource === undefined) { + return c.json( + { data: null, error: { code: 'INTERNAL_ERROR', message: 'EA source unavailable.' } }, + 500, + ); + } + entries.push({ name: `Experts/${bundle.ea}`, data: eaSource }); + + for (const include of bundle.includes) { + const src = EA_SOURCE[include]; + if (src === undefined) { + return c.json( + { data: null, error: { code: 'INTERNAL_ERROR', message: `Missing include: ${include}` } }, + 500, + ); + } + entries.push({ name: `Include/${include}`, data: src }); + } + + entries.push({ name: 'README.txt', data: buildSourceReadme(bundle) }); + + const zip = createZip(entries); + const filename = `${bundle.ea.replace(/\.mq5$/, '')}_Source.zip`; + + return new Response(zip, { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': String(zip.length), + 'Cache-Control': 'private, max-age=3600', + }, + }); +}); + export { accounts }; From 91a8ca6fdc70a7309c352d09ffd9617e9affac59 Mon Sep 17 00:00:00 2001 From: ghwmelite-dotcom Date: Wed, 1 Jul 2026 13:14:50 +0000 Subject: [PATCH 2/3] ci(pr-check): remove duplicate pnpm version pin pnpm/action-setup@v4 errors with "Multiple versions of pnpm specified" when both the workflow's `version:` key and package.json's `packageManager` field are set. Drop the `version: 9` override so the action reads the pinned version (pnpm@9.15.0) from packageManager, which is pnpm's recommended setup. Unblocks the PR typecheck for all PRs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pr-check.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index e2d805a..22463fe 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -12,8 +12,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: From 95f72d2045bc3e3a7399adf6d7595409c338633d Mon Sep 17 00:00:00 2001 From: ghwmelite-dotcom Date: Wed, 1 Jul 2026 13:27:36 +0000 Subject: [PATCH 3/3] fix(api-gateway): clear pre-existing typecheck errors blocking CI The PR typecheck (previously never run due to the pnpm setup bug) surfaced 6 latent strict-mode errors in files unrelated to the EA-source feature: - marketPulse.ts: drop unused ApiResponse import; hoist the byCurrency bucket into a local so noUncheckedIndexedAccess sees the assignment (fixes 3x "Object is possibly undefined"). Behavior unchanged. - strategyHub.ts: remove dead masterAccount query (result never read; magic number derives from magicFromSlug); underscore-prefix the unused generateFallbackSignal params (keeps arity for callers). Co-Authored-By: Claude Opus 4.8 (1M context) --- workers/api-gateway/src/routes/marketPulse.ts | 9 ++++----- workers/api-gateway/src/routes/strategyHub.ts | 7 +------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/workers/api-gateway/src/routes/marketPulse.ts b/workers/api-gateway/src/routes/marketPulse.ts index e2c5013..8f0cf1f 100644 --- a/workers/api-gateway/src/routes/marketPulse.ts +++ b/workers/api-gateway/src/routes/marketPulse.ts @@ -1,5 +1,4 @@ import { Hono } from 'hono'; -import type { ApiResponse } from '@edgerelay/shared'; import type { Env } from '../types.js'; export const marketPulse = new Hono<{ Bindings: Env }>(); @@ -23,10 +22,10 @@ marketPulse.get('/calendar', async (c) => { // Compute events by currency for heat map const byCurrency: Record = {}; for (const e of (results || []) as Array<{ currency: string; impact: string }>) { - if (!byCurrency[e.currency]) byCurrency[e.currency] = { high: 0, medium: 0, total: 0 }; - byCurrency[e.currency].total++; - if (e.impact === 'high') byCurrency[e.currency].high++; - else byCurrency[e.currency].medium++; + const bucket = byCurrency[e.currency] ?? (byCurrency[e.currency] = { high: 0, medium: 0, total: 0 }); + bucket.total++; + if (e.impact === 'high') bucket.high++; + else bucket.medium++; } return new Response(JSON.stringify({ data: { events: results || [], byCurrency }, error: null }), { diff --git a/workers/api-gateway/src/routes/strategyHub.ts b/workers/api-gateway/src/routes/strategyHub.ts index c4e307c..bff8c48 100644 --- a/workers/api-gateway/src/routes/strategyHub.ts +++ b/workers/api-gateway/src/routes/strategyHub.ts @@ -1013,11 +1013,6 @@ strategyHub.post('/generate-custom', async (c) => { ); } - // Fetch master account for magic number - const masterAccount = await c.env.DB.prepare( - `SELECT id, api_key FROM accounts WHERE user_id = ? AND role = 'master' LIMIT 1`, - ).bind(userId).first<{ id: string; api_key: string }>(); - const magicNumber = magicFromSlug(`${userId}:custom:${body.name}`); // Build AI prompt for signal logic generation @@ -1255,7 +1250,7 @@ function generateStrategyInputs(indicators: string[]): string { return lines.length > 0 ? lines.join('\n') : '// No additional strategy parameters'; } -function generateFallbackSignal(indicators: string[], entryConditions: string): string { +function generateFallbackSignal(_indicators: string[], _entryConditions: string): string { // Generate a basic MA crossover as fallback return `// Fallback signal logic — customize in MetaEditor static int maFastHandle = INVALID_HANDLE;