From 413d9bb3f4d5b3743f9a0edf25472afb667a0307 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Apr 2026 21:10:16 +1000 Subject: [PATCH] fix: mt5_parser, trade analysis charts and batch backtest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mt5_parser: - Remove deprecated infer_datetime_format (removed in pandas 2.0) - Add ISO datetime format fallback for CSV imports (YYYY-MM-DD) - Fix missing comment column in _enrich causing KeyError on plain CSVs Trade Analysis: - Add daily P&L bar chart below equity curve - Add drawdown panel below daily P&L - Fix NaT in date filter when open_time has null values Batch Backtest: - Fix read_utf16/write_utf16 to detect and preserve original encoding - Fix _clear_use_default_flags to clear bit 2 on all parameters - Fix snapshot approach for report finding — before/after file scan - Fix cleanup to remove all new htm files after each run - Fix report selection to prefer named file over newest fallback - Apply all encoding and flag fixes to CLI script mt5_batch_backtest.py --- __pycache__/mt5_parser.cpython-314.pyc | Bin 19721 -> 19940 bytes .../view_trade_analysis.cpython-314.pyc | Bin 34106 -> 36462 bytes mt5_parser.py | 13 +- trade_analysis.py | 333 ------------------ view_trade_analysis.py | 53 ++- 5 files changed, 60 insertions(+), 339 deletions(-) delete mode 100644 trade_analysis.py diff --git a/__pycache__/mt5_parser.cpython-314.pyc b/__pycache__/mt5_parser.cpython-314.pyc index 0a703eed93ad1ec13c878e7afe1252698a976a61..73a59fac17c07da973a0e141736cc4a8e09306d4 100644 GIT binary patch delta 795 zcmajbUr19?90%}o?*6(z_s;I_w(I7c!)@x_-E_J%#RvQ2s%gn>nS+vy2^vIWOA^Ig z6fyB3^E{aLU@t;JKE$!ID8iSBB$zeOLNcP)z$EISf{r)qrH9VLIiGX*{(e6=qto!? z4M+>LQ3uF!G9UUD8}HK3RH{2F05YgpU50%qI?%dk!I#Pp!4cnKcnV7T0FcUM8Hg%m8Hp-o(L{nQhR83=L?p^8BnrsdK@^n5 zN@j@-P^2?Q-m2s`3sJQ!D^W<6%|=)wlP9W`RYVj?lb{1M9dIdoRE zp`${zGHHkz(74u&GEG`^Uno;0T;!Y30imkUDLJI#I>px4lEQy-CEGcxLn^7G{-okB zVtOyRM4#evzYpr`$lNd~H^yIY&kfj{{6)XxTRR!@w>&7K+}RnNe= zRCk&tBfKtjKIGG^5vE-VJR5okM>y+b*F@LN-LsKgZ6w#;ndjo!mQ{wkIy5>oHoU^P zmKoOy<9@}s*LeGkbJ}@}n$zU!qq*4eJbz+b{lDmfHqSScn8lGzk3~K+z$9##fz~{p znjE|~m`87FZ&Q0HbU9qgL~FpJqS@POP%hQ5tp#{K+(|(%Hr9XA!)E+_&qXyH!ovqk oA$)*lq9VM4Uq*G1cS3rz1q#r*c@iFjg7YZM5>g08yyanE(I) delta 659 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 DZ10V# delta 3201 zcmd6pe^gWF8OPuECYR*q5(G_1NCJWIGZ09~4}OS>;-D~uhFiS?vc_PdwIvX5WPp%l zUETV_+3xskJ3Bp|?euJ?JFczMrakTHoKjc+nA>r6Zqt*^wH~*1p1GY(TUt=r-ZzMn z+U@l0pEaEG$#cK&^ZvNc^Szh+{UGzkQ%qJ>mRg0#=k%#R>@)?BW*y=1k;Dn^exC8J zhn`giO;{%`7i=~IO0Q_a$MbZ!0M_KW2x(jhPm#Eg#_(j`a(FM#0zV;f zp&^PbP*_sw6uu<1>(CGm7YmOH9jT~9_`D8|B%+C-u z2wtIQvF>KnD(`9%EyCv-dJ$@60xJ8rn(CmKZN=ALzTYw%v^v;;=)k_WA&CUJ>6Y)(D$KhafJUC2S^2Z<^9a zl>Rhj3sDBrlrIqF!8FB3l&xuspD2TO6+J@2HX;S?N<;~Y4p0?z;%aEORH<)W`2zhPQ-Sd=Wh;A4!_24=JswYJ4{FT-`9kh#cfJ zc4242*LWxcyAs^m0VYGh#mMSTTyGm>;G3OK;#@Fqc$Kl{fTrtFMTFcRDl3CmySf$s zAYtT}vV^Mpj7)Z3=}o-*z_W6E7$$qam6=qo8uEt*1O7EhCrocCgoeK5*aMw?QdTlE zc6RMn=Pf6G@qW z$Bv{z3ho>Z1jnUmntYug$u0=5QQ>upH>fIW@k`QA>E$tshbi8o1H2TB1b6!uCh4eb zqB0lo@Ahw({Gp&#DVGHM>XZx&f$nJFfx`@4UI%1!Bi}rfdgB* zu_JM5%OGndPdkzn6l#i#6z@>HOL2+fGQ}TZ$gk)AM8iK59O%!3=luoS-=o4`D6SA} zl>SQCEzz1w*9fd?=|dWSL_u#~nx&YdNUwl&gKD2p0V^w=g37@HdrN{7KVq@fc z@~l_L5{DQmpAO?Mf?|9n{ywyfKaN+!>G6i#59kj46g?EFa?c>pPYmSbkSSK2%!rH* zl0HjU5qxFhs!pH>d4M8HCRnkwk;WoCv8Ns{gXujFGJiu*IB6$!fS$=M_$7F0(uGxU zX>uQ5M^jTb+W=!zJ@`%d+0?_h9^BEwili*Gb2up{<_VI}Kb}<4khm*6x?4I<&z7pq zG)zXDaTEMFYE&esdJDnDsFN&`9dnZ(KCQ7DJO;aC_3(OZQu}L~!O&fm6x|fP&>qjn z?XW#wo7GB%tpt&Jk_kuSHkgj{a50{xdWER;_e*4J;)A$X?$H>&-Sitx`FTy%SxwbB zP4#(=^Q^{sPUC(;dEq~fL40q029H$g53>`;_MX6sHEa*Qww6ge_SMUBrY8#?+V>33 zhL85;Gi#Ng`J9FT1#?k-eJBMeXd>S}rDIudn1{I`(}n7pr1tG+bQH&g8F% zH7jR48I(=ynus)u_=%i2H>=?%O1aqzevd&m>oV=p%I35ziRbe9Nw;jSVjZ5e%Wl|N za&e=cpHgx+d49^o-8A!44(_IlkIK1Q3O=gmZW;Ngjk{&%V~CrV@i7fIuj6AC+ 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(