DEV Community

Cover image for How to Code a Stock & Crypto "Average Down" (DCA) Algorithm in Vanilla JS
Raj
Raj

Posted on

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 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:

  1. The Volume-Weighted Average Price (VWAP) math for multi-lot purchases.
  2. The analytical formula that calculates the exact number of shares needed to hit a target average price without iteration or guessing.
  3. The Harmonic Mean advantage of Dollar-Cost Averaging (DCA).
  4. 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 }
Enter fullscreen mode Exit fullscreen mode

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 N1 shares at price P1.
  • 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 }
Enter fullscreen mode Exit fullscreen mode

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))
  };
}
Enter fullscreen mode Exit fullscreen mode

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:


Key Takeaways for Developers

  1. Avoid Guessing Loops: Don't write while loops to increment shares until you hit a target average price. Use the closed-form equation: N2 = N1 * (P1 - P_target) / (P_target - P2).
  2. 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 use Number.toFixed() or BigNumber representations for transaction engines.
  3. Guard Boundary Asymptotes: Always validate P2 < P_target < P1 to 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!

Top comments (0)