feat: trade analysis overhaul — editing, grouping, comparison views
Trade Analysis: - Add editable raw trade log with # index and Group column - Group column merges multiple entries into one position on Update - Reset button clears edited version back to original upload - View selector: Original / Edited / Both — appears after first Update - Both mode: stats show with coloured delta arrows vs edited version - Both mode: equity, drawdown and daily P&L charts overlay both versions - Both mode: strategy comparison table shows inline delta values - Position Summary expander above monthly table — all positions listed (grouped with entry count, individual as single rows) sorted by open time - Download exports edited version when available, original otherwise - Trade # multiselect filter to show specific trades - All charts and tables (DOW, hour, monthly) respect view selection - Fix: Group column positioned second after # index - Fix: DataFrame truth value error in download button - Fix: Both mode now uses edited df for DOW/hour/monthly charts mt5_parser: - Add trading_days and trades_per_day to calc_stats - Add ISO datetime format fallback for CSV imports - Fix missing comment column KeyError in _enrich - Fix NaT in date filter when open_time has null values
This commit is contained in:
@@ -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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
|
||||
+460
-107
@@ -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'
|
||||
)
|
||||
Reference in New Issue
Block a user