Skip to content

Five valid v6 scripts that translate and run with wrong values and no diagnostic #291

Open

Description

@letiep97

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:

  1. diff is 0. The [] operator can follow an expression and refers to past bars (history-referencing operator). Checked on TradingView (BINANCE:BTCUSDT 1h): diff is 0 on every bar, as in the control.
  2. v equals hist. A function can read any global declared before it, tuple elements included (tuple declarations, function scopes). Checked on TradingView (BINANCE:BTCUSDT 1h): v equals hist on every bar, with the same values as the control (first defined on bar 33, -245.641901 on bar 1440).
  3. seen is 481. A string declared with na is na until it is assigned (na value). Checked on TradingView (BINANCE:BTCUSDT 1h): seen is 481 on bar 1440, as in the control.
  4. n and total are 3 (array.new<type>() takes size and initial_value by name or by position). Checked on TradingView (BINANCE:BTCUSDT 1h): n and total are 3 on every bar, as in the control.
  5. mult is 4 on every bar. A variable declared without var is declared again from its initial expression on each bar (variable reassignment, execution model). Checked on TradingView (BINANCE:BTCUSDT 1h): mult is 4 on every bar, as in the control.

Workaround

  1. Name the expression first (mid = (close + open) / 2) and index the variable.
  2. Copy the tuple element to a plain variable and read that variable in the function.
  3. Write string(na), or declare the variable with var.
  4. Pass the array.new arguments by position.
  5. Keep the input in its own variable and compute from it.

Activity

  1. luisleo526 commented on Nov 1, 2026

    @luisleo526
    Contributor

    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 6, 2026

    @luisleo526
    Contributor

    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.

    # Case TradingView Before main at 5b72a0c Matches TradingView?
    1 [] on an expression inside a loop diff is 0 on every bar diff from -3183.41 to 2842.82 diff is 0 on every bar that has a value (bars 3 to 1440) Yes
    2 a global from a tuple declaration read inside a function v equals hist; first value on bar 33; -245.641901 on bar 1440 v is 0 on every bar v equals hist on every bar from bar 33 on; -245.6419009848 on bar 1440; PineForge also returns a number on bars 0 to 32 Yes for the reported defect. No on bars 0 to 32: PineForge returns a number where TradingView has na. 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 report
    3 string x = na without var seen is 481 on bar 1440 1441 481 Yes
    4 array.new<float>(size = 3, initial_value = 1.0) n and total are 3 on every bar 0 and na 3 and 3 on every bar Yes
    5 x = input.float(...) then x := x * 2 mult is 4 on every bar 4, 8, 16, ... inf 4 on every bar Yes

    Still open: the difference of case 2 on bars 0 to 32. ta.macd on 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).

  3. luisleo526 commented on Nov 8, 2026

    @luisleo526
    Contributor

    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 7d7681dc with engine main at 755e6658, on the same 1,441-bar BTCUSDT 1h sample used in our earlier comment.

    # Case Result on main today Change Matches the value the issue prints?
    1 [] on an expression inside a loop diff is 0 on every bar with a value, bars 3 to 1440 #327 (08261a24) Yes
    2 a global from a tuple declaration read inside a function v equals hist on bars 33 to 1440; v is -245.6419009848 on bar 1440, agreeing with the printed -245.641901 #327 (08261a24) fixes the global read Matches from bar 33; open on bars 0 to 32
    3 string x = na without var seen is 481 on bar 1440 #327 (08261a24) Yes: 481
    4 array.new<float>(size = 3, initial_value = 1.0) n and total are 3 on every bar #324 (a72cfcb1), the earlier array-argument binding fix Yes: 3 and 3
    5 x = input.float(...) then x := x * 2 mult is 4 on all 1,441 bars #327 (08261a24) Yes: 4

    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 of ta.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.

  4. luisleo526 commented on Nov 9, 2026

    @luisleo526
    Contributor

    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.

  5. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Contributor

    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.

  6. luisleo526 commented on Nov 13, 2026

    @luisleo526
    Contributor

    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

  7. luisleo526 commented on Nov 13, 2026

    @luisleo526
    Contributor

    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.macd returns numbers on its first bars, where TradingView returns na. With ta.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, not na, so a guard such as if not na(hist) lets the trade through.

    Workaround until the fix ships. Skip the start-up bars explicitly:

    [macdLine, signalLine, hist] = ta.macd(close, fastLength, slowLength, signalLength)
    ready = bar_index >= slowLength + signalLength - 2
    if ready and ta.crossover(macdLine, signalLine)
        strategy.entry("L", strategy.long)
    

    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.kc and ta.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.

  8. luisleo526 commented on Nov 14, 2026

    @luisleo526
    Contributor

    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.

  9. luisleo526 commented on Nov 14, 2026

    @luisleo526
    Contributor

    Update 2026-11-14: the MACD part of this issue is fixed in the engine on main (ecc3348c). ta.macd no longer publishes values before its inputs are defined. The MACD line now starts at bar slowLength - 1, and the signal and histogram at bar slowLength + 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

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