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.
Á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" --> EAGiá 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" --> EAConfig 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
- Admin mở
/admin/easupported→ Thêm EA: nhập tên → slug tự gợi ý (unique) → secret token tự sinh. - Copy URL
https://fxconsole.gamelau.uk/easupported/{slug}và Secret token vào.setcủa EA. - Chạy EA → heartbeat đầu tiên tạo account trong danh sách, cột Schema = Reported.
- The admin opens
/admin/easupported→ Add EA (“Thêm EA”): enter a name → a unique slug is suggested → a secret token is generated. - Copy the URL
https://fxconsole.gamelau.uk/easupported/{slug}and the Secret token into the EA's.set. - 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 |
|---|---|---|---|
slug | string [a-z0-9-] |
đoạn sau /easupported/ trong InpCloudMyApiDomainthe segment after /easupported/ in InpCloudMyApiDomain |
URL, EA Link |
composite_id | string ≤160 | slug(ACCOUNT_COMPANY)::slug(ACCOUNT_SERVER)::login |
account key, topic EA Linkaccount key, EA Link topic |
magic | string chữ sốdigit string | IntegerToString(InpMagic) |
account key, tmap |
account | string | IntegerToString(ACCOUNT_LOGIN) |
heartbeat, tmap |
node_id | string [A-Za-z0-9_.:-] ≤128 |
InpCloudNodeId hoặc auto-<hash(TERMINAL_PATH)>InpCloudNodeId or auto-<hash(TERMINAL_PATH)> |
leader election, EA Link |
inst | string 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_idPHẢI dùng đúng hàm slugify dưới đây (giống AccountStatusUpdater), nếu không account sẽ không khớp vớimt5_accounts:
- An account on the server is identified by
(ea_id, composite_id, magic). composite_idMUST use exactly the slugify function below (the same as AccountStatusUpdater); otherwise the account will not matchmt5_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:
401sai token ·404slug không tồn tại ·422sai dữ liệu ({"message","errors"}) ·-1từ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:
401wrong token ·404slug does not exist ·422invalid data ({"message","errors"}) ·-1fromWebRequest= 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–9999 | 100 |
timeout_sec | int 30–3600 (mặc định 90)int 30–3600 (default 90) | 90 | |
server | string ≤128 (ACCOUNT_COMPANY) | "Vantage International Ltd" | |
account_name | string ≤128 | "DINH DUY TRAN" | |
config_schema | NÊNSHOULD | array (§6) | [{...}] |
payload | NÊNSHOULD | array {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
typetrong schema. hot_keyslà CSV các key đang override. Đây là nguồn chân lý: key nào không có tronghot_keysthì 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_keysis a CSV of the keys currently overridden. It is the source of truth: for any key not inhot_keysthe 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
savemỗ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
saveevery 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 |
default | NÊNSHOULD | trù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:
- 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ũ.
- 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ênPauseở b21.1 phải lấy4. - 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).
- Giá trị ngoài miền hợp lệ → EA PHẢI giữ nguyên giá trị hiện tại và
Printcảnh báo, không được crash hoặc về 0. - 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.
- 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:
- A released key MUST NOT be renamed or change type. If a change is needed, create a new key and retire the old one.
- 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 = 3is taken by the b22+ test line, soPausein b21.1 had to use4. - 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).
- An out-of-range value → the EA MUST keep its current value and
Printa warning; it must not crash or reset to 0. - 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.
- 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ùngDoubleToString. - 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 useDoubleToString. - 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 đọcInpX(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_xand MUST NOT readInpX(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_keysNÊN bao bằng dấu phẩy (",key,") đểlotkhông khớp nhầmlot_balance_trigger. - Tách
ApplyHotConfigkhỏ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_keysSHOULD be wrapped in commas (",key,") so thatlotdoes not falsely matchlot_balance_trigger. - Keep
ApplyHotConfigseparate 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
- Khai báo global
g_foo. OnInit:g_foo = InpFoo;.- Thay mọi chỗ logic đọc
InpFoobằngg_foo(giữ dònginputvà dòng init). - Parse trong
EA_ApplyHotConfig(kiểm tra miền hợp lệ). - Revert khi vắng trong
hot_keys+ thêm vào dòng panel. - Thêm entry vào
config_schema(§6). Nhóm B thì tạo lại handle trong bước 4. - Bump build tag (§14).
Server và web không cần sửa gì.
- Declare the global
g_foo. OnInit:g_foo = InpFoo;.- Replace every place where logic reads
InpFoowithg_foo(keep theinputline and the init line). - Parse it in
EA_ApplyHotConfig(with a range check). - Revert it when absent from
hot_keys+ add it to the panel line. - Add an entry to
config_schema(§6). For group B, recreate the handle in step 4. - Bump the build tag (§14).
The server and the web need no changes.
9. EA Link (realtime)9. EA Link (realtime)
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
SocketTlsHandshakekhô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
SocketTlsHandshakedoes 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 |
|---|---|
| L1 | PHẢ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. |
| L2 | PHẢ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(). |
| L3 | Nhậ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. |
| L4 | Sau 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. |
| L5 | Nhậ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. |
| L6 | inst 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. |
| L7 | KHÔ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). |
| L8 | PHẢI gọi Close("deinit") trong OnDeinit.MUST call Close("deinit") in OnDeinit. |
| L9 | PHẢ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.mdtrướ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.mdbefore 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ó
prioritynhỏ nhất (hoà thìnode_idnhỏ hơn theo alphabet). Còn sống = heartbeat trongtimeout_sec. - Chỉ leader được mở/đóng lệnh và
save/clearticket 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ó
prioritykhác nhau; NÊN đặtInpCloudNodeIdrõ ràng (ID auto đổi khi đổi thư mục terminal). InpCloudHeartbeatTimeoutSecPHẢ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 smallernode_id). Alive = heartbeat withintimeout_sec. - Only the leader may open/close orders and
save/clearthe 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;InpCloudNodeIdSHOULD be set explicitly (the auto ID changes when the terminal folder changes). InpCloudHeartbeatTimeoutSecMUST 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() | OnTimer | 200 ms | GetTickCount64 |
| Heartbeat định kỳPeriodic heartbeat | OnTimer | InpCloudSyncIntervalSec | GetTickCount64 |
| Kéo config do tín hiệuSignal-driven config pull | OnTimer | ≤ 1 lần/giây≤ 1 per second | GetTickCount64 |
| Panel | OnTimer (+ OnTick nếu cần)OnTimer (+ OnTick if needed) | 1 s | GetTickCount64 |
| Ticket map save cloudCloud ticket map save | OnTimer | theo chu kỳ, chỉ khi map đổiper cycle, only when the map changed | GetTickCount64 |
WebRequestlà đồng bộ: mỗi lần gọi có thể chặn tớiInpCloudWebTimeoutMs. 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à đểOnTimergử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.
OnDeinitNÊ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).
WebRequestis synchronous: each call can block for up toInpCloudWebTimeoutMs. 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 letOnTimersend.- 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.
OnDeinitSHOULD 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
- Token theo product: mỗi EA product có secret riêng. Nghi lộ thì bấm Regenerate trên
/admin/easupportedrồi cập nhật.setcủa mọi terminal. - Không lộ token: không
Printtoken, không đưa vào payload, không commit.setchứa token (chỉ commit.setmẫu để trốngInpCloudBearerToken). - 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). - 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).
- Endpoint nội bộ (
/api/internal/*) chỉ dành cho broker, xác thực bằngX-Broker-Secretvà fail-closed khi server chưa cấu hình secret. - Kiểm tra miền giá trị mọi config nhận từ server trước khi áp (§6 quy tắc 4).
- Per-product tokens: every EA product has its own secret. If a leak is suspected, click Regenerate on
/admin/easupportedand update the.seton every terminal. - Never expose the token: do not
Printit, do not put it in the payload, do not commit a.setcontaining it (commit only sample.setfiles withInpCloudBearerTokenleft empty). - The socket carries no secrets: EA Link sends only an HMAC signature, never the token; the broker only holds
sha256(token). - Control values over HTTPS only: config and commands always come from a certificate-verified HTTPS endpoint (P3).
- Internal endpoints (
/api/internal/*) are for brokers only, authenticated withX-Broker-Secret, and fail closed when the server has no secret configured. - 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
Buildtrong 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
.ex5mớ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.mdkhi đổ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
Buildrow 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.mdwhen 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 |
|---|---|
easupported | product: slug (unique), name, secret_token, description, document, config_schemaproduct: slug (unique), name, secret_token, description, document, config_schema |
ea_account | account 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_config | override theo account: ea_account_id, param, value, updated_byper-account overrides: ea_account_id, param, value, updated_by |
tmap_entries, tmap_nodes | ticket 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} | EA | Bearer secret token của productBearer: the product's secret token |
/admin/easupported, /admin/easupported/{slug}, /admin/easupported/{slug}/account/{id} | admin | session + quyền adminsession + admin permission |
POST /api/internal/ea-link/key | EaLinkBroker | X-Broker-Secret → trả sha256(secret_token)X-Broker-Secret → returns sha256(secret_token) |
15.3 EA Link15.3 EA Link
| Thành phầnComponent | Giá trịValue |
|---|---|
| Broker | monitor/EaLinkBroker · container fxconsole-ea-link-broker · TLS TCP 9607monitor/EaLinkBroker · container fxconsole-ea-link-broker · TLS TCP 9607 |
| Tín hiệuSignal | Laravel 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 |
| Presence | hash 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 secret | Broker đọ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 code | app/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
HTTP
- ☐ Heartbeat chạy trong
OnTimer, chu kỳ đo bằngGetTickCount64, ≤ 60s. - ☐ Gửi
config_schema+payload(cóBuild,Balance,Equity). - ☐ Parse config tách khỏi leader state;
hot_keysrevert đúng; kiểm tra miền giá trị. - ☐ Không gọi mạng trong tester.
- ☐ Heartbeat runs in
OnTimer, cycle measured withGetTickCount64, ≤ 60s. - ☐ Sends
config_schema+payload(withBuild,Balance,Equity). - ☐ Config parsing separated from leader state;
hot_keysreverts 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.
EA Link
- ☐ Dùng
Include/EaLink.mqh;InpLinkModemặc định HYBRID. - ☐
Poll()trongOnTimer200ms; việc nặng gate 1s. - ☐
cfg→ kéo HTTPS (debounce 1s) →cfg_ack;hello_ok→ kéo bù. - ☐
inst = ChartID();Close()trongOnDeinit. - ☐ Dòng
Link:trên panel và payload.
- ☐ Uses
Include/EaLink.mqh;InpLinkModedefaults to HYBRID. - ☐
Poll()in a 200msOnTimer; heavy work gated to 1s. - ☐
cfg→ HTTPS pull (1s debounce) →cfg_ack;hello_ok→ catch-up pull. - ☐
inst = ChartID();Close()inOnDeinit. - ☐
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.mdupdated.
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.42 | P4, §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.