Backtest System — Dependency Map

This document maps every dependency in the backtest pipeline: what data is required, how the simulation works per entry strategy, how SL/TP levels are derived, and what the output means. Read alongside Trading System — Dependency Map for live trading context.


1. System Overview

Admin UI ──► POST /api/ohlc/optimise (mi-casa) ──► POST /backtest (ohlc-service)
                                                         │
                                               backtestService.runBacktest
                                                         │
                                    ┌────────────────────┴──────────────────────┐
                                    │                                           │
                               saved_patterns                           walk_forward
                               (ohlcPatternRepo)                   (walkForwardScanner)
                                    │                                           │
                         createBacktestService                createWalkForwardBacktestService
                                    │                                           │
                              simulateTrade ◄─────── calculateSlTp ◄─── config.multipliers
                                    │
                              aggregateStats
                                    │
                            BacktestResult (SSE stream)

Two backtest engines exist, selected by optimisationMode:

  • saved_patterns (default) — uses patterns already stored in ohlc_patterns DB table.
  • walk_forward — re-scans candle history with a sliding window; no DB patterns required.

Both engines share simulateTrade and calculateSlTp.


2. Prerequisites for a Backtest to Run

#ConditionSource
1OHLC_SERVICE_URL and OHLC_SERVICE_API_KEY env vars setoptimisationController.triggerOptimisation
2Caller is authenticated and has admin roleoptimisationRoutes.ts — all three routes guard with requireRole('admin')
3Valid symbol, timeframe in request bodyZod schema in backtestRoute.ts
4ohlc_patterns table has data for symbol+timeframe (saved_patterns mode)ohlcPatternRepository.getBySymbolAndTimeframeWithDetectionTime
5ohlc_candles table has data for symbol+timeframe (walk_forward + prz_touch/candle_close)ohlcCandleRepository.getByRange / getFrom

3. Entry Strategy Simulation Logic

Each pattern produces one BacktestTrade. The entry price, SL/TP anchor, and candles used for simulation depend on tradingConfig.entryStrategy:

3a. swing_confirmation (default)

StepDetail
SL/TP anchorD-point (dPrice)
Entry candleFixed: futureCandles[PIVOT_LOOKBACK + 1] — the 6th candle after detectedAt; user entryLagCandles is ignored for this strategy
Entry pricefutureCandles[6].open (or dPrice if fewer than 7 future candles exist)
Simulation startsFrom detectedAt + 1ms
Outcome when no candles'open'

3b. prz_touch

StepDetail
PRZ bandPrefer pattern.przLow/High; fall back to SL-derived band from calculateSlTp
Touch detectionfindPrzTouchCandle(candlesFromCPoint, przLow, przHigh, isBullish) — first candle where low ≤ przHigh (bullish) or high ≥ przLow (bearish)
Entry priceTouch candle’s low (bullish) or high (bearish) — the extreme that entered the PRZ
SL/TP anchorprzTouchPrice (overrides dPrice)
Simulation startsCandles strictly after touch candle
Outcome when no touch'open' — PRZ never reached

3c. candle_close

Same as prz_touch for touch detection, then:

StepDetail
Entry priceOpen of the candle AFTER the touch candle
SimulationPasses entryLagCandles: 1 to simulateTrade, candles from touch candle onward
Outcome when no candle after touchUses przTouchPrice as fallback entry

3d. abcd (emerging ABC path)

ABCD backtests use the emerging ABC path, not swing_confirmation: they validate a consecutive XABC sequence, wait for the first PRZ touch after C, and enter at the touch price. swing_confirmation remains the behavior for the other harmonic strategies only.


4. SL/TP Calculation

calculateSlTp(input, config?) in slTpCalculator.ts:

  1. Try candlestick pattern formulas (computeCandlestickSlTp)
  2. Fall back to pattern-type specific formulas (harmonic ratios, head-and-shoulders, cypher, etc.)
  3. Apply multipliers from config: sl *= slMultiplier, tp1 *= tp1Multiplier, etc. (scaled relative to D-point)

The returned SlTpLevels = { sl, tp1, tp2, tp3 } are all absolute prices.


5. Trade Simulation (simulateTrade)

Runs candle-by-candle after entry. Uses these config fields:

Config fieldEffect
entryLagCandlesSkip N candles; use candlesAfterD[N].open as entry price
tpTargetMode'tp1' closes at TP1; 'tp1_tp2' closes at TP2; 'tp1_tp2_tp3' closes at TP3
tpCountLegacy alias — mapped to tpTargetMode (1→tp1, 2→tp1_tp2, 3→tp1_tp2_tp3)
partialExitEnabledWhen true: close partialExitTp1Percent% at TP1, move SL to breakeven
partialExitTp1Percent% of position to exit at TP1 (default: 33.3)
partialCloseLegacy boolean alias for partialExitEnabled

Resolution rules (non-partial path)

On each candle c:

  1. SL hit: isBullish ? c.low ≤ sl : c.high ≥ slalways wins on the same candle as TP
  2. TP hit: isBullish ? c.high ≥ targetTp : c.low ≤ targetTp

No candle hit → outcome 'open'.

Partial-exit path (3 phases)

PhaseConditionAction
Phase 1Pre-TP1SL beats TP1 on same candle; TP1 hit → partial exit + SL → breakeven
Phase 2Post-TP1, pre-TP2SL (at breakeven) → remainder exits at 0; TP2 hit → close all (tp1_tp2) or become milestone (tp1_tp2_tp3)
Phase 3Post-TP2SL (at breakeven) → remainder exits at 0; TP3 hit → close all

6. Signal Score Filtering

When tradingConfig.minSignalScore is set:

filteredPatterns = allPatterns.filter(p =>
  p.signalScore == null  ← null scores PASS through
  || parseFloat(p.signalScore) >= minSignalScore
)

⚠️ Bug XXX: Patterns without a signalScore (null) always pass through the filter even when minSignalScore > 0. In live trading, events with null/low confidence are blocked by the engine’s minSignalScore guard. Backtest results for configs with a non-zero minSignalScore are therefore over-optimistic: they include patterns the live engine would reject.


7. Walk-Forward Mode

createWalkForwardScanner slides a window of WALK_FORWARD_WINDOW_SIZE = 300 candles:

  1. At each position, run detectEmergingPatterns on the window.
  2. Deduplicate by patternType:direction:x.time:c.time:fibLabel — only first occurrence (with firstEmergingAt) is kept.
  3. After scan, resolve przTouchTime/Price for each pattern from candles after firstEmergingAt.

The walk-forward backtest then runs simulateTrade on each scanned pattern with every config combination (50 grid configs per pattern).


8. Backtest Result Shape

BacktestResult {
  symbol, timeframe, patternType,
  totalPatterns,     // all patterns (including open)
  completedTrades,   // trades with outcome !== 'open'
  openTrades,        // trades with outcome === 'open'
  wins,              // completed with outcome !== 'sl'
  losses,            // completed with outcome === 'sl'
  winRate,           // wins / completedTrades
  avgWinPips, avgLossPips,
  profitFactor,      // null when no losses
  totalPnlPips,
  maxDrawdownPips, maxDrawdownPct,  // from sorted-by-entryTime pnl sequence
  trades: BacktestTrade[]
}

Note: maxDrawdown is computed on a totalPnlPips equity curve sorted by entryTime. If entryTime is inaccurate (see Bug above), drawdown ordering may be wrong.


9. API

POST /api/ohlc/backtest/strategy (proxied from mi-casa)

Auth: JWT + markets access
Body: { strategyType, symbol, timeframe, from, to?, params?, entryLagCandles?, tradingConfig? }
Response: SSE stream of { type: 'progress' | 'result' | 'error', ... } events

The legacy pattern backtest route and saved-pattern engine (createBacktestService.ts) were removed in issues #920 and #921. Backtest and optimisation are now strategy-only (harmonic patterns: cypher, gartley, bat, butterfly, crab, shark, abcd).

POST /api/ohlc/optimise (proxied from mi-casa)

Auth: JWT + admin role
Triggers optimisation which calls runBacktest 50 times internally (see Optimisation System — Dependency Map)


10. Known Bugs / Limitations

IDSummarySeverity
#461entryPrice in output is lag=0 open even when entryLagCandles > 0fixed for swing_confirmation (lag hardcoded to 6; entryPrice set to lagCandle.open)Medium
#460minSignalScore filter: patterns with null signalScore always passMedium
By designwalk_forward patterns pre-compute SL/TP from PRZ midpoint; actual simulation may use different anchor (przTouchPrice)Low / cosmetic