From a784d26875c5f0ba160d6aba32781e0876aecf04 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Apr 2026 08:23:56 +1000 Subject: [PATCH] feat: trade analysis equity overlays, theme settings, chart fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trade Analysis: - Add toggleable equity curve overlays: Account total, By Strategy, By Symbol, By Day of Week — checkboxes above each chart - Fix all chart backgrounds to transparent (inherit page theme) - Fix grid and font colours to work on both dark and light backgrounds Settings page: - Full theme control: Dark / Light / Custom radio with Apply button - Custom mode: colour pickers for accent, page bg, sidebar bg, text, font selector and base dark/light toggle - Preview swatches before applying - Discard / Reset to Dark button - Text size: Normal / Large (+2px) / Extra Large (+4px) via CSS injection - inject_theme_css() helper for app.py to apply font size on all pages - Theme written to .streamlit/config.toml on apply --- .streamlit/config.toml | 7 + __pycache__/view_settings.cpython-314.pyc | Bin 1640 -> 11767 bytes .../view_trade_analysis.cpython-314.pyc | Bin 25633 -> 28639 bytes app.py | 2 + view_settings.py | 217 ++++++++++++++++-- view_trade_analysis.py | 101 ++++++-- 6 files changed, 280 insertions(+), 47 deletions(-) create mode 100644 .streamlit/config.toml diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..e9e9e5e --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,7 @@ +[theme] +base = "light" +primaryColor = "#2E75B6" +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f0f2f6" +textColor = "#1a1a1a" +font = "sans serif" diff --git a/__pycache__/view_settings.cpython-314.pyc b/__pycache__/view_settings.cpython-314.pyc index 8037d13e3cad3574b137e3cee00e2c22e47c23a4..89d62f5dfa0fbe265ed05483edf98e35618c4b7c 100644 GIT binary patch literal 11767 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 literal 1640 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 delta 1905 zcmaKse@s(X6vy9dUw;Fo?|v{hH&|u@nQpr72XWcvmMxn75(0$oMq?JYWhP7JHvSoZ?VeVQZpmKzK0W7q z&;51Iy}faR9lye+m8GSc8TzbHugk@r3uzbKnqOIbo3Fv?8%$bFn+B)1t}(Z1cufh7 z>d@nHt^l8CwP>|%THIWdsi9J(Wo0{!U|jhyQu2Ww_xilH!@AtYb6yRvb8C`5ujdWC zkvF*wNq;??{9VPgX`5}hWn0D&%bR%%Z@FiL6h4(tTOQ#a-O8uC@ASV5V8t zSUw;TogxRN(14-X?XMQP-FWq*6+<;nqJM0EL=H!t2Ez6eHcGf&Rg?$^!G!CU6@{39 zepOKs;etZ1B)jNlmeNt6ODJ7i>4I;SYprvMD;5M{AnkD}T!#cX7;<>jpz741Fswx1 zE%eGtbSXR0ZCzEBi@uuxuFCSt_0CC_m;y)^6Z0Ss?J}uBM-qDo%l>_RNa~jRCpp3@ zghhl66Sm7~#81^VXj>;){9|3#P?OU_EFl$zeFp?3B854h_+ic20~D;k`jEmeoM^)H4iayYxaV*XvSJJRy%90T(DNnSgU5O?qM!o z_`sNJzOZ7UuzIGjdbY4;m>cexG3H;x&Fz`n_nzDvPqUw>olkR&)lU@6KUf{NRstde99k!IvctI zi7HQ;(FnvHqoAUI;N+p>AUy{-0l7Mi;_xX!55y89wz!oP~IPr&=rW(C4$f>wb3hpQ=n?=1cXk5`x*dhk6y=( z&7~D@K(k3y5BgL@S4dcqXsJhx2k9yIfn`?IP)JJHXdItzZqjS%9SP0H{%Ede^U9_- zcoMakvlXAMKfi9uK7}svbFPM%hu3gJZ^KM*T~R9FEZ`l$yMXh6_W&`90ef>vqoXV8 zXiy`_p>{j!g(!L-T0a2Xtqb}P+`Cm7FelL#&N6lyKhn7?mo6BN5&8^p3Gg}K3;b$l z0ecfqbw10E;Ck;?b`T%;mg*C dict: + """Read .streamlit/config.toml, return dict of theme keys.""" + if not os.path.isfile(CONFIG_FILE): + return dict(THEMES["Dark"]) + try: + text = open(CONFIG_FILE).read() + found = {} + for key in ["base","primaryColor","backgroundColor", + "secondaryBackgroundColor","textColor","font"]: + m = re.search(key + r'\s*=\s*"([^"]*)"', text) + if m: + found[key] = m.group(1) + return {**THEMES["Dark"], **found} + except Exception: + return dict(THEMES["Dark"]) + + +def _write_config(theme: dict): + os.makedirs(CONFIG_DIR, exist_ok=True) + lines = ["[theme]\n"] + for k, v in theme.items(): + lines.append(f'{k} = "{v}"\n') + with open(CONFIG_FILE, "w") as f: + f.writelines(lines) + + +def _is_custom(cfg: dict) -> bool: + """Return True if the saved config differs from both standard themes.""" + for t in THEMES.values(): + if all(cfg.get(k) == v for k, v in t.items()): + return False + return True + + +def inject_theme_css(): + """Call from app.py on every page to apply font size 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) + if _delta > 0: + st.markdown(f"""""", unsafe_allow_html=True) def render(): - st.title("⚙️ Settings") + st.title("Settings") - st.markdown(""" -
- MT5 Tools — settings and information. -
- """, unsafe_allow_html=True) + # ── Theme selector ──────────────────────────────────────────────────────── + st.subheader("Theme") + cfg = _read_config() + + # Determine current theme state + if _is_custom(cfg): + current_mode = "Custom" + elif cfg.get("base") == "light": + current_mode = "Light" + else: + current_mode = "Dark" + + # Always show all three options — Custom opens the colour pickers + theme_options = ["Dark", "Light", "Custom"] + selected = st.radio("Theme", theme_options, horizontal=True, key="st_theme_pick", + index=theme_options.index(current_mode) if current_mode in theme_options else 0) + + # ── Apply button for Dark / Light ──────────────────────────────────────── + if selected in ("Dark", "Light"): + if st.button(f"Apply {selected} Theme", type="primary", key="st_apply_base"): + _write_config(THEMES[selected]) + st.success(f"{selected} theme applied — reloading...") + st.rerun() + + # ── Custom colour section — only show when Custom is selected ───────────── + if selected == "Custom": + st.caption("Customise colours below then click **Apply Custom Theme**.") + st.markdown("---") + + c1, c2, c3 = st.columns(3) + with c1: + primary = st.color_picker("Accent colour", + value=cfg.get("primaryColor","#7c6af7"), + key="st_primary", help="Buttons, links, highlights") + with c2: + bg = st.color_picker("Page background", + value=cfg.get("backgroundColor","#0e1117"), + key="st_bg") + sidebar_bg = st.color_picker("Sidebar / widget background", + value=cfg.get("secondaryBackgroundColor","#1a1f2e"), + key="st_sidebar_bg") + with c3: + text_color = st.color_picker("Text colour", + value=cfg.get("textColor","#fafafa"), + key="st_text") + font = st.selectbox("Font", ["sans serif", "serif", "monospace"], + index=["sans serif","serif","monospace"].index( + cfg.get("font","sans serif")), + key="st_font") + base_for_custom = st.radio("Base", ["dark","light"], horizontal=True, + key="st_custom_base", + index=0 if cfg.get("base","dark")=="dark" else 1, + help="Underlying dark or light base for your custom colours") + + # Preview swatches + st.markdown("**Preview**") + sw1, sw2, sw3, sw4 = st.columns(4) + for col, color, label in [ + (sw1, bg, "Page BG"), + (sw2, sidebar_bg, "Sidebar BG"), + (sw3, primary, "Accent"), + (sw4, text_color, "Text"), + ]: + col.markdown( + f'
' + f'{label}
{color}
', + unsafe_allow_html=True) + + st.markdown("") + btn1, btn2 = st.columns([2, 1]) + if btn1.button("Apply Custom Theme", type="primary", key="st_apply"): + try: + _write_config({ + "base": base_for_custom, + "primaryColor": primary, + "backgroundColor": bg, + "secondaryBackgroundColor": sidebar_bg, + "textColor": text_color, + "font": font, + }) + st.success("Custom theme saved — reloading...") + st.rerun() + except Exception as e: + st.error(f"Could not write config: {e}") + + if btn2.button("Discard / Reset to Dark", key="st_reset"): + _write_config(THEMES["Dark"]) + st.success("Reset to Dark — reloading...") + st.rerun() + + elif selected in ("Dark", "Light"): + st.caption("Select **Custom** to adjust individual colours.") + + with st.expander("Config file"): + st.code(CONFIG_FILE) + if os.path.isfile(CONFIG_FILE): + st.code(open(CONFIG_FILE).read(), language="toml") + else: + st.caption("No config.toml yet — created on first Apply.") + + # ── Text size ───────────────────────────────────────────────────────────── + st.divider() + st.subheader("Text Size") + st.caption("Applies immediately across all pages — no reload needed.") + st.radio("Text size", ["Normal", "Large (+2px)", "Extra Large (+4px)"], + horizontal=True, key="st_font_size") + inject_theme_css() + + # ── About ───────────────────────────────────────────────────────────────── + st.divider() st.subheader("About") st.markdown(""" - **MT5 Tools** is a standalone trade analysis and comparison dashboard. +**MT5 Tools** — standalone trade analysis and portfolio construction dashboard for MetaTrader 5. - **Supported file formats:** - - MT5 Account History HTML export (`.htm` / `.html`) - - MT5 Strategy Tester Backtest Report (`.htm` / `.html`) - - Quant Analyzer CSV export (`listOfTrades_*.csv`) - - **Pages:** - - **Trade Analysis** — statistics, equity curves, day/hour breakdown for a single report - - **Trade Compare** — match and compare two reports to measure slippage and variance - """) +**Supported formats:** MT5 Account History HTM · MT5 Backtest HTM · Quant Analyzer CSV · IC Markets XLSX +**Pages:** Trade Analysis · Trade Compare · Portfolio Builder · Portfolio Master · EA Comparator · Batch Backtest *(Windows only)* +""") st.subheader("Requirements") - st.code("pip install streamlit streamlit-option-menu pandas plotly", language="bash") - + st.code("pip install -r requirements.txt", language="bash") st.subheader("Launch") - st.code("streamlit run app.py", language="bash") + st.code("streamlit run app.py", language="bash") \ No newline at end of file diff --git a/view_trade_analysis.py b/view_trade_analysis.py index a9b2f7a..8ac3222 100644 --- a/view_trade_analysis.py +++ b/view_trade_analysis.py @@ -234,27 +234,78 @@ def render(): c4.metric("Short Win Rate",f"{stats['short_win_rate']}%") def render_equity_curve(df_plot, label="Equity Curve"): + import pandas as pd df_s = df_plot.sort_values('close_time').copy() - df_s['cumulative'] = df_s['net_profit'].cumsum() + + # ── Overlay toggles ─────────────────────────────────────────────── + ov_cols = st.columns(4) + show_account = ov_cols[0].checkbox("Account total", value=True, key=f"eq_account_{label}") + show_strategy = ov_cols[1].checkbox("By Strategy", value=False, key=f"eq_strat_{label}") + show_symbol = ov_cols[2].checkbox("By Symbol", value=False, key=f"eq_sym_{label}") + show_dow = ov_cols[3].checkbox("By Day of Week", value=False, key=f"eq_dow_{label}") + + COLORS = ['#7c6af7','#34C27A','#F5A623','#E05555','#4C8EF5', + '#A78BFA','#22D3EE','#FB923C','#F472B6','#86EFAC'] + fig = go.Figure() - fig.add_trace(go.Scatter( - x=df_s['close_time'], y=df_s['cumulative'], - mode='lines', - line=dict(color='#7c6af7', width=2), - fill='tozeroy', - fillcolor='rgba(124,106,247,0.08)', - name='Equity' - )) + + # Account total + if show_account: + df_s['_cum'] = df_s['net_profit'].cumsum() + fig.add_trace(go.Scatter( + x=df_s['close_time'], y=df_s['_cum'], + mode='lines', name='Account', + line=dict(color='#7c6af7', width=2), + fill='tozeroy', fillcolor='rgba(124,106,247,0.06)', + )) + + # By Strategy + if show_strategy and 'strategy' in df_s.columns: + for i, strat in enumerate(sorted(df_s['strategy'].dropna().unique())): + sub = df_s[df_s['strategy']==strat].copy() + sub['_cum'] = sub['net_profit'].cumsum() + fig.add_trace(go.Scatter( + x=sub['close_time'], y=sub['_cum'], + mode='lines', name=strat, + line=dict(color=COLORS[(i+1)%len(COLORS)], width=1.5, dash='dot'), + )) + + # By Symbol + if show_symbol and 'symbol' in df_s.columns: + for i, sym in enumerate(sorted(df_s['symbol'].dropna().unique())): + sub = df_s[df_s['symbol']==sym].copy() + sub['_cum'] = sub['net_profit'].cumsum() + fig.add_trace(go.Scatter( + x=sub['close_time'], y=sub['_cum'], + mode='lines', name=sym, + line=dict(color=COLORS[(i+2)%len(COLORS)], width=1.5, dash='dash'), + )) + + # By Day of Week + if show_dow and 'day_of_week' in df_s.columns: + dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday'] + for i, dow in enumerate(dow_order): + sub = df_s[df_s['day_of_week']==dow].copy() + if sub.empty: continue + sub['_cum'] = sub['net_profit'].cumsum() + fig.add_trace(go.Scatter( + x=sub['close_time'], y=sub['_cum'], + mode='lines', name=dow, + line=dict(color=COLORS[(i+3)%len(COLORS)], width=1.5), + )) + fig.update_layout( - title=label, height=300, - plot_bgcolor='rgba(10,10,15,1)', - paper_bgcolor='rgba(10,10,15,1)', - font=dict(color='#aaa', family='JetBrains Mono'), - xaxis=dict(gridcolor='rgba(255,255,255,0.04)'), - yaxis=dict(gridcolor='rgba(255,255,255,0.04)', tickprefix='$'), - margin=dict(l=60, r=20, t=40, b=40) + title=label, height=360, + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font=dict(family='sans-serif'), + xaxis=dict(gridcolor='rgba(128,128,128,0.15)', showgrid=True), + yaxis=dict(gridcolor='rgba(128,128,128,0.15)', tickprefix='$', showgrid=True), + margin=dict(l=60, r=20, t=40, b=40), + legend=dict(bgcolor='rgba(0,0,0,0)', borderwidth=0), + hovermode='x unified', ) - st.plotly_chart(fig, use_container_width=True) + st.plotly_chart(fig, use_container_width=True, key=f"eq_fig_{label}") def render_dow_chart(df_plot): dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] @@ -272,10 +323,10 @@ def render(): fig.add_trace(go.Bar(x=dow.index, y=losses_dow, name='Loss', marker_color='rgba(230,57,70,0.8)')) fig.update_layout( title='P&L by Day of Week', height=280, barmode='relative', - plot_bgcolor='rgba(10,10,15,1)', paper_bgcolor='rgba(10,10,15,1)', - font=dict(color='#aaa'), margin=dict(l=60, r=20, t=40, b=40), - xaxis=dict(gridcolor='rgba(255,255,255,0.04)'), - yaxis=dict(gridcolor='rgba(255,255,255,0.04)', tickprefix='$'), + plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', + font=dict(family='sans-serif'), margin=dict(l=60, r=20, t=40, b=40), + xaxis=dict(gridcolor='rgba(128,128,128,0.15)'), + yaxis=dict(gridcolor='rgba(128,128,128,0.15)', tickprefix='$'), legend=dict(bgcolor='rgba(0,0,0,0.3)') ) st.plotly_chart(fig, use_container_width=True) @@ -298,10 +349,10 @@ def render(): fig.add_trace(go.Bar(x=losses_h.index, y=losses_h, name='Loss', marker_color='rgba(230,57,70,0.8)')) fig.update_layout( title='P&L by Hour of Day', height=280, barmode='relative', - plot_bgcolor='rgba(10,10,15,1)', paper_bgcolor='rgba(10,10,15,1)', - font=dict(color='#aaa'), margin=dict(l=60, r=20, t=40, b=40), - xaxis=dict(gridcolor='rgba(255,255,255,0.04)', title='Hour (UTC)'), - yaxis=dict(gridcolor='rgba(255,255,255,0.04)', tickprefix='$'), + plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', + font=dict(family='sans-serif'), margin=dict(l=60, r=20, t=40, b=40), + xaxis=dict(gridcolor='rgba(128,128,128,0.15)', title='Hour (UTC)'), + yaxis=dict(gridcolor='rgba(128,128,128,0.15)', tickprefix='$'), legend=dict(bgcolor='rgba(0,0,0,0.3)') ) st.plotly_chart(fig, use_container_width=True)