Trading Win Rate Calculator

Sometimes obvious things are not obvious, hence Trading Win Rate Calculator

Save following code as .html and open in a browser.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Trading Win Rate Calculator</title>

  <style>
    :root {
      --bg: #f4f6f8;
      --card: #ffffff;
      --border: #d9dee5;
      --text: #17202a;
      --muted: #687381;
      --blue: #2563eb;
      --green: #08794d;
      --red: #b42333;
      --yellow: #fff7d6;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      background: var(--bg);
      color: var(--text);
      font-family: Arial, Helvetica, sans-serif;
    }

    .page {
      width: min(1450px, calc(100% - 32px));
      margin: 28px auto 40px;
    }

    h1 {
      margin: 0 0 8px;
      font-size: clamp(24px, 3vw, 36px);
      letter-spacing: -0.5px;
    }

    .subtitle {
      margin: 0 0 20px;
      color: var(--muted);
      line-height: 1.5;
    }

    .controls {
      display: grid;
      grid-template-columns: repeat(4, minmax(150px, 1fr));
      gap: 14px;
      margin-bottom: 18px;
    }

    .control,
    .summary,
    .matrix-card {
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 10px;
      box-shadow: 0 2px 8px rgba(20, 30, 40, 0.04);
    }

    .control {
      padding: 14px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      color: var(--muted);
      font-size: 13px;
      font-weight: 700;
    }

    .input-row {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    input[type="number"],
    select {
      width: 100%;
      min-width: 0;
      padding: 9px 10px;
      border: 1px solid #c7ced8;
      border-radius: 6px;
      color: var(--text);
      background: #fff;
      font-size: 15px;
    }

    input[type="range"] {
      width: 100%;
      margin-top: 9px;
      accent-color: var(--blue);
    }

    .unit {
      color: var(--muted);
      font-size: 13px;
      white-space: nowrap;
    }

    .checkbox-control {
      display: flex;
      align-items: center;
      gap: 8px;
      min-height: 84px;
    }

    .checkbox-control label {
      margin: 0;
      color: var(--text);
      cursor: pointer;
    }

    .checkbox-control input {
      width: 17px;
      height: 17px;
      accent-color: var(--blue);
      cursor: pointer;
    }

    .summary {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      margin-bottom: 18px;
      overflow: hidden;
    }

    .stat {
      padding: 16px 18px;
      border-right: 1px solid var(--border);
    }

    .stat:last-child {
      border-right: 0;
    }

    .stat-label {
      color: var(--muted);
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .05em;
    }

    .stat-value {
      margin-top: 7px;
      font-size: 20px;
      font-weight: 700;
    }

    .positive {
      color: var(--green);
    }

    .negative {
      color: var(--red);
    }

    .matrix-card {
      padding: 18px;
    }

    .matrix-heading {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 14px;
    }

    .matrix-heading h2 {
      margin: 0;
      font-size: 18px;
    }

    .legend {
      display: flex;
      align-items: center;
      gap: 12px;
      color: var(--muted);
      font-size: 12px;
    }

    .legend span {
      display: inline-flex;
      align-items: center;
      gap: 5px;
    }

    .swatch {
      width: 13px;
      height: 13px;
      border-radius: 3px;
      border: 1px solid rgba(0, 0, 0, .08);
    }

    .swatch.profit {
      background: #c9f1df;
    }

    .swatch.loss {
      background: #ffd3d8;
    }

    .swatch.break-even {
      background: var(--yellow);
    }

    .matrix-scroll-controls {
      display: grid;
      grid-template-columns: 1fr 220px;
      gap: 16px;
      align-items: center;
      margin-bottom: 10px;
      padding: 10px 12px;
      background: #f8fafb;
      border: 1px solid var(--border);
      border-radius: 7px;
    }

    .matrix-scroll-controls label {
      display: flex;
      align-items: center;
      gap: 10px;
      margin: 0;
      color: var(--muted);
      font-size: 12px;
      white-space: nowrap;
    }

    .matrix-scroll-controls input {
      width: 100%;
      margin: 0;
    }

    .table-wrap {
      max-height: 70vh;
      overflow: auto;
      border: 1px solid var(--border);
      border-radius: 7px;
    }

    table {
      width: max-content;
      min-width: 100%;
      border-collapse: collapse;
      table-layout: fixed;
      font-size: 12px;
    }

    th,
    td {
      min-width: 92px;
      height: 54px;
      padding: 6px 8px;
      border-right: 1px solid var(--border);
      border-bottom: 1px solid var(--border);
      text-align: center;
      white-space: nowrap;
    }

    th {
      background: #eef2f6;
      color: #394553;
      font-weight: 700;
    }

    th:first-child,
    td:first-child {
      position: sticky;
      left: 0;
      z-index: 2;
      width: 145px;
      min-width: 145px;
    }

    th:first-child {
      z-index: 3;
      background: #e6ebf1;
    }

    td:first-child {
      background: #f8fafb;
      text-align: left;
      font-weight: 700;
    }

    tr:last-child td {
      border-bottom: 0;
    }

    th:last-child,
    td:last-child {
      border-right: 0;
    }

    td.result {
      font-variant-numeric: tabular-nums;
      font-weight: 700;
      line-height: 1.35;
    }

    td.result.profit {
      background: #e1f8ed;
      color: var(--green);
    }

    td.result.loss {
      background: #ffe8eb;
      color: var(--red);
    }

    td.result.break-even {
      background: var(--yellow);
      color: #795f00;
    }

    .dollars {
      font-size: 12px;
      letter-spacing: -0.2px;
    }

    .points {
      color: inherit;
      opacity: .75;
      font-size: 11px;
      font-weight: 400;
    }

    small {
      color: var(--muted);
      font-size: 10px;
      font-weight: 400;
    }

    .formula {
      margin: 14px 2px 0;
      color: var(--muted);
      font-size: 13px;
      line-height: 1.6;
    }

    code {
      padding: 2px 5px;
      border-radius: 4px;
      background: #eef2f6;
      color: #364152;
    }

    @media (max-width: 1150px) {
      .controls {
        grid-template-columns: repeat(3, minmax(150px, 1fr));
      }

      .summary {
        grid-template-columns: repeat(3, 1fr);
      }

      .stat:nth-child(3) {
        border-right: 0;
      }

      .stat:nth-child(-n+3) {
        border-bottom: 1px solid var(--border);
      }
    }

    @media (max-width: 700px) {
      .page {
        width: calc(100% - 18px);
        margin-top: 18px;
      }

      .controls {
        grid-template-columns: repeat(2, minmax(140px, 1fr));
      }

      .summary {
        grid-template-columns: repeat(2, 1fr);
      }

      .stat:nth-child(2),
      .stat:nth-child(4) {
        border-right: 0;
      }

      .stat:nth-child(3) {
        border-right: 1px solid var(--border);
      }

      .stat:nth-child(4) {
        border-bottom: 1px solid var(--border);
      }

      .matrix-card {
        padding: 10px;
      }

      .matrix-heading {
        align-items: flex-start;
        flex-direction: column;
      }

      .matrix-scroll-controls {
        grid-template-columns: 1fr;
        gap: 8px;
      }
    }

    @media (max-width: 450px) {
      .controls {
        grid-template-columns: 1fr;
      }

      .summary {
        grid-template-columns: 1fr 1fr;
      }

      .stat {
        padding: 13px;
      }

      .stat-value {
        font-size: 18px;
      }
    }
  </style>
</head>

<body>
  <main class="page">
    <h1>Trading Win Rate Calculator</h1>

    <p class="subtitle">
      Compare expected results for different profit targets and stop-loss sizes,
      including fees, fractional points, and dollar values.
    </p>

    <section class="controls">
      <div class="control">
        <label for="winRate">Winning trades</label>

        <div class="input-row">
          <input id="winRate" type="number" min="0" max="100" step="1" value="50">
          <span class="unit">%</span>
        </div>

        <input id="winRateSlider" type="range" min="0" max="100" step="1" value="50">
      </div>

      <div class="control">
        <label for="tradeCount">Number of trades</label>

        <div class="input-row">
          <input id="tradeCount" type="number" min="1" step="1" value="100">
          <span class="unit">trades</span>
        </div>
      </div>

      <div class="control">
        <label for="maxProfit">Maximum profit target</label>

        <div class="input-row">
          <input id="maxProfit" type="number" min="0.01" step="0.25" value="10">
          <span class="unit">points</span>
        </div>
      </div>

      <div class="control">
        <label for="maxLoss">Maximum stop loss</label>

        <div class="input-row">
          <input id="maxLoss" type="number" min="0.01" step="0.25" value="10">
          <span class="unit">points</span>
        </div>
      </div>

      <div class="control">
        <label for="ticksPerPoint">Ticks per point</label>

        <select id="ticksPerPoint">
          <option value="1">1 tick</option>
          <option value="2">2 ticks</option>
          <option value="4" selected>4 ticks</option>
          <option value="8">8 ticks</option>
          <option value="10">10 ticks</option>
          <option value="20">20 ticks</option>
        </select>
      </div>

      <div class="control">
        <label for="pointValue">Dollar value per point</label>

        <div class="input-row">
          <input id="pointValue" type="number" min="0.01" step="0.01" value="50">
          <span class="unit">$</span>
        </div>
      </div>

      <div class="control">
        <label for="feePerTrade">Fees per trade</label>

        <div class="input-row">
          <input id="feePerTrade" type="number" min="0" step="0.01" value="0">
          <span class="unit">$</span>
        </div>
      </div>

      <div class="control checkbox-control">
        <input id="showPoints" type="checkbox" checked>
        <label for="showPoints">Show point equivalent</label>
      </div>
    </section>

    <section class="summary">
      <div class="stat">
        <div class="stat-label">Win rate</div>
        <div id="summaryWinRate" class="stat-value">50%</div>
      </div>

      <div class="stat">
        <div class="stat-label">Best result</div>
        <div id="bestResult" class="stat-value positive">$0</div>
      </div>

      <div class="stat">
        <div class="stat-label">Best points</div>
        <div id="bestPoints" class="stat-value positive">0.00 pts</div>
      </div>

      <div class="stat">
        <div class="stat-label">Best per trade</div>
        <div id="expectedPerTrade" class="stat-value">$0</div>
      </div>

      <div class="stat">
        <div class="stat-label">Break-even at 1:1</div>
        <div id="breakEvenRate" class="stat-value">50.00%</div>
      </div>
    </section>

    <section class="matrix-card">
      <div class="matrix-heading">
        <h2>Net result after all trades</h2>

        <div class="legend">
          <span><i class="swatch profit"></i> Profit</span>
          <span><i class="swatch loss"></i> Loss</span>
          <span><i class="swatch break-even"></i> Break-even</span>
        </div>
      </div>

      <div class="matrix-scroll-controls">
        <label>
          Horizontal scroll
          <input id="horizontalSlider" type="range" min="0" max="0" value="0">
        </label>

        <label>
          Vertical scroll
          <input id="verticalSlider" type="range" min="0" max="0" value="0">
        </label>
      </div>

      <div class="table-wrap">
        <table id="resultsTable">
          <thead></thead>
          <tbody></tbody>
        </table>
      </div>

      <p class="formula">
        Gross points per trade:
        <code>(win rate × profit points) − (loss rate × loss points)</code><br>

        Net dollars per trade:
        <code>(gross points × dollar value per point) − fee per trade</code><br>

        Dollar values are rounded to whole dollars for display, while calculations retain cents internally.
      </p>
    </section>
  </main>

  <script>
    const $ = id => document.getElementById(id);

    const winRate = $("winRate");
    const winRateSlider = $("winRateSlider");
    const tradeCount = $("tradeCount");
    const maxProfit = $("maxProfit");
    const maxLoss = $("maxLoss");
    const ticksPerPoint = $("ticksPerPoint");
    const pointValue = $("pointValue");
    const feePerTrade = $("feePerTrade");
    const showPoints = $("showPoints");

    const summaryWinRate = $("summaryWinRate");
    const bestResult = $("bestResult");
    const bestPoints = $("bestPoints");
    const expectedPerTrade = $("expectedPerTrade");
    const breakEvenRate = $("breakEvenRate");

    const tableWrap = document.querySelector(".table-wrap");
    const tableHead = document.querySelector("#resultsTable thead");
    const tableBody = document.querySelector("#resultsTable tbody");

    const horizontalSlider = $("horizontalSlider");
    const verticalSlider = $("verticalSlider");

    function positiveNumber(input, fallback) {
      const number = Number(input.value);
      return Number.isFinite(number) && number > 0
        ? number
        : fallback;
    }

    function decimalsForStep(step) {
      if (step >= 1) return 0;
      if (step >= 0.1) return 1;
      return 2;
    }

    function formatNumber(value, decimals = 2) {
      return Number(value).toLocaleString(undefined, {
        minimumFractionDigits: decimals,
        maximumFractionDigits: decimals
      });
    }

    // Dollar values are rounded to whole dollars for display.
    function formatDollars(value) {
      const rounded = Math.round(value);

      return `${rounded < 0 ? "-$" : "$"}${Math.abs(rounded).toLocaleString()}`;
    }

    function formatSignedPoints(value) {
      return `${value > 0 ? "+" : ""}${formatNumber(value, 2)} pts`;
    }

    function updateScrollSliders() {
      const horizontalMaximum = Math.max(
        0,
        tableWrap.scrollWidth - tableWrap.clientWidth
      );

      const verticalMaximum = Math.max(
        0,
        tableWrap.scrollHeight - tableWrap.clientHeight
      );

      horizontalSlider.max = horizontalMaximum;
      verticalSlider.max = verticalMaximum;

      horizontalSlider.value = Math.min(
        tableWrap.scrollLeft,
        horizontalMaximum
      );

      verticalSlider.value = Math.min(
        tableWrap.scrollTop,
        verticalMaximum
      );

      horizontalSlider.disabled = horizontalMaximum === 0;
      verticalSlider.disabled = verticalMaximum === 0;
    }

    horizontalSlider.addEventListener("input", () => {
      tableWrap.scrollLeft = Number(horizontalSlider.value);
    });

    verticalSlider.addEventListener("input", () => {
      tableWrap.scrollTop = Number(verticalSlider.value);
    });

    tableWrap.addEventListener("scroll", updateScrollSliders);
    window.addEventListener("resize", updateScrollSliders);

    function update() {
      const rate = Math.min(
        100,
        Math.max(0, Number(winRate.value) || 0)
      );

      const trades = Math.max(
        1,
        Math.floor(positiveNumber(tradeCount, 100))
      );

      const profitLimit = positiveNumber(maxProfit, 10);
      const lossLimit = positiveNumber(maxLoss, 10);
      const ticks = Math.max(1, Number(ticksPerPoint.value));
      const dollarsPerPoint = positiveNumber(pointValue, 50);
      const fee = Math.max(0, Number(feePerTrade.value) || 0);

      winRate.value = rate;
      winRateSlider.value = rate;

      const winFraction = rate / 100;
      const lossFraction = 1 - winFraction;

      const profitSteps = Math.max(
        1,
        Math.round(profitLimit * ticks)
      );

      const lossSteps = Math.max(
        1,
        Math.round(lossLimit * ticks)
      );

      const pointIncrement = 1 / ticks;
      const pointDecimals = decimalsForStep(pointIncrement);

      let bestDollarResult = -Infinity;
      let bestPointResult = 0;

      tableHead.innerHTML = `
        <tr>
          <th scope="col">
            Stop loss ↓<br>
            Profit target →
          </th>

          ${Array.from({ length: profitSteps }, (_, index) => {
            const points = (index + 1) * pointIncrement;
            const tickText = ticks > 1
              ? `<br><small>${index + 1} tick${index === 0 ? "" : "s"}</small>`
              : "";

            return `
              <th scope="col">
                ${formatNumber(points, pointDecimals)} pt
                ${tickText}
              </th>
            `;
          }).join("")}
        </tr>
      `;

      tableBody.innerHTML = "";

      for (let lossIndex = 1; lossIndex <= lossSteps; lossIndex++) {
        const lossPoints = lossIndex * pointIncrement;
        const row = document.createElement("tr");

        row.innerHTML = `
          <td>
            ${formatNumber(lossPoints, pointDecimals)} pt
            ${ticks > 1
              ? `<br><small>${lossIndex} tick${lossIndex === 1 ? "" : "s"}</small>`
              : ""}
          </td>
        `;

        for (
          let profitIndex = 1;
          profitIndex <= profitSteps;
          profitIndex++
        ) {
          const profitPoints = profitIndex * pointIncrement;

          const grossPointsPerTrade =
            (winFraction * profitPoints) -
            (lossFraction * lossPoints);

          const netDollarsPerTrade =
            (grossPointsPerTrade * dollarsPerPoint) - fee;

          const totalPoints = grossPointsPerTrade * trades;
          const totalDollars = netDollarsPerTrade * trades;

          if (totalDollars > bestDollarResult) {
            bestDollarResult = totalDollars;
            bestPointResult = totalPoints;
          }

          const cell = document.createElement("td");
          cell.className = "result";

          if (totalDollars > 0.005) {
            cell.classList.add("profit");
          } else if (totalDollars < -0.005) {
            cell.classList.add("loss");
          } else {
            cell.classList.add("break-even");
          }

          const pointLine = showPoints.checked
            ? `<div class="points">${formatSignedPoints(totalPoints)}</div>`
            : "";

          cell.innerHTML = `
            <div class="dollars">${formatDollars(totalDollars)}</div>
            ${pointLine}
          `;

          cell.title =
            `Profit target: ${formatNumber(profitPoints, pointDecimals)} points\n` +
            `Stop loss: ${formatNumber(lossPoints, pointDecimals)} points\n` +
            `Net per trade: ${formatDollars(netDollarsPerTrade)}\n` +
            `Total points: ${formatSignedPoints(totalPoints)}`;

          row.appendChild(cell);
        }

        tableBody.appendChild(row);
      }

      /*
        Break-even win rate for a 1:1 trade after fees:

        win rate × point
        - loss rate × point
        - fee / dollar value per point = 0
      */
      const oneToOneBreakEven =
        ((1 + fee / dollarsPerPoint) / 2) * 100;

      const bestPerTrade =
        bestDollarResult / trades;

      summaryWinRate.textContent = `${rate}%`;

      bestResult.textContent = formatDollars(bestDollarResult);
      bestResult.className =
        `stat-value ${bestDollarResult >= 0 ? "positive" : "negative"}`;

      bestPoints.textContent = formatSignedPoints(bestPointResult);
      bestPoints.className =
        `stat-value ${bestPointResult >= 0 ? "positive" : "negative"}`;

      expectedPerTrade.textContent =
        `${bestPerTrade >= 0 ? "+" : ""}${formatDollars(bestPerTrade)}`;

      expectedPerTrade.className =
        `stat-value ${bestPerTrade >= 0 ? "positive" : "negative"}`;

      breakEvenRate.textContent =
        `${formatNumber(oneToOneBreakEven, 2)}%`;

      requestAnimationFrame(updateScrollSliders);
    }

    winRate.addEventListener("input", update);

    winRateSlider.addEventListener("input", () => {
      winRate.value = winRateSlider.value;
      update();
    });

    [
      tradeCount,
      maxProfit,
      maxLoss,
      ticksPerPoint,
      pointValue,
      feePerTrade,
      showPoints
    ].forEach(input => {
      input.addEventListener("input", update);
      input.addEventListener("change", update);
    });

    update();
  </script>
</body>
</html>

2 Likes

This is Pretty cool to have as a tool. can we use the code ?

Of course, use it as you will. Btw, there’s a formatting bug where two 0.25 ticks show as .3 pts…

1 Like

The .3 display comes from decimalsForStep(): a 0.25 increment enters the step >= 0.1 branch and gets only one decimal, so 0.25 and 0.75 are rounded. The clean fix is to derive precision from the tick increment, or return two decimals whenever the increment is 0.25.

There is one more calculation boundary worth fixing. The current 1:1 break-even formula assumes a one-point stop and target. For equal distance d, dollar value V, and round-turn fee c, break-even is 50% + c / (2 × d × V). For an unequal target W and stop L, it is (L + c / V) / (W + L). That keeps the percentage correct when the user changes the point distance.

1 Like