diff --git a/__pycache__/mt5_parser.cpython-314.pyc b/__pycache__/mt5_parser.cpython-314.pyc
index 0a703ee..73a59fa 100644
Binary files a/__pycache__/mt5_parser.cpython-314.pyc and b/__pycache__/mt5_parser.cpython-314.pyc differ
diff --git a/__pycache__/view_trade_analysis.cpython-314.pyc b/__pycache__/view_trade_analysis.cpython-314.pyc
index 580fe7e..9da82a4 100644
Binary files a/__pycache__/view_trade_analysis.cpython-314.pyc and b/__pycache__/view_trade_analysis.cpython-314.pyc differ
diff --git a/mt5_parser.py b/mt5_parser.py
index af9151c..9b2ae25 100644
--- a/mt5_parser.py
+++ b/mt5_parser.py
@@ -67,6 +67,8 @@ def _enrich(df):
)
df['win'] = df['net_profit'] > 0
df['type'] = df['type'].str.lower().str.strip()
+ if 'comment' not in df.columns:
+ df['comment'] = ''
df['strategy'] = df['comment'].apply(extract_strategy)
# Normalise symbol — strip .a suffix for display matching
df['symbol_base'] = df['symbol'].str.replace(r'\.[a-z]+$', '', regex=True).str.upper()
@@ -256,12 +258,15 @@ def parse_quant_csv(file_bytes):
df = pd.DataFrame(result)
- # Parse datetimes — QA uses DD.MM.YYYY HH:MM:SS
+ # Parse datetimes — try QA format first (DD.MM.YYYY), then ISO (YYYY-MM-DD)
for col in ['open_time', 'close_time']:
if col in df.columns:
- df[col] = pd.to_datetime(df[col], format='%d.%m.%Y %H:%M:%S', errors='coerce')
- if df[col].isna().all():
- df[col] = pd.to_datetime(df[col], infer_datetime_format=True, errors='coerce')
+ parsed = pd.to_datetime(df[col], format='%d.%m.%Y %H:%M:%S', errors='coerce')
+ if parsed.isna().all():
+ parsed = pd.to_datetime(df[col], format='%Y-%m-%d %H:%M:%S', errors='coerce')
+ if parsed.isna().all():
+ parsed = pd.to_datetime(df[col], errors='coerce')
+ df[col] = parsed
# QA comm_swap is combined — split evenly as approximation if no separate swap
if 'commission' in df.columns and 'swap' not in df.columns:
diff --git a/trade_analysis.py b/trade_analysis.py
deleted file mode 100644
index 95bf2ff..0000000
--- a/trade_analysis.py
+++ /dev/null
@@ -1,333 +0,0 @@
-"""
-pages/trade_analysis.py
-=======================
-MT5 Trade Analysis page — migrated from main dashboard.
-"""
-
-import streamlit as st
-import plotly.graph_objects as go
-import sys, os
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from mt5_parser import detect_and_parse, calc_stats
-
-
-def render():
- st.title("📊 Trade Analysis")
-
- # ── Session state ─────────────────────────────────────────────────────────
- if 'ta_df' not in st.session_state:
- st.session_state['ta_df'] = None
- st.session_state['ta_format'] = None
-
- # ── File upload ───────────────────────────────────────────────────────────
- col1, col2 = st.columns([4, 1])
- with col1:
- uploaded = st.file_uploader(
- "Upload MT5 Report (HTM/HTML) or Quant Analyzer CSV",
- type=['html', 'htm', 'csv'],
- key='ta_upload'
- )
- with col2:
- st.markdown("
", unsafe_allow_html=True)
- if st.button("🗑 Clear", key='ta_clear'):
- st.session_state['ta_df'] = None
- st.session_state['ta_format'] = None
- st.rerun()
-
- if uploaded:
- df, fmt = detect_and_parse(uploaded.read(), uploaded.name)
- if df is not None:
- st.session_state['ta_df'] = df
- st.session_state['ta_format'] = fmt
- st.success(f"✓ Loaded {len(df)} trades — {fmt}")
- else:
- st.error("Could not parse report — check file format")
-
- df_all = st.session_state['ta_df']
- fmt = st.session_state['ta_format']
-
- if df_all is None or len(df_all) == 0:
- st.markdown("""
-
- Upload an MT5 account history report (.htm/.html), MT5 backtest report,
- or a Quant Analyzer CSV export to begin analysis.
-
- """, unsafe_allow_html=True)
- return
-
- if fmt:
- st.caption(f"Format detected: **{fmt}** · {len(df_all)} total trades")
-
- # ── Filters ───────────────────────────────────────────────────────────────
- st.divider()
- fc1, fc2, fc3, fc4 = st.columns(4)
-
- with fc1:
- date_min = df_all['open_time'].min().date()
- date_max = df_all['open_time'].max().date()
- date_from = st.date_input("From", value=date_min, min_value=date_min,
- max_value=date_max, key='ta_from')
- date_to = st.date_input("To", value=date_max, min_value=date_min,
- max_value=date_max, key='ta_to')
-
- with fc2:
- symbols = sorted(df_all['symbol'].dropna().unique().tolist())
- sel_symbol = st.multiselect("Symbol", symbols, key='ta_sym')
-
- with fc3:
- strategies = sorted(df_all['strategy'].dropna().unique().tolist())
- sel_strategy = st.multiselect("Strategy / EA", strategies, key='ta_strat')
-
- with fc4:
- days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
- sel_days = st.multiselect("Day of week", days, key='ta_days')
- sel_type = st.multiselect("Type", ['buy', 'sell'], key='ta_type')
-
- # Apply filters
- df = df_all.copy()
- df = df[(df['open_time'].dt.date >= date_from) &
- (df['open_time'].dt.date <= date_to)]
- if sel_symbol:
- df = df[df['symbol'].isin(sel_symbol)]
- if sel_strategy:
- df = df[df['strategy'].isin(sel_strategy)]
- if sel_days:
- df = df[df['day_of_week'].isin(sel_days)]
- if sel_type:
- df = df[df['type'].isin(sel_type)]
-
- st.caption(f"Showing **{len(df)}** trades after filters")
-
- # ── Analysis mode ─────────────────────────────────────────────────────────
- mode = st.radio(
- "Analysis mode",
- ["Overall", "By Strategy", "By Symbol", "By Day of Week"],
- horizontal=True, key='ta_mode'
- )
- st.divider()
-
- # ── Helpers ───────────────────────────────────────────────────────────────
- def render_stats(stats, label=""):
- if label:
- st.markdown(f"**{label}**")
-
- c1, c2, c3, c4, c5 = st.columns(5)
- c1.metric("Net Profit", f"${stats['net_profit']:,.2f}")
- c2.metric("Win Rate", f"{stats['win_rate']}%")
- c3.metric("Profit Factor", f"{stats['profit_factor']}")
- c4.metric("R:R Ratio", f"{stats['rr_ratio']}")
- c5.metric("Expectancy", f"${stats['expectancy']:,.2f}")
-
- c1, c2, c3, c4, c5 = st.columns(5)
- c1.metric("Total Trades", stats['total_trades'])
- c2.metric("Avg Win", f"${stats['avg_win']:,.2f}")
- c3.metric("Avg Loss", f"${stats['avg_loss']:,.2f}")
- c4.metric("Max DD", f"${stats['max_drawdown']:,.2f}")
- c5.metric("Best Trade", f"${stats['best_trade']:,.2f}")
-
- c1, c2, c3, c4, c5 = st.columns(5)
- c1.metric("Max Consec Wins", stats['max_consec_wins'])
- c2.metric("Max Consec Losses", stats['max_consec_losses'])
- c3.metric("Avg Win Dur", f"{stats['avg_win_duration']}m")
- c4.metric("Avg Loss Dur", f"{stats['avg_loss_duration']}m")
- c5.metric("Worst Trade", f"${stats['worst_trade']:,.2f}")
-
- c1, c2, c3, c4 = st.columns(4)
- c1.metric("Long Trades", stats['long_trades'])
- c2.metric("Long Win Rate", f"{stats['long_win_rate']}%")
- c3.metric("Short Trades", stats['short_trades'])
- c4.metric("Short Win Rate",f"{stats['short_win_rate']}%")
-
- def render_equity_curve(df_plot, label="Equity Curve"):
- df_s = df_plot.sort_values('close_time').copy()
- df_s['cumulative'] = df_s['net_profit'].cumsum()
- 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'
- ))
- 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)
- )
- st.plotly_chart(fig, use_container_width=True)
-
- def render_dow_chart(df_plot):
- dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
- dow = df_plot.groupby('day_of_week').agg(
- trades = ('net_profit', 'count'),
- net_profit = ('net_profit', 'sum'),
- win_rate = ('win', lambda x: round(x.mean()*100, 1))
- ).reindex([d for d in dow_order if d in df_plot['day_of_week'].unique()])
-
- wins_dow = df_plot[df_plot['win']].groupby('day_of_week')['net_profit'].sum().reindex(dow.index, fill_value=0)
- losses_dow = df_plot[~df_plot['win']].groupby('day_of_week')['net_profit'].sum().reindex(dow.index, fill_value=0)
-
- fig = go.Figure()
- fig.add_trace(go.Bar(x=dow.index, y=wins_dow, name='Profit', marker_color='rgba(45,198,83,0.8)'))
- 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='$'),
- legend=dict(bgcolor='rgba(0,0,0,0.3)')
- )
- st.plotly_chart(fig, use_container_width=True)
-
- dt = dow.reset_index()
- dt.columns = ['Day', 'Trades', 'Net Profit', 'Win Rate %']
- dt['Net Profit'] = dt['Net Profit'].round(2)
- st.dataframe(dt, use_container_width=True, hide_index=True)
-
- def render_hour_chart(df_plot):
- hourly = df_plot.groupby('hour').agg(
- trades = ('net_profit', 'count'),
- net_profit = ('net_profit', 'sum'),
- )
- wins_h = df_plot[df_plot['win']].groupby('hour')['net_profit'].sum().reindex(hourly.index, fill_value=0)
- losses_h = df_plot[~df_plot['win']].groupby('hour')['net_profit'].sum().reindex(hourly.index, fill_value=0)
-
- fig = go.Figure()
- fig.add_trace(go.Bar(x=wins_h.index, y=wins_h, name='Profit', marker_color='rgba(45,198,83,0.8)'))
- 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='$'),
- legend=dict(bgcolor='rgba(0,0,0,0.3)')
- )
- st.plotly_chart(fig, use_container_width=True)
-
- def colour_profit(val):
- try:
- v = float(str(val).replace(',', ''))
- if v > 0: return 'background-color: rgba(0,180,0,0.12)'
- if v < 0: return 'background-color: rgba(180,0,0,0.12)'
- except:
- pass
- return ''
-
- # ── Render mode ───────────────────────────────────────────────────────────
- if mode == "Overall":
- stats = calc_stats(df)
- render_stats(stats, "Overall Statistics")
- render_equity_curve(df)
- col1, col2 = st.columns(2)
- with col1:
- render_dow_chart(df)
- with col2:
- render_hour_chart(df)
-
- elif mode == "By Strategy":
- strats = sorted(df['strategy'].dropna().unique().tolist())
- if not strats:
- st.info("No strategies found")
- else:
- st.subheader("Strategy Comparison")
- rows = []
- for s in strats:
- sdf = df[df['strategy'] == s]
- stat = calc_stats(sdf)
- rows.append({
- 'Strategy' : s,
- 'Trades' : stat['total_trades'],
- 'Net Profit' : stat['net_profit'],
- 'Win Rate %' : stat['win_rate'],
- 'Profit Factor' : stat['profit_factor'],
- 'R:R' : stat['rr_ratio'],
- 'Expectancy' : stat['expectancy'],
- 'Max DD' : stat['max_drawdown'],
- 'Max Consec W' : stat['max_consec_wins'],
- 'Max Consec L' : stat['max_consec_losses'],
- })
- sdf_sum = __import__('pandas').DataFrame(rows).sort_values('Net Profit', ascending=False)
- st.dataframe(
- sdf_sum.style.map(colour_profit, subset=['Net Profit', 'Expectancy', 'Max DD']),
- use_container_width=True, hide_index=True
- )
- st.divider()
- sel = st.selectbox("Select strategy for detail", strats)
- if sel:
- sdf = df[df['strategy'] == sel]
- stat = calc_stats(sdf)
- render_stats(stat, sel)
- render_equity_curve(sdf, f"{sel} — Equity Curve")
- col1, col2 = st.columns(2)
- with col1: render_dow_chart(sdf)
- with col2: render_hour_chart(sdf)
-
- elif mode == "By Symbol":
- syms = sorted(df['symbol'].dropna().unique().tolist())
- rows = []
- for s in syms:
- sdf = df[df['symbol'] == s]
- stat = calc_stats(sdf)
- rows.append({
- 'Symbol' : s,
- 'Trades' : stat['total_trades'],
- 'Net Profit' : stat['net_profit'],
- 'Win Rate %' : stat['win_rate'],
- 'Profit Factor' : stat['profit_factor'],
- 'R:R' : stat['rr_ratio'],
- 'Expectancy' : stat['expectancy'],
- 'Max DD' : stat['max_drawdown'],
- })
- sdf_sum = __import__('pandas').DataFrame(rows).sort_values('Net Profit', ascending=False)
- st.dataframe(
- sdf_sum.style.map(colour_profit, subset=['Net Profit', 'Expectancy', 'Max DD']),
- use_container_width=True, hide_index=True
- )
- sel = st.selectbox("Select symbol for detail", syms)
- if sel:
- sdf = df[df['symbol'] == sel]
- stat = calc_stats(sdf)
- render_stats(stat, sel)
- render_equity_curve(sdf, f"{sel} — Equity Curve")
- col1, col2 = st.columns(2)
- with col1: render_dow_chart(sdf)
- with col2: render_hour_chart(sdf)
-
- elif mode == "By Day of Week":
- render_dow_chart(df)
- render_hour_chart(df)
-
- # ── Raw trade log ─────────────────────────────────────────────────────────
- st.divider()
- with st.expander("Raw Trade Log"):
- show_cols = ['open_time', 'close_time', 'symbol', 'type', 'strategy',
- 'volume', 'open_price', 'close_price', 'sl', 'tp',
- 'commission', 'swap', 'profit', 'net_profit', 'duration_min']
- show_cols = [c for c in show_cols if c in df.columns]
-
- def colour_net(val):
- try:
- v = float(val)
- if v > 0: return 'background-color: rgba(0,180,0,0.12)'
- if v < 0: return 'background-color: rgba(180,0,0,0.12)'
- except:
- pass
- return ''
-
- st.dataframe(
- df[show_cols].style.map(colour_net, subset=['net_profit', 'profit']),
- use_container_width=True, hide_index=True, height=400
- )
- st.download_button(
- "⬇ Download filtered trades CSV",
- data = df[show_cols].to_csv(index=False),
- file_name = f"mt5_trades_{date_from}_{date_to}.csv",
- mime = 'text/csv'
- )
diff --git a/view_trade_analysis.py b/view_trade_analysis.py
index a8732ad..e1532c0 100644
--- a/view_trade_analysis.py
+++ b/view_trade_analysis.py
@@ -161,8 +161,9 @@ def render():
fc1, fc2, fc3, fc4 = st.columns(4)
with fc1:
- date_min = df_all['open_time'].min().date()
- date_max = df_all['open_time'].max().date()
+ valid_times = df_all['open_time'].dropna()
+ date_min = valid_times.min().date()
+ date_max = valid_times.max().date()
date_from = st.date_input("From", value=date_min, min_value=date_min,
max_value=date_max, key='ta_from')
date_to = st.date_input("To", value=date_max, min_value=date_min,
@@ -301,6 +302,54 @@ def render():
)
st.plotly_chart(fig, use_container_width=True, key=f"eq_fig_{safe_key}")
+ # ── Daily P&L bars ────────────────────────────────────────────────
+ st.markdown("**Daily P&L**")
+ daily = (df_s.groupby(df_s['close_time'].dt.date)['net_profit']
+ .sum().reset_index())
+ daily.columns = ['date','pnl']
+ daily['color'] = daily['pnl'].apply(
+ lambda v: 'rgba(52,194,122,0.75)' if v >= 0 else 'rgba(220,80,80,0.75)')
+ fig_d = go.Figure(go.Bar(
+ x=daily['date'], y=daily['pnl'],
+ marker_color=daily['color'], name='Daily P&L',
+ ))
+ fig_d.update_layout(
+ height=160,
+ 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=False,
+ showticklabels=False),
+ yaxis=dict(gridcolor='rgba(128,128,128,0.15)', tickprefix='$',
+ showgrid=True, zeroline=True,
+ zerolinecolor='rgba(128,128,128,0.3)'),
+ margin=dict(l=60, r=20, t=8, b=20),
+ showlegend=False,
+ )
+ st.plotly_chart(fig_d, use_container_width=True, key=f"eq_daily_{safe_key}")
+
+ # ── Drawdown panel ────────────────────────────────────────────────
+ st.markdown("**Drawdown**")
+ df_s['_cum2'] = df_s['net_profit'].cumsum()
+ df_s['_peak'] = df_s['_cum2'].cummax()
+ df_s['_dd'] = df_s['_cum2'] - df_s['_peak']
+ fig_dd = go.Figure(go.Scatter(
+ x=df_s['close_time'], y=df_s['_dd'],
+ mode='lines', fill='tozeroy',
+ line=dict(color='rgba(220,80,80,0.6)', width=1),
+ fillcolor='rgba(220,80,80,0.12)', name='Drawdown',
+ ))
+ fig_dd.update_layout(
+ height=120,
+ 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=8, b=40),
+ showlegend=False,
+ )
+ st.plotly_chart(fig_dd, use_container_width=True, key=f"eq_dd_{safe_key}")
+
def render_dow_chart(df_plot):
dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
dow = df_plot.groupby('day_of_week').agg(