Skip to content

Five order and request.security behaviours that differ from TradingView: exits placed with the entry, per-entry tick exits, trade size sign, lookahead_on #694

Open

Description

@letiep97

Summary

In each case below the generated C++ passes the arguments the Pine call has, and the result differs from what TradingView documents. Each case has a control that differs in one place and behaves as documented.

# Pine form PineForge result
1 strategy.exit(..., profit = <ticks>, stop = <price>) called with the entry the stop is not active: 9 of 22 trades pass their stop and stay open
2 strategy.exit() without from_entry, called with the entry the exit does not cover that entry until the next call
3 pyramiding, profit/loss in ticks exits measured from the average entry price, not from each entry's price
4 strategy.opentrades.size() / strategy.closedtrades.size() of a short positive
5 request.security(..., 'D', close, lookahead = barmerge.lookahead_on) the current chart close (developing day), not the day's close

Cases 1 and 2 may share a cause: in both, the exit is called on the bar where the entry order is placed, before it fills. Their controls show that an exit called on that bar works when it has a from_entry and both legs of one kind (two prices, or two tick distances).

Versions

pineforge-engine v1.9.1 (fc0a2df), pineforge-codegen 1.9.1, Apple clang 17.0.0, macOS 15.8 arm64. Each script was built as is and run on BTCUSDT 1h bars from 2024-01-01 to 2024-03-01 (1,441 bars), initial capital 10000 through strategy_set_override, no commission or slippage, input timeframe = script timeframe = 1h. Values were read back with // @pf-trace lines; trade lists come from the run report. The tick size of the series is 0.01, so 50000 ticks are 500. Codegen main at c1cc532 emits the same calls and gives the same results.

Reproduction

Cases 1 to 4 share this header:

//@version=6
strategy('repro', default_qty_type = strategy.percent_of_equity, default_qty_value = 10)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)

1. profit in ticks with stop as a price, called with the entry

if ta.crossover(fast, slow)
    strategy.entry('Long', strategy.long)
    strategy.exit('Long exit', 'Long', profit = 50000, stop = close * 0.99)

Generated: strategy_exit(std::string("Long exit"), std::string("Long"), na<double>(), (current_bar_.close * 0.99), na<double>(), na<double>(), na<double>(), 100.0, "", na<double>(), "", 50000, na<double>());

22 trades. In 9 of them the bar low reaches the stop before the trade closes. Example: entry on bar 118 at 43946.26, stop 43506.80, the low reaches 43506.80 on bar 126, but the trade stays open and closes on bar 157 at the take-profit 44446.26 (entry + 500).

Control, limit = close * 1.012, stop = close * 0.99: 29 trades, none passes its stop; the same entry on bar 118 is stopped out on bar 126 at 43506.79.

2. strategy.exit() without from_entry, called with the entry

if ta.crossover(fast, slow)
    strategy.entry('Long', strategy.long)
    strategy.exit('Long exit', profit = 30000, loss = 30000)

Generated: strategy_exit(std::string("Long exit"), "", ...); with 30000, 30000 for profit and loss.

17 trades, none exits on its entry bar. The first entry, bar 87 at 43676.07, closes at 43976.07 (entry + 300) on bar 118, the bar of the next call, although its take-profit was reached on bar 87.

Control, strategy.exit('Long exit', 'Long', profit = 30000, loss = 30000): 33 trades, 9 exit on their entry bar; the bar 87 entry closes on bar 87 at 43976.07.

3. Pyramiding with tick exits

// header with pyramiding = 2 added to strategy()
if ta.crossover(fast, slow) or (strategy.position_size > 0 and ta.crossover(close, fast))
    strategy.entry('Long', strategy.long)
strategy.exit('Long exit', 'Long', profit = 50000, loss = 50000)

Generated: cfg.pyramiding = 2; and strategy_exit(std::string("Long exit"), std::string("Long"), ..., 50000, 50000);

50 trades; only 10 exit exactly 50000 ticks from their own entry price. Example: entries on bar 118 at 43946.26 and bar 124 at 43931.10 both close on bar 126 at 43438.68, which is their average entry price 43938.68 minus 500. The first exit distances in ticks are 50000, 50758, 49242, 54565, 45427.

Control, pyramiding = 1: 30 trades, all 30 exit exactly 50000 ticks from their entry.

4. Trade size of a short is positive

if ta.crossunder(fast, slow)
    strategy.entry('Short', strategy.short)
if ta.crossover(fast, slow)
    strategy.close('Short')
float size = strategy.opentrades > 0 ? strategy.opentrades.size(0) : 0.0
// @pf-trace size=size
float lastClosed = strategy.closedtrades > 0 ? strategy.closedtrades.size(strategy.closedtrades - 1) : 0.0
// @pf-trace lastClosed=lastClosed

The script only opens shorts (36 trades). size ranges from 0 to +0.02527 and lastClosed from 0 to +0.02527. With float size = strategy.position_size instead, size ranges from -0.02527 to 0.

Generated: open_trade_size(0) and closed_trade_size(...). They return the stored quantity without a sign: BacktestEngine::open_trade_size in src/engine_trade_accessors.cpp returns pyramid_entries_[idx].qty, and closed_trade_size in include/pineforge/engine.hpp returns trades_.qty_at(idx).

5. lookahead_on on a higher timeframe without an offset

//@version=6
strategy('repro')
daily = request.security(syminfo.tickerid, 'D', close, lookahead = barmerge.lookahead_on)
// @pf-trace daily=daily
plot(daily)

Generated: register_security_eval(0, "D", input_tf_, true, false);. The run's security diagnostics show 60 complete and 1381 partial evaluations.

daily equals the chart bar's own close on 1441 of 1441 bars, and equals the UTC day's final close (computed from the same 1h bars) on only 61. Example, bar 10: daily is 42691.10, the chart close; the day's final close is 44179.55.

Control, close[1] with lookahead_on: daily equals the previous UTC day's final close on all 1417 bars where it is defined.

Codegen's own warning on this script says the completed value is used: "request.security lookahead_on changes HTF timing: the completed higher-timeframe value is exposed from the bucket's first chart bar". The engine instead runs the payload on the developing daily bar. docs/coverage.md describes partial updates under lookahead_on as the intended higher-timeframe behaviour.

Expected

  1. Both legs active from the fill. profit sets the take-profit in ticks from the entry price and stop sets the stop-loss price; they are separate legs (take-profit and stop-loss). Checked on TradingView (BINANCE:BTCUSDT 1h): no trade stays open on a bar whose low reaches its stop; the bar 118 entry at 43946.26 is stopped out on bar 126 at 43506.79, as in the control.
  2. 33 trades as in the control. Without from_entry, one strategy.exit() call places exit orders for every entry created before or on the bar of the call (exits for multiple entries). Checked on TradingView (BINANCE:BTCUSDT 1h): the script and its control give the same trade list: 33 trades from bar 0 to bar 1440, 9 of them closed on their entry bar; the bar 87 entry closes on bar 87 at 43976.07.
  3. Each entry closes 500 from its own entry price. With several entries of one id, strategy.exit() places a separate exit order for each trade (exits for multiple entries), and profit and loss are measured from the entry price (take-profit and stop-loss). Checked on TradingView (BINANCE:BTCUSDT 1h): the bar 118 and bar 124 entries close on bar 126 at 43446.26 and 43431.10, each 500 below its own entry price. Every exit fills at an entry's own price plus or minus 500; when two exits fill, TradingView closes the trades first in, first out, so for 20 of the 63 trades closed by bar 1440 the exit price is the other entry's level. The control gives 30 trades, all exactly 50000 ticks from their entry.
  4. Negative for a short. Both functions return the direction together with the size: positive for a long trade, negative for a short (strategy.opentrades.size(), strategy.closedtrades.size()). Checked on TradingView (BINANCE:BTCUSDT 1h): size and lastClosed are 0 or negative on every bar, and size has the same range and last value as the control's strategy.position_size.
  5. On historical bars, the day's final close from the first hourly bar of the day. With lookahead_on and no offset, historical values of a higher timeframe are taken from the start of each period and already hold the period's final value; only realtime bars see the developing value (lookahead). If the partial update is intended, the codegen warning above describes the opposite and could be aligned, and a note that this form differs from TradingView would help. Checked on TradingView (BINANCE:BTCUSDT 1h): on historical bars daily is the UTC day's final close from the day's first hourly bar (44179.55 on bar 0) and does not change within a day; it equals the chart close only on the day's last bar. The control gives the previous day's close, 61130.98 on bar 1440, as on PineForge.

Workaround

  1. Give both legs as prices (limit and stop), as in the control.
  2. Pass from_entry.
  3. None tested that keeps pyramiding. With pyramiding = 1 the tick exits are correct.
  4. For an open trade, take the sign from strategy.position_size. For a closed trade we have none tested.
  5. None for this form. With an offset (close[1]) the values agree with TradingView's, but they are the previous day's close, so the script's meaning changes.

Activity

  1. luisleo526 commented on Nov 1, 2026

    @luisleo526
    Collaborator

    Thank you for the detailed report. We are checking each case against TradingView's behaviour and our source, and will post a result for each case in this issue by the end of 2026-11-03 (UTC). If a case needs longer, we will say so here with the reason.

  2. luisleo526 commented on Nov 5, 2026

    @luisleo526
    Collaborator

    We missed the date we gave for this update (the end of 2026-11-03 UTC), and we're sorry. Here is where each case stands.

    1. Mixed tick/absolute exits: not yet confirmed against TradingView on your script. The cause is still under investigation. Fix: in progress; no PR yet; planned for 1.11.0.
    2. Global same-bar exit parenting: not yet confirmed against TradingView on your script. The cause is still under investigation. Fix: in progress; no PR yet; planned for 1.11.0.
    3. Per-entry levels and FIFO attribution: not yet confirmed against TradingView on your script. Our current test of the fill-derived stop level still fails, so the cause is not closed yet. Fix: in progress; no PR yet; planned for 1.11.0.
    4. Signed trade sizes: not yet re-confirmed against TradingView on your script. Cause found: stored lot and trade quantities are positive, and the accessors report them without their long/short direction. Fix: in progress; no PR yet; planned for 1.11.0.
    5. Historical completed higher-timeframe projection: not yet re-confirmed against TradingView on your script. Cause found: the historical projection default is off, separately from its explicit control. Fix: in progress; no PR yet; planned for 1.11.0.

    The fixes are in progress, so numbers measured now may still change. We will post the final parity result for each case here when its fix is released.

    Our next update here will be by 2026-11-05 04:00 UTC, together with the 1.11.0 release plan.

  3. luisleo526 commented on Nov 5, 2026

    @luisleo526
    Collaborator

    Here is the update we promised for 04:00 UTC today. We are sorry we missed the original November 3 deadline.

    1. Mixed absolute and tick-based exits: The original-script comparison with TradingView is not yet complete. The handling of an absolute sibling when a relative exit is activated is being repaired. Fix: in progress; fix PR/merged commit: none to report; planned release: 1.11.0, subject to validation.
    2. Exits without an entry selector: The original-script comparison with TradingView is not yet complete. Coverage of entries created later in the same calculation is being repaired. Fix: in progress; fix PR/merged commit: none to report; planned release: 1.11.0, subject to validation.
    3. Per-entry tick levels and FIFO attribution: The original-script comparison with TradingView is not yet complete. Earlier checks failed, and corrected test inputs and source changes still need a fresh run before we can claim a fix. Fix: in progress; fix PR/merged commit: none to report; planned release: 1.11.0, subject to validation.
    4. Signed open and closed trade sizes: The original-script comparison with TradingView is not yet complete. The candidate preserves long/short direction when reporting stored quantities, and its focused regression passes; independent review and integration remain outstanding. Fix: in progress; fix PR/merged commit: none to report; planned release: 1.11.0, subject to validation.
    5. Historical higher-timeframe values: The original-script comparison with TradingView is not yet complete. The candidate enables final-period historical projection and passes a focused regression. The same work also covers the reported developing hourly value under lookahead_on and the first clipped weekly bucket: the expected weekly value remains na until a bucket starting inside the loaded window completes. Those additional cases still need validation. Fix: in progress; fix PR/merged commit: none to report; planned release: 1.11.0, subject to validation.

    These fixes are still in progress, so current measurements may change. We will post the final parity result for each case here when its fix is released.

    The 1.11.0 release date has not yet been confirmed. We will add the release-planning decision here once it is finalized.

  4. luisleo526 commented on Nov 6, 2026

    @luisleo526
    Collaborator

    We’re sorry we missed the previous update. Here is the current status for each open case:

    1. Mixed tick and absolute exits: In progress. Investigation and fix work continue; the final parity check is still pending.
    2. Global same-bar exit parenting: In progress. Investigation and fix work continue; the final parity check is still pending.
    3. Per-entry levels and FIFO attribution: In progress. The behavior is still being worked through; the final parity check is still pending.
    4. Signed trade sizes: In progress. The directional fix is written, and the broader caller-test update is under way. This case is not complete yet.
    5. Historical completed higher-timeframe projection: In progress. The cause and fix work remain under review; the final parity check is still pending.

    Numbers in earlier comments may still change. No case is marked fixed until its fix ships and its final parity check is complete; we will post each result here when it is ready.

  5. luisleo526 commented on Nov 6, 2026

    @luisleo526
    Collaborator

    Signed trade-size update for engine issue #694, case 4:

    The fix is in pull request #760. It changes short-trade size reporting to preserve direction while retaining the existing quantity values. The fix is not on the main branch yet; CI and parity checks are still pending. We will post another update when it lands. No release date is promised.

  6. luisleo526 commented on Nov 7, 2026

    @luisleo526
    Collaborator

    Update on item 5 of the report in pineforge-codegen-oss#295 (strategy.exit in ticks, called without a usable from_entry): it is fixed on the engine's main branch.

    What was reported. The script enters long with a market strategy.entry when two EMAs cross and, on every bar, calls strategy.exit('Long exit', na, profit = 30000, loss = 30000): the profit and the loss in ticks, and na as from_entry, which means the same as leaving it out. TradingView binds such an exit to the entry that was placed before it in the same execution, so its legs can fill on the entry's own bar: its list has 9 trades closed on their entry bar. The engine applied the legs one bar late, with the next call of strategy.exit, and for an exit called only on the signal bar it did not apply them at all.

    What changed. Pull request #769 was merged into main on 2026-11-07 (squash commit 06eb496). A strategy.exit given in ticks without from_entry that follows a market entry in the same execution now arms when that entry fills, the way the exit with an explicit from_entry already did.

    What it measures. We ran the reporter's script on a one-hour BTCUSDT feed with the engine built from main just before the change (b09f79d) and with the engine built from the merge commit (06eb496), and counted the trades filled from bar 21 on (before that, ta.ema seeds differently from TradingView, which PineForge reports as PF-W1505; that is a separate matter):

    • before: 32 trades, none closed on its entry bar; the entry on bar 87 (at 43676.07) closed on bar 88 at 44051.10;
    • after: 33 trades, 9 closed on their entry bar; the entry on bar 87 closes on bar 87 at 43976.07.

    The reporter's TradingView list has 33 trades, 9 of them closed on their entry bar, and the same bar-87 exit at 43976.07. Counted from the first bar, the engine gives 34 trades before and 35 after; the two extra ones fall in the first 21 bars. After the change, the script with na, the script without from_entry and the script with an explicit from_entry give the same trade list. In the same run, every other script (the other items of that report and the related reports) gave identical results on both commits.

    What is not changed. Entries with a limit or a stop price are not changed (the exit is bound only to a market entry), and nothing changes under calc_on_order_fills. An unnamed exit that comes before the entry statement in the script, so that no entry exists when it is called, still has no effect on that entry. No TradingView tape pins the shapes with several market entries sharing one unnamed exit, or with an entry that fills on a later bar (pyramiding); this change makes no claim about them. Strategies that use the shape in the report will see their trades move; that is the point of the change.

    The change is on main now. It is not in a published release yet, and this update gives no release date.

  7. luisleo526 commented on Nov 7, 2026

    @luisleo526
    Collaborator

    Update on case 4 (trade size sign): fixed on main in 63f3978 (#776, which carries #764). Pine trade-size accessors now return positive quantities for long trades and negative quantities for short trades, as TradingView documents; raw C report quantities keep their magnitude with a separate direction field. This ships in 1.11.0.

    Cases 1, 2, 3 and 5 are not fixed yet, so this issue stays open; we will post here when each one is on main.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions