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:
+111
-56
@@ -304,7 +304,11 @@ def render():
|
|||||||
'bb_queue' : None,
|
'bb_queue' : None,
|
||||||
'bb_thread' : None,
|
'bb_thread' : None,
|
||||||
'bb_complete' : False,
|
'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():
|
}.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
|
||||||
@@ -443,6 +447,9 @@ def render():
|
|||||||
if set_files:
|
if set_files:
|
||||||
st.success(f"Found **{len(set_files)}** .set file(s)" +
|
st.success(f"Found **{len(set_files)}** .set file(s)" +
|
||||||
(f" · {skipped} Optimization file(s) excluded" if skipped else ""))
|
(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:
|
else:
|
||||||
st.warning("No .set files found in that folder")
|
st.warning("No .set files found in that folder")
|
||||||
elif set_folder:
|
elif set_folder:
|
||||||
@@ -498,7 +505,7 @@ def render():
|
|||||||
with col1:
|
with col1:
|
||||||
instr_mode = st.radio(
|
instr_mode = st.radio(
|
||||||
"Instrument default",
|
"Instrument default",
|
||||||
["One for all files", "Extract from filename"],
|
["One for all files", "Extract from filename", "Manual (edit in table)"],
|
||||||
key='bb_instr_mode'
|
key='bb_instr_mode'
|
||||||
)
|
)
|
||||||
instr_global = None
|
instr_global = None
|
||||||
@@ -506,9 +513,11 @@ def render():
|
|||||||
if instr_mode == "One for all files":
|
if instr_mode == "One for all files":
|
||||||
instr_global = st.text_input("Instrument (without suffix)",
|
instr_global = st.text_input("Instrument (without suffix)",
|
||||||
placeholder="GBPJPY", key='bb_instr').upper()
|
placeholder="GBPJPY", key='bb_instr').upper()
|
||||||
else:
|
elif instr_mode == "Extract from filename":
|
||||||
instr_n_chars = st.number_input("Characters from filename start",
|
instr_n_chars = st.number_input("Characters from filename start",
|
||||||
min_value=1, max_value=12, value=6, key='bb_nchars')
|
min_value=1, max_value=12, value=6, key='bb_nchars')
|
||||||
|
else:
|
||||||
|
st.caption("Edit each Symbol directly in the table below.")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
tf_mode = st.radio(
|
tf_mode = st.radio(
|
||||||
@@ -563,7 +572,7 @@ def render():
|
|||||||
if strategy_name.strip():
|
if strategy_name.strip():
|
||||||
rpt_name = f"{strategy_name.strip()}_{sym_part}_{period}_{ml}_{stem}"
|
rpt_name = f"{strategy_name.strip()}_{sym_part}_{period}_{ml}_{stem}"
|
||||||
else:
|
else:
|
||||||
rpt_name = f"{stem}_{ml}"
|
rpt_name = f"{stem}_{sym_part}_{period}_{ml}"
|
||||||
|
|
||||||
preview_rows.append({
|
preview_rows.append({
|
||||||
'File' : fn,
|
'File' : fn,
|
||||||
@@ -576,37 +585,81 @@ def render():
|
|||||||
# ── Editable preview table ─────────────────────────────────────────────────
|
# ── Editable preview table ─────────────────────────────────────────────────
|
||||||
st.divider()
|
st.divider()
|
||||||
st.subheader("5 — Preview & Edit")
|
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')
|
lot_col_disabled = lot_mode in ('asis', 'manual')
|
||||||
|
locked = st.session_state.get('bb_locked_table')
|
||||||
|
|
||||||
edited = st.data_editor(
|
if locked is not None:
|
||||||
pd.DataFrame(preview_rows),
|
# ── Locked mode ───────────────────────────────────────────────────────
|
||||||
use_container_width=True,
|
st.success("✅ Table locked — these values will be used for the run.")
|
||||||
hide_index=True,
|
if st.button("🔓 Unlock & Edit", key='bb_unlock'):
|
||||||
column_config={
|
st.session_state['bb_locked_table'] = None
|
||||||
'File' : st.column_config.TextColumn('File', disabled=True),
|
st.rerun()
|
||||||
'Symbol' : st.column_config.TextColumn('Symbol'),
|
st.dataframe(pd.DataFrame(locked), use_container_width=True, hide_index=True)
|
||||||
'Period' : st.column_config.SelectboxColumn('Period',
|
final_df = pd.DataFrame(locked)
|
||||||
options=['Daily','H4','H1','M30','M15','M5','M1','Weekly']),
|
else:
|
||||||
'Lot Value' : st.column_config.TextColumn('Lot Value',
|
# ── Edit mode ─────────────────────────────────────────────────────────
|
||||||
disabled=lot_col_disabled,
|
if instr_mode == "Manual (edit in table)":
|
||||||
help='Disabled for as-is and manual modes'),
|
st.caption("Edit Symbol for each file, then click Lock Table. Changes are lost on rerun until locked.")
|
||||||
'Report Name': st.column_config.TextColumn('Report Name', help='Editable — final filename used for .htm report'),
|
else:
|
||||||
},
|
st.caption("Review values, edit if needed, then click Lock Table before running.")
|
||||||
key='bb_preview_table'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract final values from edited table
|
edited = st.data_editor(
|
||||||
instruments = list(edited['Symbol'])
|
pd.DataFrame(preview_rows),
|
||||||
periods = list(edited['Period'])
|
use_container_width=True,
|
||||||
report_names = [str(r).replace('.htm','') for r in edited['Report Name']]
|
hide_index=True,
|
||||||
lot_values = []
|
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):
|
for i, fp in enumerate(set_files):
|
||||||
if lot_mode == 'manual':
|
if lot_mode == 'manual':
|
||||||
lot_values.append(manual_lots or '0.01')
|
lot_values.append(manual_lots or '0.01')
|
||||||
elif lot_mode in ('balance', 'balance_ask'):
|
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:
|
else:
|
||||||
lot_values.append(None)
|
lot_values.append(None)
|
||||||
|
|
||||||
@@ -646,9 +699,11 @@ def render():
|
|||||||
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
|
||||||
@@ -666,28 +721,29 @@ def render():
|
|||||||
|
|
||||||
with col_stop:
|
with col_stop:
|
||||||
if st.session_state['bb_running']:
|
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 ───────────────────────────────────────────────────────
|
# ── 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.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']:
|
||||||
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']:
|
||||||
|
st.progress(1.0, text=f"{done} / {total} complete")
|
||||||
|
|
||||||
results = st.session_state['bb_results']
|
|
||||||
if results:
|
if results:
|
||||||
status_icon = {
|
status_icon = {'running':'⏳','done':'✅','failed':'❌','error':'⚠️'}
|
||||||
'running': '⏳',
|
|
||||||
'done' : '✅',
|
|
||||||
'failed' : '❌',
|
|
||||||
'error' : '⚠️',
|
|
||||||
}
|
|
||||||
|
|
||||||
def colour_status(val):
|
def colour_status(val):
|
||||||
if val == '✅': return 'color: #2dc653'
|
if val == '✅': return 'color: #2dc653'
|
||||||
@@ -696,27 +752,26 @@ 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(
|
||||||
df_prog.style.map(colour_status, subset=['Status']),
|
pd.DataFrame(rows).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']:
|
||||||
done = len([r for r in results if r.get('status') == 'done'])
|
n_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')])
|
n_failed = len([r for r in results if r.get('status') in ('failed','error')])
|
||||||
st.success(f"Batch complete — {done} succeeded, {failed} failed")
|
st.success(f"Batch complete — {n_done} succeeded, {n_failed} failed")
|
||||||
st.caption(f"Reports saved to: {report_folder}")
|
st.caption(f"Reports saved to: {st.session_state.get('bb_report_dir', report_folder)}")
|
||||||
|
|
||||||
if st.button("🔄 Clear & Run Another"):
|
if st.button("🔄 Clear & Run Another"):
|
||||||
st.session_state['bb_results'] = []
|
for k in ('bb_results','bb_complete','bb_running','bb_total',
|
||||||
st.session_state['bb_complete'] = False
|
'bb_report_dir','bb_locked_table'):
|
||||||
st.session_state['bb_running'] = False
|
st.session_state[k] = [] if k == 'bb_results' else False if k in ('bb_complete','bb_running') else None
|
||||||
st.rerun()
|
st.rerun()
|
||||||
Reference in New Issue
Block a user