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.
Summary
When a
for-loop accesses a Series subscript (close[i]) inside a nested function whose loop bound is dynamic (derived frombar_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 adefor at the top level ofmain().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 differsgenerate_data.py(deterministic synthetic data)nested_function_divergence.py(Series-subscript loop inside a nesteddef)inline_equivalent.py(same logic, no nested function)Results on v6.5.0
totalnested_function_divergence.pyinline_equivalent.pyExpected: identical (same logic). Actual: the nested-function version under-counts by 13. The divergence starts at the first matching bar and accumulates.
Minimal trigger
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.