diff --git a/mt5_batch_config.json b/mt5_batch_config.json deleted file mode 100644 index 5575889..0000000 --- a/mt5_batch_config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "terminal_path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe", - "tester_folder": "C:\\Users\\pc\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075\\Tester", - "terminal_label": "\ufffd\ufffdC\u0000:\u0000\\\u0000P\u0000r\u0000o\u0000g\u0000r\u0000a\u0000m\u0000 \u0000F\u0000i\u0000l\u0000e\u0000s\u0000\\\u0000M\u0000e\u0000t\u0000a\u0000T\u0000r\u0000a\u0000d\u0000e\u0000r\u0000 \u00005\u0000", - "ea_name": "Market\\Ultimate Breakout System.ex5", - "from_date": "2018.01.01", - "to_date": "2026.04.01", - "model": "1", - "deposit": "10000", - "currency": "USD", - "leverage": "100", - "suffix": ".a" -} \ No newline at end of file diff --git a/set_comparator.py b/set_comparator.py index d993df0..9a0b867 100644 --- a/set_comparator.py +++ b/set_comparator.py @@ -4,19 +4,38 @@ from pathlib import Path def parse_set_file(file_bytes, filename): """Parse a .set file and return dict of {param: value} and ordered param list""" - text = file_bytes.decode('utf-16') + # MT5 .set files can be UTF-16 LE (with BOM) or plain UTF-8 depending on MT5 version + if file_bytes[:2] == b'\xff\xfe': + text = file_bytes[2:].decode('utf-16-le') + elif file_bytes[:2] == b'\xfe\xff': + text = file_bytes[2:].decode('utf-16-be') + else: + # No BOM — try UTF-8 first, fall back to latin-1 + try: + text = file_bytes.decode('utf-8') + except Exception: + text = file_bytes.decode('latin-1') params = {} raw_lines = {} # preserve full line for export order = [] + # Detect format: Ultimate Breakout .set files use || delimiter + # Other EA formats (plain key=value with comma-suffixed metadata) are not fully supported + is_ubs_format = any('||' in line for line in text.splitlines() if '=' in line) + + all_raw_lines = [] # all lines in order for round-trip export for line in text.splitlines(): - line = line.strip() - if not line or line.startswith(';'): + stripped = line.strip() + all_raw_lines.append(line) + if not stripped or stripped.startswith(';'): continue - if '=' not in line: + if '=' not in stripped: continue - key, _, rest = line.partition('=') + key, _, rest = stripped.partition('=') key = key.strip() + # Skip optimisation metadata lines (ParamName,F / ParamName,1 etc.) + if ',' in key: + continue # Value is first field before || parts = rest.split('||') value = parts[0].strip() @@ -24,7 +43,10 @@ def parse_set_file(file_bytes, filename): raw_lines[key] = rest # preserve everything after = order.append(key) - return params, raw_lines, order + # Store full original lines for round-trip (preserves metadata lines) + raw_lines['__all_lines__'] = all_raw_lines + + return params, raw_lines, order, is_ubs_format def build_comparison_df(files_data): """ @@ -58,7 +80,15 @@ def export_set_file(filename, params_edited, raw_lines, order, original_bytes): Returns bytes (utf-16 encoded) """ # Get original header comments - text = original_bytes.decode('utf-16') + if original_bytes[:2] == b'\xff\xfe': + text = original_bytes[2:].decode('utf-16-le') + elif original_bytes[:2] == b'\xfe\xff': + text = original_bytes[2:].decode('utf-16-be') + else: + try: + text = original_bytes.decode('utf-8') + except Exception: + text = original_bytes.decode('latin-1') lines = text.splitlines() header_lines = [] for line in lines: @@ -67,19 +97,43 @@ def export_set_file(filename, params_edited, raw_lines, order, original_bytes): else: break - output_lines = header_lines.copy() - - for key in order: - if key not in params_edited: - continue - new_value = params_edited[key] - rest = raw_lines.get(key, new_value) - parts = rest.split('||') - parts[0] = str(new_value) - output_lines.append(f"{key}={'||'.join(parts)}") - - content = '\r\n'.join(output_lines) + '\r\n' - return content.encode('utf-16') + # Use the full original line list for round-trip, only updating real param values + all_lines = raw_lines.get('__all_lines__', []) + if all_lines: + output_lines = [] + for line in all_lines: + stripped = line.strip() + if not stripped or stripped.startswith(';') or '=' not in stripped: + output_lines.append(line) + continue + key, _, rest = stripped.partition('=') + key = key.strip() + if ',' in key or key not in params_edited: + # Metadata line or unknown — preserve as-is + output_lines.append(line) + else: + new_value = params_edited[key] + parts = rest.split('||') + parts[0] = str(new_value) + output_lines.append(f"{key}={'||'.join(parts)}") + content = '\r\n'.join(output_lines) + '\r\n' + else: + # Fallback: rebuild from order (original behaviour) + output_lines = header_lines.copy() + for key in order: + if key not in params_edited: + continue + new_value = params_edited[key] + rest = raw_lines.get(key, new_value) + parts = rest.split('||') + parts[0] = str(new_value) + output_lines.append(f"{key}={'||'.join(parts)}") + content = '\r\n'.join(output_lines) + '\r\n' + # Preserve original encoding on export + if original_bytes[:2] in (b'\xff\xfe', b'\xfe\xff'): + return b'\xff\xfe' + content.encode('utf-16-le') + else: + return content.encode('utf-8') def create_zip(files_export): """ diff --git a/view_set_comparator.py b/view_set_comparator.py index 5e78b10..f9fa28b 100644 --- a/view_set_comparator.py +++ b/view_set_comparator.py @@ -169,9 +169,11 @@ def render(): key=f"ea_editor_{edit_file}" ) - st.session_state['ea_edited'][edit_file] = dict( - zip(edited['Parameter'], edited['Value'].astype(str)) - ) + # Guard against Streamlit returning edited df without expected columns + if 'Parameter' in edited.columns and 'Value' in edited.columns: + st.session_state['ea_edited'][edit_file] = dict( + zip(edited['Parameter'], edited['Value'].astype(str)) + ) col1, col2 = st.columns(2) with col1: