EA GoldenRobberGoldenRobber EA

Chuẩn tích hợp EA ↔ fxconsoleEA ↔ fxconsole integration standard

Hợp đồng bắt buộc cho mọi EA MT5 mới kết nối fxconsole: remote config, hot-reload, telemetry và kênh realtime EA Link. The mandatory contract for every new MT5 EA that connects to fxconsole: remote config, hot-reload, telemetry and the EA Link realtime channel.

  • Cập nhậtUpdated 13/09/2026
  • Phiên bản chuẩnStandard version 2.0 (2026-09-12)
  • NguồnSource EA-HOTRELOAD-STANDARD.md
  • Bản tham chiếuReference implementation GoldenRobber 1.42 (build b1) + Include/EaLink.mqh + monitor/EaLinkBroker

Áp dụng: mọi EA MT5 mới kết nối fxconsole (remote config, hot-reload, telemetry, realtime link).

Quy ước từ khoá:

  • PHẢI / KHÔNG ĐƯỢC: bắt buộc. EA vi phạm thì không được đưa lên production.
  • NÊN: khuyến nghị mạnh. Muốn làm khác thì phải ghi lý do trong lịch sử build.
  • CÓ THỂ: tuỳ chọn.

Applies to: every new MT5 EA that connects to fxconsole (remote config, hot-reload, telemetry, realtime link).

Keyword conventions:

  • MUST / MUST NOT: mandatory. An EA that violates them may not go to production.
  • SHOULD: strongly recommended. Doing otherwise requires a reason recorded in the build history.
  • MAY: optional.

1. Nguyên tắc cốt lõi1. Core principles

# Nguyên tắcPrinciple Hệ quảConsequence
P1 1 EA product = 1 slug + 1 secret token.1 EA product = 1 slug + 1 secret token. Mọi URL, config, tín hiệu realtime đều nằm trong vùng /easupported/{slug}. Product này không đọc hay nhận được dữ liệu của product khác.Every URL, config and realtime signal lives inside the /easupported/{slug} scope. One product can neither read nor receive another product's data.
P2 Server là nguồn chân lý của config override; input là giá trị fallback.The server is the source of truth for config overrides; input is the fallback value. Có override thì dùng giá trị server. Không có override (hoặc mất kết nối) thì dùng input.With an override, the server value is used. Without an override (or when disconnected), the input is used.
P3 Giá trị config chỉ đi qua HTTPS.Config values travel over HTTPS only. Socket (EA Link) chỉ mang tín hiệu. MT5 không kiểm chứng chỉ TLS của socket nên không tin được để mang giá trị.The socket (EA Link) only carries signals. MT5 does not verify the socket's TLS certificate, so it cannot be trusted to carry values.
P4 Network không được làm treo logic giao dịch.Networking must not stall trading logic. Mọi gọi mạng chạy trong OnTimer, có timeout, có backoff. Không gọi mạng trong vòng lặp lệnh.Every network call runs in OnTimer, with a timeout and backoff. No network calls inside order loops.
P5 Nhịp thời gian đo bằng GetTickCount64().Timing is measured with GetTickCount64(). TimeCurrent() là giờ quote cuối, đứng yên khi đóng thị trường → không dùng để hẹn giờ gọi mạng.TimeCurrent() is the last quote time and freezes while the market is closed → do not use it to schedule network calls.
P6 Backtest không gọi mạng.Backtests make no network calls. MQLInfoInteger(MQL_TESTER) → tắt heartbeat, tmap cloud, EA Link.MQLInfoInteger(MQL_TESTER) → heartbeat, cloud tmap and EA Link are off.
P7 Token không bao giờ bị lộ.The token is never exposed. Không log, không commit .set chứa token, không gửi token qua socket.Never logged, no committed .set containing the token, never sent over the socket.
P8 Mất server thì EA vẫn giao dịch an toàn.Without the server the EA still trades safely. Server/broker chết không làm EA dừng, không làm EA đổi hành vi ngoài giá trị input.A dead server/broker neither stops the EA nor changes its behaviour beyond its input values.

2. Kiến trúc2. Architecture

flowchart LR
  ADM(["Admin"])
  subgraph FX["fxconsole (Laravel)"]
    PAGE["/admin/easupported/{slug}"]
    CFG[("ea_config (MySQL)")]
    PUB["Redis PUBLISH ea:link:cfg"]
    HB["/easupported/{slug}/api/tmap/heartbeat"]
  end
  BRK["EaLinkBroker :9607 (Node)"]
  EA["EA trên MT5<br/>(mỗi chart = 1 inst)"]
  ADM --> PAGE --> CFG --> PUB --> BRK
  BRK -- "TLS: tín hiệu cfg, không có giá trị" --> EA
  EA -- "HTTPS (cert thật)" --> HB
  HB -- "values + hot_keys" --> EA

Giá trị config chỉ đi theo đường HTTPS heartbeat; EaLinkBroker chỉ báo “có thay đổi”.

flowchart LR
  ADM(["Admin"])
  subgraph FX["fxconsole (Laravel)"]
    PAGE["/admin/easupported/{slug}"]
    CFG[("ea_config (MySQL)")]
    PUB["Redis PUBLISH ea:link:cfg"]
    HB["/easupported/{slug}/api/tmap/heartbeat"]
  end
  BRK["EaLinkBroker :9607 (Node)"]
  EA["EA on MT5<br/>(each chart = 1 inst)"]
  ADM --> PAGE --> CFG --> PUB --> BRK
  BRK -- "TLS: cfg signal, no values" --> EA
  EA -- "HTTPS (real cert)" --> HB
  HB -- "values + hot_keys" --> EA

Config values only travel along the HTTPS heartbeat path; EaLinkBroker only says “something changed”.

Hai kênh:

Two channels:

KênhChannel Bắt buộcRequired ChiềuDirection Mang gìCarries Độ trễLatency
HTTP heartbeat PHẢIMUST EA → server → EA schema, telemetry, leader, giá trị configschema, telemetry, leader, config values = chu kỳ heartbeat (10–30s)= heartbeat cycle (10–30s)
EA Link NÊN (mặc định bật)SHOULD (on by default) server → EA tín hiệu cfgcfg signal ≈ ≤1s (EA kéo lại qua HTTPS)≈ ≤1s (the EA pulls again over HTTPS)

3. Đăng ký product và input chuẩn3. Product registration and standard inputs

3.1 Đăng ký3.1 Registration

  1. Admin mở /admin/easupported → Thêm EA: nhập tên → slug tự gợi ý (unique) → secret token tự sinh.
  2. Copy URL https://fxconsole.gamelau.uk/easupported/{slug} và Secret token vào .set của EA.
  3. Chạy EA → heartbeat đầu tiên tạo account trong danh sách, cột Schema = Reported.
  1. The admin opens /admin/easupported → Add EA (“Thêm EA”): enter a name → a unique slug is suggested → a secret token is generated.
  2. Copy the URL https://fxconsole.gamelau.uk/easupported/{slug} and the Secret token into the EA's .set.
  3. Run the EA → the first heartbeat creates the account in the list, with the Schema column = Reported.

3.2 Input chuẩn3.2 Standard inputs

EA PHẢI dùng đúng tên input dưới đây. Mục đích là để .set, tài liệu vận hành và công cụ deploy (vps-agent inject key/value) dùng chung được cho mọi EA.

The EA MUST use exactly the input names below, so that .set files, operating docs and deploy tooling (vps-agent key/value injection) work the same for every EA.

//--- Inputs: Remote Config & Cloud Sync
input string   InpGroupCloudSync           = "=== Remote Config & Cloud Sync ===";
input bool     InpCloudSyncEnable          = false;   // Bật heartbeat / remote config (chỉ live)
input string   InpCloudBearerToken         = "";      // Secret token của EA product
input string   InpCloudMyApiDomain         = "";      // https://<host>/easupported/{slug}
input int      InpCloudSyncIntervalSec     = 10;      // Chu kỳ heartbeat (giây), 5–60
input int      InpCloudWebTimeoutMs        = 5000;    // WebRequest timeout (ms)
//--- Multi-node (chỉ khi EA hỗ trợ leader election, §10)
input int      InpCloudNodePriority        = 100;     // 1 = cao nhất
input string   InpCloudNodeId              = "";      // trống = auto
input int      InpCloudHeartbeatTimeoutSec = 90;      // ≥ 3 × InpCloudSyncIntervalSec
//--- EA Link (§9)
enum ENUM_LinkMode { LINK_HTTP_ONLY = 0,   // HTTP only
                     LINK_HYBRID    = 1 }; // HYBRID (HTTP + Link realtime)
input ENUM_LinkMode InpLinkMode            = LINK_HYBRID;
input string        InpLinkHost            = "";      // trống = host của InpCloudMyApiDomain
input int           InpLinkPort            = 9607;

Code comments are kept in Vietnamese as in the source. They read: InpCloudSyncEnable — enable heartbeat / remote config (live only); InpCloudBearerToken — the EA product's secret token; InpCloudSyncIntervalSec — heartbeat cycle in seconds, 5–60; “Multi-node” group — only when the EA supports leader election (§10); InpCloudNodePriority — 1 = highest; InpCloudNodeId — empty = auto; InpLinkHost — empty = host of InpCloudMyApiDomain.

Input Quy tắcRule
InpCloudMyApiDomain PHẢI có dạng https://<host>/easupported/{slug}, không có dấu / cuối. Slug của EA Link được lấy từ chính URL này.MUST have the form https://<host>/easupported/{slug}, with no trailing /. The EA Link slug is taken from this very URL.
InpCloudSyncIntervalSec PHẢI ≤ 60 (server coi account offline sau 120s không heartbeat). NÊN để 10.MUST be ≤ 60 (the server treats an account as offline after 120s without a heartbeat). SHOULD be 10.
InpCloudBearerToken KHÔNG ĐƯỢC hardcode trong source hay commit trong .set.MUST NOT be hardcoded in source or committed in a .set.
InpLinkMode NÊN mặc định LINK_HYBRID.SHOULD default to LINK_HYBRID.

MT5: host trong InpCloudMyApiDomain (và InpLinkHost nếu khác) PHẢI có trong Tools → Options → Expert Advisors → Allow WebRequest for listed URL. Socket MQL5 dùng chung danh sách này.

MT5: the host in InpCloudMyApiDomain (and InpLinkHost if different) MUST be listed in Tools → Options → Expert Advisors → Allow WebRequest for listed URL. MQL5 sockets share this same list.

4. Định danh4. Identity

TrườngField KiểuType Cách tínhHow it is derived Dùng ởUsed in
slugstring [a-z0-9-] đoạn sau /easupported/ trong InpCloudMyApiDomainthe segment after /easupported/ in InpCloudMyApiDomain URL, EA Link
composite_idstring ≤160 slug(ACCOUNT_COMPANY)::slug(ACCOUNT_SERVER)::login account key, topic EA Linkaccount key, EA Link topic
magicstring chữ sốdigit string IntegerToString(InpMagic) account key, tmap
accountstring IntegerToString(ACCOUNT_LOGIN) heartbeat, tmap
node_idstring [A-Za-z0-9_.:-] ≤128 InpCloudNodeId hoặc auto-<hash(TERMINAL_PATH)>InpCloudNodeId or auto-<hash(TERMINAL_PATH)> leader election, EA Link
inststring chữ sốdigit string IntegerToString(ChartID()) EA Link (phân biệt 2 chart cùng account/magic)EA Link (tells apart 2 charts with the same account/magic)
  • Một account trên server được định danh bằng (ea_id, composite_id, magic).
  • composite_id PHẢI dùng đúng hàm slugify dưới đây (giống AccountStatusUpdater), nếu không account sẽ không khớp với mt5_accounts:
  • An account on the server is identified by (ea_id, composite_id, magic).
  • composite_id MUST use exactly the slugify function below (the same as AccountStatusUpdater); otherwise the account will not match mt5_accounts:
string EA_Slugify(const string s) {           // lowercase, [a-z0-9], gộp ký tự khác thành 1 dấu '-'
   string out = ""; int n = StringLen(s); bool dash = true;
   for (int i = 0; i < n; i++) {
      ushort c = StringGetCharacter(s, i);
      if (c >= 'A' && c <= 'Z')                               { out += ShortToString((ushort)(c + 32)); dash = false; }
      else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { out += ShortToString(c); dash = false; }
      else if (!dash)                                         { out += "-"; dash = true; }
   }
   if (StringLen(out) > 0 && StringGetCharacter(out, StringLen(out) - 1) == '-')
      out = StringSubstr(out, 0, StringLen(out) - 1);
   return out;
}
string EA_BuildCompositeId() {
   string co = EA_Slugify(AccountInfoString(ACCOUNT_COMPANY)); if (co == "") co = "unknown-company";
   string sv = EA_Slugify(AccountInfoString(ACCOUNT_SERVER));  if (sv == "") sv = "unknown-server";
   return co + "::" + sv + "::" + IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN));
}

The comment on EA_Slugify reads: lowercase, [a-z0-9], every run of other characters collapses into a single -.

5. HTTP API5. HTTP API

5.1 Chung5.1 General

Base URL: InpCloudMyApiDomain + /api/tmap/<endpoint>. Header PHẢI có:

Base URL: InpCloudMyApiDomain + /api/tmap/<endpoint>. The headers MUST include:

Content-Type: application/json; charset=utf-8
Accept: application/json
Authorization: Bearer <InpCloudBearerToken>
  • Mã lỗi: 401 sai token · 404 slug không tồn tại · 422 sai dữ liệu ({"message","errors"}) · -1 từ WebRequest = lỗi mạng hoặc host chưa có trong Allow WebRequest.
  • Chuỗi JSON PHẢI được escape: \ " và mọi ký tự < 0x20 (tên broker/account đôi khi chứa ký tự điều khiển).
  • Error codes: 401 wrong token · 404 slug does not exist · 422 invalid data ({"message","errors"}) · -1 from WebRequest = network error or host not in Allow WebRequest.
  • JSON strings MUST be escaped: \, " and every character < 0x20 (broker/account names sometimes contain control characters).
Method Endpoint Bắt buộcRequired Mục đíchPurpose
POST/heartbeat PHẢIMUST schema + telemetry + leader + nhận configschema + telemetry + leader + receive config
POST/save CÓ THỂMAY lưu ticket map (EA cần phục hồi mapping ticket)store the ticket map (for EAs that need to recover ticket mapping)
GET/load?magic=&account= CÓ THỂMAY nạp ticket map khi khởi độngload the ticket map at startup
DELETE/clear?magic=&account= CÓ THỂMAY xoá ticket mapdelete the ticket map

5.2 Heartbeat — request5.2 Heartbeat — request

Field Bắt buộcRequired Kiểu / giới hạnType / limit Ví dụExample
magic✓string ≤32"21478308"
account✓string ≤64"25938032"
composite_id✓string ≤160"vantage::vantagemarkets-live-3::25938032"
node_id✓string ≤128"auto-634649047"
priority✓int 1–9999100
timeout_secint 30–3600 (mặc định 90)int 30–3600 (default 90)90
serverstring ≤128 (ACCOUNT_COMPANY)"Vantage International Ltd"
account_namestring ≤128"DINH DUY TRAN"
config_schemaNÊNSHOULDarray (§6)[{...}]
payloadNÊNSHOULDarray {name,data} (§7)[{"name":"Equity","data":"50041.18"}]

EA không dùng leader election vẫn PHẢI gửi node_id và priority (dùng auto-... và 100).

An EA that does not use leader election MUST still send node_id and priority (use auto-... and 100).

5.3 Heartbeat — response5.3 Heartbeat — response

{
  "ok": true,
  "is_leader": true,
  "current_leader_id": "auto-634649047",
  "current_leader_priority": 100,
  "alive_count": 1,
  "alive_nodes": [{ "node_id": "auto-634649047", "priority": 100, "age_sec": 0 }],
  "hot_keys": "manual_init,strong_bypass",
  "manual_init": 4,
  "strong_bypass": false
}
  • Key config chỉ xuất hiện khi đang có override cho account đó, và đã có kiểu JSON đúng theo type trong schema.
  • hot_keys là CSV các key đang override. Đây là nguồn chân lý: key nào không có trong hot_keys thì EA PHẢI trả về giá trị input. "hot_keys":"" nghĩa là không còn override nào → revert toàn bộ.
  • Parse thành công thì EA PHẢI ghi nhận thời điểm heartbeat OK (dùng cho panel và EA Link, xem §9.5).
  • Config keys appear only while an override exists for that account, already typed as JSON according to the schema's type.
  • hot_keys is a CSV of the keys currently overridden. It is the source of truth: for any key not in hot_keys the EA MUST return to its input value. "hot_keys":"" means no override remains → revert everything.
  • On a successful parse the EA MUST record the time of the OK heartbeat (used by the panel and EA Link, see §9.5).

5.4 Ticket map (tuỳ chọn)5.4 Ticket map (optional)

POST /save:

{ "magic": "21478308", "account": "25938032",
  "entries": [ { "ticket": 123, "round_id": 7, "add_num": 2, "is_dca_pos": true, "pos_type": 0, "comment": "..." } ] }

→ {"ok":true,"count":N} (hoặc "skipped":true khi map không đổi; server bỏ qua ghi DB).

GET /load?magic=&account= → {"ok":true,"magic","account","entries":[...]} · DELETE /clear?magic=&account= → {"ok":true,"deleted":N}.

  • Chỉ leader được save/clear (§10).
  • KHÔNG NÊN gửi save mỗi chu kỳ khi map không đổi. EA NÊN tự so sánh trước khi gửi.

→ {"ok":true,"count":N} (or "skipped":true when the map is unchanged; the server skips the DB write).

GET /load?magic=&account= → {"ok":true,"magic","account","entries":[...]} · DELETE /clear?magic=&account= → {"ok":true,"deleted":N}.

  • Only the leader may save/clear (§10).
  • The EA SHOULD NOT send save every cycle when the map is unchanged. The EA SHOULD compare before sending.

Chi tiết lưu và phục hồi ticket map: Khôi phục TMap.

Ticket map storage and recovery in detail: TMap disaster recovery.

6. config_schema6. config_schema

EA tự khai báo danh sách config chỉnh được và gửi trong mỗi heartbeat. Server render UI từ schema, không hardcode gì.

The EA declares its own list of editable configs and sends it in every heartbeat. The server renders the UI from the schema; nothing is hardcoded.

[
  { "key": "manual_init", "label": "Manual Init", "type": "enum", "default": 0,
    "options": [ {"value":0,"label":"OFF"}, {"value":1,"label":"Only BUY"},
                 {"value":2,"label":"Only SELL"}, {"value":4,"label":"Pause (no new init)"} ] },
  { "key": "strong_bypass", "label": "StrongMove bypass", "type": "bool", "default": false },
  { "key": "lot_balance_trigger", "label": "Lot Balance trigger", "type": "float", "default": 2.0 }
]
Field Bắt buộcRequired Ghi chúNotes
key✓snake_case, unique, trùng tên field trong response heartbeatsnake_case, unique, same as the field name in the heartbeat response
label✓hiển thị trên webshown on the web
type✓bool · int · float · enum · string
defaultNÊNSHOULDtrùng giá trị mặc định của inputequal to the input's default value
options✓ với enum✓ for enum[{value:int, label}]

Quy tắc:

  1. Key đã phát hành KHÔNG ĐƯỢC đổi tên hay đổi kiểu. Cần đổi thì tạo key mới và bỏ key cũ.
  2. Giá trị enum đã phát hành KHÔNG ĐƯỢC đánh số lại hoặc dùng lại cho nghĩa khác, kể cả ở nhánh build khác cùng slug. Ví dụ: Auto = 3 đã dùng ở dòng test b22+, nên Pause ở b21.1 phải lấy 4.
  3. Mọi giá trị có trong schema PHẢI được parser chấp nhận. Schema có mà parser từ chối là lỗi (xem §17: b28 khai báo Auto = 3 nhưng parse chỉ nhận 0–2).
  4. Giá trị ngoài miền hợp lệ → EA PHẢI giữ nguyên giá trị hiện tại và Print cảnh báo, không được crash hoặc về 0.
  5. Schema được lưu theo product, không theo account. Nhiều build khác schema cùng chạy trên 1 slug thì web hiển thị schema của account heartbeat gần nhất. Vì vậy các build cùng slug NÊN giữ schema tương thích.
  6. Chuỗi JSON schema PHẢI hợp lệ: dấu phẩy giữa các object, object cuối không có phẩy.

Rules:

  1. A released key MUST NOT be renamed or change type. If a change is needed, create a new key and retire the old one.
  2. A released enum value MUST NOT be renumbered or reused for another meaning, even on a different build branch of the same slug. Example: Auto = 3 is taken by the b22+ test line, so Pause in b21.1 had to use 4.
  3. Every value present in the schema MUST be accepted by the parser. A value the schema offers but the parser rejects is a bug (see §17: b28 declares Auto = 3 but its parser only accepts 0–2).
  4. An out-of-range value → the EA MUST keep its current value and Print a warning; it must not crash or reset to 0.
  5. The schema is stored per product, not per account. When several builds with different schemas run on one slug, the web shows the schema of the account with the most recent heartbeat. Builds on the same slug therefore SHOULD keep compatible schemas.
  6. The schema JSON string MUST be valid: commas between objects, no comma after the last object.

7. payload (telemetry)7. payload (telemetry)

  • Mảng { "name": string, "data": string }, cả hai đều là string. Số thì dùng DoubleToString.
  • Thứ tự trong mảng = thứ tự hiển thị ở /admin/easupported/{slug}/account/{id}.
  • Server chỉ giữ snapshot mới nhất (ghi đè mỗi heartbeat).
  • Dòng PHẢI có: Build (build tag), Balance, Equity. NÊN có: Floating P/L, Buy, Sell, Net lot, Leader, Hot Reload, Link.
  • EA có info panel NÊN đẩy nguyên nội dung panel (capture từng dòng khi vẽ) để admin thấy đúng những gì trader thấy.
  • KHÔNG ĐƯỢC đưa token, mật khẩu hay dữ liệu cá nhân ngoài tên account vào payload.
  • An array of { "name": string, "data": string }; both are strings. Numbers use DoubleToString.
  • Array order = display order on /admin/easupported/{slug}/account/{id}.
  • The server keeps only the latest snapshot (overwritten on every heartbeat).
  • Rows that MUST be present: Build (build tag), Balance, Equity. Rows that SHOULD be present: Floating P/L, Buy, Sell, Net lot, Leader, Hot Reload, Link.
  • An EA with an info panel SHOULD push the panel content verbatim (capturing each line as it is drawn) so the admin sees exactly what the trader sees.
  • The payload MUST NOT contain tokens, passwords or personal data other than the account name.
string EA_PayloadRow(const string name, const string data) {
   return "{\"name\":\"" + EA_EscapeJson(name) + "\",\"data\":\"" + EA_EscapeJson(data) + "\"}";
}

8. Cơ chế hot-reload8. Hot-reload mechanism

8.1 Global mutable8.1 Mutable globals

input của MQL5 bất biến khi chạy, nên mỗi config hot-reload PHẢI có 1 biến global:

input InpX (bất biến) ──OnInit──► g_x (global) ──logic đọc mỗi tick──► hành vi
                                     ▲
                 heartbeat parse ────┘  (không có trong hot_keys → g_x = InpX)
  • Logic PHẢI đọc g_x, KHÔNG ĐƯỢC đọc InpX (trừ dòng khai báo và dòng khởi tạo).

MQL5 input values are immutable at runtime, so every hot-reloadable config MUST have one global variable:

input InpX (immutable) ──OnInit──► g_x (global) ──logic reads every tick──► behaviour
                                      ▲
                  heartbeat parse ────┘  (not in hot_keys → g_x = InpX)
  • Logic MUST read g_x and MUST NOT read InpX (except on the declaration line and the initialisation line).

8.2 Phân loại8.2 Classification

NhómGroup NghĩaMeaning Hot-reload
A giá trị runtime thuần (ngưỡng, cờ, lot, TP)pure runtime values (thresholds, flags, lots, TP) CÓ THỂ: chỉ cần global + parseMAY: only needs a global + parsing
B tham số tạo indicator handle (TF, period)parameters used to create indicator handles (TF, period) CÓ THỂ: đổi giá trị thì PHẢI IndicatorRelease + tạo handle mớiMAY: on change the EA MUST call IndicatorRelease + create a new handle
C định danh / cấu trúc (InpMagic, domain, token, mode đổi cách ghi sổ lệnh)identity / structure (InpMagic, domain, token, modes that change order bookkeeping) KHÔNG ĐƯỢCMUST NOT

Đổi config chỉ ảnh hưởng quyết định tương lai, không undo vị thế đã mở. Ví dụ tắt hedge chỉ là ngừng mở hedge mới.

Changing a config only affects future decisions; it does not undo positions already open. For example, turning hedge off only stops new hedges from opening.

8.3 Parse chuẩn8.3 Standard parsing

bool EA_JsonNumber(const string j, const string k, double &out) {
   int p = StringFind(j, "\"" + k + "\":"); if (p < 0) return false;
   out = StringToDouble(StringSubstr(j, p + StringLen(k) + 3, 24)); return true;
}
bool EA_JsonBool(const string j, const string k, bool &out) {
   int p = StringFind(j, "\"" + k + "\":"); if (p < 0) return false;
   string v = StringSubstr(j, p + StringLen(k) + 3, 5);
   out = (StringFind(v, "true") == 0) || (StringGetCharacter(v, 0) == '1'); return true;
}

// Áp config — hàm THUẦN theo chuỗi JSON response, không đụng leader state
void EA_ApplyHotConfig(const string json) {
   double dv; bool bv;
   // 1) key có mặt → ghi global (kiểm tra miền hợp lệ)
   if (EA_JsonNumber(json, "manual_init", dv)) {
      int v = (int)dv;
      if (v == 0 || v == 1 || v == 2 || v == 4) g_manualInit = (ENUM_ManualInit)v;
      else PrintFormat("[HotReload] manual_init=%d không hợp lệ, giữ %d", v, (int)g_manualInit);
   }
   if (EA_JsonBool(json, "strong_bypass", bv)) g_strongBypass = bv;

   // 2) hot_keys authoritative → revert key vắng mặt
   int p = StringFind(json, "\"hot_keys\":\"");
   if (p < 0) return;                                  // response không có hot_keys → không revert
   int s = p + 12, e = StringFind(json, "\"", s);
   string hot = "," + ((e > s) ? StringSubstr(json, s, e - s) : "") + ",";
   if (StringFind(hot, ",manual_init,") < 0)   g_manualInit   = InpManualInit;
   if (StringFind(hot, ",strong_bypass,") < 0) g_strongBypass = InpStrongBypass;

   // 3) dòng panel "Hot Reload: ..." từ các key đang áp
   g_hotReloadLine = "";
   if (StringFind(hot, ",manual_init,") >= 0)   g_hotReloadLine += "manual_init=" + IntegerToString((int)g_manualInit) + " ";
   if (StringFind(hot, ",strong_bypass,") >= 0) g_hotReloadLine += "strong_bypass=" + (g_strongBypass ? "ON" : "OFF");
}

Comments in the code, translated: EA_ApplyHotConfig is a PURE function of the JSON response string and does not touch leader state. Step 1 — a key that is present is written to its global (with a range check; the PrintFormat says “manual_init=%d is invalid, keeping %d”). Step 2 — hot_keys is authoritative → revert keys that are absent; a response without hot_keys reverts nothing. Step 3 — build the panel line "Hot Reload: ..." from the keys currently applied.

  • Tìm key trong hot_keys NÊN bao bằng dấu phẩy (",key,") để lot không khớp nhầm lot_balance_trigger.
  • Tách ApplyHotConfig khỏi phần xử lý leader: một hàm chỉ nhận chuỗi JSON và áp config, dễ gọi lại và dễ test.
  • Key lookups in hot_keys SHOULD be wrapped in commas (",key,") so that lot does not falsely match lot_balance_trigger.
  • Keep ApplyHotConfig separate from leader handling: one function that only takes the JSON string and applies config is easy to call again and easy to test.

8.4 Recipe: thêm 1 config hot-reload8.4 Recipe: adding one hot-reload config

  1. Khai báo global g_foo.
  2. OnInit: g_foo = InpFoo;.
  3. Thay mọi chỗ logic đọc InpFoo bằng g_foo (giữ dòng input và dòng init).
  4. Parse trong EA_ApplyHotConfig (kiểm tra miền hợp lệ).
  5. Revert khi vắng trong hot_keys + thêm vào dòng panel.
  6. Thêm entry vào config_schema (§6). Nhóm B thì tạo lại handle trong bước 4.
  7. Bump build tag (§14).

Server và web không cần sửa gì.

  1. Declare the global g_foo.
  2. OnInit: g_foo = InpFoo;.
  3. Replace every place where logic reads InpFoo with g_foo (keep the input line and the init line).
  4. Parse it in EA_ApplyHotConfig (with a range check).
  5. Revert it when absent from hot_keys + add it to the panel line.
  6. Add an entry to config_schema (§6). For group B, recreate the handle in step 4.
  7. Bump the build tag (§14).

The server and the web need no changes.

9.1 Mục đích và nguyên tắc9.1 Purpose and principles

Làm EA biết ngay khi admin Apply/Clear config, thay vì chờ heartbeat kế tiếp.

  • Kênh chỉ mang tín hiệu {"t":"cfg","rev"}. Nhận tín hiệu → EA gọi heartbeat HTTPS (§5) để lấy giá trị.
  • Lý do: MT5 SocketTlsHandshake không verify certificate (broker dùng self-signed). Kẻ đứng giữa có thể giả broker, nên socket KHÔNG ĐƯỢC mang giá trị config, token, hay lệnh giao dịch.
  • Mất Link thì EA vẫn nhận config bằng heartbeat định kỳ (P8).

Let the EA know immediately when an admin applies/clears config, instead of waiting for the next heartbeat.

  • The channel only carries the signal {"t":"cfg","rev"}. On receiving it → the EA calls the HTTPS heartbeat (§5) to fetch the values.
  • Reason: MT5 SocketTlsHandshake does not verify the certificate (the broker uses a self-signed one). A man in the middle could impersonate the broker, so the socket MUST NOT carry config values, tokens or trade commands.
  • If Link is lost the EA still receives config through the periodic heartbeat (P8).
sequenceDiagram
  participant A as Admin
  participant F as fxconsole
  participant R as Redis
  participant B as EaLinkBroker port 9607
  participant E as EA (MT5)
  E->>B: kết nối TLS
  B->>E: challenge (nonce, server_t)
  E->>B: hello (slug, composite_id, magic, node_id, inst, sig)
  B->>F: POST /api/internal/ea-link/key (X-Broker-Secret)
  F-->>B: sha256(secret_token)
  B->>E: hello_ok (hb_sec 20)
  Note over E,B: hb / hb_ok mỗi 20s, im lặng 60s thì đóng
  A->>F: Apply hoặc Clear
  F->>R: PUBLISH ea:link:cfg (slug, targets, rev)
  R-->>B: PSUBSCRIBE *ea:link:cfg
  B->>E: cfg (rev)
  Note over E: debounce tối đa 1 lần kéo mỗi giây
  E->>F: HTTPS POST /easupported/slug/api/tmap/heartbeat
  F-->>E: values + hot_keys
  E->>B: cfg_ack (rev, ok)

Trình tự bắt tay rồi một lần Apply. Chỉ bước HTTPS mang giá trị config.

sequenceDiagram
  participant A as Admin
  participant F as fxconsole
  participant R as Redis
  participant B as EaLinkBroker port 9607
  participant E as EA (MT5)
  E->>B: TLS connect
  B->>E: challenge (nonce, server_t)
  E->>B: hello (slug, composite_id, magic, node_id, inst, sig)
  B->>F: POST /api/internal/ea-link/key (X-Broker-Secret)
  F-->>B: sha256(secret_token)
  B->>E: hello_ok (hb_sec 20)
  Note over E,B: hb / hb_ok every 20s, closed after 60s of silence
  A->>F: Apply or Clear
  F->>R: PUBLISH ea:link:cfg (slug, targets, rev)
  R-->>B: PSUBSCRIBE *ea:link:cfg
  B->>E: cfg (rev)
  Note over E: debounce, at most 1 pull per second
  E->>F: HTTPS POST /easupported/slug/api/tmap/heartbeat
  F-->>E: values + hot_keys
  E->>B: cfg_ack (rev, ok)

The handshake followed by one Apply. Only the HTTPS step carries config values.

9.2 Yêu cầu với EA9.2 Requirements for the EA

# Yêu cầuRequirement
L1PHẢI dùng Include/EaLink.mqh (CEaLink), KHÔNG ĐƯỢC tự viết lại protocol.MUST use Include/EaLink.mqh (CEaLink) and MUST NOT reimplement the protocol.
L2PHẢI chỉ gọi Poll() trong OnTimer. Timer NÊN là EventSetMillisecondTimer(200) khi Link bật; việc nặng (heartbeat định kỳ, panel) gate 1s bằng GetTickCount64().MUST call Poll() only in OnTimer. The timer SHOULD be EventSetMillisecondTimer(200) when Link is on; heavy work (periodic heartbeat, panel) is gated to 1s with GetTickCount64().
L3Nhận cfg → PHẢI kéo heartbeat HTTPS; PHẢI debounce tối đa 1 lần kéo/giây; gom nhiều cfg liên tiếp thành 1 lần kéo.On cfg → MUST pull the HTTPS heartbeat; MUST debounce to at most 1 pull/second; several consecutive cfg frames collapse into 1 pull.
L4Sau khi kéo → PHẢI gửi {"t":"cfg_ack","rev":<rev>,"ok":true|false} và Print 1 dòng log kèm độ trễ.After pulling → MUST send {"t":"cfg_ack","rev":<rev>,"ok":true|false} and Print one log line including the latency.
L5Nhận hello_ok (vừa kết nối lại) → PHẢI kéo bù 1 lần nếu heartbeat OK gần nhất đã cũ hơn 5s.On hello_ok (just reconnected) → MUST do one catch-up pull if the last OK heartbeat is older than 5s.
L6inst PHẢI là ChartID(); slug PHẢI lấy từ InpCloudMyApiDomain; token PHẢI là InpCloudBearerToken.inst MUST be ChartID(); slug MUST come from InpCloudMyApiDomain; the token MUST be InpCloudBearerToken.
L7KHÔNG ĐƯỢC bật Link trong tester, hoặc khi thiếu slug/token (log 1 dòng lý do rồi chạy HTTP only).MUST NOT enable Link in the tester, or when the slug/token is missing (log one line with the reason, then run HTTP only).
L8PHẢI gọi Close("deinit") trong OnDeinit.MUST call Close("deinit") in OnDeinit.
L9PHẢI hiển thị StatusText() trên panel và trong payload (§11).MUST show StatusText() on the panel and in the payload (§11).

9.3 Protocol (tóm tắt)9.3 Protocol (summary)

NDJSON UTF-8, 1 JSON / dòng, tối đa 8 KiB.

NDJSON UTF-8, 1 JSON per line, 8 KiB maximum.

HướngDirection Frame Ghi chúNotes
S→EA{"t":"challenge","v":1,"nonce","server_t"}ngay sau TLSright after TLS
EA→S{"t":"hello","v":1,"slug","composite_id","magic","node_id","inst","ea","ea_ver","nonce","sig"}trong 5swithin 5s
S→EA{"t":"hello_ok","hb_sec":20}xác thực OKauthentication OK
S→EA{"t":"hello_err","reason"}bad_format · unknown_ea · auth_unavailable · bad_sig · timeout
EA→S / S→EA{"t":"hb"} / {"t":"hb_ok"}mỗi 20s; im lặng 60s → đóngevery 20s; 60s of silence → closed
S→EA{"t":"cfg","rev":<ms>}config của account này vừa đổithis account's config just changed
EA→S{"t":"cfg_ack","rev","ok"}sau khi kéo xongafter the pull finishes
S→EA{"t":"bye","reason"}shutdown · superseded · timeout · bad_frames · line_too_long · buffer_overflow

Xác thực (token thô không đi qua socket):

Authentication (the raw token never crosses the socket):

key = hex(sha256(secret_token))              // chuỗi 64 ký tự hex; dùng NGUYÊN chuỗi làm HMAC key
msg = "ealink1|" + nonce + "|" + slug + "|" + composite_id + "|" + magic + "|" + node_id + "|" + inst
sig = hex(HMAC-SHA256(key, msg))

The comment on key reads: a 64-character hex string; the WHOLE string is used as the HMAC key.

Vùng tách biệt: topic = ea/{slug}/{composite_id}/{magic}, trong topic mỗi kết nối có khoá {node_id}#{inst}. Kết nối lại cùng khoá thì thay kết nối cũ (superseded); khác inst thì cùng tồn tại và đều nhận tín hiệu.

Chi tiết đầy đủ và cách vận hành broker: monitor/EaLinkBroker/README.md · Các broker realtime.

Isolation: topic = ea/{slug}/{composite_id}/{magic}; within a topic every connection has the key {node_id}#{inst}. Reconnecting with the same key replaces the old connection (superseded); a different inst coexists and also receives signals.

Full details and broker operations: monitor/EaLinkBroker/README.md · Realtime brokers.

9.4 API CEaLink9.4 CEaLink API

HàmFunction Mô tảDescription
bool Configure(host, port, slug, token, compositeId, magic, nodeId, inst, eaName, eaVer)Gọi 1 lần ở OnInit. Chỉ lưu sha256(token). Tự lọc node_id/magic/inst về tập ký tự hợp lệ.Called once in OnInit. Stores only sha256(token). Filters node_id/magic/inst down to the valid character set.
int Poll(string &frames[])Gọi mỗi OnTimer. Tự lo connect, TLS, challenge, hb, reconnect. Trả về frame ứng dụng (cfg, hello_ok).Called on every OnTimer. Handles connect, TLS, challenge, hb and reconnect itself. Returns application frames (cfg, hello_ok).
bool Send(string json)Gửi 1 frame khi đang UP.Sends one frame while UP.
void Close(string reason)Đóng kết nối, tắt Link.Closes the connection and turns Link off.
bool IsUp() · ENUM_EALINK_STATE State()Trạng thái.State.
string StatusText()Link: UP host:port (Ns) · Link: authenticating... · Link: DOWN, retry Ns · Link: AUTH FAIL: <reason>, retry Ns
EaLink_ParseSlug(url) · EaLink_ParseHost(url)Tách slug / host từ InpCloudMyApiDomain.Extract the slug / host from InpCloudMyApiDomain.
EaLink_JsonStr(json, key) · EaLink_JsonInt(json, key, def)Đọc field của frame.Read a frame field.

Hành vi có sẵn trong include: connect timeout 2s; send/recv timeout 2s; handshake phải xong trong 10s; backoff 2→4→8…60s ±20% jitter, reset sau khi UP ổn định 30s; bad_sig/unknown_ea chờ 5 phút; log lỗi lần đầu và mỗi 10 lần.

Behaviour built into the include: connect timeout 2s; send/recv timeout 2s; the handshake must finish within 10s; backoff 2→4→8…60s with ±20% jitter, reset after 30s of stable UP; bad_sig/unknown_ea wait 5 minutes; errors are logged the first time and every 10th time.

9.5 Recipe tích hợp9.5 Integration recipe

#include "Include\EaLink.mqh"

CEaLink g_link;
bool    g_linkEnabled = false, g_linkPullPending = false;
long    g_linkPendingRev = 0;
ulong   g_linkPendingSinceMs = 0, g_linkLastPullMs = 0, g_lastTimer1sMs = 0, g_hbOkMs = 0;
int     g_hbOkSeq = 0;           // tăng mỗi lần heartbeat parse thành công

int OnInit() {
   // ... g_compositeId = EA_BuildCompositeId(); heartbeat đầu tiên ...
   if (InpLinkMode == LINK_HYBRID && InpCloudSyncEnable && !MQLInfoInteger(MQL_TESTER)) {
      string slug = EaLink_ParseSlug(InpCloudMyApiDomain);
      string host = (InpLinkHost != "") ? InpLinkHost : EaLink_ParseHost(InpCloudMyApiDomain);
      if (slug != "" && host != "" && InpCloudBearerToken != "")
         g_linkEnabled = g_link.Configure(host, InpLinkPort, slug, InpCloudBearerToken, g_compositeId,
                                          IntegerToString(InpMagic), EA_ResolveNodeId(),
                                          IntegerToString(ChartID()), "MyEA", "1.00-" + EA_BUILD_TAG);
      if (!g_linkEnabled) Print("[EaLink] TẮT: cần InpCloudMyApiDomain .../easupported/{slug} + InpCloudBearerToken");
   }
   if (!MQLInfoInteger(MQL_TESTER)) {
      if (g_linkEnabled) EventSetMillisecondTimer(200); else EventSetTimer(1);
   }
   return INIT_SUCCEEDED;
}

void OnTimer() {
   if (g_linkEnabled) EA_LinkPump();
   ulong now = GetTickCount64();
   if (g_linkEnabled && now - g_lastTimer1sMs < 1000) return;
   g_lastTimer1sMs = now;
   EA_HeartbeatPulse();     // heartbeat định kỳ theo InpCloudSyncIntervalSec (đo bằng GetTickCount64)
   EA_UpdatePanel();
}

void EA_LinkPump() {
   string f[]; int n = g_link.Poll(f);
   ulong now = GetTickCount64();
   for (int i = 0; i < n; i++) {
      string t = EaLink_JsonStr(f[i], "t");
      if (t == "cfg" || (t == "hello_ok" && (g_hbOkMs == 0 || now - g_hbOkMs > 5000))) {
         long rev = (t == "cfg") ? EaLink_JsonInt(f[i], "rev", 0) : 0;
         if (rev > g_linkPendingRev) g_linkPendingRev = rev;
         if (!g_linkPullPending) g_linkPendingSinceMs = now;
         g_linkPullPending = true;
      }
   }
   if (!g_linkPullPending || now - g_linkLastPullMs < 1000) return;      // L3 debounce
   g_linkPullPending = false; g_linkLastPullMs = now;
   int before = g_hbOkSeq;
   EA_SendHeartbeat();                                                     // HTTPS, áp config qua EA_ApplyHotConfig
   bool ok = (g_hbOkSeq != before);
   if (g_linkPendingRev > 0) {                                             // L4
      g_link.Send("{\"t\":\"cfg_ack\",\"rev\":" + IntegerToString(g_linkPendingRev) + ",\"ok\":" + (ok ? "true" : "false") + "}");
      PrintFormat("[EaLink] cfg rev=%s %s trong %d ms", IntegerToString(g_linkPendingRev),
                  ok ? "đã áp" : "kéo LỖI", (int)(GetTickCount64() - g_linkPendingSinceMs));
   }
   g_linkPendingRev = 0;
}

void OnDeinit(const int reason) {
   EventKillTimer();
   if (g_linkEnabled) g_link.Close("deinit");
}

Comments and log strings in the code, translated: g_hbOkSeq — incremented on every successful heartbeat parse; OnInit — “… compute g_compositeId, first heartbeat …”; the Print “[EaLink] TẮT: cần …” means “Link OFF: needs InpCloudMyApiDomain …/easupported/{slug} + InpCloudBearerToken”; EA_HeartbeatPulse() — periodic heartbeat on InpCloudSyncIntervalSec (measured with GetTickCount64); EA_SendHeartbeat() — HTTPS, applies config via EA_ApplyHotConfig; the PrintFormat reads “cfg rev=… applied (“đã áp”) / pull FAILED (“kéo LỖI”) in (“trong”) N ms”.

Trong hàm parse heartbeat, khi thành công: g_hbOkSeq++; g_hbOkMs = GetTickCount64();.

In the heartbeat parse function, on success: g_hbOkSeq++; g_hbOkMs = GetTickCount64();.

9.6 Mở rộng sau này9.6 Future extensions

  • Loại frame mới (vd telemetry realtime, command) PHẢI cập nhật mục này + monitor/EaLinkBroker/README.md trước khi code.
  • Frame điều khiển (command) PHẢI giữ nguyên tắc P3: socket chỉ báo "có lệnh id=N", EA kéo chi tiết qua HTTPS rồi mới thực thi.
  • A new frame type (e.g. realtime telemetry, commands) MUST update this section + monitor/EaLinkBroker/README.md before any code is written.
  • Control frames (commands) MUST keep principle P3: the socket only announces "there is command id=N"; the EA fetches the details over HTTPS before executing.

10. Multi-node leader election10. Multi-node leader election

Dùng khi chạy cùng account + magic trên nhiều terminal/VPS để dự phòng. EA chạy 1 node CÓ THỂ bỏ qua phần này (vẫn gửi node_id + priority).

  • Leader = node còn sống có priority nhỏ nhất (hoà thì node_id nhỏ hơn theo alphabet). Còn sống = heartbeat trong timeout_sec.
  • Chỉ leader được mở/đóng lệnh và save/clear ticket map. Follower chỉ đồng bộ và theo dõi.
  • Heartbeat thất bại liên tục > 2 × InpCloudHeartbeatTimeoutSec → EA NÊN tự nhận leader để không bỏ mặc vị thế.
  • Chưa từng nhận heartbeat thành công → mặc định leader.
  • Mỗi node PHẢI có priority khác nhau; NÊN đặt InpCloudNodeId rõ ràng (ID auto đổi khi đổi thư mục terminal).
  • InpCloudHeartbeatTimeoutSec PHẢI ≥ 3 × InpCloudSyncIntervalSec.

Used when running the same account + magic on several terminals/VPSes for redundancy. An EA running a single node MAY skip this section (it still sends node_id + priority).

  • Leader = the alive node with the lowest priority (on a tie, the alphabetically smaller node_id). Alive = heartbeat within timeout_sec.
  • Only the leader may open/close orders and save/clear the ticket map. Followers only sync and observe.
  • Heartbeats failing continuously for > 2 × InpCloudHeartbeatTimeoutSec → the EA SHOULD take leadership itself so positions are not abandoned.
  • Never received a successful heartbeat → leader by default.
  • Each node MUST have a distinct priority; InpCloudNodeId SHOULD be set explicitly (the auto ID changes when the terminal folder changes).
  • InpCloudHeartbeatTimeoutSec MUST be ≥ 3 × InpCloudSyncIntervalSec.

11. Info panel chuẩn11. Standard info panel

Các dòng dưới đây PHẢI có khi InpCloudSyncEnable = true. Nội dung panel NÊN được đẩy nguyên vào payload.

The lines below MUST be present when InpCloudSyncEnable = true. The panel content SHOULD be pushed verbatim into payload.

DòngLine Nội dungContent MàuColour
Tiêu đềTitle <EAName> v<version> (<build>) + cờ trạng thái đặc biệt (vd [PAUSED])<EAName> v<version> (<build>) + special status flag (e.g. [PAUSED]) trắngwhite
Server Server: connecting... · Server: CONNECTED (Ns ago) · Server: NO SYNC (Ns) xám / xanh / đỏgrey / green / red
Hot Reload Hot Reload: key=value, ... (ẩn khi không có override)Hot Reload: key=value, ... (hidden when there is no override) vàngyellow
Link CEaLink::StatusText() + | cfg Ns ago; Link: HTTP only khi tắtCEaLink::StatusText() + | cfg Ns ago; Link: HTTP only when off xanh khi UP, đỏ khi DOWN, xám khi tắtgreen when UP, red when DOWN, grey when off
Node Node: <id> (pri=N) | LEADER|FOLLOWER | Alive=N (chỉ EA có §10)Node: <id> (pri=N) | LEADER|FOLLOWER | Alive=N (only EAs implementing §10) xanh / xámgreen / grey

Tuổi heartbeat trên dòng Server NÊN đo bằng GetTickCount64() để không đứng yên khi đóng thị trường.

The heartbeat age on the Server line SHOULD be measured with GetTickCount64() so it does not freeze while the market is closed.

12. Nhịp thời gian và hiệu năng12. Timing and performance

ViệcTask Ở đâuWhere NhịpCadence Đồng hồClock
EA Link Poll()OnTimer200 msGetTickCount64
Heartbeat định kỳPeriodic heartbeatOnTimerInpCloudSyncIntervalSecGetTickCount64
Kéo config do tín hiệuSignal-driven config pullOnTimer≤ 1 lần/giây≤ 1 per secondGetTickCount64
PanelOnTimer (+ OnTick nếu cần)OnTimer (+ OnTick if needed)1 sGetTickCount64
Ticket map save cloudCloud ticket map saveOnTimertheo chu kỳ, chỉ khi map đổiper cycle, only when the map changedGetTickCount64
  • WebRequest là đồng bộ: mỗi lần gọi có thể chặn tới InpCloudWebTimeoutMs. KHÔNG ĐƯỢC gọi trong vòng lặp duyệt/đóng lệnh; sự kiện quan trọng (vd partial close) NÊN chỉ đánh dấu "cần sync" và để OnTimer gửi.
  • KHÔNG ĐƯỢC retry mạng liên tục mỗi tick/mỗi giây khi lỗi; phải có backoff hoặc chờ chu kỳ kế.
  • Thời điểm "đã gửi" NÊN chỉ cập nhật sau khi gửi thành công, hoặc cập nhật trước nhưng có backoff rõ ràng. Tránh cả hai kiểu lỗi: spam retry, hoặc im lặng cả chu kỳ sau một lần lỗi.
  • OnDeinit NÊN gửi tối đa 1 heartbeat/save cuối, không chuỗi nhiều request (MT5 có thể kill EA nếu deinit quá lâu).
  • WebRequest is synchronous: each call can block for up to InpCloudWebTimeoutMs. It MUST NOT be called inside order scan/close loops; important events (e.g. a partial close) SHOULD only set a "needs sync" flag and let OnTimer send.
  • Network calls MUST NOT be retried continuously every tick/second on failure; use backoff or wait for the next cycle.
  • The "last sent" time SHOULD only be updated after a successful send, or updated beforehand with an explicit backoff. Avoid both failure modes: retry spam, or a whole silent cycle after a single failure.
  • OnDeinit SHOULD send at most 1 final heartbeat/save, not a chain of requests (MT5 may kill the EA if deinit takes too long).

13. Bảo mật13. Security

  1. Token theo product: mỗi EA product có secret riêng. Nghi lộ thì bấm Regenerate trên /admin/easupported rồi cập nhật .set của mọi terminal.
  2. Không lộ token: không Print token, không đưa vào payload, không commit .set chứa token (chỉ commit .set mẫu để trống InpCloudBearerToken).
  3. Socket không chở bí mật: EA Link chỉ gửi chữ ký HMAC, không gửi token; broker chỉ giữ sha256(token).
  4. Giá trị điều khiển chỉ qua HTTPS: config và lệnh luôn lấy từ endpoint HTTPS có kiểm chứng chỉ (P3).
  5. Endpoint nội bộ (/api/internal/*) chỉ dành cho broker, xác thực bằng X-Broker-Secret và fail-closed khi server chưa cấu hình secret.
  6. Kiểm tra miền giá trị mọi config nhận từ server trước khi áp (§6 quy tắc 4).
  1. Per-product tokens: every EA product has its own secret. If a leak is suspected, click Regenerate on /admin/easupported and update the .set on every terminal.
  2. Never expose the token: do not Print it, do not put it in the payload, do not commit a .set containing it (commit only sample .set files with InpCloudBearerToken left empty).
  3. The socket carries no secrets: EA Link sends only an HMAC signature, never the token; the broker only holds sha256(token).
  4. Control values over HTTPS only: config and commands always come from a certificate-verified HTTPS endpoint (P3).
  5. Internal endpoints (/api/internal/*) are for brokers only, authenticated with X-Broker-Secret, and fail closed when the server has no secret configured.
  6. Range-check every config received from the server before applying it (§6 rule 4).

14. Version, build và phát hành14. Versions, builds and releases

  • #property version "X.YY" PHẢI đổi khi có tính năng mới; #define <EA>_BUILD_TAG "bN" PHẢI bump mỗi lần sửa code.
  • Mỗi build PHẢI thêm 1 dòng lịch sử ở đầu file: build, tách từ đâu, thay đổi gì, cái gì giữ nguyên.
  • Tiêu đề panel và dòng Build trong payload PHẢI hiện build tag. Tên file không đủ để biết build.
  • Tách nhánh từ một build cũ (vd hotfix từ production) PHẢI ghi rõ lineage trong lịch sử build và trong RELEASES.md ("tách từ b21.1, không có tính năng b22–b28").
  • Nhiều build cùng slug: tuân thủ §6 quy tắc 2–5.
  • Deploy .ex5 mới: gỡ EA khỏi chart rồi kéo lại (F7 khi EA đang gắn không nạp binary mới). Hộp thoại MT5 LiveUpdate → chọn Later.
  • Cập nhật RELEASES.md khi đổi bản production/test (xem Build & release).
  • #property version "X.YY" MUST change when features are added; #define <EA>_BUILD_TAG "bN" MUST be bumped on every code change.
  • Every build MUST add one history line at the top of the file: build, what it was forked from, what changed, what stayed the same.
  • The panel title and the Build row in the payload MUST show the build tag. The file name is not enough to know the build.
  • Branching from an old build (e.g. a hotfix off production) MUST record the lineage in the build history and in RELEASES.md ("forked from b21.1, no b22–b28 features").
  • Several builds on one slug: follow §6 rules 2–5.
  • Deploying a new .ex5: remove the EA from the chart and drag it back on (F7 while the EA is attached does not load the new binary). MT5 LiveUpdate dialog → choose Later.
  • Update RELEASES.md when the production/test build changes (see Builds & releases).

15. Tham chiếu phía server15. Server-side reference

15.1 Bảng15.1 Tables

BảngTable Vai tròRole
easupportedproduct: slug (unique), name, secret_token, description, document, config_schemaproduct: slug (unique), name, secret_token, description, document, config_schema
ea_accountaccount phát hiện từ heartbeat: ea_id, composite_id, login, name, magic, server, payload, last_heartbeat; unique (ea_id, composite_id, magic)accounts discovered from heartbeats: ea_id, composite_id, login, name, magic, server, payload, last_heartbeat; unique (ea_id, composite_id, magic)
ea_configoverride theo account: ea_account_id, param, value, updated_byper-account overrides: ea_account_id, param, value, updated_by
tmap_entries, tmap_nodesticket map, node leader election (theo magic + account)ticket map, leader-election nodes (keyed by magic + account)

15.2 Endpoint15.2 Endpoints

Endpoint Ai gọiCaller Auth
/easupported/{slug}/api/tmap/{heartbeat,save,load,clear}EABearer secret token của productBearer: the product's secret token
/admin/easupported, /admin/easupported/{slug}, /admin/easupported/{slug}/account/{id}adminsession + quyền adminsession + admin permission
POST /api/internal/ea-link/keyEaLinkBrokerX-Broker-Secret → trả sha256(secret_token)X-Broker-Secret → returns sha256(secret_token)
Thành phầnComponent Giá trịValue
Brokermonitor/EaLinkBroker · container fxconsole-ea-link-broker · TLS TCP 9607monitor/EaLinkBroker · container fxconsole-ea-link-broker · TLS TCP 9607
Tín hiệuSignalLaravel Redis::publish('ea:link:cfg', {"slug","targets":[{"composite_id","magic"}],"rev"}) sau Apply/Clear; broker PSUBSCRIBE *ea:link:cfgLaravel Redis::publish('ea:link:cfg', {"slug","targets":[{"composite_id","magic"}],"rev"}) after Apply/Clear; broker PSUBSCRIBE *ea:link:cfg
Presencehash laravel-database-ea_link:presence:{slug}:{composite_id}:{magic}, field {node_id}#{inst} = unix giây, TTL 90s; web coi là live nếu < 60s → badge Linkhash laravel-database-ea_link:presence:{slug}:{composite_id}:{magic}, field {node_id}#{inst} = unix seconds, TTL 90s; the web treats it as live if < 60s → Link badge
Secret broker → LaravelBroker → Laravel secretBroker đọc settings.broker_internal_secret thẳng từ MySQL (cache 30s) → Redis app_config:broker_internal_secret → .env BROKER_INTERNAL_SECRET. Không phụ thuộc admin mở trang sau deploy; .env của broker PHẢI có DB_*The broker reads settings.broker_internal_secret directly from MySQL (30s cache) → Redis app_config:broker_internal_secret → .env BROKER_INTERNAL_SECRET. It does not depend on an admin opening a page after deploy; the broker's .env MUST contain DB_*
Code LaravelLaravel codeapp/Services/EaLinkNotifier.php, app/Http/Controllers/Api/EaLinkInternalController.php

Cổng các broker: 9600/9601 TickBroker · 9602/9603 AccountStatusBroker · 9605/9606 VpsStreamBroker · 9607 EaLinkBroker.

Broker ports: 9600/9601 TickBroker · 9602/9603 AccountStatusBroker · 9605/9606 VpsStreamBroker · 9607 EaLinkBroker.

15.4 Web15.4 Web

  • /admin/easupported/{slug}: danh sách account (Online khi heartbeat < 120s, badge Link/HTTP), chọn nhiều account → Apply Hot config / Clear config (render theo schema).
  • /admin/easupported/{slug}/account/{id}: bảng payload 2 cột.
  • /admin/easupported/{slug}: account list (Online when the heartbeat is < 120s old, Link/HTTP badge); select several accounts → Apply Hot config / Clear config (rendered from the schema).
  • /admin/easupported/{slug}/account/{id}: two-column payload table.

16. Checklist tuân thủ16. Compliance checklist

Định danh & cấu hìnhIdentity & configuration

  • ☐ Input đúng tên §3.2; InpCloudMyApiDomain dạng https://host/easupported/{slug}.
  • ☐ composite_id dùng đúng hàm slugify §4; magic gửi dạng string.
  • ☐ Không có token trong source/.set commit.
  • ☐ Input names exactly as in §3.2; InpCloudMyApiDomain in the form https://host/easupported/{slug}.
  • ☐ composite_id uses exactly the slugify function from §4; magic is sent as a string.
  • ☐ No token in source or in a committed .set.

HTTP

  • ☐ Heartbeat chạy trong OnTimer, chu kỳ đo bằng GetTickCount64, ≤ 60s.
  • ☐ Gửi config_schema + payload (có Build, Balance, Equity).
  • ☐ Parse config tách khỏi leader state; hot_keys revert đúng; kiểm tra miền giá trị.
  • ☐ Không gọi mạng trong tester.
  • ☐ Heartbeat runs in OnTimer, cycle measured with GetTickCount64, ≤ 60s.
  • ☐ Sends config_schema + payload (with Build, Balance, Equity).
  • ☐ Config parsing separated from leader state; hot_keys reverts correctly; values range-checked.
  • ☐ No network calls in the tester.

Schema

  • ☐ Key/type/enum value không đổi so với build đã phát hành cùng slug.
  • ☐ Parser nhận mọi giá trị có trong schema.
  • ☐ Keys/types/enum values unchanged from builds already released on the same slug.
  • ☐ The parser accepts every value in the schema.
  • ☐ Dùng Include/EaLink.mqh; InpLinkMode mặc định HYBRID.
  • ☐ Poll() trong OnTimer 200ms; việc nặng gate 1s.
  • ☐ cfg → kéo HTTPS (debounce 1s) → cfg_ack; hello_ok → kéo bù.
  • ☐ inst = ChartID(); Close() trong OnDeinit.
  • ☐ Dòng Link: trên panel và payload.
  • ☐ Uses Include/EaLink.mqh; InpLinkMode defaults to HYBRID.
  • ☐ Poll() in a 200ms OnTimer; heavy work gated to 1s.
  • ☐ cfg → HTTPS pull (1s debounce) → cfg_ack; hello_ok → catch-up pull.
  • ☐ inst = ChartID(); Close() in OnDeinit.
  • ☐ Link: line on the panel and in the payload.

Phát hànhRelease

  • ☐ Bump version/build tag + dòng lịch sử + lineage.
  • ☐ Compile MetaEditor 0 error.
  • ☐ Host có trong Allow WebRequest trên mọi terminal.
  • ☐ Test demo: panel Server: CONNECTED, Link: UP; Apply 1 config → log EA áp trong ≤ 2s; Clear → revert; tắt broker → EA vẫn chạy bằng HTTP.
  • ☐ Cập nhật RELEASES.md.
  • ☐ Version/build tag bumped + history line + lineage.
  • ☐ MetaEditor compile with 0 errors.
  • ☐ Host listed in Allow WebRequest on every terminal.
  • ☐ Demo test: panel shows Server: CONNECTED, Link: UP; Apply 1 config → EA log shows it applied within ≤ 2s; Clear → reverts; broker stopped → EA keeps running over HTTP.
  • ☐ RELEASES.md updated.

17. Sai lệch đã biết của GoldenRobber17. Known GoldenRobber deviations

EA mới KHÔNG ĐƯỢC sao chép các điểm dưới đây. GoldenRobber sẽ sửa dần ở các build sau.

New EAs MUST NOT copy the points below. GoldenRobber will fix them gradually in later builds.

# Sai lệchDeviation Build Chuẩn liên quanRelated rule
D1 CloudSyncPulse và tuổi heartbeat trên panel đo bằng TimeCurrent() → heartbeat định kỳ có thể không chạy khi đóng thị trườngCloudSyncPulse and the heartbeat age on the panel are measured with TimeCurrent() → the periodic heartbeat may not run while the market is closed ≤ 1.42 b2 (đã sửa ở b3)(fixed in b3)P5, §12
D2 TicketMapSaveImmediate gọi heartbeat + save đồng bộ ngay trong vòng Tia/partial close (OnTick)TicketMapSaveImmediate calls heartbeat + save synchronously right inside the Tia/partial-close loop (OnTick) ≤ 1.42P4, §12
D3 Heartbeat lỗi thì chờ trọn chu kỳ sau mới thử lại (timestamp cập nhật trước khi gửi)A failed heartbeat waits a whole cycle before retrying (the timestamp is updated before sending) ≤ 1.42§12
D4 Parse config + leader state trong cùng 1 hàm (TicketMapParseHeartbeatResponse); tìm key trong hot_keys bằng substring không bao dấu phẩyConfig parsing + leader state in the same function (TicketMapParseHeartbeatResponse); keys are looked up in hot_keys by substring without comma delimiters ≤ 1.42§8.3
D5 Ticket map save gửi mỗi chu kỳ dù map không đổi (server tự bỏ qua)Ticket map save is sent every cycle even when the map is unchanged (the server skips it) ≤ 1.42§5.4
D6 TicketMapEscapeJson chỉ escape \ " CR LF, không lọc ký tự điều khiển khácTicketMapEscapeJson only escapes \ " CR LF and does not filter other control characters ≤ 1.42§5.1
D7 1.41.4 b28: schema có manual_init = 3 (Auto) nhưng parser chỉ nhận 0–2; chưa có Pause = 41.41.4 b28: the schema offers manual_init = 3 (Auto) but the parser only accepts 0–2; no Pause = 4 yet 1.41.4 b28§6 quy tắc 3rule 3

Chuẩn này thay thế bản "EA ↔ fxconsole Hot-Reload Standard" (GoldenRobber 1.41.4 b16). Khi contract đổi: cập nhật file này trước, rồi đến code EA / fxconsole / broker.

This standard replaces the earlier "EA ↔ fxconsole Hot-Reload Standard" (GoldenRobber 1.41.4 b16). When the contract changes: update this file first, then the EA / fxconsole / broker code.