diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 5b71c21a8..4ff562362 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -6,12 +6,12 @@ Common Interface for bot and strategy to access data. """ import logging from pathlib import Path -from typing import List, Tuple +from typing import List, Optional, Tuple from pandas import DataFrame from freqtrade.data.history import load_pair_history -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, get_exchange from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -19,9 +19,9 @@ logger = logging.getLogger(__name__) class DataProvider(): - def __init__(self, config: dict, exchange: Exchange) -> None: + def __init__(self, config: dict, exchange_name: Optional[str] = None) -> None: self._config = config - self._exchange = exchange + self._exchange: Exchange = get_exchange(config, exchange_name) def refresh(self, pairlist: List[Tuple[str, str]], diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 4037ca704..db3e146a0 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,4 +1,6 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 +from freqtrade.exchange.exchange import (get_exchange, # noqa: F401 + delete_exchange) from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 is_exchange_bad, is_exchange_available, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a8e974991..4587b596f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -21,6 +21,7 @@ from freqtrade import (DependencyException, InvalidOrderException, from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import deep_merge_dicts + logger = logging.getLogger(__name__) @@ -772,6 +773,39 @@ class Exchange(object): raise OperationalException(e) from e +# Store Exchange objects created +__exchanges: Dict[str, Exchange] = {} + + +def get_exchange(config, exchange_name: Optional[str] = None) -> Exchange: + """ + Return Exchange object for exchange with name `exchange_name`. + Creates Exchange if it was not created before. + """ + from freqtrade.resolvers import ExchangeResolver + + name = exchange_name or config['exchange']['name'] + if name not in __exchanges: + logger.info(f"Creating Exchange object for '{name}'...") + __exchanges[name] = ExchangeResolver(name, config).exchange + logger.info(f"Exchange object for '{name}' created.") + + return __exchanges[name] + + +def delete_exchange(config, exchange_name: Optional[str] = None): + """ + Delete Exchange object for exchange with name `exchange_name`. + """ + name = exchange_name or config['exchange']['name'] + logger.info(f"Deleting Exchange object for '{name}'...") + if name in __exchanges: + del __exchanges[name] + logger.info(f"Exchange object for '{name}' deleted.") + else: + logger.warning(f"Exchange object for '{name}' was not found, cannot be deleted.") + + def is_exchange_bad(exchange_name: str) -> bool: return exchange_name in BAD_EXCHANGES diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e5ecef8bf..dc8d7ccdd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -17,10 +17,10 @@ from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.configuration import validate_config_consistency -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date +from freqtrade.exchange import Exchange, get_exchange, timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType -from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver +from freqtrade.resolvers import StrategyResolver, PairListResolver from freqtrade.state import State, RunMode from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.wallets import Wallets @@ -57,10 +57,10 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.exchange: Exchange = get_exchange(self.config) - self.wallets = Wallets(self.config, self.exchange) - self.dataprovider = DataProvider(self.config, self.exchange) + self.wallets = Wallets(self.config) + self.dataprovider = DataProvider(self.config) # Attach Dataprovider to Strategy baseclass IStrategy.dp = self.dataprovider diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 568615b53..47a482db6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,10 +15,10 @@ from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import Exchange, get_exchange, timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade -from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import IStrategy, SellType from tabulate import tabulate @@ -64,11 +64,11 @@ class Backtesting(object): self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange - self.fee = self.exchange.get_fee() + exchange: Exchange = get_exchange(self.config) + self.fee = exchange.get_fee() if self.config.get('runmode') != RunMode.HYPEROPT: - self.dataprovider = DataProvider(self.config, self.exchange) + self.dataprovider = DataProvider(self.config) IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): @@ -406,12 +406,13 @@ class Backtesting(object): timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) + exchange: Exchange = get_exchange(self.config) data = history.load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=pairs, ticker_interval=self.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, + exchange=exchange, timerange=timerange, ) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 7e0d60843..598405bf9 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -10,7 +10,7 @@ from freqtrade import constants from freqtrade.edge import Edge from freqtrade.configuration import TimeRange -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, get_exchange from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class EdgeCli(object): self.config['exchange']['uid'] = '' self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['dry_run'] = True - self.exchange = Exchange(self.config) + self.exchange: Exchange = get_exchange(self.config) self.strategy = StrategyResolver(self.config).strategy self.edge = Edge(config, self.exchange, self.strategy) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cf4740f1c..ebe615c3c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -24,6 +24,7 @@ from skopt.space import Dimension from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data, get_timeframe +from freqtrade.exchange import Exchange, get_exchange from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow users import from this file from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 @@ -342,12 +343,13 @@ class Hyperopt(Backtesting): def start(self) -> None: timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) + exchange: Exchange = get_exchange(self.config) data = load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], ticker_interval=self.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, + exchange=exchange, timerange=timerange ) @@ -371,9 +373,6 @@ class Hyperopt(Backtesting): dump(preprocessed, self.tickerdata_pickle) - # We don't need exchange instance anymore while running hyperopt - self.exchange = None # type: ignore - self.load_previous_results() cpus = cpu_count() diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 9dc6b9551..f8d72a35e 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List import pandas as pd @@ -8,8 +8,9 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, load_trades) -from freqtrade.exchange import Exchange -from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.exchange import Exchange, get_exchange +from freqtrade.resolvers import StrategyResolver + logger = logging.getLogger(__name__) @@ -19,7 +20,7 @@ try: from plotly.offline import plot import plotly.graph_objects as go except ImportError: - logger.exception("Module plotly not found \n Please install using `pip install plotly`") + logger.exception("Module plotly not found.\nPlease install using `pip install plotly`.") exit(1) @@ -28,12 +29,11 @@ def init_plotscript(config): Initialize objects needed for plotting :return: Dict with tickers, trades, pairs and strategy """ - exchange: Optional[Exchange] = None + exchange: Exchange = None # Exchange is only needed when downloading data! if config.get("refresh_pairs", False): - exchange = ExchangeResolver(config.get('exchange', {}).get('name'), - config).exchange + exchange = get_exchange(config) strategy = StrategyResolver(config).strategy if "pairs" in config: diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 6fb12a65f..a7ae7437f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -3,7 +3,6 @@ This module loads custom exchanges """ import logging -from freqtrade.exchange import Exchange import freqtrade.exchange as exchanges from freqtrade.resolvers import IResolver @@ -29,10 +28,10 @@ class ExchangeResolver(IResolver): logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") if not hasattr(self, "exchange"): - self.exchange = Exchange(config) + self.exchange = exchanges.Exchange(config) def _load_exchange( - self, exchange_name: str, kwargs: dict) -> Exchange: + self, exchange_name: str, kwargs: dict) -> exchanges.Exchange: """ Loads the specified exchange. Only checks for exchanges exported in freqtrade.exchanges diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index 2272f69a3..65e411eeb 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -14,7 +14,7 @@ def test_ohlcv(mocker, default_conf, ticker_history): exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) @@ -27,12 +27,12 @@ def test_ohlcv(mocker, default_conf, ticker_history): assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.LIVE assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) default_conf["runmode"] = RunMode.BACKTEST - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.BACKTEST assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty @@ -41,7 +41,7 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): historymock = MagicMock(return_value=ticker_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) - dp = DataProvider(default_conf, None) + dp = DataProvider(default_conf) data = dp.historic_ohlcv("UNITTEST/BTC", "5m") assert isinstance(data, DataFrame) assert historymock.call_count == 1 @@ -57,7 +57,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) @@ -70,7 +70,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.LIVE assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty @@ -78,7 +78,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): historymock = MagicMock(return_value=ticker_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) default_conf["runmode"] = RunMode.BACKTEST - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert dp.runmode == RunMode.BACKTEST assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty @@ -90,7 +90,7 @@ def test_available_pairs(mocker, default_conf, ticker_history): exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) assert len(dp.available_pairs) == 2 assert dp.available_pairs == [ ("XRP/BTC", ticker_interval), @@ -108,7 +108,7 @@ def test_refresh(mocker, default_conf, ticker_history): pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf) dp.refresh(pairs) assert refresh_mock.call_count == 1 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 56e60ec82..609deafa2 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -9,8 +9,7 @@ import arrow from freqtrade.configuration import Configuration, TimeRange from freqtrade.configuration.directory_operations import create_userdata_dir from freqtrade.data.history import download_pair_history -from freqtrade.exchange import available_exchanges -from freqtrade.resolvers import ExchangeResolver +from freqtrade.exchange import Exchange, get_exchange, available_exchanges from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -79,7 +78,7 @@ def start_download_data(args: Namespace) -> None: try: # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange: Exchange = get_exchange(config) for pair in config["pairs"]: if pair not in exchange.markets: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c8ab90276..7161443e2 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,8 +2,8 @@ """ Wallet """ import logging -from typing import Dict, NamedTuple -from freqtrade.exchange import Exchange +from typing import Dict, NamedTuple, Optional +from freqtrade.exchange import Exchange, get_exchange from freqtrade import constants logger = logging.getLogger(__name__) @@ -19,9 +19,9 @@ class Wallet(NamedTuple): class Wallets(object): - def __init__(self, config: dict, exchange: Exchange) -> None: + def __init__(self, config: dict, exchange_name: Optional[str] = None) -> None: self._config = config - self._exchange = exchange + self._exchange_name = exchange_name self._wallets: Dict[str, Wallet] = {} self.update() @@ -61,7 +61,8 @@ class Wallets(object): def update(self) -> None: - balances = self._exchange.get_balances() + exchange: Exchange = get_exchange(self._config, self._exchange_name) + balances = exchange.get_balances() for currency in balances: self._wallets[currency] = Wallet(