MakeoverMonday: Risk Valuation Matrices

MakeoverMonday
Data Viz
Python
Finance
Risk
Six risk matrices — R-multiple vs winrate, drawdown vs recovery, expectancy, losing streaks, exponential growth, consecutive losses — with your trading data
Author

chokotto

Published

February 10, 2026

Overview

This MakeoverMonday builds six risk valuation matrices used in trading: R-multiple vs required winrate, drawdown vs recovery rate, expectancy, losing streaks and capital loss, exponential growth, and consecutive-loss probability. Where possible, metrics are computed from your own data (realized_pl and daily_balance).

  • 1. R-Multiple & Required Winrate: Break-even winrate = 1 / (1 + R).
  • 2. Drawdown & Recovery Rate: Recovery % = 100 × DD / (1 − DD).
  • 3. Expectancy: (Winrate × Size × R) − ((1 − Winrate) × Size).
  • 4. Losing Streaks: Capital loss % after n consecutive losses at given risk %.
  • 5. Exponential Growth: Gain per trade over time with fixed winrate, size, R.
  • 6. Consecutive Losses: Probability of k consecutive losses by winrate.

Data and risk metrics

Show code
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from pathlib import Path

# Resolve project root and risk_metrics path (from TidyTuesday prepare_data)
base_path = Path.cwd()
while not (base_path / "data").exists() and base_path.parent != base_path:
    base_path = base_path.parent

# Same repo: risk_metrics written by 2026-02-11-tidytuesday/prepare_data.py
risk_metrics_path = base_path / "scripts" / "by_timeSeries" / "quarto" / "posts" / "2026-02-11-tidytuesday" / "data" / "risk_metrics.parquet"
if risk_metrics_path.exists():
    m = pd.read_parquet(risk_metrics_path).iloc[0]
    win_rate = float(m["win_rate_pct"])
    r_multiple = float(m["r_multiple"])
    max_dd = float(m["max_drawdown_pct"])
    max_streak = int(m["max_consecutive_losing_days"])
    account_jpy = float(m["account_size_jpy"])
    pos_pct = float(m["position_size_pct"])
else:
    win_rate, r_multiple, max_dd, max_streak = 55.0, 1.5, 10.0, 3
    account_jpy, pos_pct = 10_000_000.0, 2.0

print(f"Win rate: {win_rate:.1f}% | R: {r_multiple:.2f} | Max DD: {max_dd:.1f}% | Max streak: {max_streak} | Account: ¥{account_jpy:,.0f}")
Win rate: 55.0% | R: 1.50 | Max DD: 10.0% | Max streak: 3 | Account: ¥10,000,000

1. R-Multiple and Required Winrate

Show code
R_values = np.arange(0.5, 4.0, 0.5)
required_wr = 100 / (1 + R_values)
fig1 = go.Figure()
fig1.add_trace(go.Bar(x=[f"{r:.1f}" for r in R_values], y=required_wr, name="Required Winrate %", marker_color="#3b82f6"))
fig1.add_vline(x=r_multiple, line_dash="dash", line_color="red", annotation_text=f"Your R ≈ {r_multiple:.2f}")
fig1.update_layout(
    title="R-Multiple vs Required Winrate",
    xaxis_title="R-Multiple",
    yaxis_title="Required Winrate (%)",
    template="plotly_white",
    height=400,
    showlegend=False,
)
fig1.add_annotation(text="Formula: Required Winrate = 1 / (1 + R)", xref="paper", yref="paper", x=0.5, y=1.06, showarrow=False, font=dict(size=11))
fig1.show()

2. Drawdown and Recovery Rate

Show code
dd_pcts = [5, 10, 20, 30, 40, 50, 60, 70, 80, 90]
recovery = [100 * dd / (100 - dd) for dd in dd_pcts]
fig2 = go.Figure()
fig2.add_trace(go.Bar(x=[f"{d}%" for d in dd_pcts], y=recovery, marker_color="#10b981"))
# Mark your max DD if in range
idx = min(range(len(dd_pcts)), key=lambda i: abs(dd_pcts[i] - max_dd))
fig2.add_vline(x=idx, line_dash="dash", line_color="red", annotation_text=f"Your max DD ≈ {max_dd:.0f}%")
fig2.update_layout(
    title="Drawdown % vs Recovery Rate %",
    xaxis_title="Drawdown (%)",
    yaxis_title="Recovery needed (%)",
    template="plotly_white",
    height=400,
    showlegend=False,
)
fig2.add_annotation(text="Recovery % = 100 × DD / (1 − DD)", xref="paper", yref="paper", x=0.5, y=1.06, showarrow=False, font=dict(size=11))
fig2.show()

3. Expectancy

Show code
# Expectancy = (WR * pos * R) - ((1-WR) * pos)  in % terms
wr_dec = win_rate / 100
exp_pct = (wr_dec * pos_pct * r_multiple) - ((1 - wr_dec) * pos_pct)
fig3 = go.Figure()
fig3.add_annotation(
    text=f"<b>Expectancy</b> = (Winrate × Pos.Size × R) − ((1 − Winrate) × Pos.Size)<br>"
         f"With your data: Winrate={win_rate:.0f}%, Pos={pos_pct}%, R={r_multiple:.2f} → <b>Expectancy = {exp_pct:.2f}%</b> per trade",
    xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=13), align="center",
)
fig3.update_layout(
    title="Trade Expectancy (your parameters)",
    template="plotly_white",
    height=220,
    xaxis=dict(visible=False),
    yaxis=dict(visible=False),
)
fig3.show()

4. Losing Streaks and Loss of Capital

Show code
n_losses = list(range(1, 11))
risks = [0.01, 0.03, 0.05]
loss_pct = [[100 * (1 - (1 - r) ** n) for n in n_losses] for r in risks]
fig4 = go.Figure()
for r, row in zip(risks, loss_pct):
    fig4.add_trace(go.Scatter(x=n_losses, y=row, mode="lines+markers", name=f"{int(r*100)}% risk"))
fig4.add_vline(x=max_streak, line_dash="dot", line_color="gray", annotation_text=f"Your max streak: {max_streak}")
fig4.update_layout(
    title="Losing Streaks: Capital Loss % after n Consecutive Losses",
    xaxis_title="Consecutive losses (n)",
    yaxis_title="Loss %",
    template="plotly_white",
    height=400,
)
fig4.add_annotation(text="Loss % = 1 − (1 − risk)^n", xref="paper", yref="paper", x=0.5, y=1.06, showarrow=False, font=dict(size=11))
fig4.show()

5. Exponential Growth (Gain per Trade)

Show code
# Compound: after n trades, account grows; gain per trade at step n = (account_n - account_0) / n (simplified)
# Standard formula: account after n = A0 * (1 + E)^n where E = expectancy per trade in decimal
E = exp_pct / 100
n_trades = [1, 10, 50, 100, 200, 500]
account_0 = account_jpy if account_jpy > 0 else 10_000_000
gain_per_trade = [account_0 * ((1 + E) ** n - 1) / n for n in n_trades]
fig5 = go.Figure()
fig5.add_trace(go.Bar(x=[str(n) for n in n_trades], y=[g / 1e6 for g in gain_per_trade], marker_color="#8b5cf6"))
fig5.update_layout(
    title=f"Gain per Trade (JPY, millions) — Account ¥{account_0/1e6:.1f}M, Expectancy {exp_pct:.2f}%",
    xaxis_title="Number of trades",
    yaxis_title="Gain per trade (million JPY)",
    template="plotly_white",
    height=400,
    showlegend=False,
)
fig5.show()

6. Consecutive Losses (Probability Heatmap)

Show code
winrates = np.arange(30, 75, 5)
k_streaks = [1, 2, 3, 4, 5]
z = [[100 * (1 - wr/100)**k for k in k_streaks] for wr in winrates]
fig6 = go.Figure(data=go.Heatmap(
    z=z, x=[f"{k}" for k in k_streaks], y=[f"{int(wr)}%" for wr in winrates],
    colorscale="RdYlGn_r", text=[[f"{v:.1f}%" for v in row] for row in z], texttemplate="%{text}", textfont=dict(size=10),
))
fig6.add_hline(y=np.argmin(np.abs(winrates - win_rate)), line_dash="dash", line_color="blue")
fig6.update_layout(
    title="P(k consecutive losses) by Winrate — your winrate highlighted",
    xaxis_title="Consecutive losses (k)",
    yaxis_title="Winrate (%)",
    template="plotly_white",
    height=450,
)
fig6.show()

Key Takeaways

  1. R and winrate: Higher R lowers the winrate needed to break even.
  2. Drawdown: Recovery needed grows non-linearly (e.g. 50% DD → 100% gain to recover).
  3. Expectancy: Combines winrate, position size, and R into one per-trade edge.
  4. Streaks: A few consecutive losses at 3–5% risk can erase a large share of capital.
  5. Growth: Positive expectancy compounds; gain per trade rises with trade count.
  6. Consecutive losses: Even at 60% winrate, 3+ consecutive losses are non-negligible.

This post is part of the MakeoverMonday weekly data visualization project.

CautionDisclaimer

This analysis is for educational and practice purposes only. Risk metrics and interpretations are based on the provided dataset and do not constitute investment advice.