From d1eb6d4fed2a6a8e5d01c457f6ea529e7ef29208 Mon Sep 17 00:00:00 2001 From: vinistation <60822570+vinistation@users.noreply.github.com> Date: Fri, 28 Apr 2023 14:48:16 -0500 Subject: [PATCH 01/23] Update BasePyTorchRegressor.py Denormalization of prediction added to te PytorchMLP Model --- freqtrade/freqai/base_models/BasePyTorchRegressor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index ea6fabe49..bc47731b7 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -47,4 +47,5 @@ class BasePyTorchRegressor(BasePyTorchModel): y = self.model.model(x) y = y.cpu() pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) + pred_df = dk.denormalize_labels_from_metadata(pred_df) return (pred_df, dk.do_predict) From a66e8768c96560fd0b7ca52f82af6ee20ea19174 Mon Sep 17 00:00:00 2001 From: vinistation <60822570+vinistation@users.noreply.github.com> Date: Fri, 28 Apr 2023 15:21:56 -0500 Subject: [PATCH 02/23] Update docker-compose.yml Enable GPU Image and GPU Resources --- docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 445fbaea0..b8f48577a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,3 +26,12 @@ services: --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json --strategy SampleStrategy + # Enable GPU Image and GPU Resources + #image: freqtradeorg/freqtrade:develop_freqaitorch + #deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] From fffb056ad34aab6707cf2196146a8ee96df1a745 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 May 2023 13:43:53 +0200 Subject: [PATCH 03/23] load_exchange - force kwargs for non-required arguments --- freqtrade/resolvers/exchange_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index e888028dc..448fdeb8b 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -19,7 +19,7 @@ class ExchangeResolver(IResolver): object_type = Exchange @staticmethod - def load_exchange(config: Config, validate: bool = True, + def load_exchange(config: Config, *, validate: bool = True, load_leverage_tiers: bool = False) -> Exchange: """ Load the custom class from config parameter From fe36e774129a8c62ec297172162cd03de1d15c70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 May 2023 15:33:13 +0200 Subject: [PATCH 04/23] Split exchange_config before passing through the strategy --- freqtrade/exchange/exchange.py | 26 ++++++++++++------------ freqtrade/freqtradebot.py | 3 ++- freqtrade/resolvers/exchange_resolver.py | 8 +++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bb9c7c1b3..a70527a7f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -92,8 +92,8 @@ class Exchange: # TradingMode.SPOT always supported and not required in this list ] - def __init__(self, config: Config, *, validate: bool = True, - load_leverage_tiers: bool = False) -> None: + def __init__(self, config: Config, *, exchange_config: Optional[Config] = None, + validate: bool = True, load_leverage_tiers: bool = False) -> None: """ Initializes this module with the given config, it does basic validation whether the specified exchange and pairs are valid. @@ -136,8 +136,8 @@ class Exchange: if config['dry_run']: logger.info('Instance is running with dry_run enabled') logger.info(f"Using CCXT {ccxt.__version__}") - exchange_config = config['exchange'] - self.log_responses = exchange_config.get('log_responses', False) + exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange'] + self.log_responses = exchange_conf.get('log_responses', False) # Leverage properties self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) @@ -152,8 +152,8 @@ class Exchange: self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) if self.trading_mode == TradingMode.FUTURES: self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has) - if exchange_config.get('_ft_has_params'): - self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'), + if exchange_conf.get('_ft_has_params'): + self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'), self._ft_has) logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) @@ -165,18 +165,18 @@ class Exchange: # Initialize ccxt objects ccxt_config = self._ccxt_config - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config) - self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) + self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config) ccxt_async_config = self._ccxt_config - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_async_config) - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}), ccxt_async_config) self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 @@ -189,7 +189,7 @@ class Exchange: self._startup_candle_count, config.get('timeframe', '')) # Converts the interval provided in minutes in config to seconds - self.markets_refresh_interval: int = exchange_config.get( + self.markets_refresh_interval: int = exchange_conf.get( "markets_refresh_interval", 60) * 60 if self.trading_mode != TradingMode.SPOT and load_leverage_tiers: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ef480a8e2..9df21a4b1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -63,6 +63,7 @@ class FreqtradeBot(LoggingMixin): # Init objects self.config = config + exchange_config = deepcopy(config['exchange']) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -70,7 +71,7 @@ class FreqtradeBot(LoggingMixin): validate_config_consistency(config) self.exchange = ExchangeResolver.load_exchange( - self.config, load_leverage_tiers=True) + self.config, exchange_config=exchange_config, load_leverage_tiers=True) init_db(self.config['db_url']) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 448fdeb8b..6e5357da5 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -2,6 +2,7 @@ This module loads custom exchanges """ import logging +from typing import Any, Dict, Optional import freqtrade.exchange as exchanges from freqtrade.constants import Config @@ -19,8 +20,8 @@ class ExchangeResolver(IResolver): object_type = Exchange @staticmethod - def load_exchange(config: Config, *, validate: bool = True, - load_leverage_tiers: bool = False) -> Exchange: + def load_exchange(config: Config, *, exchange_config: Optional[Dict[str, Any]] = None, + validate: bool = True, load_leverage_tiers: bool = False) -> Exchange: """ Load the custom class from config parameter :param exchange_name: name of the Exchange to load @@ -37,13 +38,14 @@ class ExchangeResolver(IResolver): kwargs={ 'config': config, 'validate': validate, + 'exchange_config': exchange_config, 'load_leverage_tiers': load_leverage_tiers} ) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") if not exchange: - exchange = Exchange(config, validate=validate) + exchange = Exchange(config, validate=validate, exchange_config=exchange_config,) return exchange @staticmethod From b2a631e93a7062a75165797bd59f61a7d25fcccf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 May 2023 15:38:40 +0200 Subject: [PATCH 05/23] refactor remove_exchange_credentials --- freqtrade/constants.py | 2 ++ freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/common.py | 16 ++++++++-------- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/rpc/api_server/api_backtest.py | 2 ++ tests/exchange/test_exchange.py | 10 ++++------ 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b8e240419..3802ec3ad 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -690,4 +690,6 @@ BidAsk = Literal['bid', 'ask'] OBLiteral = Literal['asks', 'bids'] Config = Dict[str, Any] +# Exchange part of the configuration. +ExchangeConfig = Dict[str, Any] IntOrInf = float diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8092d5af8..12fb0c55e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 # isort: off -from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS +from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.binance import Binance diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index e60207573..10dfdf178 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -4,7 +4,7 @@ import time from functools import wraps from typing import Any, Callable, Optional, TypeVar, cast, overload -from freqtrade.constants import Config +from freqtrade.constants import ExchangeConfig from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError from freqtrade.mixins import LoggingMixin @@ -89,18 +89,18 @@ EXCHANGE_HAS_OPTIONAL = [ ] -def remove_credentials(config: Config) -> None: +def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None: """ Removes exchange keys from the configuration and specifies dry-run Used for backtesting / hyperopt / edge and utils. Modifies the input dict! """ - if config.get('dry_run', False): - config['exchange']['key'] = '' - config['exchange']['apiKey'] = '' - config['exchange']['secret'] = '' - config['exchange']['password'] = '' - config['exchange']['uid'] = '' + if dry_run: + exchange_config['key'] = '' + exchange_config['apiKey'] = '' + exchange_config['secret'] = '' + exchange_config['password'] = '' + exchange_config['uid'] = '' def calculate_backoff(retrycount, max_retries): diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a70527a7f..7c68eaa99 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,16 +20,16 @@ from dateutil import parser from pandas import DataFrame, concat from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk, - BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker, - OBLiteral, PairWithTimeframe) + BuySell, Config, EntryExit, ExchangeConfig, + ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.enums.pricetype import PriceType from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier, - retrier_async) +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials, + retrier, retrier_async) from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType, amount_to_contract_precision, amount_to_contracts, amount_to_precision, contracts_to_amount, @@ -92,7 +92,7 @@ class Exchange: # TradingMode.SPOT always supported and not required in this list ] - def __init__(self, config: Config, *, exchange_config: Optional[Config] = None, + def __init__(self, config: Config, *, exchange_config: Optional[ExchangeConfig] = None, validate: bool = True, load_leverage_tiers: bool = False) -> None: """ Initializes this module with the given config, @@ -131,12 +131,12 @@ class Exchange: # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} - remove_credentials(config) if config['dry_run']: logger.info('Instance is running with dry_run enabled') logger.info(f"Using CCXT {ccxt.__version__}") exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange'] + remove_exchange_credentials(exchange_conf, config.get('dry_run', False)) self.log_responses = exchange_conf.get('log_responses', False) # Leverage properties diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index d9d7a27f1..b168affc3 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -11,6 +11,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, BacktestResponse) @@ -38,6 +39,7 @@ async def api_start_backtest( # noqa: C901 raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") btconfig = deepcopy(config) + remove_exchange_credentials(btconfig['exchange'], True) settings = dict(bt_settings) if settings.get('freqai', None) is not None: settings['freqai'] = dict(settings['freqai']) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ebbecdad0..4c7a7dcc8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -20,7 +20,7 @@ from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_pr timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, - calculate_backoff, remove_credentials) + calculate_backoff, remove_exchange_credentials) from freqtrade.exchange.exchange import amount_to_contract_precision from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_patched_exchange, @@ -137,16 +137,14 @@ def test_init(default_conf, mocker, caplog): assert log_has('Instance is running with dry_run enabled', caplog) -def test_remove_credentials(default_conf, caplog) -> None: +def test_remove_exchange_credentials(default_conf) -> None: conf = deepcopy(default_conf) - conf['dry_run'] = False - remove_credentials(conf) + remove_exchange_credentials(conf['exchange'], False) assert conf['exchange']['key'] != '' assert conf['exchange']['secret'] != '' - conf['dry_run'] = True - remove_credentials(conf) + remove_exchange_credentials(conf['exchange'], True) assert conf['exchange']['key'] == '' assert conf['exchange']['secret'] == '' assert conf['exchange']['password'] == '' From 66c3eb28209accdb38664b7cc26475da9fe5e6ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 May 2023 07:06:18 +0200 Subject: [PATCH 06/23] Remove keys from config before loading strategy --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9df21a4b1..ed13be5b3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -23,6 +23,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, PricingError) from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds) +from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db @@ -64,6 +65,8 @@ class FreqtradeBot(LoggingMixin): # Init objects self.config = config exchange_config = deepcopy(config['exchange']) + # Remove credentials from original exchange config to avoid accidental credentail exposure + remove_exchange_credentials(config['exchange'], True) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) From c242d89cda06338231bf2b7a76110157428dd924 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 May 2023 07:10:55 +0200 Subject: [PATCH 07/23] Improve test format --- tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8aa3f63d5..97765a75b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -121,7 +121,7 @@ def test_order_dict(default_conf_usdt, mocker, runmode, caplog) -> None: freqtrade = FreqtradeBot(conf) if runmode == RunMode.LIVE: - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) + assert not log_has_re(r".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() @@ -136,7 +136,7 @@ def test_order_dict(default_conf_usdt, mocker, runmode, caplog) -> None: } freqtrade = FreqtradeBot(conf) assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) + assert not log_has_re(r".*stoploss_on_exchange .* dry-run", caplog) def test_get_trade_stake_amount(default_conf_usdt, mocker) -> None: From 68f67c5ae8c7e49745ca8454bbae7b88b9603308 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 May 2023 07:19:15 +0200 Subject: [PATCH 08/23] Test proper removal of exchange keys --- tests/test_freqtradebot.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 97765a75b..bd78e2fda 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -149,6 +149,34 @@ def test_get_trade_stake_amount(default_conf_usdt, mocker) -> None: assert result == default_conf_usdt['stake_amount'] +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_load_strategy_no_keys(default_conf_usdt, mocker, runmode, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + conf = deepcopy(default_conf_usdt) + conf['runmode'] = runmode + erm = mocker.patch('freqtrade.freqtradebot.ExchangeResolver.load_exchange') + + freqtrade = FreqtradeBot(conf) + strategy_config = freqtrade.strategy.config + assert id(strategy_config['exchange']) == id(conf['exchange']) + # Keys have been removed and are not passed to the exchange + assert strategy_config['exchange']['key'] == '' + assert strategy_config['exchange']['secret'] == '' + + assert erm.call_count == 1 + ex_conf = erm.call_args_list[0][1]['exchange_config'] + assert id(ex_conf) != id(conf['exchange']) + # Keys are still present + assert ex_conf['key'] != '' + assert ex_conf['key'] == default_conf_usdt['exchange']['key'] + assert ex_conf['secret'] != '' + assert ex_conf['secret'] == default_conf_usdt['exchange']['secret'] + + @pytest.mark.parametrize("amend_last,wallet,max_open,lsamr,expected", [ (False, 120, 2, 0.5, [60, None]), (True, 120, 2, 0.5, [60, 58.8]), From 9b1028789980b8a0b6e46fa6df7f7d6503c8dd3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 May 2023 07:27:08 +0200 Subject: [PATCH 09/23] Improve typing --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/resolvers/exchange_resolver.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ed13be5b3..21426623f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -13,7 +13,7 @@ from schedule import Scheduler from freqtrade import constants from freqtrade.configuration import validate_config_consistency -from freqtrade.constants import BuySell, Config, LongShort +from freqtrade.constants import BuySell, Config, ExchangeConfig, LongShort from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge @@ -64,7 +64,7 @@ class FreqtradeBot(LoggingMixin): # Init objects self.config = config - exchange_config = deepcopy(config['exchange']) + exchange_config: ExchangeConfig = deepcopy(config['exchange']) # Remove credentials from original exchange config to avoid accidental credentail exposure remove_exchange_credentials(config['exchange'], True) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 6e5357da5..c5c4e1a68 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -2,10 +2,10 @@ This module loads custom exchanges """ import logging -from typing import Any, Dict, Optional +from typing import Optional import freqtrade.exchange as exchanges -from freqtrade.constants import Config +from freqtrade.constants import Config, ExchangeConfig from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange from freqtrade.resolvers import IResolver @@ -20,7 +20,7 @@ class ExchangeResolver(IResolver): object_type = Exchange @staticmethod - def load_exchange(config: Config, *, exchange_config: Optional[Dict[str, Any]] = None, + def load_exchange(config: Config, *, exchange_config: Optional[ExchangeConfig] = None, validate: bool = True, load_leverage_tiers: bool = False) -> Exchange: """ Load the custom class from config parameter From 707c6744b98723ff657d1bf9388551271eaeae08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 May 2023 07:02:54 +0200 Subject: [PATCH 10/23] Fix doc and example indentation --- docs/freqai-reinforcement-learning.md | 180 +++++++++++++------------- 1 file changed, 89 insertions(+), 91 deletions(-) diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index 547bb13eb..1c95409ae 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -145,94 +145,94 @@ As you begin to modify the strategy and the prediction model, you will quickly r The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time. ```python - from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner - from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions +from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner +from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions - class MyCoolRLModel(ReinforcementLearner): +class MyCoolRLModel(ReinforcementLearner): + """ + User created RL prediction model. + + Save this file to `freqtrade/user_data/freqaimodels` + + then use it with: + + freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat + + Here the users can override any of the functions + available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this + is where the user overrides `MyRLEnv` (see below), to define custom + `calculate_reward()` function, or to override any other parts of the environment. + + This class also allows users to override any other part of the IFreqaiModel tree. + For example, the user can override `def fit()` or `def train()` or `def predict()` + to take fine-tuned control over these processes. + + Another common override may be `def data_cleaning_predict()` where the user can + take fine-tuned control over the data handling pipeline. + """ + class MyRLEnv(Base5ActionRLEnv): """ - User created RL prediction model. + User made custom environment. This class inherits from BaseEnvironment and gym.env. + Users can override any functions from those parent classes. Here is an example + of a user customized `calculate_reward()` function. - Save this file to `freqtrade/user_data/freqaimodels` - - then use it with: - - freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat - - Here the users can override any of the functions - available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this - is where the user overrides `MyRLEnv` (see below), to define custom - `calculate_reward()` function, or to override any other parts of the environment. - - This class also allows users to override any other part of the IFreqaiModel tree. - For example, the user can override `def fit()` or `def train()` or `def predict()` - to take fine-tuned control over these processes. - - Another common override may be `def data_cleaning_predict()` where the user can - take fine-tuned control over the data handling pipeline. + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. """ - class MyRLEnv(Base5ActionRLEnv): - """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. - Users can override any functions from those parent classes. Here is an example - of a user customized `calculate_reward()` function. + def calculate_reward(self, action: int) -> float: + # first, penalize if the action is not valid + if not self._is_valid(action): + return -2 + pnl = self.get_unrealized_profit() - Warning! - This is function is a showcase of functionality designed to show as many possible - environment control features as possible. It is also designed to run quickly - on small computers. This is a benchmark, it is *not* for live production. - """ - def calculate_reward(self, action: int) -> float: - # first, penalize if the action is not valid - if not self._is_valid(action): - return -2 - pnl = self.get_unrealized_profit() + factor = 100 - factor = 100 + pair = self.pair.replace(':', '') - pair = self.pair.replace(':', '') + # you can use feature values from dataframe + # Assumes the shifted RSI indicator has been generated in the strategy. + rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_" + f"{self.config['timeframe']}"].iloc[self._current_tick] - # you can use feature values from dataframe - # Assumes the shifted RSI indicator has been generated in the strategy. - rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_" - f"{self.config['timeframe']}"].iloc[self._current_tick] + # reward agent for entering trades + if (action in (Actions.Long_enter.value, Actions.Short_enter.value) + and self._position == Positions.Neutral): + if rsi_now < 40: + factor = 40 / rsi_now + else: + factor = 1 + return 25 * factor - # reward agent for entering trades - if (action in (Actions.Long_enter.value, Actions.Short_enter.value) - and self._position == Positions.Neutral): - if rsi_now < 40: - factor = 40 / rsi_now - else: - factor = 1 - return 25 * factor - - # discourage agent from not entering trades - if action == Actions.Neutral.value and self._position == Positions.Neutral: - return -1 - max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300) - trade_duration = self._current_tick - self._last_trade_tick - if trade_duration <= max_trade_duration: - factor *= 1.5 - elif trade_duration > max_trade_duration: - factor *= 0.5 - # discourage sitting in position - if self._position in (Positions.Short, Positions.Long) and \ - action == Actions.Neutral.value: - return -1 * trade_duration / max_trade_duration - # close long - if action == Actions.Long_exit.value and self._position == Positions.Long: - if pnl > self.profit_aim * self.rr: - factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) - return float(pnl * factor) - # close short - if action == Actions.Short_exit.value and self._position == Positions.Short: - if pnl > self.profit_aim * self.rr: - factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) - return float(pnl * factor) - return 0. + # discourage agent from not entering trades + if action == Actions.Neutral.value and self._position == Positions.Neutral: + return -1 + max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300) + trade_duration = self._current_tick - self._last_trade_tick + if trade_duration <= max_trade_duration: + factor *= 1.5 + elif trade_duration > max_trade_duration: + factor *= 0.5 + # discourage sitting in position + if self._position in (Positions.Short, Positions.Long) and \ + action == Actions.Neutral.value: + return -1 * trade_duration / max_trade_duration + # close long + if action == Actions.Long_exit.value and self._position == Positions.Long: + if pnl > self.profit_aim * self.rr: + factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + return float(pnl * factor) + # close short + if action == Actions.Short_exit.value and self._position == Positions.Short: + if pnl > self.profit_aim * self.rr: + factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + return float(pnl * factor) + return 0. ``` -### Using Tensorboard +## Using Tensorboard Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command: @@ -245,32 +245,30 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th ![tensorboard](assets/tensorboard.jpg) - -### Custom logging +## Custom logging FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode. - `self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called: -```py - class MyRLEnv(Base5ActionRLEnv): - """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. - Users can override any functions from those parent classes. Here is an example - of a user customized `calculate_reward()` function. - """ - def calculate_reward(self, action: int) -> float: - if not self._is_valid(action): - self.tensorboard_log("invalid") - return -2 +```python + class MyRLEnv(Base5ActionRLEnv): + """ + User made custom environment. This class inherits from BaseEnvironment and gym.env. + Users can override any functions from those parent classes. Here is an example + of a user customized `calculate_reward()` function. + """ + def calculate_reward(self, action: int) -> float: + if not self._is_valid(action): + self.tensorboard_log("invalid") + return -2 ``` !!! Note The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented. -### Choosing a base environment +## Choosing a base environment FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include: From 0d4010a0beafde62dd78603f85185c70cff7584e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 May 2023 07:25:02 +0200 Subject: [PATCH 11/23] Add sample docker-compose file for freqAI, comment about that --- docker-compose.yml | 21 +++++++++--------- docker/docker-compose-freqai.yml | 37 ++++++++++++++++++++++++++++++++ docs/freqai-configuration.md | 4 +++- 3 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 docker/docker-compose-freqai.yml diff --git a/docker-compose.yml b/docker-compose.yml index b8f48577a..10a55e0dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,15 @@ services: # image: freqtradeorg/freqtrade:develop # Use plotting image # image: freqtradeorg/freqtrade:develop_plot + # # Enable GPU Image and GPU Resources (only relevant for freqAI) + # # Make sure to uncomment the whole deploy section + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] # Build step - only needed when additional dependencies are needed # build: # context: . @@ -16,7 +25,7 @@ services: - "./user_data:/freqtrade/user_data" # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation - # before enabling this. + # for more information. ports: - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` @@ -26,12 +35,4 @@ services: --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json --strategy SampleStrategy - # Enable GPU Image and GPU Resources - #image: freqtradeorg/freqtrade:develop_freqaitorch - #deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: 1 - # capabilities: [gpu] + diff --git a/docker/docker-compose-freqai.yml b/docker/docker-compose-freqai.yml new file mode 100644 index 000000000..e8b3b89a1 --- /dev/null +++ b/docker/docker-compose-freqai.yml @@ -0,0 +1,37 @@ +--- +version: '3' +services: + freqtrade: + image: freqtradeorg/freqtrade:stable_freqaitorch + # # Enable GPU Image and GPU Resources + # # Make sure to uncomment the whole deploy section + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + + # Build step - only needed when additional dependencies are needed + # build: + # context: . + # dockerfile: "./docker/Dockerfile.custom" + restart: unless-stopped + container_name: freqtrade + volumes: + - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation + # for more information. + ports: + - "127.0.0.1:8080:8080" + # Default command used when running `docker compose up` + command: > + trade + --logfile /freqtrade/user_data/logs/freqtrade.log + --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite + --config /freqtrade/user_data/config.json + --freqai-model XGBoostClassifier + --strategy SampleStrategy + diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index e7aca20be..7f5783a6e 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -248,9 +248,11 @@ The easiest way to quickly run a pytorch model is with the following command (fo freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates ``` -!!! note "Installation/docker" +!!! Note "Installation/docker" The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?". Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`. + We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. + This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available. ### Structure From a2cbe5df0419ca7bc5593f8aead3146c4c44bcfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 May 2023 07:26:11 +0200 Subject: [PATCH 12/23] Remove trailing spaces --- docker-compose.yml | 1 - docker/docker-compose-freqai.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 10a55e0dd..3b6f45bfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,4 +35,3 @@ services: --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json --strategy SampleStrategy - diff --git a/docker/docker-compose-freqai.yml b/docker/docker-compose-freqai.yml index e8b3b89a1..6edf41238 100644 --- a/docker/docker-compose-freqai.yml +++ b/docker/docker-compose-freqai.yml @@ -34,4 +34,3 @@ services: --config /freqtrade/user_data/config.json --freqai-model XGBoostClassifier --strategy SampleStrategy - From c4c0371ed3c4fd466d89e01ef05ee8b842757d1c Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 19 May 2023 14:48:17 +0000 Subject: [PATCH 13/23] add docker comment to docker usage freqai doc section --- docs/freqai.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/freqai.md b/docs/freqai.md index 02b723a20..3c4f47212 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -78,6 +78,9 @@ pip install -r requirements-freqai.txt If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. +!!! note "docker-compose-freqai.yml" + We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available. + ### FreqAI position in open-source machine learning landscape Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data. From 53162272193cf9b5dc233650022c1cf9ff51e94c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 May 2023 09:08:52 +0200 Subject: [PATCH 14/23] Extract api backtest logic from ApiServer class --- freqtrade/rpc/api_server/api_backtest.py | 86 ++++++++++---------- freqtrade/rpc/api_server/deps.py | 7 +- freqtrade/rpc/api_server/webserver.py | 15 +--- freqtrade/rpc/api_server/webserver_bgwork.py | 16 ++++ tests/rpc/test_rpc_apiserver.py | 5 +- 5 files changed, 68 insertions(+), 61 deletions(-) create mode 100644 freqtrade/rpc/api_server/webserver_bgwork.py diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index b168affc3..4055845e1 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -16,7 +16,7 @@ from freqtrade.misc import deep_merge_dicts from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, BacktestResponse) from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode -from freqtrade.rpc.api_server.webserver import ApiServer +from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPCException @@ -30,9 +30,9 @@ router = APIRouter() async def api_start_backtest( # noqa: C901 bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): - ApiServer._bt['bt_error'] = None + ApiBG._bt['bt_error'] = None """Start backtesting if not done so already""" - if ApiServer._bgtask_running: + if ApiBG._bgtask_running: raise RPCException('Bot Background task already running') if ':' in bt_settings.strategy: @@ -63,30 +63,30 @@ async def api_start_backtest( # noqa: C901 asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy - lastconfig = ApiServer._bt['last_config'] + lastconfig = ApiBG._bt['last_config'] strat = StrategyResolver.load_strategy(btconfig) validate_config_consistency(btconfig) if ( - not ApiServer._bt['bt'] + not ApiBG._bt['bt'] or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') or lastconfig.get('timerange') != btconfig['timerange'] ): from freqtrade.optimize.backtesting import Backtesting - ApiServer._bt['bt'] = Backtesting(btconfig) - ApiServer._bt['bt'].load_bt_data_detail() + ApiBG._bt['bt'] = Backtesting(btconfig) + ApiBG._bt['bt'].load_bt_data_detail() else: - ApiServer._bt['bt'].config = btconfig - ApiServer._bt['bt'].init_backtest() + ApiBG._bt['bt'].config = btconfig + ApiBG._bt['bt'].init_backtest() # Only reload data if timeframe changed. if ( - not ApiServer._bt['data'] - or not ApiServer._bt['timerange'] + not ApiBG._bt['data'] + or not ApiBG._bt['timerange'] or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timerange') != btconfig['timerange'] ): - ApiServer._bt['data'], ApiServer._bt['timerange'] = ApiServer._bt[ + ApiBG._bt['data'], ApiBG._bt['timerange'] = ApiBG._bt[ 'bt'].load_bt_data() lastconfig['timerange'] = btconfig['timerange'] @@ -95,27 +95,27 @@ async def api_start_backtest( # noqa: C901 lastconfig['enable_protections'] = btconfig.get('enable_protections') lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') - ApiServer._bt['bt'].enable_protections = btconfig.get('enable_protections', False) - ApiServer._bt['bt'].strategylist = [strat] - ApiServer._bt['bt'].results = {} - ApiServer._bt['bt'].load_prior_backtest() + ApiBG._bt['bt'].enable_protections = btconfig.get('enable_protections', False) + ApiBG._bt['bt'].strategylist = [strat] + ApiBG._bt['bt'].results = {} + ApiBG._bt['bt'].load_prior_backtest() - ApiServer._bt['bt'].abort = False - if (ApiServer._bt['bt'].results and - strat.get_strategy_name() in ApiServer._bt['bt'].results['strategy']): + ApiBG._bt['bt'].abort = False + if (ApiBG._bt['bt'].results and + strat.get_strategy_name() in ApiBG._bt['bt'].results['strategy']): # When previous result hash matches - reuse that result and skip backtesting. logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') else: - min_date, max_date = ApiServer._bt['bt'].backtest_one_strategy( - strat, ApiServer._bt['data'], ApiServer._bt['timerange']) + min_date, max_date = ApiBG._bt['bt'].backtest_one_strategy( + strat, ApiBG._bt['data'], ApiBG._bt['timerange']) - ApiServer._bt['bt'].results = generate_backtest_stats( - ApiServer._bt['data'], ApiServer._bt['bt'].all_results, + ApiBG._bt['bt'].results = generate_backtest_stats( + ApiBG._bt['data'], ApiBG._bt['bt'].all_results, min_date=min_date, max_date=max_date) if btconfig.get('export', 'none') == 'trades': store_backtest_stats( - btconfig['exportfilename'], ApiServer._bt['bt'].results, + btconfig['exportfilename'], ApiBG._bt['bt'].results, datetime.now().strftime("%Y-%m-%d_%H-%M-%S") ) @@ -123,13 +123,13 @@ async def api_start_backtest( # noqa: C901 except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") - ApiServer._bt['bt_error'] = str(e) + ApiBG._bt['bt_error'] = str(e) pass finally: - ApiServer._bgtask_running = False + ApiBG._bgtask_running = False background_tasks.add_task(run_backtest) - ApiServer._bgtask_running = True + ApiBG._bgtask_running = True return { "status": "running", @@ -147,18 +147,18 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): Returns Result after backtesting has been ran. """ from freqtrade.persistence import LocalTrade - if ApiServer._bgtask_running: + if ApiBG._bgtask_running: return { "status": "running", "running": True, - "step": (ApiServer._bt['bt'].progress.action if ApiServer._bt['bt'] + "step": (ApiBG._bt['bt'].progress.action if ApiBG._bt['bt'] else str(BacktestState.STARTUP)), - "progress": ApiServer._bt['bt'].progress.progress if ApiServer._bt['bt'] else 0, + "progress": ApiBG._bt['bt'].progress.progress if ApiBG._bt['bt'] else 0, "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", } - if not ApiServer._bt['bt']: + if not ApiBG._bt['bt']: return { "status": "not_started", "running": False, @@ -166,13 +166,13 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest not yet executed" } - if ApiServer._bt['bt_error']: + if ApiBG._bt['bt_error']: return { "status": "error", "running": False, "step": "", "progress": 0, - "status_msg": f"Backtest failed with {ApiServer._bt['bt_error']}" + "status_msg": f"Backtest failed with {ApiBG._bt['bt_error']}" } return { @@ -181,14 +181,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "status_msg": "Backtest ended", "step": "finished", "progress": 1, - "backtest_result": ApiServer._bt['bt'].results, + "backtest_result": ApiBG._bt['bt'].results, } @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" - if ApiServer._bgtask_running: + if ApiBG._bgtask_running: return { "status": "running", "running": True, @@ -196,12 +196,12 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest running", } - if ApiServer._bt['bt']: - ApiServer._bt['bt'].cleanup() - del ApiServer._bt['bt'] - ApiServer._bt['bt'] = None - del ApiServer._bt['data'] - ApiServer._bt['data'] = None + if ApiBG._bt['bt']: + ApiBG._bt['bt'].cleanup() + del ApiBG._bt['bt'] + ApiBG._bt['bt'] = None + del ApiBG._bt['data'] + ApiBG._bt['data'] = None logger.info("Backtesting reset") return { "status": "reset", @@ -214,7 +214,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): - if not ApiServer._bgtask_running: + if not ApiBG._bgtask_running: return { "status": "not_running", "running": False, @@ -222,7 +222,7 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest ended", } - ApiServer._bt['bt'].abort = True + ApiBG._bt['bt'].abort = True return { "status": "stopping", "running": False, diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index bfc1e698c..b46ac88cd 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -6,6 +6,7 @@ from fastapi import Depends from freqtrade.enums import RunMode from freqtrade.persistence import Trade from freqtrade.persistence.models import _request_id_ctx_var +from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPC, RPCException from .webserver import ApiServer @@ -43,11 +44,11 @@ def get_api_config() -> Dict[str, Any]: def get_exchange(config=Depends(get_config)): - if not ApiServer._exchange: + if not ApiBG._exchange: from freqtrade.resolvers import ExchangeResolver - ApiServer._exchange = ExchangeResolver.load_exchange( + ApiBG._exchange = ExchangeResolver.load_exchange( config, load_leverage_tiers=False) - return ApiServer._exchange + return ApiBG._exchange def get_message_stream(): diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 8030e303b..165849a7f 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -1,6 +1,6 @@ import logging from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Optional import orjson import uvicorn @@ -36,19 +36,8 @@ class ApiServer(RPCHandler): __initialized = False _rpc: RPC - # Backtesting type: Backtesting - _bt: Dict[str, Any] = { - 'bt': None, - 'data': None, - 'timerange': None, - 'last_config': {}, - 'bt_error': None, - } _has_rpc: bool = False - _bgtask_running: bool = False _config: Config = {} - # Exchange - only available in webserver mode. - _exchange = None # websocket message stuff _message_stream: Optional[MessageStream] = None @@ -85,7 +74,7 @@ class ApiServer(RPCHandler): """ Attach rpc handler """ - if not self._has_rpc: + if not ApiServer._has_rpc: ApiServer._rpc = rpc ApiServer._has_rpc = True else: diff --git a/freqtrade/rpc/api_server/webserver_bgwork.py b/freqtrade/rpc/api_server/webserver_bgwork.py new file mode 100644 index 000000000..cdb1bd031 --- /dev/null +++ b/freqtrade/rpc/api_server/webserver_bgwork.py @@ -0,0 +1,16 @@ + +from typing import Any, Dict + + +class ApiBG(): + # Backtesting type: Backtesting + _bt: Dict[str, Any] = { + 'bt': None, + 'data': None, + 'timerange': None, + 'last_config': {}, + 'bt_error': None, + } + _bgtask_running: bool = False + # Exchange - only available in webserver mode. + _exchange = None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1e76ce557..aad4bedb6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -26,6 +26,7 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer +from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) @@ -1733,7 +1734,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): assert result['status_msg'] == 'Backtest ended' # Simulate running backtest - ApiServer._bgtask_running = True + ApiBG._bgtask_running = True rc = client_get(client, f"{BASE_URI}/backtest/abort") assert_response(rc) result = rc.json() @@ -1762,7 +1763,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): result = rc.json() assert 'Bot Background task already running' in result['error'] - ApiServer._bgtask_running = False + ApiBG._bgtask_running = False # Rerun backtest (should get previous result) rc = client_post(client, f"{BASE_URI}/backtest", data=data) From 96d74063fcadd444db473aa187246633b25a25d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 May 2023 09:12:02 +0200 Subject: [PATCH 15/23] Don't have public attributes marked as private --- freqtrade/rpc/api_server/api_backtest.py | 84 ++++++++++---------- freqtrade/rpc/api_server/deps.py | 6 +- freqtrade/rpc/api_server/webserver_bgwork.py | 6 +- tests/rpc/test_rpc_apiserver.py | 4 +- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 4055845e1..a06d65dcc 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -30,9 +30,9 @@ router = APIRouter() async def api_start_backtest( # noqa: C901 bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): - ApiBG._bt['bt_error'] = None + ApiBG.bt['bt_error'] = None """Start backtesting if not done so already""" - if ApiBG._bgtask_running: + if ApiBG.bgtask_running: raise RPCException('Bot Background task already running') if ':' in bt_settings.strategy: @@ -63,30 +63,30 @@ async def api_start_backtest( # noqa: C901 asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy - lastconfig = ApiBG._bt['last_config'] + lastconfig = ApiBG.bt['last_config'] strat = StrategyResolver.load_strategy(btconfig) validate_config_consistency(btconfig) if ( - not ApiBG._bt['bt'] + not ApiBG.bt['bt'] or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') or lastconfig.get('timerange') != btconfig['timerange'] ): from freqtrade.optimize.backtesting import Backtesting - ApiBG._bt['bt'] = Backtesting(btconfig) - ApiBG._bt['bt'].load_bt_data_detail() + ApiBG.bt['bt'] = Backtesting(btconfig) + ApiBG.bt['bt'].load_bt_data_detail() else: - ApiBG._bt['bt'].config = btconfig - ApiBG._bt['bt'].init_backtest() + ApiBG.bt['bt'].config = btconfig + ApiBG.bt['bt'].init_backtest() # Only reload data if timeframe changed. if ( - not ApiBG._bt['data'] - or not ApiBG._bt['timerange'] + not ApiBG.bt['data'] + or not ApiBG.bt['timerange'] or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timerange') != btconfig['timerange'] ): - ApiBG._bt['data'], ApiBG._bt['timerange'] = ApiBG._bt[ + ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[ 'bt'].load_bt_data() lastconfig['timerange'] = btconfig['timerange'] @@ -95,27 +95,27 @@ async def api_start_backtest( # noqa: C901 lastconfig['enable_protections'] = btconfig.get('enable_protections') lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') - ApiBG._bt['bt'].enable_protections = btconfig.get('enable_protections', False) - ApiBG._bt['bt'].strategylist = [strat] - ApiBG._bt['bt'].results = {} - ApiBG._bt['bt'].load_prior_backtest() + ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False) + ApiBG.bt['bt'].strategylist = [strat] + ApiBG.bt['bt'].results = {} + ApiBG.bt['bt'].load_prior_backtest() - ApiBG._bt['bt'].abort = False - if (ApiBG._bt['bt'].results and - strat.get_strategy_name() in ApiBG._bt['bt'].results['strategy']): + ApiBG.bt['bt'].abort = False + if (ApiBG.bt['bt'].results and + strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): # When previous result hash matches - reuse that result and skip backtesting. logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') else: - min_date, max_date = ApiBG._bt['bt'].backtest_one_strategy( - strat, ApiBG._bt['data'], ApiBG._bt['timerange']) + min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( + strat, ApiBG.bt['data'], ApiBG.bt['timerange']) - ApiBG._bt['bt'].results = generate_backtest_stats( - ApiBG._bt['data'], ApiBG._bt['bt'].all_results, + ApiBG.bt['bt'].results = generate_backtest_stats( + ApiBG.bt['data'], ApiBG.bt['bt'].all_results, min_date=min_date, max_date=max_date) if btconfig.get('export', 'none') == 'trades': store_backtest_stats( - btconfig['exportfilename'], ApiBG._bt['bt'].results, + btconfig['exportfilename'], ApiBG.bt['bt'].results, datetime.now().strftime("%Y-%m-%d_%H-%M-%S") ) @@ -123,13 +123,13 @@ async def api_start_backtest( # noqa: C901 except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") - ApiBG._bt['bt_error'] = str(e) + ApiBG.bt['bt_error'] = str(e) pass finally: - ApiBG._bgtask_running = False + ApiBG.bgtask_running = False background_tasks.add_task(run_backtest) - ApiBG._bgtask_running = True + ApiBG.bgtask_running = True return { "status": "running", @@ -147,18 +147,18 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): Returns Result after backtesting has been ran. """ from freqtrade.persistence import LocalTrade - if ApiBG._bgtask_running: + if ApiBG.bgtask_running: return { "status": "running", "running": True, - "step": (ApiBG._bt['bt'].progress.action if ApiBG._bt['bt'] + "step": (ApiBG.bt['bt'].progress.action if ApiBG.bt['bt'] else str(BacktestState.STARTUP)), - "progress": ApiBG._bt['bt'].progress.progress if ApiBG._bt['bt'] else 0, + "progress": ApiBG.bt['bt'].progress.progress if ApiBG.bt['bt'] else 0, "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", } - if not ApiBG._bt['bt']: + if not ApiBG.bt['bt']: return { "status": "not_started", "running": False, @@ -166,13 +166,13 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest not yet executed" } - if ApiBG._bt['bt_error']: + if ApiBG.bt['bt_error']: return { "status": "error", "running": False, "step": "", "progress": 0, - "status_msg": f"Backtest failed with {ApiBG._bt['bt_error']}" + "status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}" } return { @@ -181,14 +181,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "status_msg": "Backtest ended", "step": "finished", "progress": 1, - "backtest_result": ApiBG._bt['bt'].results, + "backtest_result": ApiBG.bt['bt'].results, } @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" - if ApiBG._bgtask_running: + if ApiBG.bgtask_running: return { "status": "running", "running": True, @@ -196,12 +196,12 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest running", } - if ApiBG._bt['bt']: - ApiBG._bt['bt'].cleanup() - del ApiBG._bt['bt'] - ApiBG._bt['bt'] = None - del ApiBG._bt['data'] - ApiBG._bt['data'] = None + if ApiBG.bt['bt']: + ApiBG.bt['bt'].cleanup() + del ApiBG.bt['bt'] + ApiBG.bt['bt'] = None + del ApiBG.bt['data'] + ApiBG.bt['data'] = None logger.info("Backtesting reset") return { "status": "reset", @@ -214,7 +214,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): - if not ApiBG._bgtask_running: + if not ApiBG.bgtask_running: return { "status": "not_running", "running": False, @@ -222,7 +222,7 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest ended", } - ApiBG._bt['bt'].abort = True + ApiBG.bt['bt'].abort = True return { "status": "stopping", "running": False, diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index b46ac88cd..8fd105d3e 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -44,11 +44,11 @@ def get_api_config() -> Dict[str, Any]: def get_exchange(config=Depends(get_config)): - if not ApiBG._exchange: + if not ApiBG.exchange: from freqtrade.resolvers import ExchangeResolver - ApiBG._exchange = ExchangeResolver.load_exchange( + ApiBG.exchange = ExchangeResolver.load_exchange( config, load_leverage_tiers=False) - return ApiBG._exchange + return ApiBG.exchange def get_message_stream(): diff --git a/freqtrade/rpc/api_server/webserver_bgwork.py b/freqtrade/rpc/api_server/webserver_bgwork.py index cdb1bd031..925f34de3 100644 --- a/freqtrade/rpc/api_server/webserver_bgwork.py +++ b/freqtrade/rpc/api_server/webserver_bgwork.py @@ -4,13 +4,13 @@ from typing import Any, Dict class ApiBG(): # Backtesting type: Backtesting - _bt: Dict[str, Any] = { + bt: Dict[str, Any] = { 'bt': None, 'data': None, 'timerange': None, 'last_config': {}, 'bt_error': None, } - _bgtask_running: bool = False + bgtask_running: bool = False # Exchange - only available in webserver mode. - _exchange = None + exchange = None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index aad4bedb6..3b1a2616f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1734,7 +1734,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): assert result['status_msg'] == 'Backtest ended' # Simulate running backtest - ApiBG._bgtask_running = True + ApiBG.bgtask_running = True rc = client_get(client, f"{BASE_URI}/backtest/abort") assert_response(rc) result = rc.json() @@ -1763,7 +1763,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): result = rc.json() assert 'Bot Background task already running' in result['error'] - ApiBG._bgtask_running = False + ApiBG.bgtask_running = False # Rerun backtest (should get previous result) rc = client_post(client, f"{BASE_URI}/backtest", data=data) From 914195acf4e8ffafb3d6854039ef5ede949bf33f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 May 2023 09:14:00 +0200 Subject: [PATCH 16/23] Ensure one test can't fail 20 others --- tests/rpc/test_rpc_apiserver.py | 224 ++++++++++++++++---------------- 1 file changed, 114 insertions(+), 110 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3b1a2616f..842981ad0 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -21,6 +21,7 @@ from freqtrade.__init__ import __version__ from freqtrade.enums import CandleType, RunMode, State, TradingMode from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre +from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer @@ -1666,137 +1667,140 @@ def test_sysinfo(botclient): def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): - ftbot, client = botclient - mocker.patch(f'{EXMS}.get_fee', fee) + try: + ftbot, client = botclient + mocker.patch(f'{EXMS}.get_fee', fee) - rc = client_get(client, f"{BASE_URI}/backtest") - # Backtest prevented in default mode - assert_response(rc, 502) + rc = client_get(client, f"{BASE_URI}/backtest") + # Backtest prevented in default mode + assert_response(rc, 502) - ftbot.config['runmode'] = RunMode.WEBSERVER - # Backtesting not started yet - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) + ftbot.config['runmode'] = RunMode.WEBSERVER + # Backtesting not started yet + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) - result = rc.json() - assert result['status'] == 'not_started' - assert not result['running'] - assert result['status_msg'] == 'Backtest not yet executed' - assert result['progress'] == 0 + result = rc.json() + assert result['status'] == 'not_started' + assert not result['running'] + assert result['status_msg'] == 'Backtest not yet executed' + assert result['progress'] == 0 - # Reset backtesting - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'reset' - assert not result['running'] - assert result['status_msg'] == 'Backtest reset' - ftbot.config['export'] = 'trades' - ftbot.config['backtest_cache'] = 'day' - ftbot.config['user_data_dir'] = Path(tmpdir) - ftbot.config['exportfilename'] = Path(tmpdir) / "backtest_results" - ftbot.config['exportfilename'].mkdir() + # Reset backtesting + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' + ftbot.config['export'] = 'trades' + ftbot.config['backtest_cache'] = 'day' + ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['exportfilename'] = Path(tmpdir) / "backtest_results" + ftbot.config['exportfilename'].mkdir() - # start backtesting - data = { - "strategy": CURRENT_TEST_STRATEGY, - "timeframe": "5m", - "timerange": "20180110-20180111", - "max_open_trades": 3, - "stake_amount": 100, - "dry_run_wallet": 1000, - "enable_protections": False - } - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc) - result = rc.json() + # start backtesting + data = { + "strategy": CURRENT_TEST_STRATEGY, + "timeframe": "5m", + "timerange": "20180110-20180111", + "max_open_trades": 3, + "stake_amount": 100, + "dry_run_wallet": 1000, + "enable_protections": False + } + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc) + result = rc.json() - assert result['status'] == 'running' - assert result['progress'] == 0 - assert result['running'] - assert result['status_msg'] == 'Backtest started' + assert result['status'] == 'running' + assert result['progress'] == 0 + assert result['running'] + assert result['status_msg'] == 'Backtest started' - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) - result = rc.json() - assert result['status'] == 'ended' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' - assert result['progress'] == 1 - assert result['backtest_result'] + result = rc.json() + assert result['status'] == 'ended' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' + assert result['progress'] == 1 + assert result['backtest_result'] - rc = client_get(client, f"{BASE_URI}/backtest/abort") - assert_response(rc) - result = rc.json() - assert result['status'] == 'not_running' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'not_running' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' - # Simulate running backtest - ApiBG.bgtask_running = True - rc = client_get(client, f"{BASE_URI}/backtest/abort") - assert_response(rc) - result = rc.json() - assert result['status'] == 'stopping' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' + # Simulate running backtest + ApiBG.bgtask_running = True + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'stopping' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' - # Get running backtest... - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'running' - assert result['running'] - assert result['step'] == "backtest" - assert result['status_msg'] == "Backtest running" + # Get running backtest... + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' + assert result['running'] + assert result['step'] == "backtest" + assert result['status_msg'] == "Backtest running" - # Try delete with task still running - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'running' + # Try delete with task still running + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' - # Post to backtest that's still running - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc, 502) - result = rc.json() - assert 'Bot Background task already running' in result['error'] + # Post to backtest that's still running + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc, 502) + result = rc.json() + assert 'Bot Background task already running' in result['error'] - ApiBG.bgtask_running = False + ApiBG.bgtask_running = False - # Rerun backtest (should get previous result) - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc) - result = rc.json() - assert log_has_re('Reusing result of previous backtest.*', caplog) + # Rerun backtest (should get previous result) + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc) + result = rc.json() + assert log_has_re('Reusing result of previous backtest.*', caplog) - data['stake_amount'] = 101 + data['stake_amount'] = 101 - mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', - side_effect=DependencyException('DeadBeef')) - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert log_has("Backtesting caused an error: DeadBeef", caplog) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', + side_effect=DependencyException('DeadBeef')) + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert log_has("Backtesting caused an error: DeadBeef", caplog) - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'error' - assert 'Backtest failed' in result['status_msg'] + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'error' + assert 'Backtest failed' in result['status_msg'] - # Delete backtesting to avoid leakage since the backtest-object may stick around. - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) + # Delete backtesting to avoid leakage since the backtest-object may stick around. + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) - result = rc.json() - assert result['status'] == 'reset' - assert not result['running'] - assert result['status_msg'] == 'Backtest reset' + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' - # Disallow base64 strategies - data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik=" - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc, 500) + # Disallow base64 strategies + data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik=" + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc, 500) + finally: + Backtesting.cleanup() def test_api_backtest_history(botclient, mocker, testdatadir): From a87b215d67bce2fdd46455824f3ba63fdff0ab15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 May 2023 09:50:59 +0200 Subject: [PATCH 17/23] Fix odd import --- freqtrade/freqai/torch/PyTorchTransformerModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchTransformerModel.py b/freqtrade/freqai/torch/PyTorchTransformerModel.py index 702a7a08b..162459776 100644 --- a/freqtrade/freqai/torch/PyTorchTransformerModel.py +++ b/freqtrade/freqai/torch/PyTorchTransformerModel.py @@ -1,7 +1,7 @@ import math import torch -import torch.nn as nn +from torch import nn """ @@ -68,7 +68,7 @@ class PyTorchTransformerModel(nn.Module): return x -class PositionalEncoding(torch.nn.Module): +class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): """ Args From abc82a3fdf36e522ca360fe55c2cc87de091689a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 May 2023 12:04:18 +0200 Subject: [PATCH 18/23] Simplify api backtesting by extracting the background_method --- freqtrade/rpc/api_server/api_backtest.py | 148 ++++++++++++----------- 1 file changed, 75 insertions(+), 73 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index a06d65dcc..8fa1a87b8 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, BackgroundTasks, Depends from fastapi.exceptions import HTTPException from freqtrade.configuration.config_validation import validate_config_consistency +from freqtrade.constants import Config from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException, OperationalException @@ -26,8 +27,80 @@ logger = logging.getLogger(__name__) router = APIRouter() +def __run_backtest_bg(btconfig: Config): + from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats + from freqtrade.resolvers import StrategyResolver + asyncio.set_event_loop(asyncio.new_event_loop()) + try: + # Reload strategy + lastconfig = ApiBG.bt['last_config'] + strat = StrategyResolver.load_strategy(btconfig) + validate_config_consistency(btconfig) + + if ( + not ApiBG.bt['bt'] + or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') + or lastconfig.get('timerange') != btconfig['timerange'] + ): + from freqtrade.optimize.backtesting import Backtesting + ApiBG.bt['bt'] = Backtesting(btconfig) + ApiBG.bt['bt'].load_bt_data_detail() + else: + ApiBG.bt['bt'].config = btconfig + ApiBG.bt['bt'].init_backtest() + # Only reload data if timeframe changed. + if ( + not ApiBG.bt['data'] + or not ApiBG.bt['timerange'] + or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('timerange') != btconfig['timerange'] + ): + ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[ + 'bt'].load_bt_data() + + lastconfig['timerange'] = btconfig['timerange'] + lastconfig['timeframe'] = strat.timeframe + lastconfig['protections'] = btconfig.get('protections', []) + lastconfig['enable_protections'] = btconfig.get('enable_protections') + lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') + + ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False) + ApiBG.bt['bt'].strategylist = [strat] + ApiBG.bt['bt'].results = {} + ApiBG.bt['bt'].load_prior_backtest() + + ApiBG.bt['bt'].abort = False + if (ApiBG.bt['bt'].results and + strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): + # When previous result hash matches - reuse that result and skip backtesting. + logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') + else: + min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( + strat, ApiBG.bt['data'], ApiBG.bt['timerange']) + + ApiBG.bt['bt'].results = generate_backtest_stats( + ApiBG.bt['data'], ApiBG.bt['bt'].all_results, + min_date=min_date, max_date=max_date) + + if btconfig.get('export', 'none') == 'trades': + store_backtest_stats( + btconfig['exportfilename'], ApiBG.bt['bt'].results, + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ) + + logger.info("Backtest finished.") + + except (Exception, OperationalException, DependencyException) as e: + logger.exception(f"Backtesting caused an error: {e}") + ApiBG.bt['bt_error'] = str(e) + pass + finally: + ApiBG.bgtask_running = False + + @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -async def api_start_backtest( # noqa: C901 +async def api_start_backtest( bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): ApiBG.bt['bt_error'] = None @@ -56,79 +129,8 @@ async def api_start_backtest( # noqa: C901 # Start backtesting # Initialize backtesting object - def run_backtest(): - from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - store_backtest_stats) - from freqtrade.resolvers import StrategyResolver - asyncio.set_event_loop(asyncio.new_event_loop()) - try: - # Reload strategy - lastconfig = ApiBG.bt['last_config'] - strat = StrategyResolver.load_strategy(btconfig) - validate_config_consistency(btconfig) - if ( - not ApiBG.bt['bt'] - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') - or lastconfig.get('timerange') != btconfig['timerange'] - ): - from freqtrade.optimize.backtesting import Backtesting - ApiBG.bt['bt'] = Backtesting(btconfig) - ApiBG.bt['bt'].load_bt_data_detail() - else: - ApiBG.bt['bt'].config = btconfig - ApiBG.bt['bt'].init_backtest() - # Only reload data if timeframe changed. - if ( - not ApiBG.bt['data'] - or not ApiBG.bt['timerange'] - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timerange') != btconfig['timerange'] - ): - ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[ - 'bt'].load_bt_data() - - lastconfig['timerange'] = btconfig['timerange'] - lastconfig['timeframe'] = strat.timeframe - lastconfig['protections'] = btconfig.get('protections', []) - lastconfig['enable_protections'] = btconfig.get('enable_protections') - lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') - - ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False) - ApiBG.bt['bt'].strategylist = [strat] - ApiBG.bt['bt'].results = {} - ApiBG.bt['bt'].load_prior_backtest() - - ApiBG.bt['bt'].abort = False - if (ApiBG.bt['bt'].results and - strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): - # When previous result hash matches - reuse that result and skip backtesting. - logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') - else: - min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( - strat, ApiBG.bt['data'], ApiBG.bt['timerange']) - - ApiBG.bt['bt'].results = generate_backtest_stats( - ApiBG.bt['data'], ApiBG.bt['bt'].all_results, - min_date=min_date, max_date=max_date) - - if btconfig.get('export', 'none') == 'trades': - store_backtest_stats( - btconfig['exportfilename'], ApiBG.bt['bt'].results, - datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - ) - - logger.info("Backtest finished.") - - except (Exception, OperationalException, DependencyException) as e: - logger.exception(f"Backtesting caused an error: {e}") - ApiBG.bt['bt_error'] = str(e) - pass - finally: - ApiBG.bgtask_running = False - - background_tasks.add_task(run_backtest) + background_tasks.add_task(__run_backtest_bg, btconfig=btconfig) ApiBG.bgtask_running = True return { From 68ab147f5765a6f8e028b1c430675d01d44abac8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 May 2023 12:52:49 +0000 Subject: [PATCH 19/23] Bump cachetools from 4.2.2 to 5.3.0 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.2.2 to 5.3.0. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.2.2...v5.3.0) --- updated-dependencies: - dependency-name: cachetools dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5d2f4147c..4c5044877 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-telegram-bot==20.3 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.23.3 arrow==1.2.3 -cachetools==4.2.2 +cachetools==5.3.0 requests==2.30.0 urllib3==2.0.2 jsonschema==4.17.3 From 8c866abad8fdb94ccfdf364957984627789b8b75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 03:56:52 +0000 Subject: [PATCH 20/23] Bump ccxt from 3.0.103 to 3.1.5 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.103 to 3.1.5. - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.103...3.1.5) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c5044877..86dfc0dee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.3 pandas==2.0.1 pandas-ta==0.3.14b -ccxt==3.0.103 +ccxt==3.1.5 cryptography==40.0.2; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' aiohttp==3.8.4 From 4e3de64c573f113e5a4d8cd4bfdd8e97adc461e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 03:57:01 +0000 Subject: [PATCH 21/23] Bump fastapi from 0.95.1 to 0.95.2 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.95.1 to 0.95.2. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.95.1...0.95.2) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c5044877..bb894803e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ orjson==3.8.12 sdnotify==0.3.2 # API Server -fastapi==0.95.1 +fastapi==0.95.2 pydantic==1.10.7 uvicorn==0.22.0 pyjwt==2.7.0 From 811198cc3e87ecc038d9ff25341cb6d9f0e42fea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 03:57:15 +0000 Subject: [PATCH 22/23] Bump pre-commit from 3.3.1 to 3.3.2 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.3.1...v3.3.2) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b1ba32b43..fdb9dfde7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.267 mypy==1.3.0 -pre-commit==3.3.1 +pre-commit==3.3.2 pytest==7.3.1 pytest-asyncio==0.21.0 pytest-cov==4.0.0 From 96eb109b4edfa63f02148fd53f8fbd94b23bc90c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 03:59:01 +0000 Subject: [PATCH 23/23] Bump mkdocs-material from 9.1.12 to 9.1.14 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.12 to 9.1.14. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.12...9.1.14) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... 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 f7c0aebe9..c5e478c78 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.3 -mkdocs-material==9.1.12 +mkdocs-material==9.1.14 mdx_truly_sane_lists==1.3 pymdown-extensions==10.0.1 jinja2==3.1.2