diff --git a/__pycache__/mt5_parser.cpython-314.pyc b/__pycache__/mt5_parser.cpython-314.pyc index e488a62..0a703ee 100644 Binary files a/__pycache__/mt5_parser.cpython-314.pyc and b/__pycache__/mt5_parser.cpython-314.pyc differ diff --git a/__pycache__/view_settings.cpython-314.pyc b/__pycache__/view_settings.cpython-314.pyc index 89d62f5..f8ad517 100644 Binary files a/__pycache__/view_settings.cpython-314.pyc and b/__pycache__/view_settings.cpython-314.pyc differ 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"
π 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}