Merge pull request #12808 from freqtrade/feat/strategy_params

add Strategy parameters  to api
This commit is contained in:
Matthias
2026-02-10 07:21:05 +01:00
committed by GitHub
6 changed files with 181 additions and 41 deletions

View File

@@ -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,59 @@ 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(__StrategyParameter):
param_type: Literal["DecimalParameter"]
value: float
low: float
high: float
decimals: int
class BooleanParameter(__StrategyParameter):
param_type: Literal["BooleanParameter"]
value: bool | None
opt_range: list[bool]
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] = Field(default_factory=list)
code: str
class AvailablePairs(BaseModel):

View File

@@ -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,
)
@@ -60,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()
@@ -139,6 +142,47 @@ 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=422, 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:
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
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()

View File

@@ -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
@@ -36,29 +32,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")
)
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),
}
@router.get("/exchanges", response_model=ExchangeListResponse, tags=[])
def list_exchanges(config=Depends(get_config)):
from freqtrade.exchange import list_available_exchanges

View File

@@ -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"]:
"""
@@ -255,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
@@ -268,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":
"""
@@ -276,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
@@ -288,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]

View File

@@ -2559,19 +2559,50 @@ 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/HyperoptableStrategy")
assert_response(rc)
response2 = rc.json()
assert len(response2["params"]) >= 8
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)
# 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"),
@@ -2581,6 +2612,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

View File

@@ -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"