diff --git a/pineforge_codegen/codegen/visit_call.py b/pineforge_codegen/codegen/visit_call.py index b94889e..961e16f 100644 --- a/pineforge_codegen/codegen/visit_call.py +++ b/pineforge_codegen/codegen/visit_call.py @@ -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(), na(), {immediately})' if func_name == "exit": p = self._resolve_func_args(node, "strategy.exit") diff --git a/pineforge_codegen/signatures.py b/pineforge_codegen/signatures.py index 2da4d9f..a8309c7 100644 --- a/pineforge_codegen/signatures.py +++ b/pineforge_codegen/signatures.py @@ -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)) diff --git a/pineforge_codegen/support_checker.py b/pineforge_codegen/support_checker.py index b17a32b..8be2a5a 100644 --- a/pineforge_codegen/support_checker.py +++ b/pineforge_codegen/support_checker.py @@ -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 diff --git a/tests/test_codegen_new.py b/tests/test_codegen_new.py index 0c8b366..9c02003 100644 --- a/tests/test_codegen_new.py +++ b/tests/test_codegen_new.py @@ -378,6 +378,30 @@ def test_strategy_close_forwards_qty_percent_and_immediately(): assert 'strategy_close(std::string("Long"), "", na(), 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(), na(), 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(), na(), 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) diff --git a/tests/test_compile_smoke.py b/tests/test_compile_smoke.py index 2ce4f63..8108133 100644 --- a/tests/test_compile_smoke.py +++ b/tests/test_compile_smoke.py @@ -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) diff --git a/tests/test_signatures.py b/tests/test_signatures.py index d4ceaba..fe25ff3 100644 --- a/tests/test_signatures.py +++ b/tests/test_signatures.py @@ -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", @@ -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(), na(), false)' in cpp def test_strategy_cancel_all(self): cpp = _generate(_pine('strategy.cancel_all()')) diff --git a/tests/test_support_checker.py b/tests/test_support_checker.py index b3a2310..e84ec76 100644 --- a/tests/test_support_checker.py +++ b/tests/test_support_checker.py @@ -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