Updated portfolio buidler and master pages for bulk file selection or select and scan a folder for MT5 html back test reports

This commit is contained in:
unknown
2026-05-12 13:51:56 +10:00
parent 902547d99f
commit e8ee01304a
2 changed files with 168 additions and 75 deletions
+84 -37
View File
@@ -617,51 +617,98 @@ def render():
unsafe_allow_html=True) unsafe_allow_html=True)
# ── Upload panel ───────────────────────────────────────────────────────── # ── Upload panel ─────────────────────────────────────────────────────────
with st.expander("📂 Upload Strategy Reports", with st.expander("📂 Load Strategy Reports",
expanded=not bool(st.session_state.pb_uploaded_files)): expanded=not bool(st.session_state.pb_uploaded_files)):
st.caption("Accepts: `.htm` · `.html` · `.csv` — one file per slot. "
"Increase the slot count if you need more.")
sc1, sc2 = st.columns([1, 5]) load_mode = st.radio(
with sc1: "Load method",
n_slots = st.selectbox( ["📁 Upload files", "🗂 Scan folder"],
"Slots", horizontal=True, key="pb_load_mode",
list(range(1, 21)), )
index=max(0, st.session_state.pb_n_slots - 1),
key="pb_n_slots_select", if load_mode == "📁 Upload files":
st.caption("Select one or more `.htm` · `.html` · `.csv` files — hold Ctrl/Cmd to pick multiple.")
uploaded_files = st.file_uploader(
"Strategy reports",
type=["htm", "html", "csv"],
accept_multiple_files=True,
key="pb_multi_uploader",
label_visibility="collapsed",
) )
if n_slots != st.session_state.pb_n_slots: if uploaded_files:
st.session_state.pb_n_slots = n_slots added = 0
st.rerun() for f in uploaded_files:
stem = os.path.splitext(f.name)[0]
if stem not in st.session_state.pb_uploaded_files:
df = _parse_uploaded(f)
if df is not None:
df = _ensure_columns(df, stem)
st.session_state.pb_uploaded_files[stem] = df
added += 1
if added:
st.success(f"✅ Loaded {added} file(s)")
st.rerun()
slots_per_row = 5 else: # Scan folder
for row_start in range(0, st.session_state.pb_n_slots, slots_per_row): st.caption("Enter a folder path — all `.htm` / `.html` / `.csv` files inside will be loaded.")
cols = st.columns(slots_per_row) fp_col, sub_col = st.columns([4, 1])
for j in range(slots_per_row): folder_path = fp_col.text_input(
idx = row_start + j "Folder path",
if idx >= st.session_state.pb_n_slots: placeholder=r"e.g. C:\MT5\Reports\Backtest",
break key="pb_folder_path",
with cols[j]: label_visibility="collapsed",
f = st.file_uploader( )
f"File {idx + 1}", include_sub = sub_col.checkbox("Subfolders", value=False, key="pb_folder_sub")
type=["htm", "html", "csv"],
accept_multiple_files=False, if st.button("📂 Scan & Load", key="pb_scan_btn", type="primary"):
key=f"pb_upload_slot_{idx}", if not folder_path or not os.path.isdir(folder_path):
) st.error("Folder not found — check the path and try again.")
if f is not None: else:
stem = os.path.splitext(f.name)[0] import glob as _glob
if stem not in st.session_state.pb_uploaded_files: exts = ("*.htm", "*.html", "*.csv")
df = _parse_uploaded(f) found = []
if df is not None: for ext in exts:
df = _ensure_columns(df, stem) pattern = os.path.join(folder_path, "**", ext) if include_sub \
st.session_state.pb_uploaded_files[stem] = df else os.path.join(folder_path, ext)
st.success(f"{stem}{len(df):,} trades") found.extend(_glob.glob(pattern, recursive=include_sub))
found = sorted(set(found))
if not found:
st.warning("No `.htm` / `.html` / `.csv` files found in that folder.")
else:
added = skipped = errors = 0
for fpath in found:
stem = os.path.splitext(os.path.basename(fpath))[0]
if stem in st.session_state.pb_uploaded_files:
skipped += 1
continue
try:
with open(fpath, "rb") as fh:
raw = fh.read()
parser = _get_parser()
result = parser.detect_and_parse(raw, os.path.basename(fpath))
df = result[0] if isinstance(result, tuple) else result
if df is not None:
df = _ensure_columns(df, stem)
st.session_state.pb_uploaded_files[stem] = df
added += 1
except Exception as e:
st.warning(f"Could not load **{stem}**: {e}")
errors += 1
parts = [f"{added} loaded"]
if skipped: parts.append(f"{skipped} already present")
if errors: parts.append(f"{errors} failed")
st.success(" · ".join(parts))
if added:
st.rerun()
if st.session_state.pb_uploaded_files: if st.session_state.pb_uploaded_files:
st.markdown("**Loaded strategies:**") st.markdown("---")
st.markdown(f"**{len(st.session_state.pb_uploaded_files)} strategies loaded:**")
to_remove = [] to_remove = []
for label in list(st.session_state.pb_uploaded_files): for label in list(st.session_state.pb_uploaded_files):
c1, c2 = st.columns([6,1]) c1, c2 = st.columns([6, 1])
c1.markdown(f"<span class='chip'>📈 {label}</span>", c1.markdown(f"<span class='chip'>📈 {label}</span>",
unsafe_allow_html=True) unsafe_allow_html=True)
if c2.button("", key=f"rm_{label}"): if c2.button("", key=f"rm_{label}"):
+84 -38
View File
@@ -543,48 +543,94 @@ def render():
st.rerun() st.rerun()
# ── Upload ─────────────────────────────────────────────────────────────── # ── Upload ───────────────────────────────────────────────────────────────
with st.expander("📂 Upload Backtest Files", with st.expander("📂 Load Backtest Files",
expanded=not bool(st.session_state.pm_files)): expanded=not bool(st.session_state.pm_files)):
st.caption("Accepts `.htm` · `.html` · `.csv` — one file per slot. "
"Increase the slot count if you need more.")
sc1, _ = st.columns([1, 5]) load_mode = st.radio(
with sc1: "Load method",
n_slots = st.selectbox( ["📁 Upload files", "🗂 Scan folder"],
"Slots", horizontal=True, key="pm_load_mode",
list(range(1, 21)), )
index=max(0, st.session_state.pm_n_slots - 1),
key="pm_n_slots_select", if load_mode == "📁 Upload files":
st.caption("Select one or more `.htm` · `.html` · `.csv` files — hold Ctrl/Cmd to pick multiple.")
uploaded_files = st.file_uploader(
"Backtest reports",
type=["htm", "html", "csv"],
accept_multiple_files=True,
key=f"pm_multi_uploader_{st.session_state.pm_uploader_key}",
label_visibility="collapsed",
) )
if n_slots != st.session_state.pm_n_slots: if uploaded_files:
st.session_state.pm_n_slots = n_slots added = 0
st.rerun() for f in uploaded_files:
stem = os.path.splitext(f.name)[0]
if stem not in st.session_state.pm_files:
df = _parse_file(f)
if df is not None:
df = _normalise(df.copy(), stem)
st.session_state.pm_files[stem] = df
added += 1
if added:
st.success(f"✅ Loaded {added} file(s)")
st.rerun()
slots_per_row = 5 else: # Scan folder
for row_start in range(0, st.session_state.pm_n_slots, slots_per_row): st.caption("Enter a folder path — all `.htm` / `.html` / `.csv` files inside will be loaded.")
cols = st.columns(slots_per_row) fp_col, sub_col = st.columns([4, 1])
for j in range(slots_per_row): folder_path = fp_col.text_input(
idx = row_start + j "Folder path",
if idx >= st.session_state.pm_n_slots: placeholder=r"e.g. C:\MT5\Reports\Backtest",
break key="pm_folder_path",
with cols[j]: label_visibility="collapsed",
f = st.file_uploader( )
f"File {idx + 1}", include_sub = sub_col.checkbox("Subfolders", value=False, key="pm_folder_sub")
type=["htm", "html", "csv"],
accept_multiple_files=False, if st.button("📂 Scan & Load", key="pm_scan_btn", type="primary"):
key=f"pm_upload_slot_{st.session_state.pm_uploader_key}_{idx}", if not folder_path or not os.path.isdir(folder_path):
) st.error("Folder not found — check the path and try again.")
if f is not None: else:
stem = os.path.splitext(f.name)[0] import glob as _glob
if stem not in st.session_state.pm_files: found = []
df = _parse_file(f) for ext in ("*.htm", "*.html", "*.csv"):
if df is not None: pattern = os.path.join(folder_path, "**", ext) if include_sub \
df = _normalise(df.copy(), stem) else os.path.join(folder_path, ext)
st.session_state.pm_files[stem] = df found.extend(_glob.glob(pattern, recursive=include_sub))
st.success(f"{stem}{len(df):,} trades") found = sorted(set(found))
if not found:
st.warning("No `.htm` / `.html` / `.csv` files found in that folder.")
else:
added = skipped = errors = 0
for fpath in found:
stem = os.path.splitext(os.path.basename(fpath))[0]
if stem in st.session_state.pm_files:
skipped += 1
continue
try:
with open(fpath, "rb") as fh:
raw = fh.read()
parser = _get_parser()
result = parser.detect_and_parse(raw, os.path.basename(fpath))
df = result[0] if isinstance(result, tuple) else result
if df is not None:
df = _normalise(df.copy(), stem)
st.session_state.pm_files[stem] = df
added += 1
except Exception as e:
st.warning(f"Could not load **{stem}**: {e}")
errors += 1
parts = [f"{added} loaded"]
if skipped: parts.append(f"{skipped} already present")
if errors: parts.append(f"{errors} failed")
st.success(" · ".join(parts))
if added:
st.rerun()
if st.session_state.pm_files: if st.session_state.pm_files:
# Clear all button st.markdown("---")
st.markdown(f"**{len(st.session_state.pm_files)} files loaded:**")
if st.button("🗑 Clear All Files", key="pm_clear_all"): if st.button("🗑 Clear All Files", key="pm_clear_all"):
st.session_state.pm_files = {} st.session_state.pm_files = {}
st.session_state.pm_custom_names = {} st.session_state.pm_custom_names = {}
@@ -593,7 +639,7 @@ def render():
to_remove = [] to_remove = []
for label in list(st.session_state.pm_files): for label in list(st.session_state.pm_files):
c1, c2 = st.columns([6,1]) c1, c2 = st.columns([6, 1])
c1.markdown(f"<span class='chip'>📈 {label}</span>", unsafe_allow_html=True) c1.markdown(f"<span class='chip'>📈 {label}</span>", unsafe_allow_html=True)
if c2.button("", key=f"pmrm_{label}"): if c2.button("", key=f"pmrm_{label}"):
to_remove.append(label) to_remove.append(label)
@@ -604,7 +650,7 @@ def render():
strategy_dfs: dict = st.session_state.pm_files strategy_dfs: dict = st.session_state.pm_files
if not strategy_dfs: if not strategy_dfs:
st.info("Upload backtest files above to get started.") st.info("Load backtest files above to get started.")
return return
# Explode each uploaded file into per-strategy DataFrames # Explode each uploaded file into per-strategy DataFrames