How to Code a Stock & Crypto "Average Down" (DCA) Algorithm in Vanilla JS
When markets turn volatile, one of the most common dilemmas retail investors face is "buying the dip" or "averaging down": "I bought 100 shares of an ETF at $80, and now it's dropped to $40. How many shares do I need t
When markets turn volatile, one of the most common dilemmas retail investors face is "buying the dip" or "averaging down":
"I bought 100 shares of an ETF at $80, and now it's dropped to $40. How many shares do I need to buy today to drag my break-even average price down to $50?"
Most investors try to guess this by trial and error in Excel. But as developers, we can solve this with an exact closed-form algebraic equation and wrap it in a lightweight, zero-dependency JavaScript engine.
In this tutorial, we will break down:
- The Volume-Weighted Average Price (VWAP) math for multi-lot purchases.
- The analytical formula that calculates the exact number of shares needed to hit a target average price without iteration or guessing.
- The Harmonic Mean advantage of Dollar-Cost Averaging (DCA).
- Handling floating-point financial precision and fractional cryptocurrency decimals in Vanilla JS.
1. Multiple Purchases: Weighted Average Cost Basis
A common mistake beginners make is taking the simple arithmetic mean of share prices: (P1 + P2) / 2.
This only works if you bought the exact same number of shares each time. In reality, position sizing varies. The true cost basis is the Volume-Weighted Average Price (VWAP):
P_avg = (P1*N1 + P2*N2 + ... + Pk*Nk) / (N1 + N2 + ... + Nk)
The Vanilla JavaScript Implementation
/**
* Calculate Weighted Average Cost Basis across multiple purchase lots
* @param {Array<{shares: number, price: number}>} lots
* @returns {{totalShares: number, totalInvested: number, averagePrice: number}}
*/
function calculateWeightedAverage(lots) {
let totalCost = 0;
let totalShares = 0;
for (const lot of lots) {
const shares = Number(lot.shares) || 0;
const price = Number(lot.price) || 0;
if (shares > 0 && price > 0) {
totalCost += shares * price;
totalShares += shares;
}
}
if (totalShares === 0) {
throw new Error("Total shares must be greater than zero.");
}
const averagePrice = totalCost / totalShares;
return {
totalShares: Number(totalShares.toFixed(6)),
totalInvested: Number(totalCost.toFixed(2)),
averagePrice: Number(averagePrice.toFixed(4))
};
}
// Example usage:
const portfolio = [
{ shares: 100, price: 50.00 }, // Initial batch ($5,000)
{ shares: 100, price: 30.00 }, // Dip batch ($3,000)
];
const result = calculateWeightedAverage(portfolio);
console.log(result);
// Output: { totalShares: 200, totalInvested: 8000, averagePrice: 40.00 }
By deploying \$3,000 on the pullback, the break-even requirement dropped from \$50 down to \$40 (-20.0%).
2. The Analytical Formula: "How Many Shares to Buy?"
Now for the interesting problem: solving for target price.
Suppose:
- You own
N1shares at priceP1. - The current dip market price is
P2. - You want your new combined average price to be
P_target.
How many shares (N2) must you buy at P2?
Deriving the Equation
Start with the weighted average equation:
P_target = (P1*N1 + P2*N2) / (N1 + N2)
Multiply both sides by (N1 + N2) and rearrange to isolate N2:
N2 = N1 * (P1 - P_target) / (P_target - P2)
The Code Implementation with Fuzz-Proof Guards
/**
* Calculate required share count to achieve a desired target average cost
* @param {number} currentShares (N1)
* @param {number} currentPrice (P1)
* @param {number} dipBuyPrice (P2)
* @param {number} targetAvgPrice (P_target)
*/
function calculateSharesForTarget(currentShares, currentPrice, dipBuyPrice, targetAvgPrice) {
// Edge Case 1: Negative or zero inputs
if (currentShares <= 0 || currentPrice <= 0 || dipBuyPrice <= 0 || targetAvgPrice <= 0) {
throw new Error("All parameters must be positive numbers.");
}
// Edge Case 2: Target must be lower than current price (otherwise you are averaging UP)
if (targetAvgPrice >= currentPrice) {
throw new Error("Target average must be lower than your current average price to average down.");
}
// Edge Case 3: Target can never be lower than the purchase price
// (You cannot buy at $30 and expect your average to drop below $30!)
if (targetAvgPrice <= dipBuyPrice) {
throw new Error("Target average cannot be less than or equal to the new purchase price.");
}
// Analytical Closed-Form Solution
const sharesNeeded = currentShares * ((currentPrice - targetAvgPrice) / (targetAvgPrice - dipBuyPrice));
const capitalRequired = sharesNeeded * dipBuyPrice;
return {
sharesNeeded: Number(sharesNeeded.toFixed(4)),
capitalRequired: Number(capitalRequired.toFixed(2)),
newTotalShares: Number((currentShares + sharesNeeded).toFixed(4)),
targetAvgPrice: Number(targetAvgPrice.toFixed(2))
};
}
// Example: Own 50 shares @ $80. Stock drops to $40. We want a $50 average.
const targetResult = calculateSharesForTarget(50, 80, 40, 50);
console.log(targetResult);
// Output: { sharesNeeded: 150, capitalRequired: 6000, newTotalShares: 200, targetAvgPrice: 50 }
Notice the denominator: (P_target - P2). As your target average gets closer to the new dip price, the denominator approaches zero, meaning the capital required explodes exponentially. This is why averaging down has diminishing returns as a position grows larger (dilution inertia).
3. Fixed Cash Budget: How Much Will $X Move the Needle?
Sometimes an investor doesn't have unlimited funds to hit an exact target; they only have a fixed amount of cash available (e.g., \$2,500).
/**
* Calculate resulting average price when deploying a fixed cash budget
*/
function calculateBudgetAverage(currentShares, currentPrice, dipPrice, cashBudget) {
if (currentShares <= 0 || currentPrice <= 0 || dipPrice <= 0 || cashBudget <= 0) {
throw new Error("Invalid input bounds.");
}
const newShares = cashBudget / dipPrice;
const totalShares = currentShares + newShares;
const totalCapital = (currentShares * currentPrice) + cashBudget;
const newAveragePrice = totalCapital / totalShares;
const reductionPercent = ((currentPrice - newAveragePrice) / currentPrice) * 100;
return {
newSharesBought: Number(newShares.toFixed(4)),
totalShares: Number(totalShares.toFixed(4)),
totalInvested: Number(totalCapital.toFixed(2)),
newAveragePrice: Number(newAveragePrice.toFixed(4)),
costBasisReductionPct: Number(reductionPercent.toFixed(2))
};
}
4. The Math Behind Systematic Dollar-Cost Averaging (DCA)
When people talk about Dollar-Cost Averaging, they often confuse it with standard averaging down:
- Averaging Down is opportunistic: buying only after a drop.
- Dollar-Cost Averaging (DCA) is systematic: investing fixed dollar sums on a calendar schedule (weekly/monthly).
Here is the mathematical secret of DCA: when you invest equal dollar amounts into an asset whose price fluctuates, your effective average cost per unit is the Harmonic Mean of the prices.
Because the Harmonic Mean is mathematically proven to always be less than or equal to the Arithmetic Mean (H <= A), you automatically accumulate more units when prices are depressed and fewer units when prices are elevated.
Live Production Demo & Tool
We recently built and open-sourced these algorithms as part of our free financial utility suite at CalcCentrals.
You can test the interactive client-side tool here:
👉 Average Down & DCA Calculator (CalcCentrals)
If you are interested in companion mathematical models, check out:
- SIP & DCA Compounding Calculator — Evaluates long-term periodic accumulation curves.
- CAGR Calculator — Calculates Compound Annual Growth Rate across irregular multi-year entry points.
- Compound Interest Calculator — Simulates daily, monthly, and annual compounding schedules.
Key Takeaways for Developers
-
Avoid Guessing Loops: Don't write
whileloops to increment shares until you hit a target average price. Use the closed-form equation:N2 = N1 * (P1 - P_target) / (P_target - P2). -
Watch Crypto Decimal Precision: While traditional equities usually trade in whole shares, crypto assets (like Bitcoin or Ethereum) require supporting up to 8 decimal places (
0.00000001 BTC). Avoid integer parsing (parseInt) and useNumber.toFixed()or BigNumber representations for transaction engines. -
Guard Boundary Asymptotes: Always validate
P2 < P_target < P1to protect your UI from division-by-zero crashes or returning negative share counts.
Have you built any financial tools or trading calculators in JavaScript? How do you handle decimal precision in your web apps? Let's discuss in the comments below!
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.