From ab9e968def7eb6c3c72b4855ae2f15f64ad3e2e8 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Apr 2026 09:42:08 +1000 Subject: [PATCH] Portfolio builder/master improvements, batch backtest fixes, parser FIFO fix, theme updates --- MT5Tools_Handoff_v2.docx | Bin 0 -> 13262 bytes view_portfolio_builder.py | 71 +++++++++++++++++++----- view_portfolio_master.py | 113 +++++++++++++++++++++++++++++++++----- 3 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 MT5Tools_Handoff_v2.docx diff --git a/MT5Tools_Handoff_v2.docx b/MT5Tools_Handoff_v2.docx new file mode 100644 index 0000000000000000000000000000000000000000..ac6d57a889dcd2184d78c452a084e5ecd38c6406 GIT binary patch literal 13262 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
%{{x|%d %b %Y}}: $%{{y:,.2f}}", )) - sf.update_layout( - hovermode="x unified", - hoverlabel=dict(namelength=-1, font=dict(size=12)), - ) + + # Stagnation band per strategy in matching colour + if show_st_stag: + eq_ts = sdf_s[["close_time","net_profit"]].dropna().copy() + eq_ts["cum"] = deposit + eq_ts["net_profit"].cumsum() + if not eq_ts.empty: + peak = float(eq_ts["cum"].iloc[0]) + stag_start = eq_ts["close_time"].iloc[0] + max_days = 0 + best_s = stag_start + best_e = stag_start + for _, r in eq_ts.iterrows(): + if float(r["cum"]) > peak: + days = (r["close_time"] - stag_start).days + if days > max_days: + max_days = days + best_s = stag_start + best_e = r["close_time"] + peak = float(r["cum"]) + stag_start = r["close_time"] + if max_days > 0: + # Convert hex to rgba with low opacity + hex_c = color.lstrip("#") + if len(hex_c) == 6: + r_c = int(hex_c[0:2], 16) + g_c = int(hex_c[2:4], 16) + b_c = int(hex_c[4:6], 16) + fill_color = f"rgba({r_c},{g_c},{b_c},0.12)" + ann_color = color + else: + fill_color = "rgba(255,160,80,0.12)" + ann_color = color + sf.add_vrect( + x0=best_s, x1=best_e, + fillcolor=fill_color, line_width=1, + line_color=f"rgba({r_c},{g_c},{b_c},0.3)" if len(hex_c)==6 else color, + annotation_text=f"{lbl.split()[0]}… {max_days}d", + annotation_position="top left", + annotation_font_size=9, + annotation_font_color=ann_color, + ) + st.plotly_chart(sf, use_container_width=True) # ═════════════════════════════════════════════════════════════════════════ diff --git a/view_portfolio_master.py b/view_portfolio_master.py index a35fa9a..568a2bc 100644 --- a/view_portfolio_master.py +++ b/view_portfolio_master.py @@ -476,6 +476,7 @@ def _init_state(): "pm_cancel": False, "pm_thread_results": None, "pm_progress_q": None, + "pm_uploader_key": 0, }.items(): if k not in st.session_state: st.session_state[k] = v @@ -498,16 +499,28 @@ def render(): padding:10px 14px;font-size:13px;color:#FFB347;margin:8px 0} """, unsafe_allow_html=True) - st.markdown('

🏆 Portfolio Master

', unsafe_allow_html=True) - st.markdown('

Automated portfolio construction — composite scoring, greedy & Monte Carlo search

', - unsafe_allow_html=True) + _tc1, _tc2 = st.columns([8, 1]) + with _tc1: + st.markdown('

🏆 Portfolio Master

', unsafe_allow_html=True) + st.markdown('

Automated portfolio construction — composite scoring, greedy & Monte Carlo search

', + unsafe_allow_html=True) + with _tc2: + st.markdown("
", unsafe_allow_html=True) + if st.button("🗑 Clear", key="pm_clear_session", help="Clear all files and results to start fresh"): + st.session_state.pm_uploader_key = st.session_state.get("pm_uploader_key", 0) + 1 + for _k in ["pm_files","pm_custom_names","pm_results","pm_running", + "pm_cancel","pm_thread_results","pm_progress_q","pm_cancel_event"]: + if _k in st.session_state: + del st.session_state[_k] + st.rerun() # ── Upload ─────────────────────────────────────────────────────────────── with st.expander("📂 Upload Backtest Files", expanded=not bool(st.session_state.pm_files)): st.caption("Accepts `.htm` · `.html` · `.csv`") uploaded = st.file_uploader( - "Select files", type=None, accept_multiple_files=True, key="pm_uploader", + "Select files", type=None, accept_multiple_files=True, + key=f"pm_uploader_{st.session_state.pm_uploader_key}", ) if uploaded: uploaded = [f for f in uploaded @@ -568,7 +581,8 @@ def render(): st.markdown('
Composite Score Weights
', unsafe_allow_html=True) st.caption("Weights are normalised automatically — they don't need to sum to 1.") - wc1, wc2, wc3 = st.columns(3) + wc1, wc2, wc3, wc4 = st.columns([2, 2, 2, 3]) + w_retdd = wc1.slider("Ret/DD", 0, 100, 35, key="pm_w_retdd") w_stab = wc1.slider("Stability (R²)", 0, 100, 25, key="pm_w_stab") w_stag = wc2.slider("Stagnation % ↓", 0, 100, 20, key="pm_w_stag", @@ -579,6 +593,28 @@ def render(): w_div = wc3.slider("Diversity Bonus", 0, 100, 5, key="pm_w_div", help="Rewards portfolios trading different symbols / sessions") + with wc4: + import os as _osw, re as _rew + _cfgw = _osw.path.join(_osw.path.dirname(_osw.path.abspath(__file__)), ".streamlit", "config.toml") + _lightw = False + if _osw.path.isfile(_cfgw): + _mw = _rew.search(r'base\s*=\s*"([^"]*)"', open(_cfgw).read()) + if _mw: _lightw = _mw.group(1) == "light" + _wbg = "#f0f2f6" if _lightw else "#131720" + _wbdr = "#d0d4dc" if _lightw else "#1E2535" + _wtxt = "#555e70" if _lightw else "#8899AA" + _wlbl = "#1a1a2e" if _lightw else "#CDD6F4" + st.markdown(f""" +
+Ret/DD — Net profit ÷ max drawdown. Primary return efficiency metric. Most important for risk-adjusted performance.
+Stability (R²) — How straight the equity curve is. High R² means consistent gains without large swings.
+Stagnation ↓ — Time spent below a previous equity high, as % of total period. Lower = better; score is inverted.
+Win Rate — Percentage of trades that are profitable. Higher win rate reduces psychological drawdown pressure.
+Growth Quality — Combines equity curve slope with R². Rewards portfolios that rise steadily, not just flat and stable.
+Diversity Bonus — Rewards combinations trading different symbols and/or different hours of the day. +
""", unsafe_allow_html=True) + total_w = w_retdd + w_stab + w_stag + w_wr + w_gq + w_div or 1 weights = { "ret_dd": w_retdd / total_w, @@ -901,14 +937,37 @@ def render(): # ── Summary table ───────────────────────────────────────────────── st.markdown(f"##### Top {len(results)} Portfolios") - st.markdown(""" -
-Score — Composite ranking (0–1000). Higher is better. Weighted blend of the metrics below based on your sliders.
-Stability — How straight the equity curve is (0–100). 100 = perfectly straight rising line. Computed as R² of linear regression on the equity curve.
-Growth Quality — Combines curve straightness with upward slope. Rewards portfolios that rise consistently, not just ones that are flat and stable.
-Diversity — How different the strategies are from each other (0–100), based on symbol variety and trading session overlap. 100 = completely different symbols and hours.
-Avg Corr — Average pairwise correlation of daily P&L across all strategy pairs. Lower is better — strategies that don't move together reduce portfolio drawdown.
-Avg Cond Corr — Same correlation computed only on days when the portfolio is in drawdown. Strategies that decorrelate during losses are more valuable than those that only decorrelate on good days. + import os as _os2, re as _re3 + _cfg2 = _os2.path.join(_os2.path.dirname(_os2.path.abspath(__file__)), ".streamlit", "config.toml") + _light2 = False + if _os2.path.isfile(_cfg2): + _m2 = _re3.search(r'base\s*=\s*"([^"]*)"', open(_cfg2).read()) + if _m2: _light2 = _m2.group(1) == "light" + _desc_bg = "#f0f2f6" if _light2 else "#131720" + _desc_border = "#d0d4dc" if _light2 else "#1E2535" + _desc_text = "#555e70" if _light2 else "#8899AA" + _desc_label = "#1a1a2e" if _light2 else "#CDD6F4" + _desc_thresh = lambda good, warn: ( + f'{good}  |  ' + + f'{warn}  |  ' + + f'below = poor' + ) + st.markdown(f""" +
+Score — Composite ranking (0–1000). Higher is better. Weighted blend of the metrics below based on your sliders. +  ≥700 = strong  |  400–700 = average  |  <400 = weak
+Ret/DD — Net profit divided by max drawdown. Measures return efficiency per unit of risk. +  ≥5 = strong  |  2–5 = average  |  <2 = weak
+Stability — How straight the equity curve is (0–100). 100 = perfectly straight rising line. R² of linear regression on the equity curve. +  ≥70 = strong  |  40–70 = average  |  <40 = weak
+Growth Quality — Combines curve straightness with upward slope. Rewards portfolios that rise consistently, not just ones that are flat and stable. +  ≥50 = strong  |  20–50 = average  |  <20 = weak
+Diversity — How different the strategies are from each other (0–100), based on symbol variety and trading session overlap. 100 = completely different. +  ≥60 = strong  |  30–60 = average  |  <30 = low diversity
+Avg Corr — Average pairwise correlation of daily P&L. Lower is better — strategies that don't move together reduce portfolio drawdown. +  ≤0.20 = low (good)  |  0.20–0.50 = moderate  |  >0.50 = high (bad)
+Avg Cond Corr — Same correlation computed only on drawdown days. Strategies that decorrelate during losses are more valuable. +  ≤0.20 = low (good)  |  0.20–0.50 = moderate  |  >0.50 = high (bad)
""", unsafe_allow_html=True) @@ -958,6 +1017,20 @@ def render(): neg_cols = [c for c in ["Max DD ($)","Max DD (%)","Avg Loss ($)","Avg Corr","Avg Cond Corr"] if c in res_df.columns] + def _grade(val, good, avg): + """Return green/orange/red based on good/avg thresholds (higher=better).""" + if not isinstance(val, (int, float)): return "" + if val >= good: return "background-color:rgba(52,194,122,0.15);color:#34C27A" + if val >= avg: return "background-color:rgba(247,127,0,0.12);color:#f77f00" + return "background-color:rgba(220,50,50,0.12);color:#E05555" + + def _grade_inv(val, good, avg): + """Return green/orange/red — lower is better (correlation).""" + if not isinstance(val, (int, float)): return "" + if val <= good: return "background-color:rgba(52,194,122,0.15);color:#34C27A" + if val <= avg: return "background-color:rgba(247,127,0,0.12);color:#f77f00" + return "background-color:rgba(220,50,50,0.12);color:#E05555" + styled = ( res_df.style.format(fmt) .map(_cc, subset=pos_cols if pos_cols else []) @@ -965,6 +1038,20 @@ def render(): subset=neg_cols if neg_cols else []) .map(lambda v: _cc(v, 1.0), subset=["Profit Factor"] if "Profit Factor" in res_df.columns else []) + .map(lambda v: _grade(v, 700, 400), + subset=["Score"] if "Score" in res_df.columns else []) + .map(lambda v: _grade(v, 5, 2), + subset=["Ret/DD"] if "Ret/DD" in res_df.columns else []) + .map(lambda v: _grade(v, 70, 40), + subset=["Stability"] if "Stability" in res_df.columns else []) + .map(lambda v: _grade(v, 50, 20), + subset=["Growth Quality"] if "Growth Quality" in res_df.columns else []) + .map(lambda v: _grade(v, 60, 30), + subset=["Diversity"] if "Diversity" in res_df.columns else []) + .map(lambda v: _grade_inv(v, 0.20, 0.50), + subset=["Avg Corr"] if "Avg Corr" in res_df.columns else []) + .map(lambda v: _grade_inv(v, 0.20, 0.50), + subset=["Avg Cond Corr"] if "Avg Cond Corr" in res_df.columns else []) ) st.dataframe(styled, use_container_width=True, hide_index=True)