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
8 changes: 7 additions & 1 deletion pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,13 @@ def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately})"

if func_name == "close_all":
return "strategy_close_all()"
p = self._resolve_func_args(node, "strategy.close_all")
comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
# The engine's ID-less strategy_close path closes the complete
# position. Reuse it so close_all preserves the Pine order comment
# and same-tick fill flag instead of silently discarding both.
return f'strategy_close("", {comment}, na<double>(), na<double>(), {immediately})'

if func_name == "exit":
p = self._resolve_func_args(node, "strategy.exit")
Expand Down
6 changes: 3 additions & 3 deletions pineforge_codegen/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,13 @@ def _strat(short_name: str, *sigs: FuncSig) -> None:
_strat("close", _sig([
("id", S), ("comment", S, None),
("qty", F, None), ("qty_percent", F, None),
("alert_message", S, None), ("disable_alert", B, None),
("immediately", B, False),
("alert_message", S, None), ("immediately", B, False),
("disable_alert", B, None),
], ret=VOID))

_strat("close_all", _sig([
("comment", S, None), ("alert_message", S, None),
("disable_alert", B, None), ("immediately", B, False),
("immediately", B, False), ("disable_alert", B, None),
], ret=VOID))

_strat("cancel", _sig([("id", S)], ret=VOID))
Expand Down
6 changes: 5 additions & 1 deletion pineforge_codegen/support_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,13 @@
"order": {"comment", "alert_message", "disable_alert", "qty_type"},
"exit": {"comment_profit", "comment_loss", "comment_trailing", "alert_message", "alert_profit", "alert_loss", "alert_trailing", "disable_alert"},
"close": {"alert_message", "disable_alert"},
"close_all": {"comment", "alert_message", "disable_alert", "immediately"},
"close_all": {"alert_message", "disable_alert"},
}

# Alert delivery remains outside the backtest runtime contract. Keep these
# parameters warning-only so strategies that use webhook metadata still run;
# a follow-up must plumb alert events before codegen may claim support.

# strategy.closedtrades / strategy.opentrades accessor surfaces are NOT
# symmetric in Pine v6. opentrades has no exit_* fields (a trade has not
# closed yet). Both lack ``direction`` (Pine has ``size`` whose sign carries
Expand Down
24 changes: 24 additions & 0 deletions tests/test_codegen_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,30 @@ def test_strategy_close_forwards_qty_percent_and_immediately():
assert 'strategy_close(std::string("Long"), "", na<double>(), 50, true)' in cpp


def test_strategy_close_positional_immediately_uses_official_order():
src = ('//@version=6\nstrategy("T")\n'
'strategy.close("Long", "manual", 2, 50, "", true)\n')
cpp = transpile(src)
assert ('strategy_close(std::string("Long"), std::string("manual"), '
'2, 50, true)') in cpp


def test_strategy_close_all_forwards_named_comment_and_immediately():
src = ('//@version=6\nstrategy("T")\n'
'strategy.close_all(comment="global flat", immediately=true)\n')
cpp = transpile(src)
assert ('strategy_close("", std::string("global flat"), '
'na<double>(), na<double>(), true)') in cpp


def test_strategy_close_all_positional_immediately_uses_official_order():
src = ('//@version=6\nstrategy("T")\n'
'strategy.close_all("global flat", "", true)\n')
cpp = transpile(src)
assert ('strategy_close("", std::string("global flat"), '
'na<double>(), na<double>(), true)') in cpp


def test_strategy_exit_forwards_comment_to_runtime():
src = '//@version=6\nstrategy("T")\nstrategy.exit("X", "Long", stop=100.0, comment="stop exit")\n'
cpp = _generate(src)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_compile_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ def test_strategy_orders_compile():
strategy.entry("L", strategy.long, qty=1, comment="enter")
if ta.crossunder(ta.sma(close, 10), ta.sma(close, 30))
strategy.close("L", comment="exit")
if close < open
strategy.close_all(comment="global exit", immediately=true)

strategy.exit("X", from_entry="L", profit=1.0, loss=0.5)
strategy.exit("T", from_entry="L", trail_points=10, trail_offset=5)
Expand Down
11 changes: 10 additions & 1 deletion tests/test_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ def test_strategy_functions_populated(self):
"convert_to_account", "convert_to_symbol", "default_entry_qty"]:
assert name in sigs.STRATEGY_FUNCTIONS, f"strategy.{name} missing"

def test_strategy_close_positional_order_matches_pine_v6(self):
assert sigs.get_param_names("strategy", "close") == [
"id", "comment", "qty", "qty_percent", "alert_message",
"immediately", "disable_alert",
]
assert sigs.get_param_names("strategy", "close_all") == [
"comment", "alert_message", "immediately", "disable_alert",
]

def test_str_functions_populated(self):
for name in ["tostring", "tonumber", "format", "length",
"contains", "startswith", "endswith", "pos",
Expand Down Expand Up @@ -750,7 +759,7 @@ def test_strategy_cancel_kwargs(self):

def test_strategy_close_all(self):
cpp = _generate(_pine('strategy.close_all()'))
assert "strategy_close_all()" in cpp
assert 'strategy_close("", "", na<double>(), na<double>(), false)' in cpp

def test_strategy_cancel_all(self):
cpp = _generate(_pine('strategy.cancel_all()'))
Expand Down
57 changes: 57 additions & 0 deletions tests/test_support_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,63 @@ def test_unsupported_strategy_entry_params_warn():
assert not any("strategy.entry" in d.message and "qty_type" in d.message for d in warns)


@pytest.mark.parametrize(
("call", "param_name"),
[
('strategy.close_all(alert_message="filled")', "alert_message"),
('strategy.close_all(alert_message="")', "alert_message"),
('strategy.close_all(alert_message=str.tostring(close))', "alert_message"),
("strategy.close_all(disable_alert=true)", "disable_alert"),
("strategy.close_all(disable_alert=false)", "disable_alert"),
("strategy.close_all(disable_alert=close > open)", "disable_alert"),
('strategy.close_all("flat", "filled")', "alert_message"),
('strategy.close_all("flat", "", false, true)', "disable_alert"),
('strategy.close("L", alert_message="filled")', "alert_message"),
('strategy.close("L", alert_message="")', "alert_message"),
('strategy.close("L", alert_message=str.tostring(close))', "alert_message"),
('strategy.close("L", disable_alert=true)', "disable_alert"),
('strategy.close("L", disable_alert=false)', "disable_alert"),
('strategy.close("L", disable_alert=close > open)', "disable_alert"),
('strategy.close("L", "flat", 1, 100, "filled")', "alert_message"),
('strategy.close("L", "flat", 1, 100, "", false, true)', "disable_alert"),
],
)
def test_strategy_close_alert_controls_warn_named_and_positional(call, param_name):
src = PRELUDE + call + "\n"
assert _errors(src) == []
function_name = "close_all" if "strategy.close_all" in call else "close"
warns = _warnings(src)
assert any(
d.message == (
f"strategy.{function_name} parameter '{param_name}' is not "
"supported by PineForge and is ignored."
)
for d in warns
)


@pytest.mark.parametrize(
"call",
[
'strategy.close_all(comment="flat", immediately=true)',
'strategy.close_all("flat", "", true)',
'strategy.close_all("flat", "", true, false)',
'strategy.close("L", comment="flat", immediately=true)',
'strategy.close("L", "flat", 1, 100, "", true)',
'strategy.close("L", "flat", 1, 100, "", true, false)',
],
)
def test_strategy_close_comment_and_immediately_supported_named_and_positional(call):
src = PRELUDE + call + "\n"
assert _errors(src) == []
warns = _warnings(src)
assert not any(
"strategy.close" in d.message
and ("parameter 'comment'" in d.message or "parameter 'immediately'" in d.message)
for d in warns
)


def test_strategy_exit_qty_supported():
"""``strategy.exit(..., qty=...)`` is honoured by the runtime (Pine v6
semantics: absolute exit qty per bracket), so the support checker
Expand Down
Loading