Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt

Use this file to discover all available pages before exploring further.

Dollar Cost Averaging ladders are a position-building technique where, instead of committing all capital at a single entry price, you open an initial position and add subsequent β€œrungs” at progressively better prices as the market moves against you. By spreading entries, you lower the blended effective price, which reduces the percentage drawdown needed to reach breakeven and increases the probability of hitting a profit target on the blended position. Backtest Kit implements this natively with commitAverageBuy, which tracks a harmonic mean cost basis across all entries and automatically rejects rungs that would average the wrong direction.

Backtest results

The same ladder mechanics were backtested against two distinct market regimes in 2026. Both produced significantly better outcomes than single-entry equivalents.

πŸ§— LONG DCA Ladder β€” April 2026

BTC rose +15.3% over the month, providing ideal conditions for a LONG ladder.
MetricValue
Net PNL+67.85% on deployed capital
Sharpe ratio0.12
Total trades7 (100% win rate)
Avg entries per trade2.4
Max entries in one trade5
Best trade+$16.53 (5 entries, +3.31% blended)
Worst drawdownβˆ’2.59% per position
Without DCA+12.45% PNL, βˆ’3.99% drawdown

πŸͺ‚ SHORT DCA Ladder β€” March 2026

BTC was nearly flat (βˆ’0.3%) with intra-month swings of Β±15% β€” ideal for SHORT mean-reversion.
MetricValue
Net PNL+37.83% on deployed capital
Sharpe ratio0.35
Total trades21 (95.2% win rate)
Avg entries per trade3.1
Max entries in one trade10
Best trade+$6.36 (8 entries, +0.79% blended)
Worst drawdownβˆ’10.49% per position
Without DCAβˆ’0.41% PNL (losing month)
The DCA approach flipped the March SHORT baseline from βˆ’0.41% to +37.83% β€” turning a losing single-entry month into a profitable one β€” and nearly tripled gross dollar profit in April relative to single-entry while simultaneously improving the per-position percentage drawdown.
The DCA ladder is percentage-safer but fiat-riskier. In April, DCA improved drawdown from βˆ’3.99% to βˆ’2.59% per position. However, the maximum absolute dollar drawdown rose from βˆ’3.99(1entryΓ—3.99 (1 entry Γ— 100) to βˆ’12.64(5entriesΓ—12.64 (5 entries Γ— 100). With 10 rungs fully deployed, the worst-case single-position loss is 2,500vs2,500 vs 250 for a single entry. Size your LADDER_STEP_COST accordingly.

Full strategy code (LONG DCA Ladder)

The following is the complete apr_2026_strategy implementation. It opens a LONG signal on every idle tick, dollar-cost-averages down on every active ping, and closes as soon as portfolio PnL crosses +3%.
import {
  addStrategySchema,
  listenError,
  listenActivePing,
  Log,
  Position,
  commitClosePending,
  getPositionPnlPercent,
  getPositionEntryOverlap,
  getPositionEntries,
  commitAverageBuy,
} from 'backtest-kit';

const HARD_STOP = 25.0;
const TARGET_PROFIT = 3;
const LADDER_STEP_COST = 100;
const LADDER_UPPER_STEP = 5;
const LADDER_LOWER_STEP = 1;
const LADDER_MAX_STEPS = 10;

addStrategySchema({
  strategyName: 'apr_2026_strategy',
  getSignal: async (symbol, when, currentPrice) => {
    return {
      position: 'long',
      ...Position.moonbag({
        position: 'long',
        currentPrice,
        percentStopLoss: HARD_STOP,
      }),
      minuteEstimatedTime: Infinity,
      cost: LADDER_STEP_COST,
    };
  },
});

listenActivePing(async ({ symbol, currentPrice }) => {
  const { length: steps } = await getPositionEntries(symbol);
  if (steps >= LADDER_MAX_STEPS) return;
  const hasOverlap = await getPositionEntryOverlap(symbol, currentPrice, {
    upperPercent: LADDER_UPPER_STEP,
    lowerPercent: LADDER_LOWER_STEP,
  });
  if (hasOverlap) return;
  await commitAverageBuy(symbol, LADDER_STEP_COST);
});

listenActivePing(async ({ symbol, data, timestamp }) => {
  const currentProfit = await getPositionPnlPercent(symbol);
  if (currentProfit < TARGET_PROFIT) return;
  await commitClosePending(symbol, {
    id: 'unknown',
    note: '# Closed by target pnl',
  });
});

How the strategy works

The strategy has three independent concerns, each handled in isolation: Signal opening (getSignal): Called on every idle tick (no active position). Returns a LONG signal using Position.moonbag, which sets a hard stop loss at βˆ’25% and an infinite hold time (minuteEstimatedTime: Infinity). The position will never time out β€” it stays open until explicitly closed by the profit target handler or the hard stop is hit. DCA ladder (listenActivePing β€” first handler): Called on every tick while a position is active. Checks two conditions before adding a rung: the current number of entries must be below LADDER_MAX_STEPS (10), and the current price must fall outside a Β±1–5% band around the last entry via getPositionEntryOverlap. If price is still within the band, no new entry is added β€” this prevents excessive churn on small oscillations. When both conditions pass, commitAverageBuy adds a new $100 entry and updates the harmonic mean cost basis. Profit-target close (listenActivePing β€” second handler): Also called on every active tick. Reads the current blended PnL percentage via getPositionPnlPercent and closes the entire position with commitClosePending as soon as it exceeds +3%. Because the effective price is a harmonic mean across all entries, even if individual entries are still underwater, the blended position can reach the target early.

Key techniques

getPositionEntryOverlap(symbol, currentPrice, { upperPercent, lowerPercent }) returns true if currentPrice falls within [lastEntry * (1 - lowerPercent/100), lastEntry * (1 + upperPercent/100)]. In the LONG ladder, this means: don’t add a new rung unless price has moved at least 1% below the previous entry. The asymmetric band (upperPercent: 5, lowerPercent: 1) reflects that for a LONG we primarily care about price moving down (new rung makes sense) but also want to avoid a rung being placed on a brief spike above the last entry.
Each commitAverageBuy call adds a fixed-cost rung (e.g. $100). The effective price (priceOpen) of the blended position is the harmonic mean of all accepted entry prices, weighted by cost:
effectivePrice = totalCost / totalCoins
               = Ξ£(cost_i) / Ξ£(cost_i / price_i)
This is the true break-even price for the position. After each partial or average buy, priceOpen shifts, which in turn affects whether the next commitAverageBuy will be accepted β€” the engine enforces that new entries must be below the current harmonic mean for LONG positions (or above for SHORT).
Position.moonbag({ position, currentPrice, percentStopLoss }) is a convenience factory that returns signal parameters configured for indefinite holds: minuteEstimatedTime: Infinity (never times out), a priceStopLoss at currentPrice * (1 - percentStopLoss/100), and an effectively unreachable priceTakeProfit far above current price. The position is expected to be closed programmatically (by commitClosePending) rather than by hitting the take-profit level. The stop loss is the only exchange-level protection.

Adapting for SHORT

The SHORT DCA Ladder (March 2026) uses identical mechanics. The only change is the direction flag in getSignal:
addStrategySchema({
  strategyName: 'mar_2026_strategy',
  getSignal: async (symbol, when, currentPrice) => {
    return {
      position: 'short',           // ← changed from 'long'
      ...Position.moonbag({
        position: 'short',         // ← changed from 'long'
        currentPrice,
        percentStopLoss: HARD_STOP,
      }),
      minuteEstimatedTime: Infinity,
      cost: LADDER_STEP_COST,
    };
  },
});
commitAverageBuy works identically for SHORT positions β€” for a SHORT, it adds a rung when price moves upward beyond the band, since higher entries lower the effective short cost basis. getPositionEntryOverlap automatically applies the band check in the correct direction based on the position type. The March SHORT strategy used a lower profit target (TARGET_PROFIT = 0.5) to match the mean-reverting, choppier market regime.

Running the backtest

# LONG DCA Ladder (April 2026)
npm start -- --backtest --symbol BTCUSDT \
  ./content/apr_2026.strategy/apr_2026.strategy.ts

# SHORT DCA Ladder (March 2026)
npm start -- --backtest --symbol BTCUSDT \
  ./content/mar_2026.strategy/mar_2026.strategy.ts

# Open the visual dashboard alongside either backtest
npm start -- --backtest --symbol BTCUSDT --ui \
  ./content/apr_2026.strategy/apr_2026.strategy.ts
Source code: apr_2026.strategy Β· mar_2026.strategy
When testing a DCA ladder for the first time, set LADDER_STEP_COST to a small value (e.g. 10) and LADDER_MAX_STEPS to 3 or 4. Run a short backtest window to verify the overlap logic and PnL target are behaving as expected before scaling up capital or rung count. A fully deployed 10-rung ladder with 100stepscommits100 steps commits 1,000 per position β€” make sure your backtest frame covers enough drawdown scenarios to stress-test the worst case.

Build docs developers (and LLMs) love