fix(uploaders): convert master/builder to single-file slot pattern
Multi-file uploaders trigger Windows browsers' folder-select mode which bypasses the type filter. Switch to N single-file slots (matching the working set comparator pattern) so the OS picker stays in single-file mode where folder selection is impossible. Default 5 slots, expandable up to 20 via dropdown.
This commit is contained in:
+38
-25
@@ -592,6 +592,7 @@ def _init_state():
|
|||||||
"pb_portfolios": {},
|
"pb_portfolios": {},
|
||||||
"pb_lot_overrides": {}, # label → float multiplier
|
"pb_lot_overrides": {}, # label → float multiplier
|
||||||
"pb_deposit": 10000.0,
|
"pb_deposit": 10000.0,
|
||||||
|
"pb_n_slots": 5, # number of file upload slots shown
|
||||||
}.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
|
||||||
@@ -618,31 +619,43 @@ def render():
|
|||||||
# ── Upload panel ─────────────────────────────────────────────────────────
|
# ── Upload panel ─────────────────────────────────────────────────────────
|
||||||
with st.expander("📂 Upload Strategy Reports",
|
with st.expander("📂 Upload 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`")
|
st.caption("Accepts: `.htm` · `.html` · `.csv` — one file per slot. "
|
||||||
uploaded = st.file_uploader(
|
"Increase the slot count if you need more.")
|
||||||
"Select HTM, HTML or CSV files",
|
|
||||||
type=["htm", "html", "csv"],
|
sc1, sc2 = st.columns([1, 5])
|
||||||
accept_multiple_files=True, key="pb_uploader",
|
with sc1:
|
||||||
)
|
n_slots = st.selectbox(
|
||||||
if uploaded:
|
"Slots",
|
||||||
# Streamlit already filters by type, but keep a defensive check
|
list(range(1, 21)),
|
||||||
# in case type filter is ever loosened
|
index=max(0, st.session_state.pb_n_slots - 1),
|
||||||
rejected = [f.name for f in uploaded
|
key="pb_n_slots_select",
|
||||||
if not f.name.lower().endswith((".htm",".html",".csv"))]
|
)
|
||||||
if rejected:
|
if n_slots != st.session_state.pb_n_slots:
|
||||||
st.warning(f"Ignored {len(rejected)} unsupported file(s): "
|
st.session_state.pb_n_slots = n_slots
|
||||||
+ ", ".join(rejected[:5])
|
st.rerun()
|
||||||
+ (" …" if len(rejected) > 5 else ""))
|
|
||||||
uploaded = [f for f in uploaded
|
slots_per_row = 5
|
||||||
if f.name.lower().endswith((".htm",".html",".csv"))]
|
for row_start in range(0, st.session_state.pb_n_slots, slots_per_row):
|
||||||
for f in uploaded:
|
cols = st.columns(slots_per_row)
|
||||||
stem = os.path.splitext(f.name)[0]
|
for j in range(slots_per_row):
|
||||||
if stem not in st.session_state.pb_uploaded_files:
|
idx = row_start + j
|
||||||
df = _parse_uploaded(f)
|
if idx >= st.session_state.pb_n_slots:
|
||||||
if df is not None:
|
break
|
||||||
df = _ensure_columns(df, stem)
|
with cols[j]:
|
||||||
st.session_state.pb_uploaded_files[stem] = df
|
f = st.file_uploader(
|
||||||
st.success(f"✅ **{stem}** — {len(df):,} trades")
|
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")
|
||||||
|
|
||||||
if st.session_state.pb_uploaded_files:
|
if st.session_state.pb_uploaded_files:
|
||||||
st.markdown("**Loaded strategies:**")
|
st.markdown("**Loaded strategies:**")
|
||||||
|
|||||||
+38
-26
@@ -478,6 +478,7 @@ def _init_state():
|
|||||||
"pm_thread_results": None,
|
"pm_thread_results": None,
|
||||||
"pm_progress_q": None,
|
"pm_progress_q": None,
|
||||||
"pm_uploader_key": 0,
|
"pm_uploader_key": 0,
|
||||||
|
"pm_n_slots": 5,
|
||||||
}.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
|
||||||
@@ -544,32 +545,43 @@ def render():
|
|||||||
# ── Upload ───────────────────────────────────────────────────────────────
|
# ── Upload ───────────────────────────────────────────────────────────────
|
||||||
with st.expander("📂 Upload Backtest Files",
|
with st.expander("📂 Upload Backtest Files",
|
||||||
expanded=not bool(st.session_state.pm_files)):
|
expanded=not bool(st.session_state.pm_files)):
|
||||||
st.caption("Accepts `.htm` · `.html` · `.csv` — selecting a folder will only "
|
st.caption("Accepts `.htm` · `.html` · `.csv` — one file per slot. "
|
||||||
"import the supported files inside it (others are skipped).")
|
"Increase the slot count if you need more.")
|
||||||
uploaded = st.file_uploader(
|
|
||||||
"Select files",
|
sc1, _ = st.columns([1, 5])
|
||||||
type=["htm", "html", "csv"],
|
with sc1:
|
||||||
accept_multiple_files=True,
|
n_slots = st.selectbox(
|
||||||
key=f"pm_uploader_{st.session_state.pm_uploader_key}",
|
"Slots",
|
||||||
)
|
list(range(1, 21)),
|
||||||
if uploaded:
|
index=max(0, st.session_state.pm_n_slots - 1),
|
||||||
# Some browsers ignore the type filter when a folder is selected —
|
key="pm_n_slots_select",
|
||||||
# filter again here and report what was skipped.
|
)
|
||||||
valid = [f for f in uploaded
|
if n_slots != st.session_state.pm_n_slots:
|
||||||
if f.name.lower().endswith((".htm",".html",".csv"))]
|
st.session_state.pm_n_slots = n_slots
|
||||||
rejected = len(uploaded) - len(valid)
|
st.rerun()
|
||||||
if rejected:
|
|
||||||
st.warning(f"⚠️ Skipped {rejected} non-supported file(s). "
|
slots_per_row = 5
|
||||||
f"Importing {len(valid)} valid file(s).")
|
for row_start in range(0, st.session_state.pm_n_slots, slots_per_row):
|
||||||
uploaded = valid
|
cols = st.columns(slots_per_row)
|
||||||
for f in uploaded:
|
for j in range(slots_per_row):
|
||||||
stem = os.path.splitext(f.name)[0]
|
idx = row_start + j
|
||||||
if stem not in st.session_state.pm_files:
|
if idx >= st.session_state.pm_n_slots:
|
||||||
df = _parse_file(f)
|
break
|
||||||
if df is not None:
|
with cols[j]:
|
||||||
df = _normalise(df.copy(), stem)
|
f = st.file_uploader(
|
||||||
st.session_state.pm_files[stem] = df
|
f"File {idx + 1}",
|
||||||
st.success(f"✅ **{stem}** — {len(df):,} trades")
|
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")
|
||||||
|
|
||||||
if st.session_state.pm_files:
|
if st.session_state.pm_files:
|
||||||
# Clear all button
|
# Clear all button
|
||||||
|
|||||||
Reference in New Issue
Block a user