diff --git a/.streamlit/config.toml b/.streamlit/config.toml index e9e9e5e..7d1bf4b 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,7 +1,7 @@ [theme] -base = "light" -primaryColor = "#2E75B6" -backgroundColor = "#ffffff" -secondaryBackgroundColor = "#f0f2f6" -textColor = "#1a1a1a" +base = "dark" +primaryColor = "#7c6af7" +backgroundColor = "#0e1117" +secondaryBackgroundColor = "#1a1f2e" +textColor = "#fafafa" font = "sans serif" diff --git a/__pycache__/mt5_parser.cpython-314.pyc b/__pycache__/mt5_parser.cpython-314.pyc index 73a59fa..3f8f750 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 9da82a4..0633bbb 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 9b2ae25..a8de624 100644 --- a/mt5_parser.py +++ b/mt5_parser.py @@ -350,8 +350,13 @@ def calc_stats(df): longs = df[df['type'] == 'buy'] shorts = df[df['type'] == 'sell'] + trading_days = df['open_time'].dt.date.nunique() if 'open_time' in df.columns else 0 + trades_per_day = round(total / trading_days, 2) if trading_days > 0 else 0 + return { 'total_trades' : total, + 'trading_days' : trading_days, + 'trades_per_day' : trades_per_day, 'win_rate' : win_rate, 'net_profit' : net_profit, 'gross_profit' : gross_profit, diff --git a/view_trade_analysis.py b/view_trade_analysis.py index e1532c0..2f62f25 100644 --- a/view_trade_analysis.py +++ b/view_trade_analysis.py @@ -54,8 +54,11 @@ def render(): # ── Session state ───────────────────────────────────────────────────────── for _k, _v in { - 'ta_df': None, 'ta_format': None, - 'ta_accounts': [], 'ta_ic_bytes': None, + 'ta_df': None, 'ta_format': None, + 'ta_accounts': [], 'ta_ic_bytes': None, + 'ta_df_original': None, + 'ta_df_edited': None, + 'ta_group_summary': None, }.items(): if _k not in st.session_state: st.session_state[_k] = _v @@ -85,9 +88,10 @@ def render(): if uploaded and uploaded.name.lower().endswith(('.htm','.html','.csv')): 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.session_state['ta_accounts'] = [] + st.session_state['ta_df'] = df + st.session_state['ta_df_original'] = df.copy() + st.session_state['ta_format'] = fmt + st.session_state['ta_accounts'] = [] st.success(f"✓ Loaded {len(df)} trades — {fmt}") else: st.error("Could not parse report — check file format") @@ -117,7 +121,8 @@ def render(): st.session_state['ta_format'] = "IC Markets XLSX" df_ic = parse_icmarkets_xlsx(file_bytes, account=accounts[0]) df_ic = _normalise_ic(df_ic) - st.session_state['ta_df'] = df_ic + st.session_state['ta_df'] = df_ic + st.session_state['ta_df_original'] = df_ic.copy() st.success(f"✓ Loaded {len(df_ic)} trades — {len(accounts)} account(s) found") except Exception as e: st.error(f"Error parsing file: {e}") @@ -138,8 +143,9 @@ def render(): df_ic = _normalise_ic(df_ic) st.session_state['ta_df'] = df_ic - df_all = st.session_state['ta_df'] - fmt = st.session_state['ta_format'] + df_all = st.session_state['ta_df'] + df_edited = st.session_state.get('ta_df_edited') + fmt = st.session_state['ta_format'] if df_all is None or len(df_all) == 0: st.markdown(""" @@ -153,6 +159,15 @@ def render(): if fmt: st.caption(f"Format detected: **{fmt}** · {len(df_all)} total trades") + # ── View selector ───────────────────────────────────────────────────────── + has_edited = st.session_state.get('ta_df_edited') is not None + if has_edited: + view_opts = ["Original", "Edited", "Both"] + view_sel = st.radio("View", view_opts, horizontal=True, key='ta_view_sel') + else: + view_sel = "Original" + st.session_state['ta_view_sel'] = "Original" + # ── Filters ─────────────────────────────────────────────────────────────── st.divider() if 'ta_deposit' not in st.session_state: @@ -181,20 +196,30 @@ def render(): 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') + trade_nums = [str(i) for i in range(1, len(df_all)+1)] + sel_trades = st.multiselect("Trade #", trade_nums, key='ta_idx_sel', + placeholder="All trades (filter by #)") # 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)] + def _apply_filters(src_df): + d = src_df.copy() + d = d[(d['open_time'].dt.date >= date_from) & + (d['open_time'].dt.date <= date_to)] + if sel_symbol: d = d[d['symbol'].isin(sel_symbol)] + if sel_strategy: d = d[d['strategy'].isin(sel_strategy)] + if sel_days: d = d[d['day_of_week'].isin(sel_days)] + if sel_type: d = d[d['type'].isin(sel_type)] + d = d.reset_index(drop=True) + if sel_trades: + sel_idx = [int(t)-1 for t in sel_trades if int(t)-1 < len(d)] + d = d.iloc[sel_idx].reset_index(drop=True) + return d + + df = _apply_filters(df_all) + + # Also prepare edited df if available + df_e = _apply_filters(df_edited) if df_edited is not None else None st.caption(f"Showing **{len(df)}** trades after filters") @@ -207,38 +232,93 @@ def render(): st.divider() # ── Helpers ─────────────────────────────────────────────────────────────── - def render_stats(stats, label=""): + def render_stats(stats, label="", stats_compare=None): 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}") + def _delta(key, fmt='$', higher_is_better=True): + """Return delta string for st.metric when compare stats available.""" + if stats_compare is None or key not in stats_compare: + return None + diff = stats_compare[key] - stats[key] + if diff == 0: + return None + if fmt == '$': + return f"${diff:+,.2f}" + elif fmt == '%': + return f"{diff:+.1f}%" + elif fmt == 'x': + return f"{diff:+.2f}" + else: + return f"{diff:+g}" + + def _inv_delta(key, fmt='$'): + """Delta where lower is better (e.g. drawdown, losses).""" + if stats_compare is None or key not in stats_compare: + return None + diff = stats_compare[key] - stats[key] + if diff == 0: + return None + if fmt == '$': + return f"${diff:+,.2f}" + elif fmt == '%': + return f"{diff:+.1f}%" + else: + return f"{diff:+g}" 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.metric("Net Profit", f"${stats['net_profit']:,.2f}", + delta=_delta('net_profit','$')) + c2.metric("Win Rate", f"{stats['win_rate']}%", + delta=_delta('win_rate','%')) + c3.metric("Profit Factor", f"{stats['profit_factor']}", + delta=_delta('profit_factor','x')) + c4.metric("R:R Ratio", f"{stats['rr_ratio']}", + delta=_delta('rr_ratio','x')) + c5.metric("Expectancy", f"${stats['expectancy']:,.2f}", + delta=_delta('expectancy','$')) 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.metric("Total Trades", stats['total_trades'], + delta=_delta('total_trades','')) + c2.metric("Avg Win", f"${stats['avg_win']:,.2f}", + delta=_delta('avg_win','$')) + c3.metric("Avg Loss", f"${stats['avg_loss']:,.2f}", + delta=_inv_delta('avg_loss','$'), delta_color="inverse") + c4.metric("Max DD", f"${stats['max_drawdown']:,.2f}", + delta=_inv_delta('max_drawdown','$'), delta_color="inverse") + c5.metric("Best Trade", f"${stats['best_trade']:,.2f}", + delta=_delta('best_trade','$')) + + c1, c2, c3, c4, c5 = st.columns(5) + c1.metric("Max Consec Wins", stats['max_consec_wins'], + delta=_delta('max_consec_wins','')) + c2.metric("Max Consec Losses", stats['max_consec_losses'], + delta=_inv_delta('max_consec_losses',''), delta_color="inverse") + c3.metric("Avg Win Dur", f"{stats['avg_win_duration']}m", + delta=_delta('avg_win_duration','')) + c4.metric("Avg Loss Dur", f"{stats['avg_loss_duration']}m", + delta=_inv_delta('avg_loss_duration',''), delta_color="inverse") + c5.metric("Worst Trade", f"${stats['worst_trade']:,.2f}", + delta=_inv_delta('worst_trade','$'), delta_color="inverse") 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']}%") + c1.metric("Long Trades", stats['long_trades'], + delta=_delta('long_trades','')) + c2.metric("Long Win Rate", f"{stats['long_win_rate']}%", + delta=_delta('long_win_rate','%')) + c3.metric("Short Trades", stats['short_trades'], + delta=_delta('short_trades','')) + c4.metric("Short Win Rate",f"{stats['short_win_rate']}%", + delta=_delta('short_win_rate','%')) - def render_equity_curve(df_plot, label="Equity Curve"): + c1, c2, c3, c4 = st.columns(4) + c1.metric("Trading Days", stats.get('trading_days', 0), + delta=_delta('trading_days','')) + c2.metric("Trades / Day", f"{stats.get('trades_per_day', 0)}", + delta=_delta('trades_per_day','x')) + + def render_equity_curve(df_plot, label="Equity Curve", df_compare=None, compare_label="Edited"): import pandas as pd df_s = df_plot.sort_values('close_time').copy() @@ -257,10 +337,20 @@ def render(): 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', + x=df_s['close_time'], y=df_s['_cum'], mode='lines', name='Original', line=dict(color='#7c6af7', width=2), fill='tozeroy', fillcolor='rgba(124,106,247,0.06)', )) + # Overlay edited/compare line if provided + if df_compare is not None: + df_c = df_compare.sort_values('close_time').copy() + df_c['_cum'] = df_c['net_profit'].cumsum() + fig.add_trace(go.Scatter( + x=df_c['close_time'], y=df_c['_cum'], mode='lines', + name=compare_label, + line=dict(color='#34C27A', width=2, dash='dash'), + fill='tozeroy', fillcolor='rgba(52,194,122,0.04)', + )) if show_strategy and 'strategy' in df_s.columns: for i, strat in enumerate(sorted(df_s['strategy'].dropna().unique())): @@ -302,53 +392,86 @@ def render(): ) st.plotly_chart(fig, use_container_width=True, key=f"eq_fig_{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() + fig_dd.add_trace(go.Scatter( + x=df_s['close_time'], y=df_s['_dd'], + mode='lines', fill='tozeroy', + line=dict(color='rgba(124,106,247,0.8)', width=1.5), + fillcolor='rgba(124,106,247,0.08)', name='DD Original', + )) + if df_compare is not None: + df_c2 = df_compare.sort_values('close_time').copy() + df_c2['_cum2'] = df_c2['net_profit'].cumsum() + df_c2['_peak'] = df_c2['_cum2'].cummax() + df_c2['_dd'] = df_c2['_cum2'] - df_c2['_peak'] + fig_dd.add_trace(go.Scatter( + x=df_c2['close_time'], y=df_c2['_dd'], + mode='lines', fill='tozeroy', + line=dict(color='rgba(220,80,80,0.8)', width=1.5), + fillcolor='rgba(220,80,80,0.08)', name=f'DD {compare_label}', + )) + fig_dd.update_layout( + height=130, + 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, + showticklabels=False), + yaxis=dict(gridcolor='rgba(128,128,128,0.15)', tickprefix='$', + showgrid=True), + margin=dict(l=60, r=20, t=8, b=4), + showlegend=False, + ) + st.plotly_chart(fig_dd, use_container_width=True, key=f"eq_dd_{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}") + lambda v: 'rgba(52,194,122,0.85)' if v >= 0 else 'rgba(220,80,80,0.85)') - # ── 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_d = go.Figure() + fig_d.add_trace(go.Bar( + x=daily['date'], y=daily['pnl'], + marker_color=daily['color'], + name='Original', + offsetgroup=0, )) - fig_dd.update_layout( - height=120, + + if df_compare is not None: + dc = df_compare.sort_values('close_time').copy() + daily_c = (dc.groupby(dc['close_time'].dt.date)['net_profit'] + .sum().reset_index()) + daily_c.columns = ['date','pnl'] + daily_c['color'] = daily_c['pnl'].apply( + lambda v: 'rgba(124,106,247,0.45)' if v >= 0 else 'rgba(255,165,0,0.45)') + fig_d.add_trace(go.Bar( + x=daily_c['date'], y=daily_c['pnl'], + marker_color=daily_c['color'], + name=compare_label, + offsetgroup=1, + )) + + fig_d.update_layout( + height=160, barmode='group', 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, + showgrid=True, zeroline=True, + zerolinecolor='rgba(128,128,128,0.4)'), + margin=dict(l=60, r=20, t=4, b=40), + legend=dict(bgcolor='rgba(0,0,0,0)', orientation='h', + yanchor='bottom', y=1.02), + showlegend=df_compare is not None, ) - st.plotly_chart(fig_dd, use_container_width=True, key=f"eq_dd_{safe_key}") + st.plotly_chart(fig_d, use_container_width=True, key=f"eq_daily_{safe_key}") def render_dow_chart(df_plot): dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] @@ -477,16 +600,55 @@ def render(): # ── Render mode ─────────────────────────────────────────────────────────── if mode == "Overall": - stats = calc_stats(df) - render_stats(stats, "Overall Statistics") - render_equity_curve(df) + stats = calc_stats(df) + stats_e = calc_stats(df_e) if df_e is not None else None + if view_sel == "Edited" and stats_e: + render_stats(stats_e, "Overall Statistics (Edited)") + elif view_sel == "Both" and stats_e: + render_stats(stats, "Overall Statistics", stats_compare=stats_e) + else: + render_stats(stats, "Overall Statistics") + if view_sel == "Both" and df_e is not None: + render_equity_curve(df, label="Equity Curve", df_compare=df_e, + compare_label="Edited") + elif view_sel == "Edited" and df_e is not None: + render_equity_curve(df_e, label="Equity Curve (Edited)") + else: + render_equity_curve(df) + _df_charts = df_e if (view_sel in ("Edited","Both") and df_e is not None) else df col1, col2 = st.columns(2) with col1: - render_dow_chart(df) + render_dow_chart(_df_charts) with col2: - render_hour_chart(df) + render_hour_chart(_df_charts) st.divider() - render_monthly_table(df, "Monthly Performance", key_prefix="mt_overall") + # ── Grouped trades summary (collapsible) ────────────────────────── + group_summary = st.session_state.get('ta_group_summary') + if group_summary and view_sel in ("Edited", "Both"): + import pandas as _pd + n_groups = len([r for r in group_summary if r['Group'] != '—']) + n_single = len([r for r in group_summary if r['Group'] == '—']) + with st.expander( + f"Position Summary — {len(group_summary)} positions " + f"({n_groups} grouped, {n_single} individual)", expanded=False): + st.caption("Grouped positions show merged entries. Individual trades show as single rows. Sorted by open time.") + gs_df = _pd.DataFrame(group_summary) + def _colour_pnl(val): + try: + v = float(val) + if v > 0: return 'background-color: rgba(52,194,122,0.15)' + if v < 0: return 'background-color: rgba(220,80,80,0.15)' + except: pass + return '' + st.dataframe( + gs_df.style.map(_colour_pnl, subset=['Net P&L ($)']), + use_container_width=True, hide_index=True + ) + if view_sel == "Both" and df_e is not None: + render_monthly_table(df, "Monthly Performance (Original)", key_prefix="mt_overall_orig") + render_monthly_table(df_e,"Monthly Performance (Edited)", key_prefix="mt_overall_edit") + else: + render_monthly_table(_df_charts, "Monthly Performance", key_prefix="mt_overall") elif mode == "By Strategy": strats = sorted(df['strategy'].dropna().unique().tolist()) @@ -494,11 +656,12 @@ def render(): st.info("No strategies found") else: st.subheader("Strategy Comparison") + _df_s = df_e if (view_sel in ("Edited","Both") and df_e is not None) else df rows = [] for s in strats: - sdf = df[df['strategy'] == s] + sdf = _df_s[_df_s['strategy'] == s] if s in _df_s['strategy'].values else df[df['strategy']==s] stat = calc_stats(sdf) - rows.append({ + row = { 'Strategy' : s, 'Trades' : stat['total_trades'], 'Net Profit' : stat['net_profit'], @@ -509,24 +672,49 @@ def render(): '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 - ) + } + if view_sel == "Both" and df_e is not None: + sdf_e = df_e[df_e['strategy'] == s] if s in df_e['strategy'].values else None + if sdf_e is not None and len(sdf_e): + stat_e = calc_stats(sdf_e) + def _arr(k, higher=''): + diff = stat_e[k] - stat[k] + if abs(diff) < 0.001: return '' + arrow = '▲' if diff > 0 else '▼' + color = 'green' if (diff > 0) == (k not in ('max_drawdown','max_consec_losses')) else 'red' + return f" {arrow}{abs(diff):.2f}" + row['Net Profit'] = f"{stat['net_profit']:.2f}{_arr('net_profit')}" + row['Win Rate %'] = f"{stat['win_rate']}{_arr('win_rate')}" + row['Profit Factor'] = f"{stat['profit_factor']}{_arr('profit_factor')}" + row['Expectancy'] = f"{stat['expectancy']:.2f}{_arr('expectancy')}" + row['Max DD'] = f"{stat['max_drawdown']:.2f}{_arr('max_drawdown')}" + rows.append(row) + import pandas as pd + sdf_sum = pd.DataFrame(rows).sort_values('Net Profit', ascending=False) + st.dataframe(sdf_sum, use_container_width=True, hide_index=True) st.divider() sel = st.selectbox("Select strategy for detail", strats) if sel: - sdf = df[df['strategy'] == sel] + sdf = _df_s[_df_s['strategy'] == sel] if sel in _df_s['strategy'].values else df[df['strategy']==sel] stat = calc_stats(sdf) - render_stats(stat, sel) + sdf_e_sel = df_e[df_e['strategy']==sel] if (df_e is not None and sel in df_e['strategy'].values) else None + stats_e_sel = calc_stats(sdf_e_sel) if sdf_e_sel is not None and len(sdf_e_sel) else None + if view_sel == "Both" and stats_e_sel: + render_stats(stat, sel, stats_compare=stats_e_sel) + elif view_sel == "Edited" and stats_e_sel: + render_stats(stats_e_sel, f"{sel} (Edited)") + else: + 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) st.divider() - render_monthly_table(sdf, "Monthly Performance", key_prefix=f"mt_strat_{sel}") + if view_sel == "Both" and sdf_e_sel is not None and len(sdf_e_sel): + render_monthly_table(sdf, "Monthly Performance (Original)", key_prefix=f"mt_strat_orig_{sel}") + render_monthly_table(sdf_e_sel,"Monthly Performance (Edited)", key_prefix=f"mt_strat_edit_{sel}") + else: + render_monthly_table(sdf, "Monthly Performance", key_prefix=f"mt_strat_{sel}") elif mode == "By Symbol": syms = sorted(df['symbol'].dropna().unique().tolist()) @@ -544,34 +732,198 @@ def render(): 'Expectancy' : stat['expectancy'], 'Max DD' : stat['max_drawdown'], }) - sdf_sum = __import__('pandas').DataFrame(rows).sort_values('Net Profit', ascending=False) + import pandas as pd + sdf_sum = pd.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 ) + _df_sym = df_e if (view_sel in ("Edited","Both") and df_e is not None) else df sel = st.selectbox("Select symbol for detail", syms) if sel: - sdf = df[df['symbol'] == sel] + sdf = _df_sym[_df_sym['symbol'] == sel] if sel in _df_sym['symbol'].values else df[df['symbol']==sel] stat = calc_stats(sdf) - render_stats(stat, sel) + sdf_e_sel = df_e[df_e['symbol']==sel] if (df_e is not None and sel in df_e['symbol'].values) else None + stats_e_sel = calc_stats(sdf_e_sel) if sdf_e_sel is not None and len(sdf_e_sel) else None + if view_sel == "Both" and stats_e_sel: + render_stats(stat, sel, stats_compare=stats_e_sel) + elif view_sel == "Edited" and stats_e_sel: + render_stats(stats_e_sel, f"{sel} (Edited)") + else: + 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) st.divider() - render_monthly_table(sdf, "Monthly Performance", key_prefix=f"mt_sym_{sel}") + if view_sel == "Both" and sdf_e_sel is not None and len(sdf_e_sel): + render_monthly_table(sdf, "Monthly Performance (Original)", key_prefix=f"mt_sym_orig_{sel}") + render_monthly_table(sdf_e_sel,"Monthly Performance (Edited)", key_prefix=f"mt_sym_edit_{sel}") + else: + render_monthly_table(sdf, "Monthly Performance", key_prefix=f"mt_sym_{sel}") elif mode == "By Day of Week": - render_dow_chart(df) - render_hour_chart(df) + _df_dow = df_e if (view_sel in ("Edited","Both") and df_e is not None) else df + render_dow_chart(_df_dow) + render_hour_chart(_df_dow) # ── Raw trade log ───────────────────────────────────────────────────────── st.divider() with st.expander("Raw Trade Log"): - show_cols = ['open_time', 'close_time', 'symbol', 'type', 'strategy', + edit_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] + edit_cols = [c for c in edit_cols if c in st.session_state['ta_df'].columns] + + # Show full dataset (not filtered) with trade index and Group column + df_edit = st.session_state['ta_df'][edit_cols].copy() + df_edit.insert(0, '#', range(1, len(df_edit) + 1)) + # Preserve existing Group column if already edited + existing_edited = st.session_state.get('ta_df_edited') + if existing_edited is not None and 'Group' in existing_edited.columns: + df_edit.insert(1, 'Group', existing_edited['Group'].values[:len(df_edit)]) + else: + df_edit.insert(1, 'Group', '') + + st.caption( + "Edit any cell then click **Update**. " + "Enter the same label in **Group** for trades to merge into one position. " + "**Reset** restores the original upload." + ) + bc1, bc2, bc3 = st.columns([1, 1, 6]) + do_update = bc1.button("✅ Update", type="primary", key="ta_log_update") + do_reset = bc2.button("↩️ Reset", key="ta_log_reset") + + edited = st.data_editor( + df_edit, + use_container_width=True, + hide_index=True, + height=400, + column_config={ + '#': st.column_config.NumberColumn('#', disabled=True, width='small'), + 'Group': st.column_config.TextColumn('Group', width='small', + help='Same label = merge into one trade on Update'), + 'open_time': st.column_config.DatetimeColumn('open_time', format='YYYY-MM-DD HH:mm:ss'), + 'close_time': st.column_config.DatetimeColumn('close_time', format='YYYY-MM-DD HH:mm:ss'), + 'symbol': st.column_config.TextColumn('symbol'), + 'type': st.column_config.SelectboxColumn('type', options=['buy','sell']), + 'strategy': st.column_config.TextColumn('strategy'), + 'volume': st.column_config.NumberColumn('volume', format='%.2f'), + 'open_price': st.column_config.NumberColumn('open_price', format='%.5f'), + 'close_price': st.column_config.NumberColumn('close_price', format='%.5f'), + 'profit': st.column_config.NumberColumn('profit', format='%.2f'), + 'net_profit': st.column_config.NumberColumn('net_profit', format='%.2f'), + }, + key='ta_log_editor' + ) + + if do_update: + import pandas as pd + upd = edited.drop(columns=['#']) + for col in ['open_time','close_time']: + if col in upd.columns: + upd[col] = pd.to_datetime(upd[col], errors='coerce') + for col in ['profit','net_profit','volume','open_price','close_price', + 'commission','swap','sl','tp','duration_min']: + if col in upd.columns: + upd[col] = pd.to_numeric(upd[col], errors='coerce') + + # ── Merge grouped trades ────────────────────────────────────── + upd_with_groups = upd.copy() # preserve Group labels for summary + groups = upd['Group'].fillna('').str.strip() + ungrouped = upd[groups == ''].drop(columns=['Group']) + grouped_rows = [] + for label, grp in upd[groups != ''].groupby(groups): + merged = { + 'open_time': grp['open_time'].min(), + 'close_time': grp['close_time'].max(), + 'symbol': grp['symbol'].iloc[0], + 'type': grp['type'].iloc[0], + 'strategy': grp['strategy'].iloc[0], + 'volume': grp['volume'].sum(), + 'open_price': grp['open_price'].iloc[0], + 'close_price': grp['close_price'].iloc[-1], + 'profit': grp['profit'].sum() if 'profit' in grp else 0, + 'net_profit': grp['net_profit'].sum(), + 'commission': grp['commission'].sum() if 'commission' in grp else 0, + 'swap': grp['swap'].sum() if 'swap' in grp else 0, + } + if 'sl' in grp: merged['sl'] = grp['sl'].iloc[0] + if 'tp' in grp: merged['tp'] = grp['tp'].iloc[0] + grouped_rows.append(merged) + + if grouped_rows: + df_grouped = pd.DataFrame(grouped_rows) + upd = pd.concat([ungrouped, df_grouped], ignore_index=True) + upd = upd.sort_values('open_time').reset_index(drop=True) + else: + upd = ungrouped + + upd['duration_min'] = ((upd['close_time'] - upd['open_time']) + .dt.total_seconds() / 60).round(1) + upd['win'] = upd['net_profit'] > 0 + upd['day_of_week'] = upd['open_time'].dt.day_name() + upd['hour'] = upd['open_time'].dt.hour + # Preserve non-editable columns + orig = st.session_state['ta_df'] + for col in orig.columns: + if col not in upd.columns: + upd[col] = orig[col].values[:len(upd)] + upd['comment'] = upd.get('comment', '') + upd['source'] = upd.get('source', 'manual') + st.session_state['ta_df_edited'] = upd + + # Build full position summary — grouped and ungrouped trades + summary_rows = [] + grp_labels = upd_with_groups['Group'].fillna('').str.strip() + + # Grouped trades first + for label, grp in upd_with_groups[grp_labels != ''].groupby(grp_labels[grp_labels != '']): + net = grp['net_profit'].sum() + summary_rows.append({ + 'Group': label, + 'Entries': len(grp), + 'Symbol': grp['symbol'].iloc[0], + 'Type': grp['type'].iloc[0], + 'Open Time': grp['open_time'].min(), + 'Close Time': grp['close_time'].max(), + 'Total Volume': round(grp['volume'].sum(), 2), + 'Net P&L ($)': round(net, 2), + 'Win': '✅' if net > 0 else '❌', + }) + + # Individual (ungrouped) trades + for _, row in upd_with_groups[grp_labels == ''].iterrows(): + net = row['net_profit'] + summary_rows.append({ + 'Group': '—', + 'Entries': 1, + 'Symbol': row['symbol'], + 'Type': row['type'], + 'Open Time': row['open_time'], + 'Close Time': row['close_time'], + 'Total Volume': round(row['volume'], 2), + 'Net P&L ($)': round(net, 2), + 'Win': '✅' if net > 0 else '❌', + }) + + # Sort by open time + summary_rows.sort(key=lambda r: r['Open Time'] if r['Open Time'] is not None else pd.Timestamp.min) + st.session_state['ta_group_summary'] = summary_rows if summary_rows else None + + n_merged = len(groups[groups != ''].unique()) + st.success( + f"Saved — {len(upd)} trades " + f"({n_merged} group(s) merged). " + "Select 'Edited' or 'Both' to compare." + ) + st.rerun() + + if do_reset: + st.session_state['ta_df_edited'] = None + st.session_state['ta_group_summary'] = None + st.success("Edited version cleared.") + st.rerun() def colour_net(val): try: @@ -582,13 +934,14 @@ def render(): 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.divider() + # Download uses edited version if available, otherwise original + _dl_df = st.session_state['ta_df_edited'] if st.session_state.get('ta_df_edited') is not None else st.session_state['ta_df'] + _dl_cols = [c for c in edit_cols if c in _dl_df.columns] + _dl_label = "Edited" if st.session_state.get('ta_df_edited') is not None else "Original" st.download_button( - "⬇ Download filtered trades CSV", - data = df[show_cols].to_csv(index=False), + f"⬇ Download {_dl_label} trades CSV", + data = _dl_df[_dl_cols].to_csv(index=False), file_name = f"mt5_trades_{date_from}_{date_to}.csv", mime = 'text/csv' ) \ No newline at end of file