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).
This commit is contained in:
2026-08-11 01:09:34 -04:00
parent f39464287c
commit 6d0c0716fa
2 changed files with 116 additions and 0 deletions
+107
View File
@@ -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())