From a7e35f5d78694b3d17fbeb2046acfa2eab03fb97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Feb 2026 13:30:14 +0100 Subject: [PATCH 01/17] chore: cleanup web_ui definition --- freqtrade/rpc/api_server/web_ui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 0a8a73011..8d143c57d 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -5,20 +5,20 @@ from fastapi.exceptions import HTTPException from starlette.responses import FileResponse -router_ui = APIRouter() +router_ui = APIRouter(include_in_schema=False, tags=["Web UI"]) -@router_ui.get("/favicon.ico", include_in_schema=False) +@router_ui.get("/favicon.ico") async def favicon(): return FileResponse(str(Path(__file__).parent / "ui/favicon.ico")) -@router_ui.get("/fallback_file.html", include_in_schema=False) +@router_ui.get("/fallback_file.html") async def fallback(): return FileResponse(str(Path(__file__).parent / "ui/fallback_file.html")) -@router_ui.get("/ui_version", include_in_schema=False) +@router_ui.get("/ui_version") async def ui_version(): from freqtrade.commands.deploy_ui import read_ui_version @@ -30,7 +30,7 @@ async def ui_version(): } -@router_ui.get("/{rest_of_path:path}", include_in_schema=False) +@router_ui.get("/{rest_of_path:path}") async def index_html(rest_of_path: str): """ Emulate path fallback to index.html. From bea006880bad943b445229b14c7397d35069027d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Feb 2026 18:21:42 +0100 Subject: [PATCH 02/17] feat: add paramType to Strategy Parameters --- freqtrade/strategy/parameters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index c6363ce99..db89339c2 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -70,6 +70,10 @@ class BaseParameter(ABC): def __repr__(self): return f"{self.__class__.__name__}({self.value})" + @property + def param_type(self) -> str: + return self.__class__.__name__ + @abstractmethod def get_space(self, name: str) -> Union["Integer", "Real", "SKDecimal", "Categorical"]: """ From f6e465a0bad73f7e3e73596ce40702bd6d290aa8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Feb 2026 18:23:09 +0100 Subject: [PATCH 03/17] test: add test for paramType property --- tests/strategy/test_strategy_parameters.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/strategy/test_strategy_parameters.py b/tests/strategy/test_strategy_parameters.py index 4bc10c0b5..bb379c774 100644 --- a/tests/strategy/test_strategy_parameters.py +++ b/tests/strategy/test_strategy_parameters.py @@ -43,6 +43,7 @@ def test_hyperopt_int_parameter(): HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) assert len(list(intpar.range)) == 1 + assert intpar.param_type == "IntParameter" def test_hyperopt_real_parameter(): @@ -60,6 +61,7 @@ def test_hyperopt_real_parameter(): assert isinstance(fltpar.get_space(""), FloatDistribution) assert not hasattr(fltpar, "range") + assert fltpar.param_type == "RealParameter" def test_hyperopt_decimal_parameter(): @@ -94,6 +96,7 @@ def test_hyperopt_decimal_parameter(): HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) assert len(list(decimalpar.range)) == 1 + assert decimalpar.param_type == "DecimalParameter" def test_hyperopt_categorical_parameter(): @@ -133,3 +136,5 @@ def test_hyperopt_categorical_parameter(): HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) assert len(list(catpar.range)) == 1 assert len(list(boolpar.range)) == 1 + assert boolpar.param_type == "BooleanParameter" + assert catpar.param_type == "CategoricalParameter" From 600b6a7d2372d73be5e5213061c74db6e5418ea8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Feb 2026 20:13:36 +0100 Subject: [PATCH 04/17] refactor: rename "decimal" variable in Decimal parameter --- freqtrade/strategy/parameters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index db89339c2..c899f2952 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -259,8 +259,8 @@ class DecimalParameter(NumericParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to optuna's NumericParameter. """ - self._decimals = decimals - default = round(default, self._decimals) + self.decimals = decimals + default = round(default, self.decimals) super().__init__( low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs @@ -272,7 +272,7 @@ class DecimalParameter(NumericParameter): @value.setter def value(self, new_value: float): - self._value = round(new_value, self._decimals) + self._value = round(new_value, self.decimals) def get_space(self, name: str) -> "SKDecimal": """ @@ -280,7 +280,7 @@ class DecimalParameter(NumericParameter): :param name: A name of parameter field. """ return SKDecimal( - low=self.low, high=self.high, decimals=self._decimals, name=name, **self._space_params + low=self.low, high=self.high, decimals=self.decimals, name=name, **self._space_params ) @property @@ -292,9 +292,9 @@ class DecimalParameter(NumericParameter): calculating 100ds of indicators. """ if self.can_optimize(): - low = int(self.low * pow(10, self._decimals)) - high = int(self.high * pow(10, self._decimals)) + 1 - return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)] + low = int(self.low * pow(10, self.decimals)) + high = int(self.high * pow(10, self.decimals)) + 1 + return [round(n * pow(0.1, self.decimals), self.decimals) for n in range(low, high)] else: return [self.value] From 5ab8338c3ee572392d6d055496406bbfc69fd20b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Feb 2026 20:33:07 +0100 Subject: [PATCH 05/17] feat: add strategy-parameters to /strategy endpoint --- freqtrade/rpc/api_server/api_schemas.py | 56 +++++++++++++++++++++-- freqtrade/rpc/api_server/api_webserver.py | 4 +- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 720f8c95d..0977511a9 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,7 +1,7 @@ from datetime import date, datetime -from typing import Any +from typing import Annotated, Any, Literal -from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny, model_validator +from pydantic import AwareDatetime, BaseModel, Field, RootModel, SerializeAsAny, model_validator from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode @@ -527,10 +527,60 @@ class FreqAIModelListResponse(BaseModel): freqaimodels: list[str] +class __StrategyParameter(BaseModel): + param_type: str + name: str + space: str + load: bool + optimize: bool + + +class __IntParameter(__StrategyParameter): + param_type: Literal["IntParameter"] + value: int + low: int + high: int + + +class __RealParameter(__StrategyParameter): + param_type: Literal["RealParameter"] + value: float + low: float + high: float + + +class __DecimalParameter(__RealParameter): + param_type: Literal["DecimalParameter"] + decimals: int + + +class __BooleanParameter(__StrategyParameter): + param_type: Literal["BooleanParameter"] + value: Any + opt_range: list[Any] + + +class __CategoricalParameter(__StrategyParameter): + param_type: Literal["CategoricalParameter"] + value: Any + opt_range: list[Any] + + +AllParameters = Annotated[ + __BooleanParameter + | __CategoricalParameter + | __DecimalParameter + | __IntParameter + | __RealParameter, + Field(discriminator="param_type"), +] + + class StrategyResponse(BaseModel): strategy: str - code: str timeframe: str | None + params: list[AllParameters] | None + code: str class AvailablePairs(BaseModel): diff --git a/freqtrade/rpc/api_server/api_webserver.py b/freqtrade/rpc/api_server/api_webserver.py index 7b2cce0fc..e19ae94a8 100644 --- a/freqtrade/rpc/api_server/api_webserver.py +++ b/freqtrade/rpc/api_server/api_webserver.py @@ -48,14 +48,16 @@ def get_strategy(strategy: str, config=Depends(get_config)): strategy_obj = StrategyResolver._load_strategy( strategy, config_, extra_dir=config_.get("strategy_path") ) + strategy_obj.ft_load_hyper_params() except OperationalException: raise HTTPException(status_code=404, detail="Strategy not found") except Exception as e: raise HTTPException(status_code=502, detail=str(e)) return { "strategy": strategy_obj.get_strategy_name(), - "code": strategy_obj.__source__, "timeframe": getattr(strategy_obj, "timeframe", None), + "code": strategy_obj.__source__, + "params": [p for _, p in strategy_obj.enumerate_parameters()], } From 100babc0561d38d3ccd13fecdc113f01ce650af9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Feb 2026 20:15:33 +0100 Subject: [PATCH 06/17] test: improve api-strategy test --- tests/rpc/test_rpc_apiserver.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 57831935b..5a7c6f872 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2559,13 +2559,27 @@ def test_api_strategy(botclient, tmp_path, mocker): rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") assert_response(rc) - assert rc.json()["strategy"] == CURRENT_TEST_STRATEGY + response = rc.json() + assert response["strategy"] == CURRENT_TEST_STRATEGY data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text( encoding="utf-8" ) - assert rc.json()["code"] == data - + assert response["code"] == data + assert "params" in response + assert isinstance(response["params"], list) + assert len(response["params"]) == 6 + buy_rsi = next(p for p in response["params"] if p["name"] == "buy_rsi") + assert buy_rsi == { + "param_type": "IntParameter", + "name": "buy_rsi", + "space": "buy", + "load": True, + "optimize": True, + "value": 35, # Parameter from buy_params + "low": 0, + "high": 50, + } rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") assert_response(rc, 404) From 570bfef4b307ddf35a20c0b1e8223c9b84dc4351 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Feb 2026 20:26:13 +0100 Subject: [PATCH 07/17] test: /strategy/ includes random spaces --- tests/rpc/test_rpc_apiserver.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a7c6f872..b26805988 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2568,7 +2568,7 @@ def test_api_strategy(botclient, tmp_path, mocker): assert response["code"] == data assert "params" in response assert isinstance(response["params"], list) - assert len(response["params"]) == 6 + assert len(response["params"]) >= 6 buy_rsi = next(p for p in response["params"] if p["name"] == "buy_rsi") assert buy_rsi == { "param_type": "IntParameter", @@ -2580,6 +2580,24 @@ def test_api_strategy(botclient, tmp_path, mocker): "low": 0, "high": 50, } + + rc = client_get(client, f"{BASE_URI}/strategy/HyperoptableStrategy") + assert_response(rc) + response2 = rc.json() + assert len(response2["params"]) >= 8 + buy_rsi = next(p for p in response2["params"] if p["name"] == "buy_rsi") + param_exitaaa = next(p for p in response2["params"] if p["name"] == "exitaaa") + assert param_exitaaa == { + "param_type": "IntParameter", + "name": "exitaaa", + "space": "exitaspace", + "load": True, + "optimize": True, + "value": 5, + "low": 0, + "high": 10, + } + rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") assert_response(rc, 404) From ac6b396ed3c412dc5e6f937e4e40df06ad29815d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Feb 2026 13:19:13 +0100 Subject: [PATCH 08/17] chore: improve type-safety --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 0977511a9..7e7787672 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -557,7 +557,7 @@ class __DecimalParameter(__RealParameter): class __BooleanParameter(__StrategyParameter): param_type: Literal["BooleanParameter"] value: Any - opt_range: list[Any] + opt_range: list[bool] class __CategoricalParameter(__StrategyParameter): From ba57ab9a1b932fa109ddab30b4e8cca89c6ea53e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Feb 2026 13:36:26 +0100 Subject: [PATCH 09/17] feat: add trade mode /strategy endpoint --- freqtrade/rpc/api_server/api_v1.py | 39 +++++++++++++++++++++++ freqtrade/rpc/api_server/api_webserver.py | 25 --------------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 841acca96..18c37799b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException from freqtrade import __version__ from freqtrade.enums import RunMode, State +from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload from freqtrade.rpc.api_server.api_schemas import ( @@ -17,6 +18,7 @@ from freqtrade.rpc.api_server.api_schemas import ( Ping, PlotConfig, ShowConfig, + StrategyResponse, SysInfo, Version, ) @@ -139,6 +141,43 @@ def markets( } +@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["Strategy"]) +def get_strategy( + strategy: str, config=Depends(get_config), rpc: RPC | None = Depends(get_rpc_optional) +): + if ":" in strategy: + raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") + + if not rpc or config["runmode"] == RunMode.WEBSERVER: + # webserver mode + config_ = deepcopy(config) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + + try: + strategy_obj = StrategyResolver._load_strategy( + strategy, config_, extra_dir=config_.get("strategy_path") + ) + strategy_obj.ft_load_hyper_params() + except OperationalException: + raise HTTPException(status_code=404, detail="Strategy not found") + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) + else: + # trade mode + strategy_obj = rpc._freqtrade.strategy + if strategy_obj.get_strategy_name() != strategy: + raise HTTPException( + status_code=404, + detail="Only the currently active strategy is available in trade mode", + ) + return { + "strategy": strategy_obj.get_strategy_name(), + "timeframe": getattr(strategy_obj, "timeframe", None), + "code": strategy_obj.__source__, + "params": [p for _, p in strategy_obj.enumerate_parameters()], + } + + @router.get("/sysinfo", response_model=SysInfo, tags=["Info"]) def sysinfo(): return RPC._rpc_sysinfo() diff --git a/freqtrade/rpc/api_server/api_webserver.py b/freqtrade/rpc/api_server/api_webserver.py index e19ae94a8..24c409b02 100644 --- a/freqtrade/rpc/api_server/api_webserver.py +++ b/freqtrade/rpc/api_server/api_webserver.py @@ -36,31 +36,6 @@ def list_strategies(config=Depends(get_config)): return {"strategies": [x["name"] for x in strategies]} -@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["Strategy"]) -def get_strategy(strategy: str, config=Depends(get_config)): - if ":" in strategy: - raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") - - config_ = deepcopy(config) - from freqtrade.resolvers.strategy_resolver import StrategyResolver - - try: - strategy_obj = StrategyResolver._load_strategy( - strategy, config_, extra_dir=config_.get("strategy_path") - ) - strategy_obj.ft_load_hyper_params() - except OperationalException: - raise HTTPException(status_code=404, detail="Strategy not found") - except Exception as e: - raise HTTPException(status_code=502, detail=str(e)) - return { - "strategy": strategy_obj.get_strategy_name(), - "timeframe": getattr(strategy_obj, "timeframe", None), - "code": strategy_obj.__source__, - "params": [p for _, p in strategy_obj.enumerate_parameters()], - } - - @router.get("/exchanges", response_model=ExchangeListResponse, tags=[]) def list_exchanges(config=Depends(get_config)): from freqtrade.exchange import list_available_exchanges From a7890a964fce3eea439125980f5bfcba12026345 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Feb 2026 13:52:33 +0100 Subject: [PATCH 10/17] test: add test for /strategy live mode --- tests/rpc/test_rpc_apiserver.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b26805988..b92703e65 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2613,6 +2613,40 @@ def test_api_strategy(botclient, tmp_path, mocker): assert_response(rc, 502) +def test_api_strategy_trade_mode(botclient, tmp_path, mocker): + ftbot, client = botclient + ftbot.config["user_data_dir"] = tmp_path + + rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") + + assert_response(rc) + response = rc.json() + assert response["strategy"] == CURRENT_TEST_STRATEGY + + data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text( + encoding="utf-8" + ) + assert response["code"] == data + assert "params" in response + assert isinstance(response["params"], list) + assert len(response["params"]) >= 6 + buy_rsi = next(p for p in response["params"] if p["name"] == "buy_rsi") + assert buy_rsi == { + "param_type": "IntParameter", + "name": "buy_rsi", + "space": "buy", + "load": True, + "optimize": True, + "value": 35, # Parameter from buy_params + "low": 0, + "high": 50, + } + + rc = client_get(client, f"{BASE_URI}/strategy/HyperoptableStrategy") + assert_response(rc, 404) + assert rc.json()["detail"] == "Only the currently active strategy is available in trade mode" + + def test_api_exchanges(botclient): _ftbot, client = botclient _ftbot.config["runmode"] = RunMode.WEBSERVER From 348afa46f066ca084ca2ed856db010f91926a0bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Feb 2026 19:41:59 +0100 Subject: [PATCH 11/17] chore: update api_version to 2.47 --- freqtrade/rpc/api_server/api_v1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 18c37799b..34cf165bf 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -62,7 +62,8 @@ logger = logging.getLogger(__name__) # 2.44: Add candle_types parameter to download-data endpoint # 2.45: Add price to forceexit endpoint # 2.46: Add prepend_data to download-data endpoint -API_VERSION = 2.46 +# 2.47: Add Strategy parameters +API_VERSION = 2.47 # Public API, requires no auth. router_public = APIRouter() From 9eb668d08ad9ed2e516b59140a297950fee081d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Feb 2026 19:58:36 +0100 Subject: [PATCH 12/17] chore: improve response security --- freqtrade/rpc/api_server/api_v1.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 34cf165bf..366dabead 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -147,7 +147,7 @@ def get_strategy( strategy: str, config=Depends(get_config), rpc: RPC | None = Depends(get_rpc_optional) ): if ":" in strategy: - raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") + raise HTTPException(status_code=422, detail="base64 encoded strategies are not allowed.") if not rpc or config["runmode"] == RunMode.WEBSERVER: # webserver mode @@ -162,7 +162,11 @@ def get_strategy( except OperationalException: raise HTTPException(status_code=404, detail="Strategy not found") except Exception as e: - raise HTTPException(status_code=502, detail=str(e)) + logger.exception("Unexpected error while loading strategy '%s'.", strategy) + raise HTTPException( + status_code=502, + detail="Unexpected error while loading strategy.", + ) else: # trade mode strategy_obj = rpc._freqtrade.strategy From 40363757592aa145901a29aec24adcde40e6af2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Feb 2026 19:58:45 +0100 Subject: [PATCH 13/17] test: remove pointless test calculation --- tests/rpc/test_rpc_apiserver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b92703e65..b69dab6b7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2585,7 +2585,6 @@ def test_api_strategy(botclient, tmp_path, mocker): assert_response(rc) response2 = rc.json() assert len(response2["params"]) >= 8 - buy_rsi = next(p for p in response2["params"] if p["name"] == "buy_rsi") param_exitaaa = next(p for p in response2["params"] if p["name"] == "exitaaa") assert param_exitaaa == { "param_type": "IntParameter", From a91fd66abc4764fd0fd9a6aec0c995c393dcb27a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Feb 2026 20:00:23 +0100 Subject: [PATCH 14/17] fix: improve openAPI schema definition --- freqtrade/rpc/api_server/api_schemas.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 7e7787672..468cfa4a9 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -535,43 +535,39 @@ class __StrategyParameter(BaseModel): optimize: bool -class __IntParameter(__StrategyParameter): +class IntParameter(__StrategyParameter): param_type: Literal["IntParameter"] value: int low: int high: int -class __RealParameter(__StrategyParameter): +class RealParameter(__StrategyParameter): param_type: Literal["RealParameter"] value: float low: float high: float -class __DecimalParameter(__RealParameter): +class DecimalParameter(RealParameter): param_type: Literal["DecimalParameter"] decimals: int -class __BooleanParameter(__StrategyParameter): +class BooleanParameter(__StrategyParameter): param_type: Literal["BooleanParameter"] - value: Any + value: bool | None opt_range: list[bool] -class __CategoricalParameter(__StrategyParameter): +class CategoricalParameter(__StrategyParameter): param_type: Literal["CategoricalParameter"] value: Any opt_range: list[Any] AllParameters = Annotated[ - __BooleanParameter - | __CategoricalParameter - | __DecimalParameter - | __IntParameter - | __RealParameter, + BooleanParameter | CategoricalParameter | DecimalParameter | IntParameter | RealParameter, Field(discriminator="param_type"), ] @@ -579,7 +575,7 @@ AllParameters = Annotated[ class StrategyResponse(BaseModel): strategy: str timeframe: str | None - params: list[AllParameters] | None + params: list[AllParameters] = Field(default_factory=list) code: str From aa1de750e9755d599b57efff8927fc76db94556c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Feb 2026 20:02:40 +0100 Subject: [PATCH 15/17] test: update test for new response code --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b69dab6b7..6f3adde1d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2602,7 +2602,7 @@ def test_api_strategy(botclient, tmp_path, mocker): # Disallow base64 strategies rc = client_get(client, f"{BASE_URI}/strategy/xx:cHJpbnQoImhlbGxvIHdvcmxkIik=") - assert_response(rc, 500) + assert_response(rc, 422) mocker.patch( "freqtrade.resolvers.strategy_resolver.StrategyResolver._load_strategy", side_effect=Exception("Test"), From cb1266225ebadaf69b3db4ea9c386913607c132e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Feb 2026 06:48:47 +0100 Subject: [PATCH 16/17] chore: fix unused imports --- freqtrade/rpc/api_server/api_v1.py | 2 +- freqtrade/rpc/api_server/api_webserver.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 366dabead..29d3e3970 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -161,7 +161,7 @@ def get_strategy( strategy_obj.ft_load_hyper_params() except OperationalException: raise HTTPException(status_code=404, detail="Strategy not found") - except Exception as e: + except Exception: logger.exception("Unexpected error while loading strategy '%s'.", strategy) raise HTTPException( status_code=502, diff --git a/freqtrade/rpc/api_server/api_webserver.py b/freqtrade/rpc/api_server/api_webserver.py index 24c409b02..3ca02bc5a 100644 --- a/freqtrade/rpc/api_server/api_webserver.py +++ b/freqtrade/rpc/api_server/api_webserver.py @@ -1,19 +1,15 @@ import logging -from copy import deepcopy from fastapi import APIRouter, Depends -from fastapi.exceptions import HTTPException from freqtrade.data.history.datahandlers import get_datahandler from freqtrade.enums import CandleType, TradingMode -from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.api_schemas import ( AvailablePairs, ExchangeListResponse, FreqAIModelListResponse, HyperoptLossListResponse, StrategyListResponse, - StrategyResponse, ) from freqtrade.rpc.api_server.deps import get_config From b52805cd9651362c5a11d7f8cbaff18bef6823f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Feb 2026 06:49:45 +0100 Subject: [PATCH 17/17] chore: split Decimal and RealParameters --- freqtrade/rpc/api_server/api_schemas.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 468cfa4a9..005f092bb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -549,8 +549,11 @@ class RealParameter(__StrategyParameter): high: float -class DecimalParameter(RealParameter): +class DecimalParameter(__StrategyParameter): param_type: Literal["DecimalParameter"] + value: float + low: float + high: float decimals: int