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:
Executable
+107
@@ -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())
|
||||||
Executable
+9
@@ -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"
|
||||||
Reference in New Issue
Block a user