fix: batch backtest, set comparator and config editor

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
This commit is contained in:
unknown
2026-04-14 15:46:27 +10:00
parent 25b734a559
commit 789cf67bba
4 changed files with 113 additions and 72 deletions
Binary file not shown.
Binary file not shown.
+104 -70
View File
@@ -42,12 +42,6 @@ SUFFIX = '.a'
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── 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(): def load_config():
if os.path.isfile(CONFIG_FILE): if os.path.isfile(CONFIG_FILE):
try: try:
@@ -303,28 +297,14 @@ def run_batch(set_files, cfg, report_folder, lot_mode, lot_values,
def render(): def render():
st.title("📋 Batch Backtest") 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 ───────────────────────────────────────────────────────── # ── Session state ─────────────────────────────────────────────────────────
for k, v in { for k, v in {
'bb_running' : False, 'bb_running' : False,
'bb_results' : [], 'bb_results' : [],
'bb_queue' : None, 'bb_queue' : None,
'bb_thread' : None, 'bb_thread' : None,
'bb_complete' : False, 'bb_complete' : False,
'bb_total' : 0, 'bb_edit_cfg' : False,
'bb_report_dir': '',
}.items(): }.items():
if k not in st.session_state: if k not in st.session_state:
st.session_state[k] = v st.session_state[k] = v
@@ -336,14 +316,72 @@ def render():
return return
with st.expander("📁 Active Config", expanded=False): with st.expander("📁 Active Config", expanded=False):
c1, c2 = st.columns(2) # ── View mode ─────────────────────────────────────────────────────
c1.markdown(f"**Terminal:** {cfg.get('terminal_label', 'Unknown')}") if not st.session_state.get('bb_edit_cfg', False):
c1.markdown(f"**EA:** `{cfg.get('ea_name', '')}`") c1, c2 = st.columns(2)
c1.markdown(f"**Dates:** {cfg.get('from_date')}{cfg.get('to_date')}") c1.markdown(f"**Terminal:** {cfg.get('terminal_label', 'Unknown')}")
c2.markdown(f"**Model:** {cfg.get('model')} ({MODEL_LABELS.get(cfg.get('model','1'), '?')})") c1.markdown(f"**EA:** `{cfg.get('ea_name', '')}`")
c2.markdown(f"**Deposit:** {cfg.get('deposit')} {cfg.get('currency')}") c1.markdown(f"**Dates:** {cfg.get('from_date')} {cfg.get('to_date')}")
c2.markdown(f"**Leverage:** {cfg.get('leverage')} **Suffix:** `{cfg.get('suffix', '.a')}`") c2.markdown(f"**Model:** {cfg.get('model')} ({MODEL_LABELS.get(cfg.get('model','1'), '?')})")
st.caption(f"Config file: {CONFIG_FILE}") 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') suffix = cfg.get('suffix', '.a')
@@ -399,7 +437,7 @@ def render():
else: else:
all_files = sorted(glob.glob(os.path.join(set_folder, '*.set'))) all_files = sorted(glob.glob(os.path.join(set_folder, '*.set')))
set_files = [f for f in all_files 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, '/')] and '_batch_modified' not in os.path.normpath(f).replace(os.sep, '/')]
skipped = len(all_files) - len(set_files) skipped = len(all_files) - len(set_files)
if set_files: if set_files:
@@ -596,23 +634,21 @@ def render():
results.append(update) results.append(update)
st.session_state['bb_results'] = results st.session_state['bb_results'] = results
# ── Run / Cancel buttons ────────────────────────────────────────────────── col_run, col_stop = st.columns([2, 1])
col_run, col_stop = st.columns([3, 1])
with col_run: with col_run:
run_disabled = st.session_state['bb_running'] or not set_files run_disabled = st.session_state['bb_running'] or not set_files
if st.button("▶ Start Batch", type="primary", if st.button("▶ Start Batch", type="primary",
disabled=run_disabled, use_container_width=True): disabled=run_disabled, use_container_width=True):
# Validate
if not os.path.isfile(cfg['terminal_path']): if not os.path.isfile(cfg['terminal_path']):
st.error(f"terminal64.exe not found at: {cfg['terminal_path']}") st.error(f"terminal64.exe not found at: {cfg['terminal_path']}")
elif not os.path.isdir(cfg['tester_folder']): elif not os.path.isdir(cfg['tester_folder']):
st.error(f"Tester folder not found: {cfg['tester_folder']}") st.error(f"Tester folder not found: {cfg['tester_folder']}")
else: else:
st.session_state['bb_results'] = [] st.session_state['bb_results'] = []
st.session_state['bb_complete'] = False st.session_state['bb_complete'] = False
st.session_state['bb_running'] = True 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() q = queue.Queue()
st.session_state['bb_queue'] = q st.session_state['bb_queue'] = q
@@ -630,32 +666,28 @@ def render():
with col_stop: with col_stop:
if st.session_state['bb_running']: if st.session_state['bb_running']:
if st.button("⛔ Cancel", use_container_width=True): st.warning("⏳ Batch running...")
st.session_state['bb_running'] = False
st.session_state['bb_complete'] = True
# ── Progress display ─────────────────────────────────────────────────────── # ── 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.divider()
st.subheader("Progress") 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']: if st.session_state['bb_running']:
# Show live progress bar and re-poll done = len([r for r in st.session_state['bb_results'] if r.get('status') in ('done', 'failed', 'error')])
st.progress(done / total if total else 0, total = len(set_files)
text=f"In progress — {done} / {total} complete") st.progress(done / total if total else 0, text=f"{done} / {total} complete")
time.sleep(2) time.sleep(2)
st.rerun() 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: if results:
status_icon = {'running':'','done':'','failed':'','error':'⚠️'} status_icon = {
'running': '',
'done' : '',
'failed' : '',
'error' : '⚠️',
}
def colour_status(val): def colour_status(val):
if val == '': return 'color: #2dc653' if val == '': return 'color: #2dc653'
@@ -664,25 +696,27 @@ def render():
return 'color: #aaa' return 'color: #aaa'
rows = [{ rows = [{
'Status' : status_icon.get(r.get('status',''), ''), 'Status' : status_icon.get(r.get('status', ''), ''),
'File' : r.get('file',''), 'File' : r.get('file', ''),
'Symbol' : r.get('symbol',''), 'Symbol' : r.get('symbol', ''),
'Period' : r.get('period',''), 'Period' : r.get('period', ''),
'Message': r.get('message',''), 'Message': r.get('message', ''),
} for r in results] } for r in results]
df_prog = pd.DataFrame(rows)
st.dataframe( 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 use_container_width=True, hide_index=True
) )
if st.session_state['bb_complete']: if st.session_state['bb_complete']:
n_done = len([r for r in results if r.get('status') == 'done']) 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')]) 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.success(f"Batch complete — {done} succeeded, {failed} failed")
st.caption(f"Reports saved to: {st.session_state.get('bb_report_dir', report_folder)}") st.caption(f"Reports saved to: {report_folder}")
if st.button("🔄 Clear & Run Another"): if st.button("🔄 Clear & Run Another"):
for k in ('bb_results','bb_complete','bb_running','bb_total','bb_report_dir'): st.session_state['bb_results'] = []
st.session_state[k] = [] if k == 'bb_results' else (False if k != 'bb_total' else 0) st.session_state['bb_complete'] = False
st.session_state['bb_running'] = False
st.rerun() st.rerun()
+9 -2
View File
@@ -53,14 +53,21 @@ def render():
if uploaded is not None: if uploaded is not None:
file_bytes = uploaded.read() file_bytes = uploaded.read()
fname = uploaded.name 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_files'][fname] = params
st.session_state['ea_raw'][fname] = raw_lines st.session_state['ea_raw'][fname] = raw_lines
st.session_state['ea_order'][fname] = order st.session_state['ea_order'][fname] = order
st.session_state['ea_bytes'][fname] = file_bytes st.session_state['ea_bytes'][fname] = file_bytes
if fname not in st.session_state['ea_edited']: if fname not in st.session_state['ea_edited']:
st.session_state['ea_edited'][fname] = params.copy() 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 ────────────────────────────────────────────────────── # ── Comparison table ──────────────────────────────────────────────────────
files_data = st.session_state['ea_files'] files_data = st.session_state['ea_files']