Skip to content

Series subscript in nested function produces incorrect results (v6.5.0) #67

@danielmayerfrank

Description

@danielmayerfrank

Summary

When a for-loop accesses a Series subscript (close[i]) inside a nested function whose loop bound is dynamic (derived from bar_index), the runtime produces different (fewer) results than the identical logic written inline. The two scripts are semantically equivalent — the only difference is whether the Series-subscript loop lives inside a def or at the top level of main().

Version: pynesys-pynecore 6.5.0
Pine compiles cleanly: yes (pine_check: 0 errors, 0 warnings)

Reproduction

pip install pynesys-pynecore==6.5.0
python generate_data.py
pyne run --plot nested_output.csv nested_function_divergence.py synthetic_5000.csv
pyne run --plot inline_output.csv inline_equivalent.py synthetic_5000.csv
# compare the final 'total' column — should be identical, but differs

generate_data.py (deterministic synthetic data)

import csv, random
random.seed(42)
N_BARS = 5000
START_TS = 1700000000
price = 100.0
rows = []
for i in range(N_BARS):
    move = random.gauss(0, 0.2)
    o = price
    h = o + abs(random.gauss(0, 0.1))
    l = o - abs(random.gauss(0, 0.1))
    c = o + move
    price = c
    rows.append((START_TS + i * 60, round(o, 6), round(h, 6), round(l, 6), round(c, 6), random.randint(100, 10000)))
with open("synthetic_5000.csv", "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["time", "open", "high", "low", "close", "volume"])
    w.writerows(rows)

nested_function_divergence.py (Series-subscript loop inside a nested def)

"""@pyne"""
from pynecore import pine_range
from pynecore.lib import array, bar_index, barstate, close, display, na, plot, script
from pynecore.types import Persistent

@script.indicator("Nested Function Divergence", overlay=False)
def main():
    total: Persistent[int] = 0
    anchors: Persistent[list[int]] = array.new_int()

    def check_window(anchor: int) -> bool:
        channel: int = bar_index - anchor
        if channel < 2 or channel > 11:
            return False
        if na(close[channel]):
            return False
        all_above: bool = True
        for i in pine_range(0, channel - 1):
            if na(close[i]) or close[i] <= close[channel]:
                all_above = False
        return all_above

    if barstate.isconfirmed and bar_index >= 3:
        array.push(anchors, bar_index - 1)
        expire_idx: int = array.size(anchors) - 1
        while expire_idx >= 0:
            a: int = array.get(anchors, expire_idx)
            if bar_index - a > 11:
                array.remove(anchors, expire_idx)
            expire_idx -= 1
        idx: int = 0
        while idx < array.size(anchors):
            a: int = array.get(anchors, idx)
            if check_window(a):
                total += 1
            idx += 1

    plot(total, 'total', display=display.data_window)

inline_equivalent.py (same logic, no nested function)

"""@pyne"""
from pynecore import pine_range
from pynecore.lib import array, bar_index, barstate, close, display, na, plot, script
from pynecore.types import Persistent

@script.indicator("Inline Equivalent", overlay=False)
def main():
    total: Persistent[int] = 0
    anchors: Persistent[list[int]] = array.new_int()

    if barstate.isconfirmed and bar_index >= 3:
        array.push(anchors, bar_index - 1)
        expire_idx: int = array.size(anchors) - 1
        while expire_idx >= 0:
            a: int = array.get(anchors, expire_idx)
            if bar_index - a > 11:
                array.remove(anchors, expire_idx)
            expire_idx -= 1
        idx: int = 0
        while idx < array.size(anchors):
            anchor: int = array.get(anchors, idx)
            channel: int = bar_index - anchor
            matched: bool = False
            if channel >= 2 and channel <= 11 and (not na(close[channel])):
                all_above: bool = True
                for i in pine_range(0, channel - 1):
                    if na(close[i]) or close[i] <= close[channel]:
                        all_above = False
                if all_above:
                    matched = True
            if matched:
                total += 1
            idx += 1

    plot(total, 'total', display=display.data_window)

Results on v6.5.0

Script Final total
nested_function_divergence.py 11337
inline_equivalent.py 11350

Expected: identical (same logic). Actual: the nested-function version under-counts by 13. The divergence starts at the first matching bar and accumulates.

Minimal trigger

def check_window(anchor: int) -> bool:
    channel: int = bar_index - anchor      # dynamic, data-dependent bound
    for i in pine_range(0, channel - 1):   # loop bound is dynamic
        if close[i] <= close[channel]:     # Series subscript inside nested fn
            ...

Inlining the same logic produces the correct result. The bug is specific to Series-subscript resolution inside nested-function scope when the subscript index comes from a loop with dynamic bounds.

Practical impact

PyneComp compiles Pine user-defined functions into Python nested defs, so any Pine indicator whose function loops over Series data with dynamic bounds will produce incorrect results when run through PyneCore 6.5.0 — even though the source compiles cleanly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions