Merge pull request #12808 from freqtrade/feat/strategy_params
add Strategy parameters to api
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from datetime import date, datetime
|
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.constants import DL_DATA_TIMEFRAMES, IntOrInf
|
||||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||||
@@ -527,10 +527,59 @@ class FreqAIModelListResponse(BaseModel):
|
|||||||
freqaimodels: list[str]
|
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):
|
class StrategyResponse(BaseModel):
|
||||||
strategy: str
|
strategy: str
|
||||||
code: str
|
|
||||||
timeframe: str | None
|
timeframe: str | None
|
||||||
|
params: list[AllParameters] = Field(default_factory=list)
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
class AvailablePairs(BaseModel):
|
class AvailablePairs(BaseModel):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException
|
|||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.enums import RunMode, State
|
from freqtrade.enums import RunMode, State
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
|
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
|
||||||
from freqtrade.rpc.api_server.api_schemas import (
|
from freqtrade.rpc.api_server.api_schemas import (
|
||||||
@@ -17,6 +18,7 @@ from freqtrade.rpc.api_server.api_schemas import (
|
|||||||
Ping,
|
Ping,
|
||||||
PlotConfig,
|
PlotConfig,
|
||||||
ShowConfig,
|
ShowConfig,
|
||||||
|
StrategyResponse,
|
||||||
SysInfo,
|
SysInfo,
|
||||||
Version,
|
Version,
|
||||||
)
|
)
|
||||||
@@ -60,7 +62,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 2.44: Add candle_types parameter to download-data endpoint
|
# 2.44: Add candle_types parameter to download-data endpoint
|
||||||
# 2.45: Add price to forceexit endpoint
|
# 2.45: Add price to forceexit endpoint
|
||||||
# 2.46: Add prepend_data to download-data 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.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
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"])
|
@router.get("/sysinfo", response_model=SysInfo, tags=["Info"])
|
||||||
def sysinfo():
|
def sysinfo():
|
||||||
return RPC._rpc_sysinfo()
|
return RPC._rpc_sysinfo()
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.exceptions import HTTPException
|
|
||||||
|
|
||||||
from freqtrade.data.history.datahandlers import get_datahandler
|
from freqtrade.data.history.datahandlers import get_datahandler
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.rpc.api_server.api_schemas import (
|
from freqtrade.rpc.api_server.api_schemas import (
|
||||||
AvailablePairs,
|
AvailablePairs,
|
||||||
ExchangeListResponse,
|
ExchangeListResponse,
|
||||||
FreqAIModelListResponse,
|
FreqAIModelListResponse,
|
||||||
HyperoptLossListResponse,
|
HyperoptLossListResponse,
|
||||||
StrategyListResponse,
|
StrategyListResponse,
|
||||||
StrategyResponse,
|
|
||||||
)
|
)
|
||||||
from freqtrade.rpc.api_server.deps import get_config
|
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]}
|
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=[])
|
@router.get("/exchanges", response_model=ExchangeListResponse, tags=[])
|
||||||
def list_exchanges(config=Depends(get_config)):
|
def list_exchanges(config=Depends(get_config)):
|
||||||
from freqtrade.exchange import list_available_exchanges
|
from freqtrade.exchange import list_available_exchanges
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ class BaseParameter(ABC):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.value})"
|
return f"{self.__class__.__name__}({self.value})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def param_type(self) -> str:
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_space(self, name: str) -> Union["Integer", "Real", "SKDecimal", "Categorical"]:
|
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 load: Load parameter value from {space}_params.
|
||||||
:param kwargs: Extra parameters to optuna's NumericParameter.
|
:param kwargs: Extra parameters to optuna's NumericParameter.
|
||||||
"""
|
"""
|
||||||
self._decimals = decimals
|
self.decimals = decimals
|
||||||
default = round(default, self._decimals)
|
default = round(default, self.decimals)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs
|
low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs
|
||||||
@@ -268,7 +272,7 @@ class DecimalParameter(NumericParameter):
|
|||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self, new_value: float):
|
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":
|
def get_space(self, name: str) -> "SKDecimal":
|
||||||
"""
|
"""
|
||||||
@@ -276,7 +280,7 @@ class DecimalParameter(NumericParameter):
|
|||||||
:param name: A name of parameter field.
|
:param name: A name of parameter field.
|
||||||
"""
|
"""
|
||||||
return SKDecimal(
|
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
|
@property
|
||||||
@@ -288,9 +292,9 @@ class DecimalParameter(NumericParameter):
|
|||||||
calculating 100ds of indicators.
|
calculating 100ds of indicators.
|
||||||
"""
|
"""
|
||||||
if self.can_optimize():
|
if self.can_optimize():
|
||||||
low = int(self.low * pow(10, self._decimals))
|
low = int(self.low * pow(10, self.decimals))
|
||||||
high = int(self.high * pow(10, self._decimals)) + 1
|
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)]
|
return [round(n * pow(0.1, self.decimals), self.decimals) for n in range(low, high)]
|
||||||
else:
|
else:
|
||||||
return [self.value]
|
return [self.value]
|
||||||
|
|
||||||
|
|||||||
@@ -2559,19 +2559,50 @@ def test_api_strategy(botclient, tmp_path, mocker):
|
|||||||
rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}")
|
rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}")
|
||||||
|
|
||||||
assert_response(rc)
|
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(
|
data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text(
|
||||||
encoding="utf-8"
|
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")
|
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
|
||||||
assert_response(rc, 404)
|
assert_response(rc, 404)
|
||||||
|
|
||||||
# Disallow base64 strategies
|
# Disallow base64 strategies
|
||||||
rc = client_get(client, f"{BASE_URI}/strategy/xx:cHJpbnQoImhlbGxvIHdvcmxkIik=")
|
rc = client_get(client, f"{BASE_URI}/strategy/xx:cHJpbnQoImhlbGxvIHdvcmxkIik=")
|
||||||
assert_response(rc, 500)
|
assert_response(rc, 422)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"freqtrade.resolvers.strategy_resolver.StrategyResolver._load_strategy",
|
"freqtrade.resolvers.strategy_resolver.StrategyResolver._load_strategy",
|
||||||
side_effect=Exception("Test"),
|
side_effect=Exception("Test"),
|
||||||
@@ -2581,6 +2612,40 @@ def test_api_strategy(botclient, tmp_path, mocker):
|
|||||||
assert_response(rc, 502)
|
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):
|
def test_api_exchanges(botclient):
|
||||||
_ftbot, client = botclient
|
_ftbot, client = botclient
|
||||||
_ftbot.config["runmode"] = RunMode.WEBSERVER
|
_ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ def test_hyperopt_int_parameter():
|
|||||||
|
|
||||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||||
assert len(list(intpar.range)) == 1
|
assert len(list(intpar.range)) == 1
|
||||||
|
assert intpar.param_type == "IntParameter"
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_real_parameter():
|
def test_hyperopt_real_parameter():
|
||||||
@@ -60,6 +61,7 @@ def test_hyperopt_real_parameter():
|
|||||||
assert isinstance(fltpar.get_space(""), FloatDistribution)
|
assert isinstance(fltpar.get_space(""), FloatDistribution)
|
||||||
|
|
||||||
assert not hasattr(fltpar, "range")
|
assert not hasattr(fltpar, "range")
|
||||||
|
assert fltpar.param_type == "RealParameter"
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_decimal_parameter():
|
def test_hyperopt_decimal_parameter():
|
||||||
@@ -94,6 +96,7 @@ def test_hyperopt_decimal_parameter():
|
|||||||
|
|
||||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||||
assert len(list(decimalpar.range)) == 1
|
assert len(list(decimalpar.range)) == 1
|
||||||
|
assert decimalpar.param_type == "DecimalParameter"
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_categorical_parameter():
|
def test_hyperopt_categorical_parameter():
|
||||||
@@ -133,3 +136,5 @@ def test_hyperopt_categorical_parameter():
|
|||||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||||
assert len(list(catpar.range)) == 1
|
assert len(list(catpar.range)) == 1
|
||||||
assert len(list(boolpar.range)) == 1
|
assert len(list(boolpar.range)) == 1
|
||||||
|
assert boolpar.param_type == "BooleanParameter"
|
||||||
|
assert catpar.param_type == "CategoricalParameter"
|
||||||
|
|||||||
Reference in New Issue
Block a user