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 8037d13..89d62f5 100644 Binary files a/__pycache__/view_settings.cpython-314.pyc and b/__pycache__/view_settings.cpython-314.pyc differ diff --git a/__pycache__/view_trade_analysis.cpython-314.pyc b/__pycache__/view_trade_analysis.cpython-314.pyc index bbb3962..bb54cc6 100644 Binary files a/__pycache__/view_trade_analysis.cpython-314.pyc and b/__pycache__/view_trade_analysis.cpython-314.pyc differ diff --git a/app.py b/app.py index 9abd7c4..8e737d4 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,8 @@ Launch: streamlit run app.py import streamlit as st from streamlit_option_menu import option_menu import importlib, sys, os +import view_settings +view_settings.inject_theme_css() # ── Page config ─────────────────────────────────────────────────────────────── st.set_page_config( diff --git a/view_settings.py b/view_settings.py index 39d7a04..ead25e9 100644 --- a/view_settings.py +++ b/view_settings.py @@ -1,37 +1,210 @@ """ -pages/settings.py -================= -Settings page for MT5 Tools dashboard. +view_settings.py +================ +Settings page — theme, custom colours, font size. """ import streamlit as st +import os, re + +CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".streamlit") +CONFIG_FILE = os.path.join(CONFIG_DIR, "config.toml") + +THEMES = { + "Dark": { + "base": "dark", + "primaryColor": "#7c6af7", + "backgroundColor": "#0e1117", + "secondaryBackgroundColor": "#1a1f2e", + "textColor": "#fafafa", + "font": "sans serif", + }, + "Light": { + "base": "light", + "primaryColor": "#2E75B6", + "backgroundColor": "#ffffff", + "secondaryBackgroundColor": "#f0f2f6", + "textColor": "#1a1a1a", + "font": "sans serif", + }, +} + + +def _read_config() -> 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)