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:
+75
-28
@@ -617,48 +617,95 @@ 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 n_slots != st.session_state.pb_n_slots:
|
|
||||||
st.session_state.pb_n_slots = n_slots
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
slots_per_row = 5
|
if load_mode == "📁 Upload files":
|
||||||
for row_start in range(0, st.session_state.pb_n_slots, slots_per_row):
|
st.caption("Select one or more `.htm` · `.html` · `.csv` files — hold Ctrl/Cmd to pick multiple.")
|
||||||
cols = st.columns(slots_per_row)
|
uploaded_files = st.file_uploader(
|
||||||
for j in range(slots_per_row):
|
"Strategy reports",
|
||||||
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"],
|
type=["htm", "html", "csv"],
|
||||||
accept_multiple_files=False,
|
accept_multiple_files=True,
|
||||||
key=f"pb_upload_slot_{idx}",
|
key="pb_multi_uploader",
|
||||||
|
label_visibility="collapsed",
|
||||||
)
|
)
|
||||||
if f is not None:
|
if uploaded_files:
|
||||||
|
added = 0
|
||||||
|
for f in uploaded_files:
|
||||||
stem = os.path.splitext(f.name)[0]
|
stem = os.path.splitext(f.name)[0]
|
||||||
if stem not in st.session_state.pb_uploaded_files:
|
if stem not in st.session_state.pb_uploaded_files:
|
||||||
df = _parse_uploaded(f)
|
df = _parse_uploaded(f)
|
||||||
if df is not None:
|
if df is not None:
|
||||||
df = _ensure_columns(df, stem)
|
df = _ensure_columns(df, stem)
|
||||||
st.session_state.pb_uploaded_files[stem] = df
|
st.session_state.pb_uploaded_files[stem] = df
|
||||||
st.success(f"✅ {stem} — {len(df):,} trades")
|
added += 1
|
||||||
|
if added:
|
||||||
|
st.success(f"✅ Loaded {added} file(s)")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
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:
|
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])
|
||||||
|
|||||||
+75
-29
@@ -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 n_slots != st.session_state.pm_n_slots:
|
|
||||||
st.session_state.pm_n_slots = n_slots
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
slots_per_row = 5
|
if load_mode == "📁 Upload files":
|
||||||
for row_start in range(0, st.session_state.pm_n_slots, slots_per_row):
|
st.caption("Select one or more `.htm` · `.html` · `.csv` files — hold Ctrl/Cmd to pick multiple.")
|
||||||
cols = st.columns(slots_per_row)
|
uploaded_files = st.file_uploader(
|
||||||
for j in range(slots_per_row):
|
"Backtest reports",
|
||||||
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"],
|
type=["htm", "html", "csv"],
|
||||||
accept_multiple_files=False,
|
accept_multiple_files=True,
|
||||||
key=f"pm_upload_slot_{st.session_state.pm_uploader_key}_{idx}",
|
key=f"pm_multi_uploader_{st.session_state.pm_uploader_key}",
|
||||||
|
label_visibility="collapsed",
|
||||||
)
|
)
|
||||||
if f is not None:
|
if uploaded_files:
|
||||||
|
added = 0
|
||||||
|
for f in uploaded_files:
|
||||||
stem = os.path.splitext(f.name)[0]
|
stem = os.path.splitext(f.name)[0]
|
||||||
if stem not in st.session_state.pm_files:
|
if stem not in st.session_state.pm_files:
|
||||||
df = _parse_file(f)
|
df = _parse_file(f)
|
||||||
if df is not None:
|
if df is not None:
|
||||||
df = _normalise(df.copy(), stem)
|
df = _normalise(df.copy(), stem)
|
||||||
st.session_state.pm_files[stem] = df
|
st.session_state.pm_files[stem] = df
|
||||||
st.success(f"✅ {stem} — {len(df):,} trades")
|
added += 1
|
||||||
|
if added:
|
||||||
|
st.success(f"✅ Loaded {added} file(s)")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
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:
|
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 = {}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user