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>
