Merge branch 'develop' into ci/ccxt.pro

This commit is contained in:
Matthias
2024-06-18 20:34:18 +02:00
25 changed files with 188 additions and 82 deletions

View File

@@ -533,12 +533,12 @@ jobs:
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
deploy-docker: deploy-docker:

View File

@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "7.0.0" rev: "7.1.0"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject] additional_dependencies: [Flake8-pyproject]
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.4.8' rev: 'v0.4.9'
hooks: hooks:
- id: ruff - id: ruff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
markdown==3.6 markdown==3.6
mkdocs==1.6.0 mkdocs==1.6.0
mkdocs-material==9.5.26 mkdocs-material==9.5.27
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1 pymdown-extensions==10.8.1
jinja2==3.1.4 jinja2==3.1.4

View File

@@ -118,6 +118,14 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
freqtrade-client --config rest_config.json <command> [optional parameters] freqtrade-client --config rest_config.json <command> [optional parameters]
``` ```
Commands with many arguments may require keyword arguments (for clarity) - which can be provided as follows:
``` bash
freqtrade-client --config rest_config.json forceenter BTC/USDT long enter_tag=GutFeeling
```
This method will work for all arguments - check the "show" command for a list of available parameters.
??? Note "Programmatic use" ??? Note "Programmatic use"
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API. The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
to do so, please use the following: to do so, please use the following:

View File

@@ -279,7 +279,7 @@ class Exchange:
self._exchange_ws.cleanup() self._exchange_ws.cleanup()
logger.debug("Exchange object destroyed, closing async loop") logger.debug("Exchange object destroyed, closing async loop")
if ( if (
self._api_async getattr(self, "_api_async", None)
and inspect.iscoroutinefunction(self._api_async.close) and inspect.iscoroutinefunction(self._api_async.close)
and self._api_async.session and self._api_async.session
): ):

View File

@@ -1466,6 +1466,8 @@ class RPC:
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
# Manually load hyperparameters, as we don't call the bot-start callback.
strategy.ft_load_hyper_params(False)
if strategy.plot_config and "subplots" not in strategy.plot_config: if strategy.plot_config and "subplots" not in strategy.plot_config:
strategy.plot_config["subplots"] = {} strategy.plot_config["subplots"] = {}

View File

@@ -81,12 +81,12 @@ def print_commands():
print(f"{x}\n\t{doc}\n") print(f"{x}\n\t{doc}\n")
def main_exec(args: Dict[str, Any]): def main_exec(parsed: Dict[str, Any]):
if args.get("show"): if parsed.get("show"):
print_commands() print_commands()
sys.exit() sys.exit()
config = load_config(args["config"]) config = load_config(parsed["config"])
url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1") url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1")
port = config.get("api_server", {}).get("listen_port", "8080") port = config.get("api_server", {}).get("listen_port", "8080")
username = config.get("api_server", {}).get("username") username = config.get("api_server", {}).get("username")
@@ -96,13 +96,24 @@ def main_exec(args: Dict[str, Any]):
client = FtRestClient(server_url, username, password) client = FtRestClient(server_url, username, password)
m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")] m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")]
command = args["command"] command = parsed["command"]
if command not in m: if command not in m:
logger.error(f"Command {command} not defined") logger.error(f"Command {command} not defined")
print_commands() print_commands()
return return
print(json.dumps(getattr(client, command)(*args["command_arguments"]))) # Split arguments with = into key/value pairs
kwargs = {x.split("=")[0]: x.split("=")[1] for x in parsed["command_arguments"] if "=" in x}
args = [x for x in parsed["command_arguments"] if "=" not in x]
try:
res = getattr(client, command)(*args, **kwargs)
print(json.dumps(res))
except TypeError as e:
logger.error(f"Error executing command {command}: {e}")
sys.exit(1)
except Exception as e:
logger.error(f"Fatal Error executing command {command}: {e}")
sys.exit(1)
def main(): def main():

View File

@@ -54,7 +54,7 @@ class FtRestClient:
# return resp.text # return resp.text
return resp.json() return resp.json()
except ConnectionError: except ConnectionError:
logger.warning("Connection error") logger.warning(f"Connection error - could not connect to {netloc}.")
def _get(self, apipath, params: ParamsT = None): def _get(self, apipath, params: ParamsT = None):
return self._call("GET", apipath, params=params) return self._call("GET", apipath, params=params)
@@ -312,20 +312,48 @@ class FtRestClient:
data = {"pair": pair, "price": price} data = {"pair": pair, "price": price}
return self._post("forcebuy", data=data) return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None): def forceenter(
self,
pair,
side,
price=None,
*,
order_type=None,
stake_amount=None,
leverage=None,
enter_tag=None,
):
"""Force entering a trade """Force entering a trade
:param pair: Pair to buy (ETH/BTC) :param pair: Pair to buy (ETH/BTC)
:param side: 'long' or 'short' :param side: 'long' or 'short'
:param price: Optional - price to buy :param price: Optional - price to buy
:param order_type: Optional keyword argument - 'limit' or 'market'
:param stake_amount: Optional keyword argument - stake amount (as float)
:param leverage: Optional keyword argument - leverage (as float)
:param enter_tag: Optional keyword argument - entry tag (as string, default: 'force_enter')
:return: json object of the trade :return: json object of the trade
""" """
data = { data = {
"pair": pair, "pair": pair,
"side": side, "side": side,
} }
if price: if price:
data["price"] = price data["price"] = price
if order_type:
data["ordertype"] = order_type
if stake_amount:
data["stakeamount"] = stake_amount
if leverage:
data["leverage"] = leverage
if enter_tag:
data["entry_tag"] = enter_tag
return self._post("forceenter", data=data) return self._post("forceenter", data=data)
def forceexit(self, tradeid, ordertype=None, amount=None): def forceexit(self, tradeid, ordertype=None, amount=None):

View File

@@ -1,5 +1,5 @@
import re import re
from unittest.mock import MagicMock from unittest.mock import ANY, MagicMock
import pytest import pytest
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
@@ -52,70 +52,89 @@ def test_FtRestClient_call_invalid(caplog):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method,args", "method,args,kwargs",
[ [
("start", []), ("start", [], {}),
("stop", []), ("stop", [], {}),
("stopbuy", []), ("stopbuy", [], {}),
("reload_config", []), ("reload_config", [], {}),
("balance", []), ("balance", [], {}),
("count", []), ("count", [], {}),
("entries", []), ("entries", [], {}),
("exits", []), ("exits", [], {}),
("mix_tags", []), ("mix_tags", [], {}),
("locks", []), ("locks", [], {}),
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"]), ("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"], {}),
("delete_lock", [2]), ("delete_lock", [2], {}),
("daily", []), ("daily", [], {}),
("daily", [15]), ("daily", [15], {}),
("weekly", []), ("weekly", [], {}),
("weekly", [15]), ("weekly", [15], {}),
("monthly", []), ("monthly", [], {}),
("monthly", [12]), ("monthly", [12], {}),
("edge", []), ("edge", [], {}),
("profit", []), ("profit", [], {}),
("stats", []), ("stats", [], {}),
("performance", []), ("performance", [], {}),
("status", []), ("status", [], {}),
("version", []), ("version", [], {}),
("show_config", []), ("show_config", [], {}),
("ping", []), ("ping", [], {}),
("logs", []), ("logs", [], {}),
("logs", [55]), ("logs", [55], {}),
("trades", []), ("trades", [], {}),
("trades", [5]), ("trades", [5], {}),
("trades", [5, 5]), # With offset ("trades", [5, 5], {}), # With offset
("trade", [1]), ("trade", [1], {}),
("delete_trade", [1]), ("delete_trade", [1], {}),
("cancel_open_order", [1]), ("cancel_open_order", [1], {}),
("whitelist", []), ("whitelist", [], {}),
("blacklist", []), ("blacklist", [], {}),
("blacklist", ["XRP/USDT"]), ("blacklist", ["XRP/USDT"], {}),
("blacklist", ["XRP/USDT", "BTC/USDT"]), ("blacklist", ["XRP/USDT", "BTC/USDT"], {}),
("forcebuy", ["XRP/USDT"]), ("forcebuy", ["XRP/USDT"], {}),
("forcebuy", ["XRP/USDT", 1.5]), ("forcebuy", ["XRP/USDT", 1.5], {}),
("forceenter", ["XRP/USDT", "short"]), ("forceenter", ["XRP/USDT", "short"], {}),
("forceenter", ["XRP/USDT", "short", 1.5]), ("forceenter", ["XRP/USDT", "short", 1.5], {}),
("forceexit", [1]), ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market"}),
("forceexit", [1, "limit"]), ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market", "stake_amount": 100}),
("forceexit", [1, "limit", 100]), (
("strategies", []), "forceenter",
("strategy", ["sampleStrategy"]), ["XRP/USDT", "short", 1.5],
("pairlists_available", []), {"order_type": "market", "stake_amount": 100, "leverage": 10.0},
("plot_config", []), ),
("available_pairs", []), (
("available_pairs", ["5m"]), "forceenter",
("pair_candles", ["XRP/USDT", "5m"]), ["XRP/USDT", "short", 1.5],
("pair_candles", ["XRP/USDT", "5m", 500]), {
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"]), "order_type": "market",
("sysinfo", []), "stake_amount": 100,
("health", []), "leverage": 10.0,
"enter_tag": "test_force_enter",
},
),
("forceexit", [1], {}),
("forceexit", [1, "limit"], {}),
("forceexit", [1, "limit", 100], {}),
("strategies", [], {}),
("strategy", ["sampleStrategy"], {}),
("pairlists_available", [], {}),
("plot_config", [], {}),
("available_pairs", [], {}),
("available_pairs", ["5m"], {}),
("pair_candles", ["XRP/USDT", "5m"], {}),
("pair_candles", ["XRP/USDT", "5m", 500], {}),
("pair_candles", ["XRP/USDT", "5m", 500], {"columns": ["close_time,close"]}),
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}),
("pair_history", ["XRP/USDT", "5m"], {"strategy": "SampleStrategy"}),
("sysinfo", [], {}),
("health", [], {}),
], ],
) )
def test_FtRestClient_call_explicit_methods(method, args): def test_FtRestClient_call_explicit_methods(method, args, kwargs):
client, mock = get_rest_client() client, mock = get_rest_client()
exec = getattr(client, method) exec = getattr(client, method)
exec(*args) exec(*args, **kwargs)
assert mock.call_count == 1 assert mock.call_count == 1
@@ -148,3 +167,40 @@ def test_ft_client(mocker, capsys, caplog):
) )
main_exec(args) main_exec(args)
assert log_has_re("Command whatever not defined", caplog) assert log_has_re("Command whatever not defined", caplog)
@pytest.mark.parametrize(
"params, expected_args, expected_kwargs",
[
("forceenter BTC/USDT long", ["BTC/USDT", "long"], {}),
("forceenter BTC/USDT long limit", ["BTC/USDT", "long", "limit"], {}),
(
# Skip most parameters, only providing enter_tag
"forceenter BTC/USDT long enter_tag=deadBeef",
["BTC/USDT", "long"],
{"enter_tag": "deadBeef"},
),
(
"forceenter BTC/USDT long invalid_key=123",
[],
SystemExit,
# {"invalid_key": "deadBeef"},
),
],
)
def test_ft_client_argparsing(mocker, params, expected_args, expected_kwargs, caplog):
mocked_method = params.split(" ")[0]
mocker.patch("freqtrade_client.ft_client.load_config", return_value={}, autospec=True)
mm = mocker.patch(
f"freqtrade_client.ft_client.FtRestClient.{mocked_method}", return_value={}, autospec=True
)
args = add_arguments(params.split(" "))
if isinstance(expected_kwargs, dict):
main_exec(args)
mm.assert_called_once_with(ANY, *expected_args, **expected_kwargs)
else:
with pytest.raises(expected_kwargs):
main_exec(args)
assert log_has_re(f"Error executing command {mocked_method}: got an unexpected .*", caplog)
mm.assert_not_called()

View File

@@ -7,7 +7,7 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==4.0.1 coveralls==4.0.1
ruff==0.4.8 ruff==0.4.9
mypy==1.10.0 mypy==1.10.0
pre-commit==3.7.1 pre-commit==3.7.1
pytest==8.2.2 pytest==8.2.2

View File

@@ -2,7 +2,8 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==2.2.2 torch==2.3.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
stable_baselines3==2.3.2 stable_baselines3==2.3.2
sb3_contrib>=2.2.1 sb3_contrib>=2.2.1

View File

@@ -6,7 +6,7 @@
scikit-learn==1.5.0 scikit-learn==1.5.0
joblib==1.4.2 joblib==1.4.2
catboost==1.2.5; 'arm' not in platform_machine catboost==1.2.5; 'arm' not in platform_machine
lightgbm==4.3.0 lightgbm==4.4.0
xgboost==2.0.3 xgboost==2.0.3
tensorboard==2.17.0 tensorboard==2.17.0
datasieve==0.1.7 datasieve==0.1.7

View File

@@ -5,4 +5,4 @@
scipy==1.13.1 scipy==1.13.1
scikit-learn==1.5.0 scikit-learn==1.5.0
ft-scikit-optimize==0.9.2 ft-scikit-optimize==0.9.2
filelock==3.14.0 filelock==3.15.1

View File

@@ -4,7 +4,7 @@ bottleneck==1.3.8
numexpr==2.10.0 numexpr==2.10.0
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.3.42 ccxt==4.3.46
cryptography==42.0.8 cryptography==42.0.8
aiohttp==3.9.5 aiohttp==3.9.5
SQLAlchemy==2.0.30 SQLAlchemy==2.0.30
@@ -14,9 +14,9 @@ httpx>=0.24.1
humanize==4.9.0 humanize==4.9.0
cachetools==5.3.3 cachetools==5.3.3
requests==2.32.3 requests==2.32.3
urllib3==2.2.1 urllib3==2.2.2
jsonschema==4.22.0 jsonschema==4.22.0
TA-Lib==0.4.30 TA-Lib==0.4.31
technical==1.4.3 technical==1.4.3
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
@@ -32,14 +32,14 @@ py_find_1st==1.1.6
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.17 python-rapidjson==1.17
# Properly format api responses # Properly format api responses
orjson==3.10.3 orjson==3.10.5
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.111.0 fastapi==0.111.0
pydantic==2.7.3 pydantic==2.7.4
uvicorn==0.30.1 uvicorn==0.30.1
pyjwt==2.8.0 pyjwt==2.8.0
aiofiles==23.2.1 aiofiles==23.2.1