Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pineforge_codegen/codegen/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.<f>" 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,
Expand Down
7 changes: 6 additions & 1 deletion pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>() "
": position_entry_price_)")
if node.member == "position_entry_name":
return "position_entry_name()"
# Trade counts
Expand Down
53 changes: 53 additions & 0 deletions tests/test_security_math_reducer.py
Original file line number Diff line number Diff line change
@@ -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
Loading