From 789cf67bba4fbe4034c4682ced1c2799f18f03b8 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 15:46:27 +1000 Subject: [PATCH] fix: batch backtest, set comparator and config editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch Backtest: - Fix run_batch signature missing report_names parameter (recurring) - Add inline config editor to Active Config expander — edit and save all settings directly from the UI without re-running CLI script Set Comparator: - Fix UTF-8/UTF-16 encoding detection using BOM check - Fix parse to skip optimisation metadata lines (ParamName,F etc.) - Fix export to preserve all original lines including metadata - Add UBS format detection — warning shown for non-Ultimate Breakout files - Fix view_set_comparator to unpack 4-value return from parse_set_file --- __pycache__/set_comparator.cpython-314.pyc | Bin 3865 -> 6257 bytes .../view_set_comparator.cpython-314.pyc | Bin 11580 -> 11971 bytes view_batch_backtest.py | 174 +++++++++++------- view_set_comparator.py | 11 +- 4 files changed, 113 insertions(+), 72 deletions(-) diff --git a/__pycache__/set_comparator.cpython-314.pyc b/__pycache__/set_comparator.cpython-314.pyc index 095c6fa03ee215ad371147ef52f4a67a7a454eba..fa7ebc9384b1847adf285830b02ef6ed857d6f50 100644 GIT binary patch literal 6257 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 delta 1625 zcmZvcO>7fK6vt=1yS8_|-u1^O4%l%M5)8q7Bs7Jll+Z&(PWSyVcr?FDPo_DYSd*MqV24@=f2?JPBx= z{;K`g_>`Y43NiH!bo?el0*a&QQH_6-9Y8}odCE@gd*&|LXZDO7aF|05bNHF->eV5S z;(+f|mKhoxsM-Oijb@;dH?K0JKZkKiikVPNZ8pno;c zzr3>%NLJo&211pi)O|$V2M+`4l2TLd1^TO8QwcB3&CgxGTIC)n;YE9?Tq`eIYmwxy zk>u}^+VshbxzCT#QY5-`t#)mBrV-f{oR_NXBG;6I3&y;0eYVPypFLyUQ54*^9!FBB zuCMq8R=9yDuo`rG%KMyQ6f!9u8iT7vxC@-a%)(}imnSBSW68KbO1_Xn0uch+?M+uOnU)C6wmUY>J-FE2jBJnR2^LLi=BG54fIdcPoTBIdiltwkEP(Hx`&dltr%OgY*S8n=Baz^VvKvNVFLhM2b1uI) zBw~Tq=_`RL!IXcQc`UZAoWQd%51ejq%1-pc4X!`5>c zZtDd&auFM!8ii~=bNY&{TR4Oe*&m9OyJ zQ!Lne!M5nC64W+m!!g=J(Jz}tY;Q>H5VRhI&-xo&1+7cSA6=9BS0ySkDanJ%qyQNX zkCi2)_N|2nSHpviaH2Btm!ei#(9v(c0V*T4|E`*-3Qe_lX|guCJkU@_K!s}0(y`jH zy4_F*tNb5oq%M%DDqqdcdz)T$%^O|yM(b8xUmjUL@$JbQCvTM++uvFlJg^cwc+dOx zBLxMz)5`GVy<1w@L*}QQY!&8b{uMDOl#Pl#)1HMD8lh+t+Ncn+(*KFB>#ze9&_Ey;N#&QCjNLd$_)PVj>vSka{LcvxND05 diff --git a/__pycache__/view_set_comparator.cpython-314.pyc b/__pycache__/view_set_comparator.cpython-314.pyc index 89df84dbc3ad6994ea4cceed52782d6580277f1b..690a8fd36c2f42e7bc0baf7e581ddd9c07127378 100644 GIT binary patch delta 2608 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 o#UG)2Y(N{729-(#A80LOZsE&u=k diff --git a/view_batch_backtest.py b/view_batch_backtest.py index 6ead0f5..e09c44c 100644 --- a/view_batch_backtest.py +++ b/view_batch_backtest.py @@ -42,12 +42,6 @@ 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: @@ -303,28 +297,14 @@ 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_total' : 0, - 'bb_report_dir': '', + 'bb_running' : False, + 'bb_results' : [], + 'bb_queue' : None, + 'bb_thread' : None, + 'bb_complete' : False, + 'bb_edit_cfg' : False, }.items(): if k not in st.session_state: st.session_state[k] = v @@ -336,14 +316,72 @@ def render(): return with st.expander("📁 Active Config", expanded=False): - c1, c2 = st.columns(2) - c1.markdown(f"**Terminal:** {cfg.get('terminal_label', 'Unknown')}") - c1.markdown(f"**EA:** `{cfg.get('ea_name', '')}`") - c1.markdown(f"**Dates:** {cfg.get('from_date')} → {cfg.get('to_date')}") - c2.markdown(f"**Model:** {cfg.get('model')} ({MODEL_LABELS.get(cfg.get('model','1'), '?')})") - c2.markdown(f"**Deposit:** {cfg.get('deposit')} {cfg.get('currency')}") - c2.markdown(f"**Leverage:** {cfg.get('leverage')} **Suffix:** `{cfg.get('suffix', '.a')}`") - st.caption(f"Config file: {CONFIG_FILE}") + # ── View mode ───────────────────────────────────────────────────── + if not st.session_state.get('bb_edit_cfg', False): + c1, c2 = st.columns(2) + c1.markdown(f"**Terminal:** {cfg.get('terminal_label', 'Unknown')}") + c1.markdown(f"**EA:** `{cfg.get('ea_name', '')}`") + c1.markdown(f"**Dates:** {cfg.get('from_date')} → {cfg.get('to_date')}") + c2.markdown(f"**Model:** {cfg.get('model')} ({MODEL_LABELS.get(cfg.get('model','1'), '?')})") + c2.markdown(f"**Deposit:** {cfg.get('deposit')} {cfg.get('currency')}") + c2.markdown(f"**Leverage:** {cfg.get('leverage')} **Suffix:** `{cfg.get('suffix', '.a')}`") + st.caption(f"Config file: {CONFIG_FILE}") + if st.button("✏️ Edit Config", key='bb_edit_cfg_btn'): + st.session_state['bb_edit_cfg'] = True + st.rerun() + else: + # ── Edit mode ───────────────────────────────────────────────── + st.caption("Edit and save to update mt5_batch_config.json") + e1, e2 = st.columns(2) + new_terminal = e1.text_input("terminal64.exe path", + value=cfg.get('terminal_path',''), key='bb_cfg_terminal') + new_tester = e1.text_input("Tester folder", + value=cfg.get('tester_folder',''), key='bb_cfg_tester') + new_from = e2.text_input("From date (YYYY.MM.DD)", + value=cfg.get('from_date',''), key='bb_cfg_from') + new_to = e2.text_input("To date (YYYY.MM.DD)", + value=cfg.get('to_date',''), key='bb_cfg_to') + e3, e4 = st.columns(2) + new_deposit = e3.text_input("Deposit", + value=cfg.get('deposit','10000'), key='bb_cfg_deposit') + new_currency = e3.text_input("Currency", + value=cfg.get('currency','USD'), key='bb_cfg_currency') + new_leverage = e4.text_input("Leverage", + value=cfg.get('leverage','100'), key='bb_cfg_leverage') + new_suffix = e4.text_input("Instrument suffix (e.g. .a)", + value=cfg.get('suffix','.a'), key='bb_cfg_suffix') + new_model = e3.selectbox("Model", + options=['1','2','4','5'], + format_func=lambda x: f"{x} — {MODEL_LABELS.get(x,'?')}", + index=['1','2','4','5'].index(cfg.get('model','1')), + key='bb_cfg_model') + + sv1, sv2 = st.columns(2) + if sv1.button("💾 Save Config", type="primary", key='bb_save_cfg'): + updated = dict(cfg) + updated.update({ + 'terminal_path': new_terminal, + 'tester_folder': new_tester, + 'from_date' : new_from, + 'to_date' : new_to, + 'deposit' : new_deposit, + 'currency' : new_currency, + 'leverage' : new_leverage, + 'suffix' : new_suffix, + 'model' : new_model, + }) + try: + with open(CONFIG_FILE, 'w') as f: + json.dump(updated, f, indent=2) + cfg = updated + st.session_state['bb_edit_cfg'] = False + st.success("Config saved.") + st.rerun() + except Exception as e: + st.error(f"Could not save config: {e}") + if sv2.button("Cancel", key='bb_cancel_cfg'): + st.session_state['bb_edit_cfg'] = False + st.rerun() suffix = cfg.get('suffix', '.a') @@ -399,7 +437,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 _is_optimisation_file(os.path.basename(f)) + if not os.path.basename(f).lower().startswith('optimization') and '_batch_modified' not in os.path.normpath(f).replace(os.sep, '/')] skipped = len(all_files) - len(set_files) if set_files: @@ -596,23 +634,21 @@ def render(): results.append(update) st.session_state['bb_results'] = results - # ── Run / Cancel buttons ────────────────────────────────────────────────── - col_run, col_stop = st.columns([3, 1]) + col_run, col_stop = st.columns([2, 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_total'] = len(set_files) - st.session_state['bb_report_dir']= report_folder + st.session_state['bb_results'] = [] + st.session_state['bb_complete'] = False + st.session_state['bb_running'] = True q = queue.Queue() st.session_state['bb_queue'] = q @@ -630,32 +666,28 @@ def render(): with col_stop: if st.session_state['bb_running']: - if st.button("⛔ Cancel", use_container_width=True): - st.session_state['bb_running'] = False - st.session_state['bb_complete'] = True + st.warning("⏳ Batch running...") # ── Progress display ─────────────────────────────────────────────────────── - if st.session_state['bb_running'] or st.session_state['bb_results']: + if st.session_state['bb_results'] or st.session_state['bb_running']: 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']: - # Show live progress bar and re-poll - st.progress(done / total if total else 0, - text=f"In progress — {done} / {total} complete") + 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") 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") - # Status table + results = st.session_state['bb_results'] if results: - status_icon = {'running':'⏳','done':'✅','failed':'❌','error':'⚠️'} + status_icon = { + 'running': '⏳', + 'done' : '✅', + 'failed' : '❌', + 'error' : '⚠️', + } def colour_status(val): if val == '✅': return 'color: #2dc653' @@ -664,25 +696,27 @@ 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( - pd.DataFrame(rows).style.map(colour_status, subset=['Status']), + df_prog.style.map(colour_status, subset=['Status']), use_container_width=True, hide_index=True ) if st.session_state['bb_complete']: - 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)}") + 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}") if st.button("🔄 Clear & Run Another"): - 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.session_state['bb_results'] = [] + st.session_state['bb_complete'] = False + st.session_state['bb_running'] = False st.rerun() \ No newline at end of file diff --git a/view_set_comparator.py b/view_set_comparator.py index f9fa28b..6a27195 100644 --- a/view_set_comparator.py +++ b/view_set_comparator.py @@ -53,14 +53,21 @@ def render(): if uploaded is not None: file_bytes = uploaded.read() fname = uploaded.name - params, raw_lines, order = parse_set_file(file_bytes, fname) + params, raw_lines, order, is_ubs = parse_set_file(file_bytes, fname) st.session_state['ea_files'][fname] = params st.session_state['ea_raw'][fname] = raw_lines st.session_state['ea_order'][fname] = order st.session_state['ea_bytes'][fname] = file_bytes if fname not in st.session_state['ea_edited']: st.session_state['ea_edited'][fname] = params.copy() - st.success(f"✓ {fname} — {len(params)} params") + if is_ubs: + st.success(f"✓ {fname} — {len(params)} params") + else: + st.warning( + f"⚠️ {fname} — this file does not appear to be an " + f"Ultimate Breakout System .set file. The comparator is " + f"optimised for UBS format only. Results may be incomplete." + ) # ── Comparison table ────────────────────────────────────────────────────── files_data = st.session_state['ea_files']