"What is my average price?" sounds like a one line problem. It is, but the obvious one liner is wrong, and the bug is common enough to be worth a short post.
Disclosure: I am the founder of copy.top. We publish a free dca and average price calculator, and this post explains the math behind that kind of tool. The code below is a minimal version written for this article.
The bug
You buy twice, 100 dollars each time: once at 1.00 and once at 0.50.
The tempting answer is the mean of the two prices:
(1.0 + 0.5) / 2; // 0.75, wrong It is wrong because you did not buy the same number of coins each time. The first buy got 100 coins, the second got 200. You hold 300 coins for 200 dollars, so the average price is 200 / 300, about 0.667.
When each buy is a fixed amount of money, the average price is the harmonic mean of the prices, not the arithmetic mean. You never need to remember that if you always compute it the same way:
average = total spent / total coins The code
interface Buy { spent: number; // money spent on this buy price: number; // price paid per coin } interface Position { spent: number; // total money in coins: number; // total coins held average: number; // average price, also the break-even price valueAt: (price: number) => number; pnlAt: (price: number) => number; } export function averagePrice(buys: Buy[]): Position { const valid = buys.filter((b) => b.spent > 0 && b.price > 0); if (valid.length === 0) { throw new RangeError("add at least one buy with a positive amount and price"); } const spent = valid.reduce((sum, b) => sum + b.spent, 0); const coins = valid.reduce((sum, b) => sum + b.spent / b.price, 0); return { spent, coins, average: spent / coins, valueAt: (price) => coins * price, pnlAt: (price) => coins * price - spent, }; } A worked example
const p = averagePrice([ { spent: 100, price: 1.0 }, { spent: 100, price: 0.5 }, ]); p.spent; // 200 p.coins; // 300 p.average; // 0.6666... p.pnlAt(0.8); // 40 Break-even is the average price
Your position is worth what you paid when coins * price = spent, so the break-even price is spent / coins. That is the average price. No second formula needed.
In practice the real break-even is a little higher, because selling has costs. A calculator should say that its break-even is before fees and slippage.
Two design choices
Filter or throw? The function ignores rows where the amount or the price is not positive, and throws only if nothing is left. In a form with several rows, people leave empty rows all the time. Throwing on the first empty row makes the tool annoying, and silently returning NaN makes it wrong.
Store the buys, not the average. If you keep only a running average and a coin count, you cannot edit or remove a past buy without rounding drift. Keep the list and recompute. It is a reduce over a handful of rows.
A related one liner: the market cap multiple
The same kind of question comes up with small coins: "what is my bag worth if the market cap goes from here to there?" With the supply unchanged, value scales with market cap:
const valueAtCap = (bagValue: number, capNow: number, capTarget: number) => bagValue * (capTarget / capNow); valueAtCap(250, 5_000_000, 50_000_000); // 2500 It is a hypothetical, not a forecast. Most coins never reach the cap people type into that field.
If you only need the numbers, the hosted versions are here, free and with no sign up: dca and average price and market cap. Nothing here is financial advice.


