From 6d0c0716fae13484a9c006002fb09eff0d2e5bb6 Mon Sep 17 00:00:00 2001 From: Garfield Date: Tue, 11 Aug 2026 01:09:34 -0400 Subject: [PATCH] Add sync-ea-inputs: durable fix for the chart-cache persistence bug Root cause: MT5 actually reads live EA inputs from MQL5/Profiles/Charts/Default/chartNN.chr at boot, not from the .set presets -- and it never rewrites that file on container shutdown (files were found frozen at their Aug 6 mtimes through every restart since, including graceful ones with stop_grace_period). This is why the XAUUSD spread-filter fix (Jul 30) and the Monday A/B reversion (Aug 10) both kept reverting after restarts. sync-ea-inputs.sh (+ .py) runs on every container boot via custom-cont-init.d, before MT5 launches, and writes each chart's matching bo-*.set values directly into its .chr cache -- bypassing MT5's broken save-on-exit path entirely. Validated with a real corrupt-then-restart-then-verify cycle (see vault note). Deployed copy lives at ~/mt5-docker/custom-cont-init.d/ (mt5-docker is not a git repo); this is the tracked source, kept in sync with the deployed version by hand like the other scripts here (e.g. export-history.sh). --- scripts/sync-ea-inputs.py | 107 ++++++++++++++++++++++++++++++++++++++ scripts/sync-ea-inputs.sh | 9 ++++ 2 files changed, 116 insertions(+) create mode 100755 scripts/sync-ea-inputs.py create mode 100755 scripts/sync-ea-inputs.sh diff --git a/scripts/sync-ea-inputs.py b/scripts/sync-ea-inputs.py new file mode 100755 index 0000000..94da61b --- /dev/null +++ b/scripts/sync-ea-inputs.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Syncs authoritative bo-*.set preset values into MT5's actual live-session +cache files (MQL5/Profiles/Charts/Default/chartNN.chr). + +Root cause (found 2026-08-10): MT5's "save chart state on graceful exit" +never actually fires under this Wine/container setup -- chartNN.chr files +were found frozen at their 2026-08-06 23:26 mtimes despite many later +restarts, including ones using the documented `stop_grace_period: 60s` + +`docker compose down` "graceful" path. Every restart since Aug 6 silently +reloads that one frozen snapshot, which is why a live GUI input edit +(Properties/F7 -> OK) reverts after the next restart no matter how +"gracefully" the container is stopped. This hit the XAUUSD spread filter +(Jul 30, Aug 6/7) and the Monday A/B reversion (Aug 10) alike -- general, +not symbol-specific. + +Fix: don't rely on MT5 ever writing this file correctly. Treat the +bo-*.set files (repo + Presets/) as the single source of truth and write +them directly into chartNN.chr ourselves, every boot. chartNN.chr uses +the exact same `Key=Value` input names as the .set files (verified), just +UTF-16LE with a BOM and CRLF instead of the .set files' UTF-8/LF -- and +one chart file per open chart, auto-mapped here via each file's own +`symbol=` line rather than a hardcoded chart-number list, so this keeps +working if charts are ever reordered. + +Idempotent -- safe to run any time, not just at boot. +""" +import glob +import os +import re +import sys + +PRESETS_DIR = "/config/.wine/drive_c/Program Files/MetaTrader 5/MQL5/Presets" +CHARTS_DIR = "/config/.wine/drive_c/Program Files/MetaTrader 5/MQL5/Profiles/Charts/Default" + +BOM = b"\xff\xfe" + + +def load_preset(path): + values = {} + with open(path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip("\r\n") + if not line or line.startswith(";") or line.startswith("#"): + continue + if "=" not in line: + continue + key, _, value = line.partition("=") + values[key] = value + return values + + +def sync_chart(chart_path): + with open(chart_path, "rb") as f: + raw = f.read() + + if not raw.startswith(BOM): + return None, "no UTF-16LE BOM, skipping" + + text = raw[len(BOM):].decode("utf-16-le", errors="replace") + + m = re.search(r"^symbol=(\S+)\r?$", text, re.MULTILINE) + if not m: + return None, "no symbol= line found" + symbol = m.group(1) + + preset_path = os.path.join(PRESETS_DIR, f"bo-{symbol.lower()}.set") + if not os.path.isfile(preset_path): + return symbol, f"no matching preset {preset_path}" + + preset_values = load_preset(preset_path) + + lines = text.split("\n") + changed = 0 + for i, line in enumerate(lines): + body = line[:-1] if line.endswith("\r") else line + if "=" not in body: + continue + key, _, current_value = body.partition("=") + if key in preset_values and preset_values[key] != current_value: + lines[i] = f"{key}={preset_values[key]}\r" + changed += 1 + + if changed: + new_text = "\n".join(lines) + new_raw = BOM + new_text.encode("utf-16-le") + tmp_path = chart_path + ".tmp" + with open(tmp_path, "wb") as f: + f.write(new_raw) + os.replace(tmp_path, chart_path) + + return symbol, f"{changed} value(s) synced from {os.path.basename(preset_path)}" + + +def main(): + if not os.path.isdir(CHARTS_DIR): + print(f"No charts dir at {CHARTS_DIR} -- nothing to sync yet.") + return 0 + + for chart_path in sorted(glob.glob(os.path.join(CHARTS_DIR, "chart*.chr"))): + symbol, msg = sync_chart(chart_path) + label = f"[{symbol}]" if symbol else "" + print(f" {os.path.basename(chart_path)} {label}: {msg}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sync-ea-inputs.sh b/scripts/sync-ea-inputs.sh new file mode 100755 index 0000000..fe553c9 --- /dev/null +++ b/scripts/sync-ea-inputs.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Thin wrapper: the linuxserver.io custom-cont-init.d runner invokes every +# executable file directly in this directory via `sh`, ignoring shebangs, +# so a plain .py file placed here fails outright when picked up on its +# own. The real logic lives in lib/ (a subdirectory, which the runner +# does not recurse into) and is invoked explicitly via python3 here. +# Runs on every container start (before MT5 itself, per the s6 cont-init +# ordering) -- see lib/sync-ea-inputs.py for what it does and why. +python3 "$(dirname "$0")/lib/sync-ea-inputs.py"