From 27970b424d96a9e0369c47d0586c78840cbcdd83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 10:31:05 +0100 Subject: [PATCH 01/50] Add webUI serving to api-server --- .gitignore | 1 + freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 28 +++++++++++++++++++++++++++ freqtrade/rpc/api_server/webserver.py | 4 ++++ requirements.txt | 1 + tests/rpc/test_rpc_apiserver.py | 2 +- 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/web_ui.py diff --git a/.gitignore b/.gitignore index f206fce66..4720ff5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ user_data/* user_data/notebooks/* freqtrade-plot.html freqtrade-profit-plot.html +freqtrade/rpc/api_server/ui/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py new file mode 100644 index 000000000..971f9e4de --- /dev/null +++ b/freqtrade/rpc/api_server/web_ui.py @@ -0,0 +1,28 @@ +from pathlib import Path + +from fastapi import APIRouter +from fastapi.exceptions import HTTPException +from starlette.responses import FileResponse + + +router_ui = APIRouter() + + +@router_ui.get('/favicon.ico', include_in_schema=False) +async def favicon(): + return FileResponse(Path(__file__).parent / 'ui/favicon.ico') + + +@router_ui.get('/{rest_of_path:path}', include_in_schema=False) +async def index_html(rest_of_path: str): + """ + Emulate path fallback to index.html. + """ + if rest_of_path.startswith('api') or rest_of_path.startswith('.'): + raise HTTPException(status_code=404, detail="Not Found") + uibase = Path(__file__).parent / 'ui' + if (uibase / rest_of_path).is_file(): + return FileResponse(uibase / rest_of_path) + + # Fall back to index.html, as indicated by vue router docs + return FileResponse(uibase / 'index.html') diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 9c0779274..f3eaa1ebc 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -57,12 +57,16 @@ class ApiServer(RPCHandler): from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public + from freqtrade.rpc.api_server.web_ui import router_ui + app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", dependencies=[Depends(http_basic_or_jwt_token)], ) app.include_router(router_login, prefix="/api/v1", tags=["auth"]) + # UI Router MUST be last! + app.include_router(router_ui, prefix='') app.add_middleware( CORSMiddleware, diff --git a/requirements.txt b/requirements.txt index c6cb3e445..5993fb3f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ sdnotify==0.3.2 fastapi==0.63.0 uvicorn==0.13.3 pyjwt==2.0.1 +aiofiles==0.6.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b050e5694..919481598 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -83,7 +83,7 @@ def assert_response(response, expected_code=200, needs_cors=True): def test_api_not_found(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/invalid_url") + rc = client_get(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json() == {"detail": "Not Found"} From a47616eed4648ad4b7331251c79b85354bba62d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 13:55:16 +0100 Subject: [PATCH 02/50] Add UI installation subcommand --- freqtrade/commands/__init__.py | 4 +- freqtrade/commands/arguments.py | 12 +++++- freqtrade/commands/deploy_commands.py | 54 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 21c5d6812..784b99bed 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -10,8 +10,8 @@ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, - start_new_strategy) +from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, + start_new_hyperopt, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a6c8a245f..b39c75640 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -167,8 +167,8 @@ class Arguments: from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_hyperopt, start_new_strategy, start_plot_dataframe, start_plot_profit, @@ -355,6 +355,14 @@ class Arguments: test_pairlist_cmd.set_defaults(func=start_test_pairlist) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) + # Add install-ui subcommand + install_ui_cmd = subparsers.add_parser( + 'install-ui', + help='Install FreqUI', + ) + install_ui_cmd.set_defaults(func=start_install_ui) + self._build_args(optionlist=[], parser=install_ui_cmd) + # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( 'plot-dataframe', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index a0105e140..b43ad5970 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -137,3 +137,57 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + +def clean_ui_subdir(directory: Path): + print(directory) + if directory.is_dir(): + logger.info("Removing UI directory content") + + for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root + if p.name == '.gitkeep': + continue + if p.is_file(): + p.unlink() + elif p.is_dir(): + p.rmdir() + + +def download_and_install_ui(dest_folder: Path): + import requests + from io import BytesIO + from zipfile import ZipFile + + base_url = 'https://api.github.com/repos/freqtrade/frequi/' + # Get base UI Repo path + + resp = requests.get(f"{base_url}releases") + resp.raise_for_status() + r = resp.json() + + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + + dl_url = r[0]['browser_download_url'] + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + print(destfile) + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + # Download a new version + download_and_install_ui(dest_folder) From 87ed2d750205d0e1b10a7c185f853cbb13a51d9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:06:06 +0100 Subject: [PATCH 03/50] Write some tests for UI Downloading --- freqtrade/commands/deploy_commands.py | 46 +++++++++---------- tests/commands/test_commands.py | 65 ++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b43ad5970..e951c962f 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -3,6 +3,8 @@ import sys from pathlib import Path from typing import Any, Dict +import requests + from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES @@ -140,9 +142,8 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: def clean_ui_subdir(directory: Path): - print(directory) if directory.is_dir(): - logger.info("Removing UI directory content") + logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root if p.name == '.gitkeep': @@ -153,11 +154,28 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path): - import requests +def download_and_install_ui(dest_folder: Path, dl_url: str): from io import BytesIO from zipfile import ZipFile + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -170,24 +188,6 @@ def download_and_install_ui(dest_folder: Path): r = resp.json() dl_url = r[0]['browser_download_url'] - logger.info(f"Downloading {dl_url}") - resp = requests.get(dl_url).content - with ZipFile(BytesIO(resp)) as zf: - for fn in zf.filelist: - with zf.open(fn) as x: - destfile = dest_folder / fn.filename - print(destfile) - if fn.is_dir(): - destfile.mkdir(exist_ok=True) - else: - destfile.write_bytes(x.read()) - - -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) # Download a new version - download_and_install_ui(dest_folder) + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index f8ecc8218..12d18b3a7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,16 +1,19 @@ +from io import BytesIO import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +from zipfile import ZipFile import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, start_list_timeframes, + start_new_hyperopt, start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -546,7 +549,7 @@ def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): start_new_hyperopt(get_args(args)) -def test_start_new_hyperopt_no_arg(mocker, caplog): +def test_start_new_hyperopt_no_arg(mocker): args = [ "new-hyperopt", ] @@ -555,6 +558,56 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): start_new_hyperopt(get_args(args)) +def test_start_install_ui(mocker): + clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + args = [ + "install-ui", + ] + start_install_ui(args) + assert clean_mock.call_count == 1 + + assert download_mock.call_count == 1 + + +def test_clean_ui_subdir(mocker, tmpdir, caplog): + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, True]) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_file", + side_effect=[False, True]) + rd_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.rmdir") + ul_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.unlink") + + mocker.patch("freqtrade.commands.deploy_commands.Path.glob", + return_value=[Path('test1'), Path('test2'), Path('.gitkeep')]) + folder = Path(tmpdir) / "uitests" + clean_ui_subdir(folder) + assert log_has("Removing UI directory content.", caplog) + assert rd_mock.call_count == 1 + assert ul_mock.call_count == 1 + + +def test_download_and_install_ui(mocker, tmpdir, caplog): + # Should be something "zip-like" + requests_mock = MagicMock() + file_like_object = BytesIO() + with ZipFile(file_like_object, mode='w') as zipfile: + for file in ('test1.txt', 'hello/', 'test2.txt'): + zipfile.writestr(file, file) + file_like_object.seek(0) + requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, False]) + mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") + wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" + download_and_install_ui(folder, 'http://whatever.xxx') + + assert mkdir_mock.call_count == 1 + assert wb_mock.call_count == 2 + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From a1a35115ad7a1c5610920c628ff82fe71b842c67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:46:59 +0100 Subject: [PATCH 04/50] Extract get_ui_download_url --- freqtrade/commands/deploy_commands.py | 17 +++++++++++------ tests/commands/test_commands.py | 10 +++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index e951c962f..174f1bb59 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -170,12 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) - +def get_ui_download_url() -> str: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -188,6 +183,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: r = resp.json() dl_url = r[0]['browser_download_url'] + return dl_url + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 12d18b3a7..a39044c18 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -560,13 +560,14 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') args = [ "install-ui", ] start_install_ui(args) assert clean_mock.call_count == 1 - + assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 @@ -588,7 +589,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): def test_download_and_install_ui(mocker, tmpdir, caplog): - # Should be something "zip-like" + # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() with ZipFile(file_like_object, mode='w') as zipfile: @@ -596,13 +597,16 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): zipfile.writestr(file, file) file_like_object.seek(0) requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx') + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 From ddc99553bd422c250f1d9beba59fccd90211b999 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:52:51 +0100 Subject: [PATCH 05/50] Add test case for get_ui_download_url --- tests/commands/test_commands.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index a39044c18..ba45f1618 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -13,7 +13,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -588,7 +588,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): assert ul_mock.call_count == 1 -def test_download_and_install_ui(mocker, tmpdir, caplog): +def test_download_and_install_ui(mocker, tmpdir): # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() @@ -612,6 +612,18 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): assert wb_mock.call_count == 2 +def test_get_ui_download_url(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{'assets_url': 'http://whatever.json'}], + [{'browser_download_url': 'http://download.zip'}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x = get_ui_download_url() + assert get_mock.call_count == 2 + assert x == 'http://download.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From f05f2c45e89dc9dd394bd1165ce3e7451562f3a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:15:27 +0100 Subject: [PATCH 06/50] Allow deleting of UI only --- freqtrade/commands/arguments.py | 4 +++- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/deploy_commands.py | 10 ++++++---- tests/commands/test_commands.py | 15 ++++++++++++++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b39c75640..c64c11a18 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -70,6 +70,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe"] +ARGS_INSTALL_UI = ["erase_ui_only"] + ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", @@ -361,7 +363,7 @@ class Arguments: help='Install FreqUI', ) install_ui_cmd.set_defaults(func=start_install_ui) - self._build_args(optionlist=[], parser=install_ui_cmd) + self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 668b4abf5..7dc85377d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -387,6 +387,12 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + "erase_ui_only": Arg( + '--erase', + help="Clean UI folder, don't download new version.", + action='store_true', + default=False, + ), # Templating options "template": Arg( '--template', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 174f1bb59..b9bda9c63 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -191,8 +191,10 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. clean_ui_subdir(dest_folder) + if args.get('erase_ui_only'): + logger.info("Erased UI directory content. Not downloading new version.") + else: + dl_url = get_ui_download_url() - dl_url = get_ui_download_url() - - # Download a new version - download_and_install_ui(dest_folder, dl_url) + # Download a new version + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index ba45f1618..fdc2b204b 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -565,11 +565,24 @@ def test_start_install_ui(mocker): args = [ "install-ui", ] - start_install_ui(args) + start_install_ui(get_args(args)) assert clean_mock.call_count == 1 assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 + clean_mock.reset_mock() + get_url_mock.reset_mock() + download_mock.reset_mock() + + args = [ + "install-ui", + "--erase", + ] + start_install_ui(get_args(args)) + assert clean_mock.call_count == 1 + assert get_url_mock.call_count == 0 + assert download_mock.call_count == 0 + def test_clean_ui_subdir(mocker, tmpdir, caplog): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", From e928d2991ddb4ff67aa2d52fda5caf78052e14b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:27:15 +0100 Subject: [PATCH 07/50] Add fallback file --- freqtrade/commands/deploy_commands.py | 2 +- .../rpc/api_server/ui/fallback_file.html | 31 +++++++++++++++++++ freqtrade/rpc/api_server/web_ui.py | 5 ++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/fallback_file.html diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b9bda9c63..8a8d2373e 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name == '.gitkeep': + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/freqtrade/rpc/api_server/ui/fallback_file.html b/freqtrade/rpc/api_server/ui/fallback_file.html new file mode 100644 index 000000000..7943530af --- /dev/null +++ b/freqtrade/rpc/api_server/ui/fallback_file.html @@ -0,0 +1,31 @@ + + + + + + Freqtrade UI + + + +
+

Freqtrade UI not installed.

+

Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.

+

You can then refresh this page and you should see the UI.

+
+ + diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 971f9e4de..6d397da56 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -24,5 +24,8 @@ async def index_html(rest_of_path: str): if (uibase / rest_of_path).is_file(): return FileResponse(uibase / rest_of_path) + index_file = uibase / 'index.html' + if not index_file.is_file(): + return FileResponse(uibase / 'fallback_file.html') # Fall back to index.html, as indicated by vue router docs - return FileResponse(uibase / 'index.html') + return FileResponse(index_file) From 35c2e2556ece3cc93634f4435b6044374794850d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 20:48:52 +0100 Subject: [PATCH 08/50] Document FreqUI usage --- docker-compose.yml | 5 ++++ docs/rest-api.md | 68 ++++++++++++++++++++++++++-------------------- mkdocs.yml | 2 +- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7094500b4..06cce4e8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,11 @@ services: container_name: freqtrade volumes: - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation + # before enabling this. + #ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade diff --git a/docs/rest-api.md b/docs/rest-api.md index 2c7142c61..e2b94f080 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -1,4 +1,19 @@ -# REST API Usage +# REST API & FreqUI + +## FreqUI + +Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. + +By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`. +This same command can also be used to update freqUI, should there be a new release. + +Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). + +!!! info "Alpha release" + FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). + +!!! Note "developers" + Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. ## Configuration @@ -23,9 +38,6 @@ Sample configuration: !!! Danger "Security warning" By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger "Password selection" - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. - You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: @@ -35,16 +47,22 @@ This should return the response: All other endpoints return sensitive info and require authentication and are therefore not available through a web browser. -To generate a secure password, either use a password manager, or use the below code snipped. +### Security + +To generate a secure password, best use a password manager, or use the below code. ``` python import secrets secrets.token_hex() ``` -!!! Hint +!!! Hint "JWT token" Use the same method to also generate a JWT secret key (`jwt_secret_key`). +!!! Danger "Password selection" + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. + Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. @@ -57,28 +75,20 @@ If you run your bot using docker, you'll need to have the bot listen to incoming }, ``` -Add the following to your docker command: +Uncomment the following from your docker-compose file: -``` bash - -p 127.0.0.1:8080:8080 -``` - -A complete sample-command may then look as follows: - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -p 127.0.0.1:8080:8080 \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +```yml + ports: + - "127.0.0.1:8080:8080" ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. -## Consuming the API + +## Rest API + +### Consuming the API You can consume the API by using the script `scripts/rest_client.py`. The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. @@ -89,7 +99,7 @@ python3 scripts/rest_client.py [optional parameters] By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. -### Minimalistic client config +#### Minimalistic client config ``` json { @@ -105,7 +115,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use python3 scripts/rest_client.py --config rest_config.json [optional parameters] ``` -## Available endpoints +### Available endpoints | Command | Description | |----------|-------------| @@ -264,12 +274,12 @@ whitelist ``` -## OpenAPI interface +### OpenAPI interface To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. -## Advanced API usage using JWT tokens +### Advanced API usage using JWT tokens !!! Note The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis. @@ -294,9 +304,9 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` -## CORS +### CORS -All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. diff --git a/mkdocs.yml b/mkdocs.yml index 4545e8d84..2d37ddba8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md - - REST API: rest-api.md + - REST API & FreqUI: rest-api.md - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md From 1df0aa875193fc64a1c9ec6d8aae9bd4aeda277b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 21:08:15 +0100 Subject: [PATCH 09/50] Add ui installation to docker container builds --- Dockerfile | 4 +++- Dockerfile.armhf | 4 +++- docker-compose.yml | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 445f909b0..8d4f0ebe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ RUN pip install -e . --no-cache-dir \ - && mkdir /freqtrade/user_data/ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2c7d4538a..ec6aa72b8 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/docker-compose.yml b/docker-compose.yml index 06cce4e8e..1f63059f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,8 @@ services: # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation # before enabling this. - #ports: - # - "127.0.0.1:8080:8080" + # ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade From 2af1d2d6390357c267b0ab4a3ccb5e07e7086576 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:38:40 +0100 Subject: [PATCH 10/50] Extract last FreqUI version from api response --- freqtrade/commands/deploy_commands.py | 24 ++++++++++++++++-------- tests/commands/test_commands.py | 25 +++++++++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 8a8d2373e..44ddc1fdc 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Tuple import requests @@ -170,7 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def get_ui_download_url() -> str: +def get_ui_download_url() -> Tuple[str, str]: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -178,23 +178,31 @@ def get_ui_download_url() -> str: resp.raise_for_status() r = resp.json() - assets = r[0]['assets_url'] - resp = requests.get(assets) - r = resp.json() + latest_version = r[0]['name'] + assets = r[0].get('assets', []) + dl_url = '' + if assets and len(assets) > 0: + dl_url = assets[0]['browser_download_url'] - dl_url = r[0]['browser_download_url'] - return dl_url + # URL not found - try assets url + if not dl_url: + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + dl_url = r[0]['browser_download_url'] + + return dl_url, latest_version def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. + dl_url, latest_version = get_ui_download_url() clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): logger.info("Erased UI directory content. Not downloading new version.") else: - dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index fdc2b204b..dcc0cd1d2 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,5 +1,5 @@ -from io import BytesIO import re +from io import BytesIO from pathlib import Path from unittest.mock import MagicMock, PropertyMock from zipfile import ZipFile @@ -13,7 +13,8 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url +from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, + get_ui_download_url) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -628,15 +629,31 @@ def test_download_and_install_ui(mocker, tmpdir): def test_get_ui_download_url(mocker): response = MagicMock() response.json = MagicMock( - side_effect=[[{'assets_url': 'http://whatever.json'}], + side_effect=[[{'assets_url': 'http://whatever.json', 'name': '0.0.1'}], [{'browser_download_url': 'http://download.zip'}]]) get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=response) - x = get_ui_download_url() + x, last_version = get_ui_download_url() assert get_mock.call_count == 2 + assert last_version == '0.0.1' assert x == 'http://download.zip' +def test_get_ui_download_url_direct(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{ + 'assets_url': 'http://whatever.json', + 'name': '0.0.1', + 'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x, last_version = get_ui_download_url() + assert get_mock.call_count == 1 + assert last_version == '0.0.1' + assert x == 'http://download11.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From 944d674eeb6d590685f1970c60210f80b93fc726 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:39:46 +0100 Subject: [PATCH 11/50] Store freqUI version and read it again --- freqtrade/commands/deploy_commands.py | 25 +++++++++++++++++++++---- tests/commands/test_commands.py | 10 ++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 44ddc1fdc..c4e958df6 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple import requests @@ -154,7 +154,16 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path, dl_url: str): +def read_ui_version(dest_folder: Path) -> Optional[str]: + file = dest_folder / '.uiversion' + if not file.is_file(): + return None + + with file.open('r') as f: + return f.read() + + +def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): from io import BytesIO from zipfile import ZipFile @@ -168,6 +177,8 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.mkdir(exist_ok=True) else: destfile.write_bytes(x.read()) + with (dest_folder / '.uiversion').open('w') as f: + f.write(version) def get_ui_download_url() -> Tuple[str, str]: @@ -199,10 +210,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() - clean_ui_subdir(dest_folder) + + curr_version = read_ui_version(dest_folder) + if curr_version == latest_version and not args.get('erase_ui_only'): + logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + return + if args.get('erase_ui_only'): + clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: # Download a new version - download_and_install_ui(dest_folder, dl_url) + download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index dcc0cd1d2..b243df192 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -14,7 +14,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, - get_ui_download_url) + get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -563,6 +563,7 @@ def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ "install-ui", ] @@ -616,15 +617,16 @@ def test_download_and_install_ui(mocker, tmpdir): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) - mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') + folder.mkdir(exist_ok=True) + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') - assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 + assert read_ui_version(folder) == '22' + def test_get_ui_download_url(mocker): response = MagicMock() From 7b3d99819f60f03a9464c2f67720ed7c83c17dc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:43:16 +0100 Subject: [PATCH 12/50] Fix bug with not cleaning UI folder --- freqtrade/commands/deploy_commands.py | 5 ++--- tests/commands/test_commands.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index c4e958df6..62da6abf3 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -213,13 +213,12 @@ def start_install_ui(args: Dict[str, Any]) -> None: curr_version = read_ui_version(dest_folder) if curr_version == latest_version and not args.get('erase_ui_only'): - logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") return + clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): - clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: - # Download a new version download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b243df192..0d2b9e394 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -561,7 +561,8 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') - get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url', + return_value=('https://example.com/whatever', '0.0.1')) download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ @@ -582,7 +583,7 @@ def test_start_install_ui(mocker): ] start_install_ui(get_args(args)) assert clean_mock.call_count == 1 - assert get_url_mock.call_count == 0 + assert get_url_mock.call_count == 1 assert download_mock.call_count == 0 From a87a885ccd3221fda93d3f1c3a37c754ba64403e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:54:58 +0100 Subject: [PATCH 13/50] Don't use Path object to return fileresponses --- freqtrade/rpc/api_server/web_ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 6d397da56..4876c9077 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -22,10 +22,10 @@ async def index_html(rest_of_path: str): raise HTTPException(status_code=404, detail="Not Found") uibase = Path(__file__).parent / 'ui' if (uibase / rest_of_path).is_file(): - return FileResponse(uibase / rest_of_path) + return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(uibase / 'fallback_file.html') + return FileResponse(str(uibase / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs - return FileResponse(index_file) + return FileResponse(str(index_file)) From 28be71806f000a0ba5a96307d721065ae42c76fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:00:13 +0100 Subject: [PATCH 14/50] Install html file as well --- MANIFEST.in | 2 ++ freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/ui/favicon.ico | Bin 0 -> 126794 bytes 3 files changed, 2 insertions(+) delete mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/ui/favicon.ico diff --git a/MANIFEST.in b/MANIFEST.in index 2f59bcc7a..adbcd2e30 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include LICENSE include README.md recursive-include freqtrade *.py recursive-include freqtrade/templates/ *.j2 *.ipynb +include freqtrade/rpc/api_server/ui/fallback_file.html +include freqtrade/rpc/api_server/ui/favicon.ico diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/freqtrade/rpc/api_server/ui/favicon.ico b/freqtrade/rpc/api_server/ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78c7e43b1e6a5756492a1d53813e1ec47f98efdf GIT binary patch literal 126794 zcmeHQ378ed5q|3m3mz=dctpe%QM{t!eWB}_D8?I2G%6YsyyA_T7;zWzict*lz$=0m zibq7nAR1u>BZwC&i6+swDg;pw5X2Ri?fn1j%+S-XyXSiE?JjSMZ<(2%?&|95dOh9M zRn@s%1^zwuOb-8Vn%iYzMQ;6EF1Ho_9>d?Diq>@8W>6#!eDJ{sJ;#n6d+hDE-#%#Q z(4m+8@|V9HdDKxyjooj*{U+_S(@t-0v&}X$dEI~i{U;uM^wDE}^{Zcvy!P5_2Q&Y; zapMmE=%bG|ED~uM+A`*V5TatrlqtJia>*sv9dpbv4{xx+2D8Bqk@j!njW=#Q@x&AF zyW)x~u9`V>=C)d)}>ExA{@zt5|u~Uw{4C=bd-naE!No%7E=z6Jv0I zW7)p@?)yB7tiWID?|Y$ebn4XUn}ZHIX#AURzPU||=44q{K@LoqFyX-6cH3=gp=7I= zcn&z=fM+L7nzZ*S*bP~eEFAdo!w)w(^2j3}FQ#l212^W*%TGS}Qpg(`gG?y zapFXA|NZxi>#n;_oO4x?!No(ld!g55LZrn_wFrjyX`jdpa1+PxnM>p`OZ7#VcRyvHAZynhGoo{V&G;D7)7-x~P#-VsMeq30Bs6N)$9cq4+86+R2+ z5d#Jc5FE>6(A{#;MHgKQYOrgWK}X{N`mO6e`|R@~=#JFqSht9C&N)Z?^Pm4LyZ|P^ z$lnToGjU`V`)Uue-5ss3%9-AI^X6^Pt5>i0qsnRj{{02!eF-G0_(lHlkAH|=cG)G; z?~U!$TT2y;l2{*o6xhcJJA<=Lb>b6np&w^VyVARNCUO&l1>Qiz=@(KL7mlwc2-& zl_zzLjg3F};SYcKMwpyZH#%a(h=K@NQ&W?(27CD7hsCY8-YRas`DSN54;`-f{qKJ- z{`ki~I(mdhAAMBtTmJ30-=?m2#flXIc8H>)q9RmgF?V|A@;kzg)cf>Z^ismlPg) z=pnJ@nrnvEfBf;sk0yWGXU~-%RVSTv(mk?F=C7{`u!eo-gX^*QKqy z?z)cLzW@IFiA46Tx84%auZGI&;K75hM*SJIF$eCs=bjTmg4I5X`<1=;7n!|FiA5G`?aOTA z71zlxz4TI4F%|#%*T2NByY5<48MM)>dxw3uSYs@8#bNtgBS(%r6?J7$A`Z}(F3heE zeG*wtsZ)xwrbhEhF3Y5gc7{=bwp%tj$v5bgzh9!RD*I~a*J0}q_&G<`2Mr%SJd&=` zR$sB-(VphM`|gV*uGCw=_uDG3urs)`?1iWV3}|R**wZE(TuaTLKR;5x{^?JD5?q7Z z@U@qBoI}%YI|_XK@yBBAwbwSbi~a>Kzx?tZCFriQt^#}Aui511x#ymX(w)xQ#fB&9 zJLM$7b5U9(w4Z0(SgHxrtg2^Tm61@Bsg*6@w)U_39)1R5*|KF$otz)b@3-H6d;eXS+{op^?zu;dF)_5AoC}1l z6<>e-b%yK7pa>kmJZu8c24uo}W*^R)H7m3a;mcC6{U`cSc|e;&d5`{@Yp(HLIam3e zM|09E_ljXh6~M21AU|yN;(PEUXvmjeED**_10S( zaoF(d-q9z}1I`g;eGz?9WRTzKaK%l7u2ir+VSrdJ#`Z#wyX?L9-rE#)XHY;6ARfxC zKxgE86#G8foY-*0eb<(oP$*kF>tdYW;03t5sE1k!)~j+ojU(J;NXIWAYcz85Dp64?OU|u|Q+wcN9N9+J@M0 z*xs$T-g=IGDs`Ol-@EU=>;FcUE?sJT$4o3o+kV*Eh~NG0cLKf(iRqDPrTst{&;~~? zqu#rEKqP07mII9a2pn2|@44rmp?d$YzB1wV(&=|y4B&ngV`6yqB)M`PjF@quqrG4|wHv+%KP#SD> zM$y$FR*-cq-Z~*a1jxhBT=bv-u&b5jIV(c0F$b?r-mt~dT`hWlX---Vi zb43N}Xt`&g)*JPidv9U(+SJwA@L@jF8}Bnn!2#G}{07CfeBOQc-PLZ-dq(_oZCMje zUAio*j1i>E6DMu+%{Lc_;U1tB@!(?n#K?8hFD49Z`Z~sv2fhqaaA5Df_cq3(;2OmS z9Ip^FJ+*8VT2{dhhbGk?<@D1}PfRO)xx!$`kRe9=@B_LxMK@(d!2#&1mV14xUa?1L z?H=0tsb!Kp7fV*T*T(VG1NwC(q0cMB`Hl_fZ|mh%ubXbV>3c}aARz}n{q)myP}Im@ z`jy(?(n~KjmbKwk-xW(%Sr7fPWw7b(Rl3yY-~RTuGOE0L?w45h&sJo1t>9=vc=p+6 zX{(}@H;e;$eW4~GUVyf&38yK&n6ip<0Rm*FvB^62*kc2@bPtoN$7!EBv&UmKNI>V(c$neZ`bj#=VdsjXf=WxWD`EI~iTxJ@wJLHZtw5 zyYBiGt{EiY0Cd!Mp_rDpS6_W~UO&hQ^szRTjhkLfS$+EHr+u<%BOaGFHGf!~Lz^xa zd&cJjUj_*{fEee`p_sP+!uFEj53em}!e>h_maO8eL0?XJ!?~-CjwtU`U09$3Ja8O0 z;W_u*b8o=A3=(hv`qSwsrsXa7)os9dQl_$T(~BXijQzG~(IUSI{C3mFATDicew?G} z3Eczya8`zm8&B-g4Zu!37swpe<*@XG@o5m3A^e{pn8={n3_` zx{1I2?QeO$|BT&cqao_MTW-0<#ukC~s<9rNOY~RNlR*Lw?7Z{NMp^yqU;k?D6P|m5 zDhu)_%c`}~gfAa^?6HFRDDrz^gUOR88+n3#mq`h_DJudF*kqMslMR0U^Pe00B(4k> zQ&wr)Ux>XmuT5cVPWozjdDW|XE*+9Hh|2++tnv&98~oxIzcBU*=!o@Bl~wLRB>mIT zOkb@+$tz>P+JN69Q~MDYl;?bAkbna=S>?RT28=<8Dhu)_%PPn7O*Yvi(O*qXjlg$S zz;8q#=5&C3{`S)BS$S>cyw?UiGtSGaUSaF?7o=s7fCIL*RajgH#x6sZ1^JU@wGehz zq>E=2z%MT#R*YYjmgcb7NE~-fb-)*h`s@r6Z~%KOQ&3FH+c4b>*DI#7anp+-tE7Q@ zz)O}a@#VD;zk_k(#`*YkH#YxB*b@u|T^S_c0CeS#pqN(XIBwd2zJR8(anp+>tE5A= z3#5-gT-wz9jCW`Q`VgpPyl3o1oQ?Yo5^&&(E3T+RF)eRTJMA=EmpH!jg_!@b{$k4N zC6`~kzY-%^YQ*Ys21}QjTJ6|o#-x}u#rt-G~*-qaULsoGnM_#I%80zkyG;J$Rm#orNicLjd-q%Yg3lMb9^z6A9(lO zcMqa}9~ik>8b%<_opDcq=RSwc(`@z{piBE3kkfHSWT-5}%|M+!_mI-YE~GEv^y$+> ztEVrP+$Q<{j5E%-8P8<}U|46y@dE$8LBoCCY_-)^ZzB#lzvYPl{wk-VppQ?z5A!kN zTn#ov#_}pY^_hAe`iL^tg#0IuxAc|b+}L}s+ShvTXjcp0oPwSA#xrNafO8nNP4Y9& z0ox1DWdq<>SF9V}M1zfdx#*&c5}jcS+*R=JH0rKs{}%=vdl;|OMqk`_^8BOJaji)! z&k_oouahtImoc>sHZ+{*mepB;(1#2&@k35WZ1PyXlAQA+*3uB1fwM@)Bj0mv5v9*) zizk;$d*9Zw%GiHlK-+4I{$F7HxE6dU8-S17_pl~6$`Tmi%LvATrHHe)Sr~KB2dmMq z5^;2v%H>V>;CWcL3EGA*?wJWk-1N4TRnBSHo-i0NV1Ti`*w1Z(Z+cI(yKDfu!F|!# z08g;C{Qz_J+bEmrJJ@NyU}FyW-wAQ3A6LuSo~geIo42xG8T09T-}_#GRvTX1yZZWi zUsSkOlE<&Qx;p2NdG6FeB$Fb-Gtei!RG2Q)K^vj`(E1T1o|I7YUhy& zu!%lSeaC0_tSV;7gFpZI&p$_E0AG}6`jE#L+<4=SoP!4Va_60Q4)mDd`bGrabN?Cq?&9T< zuZS1^sfv%!*em)BW6Br0cV1dly*=$;3pv(#^%wI>{S)BSY z5jj%j1?HMJAql(RVi=a>ya$VCy;=IztQ4MFR10?g-gA#zadB^v0q%Ly7GU@CIjvO_2*A9^N0+JeXrHMjXX82r&x{!n6>R=TGS0TVXFRsBI5Tp4ZSmFE4#c#|vr|F&PJ;#w z8iBf&>)MKQW8UT-58kB$#wo?wF%Nd{-hDn0`F#jmg%9O#ajHrzg>l~Ex@cpwTsLAT zzAqO6FS=mw?-B2Bw4CpuhaNgc&7;N#hpu#IjMK(_SC$WpcO~~N#%y8R*HP&y zi#~l1b$9X$#=PP87rMtU zZS}zC>&lpDV`qmhh;fnqw$kntHhJA;MEHiCBIn^fWbRw|G4q^t2XNNXuDJ2>@E-mu zx3|7s`M;|d4CIr`aScM=VVkL>#RXAQOS{%uvq?&O`97Xn?&%%2R+(SZ^O8R|uY0;QKUL&4(Ck z6e{QU>e{9U@L)aryo`tU$X^M)*nd=9@-uDlRKI3hztb+CHlB=)F5@ZnJ(pYe zz4qFU;A!5R2zD+fF)#SfIedV2&hb?|e4aUT=C)WHFH`f&&y0T^7^c(_Ox;9GUe~ptJnL`r|iSI0e-h%$@ys8f#=9z8&6?(H1RjT`OOfl z^;Q6(Pe1zm<;WEz$f!Gq3NfWd=}^IL>LHmn-gD1j{P^+v>&~-;-3aZhw1D5p8F=Bh zQ+Ll;k>I=By`lFU(-;ed-(eljLZ)$zkMrimlqpj}=~6TBtS#zt!|1{O%jB>;d`~|N z>{qXkduQB$tS*tuVV?9B-$en$f;}8_65TnvD{zJmox@fq9{kSc;NrKTXV0D=M8VHI zz)^v}SoREDUtzoqjPuZ;(v~F;zWnmbYJBGwBubMuf$39z%j+g6_MdiU-v z!hGjdIq;r2&M`(N^ZUweuunl-jU4PW2VniJ)lWmF zzJnjj-5|i3i!r~>j~KqP9A`oSv51bsH%}+00yZIje{zo(bCJMzcBh?onr^EAG**oo zHR@!1gT`R(ITv%4CHxkgefHTS(8oP&WxaQpKXUEw_a|icTXlWq<%Rq)Zi1iJbkA>4 z*ctcaFXp_|ky-KaOwuG{mnT49fBm%+zm)q>JU5fN`LH_W_smawTl$I7W{~?`NrAE3 zqkQ+d4}&_K@e8!%Xp6!4D)^nuqTU*I=regP```cmcdlszu*V*IOhQ7SjhILCY&0UW(?@uo8%u(0fky%6M1& z_Vak);c@p10(5%iyldE)>fz$J4*vhcD`&cLoy=J0Jo};$z_~Ez3fw264Aak>XBF~F z{SkDA&YtL;Idck8ofD(O7Druf_jcQD_nIuH9uAVyUS7m^;@*-EIDd0%eA60{V3if`J>?ZKqz3v3G&hLNcY~MTyJG%% zye6xk1ioJadYiob-j#DW{03viS7JN?wCN)chwaL^5A){DD~SA*3JL914m#+dNd2|L z4m-?Pym&Fs2TKI~`}cnm1p{?qkL4a4GOjn&wU~zkYp6Q4EPm6sdGEce+kb(qoSS&> zqh2vDXu{mBCV)tKz+Cz$71x<(p7}>9i}sc_s@!j7tTpJQ-^BjOtvF|VCvE1CMEEcD zLTr&nWDD?F_ske1VX?|eC5K`4z^@|GH=pg~_Z_~18{j?>a8DfLhdS4!-gA2sLd477 zLuXTm`~vgH8_~YN`iFbz*cZrKvtXWktXcrLJxw?>%&#FRHk_XV?`{ z_a7*O`Oq8Q`%+ywUq^oh_9j%ii7hvDR-t`q7V`VY|~Xo8z6j2f+E5$%Yd??K^TGOvhL3H{PkE1#`9Ds+_R==+UE% zYa+z^Jdo#|!G7ahuCsJ_p$mObr2}j3m(WLnG2JVR-}N*#zbE@%o>E;wV<+_YT`E1c zXYS?jyfW?wgvHA$w4Aa&j-&KLw9%-(<67dZv(DmJm>QrHxCTf9^}9NjJlwEZ+5vL9 zP|eS0`o@Ak6?uh_fbZC&Y8lAQ-*tK8q;YQuzcY32B1~s@Vr_q;N=F(W);P49@^bGR zYtuac1eH$pnQ>jX=H{BFn7}cVIs>eS0y3h~qkD$l=}YKA1AE!Xmk3}3)m0aZ!L`E- z;N*G&_C9lwm`5|lhdTn9r3amUfjBtdm9izig$?7w#3%nDZrHmzgsx3QATNHa!*zK} zna1@3Xb#nB$!+xRv1SlE?zp31oPeoQr#gD0G_p&(1K8*|dM)Z{sYmnbigU$S^hzQ9 zrC`rHSgjwndfcc8jF03ce?3UJE)|!rYU3+=2k@7CD~iV@w}aUyA+BS*a>dS(r4^E|zn1 z$QbvU{WT@-^LXCg)2L5^{&O7T(#@SacfFMCaf)*-ueKXw>Ibq;f%-ti&i+Kr$7kwe zRatNlOaEcd?m4wA{12#)DkyNDlXm<-8t7ZtN1ditM7@ce4`Xt3Tdc3ZyUvKkd$Jp^ zDL}VOUkAT$!CGZ3b<}D<#CgU(&mff#uvPH=F}OF2Jzeem6!kv>UTfFd9u2zR0^Y6Q zddSOD$SY|7J@5tk9OKk~)jALdC2#EwTh;+;zG6Oe9=FRbyS$>(yQ)58tX-DW4x0UN z(@i(!*>vKptaJE|>mk@aeTdsY-}4!I*QX$-otV!CnD4)e_kr@R�|f;&=3w!Tcg` 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 Date: Sun, 31 Jan 2021 15:27:00 +0100 Subject: [PATCH 15/50] Add test for UI methods --- freqtrade/commands/deploy_commands.py | 2 +- tests/rpc/test_rpc_apiserver.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 62da6abf3..797e9f87d 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('.gitkeep', 'fallback_file.html'): + if p.name in ('favicon.ico', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 919481598..586af6dc1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -88,6 +88,21 @@ def test_api_not_found(botclient): assert rc.json() == {"detail": "Not Found"} +def test_api_ui_fallback(botclient): + ftbot, client = botclient + + rc = client_get(client, "/favicon.ico") + assert rc.status_code == 200 + + rc = client_get(client, "/fallback_file.html") + assert rc.status_code == 200 + assert '`freqtrade install-ui`' in rc.text + + # Forwarded to fallback_html or index.html (depending if it's installed or not) + rc = client_get(client, "/something") + assert rc.status_code == 200 + + def test_api_auth(): with pytest.raises(ValueError): create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") From 06e2bc94c3e111bcea180485018fd081da176104 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:37:57 +0100 Subject: [PATCH 16/50] Deploy to subdirectory --- freqtrade/commands/deploy_commands.py | 5 +++-- freqtrade/rpc/api_server/ui/installed/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 4 ++-- tests/commands/test_commands.py | 3 +++ 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/installed/.gitkeep diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 797e9f87d..5ba3db9f9 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('favicon.ico', 'fallback_file.html'): + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() @@ -169,6 +169,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): logger.info(f"Downloading {dl_url}") resp = requests.get(dl_url).content + dest_folder.mkdir(parents=True, exist_ok=True) with ZipFile(BytesIO(resp)) as zf: for fn in zf.filelist: with zf.open(fn) as x: @@ -207,7 +208,7 @@ def get_ui_download_url() -> Tuple[str, str]: def start_install_ui(args: Dict[str, Any]) -> None: - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() diff --git a/freqtrade/rpc/api_server/ui/installed/.gitkeep b/freqtrade/rpc/api_server/ui/installed/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 4876c9077..6d7e77953 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -20,12 +20,12 @@ async def index_html(rest_of_path: str): """ if rest_of_path.startswith('api') or rest_of_path.startswith('.'): raise HTTPException(status_code=404, detail="Not Found") - uibase = Path(__file__).parent / 'ui' + uibase = Path(__file__).parent / 'ui/installed/' if (uibase / rest_of_path).is_file(): return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(str(uibase / 'fallback_file.html')) + return FileResponse(str(uibase.parent / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs return FileResponse(str(index_file)) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 0d2b9e394..cec0b168e 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -622,6 +622,9 @@ def test_download_and_install_ui(mocker, tmpdir): folder = Path(tmpdir) / "uitests_dl" folder.mkdir(exist_ok=True) + + assert read_ui_version(folder) is None + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') assert wb_mock.call_count == 2 From f36c61e32fe09c4e7dda2fcb848fb6fe11de2265 Mon Sep 17 00:00:00 2001 From: Alberto del Barrio Date: Wed, 3 Feb 2021 18:11:49 +0100 Subject: [PATCH 17/50] Fix documentation links pointing to pairlists --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c6c40319d..25ae1dd31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -82,8 +82,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List -| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict From b8cb39462c72b049120a91795d8b0b7797e748a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Feb 2021 20:00:33 +0100 Subject: [PATCH 18/50] Move get_trade_stake_amount to wallets this way it can be easier used by other functions --- freqtrade/freqtradebot.py | 82 ++------------------------------------ freqtrade/rpc/rpc.py | 3 +- freqtrade/wallets.py | 78 ++++++++++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 28 ++++++++----- tests/test_integration.py | 6 ++- 5 files changed, 104 insertions(+), 93 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2656daab5..fde85e94a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -233,7 +233,7 @@ class FreqtradeBot(LoggingMixin): _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) return _whitelist - def get_free_open_trades(self): + def get_free_open_trades(self) -> int: """ Return the number of free open trades slots or 0 if max number of open trades reached @@ -439,83 +439,6 @@ class FreqtradeBot(LoggingMixin): return used_rate - def get_trade_stake_amount(self, pair: str) -> float: - """ - Calculate stake amount for the trade - :return: float: Stake amount - :raise: DependencyException if the available stake amount is too low - """ - stake_amount: float - # Ensure wallets are uptodate. - self.wallets.update() - - if self.edge: - stake_amount = self.edge.stake_amount( - pair, - self.wallets.get_free(self.config['stake_currency']), - self.wallets.get_total(self.config['stake_currency']), - Trade.total_open_trades_stakes() - ) - else: - stake_amount = self.config['stake_amount'] - if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount() - - return self._check_available_stake_amount(stake_amount) - - def _get_available_stake_amount(self) -> float: - """ - Return the total currently available balance in stake currency, - respecting tradable_balance_ratio. - Calculated as - + free amount ) * tradable_balance_ratio - - """ - val_tied_up = Trade.total_open_trades_stakes() - - # Ensure % is used from the overall balance - # Otherwise we'd risk lowering stakes with each open trade. - # (tied up + current free) * ratio) - tied up - available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * - self.config['tradable_balance_ratio']) - val_tied_up - return available_amount - - def _calculate_unlimited_stake_amount(self) -> float: - """ - Calculate stake amount for "unlimited" stake amount - :return: 0 if max number of trades reached, else stake_amount to use. - """ - free_open_trades = self.get_free_open_trades() - if not free_open_trades: - return 0 - - available_amount = self._get_available_stake_amount() - - return available_amount / free_open_trades - - def _check_available_stake_amount(self, stake_amount: float) -> float: - """ - Check if stake amount can be fulfilled with the available balance - for the stake currency - :return: float: Stake amount - """ - available_amount = self._get_available_stake_amount() - - if self.config['amend_last_stake_amount']: - # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio - # Otherwise the remaining amount is too low to trade. - if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']): - stake_amount = min(stake_amount, available_amount) - else: - stake_amount = 0 - - if available_amount < stake_amount: - raise DependencyException( - f"Available balance ({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" - ) - - return stake_amount - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -549,7 +472,8 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.get_trade_stake_amount(pair) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), + self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 491d1cde6..379fdcefa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -590,7 +590,8 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.get_trade_stake_amount(pair) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount( + pair, self._freqtrade.get_free_open_trades()) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 3680dd416..d7dcfd487 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple import arrow +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -118,3 +120,79 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets + + def _get_available_stake_amount(self) -> float: + """ + Return the total currently available balance in stake currency, + respecting tradable_balance_ratio. + Calculated as + ( + free amount ) * tradable_balance_ratio - + """ + val_tied_up = Trade.total_open_trades_stakes() + + # Ensure % is used from the overall balance + # Otherwise we'd risk lowering stakes with each open trade. + # (tied up + current free) * ratio) - tied up + available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * + self._config['tradable_balance_ratio']) - val_tied_up + return available_amount + + def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + """ + Calculate stake amount for "unlimited" stake amount + :return: 0 if max number of trades reached, else stake_amount to use. + """ + if not free_open_trades: + return 0 + + available_amount = self._get_available_stake_amount() + + return available_amount / free_open_trades + + def _check_available_stake_amount(self, stake_amount: float) -> float: + """ + Check if stake amount can be fulfilled with the available balance + for the stake currency + :return: float: Stake amount + """ + available_amount = self._get_available_stake_amount() + + if self._config['amend_last_stake_amount']: + # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio + # Otherwise the remaining amount is too low to trade. + if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']): + stake_amount = min(stake_amount, available_amount) + else: + stake_amount = 0 + + if available_amount < stake_amount: + raise DependencyException( + f"Available balance ({available_amount} {self._config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self._config['stake_currency']})" + ) + + return stake_amount + + def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + """ + Calculate stake amount for the trade + :return: float: Stake amount + :raise: DependencyException if the available stake amount is too low + """ + stake_amount: float + # Ensure wallets are uptodate. + self.update() + + if edge: + stake_amount = edge.stake_amount( + pair, + self.get_free(self._config['stake_currency']), + self.get_total(self._config['stake_currency']), + Trade.total_open_trades_stakes() + ) + else: + stake_amount = self._config['stake_amount'] + if stake_amount == UNLIMITED_STAKE_AMOUNT: + stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + + return self._check_available_stake_amount(stake_amount) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index abb91d66b..6cb126ae1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -158,7 +158,8 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount( + 'ETH/BTC', freqtrade.get_free_open_trades()) assert result == default_conf['stake_amount'] @@ -194,12 +195,14 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @@ -210,7 +213,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) @pytest.mark.parametrize("balance_ratio,result1", [ @@ -239,25 +242,25 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/BTC', result) - result = freqtrade.get_trade_stake_amount('LTC/BTC') + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.get_trade_stake_amount('XRP/BTC') + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 freqtrade = FreqtradeBot(conf) - result = freqtrade.get_trade_stake_amount('NEO/BTC') + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) assert result == 0 @@ -283,8 +286,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) - assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 - assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 + assert freqtrade.wallets.get_trade_stake_amount( + 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + assert freqtrade.wallets.get_trade_stake_amount( + 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -500,7 +505,8 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), + freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 9695977ac..8e3bd251a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -178,7 +178,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,7 +200,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.get_trade_stake_amount('XRP/BTC') < result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 From e8e5acc2e2ffcde04b75e27cfc4903e3583d2eff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Feb 2021 20:15:08 +0100 Subject: [PATCH 19/50] Fix import in strategy template --- freqtrade/templates/base_strategy.py.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 4a1b43e36..dd6b773e1 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -5,7 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- # Add your lib to import here From 99b2214d1f85b4032d1caf2b5b273c1b3e4c52a6 Mon Sep 17 00:00:00 2001 From: raoulus Date: Thu, 4 Feb 2021 15:27:18 +0100 Subject: [PATCH 20/50] setting resize_keyboard=True for slightly smaller Telegram buttons --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 99f9a8a91..0f7005639 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -910,7 +910,7 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard) + reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( From 5cd8745997ed2300fc750408d4655ca1345963f4 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 16:26:03 +0100 Subject: [PATCH 21/50] Update README.md Typo playing -> paying --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51a3409ea..bb136d7f2 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io - [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Persistence**: Persistence is achieved through sqlite. -- [x] **Dry-run**: Run the bot without playing money. +- [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). From 5165357f40c780358271f7ae6d31aa14f93e7feb Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 19:36:04 +0100 Subject: [PATCH 22/50] Update data-download.md Fix wrong path Add section about fixing wrong docker permission, if user_data is created by docker, it's permission are set to `root` --- docs/data-download.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 4c7376933..cad39ac41 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -264,7 +264,19 @@ If you are using Binance for example: ```bash mkdir -p user_data/data/binance -cp freqtrade/tests/testdata/pairs.json user_data/data/binance +cp tests/testdata/pairs.json user_data/data/binance +``` + +if you your configuration directory `user_data` was made by docker, you may get an error: + +``` +cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied +``` + +so you will need to fix your permission by doing: + +``` +chown -R $UID:$GID user_data ``` The format of the `pairs.json` file is a simple json list. From 428d2af312fea93e2e01da0feed6d808269c2723 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 19:39:25 +0100 Subject: [PATCH 23/50] add `sudo` to `chown` that was the whole point d'oh --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index cad39ac41..1e183d04b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -276,7 +276,7 @@ cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission d so you will need to fix your permission by doing: ``` -chown -R $UID:$GID user_data +sudo chown -R $UID:$GID user_data ``` The format of the `pairs.json` file is a simple json list. From 1310a7b5473a53be10784451473851de34b434a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Feb 2021 19:58:44 +0100 Subject: [PATCH 24/50] Fix bug with wrong conversion for BTCST/BTC This can happen if a pair starts with the stake-currency closes #4307 --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 491d1cde6..b17183be2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -451,7 +451,7 @@ class RPC: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) rate = tickers.get(pair, {}).get('bid', None) if rate: - if pair.startswith(stake_currency): + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total except (ExchangeError): From a816fb12454cd62a306d80f493af8abcf89cb699 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Fri, 5 Feb 2021 12:43:19 +0100 Subject: [PATCH 25/50] chore(lint): lint binance example config --- config_binance.json.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config_binance.json.example b/config_binance.json.example index 83c9748d7..4fa615d6d 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -12,15 +12,15 @@ "sell": 30 }, "bid_strategy": { - "use_order_book": false, "ask_last_balance": 0.0, + "use_order_book": false, "order_book_top": 1, "check_depth_of_market": { "enabled": false, "bids_to_ask_delta": 1 } }, - "ask_strategy":{ + "ask_strategy": { "use_order_book": false, "order_book_min": 1, "order_book_max": 1, From 0806202d47cd63674a64e6069afe08c1e9cfdaa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:02:55 +0100 Subject: [PATCH 26/50] ccxt version bump to 1.41.62 --- requirements.txt | 2 +- tests/exchange/test_ccxt_compat.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5114f397f..299c07734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.0 pandas==1.2.1 -ccxt==1.41.35 +ccxt==1.41.62 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 0c8b7bdcf..8e1d074aa 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -18,7 +18,7 @@ EXCHANGES = { 'bittrex': { 'pair': 'BTC/USDT', 'hasQuoteVolume': False, - 'timeframe': '5m', + 'timeframe': '1h', }, 'binance': { 'pair': 'BTC/USDT', @@ -120,7 +120,9 @@ class TestCCXTExchange(): ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) assert isinstance(ohlcv, dict) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) - assert len(exchange.klines(pair_tf)) > 200 + # assert len(exchange.klines(pair_tf)) > 200 + # Assume 90% uptime ... + assert len(exchange.klines(pair_tf)) > exchange._ohlcv_candle_limit * 0.90 # TODO: tests fetch_trades (?) From 86a97988c011de79be5e74f3ec408c31b11f7c37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:09:13 +0100 Subject: [PATCH 27/50] Improve wording --- docs/data-download.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 1e183d04b..04f444a8b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -267,13 +267,13 @@ mkdir -p user_data/data/binance cp tests/testdata/pairs.json user_data/data/binance ``` -if you your configuration directory `user_data` was made by docker, you may get an error: +If you your configuration directory `user_data` was made by docker, you may get the following error: ``` cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied ``` -so you will need to fix your permission by doing: +You can fix the permissions of your user-data directory as follows: ``` sudo chown -R $UID:$GID user_data From aec22c5c3d53b6a21f677c4e62427df78e073c76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:17:38 +0100 Subject: [PATCH 28/50] introduce skip_open_order_update parameter skips startup-open-order-update closes #4128 --- docs/configuration.md | 1 + freqtrade/freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 25ae1dd31..00d2830e4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -89,6 +89,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fde85e94a..71b6c7dc0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -246,7 +246,7 @@ class FreqtradeBot(LoggingMixin): Updates open orders based on order list kept in the database. Mainly updates the state of orders - but may also close trades """ - if self.config['dry_run']: + if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): # Updating open orders in dry-run does not make sense and will fail. return From 0a43988f3f9c61e82680f4166a04047acac3b626 Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 03:02:37 +0000 Subject: [PATCH 29/50] Fix sample strategy documentation link Noticed that the current link is dead. I think this would be the most appropriate link in this case. --- freqtrade/templates/sample_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b3f9fef07..b1a1bb0ea 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,7 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md You can: :return: a Dataframe with all mandatory indicators for the strategies From bc8fda8d6345086fa7bb827fc894fcfad61b20ef Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 03:13:53 +0000 Subject: [PATCH 30/50] Update sample_strategy.py Fix test --- freqtrade/templates/sample_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b1a1bb0ea..6dd5a01ce 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,7 +17,8 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md + More information in: + https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md You can: :return: a Dataframe with all mandatory indicators for the strategies From d5cf837c0f5a6e0c2e0950d49721249587e27a82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 09:23:10 +0100 Subject: [PATCH 31/50] Parse regular cancel_order call to update orders table --- freqtrade/freqtradebot.py | 5 ++++- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71b6c7dc0..a6eb75d5b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1070,7 +1070,9 @@ class FreqtradeBot(LoggingMixin): if not self.exchange.check_order_canceled_empty(order): try: # if trade is not partially completed, just delete the order - self.exchange.cancel_order(trade.open_order_id, trade.pair) + co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") return 'error cancelling order' @@ -1078,6 +1080,7 @@ class FreqtradeBot(LoggingMixin): else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info('Sell order %s for %s.', reason, trade) + trade.update_order(order) trade.close_rate = None trade.close_rate_requested = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6cb126ae1..3bd2f5607 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2609,7 +2609,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + 'freqtrade.exchange.Exchange.cancel_order_with_result', side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf) From 694f55c0a53174d430ed5f7e2e8b20ab7c3b3e74 Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 14:43:50 +0000 Subject: [PATCH 32/50] Use suggested link --- freqtrade/templates/sample_strategy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 6dd5a01ce..db1ba48b8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,8 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in: - https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies From d1bb46bed015c7d6364c2aab0a84aba9c5007527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:42 +0000 Subject: [PATCH 33/50] Bump py-find-1st from 1.1.4 to 1.1.5 Bumps [py-find-1st](https://github.com/roebel/py_find_1st) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/roebel/py_find_1st/releases) - [Commits](https://github.com/roebel/py_find_1st/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..2448afe93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ tables==3.6.1 blosc==1.10.2 # find first, C search in arrays -py_find_1st==1.1.4 +py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.0 From dd7f9181c53b91832b98c549dea2deafdbff350a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:44 +0000 Subject: [PATCH 34/50] Bump numpy from 1.20.0 to 1.20.1 Bumps [numpy](https://github.com/numpy/numpy) from 1.20.0 to 1.20.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.20.0...v1.20.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..bdaca4a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.20.0 +numpy==1.20.1 pandas==1.2.1 ccxt==1.41.62 From 676cd7bb55d85d8829c1adefce83871e98351896 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:45 +0000 Subject: [PATCH 35/50] Bump sqlalchemy from 1.3.22 to 1.3.23 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.22 to 1.3.23. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..c9559e384 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.1 ccxt==1.41.62 aiohttp==3.7.3 -SQLAlchemy==1.3.22 +SQLAlchemy==1.3.23 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.1 From 22d447b3f51a1f21632e329e991ebcbdc7f17918 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:46 +0000 Subject: [PATCH 36/50] Bump mkdocs-material from 6.2.7 to 6.2.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.7 to 6.2.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.7...6.2.8) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 85bd72323..94b2fca39 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.7 +mkdocs-material==6.2.8 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 12168cbf01edaa6c11ea5e427a52180a34f549ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 08:26:18 +0000 Subject: [PATCH 37/50] Bump ccxt from 1.41.62 to 1.41.70 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.41.62 to 1.41.70. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.41.62...1.41.70) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a9e6e5da..f496df00d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.1 -ccxt==1.41.62 +ccxt==1.41.70 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.1 From c412f8df62265131e4a302331ea1de2e5d922d2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 08:28:11 +0000 Subject: [PATCH 38/50] Bump python-telegram-bot from 13.1 to 13.2 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.1 to 13.2. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.1...v13.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a9e6e5da..c1cc036a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.2.1 ccxt==1.41.62 aiohttp==3.7.3 SQLAlchemy==1.3.23 -python-telegram-bot==13.1 +python-telegram-bot==13.2 arrow==0.17.0 cachetools==4.2.1 requests==2.25.1 From de727645ab7774abdc54c531370baa051aeb3635 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:21:33 +0100 Subject: [PATCH 39/50] FIx random test failure if certain files exist --- tests/test_configuration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index bebbc1508..94c3e24f6 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -743,18 +743,18 @@ def test_set_loggers_journald_importerror(mocker, import_fails): logger.handlers = orig_handlers -def test_set_logfile(default_conf, mocker): +def test_set_logfile(default_conf, mocker, tmpdir): patched_configuration_load_config_file(mocker, default_conf) - + f = Path(tmpdir / "test_file.log") + assert not f.is_file() arglist = [ - 'trade', '--logfile', 'test_file.log', + 'trade', '--logfile', str(f), ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf['logfile'] == "test_file.log" - f = Path("test_file.log") + assert validated_conf['logfile'] == str(f) assert f.is_file() try: f.unlink() From c5ab3a80a596769786b12821cafec4ea08108b6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:35:22 +0100 Subject: [PATCH 40/50] Check if order is a dict before parsing closes #4331 --- freqtrade/persistence/models.py | 4 ++++ tests/test_persistence.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 375709423..dff59819c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -171,6 +171,10 @@ class Order(_DECL_BASE): """ Get all non-closed orders - useful when trying to batch-update orders """ + if not isinstance(order, dict): + logger.warning(f"{order} is not a valid response object.") + return + filtered_orders = [o for o in orders if o.order_id == order.get('id')] if filtered_orders: oobj = filtered_orders[0] diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 9921f541b..d0d29f142 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1074,7 +1074,7 @@ def test_get_best_pair(fee): @pytest.mark.usefixtures("init_persistence") -def test_update_order_from_ccxt(): +def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') assert isinstance(o, Order) @@ -1120,6 +1120,14 @@ def test_update_order_from_ccxt(): with pytest.raises(DependencyException, match=r"Order-id's don't match"): o.update_from_ccxt_object(ccxt_order) + message = "aaaa is not a valid response object." + assert not log_has(message, caplog) + Order.update_orders([o], 'aaaa') + assert log_has(message, caplog) + + # Call regular update - shouldn't fail. + Order.update_orders([o], {'id': '1234'}) + @pytest.mark.usefixtures("init_persistence") def test_select_order(fee): From 427d762746980a3a50702b6ea8de1d28325e26c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:37:24 +0100 Subject: [PATCH 41/50] Improve tests for cancel_order to be more realistic --- tests/exchange/test_exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cd24e113e..352250fc7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2072,9 +2072,9 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap def test_cancel_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) @@ -2091,9 +2091,9 @@ def test_cancel_order(default_conf, mocker, exchange_name): def test_cancel_stoploss_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) From 7ee149da5d26c7889e4d561bc2154111226dc84c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 20:08:32 +0100 Subject: [PATCH 42/50] Improve plotting errorhandling closes #4327 --- freqtrade/plot/plotting.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f45ba9b25..4325e537e 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -53,7 +53,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): data_format=config.get('dataformat_ohlcv', 'json'), ) - if startup_candles: + if startup_candles and data: min_date, max_date = get_timerange(data) logger.info(f"Loading data from {min_date} to {max_date}") timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), @@ -67,14 +67,16 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): if not filename.is_dir() and not filename.is_file(): logger.warning("Backtest file is missing skipping trades.") no_trades = True - - trades = load_trades( - config['trade_source'], - db_url=config.get('db_url'), - exportfilename=filename, - no_trades=no_trades, - strategy=config.get('strategy'), - ) + try: + trades = load_trades( + config['trade_source'], + db_url=config.get('db_url'), + exportfilename=filename, + no_trades=no_trades, + strategy=config.get('strategy'), + ) + except ValueError as e: + raise OperationalException(e) from e trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, From 86fa75b2863eac67edffd20c1fdb7cc853d2d08e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 06:55:36 +0100 Subject: [PATCH 43/50] Pin version of cryptography --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 59ef69f4f..5d03a9c2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ numpy==1.20.1 pandas==1.2.1 ccxt==1.41.70 +# Pin cryptography for now due to rust build errors with piwheels +cryptography==3.3.2 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.2 From 3110d2dbb192efe7944b09f68a10410daad39fea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 20:03:03 +0100 Subject: [PATCH 44/50] Add small test cases --- tests/exchange/test_exchange.py | 3 +++ tests/optimize/test_backtesting.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 352250fc7..f35a84725 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2462,6 +2462,9 @@ def test_timeframe_to_prev_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_prev_date("5m") < date + # Does not round + time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + assert timeframe_to_prev_date('5m', time) == time def test_timeframe_to_next_date(): diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5f811e2e5..c8d4338af 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -341,12 +341,14 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') + sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'trades' + default_conf['exportfilename'] = 'export.txt' default_conf['timerange'] = '-1510694220' backtesting = Backtesting(default_conf) @@ -361,6 +363,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.bot_loop_start.call_count == 1 + assert sbs.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From aa79574c0c60ba68f8b9ee8e5649d8a785010f5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 17:09:31 +0100 Subject: [PATCH 45/50] Position-size should NEVER be over available_capital Part of #4353 --- freqtrade/edge/edge_positioning.py | 3 ++- tests/edge/test_edge.py | 39 ++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index e549a3701..2bdef1c89 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -159,7 +159,8 @@ class Edge: available_capital = (total_capital + capital_in_trade) * self._capital_ratio allowed_capital_at_risk = available_capital * self._allowed_risk max_position_size = abs(allowed_capital_at_risk / stoploss) - position_size = min(max_position_size, free_capital) + # Position size must be below available capital. + position_size = min(min(max_position_size, free_capital), available_capital) if pair in self._cached_pairs: logger.info( 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index f25dad35b..c30bce6a4 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -209,7 +209,7 @@ def test_nonexisting_stoploss(mocker, edge_conf): assert edge.stoploss('N/O') == -0.1 -def test_stake_amount(mocker, edge_conf): +def test_edge_stake_amount(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( @@ -217,20 +217,33 @@ def test_stake_amount(mocker, edge_conf): 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - free = 100 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 31.25 + assert edge._capital_ratio == 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 31.25 - free = 20 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 20 + assert edge.stake_amount('E/F', free_capital=20, total_capital=100, + capital_in_trade=25) == 20 - free = 0 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 0 + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=25) == 0 + + # Test with increased allowed_risk + # Result should be no more than allowed capital + edge._allowed_risk = 0.4 + edge._capital_ratio = 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 62.5 + + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 50 + + edge._capital_ratio = 1 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 100 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=0) == 0 def test_nonexisting_stake_amount(mocker, edge_conf): From 843fb204e9aa680b53aa16061815519d74456ca8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 20:21:07 +0100 Subject: [PATCH 46/50] Fix problem with inf values returned from dataframe for api methods --- freqtrade/rpc/api_server/api_v1.py | 2 +- freqtrade/rpc/rpc.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index a2082103b..3588f2196 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -167,7 +167,7 @@ def reload_config(rpc: RPC = Depends(get_rpc)): @router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) -def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): +def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Depends(get_rpc)): return rpc._rpc_analysed_dataframe(pair, timeframe, limit) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 27563f73b..464b341eb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,7 +9,7 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow -from numpy import NAN, int64, mean +from numpy import NAN, inf, int64, mean from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange @@ -747,6 +747,7 @@ class RPC: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) res = { From dd23f6bcbcd569a0857ac0dc1920b24c2065f05e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 20:29:31 +0100 Subject: [PATCH 47/50] Fix type for getting pairs --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 464b341eb..7549c38be 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -776,7 +776,8 @@ class RPC: }) return res - def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: + def _rpc_analysed_dataframe(self, pair: str, timeframe: str, + limit: Optional[int]) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( pair, timeframe) From 072abde9b71b34022e3a5f5c867847ec12a211fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Feb 2021 20:32:41 +0100 Subject: [PATCH 48/50] Introduce round_coin_value to simplify coin rounding --- freqtrade/constants.py | 10 +++++++++ freqtrade/misc.py | 25 +++++++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 11 +++++----- tests/test_misc.py | 28 +++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 69301ca0e..802ddc2b1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -45,6 +45,16 @@ USERPATH_NOTEBOOKS = 'notebooks' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] + +# Define decimals per coin for outputs +# Only used for outputs. +DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's +DECIMALS_PER_COIN = { + 'BTC': 8, + 'ETH': 5, +} + + # Soure files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 22e14b564..7bbc24056 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -11,10 +11,35 @@ from typing.io import IO import rapidjson +from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN + logger = logging.getLogger(__name__) +def decimals_per_coin(coin: str): + """ + Helper method getting decimal amount for this coin + example usage: f".{decimals_per_coin('USD')}f" + :param coin: Which coin are we printing the price / value for + """ + return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) + + +def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: + """ + Get price value for this coin + :param value: Value to be printed + :param coin: Which coin are we printing the price / value for + :param show_coin_name: Return string in format: "222.22 USDT" or "222.22" + :return: Formatted / rounded value (with or without coin name) + """ + if show_coin_name: + return f"{value:.{decimals_per_coin(coin)}f} {coin}" + else: + return f"{value:.{decimals_per_coin(coin)}f}" + + def shorten_date(_date: str) -> str: """ Trim the date so it fits on small screens diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8edfbaf8d..c70a4cd2d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown -from freqtrade.misc import file_dump_json +from freqtrade.misc import file_dump_json, round_coin_value, decimals_per_coin logger = logging.getLogger(__name__) @@ -38,11 +38,12 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def _get_line_floatfmt() -> List[str]: +def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) """ - return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] + return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', + '.2f', 'd', 'd', 'd', 'd'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -352,7 +353,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st """ headers = _get_line_header('Pair', stake_currency) - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] @@ -396,7 +397,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) output = [[ diff --git a/tests/test_misc.py b/tests/test_misc.py index 429da135a..e6ba70aee 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,9 +6,31 @@ from unittest.mock import MagicMock import pytest -from freqtrade.misc import (file_dump_json, file_load_json, format_ms_time, pair_to_filename, - plural, render_template, render_template_with_fallback, - safe_value_fallback, safe_value_fallback2, shorten_date) +from freqtrade.misc import (decimals_per_coin, file_dump_json, file_load_json, format_ms_time, + pair_to_filename, plural, render_template, + render_template_with_fallback, round_coin_value, safe_value_fallback, + safe_value_fallback2, shorten_date) + + +def test_decimals_per_coin(): + assert decimals_per_coin('USDT') == 3 + assert decimals_per_coin('EUR') == 3 + assert decimals_per_coin('BTC') == 8 + assert decimals_per_coin('ETH') == 5 + + +def test_round_coin_value(): + assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' + assert round_coin_value(222.2, 'USDT') == '222.200 USDT' + assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' + assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' + assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' + + assert round_coin_value(222.222222, 'USDT', False) == '222.222' + assert round_coin_value(222.2, 'USDT', False) == '222.200' + assert round_coin_value(222.12745, 'EUR', False) == '222.127' + assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' + assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' def test_shorten_date() -> None: From e7acee79045bfb5aa8cf485d5aa151d4c33fbf30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 16:05:56 +0100 Subject: [PATCH 49/50] Improve coin value output by rounding coin specific --- freqtrade/optimize/optimize_reports.py | 6 ++-- freqtrade/rpc/telegram.py | 46 ++++++++++++++------------ tests/rpc/test_rpc_telegram.py | 6 ++-- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c70a4cd2d..118253e86 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown -from freqtrade.misc import file_dump_json, round_coin_value, decimals_per_coin +from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value logger = logging.getLogger(__name__) @@ -384,7 +384,9 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren output = [[ t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], - t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], + t['profit_mean_pct'], t['profit_sum_pct'], + round_coin_value(t['profit_total_abs'], stake_currency, False), + t['profit_total_pct'], ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0f7005639..a16299e4b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -18,6 +18,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -189,14 +190,14 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{limit:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") if msg.get('fiat_currency', None): - message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: @@ -365,7 +366,7 @@ class Telegram(RPCHandler): ) stats_tab = tabulate( [[day['date'], - f"{day['abs_profit']:.8f} {stats['stake_currency']}", + f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ @@ -415,18 +416,18 @@ class Telegram(RPCHandler): # Message to display if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" - f"∙ `{profit_closed_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} " f"({profit_closed_percent_mean:.2f}%) " f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") + f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n") else: markdown_msg = "`No closed trade` \n" markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{profit_all_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " f"({profit_all_percent_mean:.2f}%) " f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" + f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}\n`" @@ -494,15 +495,17 @@ class Telegram(RPCHandler): "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) - for currency in result['currencies']: - if currency['est_stake'] > 0.0001: - curr_output = ("*{currency}:*\n" - "\t`Available: {free: .8f}`\n" - "\t`Balance: {balance: .8f}`\n" - "\t`Pending: {used: .8f}`\n" - "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) + for curr in result['currencies']: + if curr['est_stake'] > 0.0001: + curr_output = ( + f"*{curr['currency']}:*\n" + f"\t`Available: {curr['free']:.8f}`\n" + f"\t`Balance: {curr['balance']:.8f}`\n" + f"\t`Pending: {curr['used']:.8f}`\n" + f"\t`Est. {curr['stake']}: " + f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) + curr_output = "*{currency}:* not showing <1$ amount \n".format(**curr) # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -512,8 +515,9 @@ class Telegram(RPCHandler): output += curr_output output += ("\n*Estimated Value*:\n" - "\t`{stake}: {total: .8f}`\n" - "\t`{symbol}: {value: .2f}`\n").format(**result) + f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['symbol']}: " + f"{round_coin_value(result['value'], result['symbol'], False)}`\n") self._send_msg(output) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1c34b6b26..f065bb4c5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -519,7 +519,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*EUR:*' in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12.00000000' in result + assert 'BTC: 12.00000000' in result assert '*XRP:* not showing <1$ amount' in result @@ -1205,7 +1205,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.001000 BTC, 12.345 USD)`' + '*Total:* `(0.00100000 BTC, 12.345 USD)`' freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1389,7 +1389,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.001000 BTC)`') + '*Total:* `(0.00100000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From d4c8be915cc66e6995d383f27cdd2733e7343bca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 16:11:49 +0100 Subject: [PATCH 50/50] Use fstring where possible --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a16299e4b..88019601c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -505,7 +505,7 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**curr) + curr_output = f"*{curr['currency']}:* not showing <1$ amount \n" # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: