Skip to Content
Engines — real vs synthetic

Engines: real fills vs synthetic

IndicaView can show you an equity curve, a profit factor and a win rate for any Pine script. That does not mean those numbers came from a simulated position.

This page is the rule that tells you when to trust what you are looking at.

Read this before drawing any conclusion from a metric on this site or in the application.

The rule

Pine has two top-level declarations, and they produce fundamentally different results:

DeclarationWhat it doesPnL
strategy()Opens and closes real simulated positionsReal fills. PnL, win rate, profit factor and drawdown are meaningful
indicator()Plots values and shapes only. Never opens a positionSynthetic. Any trade list is reconstructed

If a script calls indicator(), the engine has no entry price, no exit price, no position size and no commission. IndicaView therefore reconstructs a trade list from the signals the script plots, and everything downstream — the equity curve, the win rate, the drawdown — is a model of that reconstruction, not a simulation of a position.

What a strategy()-mode run reports instead

The reconstruction is not involved at all. A strategy() declaration opens and closes real simulated positions, so the engine holds the four things the indicator case lacks — an entry price, an exit price, a position size and a commission — and every metric is computed from those positions rather than from a model of them.

The cost model is part of the declaration rather than a project default. The strategy() block at the end of this page sets commission_type to percent, commission_value to 0.1, slippage to 2 and default_qty_value to 100 % of equity — and each of those is a number you chose and can change. A worked example is in Strategies.

How the reconstruction works

An indicator-mode script still has to say something. In practice it plots a signal series alongside the price series, and the reconstruction layer does the rest:

  1. The script emits a buy/sell signal series. In this repository that is done with plotshape() for the visible markers plus hidden plots for the machine-readable values:

    plotshape(long_signal, "Buy", shape.triangleup, location.belowbar, color.green) plotshape(short_signal, "Sell", shape.triangledown, location.abovebar, color.red) plot(long_signal ? 1 : 0, "Long Signal", display=display.none) plot(short_signal ? 0 : 1, "Short Signal", display=display.none)
What the hidden plots actually carry

Two audiences, two artefacts. The plotshape() calls draw the markers a person reads on the chart; the plot() calls carry the same two signals as plain series that nothing displays.

The values are a 0/1 pair, and the two directions use opposite encodings: long_signal ? 1 : 0 reads as 1 on a long signal, while short_signal ? 0 : 1 reads as 0 on a short signal. Those series are the machine-readable half of the signal; the shapes are the half a person reads.

  1. The run is executed with --format full so the signals and the OHLCV series are both emitted.
  1. The reconstruction pairs each signal with the bar it fired on, applies the stop and target distances the script plots (typically ATR-based), and applies a modelled commission and slippage.

Everything in step 3 is an assumption the project made, not something your script asked for.

What step 3 decides on the script's behalf
  • The fill — each signal is paired with the bar it fired on. The engine has no entry price of its own, so the bar stands in for it.
  • The exit — the stop and target distances come from what the script plots, typically ATR-based, rather than from a position the engine was tracking.
  • The cost — commission and slippage are modelled, not declared, because an indicator() script has no declaration to put them in.

The ATR multiplier, the fill assumption and the cost model are all choices, and reasonable projects will disagree about them. That disagreement is why the surfaces label a reconstructed result estimated rather than presenting it as measured.

Synthetic results are still useful — for comparing the relative shape of two parameter sets, or checking whether a strategy family has any life in it at all. They are not a substitute for a real fill simulation.

Which shipped scripts are which

ScriptModeResult type
rsi_strategy.pinestrategy()Real fills
ema_cross.pineindicator()Synthetic
multi_signal.pineindicator()Synthetic
_template.pineindicator()Synthetic

Full detail on each script is in Strategies.

What the UI tells you

The surfaces do not hide this distinction, and the documentation mirrors them:

  • The backtest panel labels the origin of the result. When a script produces no real fills, the metrics are marked as estimated rather than presented as measured.
  • The optimizer keeps “engine” results and “synthetic” results distinct. The optimizer never silently upgrades one into the other.
  • The Analysis workspace shows metric values unavailable rather than substituting a placeholder when a real value does not exist.
Why a missing value reads as unavailable

All three surfaces are making the same promise: a number reconstructed from signals and a number measured from a filled position must never look alike.

The backtest panel labels the origin of the result, and the optimizer keeps “engine” and “synthetic” in separate lanes rather than upgrading one into the other. The Analysis workspace is the strictest of the three — it prints unavailable in place of a placeholder, so a value that does not exist cannot be read as a small number.

The rule for a new strategy

If you want trustworthy PnL, write it in strategy() mode and declare your cost model explicitly:

//@version=6 strategy("My strategy", overlay = true, commission_type = strategy.commission.percent, commission_value = 0.1, // 0.1 % slippage = 2, // in ticks default_qty_type = strategy.percent_of_equity, default_qty_value = 100)

Declaring costs is not optional hygiene in this project — it is a hard invariant. See Architecture decisions.

A strategy-mode script that omits the cost parameters will silently use the engine’s defaults, which are optimistic. That is the single easiest way to turn a losing strategy into an apparently winning one.

Last updated on