"""backend/tests/test_parameter_injection.py — Parameter injection tests. Tests for parameter injection into strategy source code, including: - _inject_params() correctness - DecimalParameter, IntParameter, CategoricalParameter default patching - Class renaming """ from __future__ import annotations import ast import pytest import backend.services.auto_quant.pipeline as pl from backend.services.auto_quant.pipeline_modules.helpers import _inject_params from .test_helpers import _hyperopt_best class TestParameterInjection: """Verify that _inject_params() produces syntactically valid, correctly patched source.""" BASE_SOURCE = """\ from freqtrade.strategy import IStrategy from pandas import DataFrame class MyStrategy(IStrategy): INTERFACE_VERSION = 3 minimal_roi = {"0": 0.10, "60": 0.02} stoploss = -0.10 trailing_stop = False timeframe = "5m" def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return dataframe """ def test_class_is_renamed(self): best = {"params_dict": {}} result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) assert "class MyStrategy_Optimized(" in result assert "class MyStrategy(" not in result.split("AUTO-GENERATED")[1] def test_stoploss_is_injected(self): best = {"params_dict": {"stoploss": -0.05}} result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) assert "stoploss = -0.05" in result def test_roi_is_injected(self): new_roi = {"0": 0.08, "30": 0.04, "60": 0.01} best = {"params_dict": {"minimal_roi": new_roi}} result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) assert "0.08" in result or "0.04" in result def test_auto_generated_header_prepended(self): best = {"params_dict": {}} result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) assert result.startswith("# AUTO-GENERATED by Auto-Quant Factory") def test_output_is_valid_python(self): best = _hyperopt_best() result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) try: ast.parse(result) except SyntaxError as exc: pytest.fail(f"Injected source has a SyntaxError: {exc}") def test_trailing_stop_is_injected_as_true(self): best = {"params_dict": {"trailing_stop": True}} result = _inject_params(self.BASE_SOURCE, "MyStrategy_Optimized", best) assert "trailing_stop = True" in result def test_hyperopt_parameter_defaults_are_injected(self): source = """\ from freqtrade.strategy import DecimalParameter, IntParameter, CategoricalParameter, IStrategy class MyStrategy(IStrategy): ts_tier1_trigger = DecimalParameter(0.020, 0.040, default=0.030, decimals=3, space="buy", optimize=True) ts_tier1_lock = DecimalParameter(0.001, 0.010, default=0.003, decimals=3, space="buy", optimize=True) rsi_threshold = IntParameter(20, 50, default=30, space="buy", optimize=True) entry_logic = CategoricalParameter(["macd_cross", "bb_breakout"], default="macd_cross", space="buy", optimize=True) """ best = { "params_dict": { "ts_tier1_trigger": 0.037, "ts_tier1_lock": 0.006, "rsi_threshold": 42, "entry_logic": "bb_breakout", } } result = _inject_params(source, "MyStrategy_Optimized", best) assert "ts_tier1_trigger = DecimalParameter(0.020, 0.040, default=0.037" in result assert "ts_tier1_lock = DecimalParameter(0.001, 0.010, default=0.006" in result assert "rsi_threshold = IntParameter(20, 50, default=42" in result assert 'entry_logic = CategoricalParameter(["macd_cross", "bb_breakout"], default="bb_breakout"' in result def test_inject_params_patches_decimal_parameter_default(self): """_inject_params must update default= in DecimalParameter declarations.""" source = """ class TestStrategy(IStrategy): ts_tier1_trigger = DecimalParameter(0.020, 0.040, default=0.030, decimals=3, space="buy", optimize=True) """ best_params = {"params_dict": {"ts_tier1_trigger": 0.035}} patched = _inject_params(source, "TestStrategy_Optimized", best_params) assert 'default=0.035' in patched assert 'default=0.030' not in patched def test_inject_params_patches_int_parameter_default(self): """_inject_params must update default= in IntParameter declarations.""" source = """ class TestStrategy(IStrategy): rsi_threshold = IntParameter(20, 50, default=30, space="buy", optimize=True) """ best_params = {"params_dict": {"rsi_threshold": 35}} patched = _inject_params(source, "TestStrategy_Optimized", best_params) assert 'default=35' in patched assert 'default=30' not in patched def test_inject_params_patches_categorical_parameter_default(self): """_inject_params must update default= in CategoricalParameter declarations.""" source = """ class TestStrategy(IStrategy): entry_logic = CategoricalParameter(["macd_cross", "rsi_oversold", "bb_breakout"], default="macd_cross", space="buy", optimize=True) """ best_params = {"params_dict": {"entry_logic": "rsi_oversold"}} patched = _inject_params(source, "TestStrategy_Optimized", best_params) assert 'default="rsi_oversold"' in patched assert 'default="macd_cross"' not in patched def test_inject_params_renames_class(self): """_inject_params must rename the strategy class.""" source = "class TestStrategy(IStrategy):" best_params = {"params_dict": {}} patched = _inject_params(source, "TestStrategy_Optimized", best_params) assert "class TestStrategy_Optimized(IStrategy):" in patched assert "class TestStrategy(IStrategy):" not in patched