mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-02-14 18:20:31 +00:00
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 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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user