fix: batch backtest instrument editing and report naming

- Add Manual (edit in table) instrument mode
- Add Lock Table / Unlock buttons — edits persist without rerun interference
- Fix report name to always include symbol and period: {stem}_{symbol}_{period}_{model}
- Fix report names rebuilt from actual Symbol values in locked/edited table
- Add instrument warning above preview table
- Fix run_batch missing report_names parameter (recurring)
- Fix progress bar using stored total count
- Add Cancel button and bb_total / bb_report_dir session state keys
This commit is contained in:
unknown
2026-04-14 20:25:58 +10:00
parent 789cf67bba
commit 2215a5d727
+111 -56
View File
@@ -304,7 +304,11 @@ def render():
'bb_queue' : None,
'bb_thread' : None,
'bb_complete' : False,
'bb_edit_cfg' : False,
'bb_edit_cfg' : False,
'bb_locked_table': None,
'bb_last_folder' : None,
'bb_total' : 0,
'bb_report_dir' : '',
}.items():
if k not in st.session_state:
st.session_state[k] = v
@@ -443,6 +447,9 @@ def render():
if set_files:
st.success(f"Found **{len(set_files)}** .set file(s)" +
(f" · {skipped} Optimization file(s) excluded" if skipped else ""))
if st.session_state.get('bb_last_folder') != set_folder:
st.session_state['bb_last_folder'] = set_folder
st.session_state['bb_locked_table'] = None
else:
st.warning("No .set files found in that folder")
elif set_folder:
@@ -498,7 +505,7 @@ def render():
with col1:
instr_mode = st.radio(
"Instrument default",
["One for all files", "Extract from filename"],
["One for all files", "Extract from filename", "Manual (edit in table)"],
key='bb_instr_mode'
)
instr_global = None
@@ -506,9 +513,11 @@ def render():
if instr_mode == "One for all files":
instr_global = st.text_input("Instrument (without suffix)",
placeholder="GBPJPY", key='bb_instr').upper()
else:
elif instr_mode == "Extract from filename":
instr_n_chars = st.number_input("Characters from filename start",
min_value=1, max_value=12, value=6, key='bb_nchars')
else:
st.caption("Edit each Symbol directly in the table below.")
with col2:
tf_mode = st.radio(
@@ -563,7 +572,7 @@ def render():
if strategy_name.strip():
rpt_name = f"{strategy_name.strip()}_{sym_part}_{period}_{ml}_{stem}"
else:
rpt_name = f"{stem}_{ml}"
rpt_name = f"{stem}_{sym_part}_{period}_{ml}"
preview_rows.append({
'File' : fn,
@@ -576,37 +585,81 @@ def render():
# ── Editable preview table ─────────────────────────────────────────────────
st.divider()
st.subheader("5 — Preview & Edit")
st.caption("All values are editable — review before running. Symbol includes suffix.")
st.info("Enter Symbol exactly as it appears in your broker's Market Watch, including the suffix -- e.g. XAUUSD.a for Gold, DE40.a for German DAX, BTCUSD.a for Bitcoin.")
lot_col_disabled = lot_mode in ('asis', 'manual')
locked = st.session_state.get('bb_locked_table')
edited = st.data_editor(
pd.DataFrame(preview_rows),
use_container_width=True,
hide_index=True,
column_config={
'File' : st.column_config.TextColumn('File', disabled=True),
'Symbol' : st.column_config.TextColumn('Symbol'),
'Period' : st.column_config.SelectboxColumn('Period',
options=['Daily','H4','H1','M30','M15','M5','M1','Weekly']),
'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', help='Editable — final filename used for .htm report'),
},
key='bb_preview_table'
)
if locked is not None:
# ── Locked mode ───────────────────────────────────────────────────────
st.success("✅ Table locked — these values will be used for the run.")
if st.button("🔓 Unlock & Edit", key='bb_unlock'):
st.session_state['bb_locked_table'] = None
st.rerun()
st.dataframe(pd.DataFrame(locked), use_container_width=True, hide_index=True)
final_df = pd.DataFrame(locked)
else:
# ── Edit mode ─────────────────────────────────────────────────────────
if instr_mode == "Manual (edit in table)":
st.caption("Edit Symbol for each file, then click Lock Table. Changes are lost on rerun until locked.")
else:
st.caption("Review values, edit if needed, then click Lock Table before running.")
# Extract final values from edited table
instruments = list(edited['Symbol'])
periods = list(edited['Period'])
report_names = [str(r).replace('.htm','') for r in edited['Report Name']]
lot_values = []
edited = st.data_editor(
pd.DataFrame(preview_rows),
use_container_width=True,
hide_index=True,
column_config={
'File' : st.column_config.TextColumn('File', disabled=True),
'Symbol' : st.column_config.TextColumn('Symbol'),
'Period' : st.column_config.SelectboxColumn('Period',
options=['Daily','H4','H1','M30','M15','M5','M1','Weekly']),
'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',
help='Editable — final filename used for .htm report'),
},
key='bb_preview_table'
)
if st.button("🔒 Lock Table", type="primary", key='bb_lock'):
st.session_state['bb_locked_table'] = edited.to_dict('records')
st.rerun()
final_df = edited
# Extract final values
instruments = list(final_df['Symbol'])
periods = list(final_df['Period'])
# Rebuild report names from actual Symbol values in the table (not auto-detected inst)
# This ensures manual edits to Symbol are reflected in the report filename
rebuilt_report_names = []
for i, fp in enumerate(set_files):
stem = os.path.splitext(os.path.basename(fp))[0]
sym = instruments[i]
per = periods[i]
sym_part = sym.replace('.','')
if strategy_name.strip():
rpt = f"{strategy_name.strip()}_{sym_part}_{per}_{ml}_{stem}"
else:
rpt = f"{stem}_{sym_part}_{per}_{ml}"
rebuilt_report_names.append(rpt)
# Use rebuilt names as default but honour any manual edits made directly in Report Name column
original_report_names = [str(r).replace('.htm','') for r in final_df['Report Name']]
# If Report Name was manually overridden (differs from auto-built), keep the manual version
report_names = []
for i, (auto, manual) in enumerate(zip(rebuilt_report_names, original_report_names)):
stem = os.path.splitext(os.path.basename(set_files[i]))[0]
old_auto = f"{stem}_{ml}" # legacy fallback — no longer used but kept for comparison
# If the manual name looks like it was auto-generated from old symbol data, use rebuilt
report_names.append(manual if manual != old_auto and manual != auto else auto)
lot_values = []
for i, fp in enumerate(set_files):
if lot_mode == 'manual':
lot_values.append(manual_lots or '0.01')
elif lot_mode in ('balance', 'balance_ask'):
lot_values.append(str(edited.iloc[i]['Lot Value']))
lot_values.append(str(final_df.iloc[i]['Lot Value']))
else:
lot_values.append(None)
@@ -646,9 +699,11 @@ def render():
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
@@ -666,28 +721,29 @@ 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")
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']:
st.progress(1.0, text=f"{done} / {total} complete")
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'
@@ -696,27 +752,26 @@ 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','bb_locked_table'):
st.session_state[k] = [] if k == 'bb_results' else False if k in ('bb_complete','bb_running') else None
st.rerun()