Repository navigation
Description
Summary
Each script below compiles on TradingView, translates without an error on pineforge-codegen 1.9.1, builds and runs, but the value it computes differs from Pine's. Each one comes with a control that differs in one place and gives the Pine value. The cause of each is visible in the generated C++.
| # | Pine form | What PineForge computes |
|---|---|---|
| 1 | [] on an expression inside a loop, ((close + open) / 2)[i] |
the offset counts loop iterations, not bars |
| 2 | a global from a tuple declaration read inside a function | 0 on every bar |
| 3 | string x = na without var |
na(x) is never true |
| 4 | array.new<float>(size = 3, initial_value = 1.0) |
an empty array |
| 5 | x = input.float(...) then x := x * 2 |
the value doubles on every bar |
Versions
pineforge-codegen 1.9.1, pineforge-engine v1.9.1 (fc0a2df), 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), with initial capital 10000 and no commission or slippage. Values were read back with // @pf-trace lines. Codegen main at c1cc532 (after #290), run from source with the same engine, gives the same values for all five. The causes are in the generated C++, so they should not depend on the platform.
Reproduction
1. [] on an expression inside a loop
//@version=6
strategy('repro')
mid = (close + open) / 2
float a = 0.0
float b = 0.0
for i = 1 to 3
a += ((close + open) / 2)[i]
b += mid[i]
diff = a - b
// @pf-trace diff=diff
plot(a)
diff should be 0. It ranges from -3183.41 to 2842.82 (last bar -599.32). The expression's history buffer is pushed on every iteration:
// inside the for loop
a += ([&]() -> double { double _hv = (((double)((current_bar_.close + current_bar_.open)) / (double)(2))); if (history_advances_new_bar()) _hist_call_1.push(_hv); else _hist_call_1.update(_hv); return _hist_call_1[/* i */]; }());Control: a += mid[i] gives diff 0 on every bar. A constant offset outside a loop, ((close + open) / 2)[1], is also correct.
2. A global from a tuple declaration read in a function is 0
//@version=6
strategy('repro')
[macdLine, signalLine, hist] = ta.macd(close, 12, 26, 9)
float histCopy = hist
readHist() => hist
v = readHist()
diff = v - hist
// @pf-trace v=v
// @pf-trace diff=diff
plot(v)
v is 0 on all 1,441 bars. Generated:
double hist = 0.0; // class member
double readHist() {
return hist; // reads the member
}
void on_source_bar(const Bar& bar) override {
auto _result__ta_macd_1 = /* ... */;
double macdLine = _result__ta_macd_1.macd_line;
double signalLine = _result__ta_macd_1.signal_line;
double hist = _result__ta_macd_1.histogram; // a new local that hides the member
histCopy = hist;
v = readHist();Control: readHist() => histCopy gives diff 0 on every bar.
3. string x = na is never na
//@version=6
strategy('repro')
string tag = na
if bar_index % 3 == 0
tag := 'x'
var int seen = 0
if not na(tag)
seen += 1
// @pf-trace seen=seen
plot(seen)
seen is 1441 on the last bar, so not na(tag) was true on every bar. Generated: tag = na<double>(); on a std::string member, which compiles as a one-character assignment, so the string is never empty, and the check is if (!((tag).empty())). Control: string tag = string(na) gives 481, one bar in three.
4. array.new with named arguments
//@version=6
strategy('repro')
values = array.new<float>(size = 3, initial_value = 1.0)
n = values.size()
total = values.sum()
// @pf-trace n=n
// @pf-trace total=total
plot(total)
n is 0 and total is na on every bar. Generated: values = std::vector<double>();. Control: array.new<float>(3, 1.0) gives 3 and 3.
5. Reassigning an input variable
//@version=6
strategy('repro')
mult = input.float(2.0, 'Mult')
mult := mult * 2
// @pf-trace mult=mult
plot(mult)
mult is 4, 8, 16, ... and reaches inf before the last bar. The input is read once and the member keeps its value between bars:
if (!_inputs_initialized_) {
mult = get_input_double("Mult", 2.0);
_inputs_initialized_ = true;
}
// every bar:
mult = (mult * 2);Control: multInput = input.float(2.0, 'Mult'), mult = multInput, mult := mult * 2 gives 4 on every bar.
Expected
The values the controls give:
diffis 0. The[]operator can follow an expression and refers to past bars (history-referencing operator). Checked on TradingView (BINANCE:BTCUSDT 1h):diffis 0 on every bar, as in the control.vequalshist. A function can read any global declared before it, tuple elements included (tuple declarations, function scopes). Checked on TradingView (BINANCE:BTCUSDT 1h):vequalshiston every bar, with the same values as the control (first defined on bar 33, -245.641901 on bar 1440).seenis 481. A string declared withnais na until it is assigned (navalue). Checked on TradingView (BINANCE:BTCUSDT 1h):seenis 481 on bar 1440, as in the control.nandtotalare 3 (array.new<type>()takessizeandinitial_valueby name or by position). Checked on TradingView (BINANCE:BTCUSDT 1h):nandtotalare 3 on every bar, as in the control.multis 4 on every bar. A variable declared withoutvaris declared again from its initial expression on each bar (variable reassignment, execution model). Checked on TradingView (BINANCE:BTCUSDT 1h):multis 4 on every bar, as in the control.
Workaround
- Name the expression first (
mid = (close + open) / 2) and index the variable. - Copy the tuple element to a plain variable and read that variable in the function.
- Write
string(na), or declare the variable withvar. - Pass the
array.newarguments by position. - Keep the input in its own variable and compute from it.
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 6, 2026
Status: four of the five cases are fixed on main and give TradingView's values. Case 2 is fixed for the defect the report names (the global is no longer 0 in the function), and it differs from TradingView in the first 33 bars. The fixes ship in the next release.
Checked with the code of main at 5b72a0c and the engine at d398279, on Linux aarch64: each script of the issue was translated, the generated C++ was built against the engine and run on BTCUSDT 1h bars from 2024-01-01 to 2024-03-01 (1,441 bars), with initial capital 10000 and no commission or slippage, as in the report. "Before" is what the report measured on 1.9.1. The TradingView values are the ones the report gives.
[]on an expression inside a loopdiffis 0 on every bardifffrom -3183.41 to 2842.82diffis 0 on every bar that has a value (bars 3 to 1440)vequalshist; first value on bar 33; -245.641901 on bar 1440vis 0 on every barvequalshiston every bar from bar 33 on; -245.6419009848 on bar 1440; PineForge also returns a number on bars 0 to 32na.ta.macd(close, 12, 26, 9)on its own also returns a value on bar 0 in our run, so this is not the shape in the reportstring x = nawithoutvarseenis 481 on bar 1440array.new<float>(size = 3, initial_value = 1.0)nandtotalare 3 on every barnax = input.float(...)thenx := x * 2multis 4 on every barinfStill open: the difference of case 2 on bars 0 to 32.
ta.macdon its own shows it (its MACD line, signal line and histogram all have a value on bar 0), so it is not the shape the report names; it is not fixed.Not run: x86 and macOS, and the report's controls (each "Yes" compares with the TradingView value the report gives).
luisleo526 commentedon Nov 8, 2026
Update: four of five cases match the values the issue prints. Case 2 no longer returns 0 in the function and matches from bar 33, but its start-up difference remains open.
Checked on 2026-11-08 on codegen main at
7d7681dcwith engine main at755e6658, on the same 1,441-bar BTCUSDT 1h sample used in our earlier comment.[]on an expression inside a loopdiffis 0 on every bar with a value, bars 3 to 144008261a24)vequalshiston bars 33 to 1440;vis -245.6419009848 on bar 1440, agreeing with the printed -245.64190108261a24) fixes the global readstring x = nawithoutvarseenis 481 on bar 144008261a24)array.new<float>(size = 3, initial_value = 1.0)nandtotalare 3 on every bara72cfcb1), the earlier array-argument binding fixx = input.float(...)thenx := x * 2multis 4 on all 1,441 bars08261a24)For case 2, the values agree from the first bar where TradingView has a value, bar 33. Before that, on bars 0 to 32, PineForge returns a number where TradingView has
na. This is a separate known start-up difference that we are working on. Today's check ofta.macd(close, 12, 26, 9)on its own also gives values from bar 0.The cases marked fixed ship in 1.11.0, whose publish target is 2026-11-10. This issue stays open until every case is resolved.
Clarify unreleased fixes and numeric helper documentation (#353)
luisleo526 commentedon Nov 9, 2026
Status at v1.11.0: four of the five cases are fixed in v1.11.0 (commit 08261a2 and others). Case 2 (a function reading a global declared by a tuple) still has a start-up difference; its fix is planned for v1.11.1. This issue stays open for case 2.
luisleo526 commentedon Nov 10, 2026
Status update (2026-11-10): the fix for case 2 (a function reading a global declared by a tuple, with a start-up difference) is written and in review. It is planned for v1.11.1, whose contents are chosen on 2026-11-12; it goes in only if its review and full checks pass by then. If it misses that cut, I will post the new date here the same day. This issue stays open until the fix ships.
luisleo526 commentedon Nov 13, 2026
Status 2026-11-13: case 2 is not yet confirmed fixed in a released version (the planned 1.11.1 was replaced by 1.12.0). Its status and date are posted here by 2026-11-14.
🤖 Generated with Claude Code
luisleo526 commentedon Nov 13, 2026
Status 2026-11-14, case 2 and a related warning for v1.12.0 and earlier.
What is wrong. In v1.12.0 and every earlier version,
ta.macdreturns numbers on its first bars, where TradingView returnsna. Withta.macd(close, 12, 26, 9), TradingView's histogram first has a value on bar 33. PineForge returns a number on bars 0 to 32. A strategy that trades on these values can therefore enter or exit on bars where it would not trade on TradingView. From bar 33 on, the values match.Checking
na()does not help. On those early bars the value is a number, notna, so a guard such asif not na(hist)lets the trade through.Workaround until the fix ships. Skip the start-up bars explicitly:
For 12/26/9 this means
bar_index >= 33. In our runs, this guard removes the early trades. We have not yet confirmed the general formula against TradingView for other lengths.Other functions. Our source review points to the same start-up handling in other functions built on exponential averages: for example
ta.ema,ta.tsi,ta.kcandta.kcw. We are checking each one against TradingView. We will post the confirmed list, with the first valid bar for each, here by 2026-11-14 12:00 UTC.Fix. The fix is planned for v1.13.0, scheduled for 2026-11-16. If it misses that release, I will post the new date here the same day. This issue stays open until the fix ships.
luisleo526 commentedon Nov 14, 2026
Status 2026-11-14: a fix is in progress and targeted for PineForge 1.13.0 (merge cut-off 2026-11-15, release 2026-11-16). This issue closes when the fix merges. If it misses 1.13.0, we post a correction here the same day.
luisleo526 commentedon Nov 14, 2026
Update 2026-11-14: the MACD part of this issue is fixed in the engine on main (ecc3348c).
ta.macdno longer publishes values before its inputs are defined. The MACD line now starts at barslowLength - 1, and the signal and histogram at barslowLength + signalLength - 2, matching TradingView; values after warm-up are unchanged. It ships in PineForge 1.13.0 (release planned 2026-11-16).The other scripts in this issue (
ta.ema,ta.kc,ta.tsi) are fixed separately, still targeted for 1.13.0 (merge cut-off 2026-11-15). This issue stays open until they merge too, and we post here when they do.🤖 Generated with Claude Code