diff --git a/backend_api_python/app/services/ai_generation_contracts.py b/backend_api_python/app/services/ai_generation_contracts.py index 8835b150a..ef61b5db7 100644 --- a/backend_api_python/app/services/ai_generation_contracts.py +++ b/backend_api_python/app/services/ai_generation_contracts.py @@ -32,7 +32,7 @@ - Read run-supplied values only inside executable handlers or scheduled callbacks with `context.params.get("name", same_default)`. The discovery context used by `initialize(context)` has no `params`; never read `context.params` in `initialize`. - Parameters may control signal periods, thresholds, target weights, stops, take profit, trailing protection, cooldowns, and bounded layer counts. - Do not disguise universe, symbol, market type, frequency, leverage permission, initial capital, date range, commission, or slippage as ordinary strategy parameters. -- Use `context.set_metadata(...)` in `initialize` for stable descriptive metadata such as direction mode and strategy family. Metadata is not a substitute for executable risk logic. +- Use `context.set_metadata(...)` in `initialize` for stable descriptive metadata such as direction mode and strategy family, passing keyword arguments such as `context.set_metadata(direction_mode="long_only", strategy_family="trend")`. Metadata is not a substitute for executable risk logic. ## Data and factors - Historical-bar signatures are exact: `get_history(count, frequency=None, field=None, security_list=None)` and `data.history(symbols, count, fields=None)`. diff --git a/backend_api_python/app/services/strategy_v2/contract.py b/backend_api_python/app/services/strategy_v2/contract.py index 5119a3fbc..d45384344 100644 --- a/backend_api_python/app/services/strategy_v2/contract.py +++ b/backend_api_python/app/services/strategy_v2/contract.py @@ -6,7 +6,7 @@ import hashlib from dataclasses import dataclass from types import SimpleNamespace -from typing import Any, Callable, Iterable +from typing import Any, Callable, Iterable, Mapping from app.utils.safe_exec import build_safe_builtins, safe_exec_with_validation from app.services.factors import FactorError, get_factor @@ -150,7 +150,15 @@ def allow_leverage(self, max_leverage: object = 1) -> None: self.leverage_allowed = value > 1.0 self.max_leverage = value - def set_metadata(self, **values: Any) -> None: + def set_metadata(self, *args: Any, **values: Any) -> None: + if len(args) == 1 and isinstance(args[0], Mapping): + self.metadata.update(args[0]) + elif len(args) == 2: + self.metadata[str(args[0])] = args[1] + elif args: + raise TypeError( + "set_metadata expects keyword arguments, a single key/value pair, or a mapping" + ) self.metadata.update(values) diff --git a/backend_api_python/tests/test_strategy_v2_contract.py b/backend_api_python/tests/test_strategy_v2_contract.py index 4585f1ac4..4bf4ade3d 100644 --- a/backend_api_python/tests/test_strategy_v2_contract.py +++ b/backend_api_python/tests/test_strategy_v2_contract.py @@ -405,6 +405,23 @@ def handle_data(context, data): assert manifest.metadata()["directionMode"] == "both" +def test_set_metadata_accepts_positional_key_value_pair(): + code = """ +def initialize(context): + context.set_universe(["Crypto:BTC/USDT@okx:swap"]) + context.subscribe(frequency="1h") + context.set_metadata("direction_mode", "long_only") + context.set_metadata({"strategy_family": "trend"}) + +def handle_data(context, data): + pass +""" + manifest = compile_strategy_v2(code).manifest + + assert manifest.direction_mode == "long_only" + assert manifest.metadata()["directionMode"] == "long_only" + + @pytest.mark.parametrize( "strategy_body,expected", [