From e8ee01304a138ebc27345e411fd232e9c832accc Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 12 May 2026 13:51:56 +1000 Subject: [PATCH] Updated portfolio buidler and master pages for bulk file selection or select and scan a folder for MT5 html back test reports --- view_portfolio_builder.py | 121 +++++++++++++++++++++++++------------ view_portfolio_master.py | 122 ++++++++++++++++++++++++++------------ 2 files changed, 168 insertions(+), 75 deletions(-) diff --git a/view_portfolio_builder.py b/view_portfolio_builder.py index a125ee8..9eac10b 100644 --- a/view_portfolio_builder.py +++ b/view_portfolio_builder.py @@ -617,51 +617,98 @@ def render(): unsafe_allow_html=True) # ── Upload panel ───────────────────────────────────────────────────────── - with st.expander("📂 Upload Strategy Reports", + with st.expander("📂 Load Strategy Reports", 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]) - with sc1: - n_slots = st.selectbox( - "Slots", - list(range(1, 21)), - index=max(0, st.session_state.pb_n_slots - 1), - key="pb_n_slots_select", + load_mode = st.radio( + "Load method", + ["📁 Upload files", "🗂 Scan folder"], + horizontal=True, key="pb_load_mode", + ) + + 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: - st.session_state.pb_n_slots = n_slots - st.rerun() + if uploaded_files: + added = 0 + 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 - for row_start in range(0, st.session_state.pb_n_slots, slots_per_row): - cols = st.columns(slots_per_row) - for j in range(slots_per_row): - idx = row_start + j - if idx >= st.session_state.pb_n_slots: - break - with cols[j]: - f = st.file_uploader( - f"File {idx + 1}", - type=["htm", "html", "csv"], - accept_multiple_files=False, - key=f"pb_upload_slot_{idx}", - ) - if f is not None: - 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 - st.success(f"✅ {stem} — {len(df):,} trades") + else: # Scan folder + st.caption("Enter a folder path — all `.htm` / `.html` / `.csv` files inside will be loaded.") + fp_col, sub_col = st.columns([4, 1]) + folder_path = fp_col.text_input( + "Folder path", + placeholder=r"e.g. C:\MT5\Reports\Backtest", + key="pb_folder_path", + label_visibility="collapsed", + ) + include_sub = sub_col.checkbox("Subfolders", value=False, key="pb_folder_sub") + + if st.button("📂 Scan & Load", key="pb_scan_btn", type="primary"): + if not folder_path or not os.path.isdir(folder_path): + st.error("Folder not found — check the path and try again.") + else: + import glob as _glob + exts = ("*.htm", "*.html", "*.csv") + found = [] + for ext in exts: + pattern = os.path.join(folder_path, "**", ext) if include_sub \ + else os.path.join(folder_path, ext) + 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: - st.markdown("**Loaded strategies:**") + st.markdown("---") + st.markdown(f"**{len(st.session_state.pb_uploaded_files)} strategies loaded:**") to_remove = [] 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"📈 {label}", unsafe_allow_html=True) if c2.button("✕", key=f"rm_{label}"): diff --git a/view_portfolio_master.py b/view_portfolio_master.py index 9976a8c..ec95ee5 100644 --- a/view_portfolio_master.py +++ b/view_portfolio_master.py @@ -543,48 +543,94 @@ def render(): st.rerun() # ── Upload ─────────────────────────────────────────────────────────────── - with st.expander("📂 Upload Backtest Files", + with st.expander("📂 Load Backtest 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]) - with sc1: - n_slots = st.selectbox( - "Slots", - list(range(1, 21)), - index=max(0, st.session_state.pm_n_slots - 1), - key="pm_n_slots_select", + load_mode = st.radio( + "Load method", + ["📁 Upload files", "🗂 Scan folder"], + horizontal=True, key="pm_load_mode", + ) + + 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: - st.session_state.pm_n_slots = n_slots - st.rerun() + if uploaded_files: + added = 0 + 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 - for row_start in range(0, st.session_state.pm_n_slots, slots_per_row): - cols = st.columns(slots_per_row) - for j in range(slots_per_row): - idx = row_start + j - if idx >= st.session_state.pm_n_slots: - break - with cols[j]: - f = st.file_uploader( - f"File {idx + 1}", - type=["htm", "html", "csv"], - accept_multiple_files=False, - key=f"pm_upload_slot_{st.session_state.pm_uploader_key}_{idx}", - ) - if f is not None: - 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 - st.success(f"✅ {stem} — {len(df):,} trades") + else: # Scan folder + st.caption("Enter a folder path — all `.htm` / `.html` / `.csv` files inside will be loaded.") + fp_col, sub_col = st.columns([4, 1]) + folder_path = fp_col.text_input( + "Folder path", + placeholder=r"e.g. C:\MT5\Reports\Backtest", + key="pm_folder_path", + label_visibility="collapsed", + ) + include_sub = sub_col.checkbox("Subfolders", value=False, key="pm_folder_sub") + + if st.button("📂 Scan & Load", key="pm_scan_btn", type="primary"): + if not folder_path or not os.path.isdir(folder_path): + st.error("Folder not found — check the path and try again.") + else: + import glob as _glob + found = [] + for ext in ("*.htm", "*.html", "*.csv"): + pattern = os.path.join(folder_path, "**", ext) if include_sub \ + else os.path.join(folder_path, ext) + 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.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: - # 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"): st.session_state.pm_files = {} st.session_state.pm_custom_names = {} @@ -593,7 +639,7 @@ def render(): to_remove = [] for label in list(st.session_state.pm_files): - c1, c2 = st.columns([6,1]) + c1, c2 = st.columns([6, 1]) c1.markdown(f"📈 {label}", unsafe_allow_html=True) if c2.button("✕", key=f"pmrm_{label}"): to_remove.append(label) @@ -604,7 +650,7 @@ def render(): strategy_dfs: dict = st.session_state.pm_files if not strategy_dfs: - st.info("Upload backtest files above to get started.") + st.info("Load backtest files above to get started.") return # Explode each uploaded file into per-strategy DataFrames