From 7e2015cb11f80e40f6649b7318e91e72f8f434ee Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Apr 2026 08:40:42 +1000 Subject: [PATCH] Fixed logic of portfolio builder and parsing of htm reports - incorrectly managed in/out trades skipping trades when multiple open entries on the same strategy. - fixed card color backgrounds in light theme - fixed strategy filters in portfolio builder --- __pycache__/mt5_parser.cpython-314.pyc | Bin 19657 -> 19721 bytes __pycache__/view_settings.cpython-314.pyc | Bin 11767 -> 15006 bytes mt5_parser.py | 26 +++--- view_portfolio_builder.py | 99 ++++++++++++++-------- view_settings.py | 54 +++++++++++- 5 files changed, 130 insertions(+), 49 deletions(-) diff --git a/__pycache__/mt5_parser.cpython-314.pyc b/__pycache__/mt5_parser.cpython-314.pyc index e488a62d996b2032c43b7e7b08a38f1d5c98cb15..0a703eed93ad1ec13c878e7afe1252698a976a61 100644 GIT binary patch delta 766 zcmX>(ld*FaBcC=OFBbz4D5X5id}g(gZ<-YA5_SfLhRFvl#5W(1l4N15nS4u5XYv&d znaOPOlB|M3QYK83&uJ)06bOY0s)MuwK{_)7gA9XrAOk~~(Bvq2Wk$itb@JN6)gXS5 zq(uZ=Ma$$p@{+28dJHB^5S<0WVZvypECA{f4p5zZfm24NKqQPWKp3JFnGBK+6PawJ zpw1{V`GCC0WI1^ru^^!^ksu)*DTJ~hS)if<1yiV^T?)>TIJu#`?*V6kWwdgOtKV zgOt^Pnqo9zI>SUazf=6r$S5}XopPldOG;)@-YwSRlEmcfTdb*hB}J7*K0u|#Vn9M| z@s{p6yUcIU!SbDrK|uH>kI3YMYPme7%PkjLt_a%@bXnPAvc7shn=FHf z`ggm@N7OYH{FxX9Ll}QP5>PJ|0@`>;*}|WL?XV)JKQG&1eKvpI&Ab|%oZ|C=Qku+F zths5a@hVkpU@E?7!DL-SdA)@oIi@0zmy0yO>WV@fnlrh| zFpF`{i7Q*rZIa}{p&;~>!!AmSv5Fa#2} zmm|y_`vfSW$y)RZqZJ=X#h)J0(HL3S2_OepdK5f4B_7>E!B5uikUi^C>2KczG$)vkzXa*~fD PTR0=5_@|1=D}59I8>GHU delta 744 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 delta 2013 zcmZ`)TWk|o5Z&wb`fZcMiHVc=oj86ZKwEZbnr3&Q(sS+)G(3e`3N>xc0gvuw-2UV#+tyDU9;{ZP$>z+GzJTrIp z+`F@qYvhPV_NxLKqd^os zr{XMKM(?;mXy){SM|wrRw>6y22!gDiq- zM8GXyQ5M1tM`2k2mzwEFzC10f8lNwhl@wZV7v`*ynrgs#f4x4NR8pnFv|6i7D(m5x zb0_h_cTTTimV-OaTAN7ra)T#1u9)Pw4sH*N)Md;BjiHvTAQA^hO*)!&wCHHn(U!7P zJ9T90Q2~xRBl?KDj%VSwh7>Gr9GOYBUMJAf>M3rG8`B@~jf)J_OnYG@;_d61kD)z- zIXp?Mu!GynZ6SLJT|^gWO!a8TTy_0CyXQTaTK5cB^)^~E;QQW24SZ%7h#l|ro+zPwB17Z{2m8GH^ zF{A5)DKl0%1Uoy^WLbT$n7O&yA~ugTvKS#t9dtS51McKnR&_hKhrKYmP(J6NZqZEJ zL<;?!Mm@_n zevfk#p4!AVg3q^^j>3sp2@)|6V--Z4m&H9~8*Gm!g)zDv^S>HT!b7xQincV-9nAF9 zOVbL!T7t%P#?djjoao~D(MR>U0M8}bi3g4)LXnl@9tW2)JgL!#RaQN#JnIajZ_jCP zF_9)Y_#-h&Vz4rqMLeFIvBi*1&3I5QSK-Z`s7b=55`&99)23(V)WP0d!!BKApc|iu zv%S9yqqx@K)S}lhQ(*BU=Vn}Eo0;>SNiO+{kO}y!FS&gK3*5+H6Law^TXDi>T&k62 z&8klCJBY1YR+I#DSw-mMZ#=nrYv3tyY;*hSE@V{jrB-Q1<8!r+Rc zS>zY1`6$U(Rn45oNx=D`$yap-NKjrYX3x5_%<+N z$SvTmW>&j)5#gHKKGA8t)@`2{v|h{Fa}Mk50()*HKilTb4e_%>0_r~z`>rNyyh$@l zQn6ByBqX_ppU25^sit#3M@kqs)S1zYhbkp91&4=x8~T`0Cysqb=8i-sT|XB_mCnIA pPVq9GF{N3SPgaW4WqGA?2A8ZgsaN3Y&}S|e>AfF*iI4%^`9Cv+;mH61 diff --git a/mt5_parser.py b/mt5_parser.py index 32956fc..af9151c 100644 --- a/mt5_parser.py +++ b/mt5_parser.py @@ -168,27 +168,31 @@ def parse_backtest_report(file_bytes): df_deals = pd.DataFrame(deals) df_deals = df_deals[df_deals['direction'].isin(['in', 'out'])] - # Match in/out pairs — pair consecutive in→out by symbol+type - open_stack = {} # key: (symbol, type_) -> list of open deals - trades = [] + # FIFO stack matching by symbol — handles concurrent positions on same symbol. + # Skip daily commission/balance rows (no symbol). + # Each 'in' is pushed to the stack; each 'out' pops the oldest open entry (FIFO). + open_stack = {} # symbol -> list of open 'in' deals (FIFO) + trades = [] for _, deal in df_deals.iterrows(): - sym = deal.get('symbol', '') - typ = deal.get('type', '') - dirn = deal.get('direction', '') - key = (sym, typ) + sym = deal.get('symbol', '').strip() + dirn = deal.get('direction', '').strip() + + # Skip commission/balance rows + if not sym: + continue if dirn == 'in': - open_stack.setdefault(key, []).append(deal) + open_stack.setdefault(sym, []).append(deal) elif dirn == 'out': - stack = open_stack.get(key, []) + stack = open_stack.get(sym, []) if stack: - entry = stack.pop(0) + entry = stack.pop(0) # FIFO — oldest open first trades.append({ 'open_time' : entry['time'], 'close_time' : deal['time'], 'symbol' : sym, - 'type' : typ, + 'type' : entry.get('type', ''), 'volume' : entry['volume'], 'open_price' : entry['price'], 'close_price': deal['price'], diff --git a/view_portfolio_builder.py b/view_portfolio_builder.py index 0a2acda..78e890f 100644 --- a/view_portfolio_builder.py +++ b/view_portfolio_builder.py @@ -344,8 +344,15 @@ def _build_equity_chart( elif chart_view == "Portfolio+Individual": # Combined line + each member underneath if not df.empty and "close_time" in df.columns and "net_profit" in df.columns: + import os as _os, re as _re2 + _cfg = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), ".streamlit", "config.toml") + _light = False + if _os.path.isfile(_cfg): + _m = _re2.search(r'base\s*=\s*"([^"]*)"', open(_cfg).read()) + if _m: _light = _m.group(1) == "light" + _portfolio_color = "#1a3a5c" if _light else "#FFFFFF" _plot_series(df["close_time"], df["net_profit"], f"{active_label} (combined)", - "#FFFFFF", 2.5) + _portfolio_color, 2.5) if show_stagnation: _add_stagnation_vrect(fig, df, deposit) for i, label in enumerate(selected_strategies): @@ -406,21 +413,22 @@ def _build_equity_chart( fig.update_layout( height=1240, margin=dict(l=60,r=20,t=24,b=10), - paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="#0E1117", + paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0, font=dict(size=11)), hovermode="x unified", bargap=0, barmode="overlay", + hoverlabel=dict(namelength=-1, font=dict(size=11)), ) # Force x-axis range to match the selected date window x_min = pd.Timestamp(date_from) if date_from else None x_max = pd.Timestamp(date_to) + pd.Timedelta(days=1) if date_to else None fig.update_xaxes( - gridcolor="#1E2130", zeroline=False, + gridcolor="rgba(128,128,128,0.15)", zeroline=False, showspikes=True, spikecolor="#445", spikethickness=1, range=[x_min, x_max] if x_min and x_max else None, ) - fig.update_yaxes(gridcolor="#1E2130", zeroline=False) + fig.update_yaxes(gridcolor="rgba(128,128,128,0.15)", zeroline=False) fig.update_yaxes(title_text="Equity ($)", row=1, col=1, tickprefix="$") fig.update_yaxes(title_text="Peak DD ($)", row=2, col=1, tickprefix="$") fig.update_yaxes(title_text="Daily P&L ($)", row=3, col=1, tickprefix="$") @@ -457,11 +465,7 @@ def _monthly_html(pivot: pd.DataFrame, mode: str = "$") -> str: return ( "" + ".mt .p{color:#34C27A}.mt .n{color:#E05555}" f"{hdr}{''.join(rows)}
" ) @@ -516,25 +520,12 @@ def _init_state(): def render(): _init_state() + # Card/stat styling injected globally by inject_theme_css() in app.py + # Also expand multiselect tags so full strategy names are visible st.markdown("""""", unsafe_allow_html=True) st.markdown('

πŸ“Š Portfolio Builder

', unsafe_allow_html=True) @@ -637,12 +628,20 @@ def render(): def _date_slider(key_prefix): if not _has_dates or _gmin == _gmax or len(_date_options) < 2: return _gmin, _gmax + skey = f"{key_prefix}_dslider" + # Use existing session state value if valid, otherwise default to full range + existing = st.session_state.get(skey) + if (existing and isinstance(existing, (list, tuple)) and len(existing) == 2 + and existing[0] in _date_options and existing[1] in _date_options): + default_val = (existing[0], existing[1]) + else: + default_val = (_gmin, _gmax) sel = st.select_slider( "Date range", options=_date_options, - value=(_gmin, _gmax), + value=default_val, format_func=lambda d: d.strftime("%d %b %Y"), - key=f"{key_prefix}_dslider", + key=skey, ) return sel[0], sel[1] @@ -832,6 +831,7 @@ def render(): # Compact stats bar above trade list tr_stats = _calc_stats(view, deposit) if tr_stats: + st.caption("Stats reflect the filtered trade list below β€” does not affect Overview, Equity Chart or Strategies tabs.") ts1,ts2,ts3,ts4,ts5,ts6 = st.columns(6) _np = tr_stats.get("net_profit",0) ts1.metric("Trades", f"{tr_stats.get('num_trades',0):,}") @@ -847,8 +847,18 @@ def render(): def _hl(val): if not isinstance(val, (int,float)): return "" - if val > 0: return "background-color:#1A3A26" - if val < 0: return "background-color:#3A1A1A" + import os, re as _re + _cfg = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".streamlit", "config.toml") + _light = False + if os.path.isfile(_cfg): + _m = _re.search(r'base\s*=\s*"([^"]*)"', open(_cfg).read()) + if _m: _light = _m.group(1) == "light" + if _light: + if val > 0: return "background-color:rgba(52,194,122,0.15)" + if val < 0: return "background-color:rgba(220,50,50,0.12)" + else: + if val > 0: return "background-color:#1A3A26" + if val < 0: return "background-color:#3A1A1A" return "" # No fixed height β€” Streamlit will size to content on the page @@ -932,7 +942,18 @@ def render(): # ═════════════════════════════════════════════════════════════════════════ with tab_st: st.markdown("##### All Strategies β€” Performance Summary") - tdf = _strategy_table(eff_dfs, deposit, lot_overrides) + # Apply same date range as Overview + eff_dfs_filtered = {} + for _lbl, _sdf in eff_dfs.items(): + if "close_time" in _sdf.columns and ov_date_from and ov_date_to: + _ct = pd.to_datetime(_sdf["close_time"]).dt.tz_localize(None) + eff_dfs_filtered[_lbl] = _sdf[ + (_ct >= pd.Timestamp(ov_date_from)) & + (_ct <= pd.Timestamp(ov_date_to) + pd.Timedelta(days=1)) + ] + else: + eff_dfs_filtered[_lbl] = _sdf + tdf = _strategy_table(eff_dfs_filtered, deposit, lot_overrides) if tdf.empty: st.info("No strategies loaded.") else: @@ -962,15 +983,16 @@ def render(): help="Rolling-average window (trades).") sf = go.Figure() sf.update_layout( - height=500, # [4] taller + height=500, margin=dict(l=40, r=20, t=10, b=10), - paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="#0E1117", + paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", legend=dict(orientation="h", y=1.08, font=dict(size=10)), - hovermode="x unified", + hovermode="closest", + hoverlabel=dict(namelength=-1, font=dict(size=11)), ) sf.update_xaxes(gridcolor="#1E2130", zeroline=False) sf.update_yaxes(gridcolor="#1E2130", zeroline=False, tickprefix="$") - for i, (lbl, sdf) in enumerate(eff_dfs.items()): + for i, (lbl, sdf) in enumerate(eff_dfs_filtered.items()): if "close_time" not in sdf.columns or "net_profit" not in sdf.columns: continue sdf_s = sdf.sort_values("close_time") @@ -980,7 +1002,12 @@ def render(): x=sdf_s["close_time"].values, y=eq_s, name=lbl, mode="lines", line=dict(color=COLORS[i % len(COLORS)], width=1.5), + hovertemplate=f"{lbl}
%{{x|%d %b %Y}}: $%{{y:,.2f}}", )) + sf.update_layout( + hovermode="x unified", + hoverlabel=dict(namelength=-1, font=dict(size=12)), + ) st.plotly_chart(sf, use_container_width=True) # ═════════════════════════════════════════════════════════════════════════ diff --git a/view_settings.py b/view_settings.py index ead25e9..b5342cf 100644 --- a/view_settings.py +++ b/view_settings.py @@ -38,7 +38,7 @@ def _read_config() -> dict: text = open(CONFIG_FILE).read() found = {} for key in ["base","primaryColor","backgroundColor", - "secondaryBackgroundColor","textColor","font"]: + "secondaryBackgroundColor","textColor","font","cardBackgroundColor"]: m = re.search(key + r'\s*=\s*"([^"]*)"', text) if m: found[key] = m.group(1) @@ -65,9 +65,55 @@ def _is_custom(cfg: dict) -> bool: def inject_theme_css(): - """Call from app.py on every page to apply font size globally.""" + """Call from app.py on every page to apply font size and card theme globally.""" _size_map = {"Normal": 0, "Large (+2px)": 2, "Extra Large (+4px)": 4} _delta = _size_map.get(st.session_state.get("st_font_size", "Normal"), 0) + + # Read card background from config + cfg = _read_config() + base = cfg.get("base", "dark") + card_bg = cfg.get("cardBackgroundColor", "") + text_col = cfg.get("textColor", "#fafafa") + + # Default card colours per base theme + if not card_bg: + card_bg = "#f0f2f6" if base == "light" else "#131720" + card_border = "rgba(0,0,0,0.10)" if base == "light" else "#1E2535" + label_col = "#555e70" if base == "light" else "#6C7A9A" + kk_col = "#4a5568" if base == "light" else "#7A8898" + row_border = "rgba(0,0,0,0.07)" if base == "light" else "#141820" + sh_border = "rgba(0,0,0,0.10)" if base == "light" else "#1E2535" + th_bg = card_bg + td_col = text_col + + css = f"""""" + st.markdown(css, unsafe_allow_html=True) + if _delta > 0: st.markdown(f"""