Skip to content

calc_on_order_fills = true: a run that calls ta.sma, ta.stdev or ta.bb takes 2.5 to 3.2 times as long on v1.11.0 as on v1.10.0 #809

Open

Description

@letiep97

Summary

With calc_on_order_fills = true, a script that calls ta.sma(), ta.stdev() or ta.bb() runs 2.5 to 3.2 times slower on v1.11.0 than on v1.10.0. The trades are identical on both versions; only the run time changes.

Without calc_on_order_fills the same scripts are 8 to 18% faster on v1.11.0, so the regression is limited to the executions that follow an order fill. ta.ema() is not affected; ta.highest() and ta.wma() hardly (1.05 and 1.11 times).

Versions

pineforge-engine v1.11.0 (a4472324) with pineforge-codegen 1.11.0, and v1.10.0 with Codegen 1.10.0. Both built from source as Release with the same options, Apple clang 17.0.0, macOS 15.8 arm64 (v1.11.0 with the two local substitutions described in #655; neither is on this path). BTCUSDT 1h bars from 2024-01-01 to 2026-01-01 (17,545 bars), initial capital 10000, no commission or slippage.

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

Reproduction

//@version=6
strategy("40 x sma", overlay = false, initial_capital = 10000, calc_on_order_fills = true)
total = 0.0
total += ta.sma(close, 20)
total += ta.sma(close, 21)
// ... one line per length, up to
total += ta.sma(close, 59)
plot(total)
up = ta.crossover(ta.ema(close, 3), ta.ema(close, 8))
down = ta.crossunder(ta.ema(close, 3), ta.ema(close, 8))
if up
    strategy.entry("Long", strategy.long, qty = 0.01)
if down
    strategy.entry("Short", strategy.short, qty = 0.01)

The other rows replace the 40 ta.sma lines with the call shown, lengths 20 to 59 again; ta.bb is [mid0, upper0, lower0] = ta.bb(close, 20, 2.0) followed by total += upper0, and so on. "No indicator" has plot(close) instead of the 42 lines. The control of each row is the same script without calc_on_order_fills = true.

Every script gives 2,241 closed trades and one open trade, the same list on both versions.

40 calls of Without calc_on_order_fills: v1.10.0 v1.11.0 With calc_on_order_fills = true: v1.10.0 v1.11.0 v1.11.0 / v1.10.0
no indicator 337 ms 274 ms 318 ms 269 ms 0.85
ta.sma(close, n) 329 ms 281 ms 824 ms 2,114 ms 2.56
ta.stdev(close, n) 335 ms 292 ms 1,341 ms 4,209 ms 3.14
ta.bb(close, n, 2.0) 348 ms 296 ms 1,860 ms 5,923 ms 3.19
ta.highest(high, n) 346 ms 285 ms 669 ms 701 ms 1.05
ta.wma(close, n) 419 ms 384 ms 730 ms 810 ms 1.11
ta.ema(close, n) 313 ms 277 ms 326 ms 277 ms 0.85

Taking the "no indicator" row away, the time that calc_on_order_fills adds per filled order (2,242 fills) is:

40 calls of v1.10.0 v1.11.0
ta.sma 0.23 ms 0.82 ms
ta.stdev 0.46 ms 1.76 ms
ta.bb 0.69 ms 2.52 ms

The window length matters little. With lengths 10 to 49, 100 to 139 and 1000 to 1039 over one year of bars (median of 3 runs), ta.sma is 2.7, 2.7 and 1.9 times slower on v1.11.0 and ta.stdev 2.9, 2.9 and 2.3 times.

What may be related

Codegen 1.10.0 and 1.11.0 generate identical C++ for the ta.sma and ta.stdev scripts (for ta.bb it differs only in the lines that assign the tuple's variables), so the change is in the engine. In v1.11.0 the two std::deque<double> members of KahanWindowSum (include/pineforge/window_sum.hpp) became OwnedDeque<double>, a std::deque with OwnedAllocator. The generated strategy keeps a copy of every indicator object in its script-state checkpoint (_pf_script_state_checkpoint_) and assigns the copies back when it restores, so each copy of an SMA, a standard deviation or a Bollinger band now goes through that allocator. This is a reading of the source, not a profile.

Expected

The run time of v1.10.0, or better, as for the scripts without calc_on_order_fills.

Workaround

None known, short of not using calc_on_order_fills.

Activity

  1. letiep97 commented on Nov 10, 2026

    @letiep97
    Author

    Three more rows from the same script and measurement (median of 3 runs), all with calc_on_order_fills = true:

    40 lines of v1.10.0 v1.11.0 v1.11.0 / v1.10.0
    total += ta.change(close, n), n from 20 to 59 652 ms 1,206 ms 1.85
    v0 = 0.0, v0 := 0.5 * close + 0.5 * nz(v0[1]), total += v0 (40 such variables) 678 ms 794 ms 1.17
    total += f(close, n), n from 10 to 49, where f(source, length) sums source[i] * (length - i) in a for i = 0 to length - 1 loop 935 ms 1,076 ms 1.15

    ta.rsi, ta.atr, ta.cum and ta.lowest are not slower (0.73 to 0.98). So ta.change belongs with ta.sma, ta.stdev and ta.bb, and variables read with a history offset are affected a little.

  2. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Thank you for the detailed timings and the repro.

    We will reproduce this on v1.10.0 and v1.11.0 with your scripts, and find which change slowed the recompute path when calc_on_order_fills = true. Our triage result will be posted here by 2026-11-11 18:00 (UTC+8). If the cause and a fix are found in time, the fix ships in 1.11.1; otherwise 1.11.1's notes will name this as a known regression.

  3. luisleo526 commented on Nov 11, 2026

    @luisleo526
    Collaborator

    Update: we missed the 18:00 (UTC+8) triage time we gave here. The bisect between v1.10.0 and v1.11.0 on your scripts has not finished yet, so the cause is not identified.

    One related measurement: a change under review for the moving-average window (exact window sums for ta.sma) runs your calc_on_order_fills = true shape about a third faster in our first timing. That timing ran on a busy machine and is being repeated. It is not a fix for the regression you found, and we have not confirmed the cause.

    New time for the triage result: 2026-11-12 12:00 (UTC+8), here. If the cause is found and the fix is small, it goes into the next engine release. Otherwise that release's notes will name this as a known regression.

  4. luisleo526 commented on Nov 12, 2026

    @luisleo526
    Collaborator

    Thanks for the report. Status update:

    • Confirmed and located. The slowdown comes from a storage change in 1.11.0 that affects the per-bar snapshots of windowed indicators (for example ta.sma, ta.stdev and Bollinger Bands) when calc_on_order_fills = true. On our measurement machine those snapshots are about three times slower than in the previous version; that factor is ours, not a measurement of your setup.
    • Results are not affected. On the cases we checked, the trades are identical; only the run time changed.
    • Cause narrowed. Switching the window-sum queue back to the previous storage in an experiment recovers most of the slowdown. A smaller part, in ta.highest and ta.wma, is not explained yet.
    • Next. The fix plan is being prepared now; we will update this issue with the fix and the release that carries it.

    🤖 Generated with Claude Code

  5. luisleo526 commented on Nov 13, 2026

    @luisleo526
    Collaborator

    Status 2026-11-13: the cause is located (see above); the fix plan is in review. The fix date is posted here by 2026-11-14.

    🤖 Generated with Claude Code

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