Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ta4j/ta4j/llms.txt

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

By default, BarSeriesManager executes trades at the next bar’s open price (TradeOnNextOpenModel). A second built-in alternative fills at the current bar’s close price (TradeOnCurrentCloseModel). For more realistic simulations, ta4j provides two pluggable execution models that capture slippage and order-book dynamics.

Default execution models

ClassFill priceWhen to use
TradeOnNextOpenModelNext bar’s openDefault. Reflects the common practice of acting on a signal at the next available open.
TradeOnCurrentCloseModelCurrent bar’s closeStrategies that act just before bar close (e.g., end-of-day rebalancing).
Both models are passed to BarSeriesManager during construction.

SlippageExecutionModel

SlippageExecutionModel simulates market impact: buys are filled at a slightly worse price (open × (1 + slippage)), sells at a slightly worse price (open × (1 − slippage)).
import org.ta4j.core.backtest.SlippageExecutionModel;

// 5 basis points (0.05%) slippage on the next-bar open
TradingRecord slippageRecord = new BarSeriesManager(series,
        new SlippageExecutionModel(series.numFactory().numOf(0.0005)))
        .run(strategy);
The slippageRatio must be in [0, 1). Values at or above 1 throw IllegalArgumentException.

Using a different base price

By default SlippageExecutionModel uses next-bar open as the reference. Pass a PriceSource to change this:
import org.ta4j.core.backtest.TradeExecutionModel.PriceSource;

// Apply slippage on the current bar's close instead
SlippageExecutionModel model = new SlippageExecutionModel(
        series.numFactory().numOf(0.0005),
        PriceSource.CURRENT_CLOSE);

StopLimitExecutionModel

StopLimitExecutionModel simulates stop-limit order behaviour with partial fill progression and order expiry. When a strategy signals an entry:
  1. A pending stop-limit order is placed at reference × (1 + stopTriggerRatio) for buys (or 1 − ratio for sells).
  2. The order is evaluated on each subsequent bar via onBar(...).
  3. Once the stop price is triggered, fills are collected up to maxBarParticipationRate × barVolume per bar.
  4. If the order reaches its limit price and is fully filled, it is committed immediately.
  5. If the order is not fully filled within maxBarsToFill bars, it expires. Partial fills at expiry are committed if they represent an entry trade or a position that is already open.
import org.ta4j.core.backtest.StopLimitExecutionModel;

// Stop trigger: 0.3% above reference open
// Limit offset: 0.4% above reference open (must be >= stop trigger)
// Max bar participation: 25% of bar volume per fill
// Order TTL: 4 bars
TradingRecord stopLimitRecord = new BarSeriesManager(series,
        new StopLimitExecutionModel(
                series.numFactory().numOf(0.003),   // stopTriggerRatio
                series.numFactory().numOf(0.004),   // limitOffsetRatio
                series.numFactory().numOf(0.25),    // maxBarParticipation
                4))                                 // maxBarsToFill
        .run(strategy, strategy.getStartingType(), series.numFactory().numOf(10));
limitOffsetRatio must be greater than or equal to stopTriggerRatio. All ratios must be in [0, 1).

Inspecting pending and rejected orders

StopLimitExecutionModel model = new StopLimitExecutionModel(
        series.numFactory().numOf(0.003),
        series.numFactory().numOf(0.004),
        series.numFactory().numOf(0.25),
        4);

BarSeriesManager manager = new BarSeriesManager(series, model);
TradingRecord record = manager.run(strategy);

// Orders that were not fully filled and expired (or were rejected for other reasons)
List<StopLimitExecutionModel.RejectedOrder> rejected = model.getRejectedOrders(record);
rejected.forEach(r -> System.out.printf("Rejected at bar %d: %s (filled=%s/%s)%n",
        r.rejectionIndex(), r.reason(), r.filledAmount(), r.requestedAmount()));

// Current pending order (if any) — useful for live mode introspection
Optional<StopLimitExecutionModel.PendingOrderSnapshot> pending = model.getPendingOrder(record);
pending.ifPresent(p -> System.out.printf("Pending: stop=%s limit=%s expiry=%d%n",
        p.stopPrice(), p.limitPrice(), p.expiryIndex()));

RejectedOrder fields

FieldDescription
signalIndexBar index when the strategy signalled
rejectionIndexBar index when rejection happened
tradeTypeBuy or sell side
requestedAmountAmount requested
filledAmountAmount that was filled before rejection
reasonHuman-readable rejection reason

Providing a custom TradingRecord

For fine-grained control over execution matching or cost models, pass a pre-configured BaseTradingRecord directly to BarSeriesManager.run(...):
import org.ta4j.core.BaseTradingRecord;
import org.ta4j.core.ExecutionMatchPolicy;

TradingRecord record = new BarSeriesManager(series).run(
        strategy,
        new BaseTradingRecord(
                TradeType.BUY,
                ExecutionMatchPolicy.FIFO,
                new ZeroCostModel(),
                new ZeroCostModel(),
                null,
                null));

TradingRecordFactory for BacktestExecutor wiring

When using BacktestExecutor or StrategyWalkForwardExecutor, you can control how fresh trading records are created for each backtest or fold run by providing a TradingRecordFactory to BarSeriesManager.
// Factory that creates LIFO-matched records with a transaction cost model
BarSeriesManager.TradingRecordFactory factory = (tradeType, startIndex, endIndex,
        transactionCostModel, holdingCostModel) ->
        new BaseTradingRecord(
                tradeType,
                ExecutionMatchPolicy.LIFO,
                transactionCostModel,
                holdingCostModel,
                null,
                null);

BarSeriesManager manager = new BarSeriesManager(series, factory);

Side-by-side comparison

The TradingRecordParityBacktest example compares next-open, current-close, and slippage-adjusted fills on the same strategy, then verifies the same fills across default, provided, and factory-configured BaseTradingRecord runs.
# Linux/macOS
./mvnw -pl ta4j-examples exec:java \
  -Dexec.mainClass=ta4jexamples.backtesting.TradingRecordParityBacktest

# Windows CMD
mvnw.cmd -pl ta4j-examples exec:java "-Dexec.mainClass=ta4jexamples.backtesting.TradingRecordParityBacktest"
Start with SlippageExecutionModel at a realistic fee level for your market (e.g., 5–10 bps for equities, 5–20 bps for crypto) before moving to StopLimitExecutionModel. The slippage model is much simpler to reason about and will reveal whether your strategy’s edge survives realistic transaction costs.

Build docs developers (and LLMs) love