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)