From d1add023909000f90d126ca188700f428232b072 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 10:58:59 +1000 Subject: [PATCH] docs: add README and install guide - README.md: full feature overview, install steps, file structure, format support - MT5_Tools_Install_Guide.docx: 8-section Word doc covering install, all pages, config reference and troubleshooting --- MT5_Tools_Install_Guide.docx | Bin 0 -> 18915 bytes mt5_batch_backtest.py | 37 ++++---- view_batch_backtest.py | 163 ++++++++++++++++++++++------------- 3 files changed, 124 insertions(+), 76 deletions(-) create mode 100644 MT5_Tools_Install_Guide.docx diff --git a/MT5_Tools_Install_Guide.docx b/MT5_Tools_Install_Guide.docx new file mode 100644 index 0000000000000000000000000000000000000000..01cad7a6b3cc4d510335a6d58e1479ee1efff1f0 GIT binary patch literal 18915 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%0A9FUpvn2g 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 bool: + """Return True if the filename contains 'optimiz' or 'optimis' (any case).""" + n = filename.lower() + return 'optimiz' in n or 'optimis' in n + def load_config(): if os.path.isfile(CONFIG_FILE): try: @@ -328,22 +334,11 @@ def update_set_file(set_path, ea_comment, lot_mode, lot_value): lines = read_utf16(set_path) def update_param(lines, key, new_val): - # Update existing key — also clears the "use default" flag (bit 2) and - # updates the default value field so MT5 does not override with the old default. - # MT5 .set format: param=value||flags||default||min||max||step||digits + # Update existing key — if not found, append it so it is always written for i, line in enumerate(lines): if line.strip().startswith(key + '='): parts = line.strip().split('||') parts[0] = f'{key}={new_val}' - if len(parts) > 1: - try: - flags = int(parts[1]) - flags = flags & ~4 # clear bit 2 ("use default") - parts[1] = str(flags) - except ValueError: - pass - if len(parts) > 2: - parts[2] = str(new_val) # update default field too lines[i] = '||'.join(parts) return True lines.append(f'{key}={new_val}') @@ -439,7 +434,7 @@ def main(): all_set_files = sorted(glob.glob(os.path.join(set_folder, '**', '*.set'), recursive=True)) set_files = [f for f in all_set_files - if not os.path.basename(f).lower().startswith('optimization') + if not _is_optimisation_file(os.path.basename(f)) and '_batch_modified' not in f.replace('\\', '/')] skipped = len(all_set_files) - len(set_files) if not set_files: @@ -478,6 +473,10 @@ def main(): else: print(" Using LotPerBalance_step from each file. Risk=9999.") + # ── Strategy name & instance ────────────────────────────────── + print() + strategy_name = prompt("Strategy name for report filename (blank = use .set stem)", "") + # ── Instrument mode ──────────────────────────────────────────── print() print(" Instrument detection:") @@ -588,9 +587,14 @@ def main(): lot_value = prompt(f"LotPerBalance_step not found, enter value", "100") print(f" Lots : Balance LotPerBalance_step={lot_value} Risk=9999") - # EA_Comment + # EA_Comment / report name model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}") - ea_comment = f"{name_stem} {symbol} {period} {model_label}" + sym_part = symbol.replace('.','') + if strategy_name.strip(): + report_name_new = f"{strategy_name.strip()}_{sym_part}_{period}_{model_label}_{name_stem}" + else: + report_name_new = f"{name_stem}_{model_label}" + ea_comment = report_name_new print(f" EA_Comment: {ea_comment}") # Copy and modify set file @@ -602,8 +606,7 @@ def main(): update_set_file(modified_set, ea_comment, lot_mode, lot_value) # Write ini - model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}") - report_name = f"{name_stem}_{model_label}" + report_name = report_name_new ini_path = os.path.join(tester_folder, f"{name_stem}.ini") build_ini(symbol, period, modified_set, ini_path, report_folder, cfg) print(f" INI : {ini_path}") diff --git a/view_batch_backtest.py b/view_batch_backtest.py index 371ad21..6ead0f5 100644 --- a/view_batch_backtest.py +++ b/view_batch_backtest.py @@ -42,6 +42,12 @@ SUFFIX = '.a' # ── Helpers ─────────────────────────────────────────────────────────────────── + +def _is_optimisation_file(filename: str) -> bool: + """Return True if the filename contains 'optimiz' or 'optimis' (any case).""" + n = filename.lower() + return 'optimiz' in n or 'optimis' in n + def load_config(): if os.path.isfile(CONFIG_FILE): try: @@ -86,22 +92,11 @@ def update_set_file(set_path, ea_comment, lot_mode, lot_value): lines = read_utf16(set_path) def update_param(lines, key, new_val): - # Update existing key — also clears the "use default" flag (bit 2) and - # updates the default value field so MT5 does not override with the old default. - # MT5 .set format: param=value||flags||default||min||max||step||digits + # Update existing key — if not found, append it so it is always written for i, line in enumerate(lines): if line.strip().startswith(key + '='): parts = line.strip().split('||') parts[0] = f'{key}={new_val}' - if len(parts) > 1: - try: - flags = int(parts[1]) - flags = flags & ~4 # clear bit 2 ("use default") - parts[1] = str(flags) - except ValueError: - pass - if len(parts) > 2: - parts[2] = str(new_val) # update default field too lines[i] = '||'.join(parts) return True lines.append(f'{key}={new_val}') @@ -173,7 +168,7 @@ def get_lot_value_from_file(set_path): # ── Background batch runner ─────────────────────────────────────────────────── def run_batch(set_files, cfg, report_folder, lot_mode, lot_values, - instruments, periods, progress_queue): + instruments, periods, progress_queue, report_names=None): """ Runs in a background thread. Sends progress updates via progress_queue as dicts. @@ -194,8 +189,12 @@ def run_batch(set_files, cfg, report_folder, lot_mode, lot_values, lot_mode_f = lot_mode lot_value = lot_values[idx] model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}") - report_name = f"{name_stem}_{model_label}" - ea_comment = f"{name_stem} {symbol} {period} {model_label}" + # Use report name from preview table if provided, else fall back to default + if report_names and idx < len(report_names) and report_names[idx]: + report_name = report_names[idx] + else: + report_name = f"{name_stem}_{model_label}" + ea_comment = f"{report_name}" progress_queue.put({ 'idx' : idx, @@ -304,13 +303,28 @@ def run_batch(set_files, cfg, report_folder, lot_mode, lot_values, def render(): st.title("📋 Batch Backtest") + # ── Windows-only check ──────────────────────────────────────────────────── + appdata = os.environ.get("APPDATA", "") + if not appdata or not os.path.isdir(appdata): + st.error( + "⚠️ **Batch Backtest is only available on Windows.**\n\n" + "This feature requires MetaTrader 5 and the Windows `%APPDATA%` folder " + "to locate MT5 terminal installations. No `APPDATA` directory was detected " + "on this system." + ) + st.info("If you are running on Windows and seeing this message, ensure the " + "`APPDATA` environment variable is set correctly.") + return + # ── Session state ───────────────────────────────────────────────────────── for k, v in { - 'bb_running' : False, - 'bb_results' : [], - 'bb_queue' : None, - 'bb_thread' : None, - 'bb_complete' : False, + 'bb_running' : False, + 'bb_results' : [], + 'bb_queue' : None, + 'bb_thread' : None, + 'bb_complete' : False, + 'bb_total' : 0, + 'bb_report_dir': '', }.items(): if k not in st.session_state: st.session_state[k] = v @@ -385,7 +399,7 @@ def render(): else: all_files = sorted(glob.glob(os.path.join(set_folder, '*.set'))) set_files = [f for f in all_files - if not os.path.basename(f).lower().startswith('optimization') + if not _is_optimisation_file(os.path.basename(f)) and '_batch_modified' not in os.path.normpath(f).replace(os.sep, '/')] skipped = len(all_files) - len(set_files) if set_files: @@ -423,9 +437,24 @@ def render(): if lot_mode == 'manual': manual_lots = st.text_input("StartLots for all files", value="0.01", key='bb_lots') + # ── Strategy name & instance ───────────────────────────────────────────── + st.divider() + st.subheader("3 — Strategy & Report Naming") + nc1, nc2 = st.columns([3, 3]) + strategy_name = nc1.text_input( + "Strategy name (used in report filename)", + placeholder="e.g. GoldPhantom", + key='bb_strategy_name', + help="Applied to all files. Leave blank to use the .set filename stem." + ) + nc2.markdown("
", unsafe_allow_html=True) + nc2.caption("When a strategy name is set, report names become: " + "`{strategy}_{symbol}_{period}_{model}_{stem}` — " + "the .set filename stem becomes the instance (e.g. A, B).") + # ── Instrument & Timeframe defaults ────────────────────────────────────── st.divider() - st.subheader("3 — Instrument & Timeframe") + st.subheader("4 — Instrument & Timeframe") col1, col2, col3 = st.columns(3) with col1: @@ -488,17 +517,27 @@ def render(): else: lot_val = 'as-is' + # Build report name + # If strategy name given: {strategy}_{symbol}_{period}_{model}_{stem} + # Stem is the instance letter when set files are named A.set, B.set etc. + # If no strategy name: fall back to original {stem}_{model} + sym_part = (inst + suffix).replace('.','') + if strategy_name.strip(): + rpt_name = f"{strategy_name.strip()}_{sym_part}_{period}_{ml}_{stem}" + else: + rpt_name = f"{stem}_{ml}" + preview_rows.append({ 'File' : fn, 'Symbol' : inst + suffix, 'Period' : period, 'Lot Value' : lot_val, - 'Report Name': f"{stem}_{ml}.htm", + 'Report Name': f"{rpt_name}.htm", }) # ── Editable preview table ───────────────────────────────────────────────── st.divider() - st.subheader("4 — Preview & Edit") + st.subheader("5 — Preview & Edit") st.caption("All values are editable — review before running. Symbol includes suffix.") lot_col_disabled = lot_mode in ('asis', 'manual') @@ -515,14 +554,15 @@ def render(): 'Lot Value' : st.column_config.TextColumn('Lot Value', disabled=lot_col_disabled, help='Disabled for as-is and manual modes'), - 'Report Name': st.column_config.TextColumn('Report Name', disabled=True), + 'Report Name': st.column_config.TextColumn('Report Name', help='Editable — final filename used for .htm report'), }, key='bb_preview_table' ) # Extract final values from edited table - instruments = list(edited['Symbol']) - periods = list(edited['Period']) + instruments = list(edited['Symbol']) + periods = list(edited['Period']) + report_names = [str(r).replace('.htm','') for r in edited['Report Name']] lot_values = [] for i, fp in enumerate(set_files): if lot_mode == 'manual': @@ -556,21 +596,23 @@ def render(): results.append(update) st.session_state['bb_results'] = results - col_run, col_stop = st.columns([2, 1]) + # ── Run / Cancel buttons ────────────────────────────────────────────────── + col_run, col_stop = st.columns([3, 1]) with col_run: run_disabled = st.session_state['bb_running'] or not set_files if st.button("▶ Start Batch", type="primary", disabled=run_disabled, use_container_width=True): - # Validate if not os.path.isfile(cfg['terminal_path']): st.error(f"terminal64.exe not found at: {cfg['terminal_path']}") elif not os.path.isdir(cfg['tester_folder']): st.error(f"Tester folder not found: {cfg['tester_folder']}") else: - st.session_state['bb_results'] = [] - st.session_state['bb_complete'] = False - st.session_state['bb_running'] = True + st.session_state['bb_results'] = [] + st.session_state['bb_complete'] = False + st.session_state['bb_running'] = True + st.session_state['bb_total'] = len(set_files) + st.session_state['bb_report_dir']= report_folder q = queue.Queue() st.session_state['bb_queue'] = q @@ -578,7 +620,8 @@ def render(): t = threading.Thread( target = run_batch, args = (set_files, cfg, report_folder, lot_mode, - lot_values, instruments, periods, q), + lot_values, instruments, periods, q, + report_names), daemon = True ) st.session_state['bb_thread'] = t @@ -587,28 +630,32 @@ def render(): with col_stop: if st.session_state['bb_running']: - st.warning("⏳ Batch running...") + if st.button("⛔ Cancel", use_container_width=True): + st.session_state['bb_running'] = False + st.session_state['bb_complete'] = True # ── Progress display ─────────────────────────────────────────────────────── - if st.session_state['bb_results'] or st.session_state['bb_running']: + if st.session_state['bb_running'] or st.session_state['bb_results']: st.divider() st.subheader("Progress") + results = st.session_state['bb_results'] + total = st.session_state.get('bb_total', max(len(results), 1)) + done = len([r for r in results if r.get('status') in ('done','failed','error')]) + if st.session_state['bb_running']: - done = len([r for r in st.session_state['bb_results'] if r.get('status') in ('done', 'failed', 'error')]) - total = len(set_files) - st.progress(done / total if total else 0, text=f"{done} / {total} complete") + # Show live progress bar and re-poll + st.progress(done / total if total else 0, + text=f"In progress — {done} / {total} complete") time.sleep(2) st.rerun() + elif st.session_state['bb_complete']: + # Show final progress bar at 100% + st.progress(1.0, text=f"{done} / {total} complete") - results = st.session_state['bb_results'] + # Status table if results: - status_icon = { - 'running': '⏳', - 'done' : '✅', - 'failed' : '❌', - 'error' : '⚠️', - } + status_icon = {'running':'⏳','done':'✅','failed':'❌','error':'⚠️'} def colour_status(val): if val == '✅': return 'color: #2dc653' @@ -617,27 +664,25 @@ def render(): return 'color: #aaa' rows = [{ - 'Status' : status_icon.get(r.get('status', ''), ''), - 'File' : r.get('file', ''), - 'Symbol' : r.get('symbol', ''), - 'Period' : r.get('period', ''), - 'Message': r.get('message', ''), + 'Status' : status_icon.get(r.get('status',''), ''), + 'File' : r.get('file',''), + 'Symbol' : r.get('symbol',''), + 'Period' : r.get('period',''), + 'Message': r.get('message',''), } for r in results] - df_prog = pd.DataFrame(rows) st.dataframe( - df_prog.style.map(colour_status, subset=['Status']), + pd.DataFrame(rows).style.map(colour_status, subset=['Status']), use_container_width=True, hide_index=True ) if st.session_state['bb_complete']: - done = len([r for r in results if r.get('status') == 'done']) - failed = len([r for r in results if r.get('status') in ('failed', 'error')]) - st.success(f"Batch complete — {done} succeeded, {failed} failed") - st.caption(f"Reports saved to: {report_folder}") + n_done = len([r for r in results if r.get('status') == 'done']) + n_failed = len([r for r in results if r.get('status') in ('failed','error')]) + st.success(f"Batch complete — {n_done} succeeded, {n_failed} failed") + st.caption(f"Reports saved to: {st.session_state.get('bb_report_dir', report_folder)}") if st.button("🔄 Clear & Run Another"): - st.session_state['bb_results'] = [] - st.session_state['bb_complete'] = False - st.session_state['bb_running'] = False + for k in ('bb_results','bb_complete','bb_running','bb_total','bb_report_dir'): + st.session_state[k] = [] if k == 'bb_results' else (False if k != 'bb_total' else 0) st.rerun() \ No newline at end of file