Skip to content

A strategy.exit called on a bar where its from_entry has no order or position costs about 20 µs: a script that calls its exits on every bar runs 8 times slower, with the same trades #858

Open

Description

@letiep97

Summary

A strategy.exit call costs about 20 µs on every bar where the entry it names has no pending order and no open position. Nothing is placed and the trades do not change, but the time adds up over the bars: a script that calls its exits on every bar, as many published strategies do, takes 4.7 s where the same script takes 0.57 s when each exit is called only while its side is open.

The smallest case is a script whose only order call is one strategy.exit: 2.0 s for 105,136 bars, against 0.05 s for a script with no order call.

The time grows with the number of bars and with the number of such calls, and the scratch cache sizes do not change it.

Versions

pineforge-engine v1.12.0 (6e27d0f2) with pineforge-codegen 1.12.0, built from source as Release, Apple clang 17.0.0, macOS 15.8 arm64 (M1 Pro). Binance BTCUSDT 30m bars from 2020-01-01 to 2026-01-01 (105,136 bars), initial capital 10000, no commission or slippage, default scratch settings.

Time is the wall time of the strategy's run over the bars; translation, compilation and data loading are not included. One run at a time, median of 5.

Reproduction

The control calls each exit on the bar that places its entry and while its side is open:

//@version=6
strategy('Exits while their side is open', overlay=true, initial_capital=10000, default_qty_type=strategy.fixed, default_qty_value=0.01)
fast = ta.ema(close, 20)
slow = ta.ema(close, 60)
var float longStop = 0.0
var float longLimit = 0.0
var float shortStop = 0.0
var float shortLimit = 0.0
goLong = ta.crossover(fast, slow) and strategy.position_size == 0
goShort = ta.crossunder(fast, slow) and strategy.position_size == 0
if goLong
    longStop := close * 0.97
    longLimit := close * 1.06
    strategy.entry('Long', strategy.long)
if goShort
    shortStop := close * 1.03
    shortLimit := close * 0.94
    strategy.entry('Short', strategy.short)
if goLong or strategy.position_size > 0
    strategy.exit('Long exit', 'Long', stop = longStop, limit = longLimit)
if goShort or strategy.position_size < 0
    strategy.exit('Short exit', 'Short', stop = shortStop, limit = shortLimit)

The slow script is the same with the two if lines above the exits removed, so both exits are called on every bar:

strategy.exit('Long exit', 'Long', stop = longStop, limit = longLimit)
strategy.exit('Short exit', 'Short', stop = shortStop, limit = shortLimit)

Both give the same 526 trades and the same net profit (-360.24).

Script Time Per bar Trades
No order call (plot(close) only) 0.05 s 0.5 µs 0
One strategy.exit('Long exit', 'Long', stop = close * 0.97, limit = close * 1.06) and nothing else 2.01 s 19 µs 0
The same and a second exit, 'Short exit' for 'Short' 4.71 s 44 µs 0
Control: each exit while its side is open 0.57 s 5.2 µs 526
Both exits on every bar 4.69 s 44 µs 526
Control plus strategy.exit('Other exit', 'Other', stop = longStop, limit = longLimit) on every bar ('Other' is never entered) 3.49 s 32 µs 526

On 5m bars over the same period (630,793 bars) the control takes 2.17 s and the script with both exits on every bar 20.97 s (891 trades each), and the one-exit script 12.37 s: the cost per bar stays the same.

With strategy_set_cache_policy_v1 giving kinds 4, 5, 6 and 12 4 MiB each, the 30m times are 0.66 s, 4.98 s and 2.02 s for the control, both exits on every bar and the one-exit script.

Where the time goes

macOS sample of the script with both exits on every bar, 5m bars, 6,713 samples in BacktestEngine::run:

  • 42%: PineExecutionAdapter::on_bar_open → erase_retired_rows → sweep_retired_rows → PlacementTable::erase → PlacementTable::change_sequences, SequenceIndex::remove, SequenceIndex::erase
  • 24%: PineStrategyHost::strategy_exit → PineExecutionAdapter::exit → submit_or_replace and remember_live → PlacementTable::try_emplace
  • By function on top of the stack: memmove 19.5%, PlacementTable::SequenceIndex::Key::byte 16.5%, scratch_impl::Core::victim, fetch and hfind_leaf 15% together, SequenceIndex::Key::read 5.4%, pwrite 3.8%

So each such call seems to add a placement row that the next bar open erases again (src/source/pine_adapter.cpp, src/source/pine_placement_retention.hpp). This is a profile, not a reading of the source.

Expected

A strategy.exit whose entry has no order and no position costs about as much as a call that is not made, so the script with both exits on every bar runs in about the control's time.

Workaround

Call each exit only on the bar that places its entry and while its side is open, as the control does. That is a change to the script, and it has to be checked against TradingView for each script, so it does not help when the script must run as published.

Activity

  1. luisleo526 commented on Nov 13, 2026

    @luisleo526
    Collaborator

    Thank you for the clear report, the minimal case and the control; the numbers and the setup make this easy to follow.

    We will reproduce it on a Release build of v1.12.0 with your two scripts and bar count, on our own hardware, and report the measured times here. We will also check whether the per-bar cost comes from the same exit-arming path that another exit change currently touches, so that one change addresses both if it can.

    Triage result (reproduced or not, and where the time goes) will be posted here by 2026-11-15.

    🤖 Generated with Claude Code

  2. luisleo526 commented on Nov 14, 2026

    @luisleo526
    Collaborator

    Status 2026-11-14: the fix for this issue is in final verification (a lab run on the release build). It ships in PineForge 1.13.0 (release 2026-11-16), and this issue closes when it merges.

  3. added a commit that references this issue on Nov 14, 2026
  4. luisleo526 commented on Nov 14, 2026

    @luisleo526
    Collaborator

    Update 2026-11-14: a first change for this merged on main in ecc3348. When an exit names a from_entry that has no order or position, the engine no longer maintains the order-sequence index it does not use. Printed orders and trade records are identical before and after on all four cases we measured, and the sequence work on those cases drops to zero. It ships in PineForge 1.13.0 (release planned 2026-11-16).

    A correction to our status comment earlier today: this issue does NOT close on this merge. The change does not yet come with a speed measurement. We still owe you the measured times on a Release build with your two scripts and bar count, as promised in our triage comment, by 2026-11-15. The issue stays open until those numbers are posted here.

    🤖 Generated with Claude Code

  5. luisleo526 commented on Nov 15, 2026

    @luisleo526
    Collaborator

    Measured times, as promised in our triage comment. Short version: the change merged in ecc3348 makes the scripts with exits about three times faster and the results are byte-identical, but an exit whose entry has no order and no position still costs about 9.5 µs per bar on main. That is not yet "about as much as a call that is not made", so this issue stays open while we remove the remaining cost.

    Setup

    • Builds: PineForge Engine v1.12.0 (6e27d0f2) and current main (5cf08f5c, which includes ecc3348c), each built from source with the engine's default Release settings. The engine library is compiled as a default build compiles it; the tests and the tutorial were not built, the examples are off by default, and the small helper our benchmark runner needs was built.
    • Machine: an arm64 cloud instance (16 vCPU, Ubuntu 24.04, GCC 13.3), one run at a time.
    • Data: public Binance BTCUSDT spot bars from 2020-01-01 to 2026-01-01: 30m (105,143 bars) and 5m (630,834 bars). These counts differ slightly from yours (105,136 and 630,793), so our trade counts differ by one or two (527 against your 526 on 30m).
    • Timing: wall time of the strategy's run only (no translation, compilation or data loading), 5 repeats per case, median. The 5 repeats of every case stay within 1.5% of their median. Your six scripts, with the default caches and with 4 MiB for kinds 4, 5, 6 and 12.

    30m bars, default caches (median of 5)

    Script v1.12.0 main main / v1.12.0
    No order call (plot(close) only) 0.103 s 0.106 s 1.03
    One exit, nothing else 3.36 s 1.11 s 0.33
    Two exits, nothing else 7.26 s 1.99 s 0.27
    Control: each exit while its side is open 0.617 s 0.596 s 0.97
    Both exits on every bar 6.44 s 2.17 s 0.34
    Control plus 'Other exit' on every bar 4.64 s 1.91 s 0.41

    On 5m bars the picture is the same (one exit 20.0 s → 6.62 s, both exits on every bar 31.5 s → 11.2 s, control 3.84 s → 3.77 s), and the 4 MiB cache setting gives the same ratios (0.27 to 0.46 on the scripts with exits). Trades, equity and position traces are byte-identical between the two builds in every run.

    What this means

    • On this machine an exit whose entry has no order and no position cost about 31 µs per bar on v1.12.0 (you measured about 19 µs on an M1 Pro); on main it costs about 9.5 µs per bar.
    • Your expected result is that such a call costs about as much as a call that is not made, so the script with both exits on every bar should run in about the control's time. On main it still takes 2.17 s against the control's 0.60 s. So this is not fixed yet; the issue stays open, and we will post again here when the remaining cost is gone.
    • The script with no order call is about 3% slower on main (0.103 s → 0.106 s).

    Limits of this measurement

    • No noise policy was set for these timings, so they are raw medians without an automatic faster/slower verdict; the spreads above are the measured ones.
      (Corrected 2026-11-15: an earlier version of this line said that a contention check during the run reported a busy interval. That result came from a deliberately constructed test of the check itself, not from the measurement runs.)
    • One build setting (how the version string is embedded in the library) is still being confirmed to match a default build exactly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions