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.

Backtest Kit treats position management as a first-class citizen of the framework rather than an afterthought. Beyond simple entry and exit, the platform supports full DCA ladders with harmonic-mean cost tracking, partial closes at any percentage, trailing take-profit and stop-loss levels that update on every tick, and automatic breakeven protection. All of these operations maintain mathematically accurate cost-basis accounting across the entire position lifespan.

Dollar Cost Averaging (DCA)

DCA in Backtest Kit is designed to lower the effective entry price of an existing position, not to pyramid into winners. The engine uses the harmonic mean of all accepted entry prices to compute the effective open price (priceOpen) after each DCA fill.

Why Harmonic Mean

When buying fixed-dollar amounts at different prices, the harmonic mean gives the true average cost per unit — the same result you get by dividing total dollars invested by total units acquired. Arithmetic mean would overstate the effective entry price.
Entry 1: $100 at price 1000 → 0.1000 coins
Entry 2: $100 at price 950  → 0.1053 coins
Entry 3: $100 at price 880  → 0.1136 coins

Harmonic mean of (1000, 950, 880) ≈ 941.46
Total invested: $300
Total coins: 0.3189
Effective price: $300 / 0.3189 ≈ $941.04  ✓

Averaging-Down Guard

By default, commitAverageBuy only accepts a new entry when the current price is strictly below the current effective entry price. Attempting to DCA at a price above the effective entry is silently rejected — no error is thrown, and the strategy continues. After each partial close, the cost basis and effective entry price are recalculated from the remaining position. This means the DCA acceptance threshold shifts after every partial, so a subsequent DCA call is evaluated against the updated effective price, not the original one.

Entry Overlap Detection

Before committing a DCA entry, you can check whether the current price falls too close to an existing DCA entry to avoid clustering entries in the same zone:
listenActivePing(async ({ symbol, currentPrice }) => {
  const hasOverlap = await getPositionEntryOverlap(symbol, currentPrice, {
    upperPercent: 5,  // reject if within 5% above any existing entry
    lowerPercent: 1,  // reject if within 1% below any existing entry
  });

  if (!hasOverlap) {
    await commitAverageBuy(symbol, 100); // $100 DCA entry
  }
});
getPositionEntryOverlap returns true if currentPrice falls within the overlap band of any existing DCA entry, letting you space entries across a meaningful price range.
Use getPositionPnlPercent(symbol) before calling commitAverageBuy to check whether the current position is already profitable. DCA into a profitable position is usually a signal to wait — the averaging-down guard will reject it anyway, but reading the PnL first can inform whether to adjust trailing stops instead.

Partial Profit and Loss Closes

Partial closes let you lock in gains or reduce exposure incrementally without fully exiting the position. Both functions accept a percentage of the current position size to close.
// Lock in 30% of the position at current price
await commitPartialProfit(symbol, 30);

// Cut 20% of exposure at a loss before stop-loss hits
await commitPartialLoss(symbol, 20);
After each partial close, the cost basis is updated to reflect only the remaining position. The priceOpen (effective entry) is recalculated from the new cost basis and remaining coin count, and this updated value determines whether the next commitAverageBuy call will be accepted.

Cost Basis Walkthrough

Consider a long position with three DCA entries followed by a partial profit close:
Entry #1 @ 1000: $100 → 0.1000 coins    costBasis = $100
Entry #2 @ 950:  $100 → 0.1053 coins    costBasis = $200
Entry #3 @ 880:  $100 → 0.1136 coins    costBasis = $300

commitPartialProfit(30%) @ 1150:
  effectivePrice  = hm(1000, 950, 880) ≈ 940
  partialValue    = 30% × $300 = $90
  coins sold      = 0.09 × 1150 = $103.50
  pnl on partial  ≈ +14.9%
  remaining basis = $210
  new priceOpen   = $210 / remaining_coins
The remaining $210 cost basis carries forward. All subsequent DCA acceptance checks and PnL calculations reference this updated basis.

Trailing Stop and Take Profit

Trailing exits move automatically with price on every active tick, ensuring gains are protected as the market moves favorably.
  • Trailing stop-loss moves upward (for longs) as price rises, locking in a floor below the current price at a fixed percentage distance. It never moves in the unfavorable direction.
  • Trailing take-profit adjusts upward as price rises, capturing additional upside beyond the original target without requiring manual intervention.
Both trail levels are recalculated on each tick using VWAP from the last five 1-minute candles. The VWAP-based pricing prevents a single-candle spike from triggering a premature exit while still responding to genuine price movement.
addStrategySchema({
  strategyName: 'trailing-strategy',
  interval: '5m',
  riskName: 'demo',
  getSignal: async (symbol) => {
    // ...signal logic...
    return {
      id: signalId,
      position: 'long',
      priceOpen: entryPrice,
      priceTakeProfit: entryPrice * 1.06,   // initial TP at +6%
      priceStopLoss: entryPrice * 0.97,     // initial SL at -3%
      trailingStop: true,                   // enable trailing stop
      trailingTake: true,                   // enable trailing take
      minuteEstimatedTime: 480,
    };
  },
});

Breakeven Protection

Breakeven protection automatically moves the stop-loss to the position’s entry price once profit crosses a configured threshold. This converts a winning trade into a risk-free position — price can retrace all the way to entry without triggering a loss.
return {
  id: signalId,
  position: 'long',
  priceOpen: entryPrice,
  priceTakeProfit: entryPrice * 1.08,
  priceStopLoss: entryPrice * 0.97,
  breakeven: {
    triggerPercent: 3,    // move SL to entry when profit reaches 3%
  },
  minuteEstimatedTime: 720,
};
Once the breakeven trigger fires, commitBreakeven is called internally and the stop-loss is set to priceOpen. The strategy’s onBreakeven callback fires at this point if one is registered. After breakeven, trailing stop behavior continues from the new floor.
Breakeven and trailing stop can be combined. Once breakeven triggers and the stop moves to entry, the trailing stop takes over from that new floor — it will continue moving upward with price but will never drop below the breakeven level.

Moonbag and Bracket Helpers

For common signal construction patterns, Backtest Kit provides convenience helpers on the Position class that compute take-profit and stop-loss prices from percentage inputs. Both helpers spread directly into the signal object returned from getSignal. Position.moonbag computes a take-profit and stop-loss from a single percentStopLoss value, automatically adjusting direction for long and short positions:
addStrategySchema({
  strategyName: 'moonbag-strategy',
  interval: '15m',
  getSignal: async (symbol, when, currentPrice) => ({
    ...Position.moonbag({
      position: 'long',
      currentPrice,
      percentStopLoss: 3,  // 3% hard stop; TP is derived symmetrically
    }),
    minuteEstimatedTime: Infinity,
    cost: 100,
  }),
});
Position.bracket computes both take-profit and stop-loss from explicit percentage distances, giving full control over the reward-to-risk ratio:
addStrategySchema({
  strategyName: 'bracket-strategy',
  interval: '15m',
  getSignal: async (symbol, when, currentPrice) => ({
    ...Position.bracket({
      position: 'long',
      currentPrice,
      percentTakeProfit: 2,  // TP 2% above entry
      percentStopLoss: 2,    // SL 2% below entry
    }),
    minuteEstimatedTime: 480,
    cost: 100,
  }),
});
Both helpers return { priceOpen, priceTakeProfit, priceStopLoss } computed from currentPrice and the given percentages. They are signal-constructor utilities — they define where the position opens and closes, not what happens after it is open.

Build docs developers (and LLMs) love