diff --git a/pineforge_codegen/codegen/security.py b/pineforge_codegen/codegen/security.py index 33e82f1..114e5cf 100644 --- a/pineforge_codegen/codegen/security.py +++ b/pineforge_codegen/codegen/security.py @@ -2173,6 +2173,19 @@ def _build_security_expr( and isinstance(expr_node.callee.object, Identifier) and expr_node.callee.object.name == "math" ): + # A rolling/stateful math reducer (e.g. math.sum -> math::Sum) is + # precomputed into a committed _secval_* by the security TA + # machinery, exactly like a ta.* call. Return that committed value + # instead of falling through to _build_security_math_call, whose + # inline lowering only covers scalar math and emits a broken + # "unsupported: math." 0.0 for a reducer. Scalar math + # (abs/round/min/max/...) is not a TA site, so this is a no-op. + math_site = self._get_ta_site(expr_node) + if math_site is not None: + math_idx = self._ta_index_by_site_id.get(id(math_site)) + math_sig = self._security_binding_stack_signature(helper_binding_stack) + if math_idx is not None and (math_idx, math_sig) in ta_results: + return ta_results[(math_idx, math_sig)] return self._build_security_math_call( sec_id, expr_node.callee.member, diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index 4f6bce6..8f6434c 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -373,7 +373,12 @@ def _visit_member_access(self, node: MemberAccess) -> str: if node.member == "position_size": return "signed_position_size()" if node.member == "position_avg_price": - return "position_entry_price_" + # Pine returns `na` when the position is flat + # (position_size == 0); the engine field is 0.0 when flat. + # Guard so the common `na(strategy.position_avg_price)` + # flat/in-position idiom is not silently defeated. + return ("(signed_position_size() == 0.0 ? na() " + ": position_entry_price_)") if node.member == "position_entry_name": return "position_entry_name()" # Trade counts diff --git a/tests/test_security_math_reducer.py b/tests/test_security_math_reducer.py new file mode 100644 index 0000000..83ce729 --- /dev/null +++ b/tests/test_security_math_reducer.py @@ -0,0 +1,53 @@ +"""Regression: a rolling/stateful math reducer (e.g. ``math.sum``) inside +``request.security`` must resolve to its committed ``_secval_*`` helper, not the +scalar-math "unsupported" ``0.0`` fallback. + +``request.security(tickerid, "D", math.sum(high[1] - low[1], 10) / 10, ...)`` +precomputes ``math.sum`` as a ``math::Sum`` TA site into ``_secval_N`` (exactly +like a ``ta.*`` reducer). But ``_build_security_expr`` routed the ``math.*`` node +to the scalar-math lowering (``_build_security_math_call``, which can only emit +inline scalar ops) BEFORE the TA-site lookup — so it emitted +``0.0 /* unsupported: math.sum */`` and the whole security silently returned 0. +The dispatch must return the committed ``_secval_`` for a math call that is a +precomputed TA site. Scalar math (abs/round/min/max/...) is not a TA site and +must still lower inline. +""" + +from __future__ import annotations + +import re + +from pineforge_codegen import transpile + + +def test_math_sum_in_security_resolves_to_committed_secval(): + src = """//@version=6 +strategy("t", overlay=true) +adr = request.security(syminfo.tickerid, "D", math.sum(high[1] - low[1], 10) / 10, lookahead=barmerge.lookahead_off) +if not na(adr) and adr > 0 + strategy.entry("L", strategy.long) +plot(close) +""" + cpp = transpile(src) + # The reducer must be precomputed as a rolling math::Sum helper... + assert "math::Sum" in cpp + # ...and it must NOT degrade to the scalar-math unsupported fallback. + assert "/* unsupported: math.sum */" not in cpp + # The request.security committed value must reference the _secval_ helper + # (here: (_secval_N / 10)), not a literal 0.0. + assert re.search(r"_req_sec_\d+\s*=\s*\(_secval_\d+ / 10\)", cpp), cpp + + +def test_scalar_math_in_security_still_lowers_inline(): + # math.abs is not a TA site: it must keep lowering inline, unaffected by the + # reducer fix (guards against over-broadly rerouting all math.* calls). + src = """//@version=6 +strategy("t", overlay=true) +d = request.security(syminfo.tickerid, "D", math.abs(close - open), lookahead=barmerge.lookahead_off) +if not na(d) and d > 0 + strategy.entry("L", strategy.long) +plot(close) +""" + cpp = transpile(src) + assert "std::abs" in cpp + assert "/* unsupported: math.abs */" not in cpp