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"{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"""