From 2044d181f32132c42af4ef1538c8d6bdaa63bbb6 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Apr 2026 08:56:08 +1000 Subject: [PATCH] feat: trade analysis monthly table, equity overlays, chart fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add monthly performance table (year x month grid) with $ / % toggle and Initial Balance input alongside the toggle - Add equity curve overlay toggles: Account total, By Strategy, By Symbol, By Day of Week — checkboxes above each chart - Fix all chart backgrounds to transparent for light/dark compatibility - Fix chart grid and font colours to work on both themes - Monthly table and equity overlays added to Overall, By Strategy and By Symbol modes --- .../view_trade_analysis.cpython-314.pyc | Bin 28639 -> 34106 bytes view_trade_analysis.py | 118 ++++++++++++++---- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/__pycache__/view_trade_analysis.cpython-314.pyc b/__pycache__/view_trade_analysis.cpython-314.pyc index bb54cc6ec661df79aa994cf75bd2a5f28b8c5cd9..580fe7e335ce19801d07d02071a908bd13188706 100644 GIT binary patch delta 10281 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 delta 5803 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 P$Q!2*a7UF$ElvM7hj2G1 diff --git a/view_trade_analysis.py b/view_trade_analysis.py index 8ac3222..a8732ad 100644 --- a/view_trade_analysis.py +++ b/view_trade_analysis.py @@ -155,6 +155,9 @@ def render(): # ── Filters ─────────────────────────────────────────────────────────────── st.divider() + if 'ta_deposit' not in st.session_state: + st.session_state['ta_deposit'] = 10000.0 + fc1, fc2, fc3, fc4 = st.columns(4) with fc1: @@ -178,6 +181,7 @@ def render(): 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) & @@ -237,67 +241,57 @@ def render(): import pandas as pd df_s = df_plot.sort_values('close_time').copy() - # ── 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'] + safe_key = label.replace(" ","_").replace("/","_").replace("—","").strip("_") + ov = st.columns(4) + show_account = ov[0].checkbox("Account total", value=True, key=f"eq_acc_{safe_key}") + show_strategy = ov[1].checkbox("By Strategy", value=False, key=f"eq_str_{safe_key}") + show_symbol = ov[2].checkbox("By Symbol", value=False, key=f"eq_sym_{safe_key}") + show_dow = ov[3].checkbox("By Day of Week", value=False, key=f"eq_dow_{safe_key}") + fig = go.Figure() - # 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', + 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, + 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, + 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): + for i, dow in enumerate(['Monday','Tuesday','Wednesday','Thursday','Friday']): 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, + 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=360, - plot_bgcolor='rgba(0,0,0,0)', - paper_bgcolor='rgba(0,0,0,0)', + 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), @@ -305,7 +299,7 @@ def render(): legend=dict(bgcolor='rgba(0,0,0,0)', borderwidth=0), hovermode='x unified', ) - st.plotly_chart(fig, use_container_width=True, key=f"eq_fig_{label}") + st.plotly_chart(fig, use_container_width=True, key=f"eq_fig_{safe_key}") def render_dow_chart(df_plot): dow_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] @@ -327,7 +321,7 @@ def render(): 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)') + legend=dict(bgcolor='rgba(0,0,0,0)') ) st.plotly_chart(fig, use_container_width=True) @@ -353,10 +347,76 @@ def render(): 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)') + legend=dict(bgcolor='rgba(0,0,0,0)') ) st.plotly_chart(fig, use_container_width=True) + def render_monthly_table(df_plot, label="Monthly Performance", key_prefix="mt"): + import pandas as pd + if 'close_time' not in df_plot.columns or df_plot.empty: + return + tmp = df_plot[['close_time','net_profit']].dropna().copy() + tmp['year'] = pd.to_datetime(tmp['close_time']).dt.year + tmp['month'] = pd.to_datetime(tmp['close_time']).dt.month + monthly = tmp.groupby(['year','month'])['net_profit'].sum().reset_index() + if monthly.empty: + return + + pivot = monthly.pivot(index='year', columns='month', values='net_profit').fillna(0) + pivot.columns = [pd.Timestamp(2000, int(m), 1).strftime('%b') for m in pivot.columns] + pivot['YTD'] = pivot.sum(axis=1) + pivot = pivot.sort_index(ascending=False) + + # Deposit for % calc — use initial deposit from session state or fallback to first equity point + deposit = st.session_state.get('ta_deposit', 10000.0) + + tog1, tog2 = st.columns([2, 3]) + toggle = tog1.radio("Unit", ["$", "%"], horizontal=True, key=f"{key_prefix}_toggle") + deposit = tog2.number_input( + "Initial Balance ($)", min_value=100.0, max_value=10_000_000.0, + value=st.session_state.get('ta_deposit', 10000.0), + step=1000.0, format="%.2f", key=f"{key_prefix}_deposit", + help="Used for % calculations") + st.session_state['ta_deposit'] = deposit + + month_order = ['Jan','Feb','Mar','Apr','May','Jun', + 'Jul','Aug','Sep','Oct','Nov','Dec','YTD'] + cols_present = [c for c in month_order if c in pivot.columns] + display = pivot[cols_present].copy() + + if toggle == "%": + display = (display / deposit * 100).round(2) + + # Build HTML table with colour coding + def _cell(val, fmt): + if val > 0: bg = "rgba(52,194,122,0.18)"; fg = "#34C27A" + elif val < 0: bg = "rgba(220,80,80,0.18)"; fg = "#E05555" + else: bg = "transparent"; fg = "#888" + txt = f"{val:+.2f}{'%' if fmt=='%' else ''}" if val != 0 else "—" + return f'{txt}' + + rows = [] + for year, row in display.iterrows(): + cells = [f'{year}'] + for col in cols_present: + cells.append(_cell(row.get(col, 0), toggle)) + rows.append("" + "".join(cells) + "") + + hdr_cells = ["Year"] + for col in cols_present: + hdr_cells.append(f"{col}") + + html = ( + "
" + "" + "" + "".join(hdr_cells) + "" + "" + "".join(rows) + "" + "
" + ) + if label: + st.markdown(f"**{label}**") + st.markdown(html, unsafe_allow_html=True) + def colour_profit(val): try: v = float(str(val).replace(',', '')) @@ -376,6 +436,8 @@ def render(): render_dow_chart(df) with col2: render_hour_chart(df) + st.divider() + render_monthly_table(df, "Monthly Performance", key_prefix="mt_overall") elif mode == "By Strategy": strats = sorted(df['strategy'].dropna().unique().tolist()) @@ -414,6 +476,8 @@ def render(): 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}") elif mode == "By Symbol": syms = sorted(df['symbol'].dropna().unique().tolist()) @@ -445,6 +509,8 @@ def render(): 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}") elif mode == "By Day of Week": render_dow_chart(df)