#!/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())