Repository navigation
Description
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
- Both legs active from the fill.
profitsets the take-profit in ticks from the entry price andstopsets 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. - 33 trades as in the control. Without
from_entry, onestrategy.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. - 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), andprofitandlossare 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. - 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):sizeandlastClosedare 0 or negative on every bar, andsizehas the same range and last value as the control'sstrategy.position_size. - On historical bars, the day's final close from the first hourly bar of the day. With
lookahead_onand 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 barsdailyis 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
- Give both legs as prices (
limitandstop), as in the control. - Pass
from_entry. - None tested that keeps pyramiding. With
pyramiding = 1the tick exits are correct. - For an open trade, take the sign from
strategy.position_size. For a closed trade we have none tested. - 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
luisleo526 commentedon Nov 1, 2026
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.
luisleo526 commentedon Nov 5, 2026
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.
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.
luisleo526 commentedon Nov 5, 2026
Here is the update we promised for 04:00 UTC today. We are sorry we missed the original November 3 deadline.
lookahead_onand the first clipped weekly bucket: the expected weekly value remainsnauntil 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.
luisleo526 commentedon Nov 6, 2026
We’re sorry we missed the previous update. Here is the current status for each open case:
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.
luisleo526 commentedon Nov 6, 2026
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.
luisleo526 commentedon Nov 7, 2026
Update on item 5 of the report in pineforge-codegen-oss#295 (
strategy.exitin ticks, called without a usablefrom_entry): it is fixed on the engine's main branch.What was reported. The script enters long with a market
strategy.entrywhen two EMAs cross and, on every bar, callsstrategy.exit('Long exit', na, profit = 30000, loss = 30000): the profit and the loss in ticks, andnaasfrom_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 ofstrategy.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.exitgiven in ticks withoutfrom_entrythat follows a market entry in the same execution now arms when that entry fills, the way the exit with an explicitfrom_entryalready 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.emaseeds differently from TradingView, which PineForge reports as PF-W1505; that is a separate matter):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 withoutfrom_entryand the script with an explicitfrom_entrygive 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.
luisleo526 commentedon Nov 7, 2026
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.