From f73a18c56c8a66e109a2b47339c66e395c829121 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 22 Nov 2018 13:34:06 +0100 Subject: [PATCH 01/26] Update ccxt from 1.17.522 to 1.17.529 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b271e09e..34a9b477e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.522 +ccxt==1.17.529 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 27a6dcf3fc13821006903573d330c06cecf9b169 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:23:35 +0100 Subject: [PATCH 02/26] getting available balance from wallet instead of API call. --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a2db84a9..49a4959c1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -338,8 +338,8 @@ class FreqtradeBot(object): stake_amount = self.config['stake_amount'] # TODO: should come from the wallet - avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) - # avaliable_amount = self.wallets.wallets[self.config['stake_currency']].free + #avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) + avaliable_amount = self.wallets.wallets[self.config['stake_currency']].free if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) From a9f04609d3c2cf2ed623c15ab223bc5f912a7b1e Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 10:17:10 +0100 Subject: [PATCH 03/26] tests fixed --- freqtrade/freqtradebot.py | 2 -- freqtrade/tests/conftest.py | 7 +++++++ freqtrade/tests/test_freqtradebot.py | 11 ++++------- freqtrade/tests/test_wallets.py | 2 ++ freqtrade/wallets.py | 5 ++++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 49a4959c1..c93811cae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -337,8 +337,6 @@ class FreqtradeBot(object): else: stake_amount = self.config['stake_amount'] - # TODO: should come from the wallet - #avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) avaliable_amount = self.wallets.wallets[self.config['stake_currency']].free if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index b6c022b45..cdd71fc9a 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -14,6 +14,7 @@ from telegram import Chat, Message, Update from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.edge import Edge +from freqtrade.wallets import Wallet from freqtrade.freqtradebot import FreqtradeBot logging.getLogger('').setLevel(logging.INFO) @@ -45,6 +46,12 @@ def get_patched_exchange(mocker, config, api_mock=None) -> Exchange: return exchange +def patch_wallet(mocker, currency='BTC', free=999.9) -> None: + mocker.patch('freqtrade.wallets.Wallet', MagicMock( + return_value=Wallet('bittrex', currency, free, 100, 1000) + )) + + def patch_edge(mocker) -> None: # "ETH/BTC", # "LTC/BTC", diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index cef89c250..bcd914d0d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -18,7 +18,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellType, SellCheckTuple -from freqtrade.tests.conftest import log_has, patch_exchange, patch_edge +from freqtrade.tests.conftest import log_has, patch_exchange, patch_edge, patch_wallet # Functions for recurrent object patching @@ -188,10 +188,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) - ) + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): @@ -206,12 +203,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, get_markets=markets ) @@ -521,11 +518,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5), get_fee=fee, get_markets=markets ) diff --git a/freqtrade/tests/test_wallets.py b/freqtrade/tests/test_wallets.py index cc10d665c..e6a17ecbf 100644 --- a/freqtrade/tests/test_wallets.py +++ b/freqtrade/tests/test_wallets.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock def test_sync_wallet_at_boot(mocker, default_conf): + default_conf['dry_run'] = False mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value={ @@ -58,6 +59,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): def test_sync_wallet_missing_data(mocker, default_conf): + default_conf['dry_run'] = False mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value={ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 82f527d2c..4415478d3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -26,7 +26,10 @@ class Wallets(object): def __init__(self, exchange: Exchange) -> None: self.exchange = exchange - self.wallets: Dict[str, Any] = {} + if self.exchange._conf['dry_run']: + self.wallets: Dict[str, Any] = {'BTC': Wallet('Bittrex', 'BTC', 999.99, 100, 1000)} + else: + self.wallets: Dict[str, Any] = {} self.update() def update(self) -> None: From 270624c0c518fa96cba53670fb93eea6d2932280 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 23 Nov 2018 13:34:08 +0100 Subject: [PATCH 04/26] Update ccxt from 1.17.529 to 1.17.533 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 34a9b477e..00026af8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.529 +ccxt==1.17.533 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 605211dbaf5957d54ea8fdc04e79640305cb7726 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 23 Nov 2018 13:34:09 +0100 Subject: [PATCH 05/26] Update scikit-learn from 0.20.0 to 0.20.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 00026af8d..b01dbdbbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ requests==2.20.1 urllib3==1.24.1 wrapt==1.10.11 pandas==0.23.4 -scikit-learn==0.20.0 +scikit-learn==0.20.1 joblib==0.13.0 scipy==1.1.0 jsonschema==2.6.0 From 3e8de28b51482b67a78ba3b333cc766437618ce6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Nov 2018 13:24:14 +0100 Subject: [PATCH 06/26] Add Note about order types support --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 62559a41e..1e144e5af 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -155,6 +155,9 @@ The below is the default which is used if this is not configured in either Strat }, ``` +**NOTE**: Not all exchanges support "market" orders. +The following message will be shown if your exchange does not support market orders: `"Exchange does not support market orders."` + ### What values for exchange.name? Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency From 412a627d9e377309912e86eac4487c5b9eab1c69 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 24 Nov 2018 13:34:05 +0100 Subject: [PATCH 07/26] Update ccxt from 1.17.533 to 1.17.535 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b01dbdbbd..b3791ccfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.533 +ccxt==1.17.535 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 29a4c99d1d4d10f6fb8d0657f5eb3d35040d1af7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 24 Nov 2018 13:34:07 +0100 Subject: [PATCH 08/26] Update pytest from 4.0.0 to 4.0.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b3791ccfe..44cfe4a07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ scipy==1.1.0 jsonschema==2.6.0 numpy==1.15.4 TA-Lib==0.4.17 -pytest==4.0.0 +pytest==4.0.1 pytest-mock==1.10.0 pytest-asyncio==0.9.0 pytest-cov==2.6.0 From 29347a693187cbce26f2d75babca002dfec9ddda Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 16:37:28 +0100 Subject: [PATCH 09/26] adding get_free to wallet --- freqtrade/freqtradebot.py | 2 +- freqtrade/tests/conftest.py | 7 +++---- freqtrade/wallets.py | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c93811cae..0e77a31c0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -337,7 +337,7 @@ class FreqtradeBot(object): else: stake_amount = self.config['stake_amount'] - avaliable_amount = self.wallets.wallets[self.config['stake_currency']].free + avaliable_amount = self.wallets.get_free(self.config['stake_currency']) if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index cdd71fc9a..63655126c 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -14,7 +14,6 @@ from telegram import Chat, Message, Update from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.edge import Edge -from freqtrade.wallets import Wallet from freqtrade.freqtradebot import FreqtradeBot logging.getLogger('').setLevel(logging.INFO) @@ -46,9 +45,9 @@ def get_patched_exchange(mocker, config, api_mock=None) -> Exchange: return exchange -def patch_wallet(mocker, currency='BTC', free=999.9) -> None: - mocker.patch('freqtrade.wallets.Wallet', MagicMock( - return_value=Wallet('bittrex', currency, free, 100, 1000) +def patch_wallet(mocker, free=999.9) -> None: + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock( + return_value=free )) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4415478d3..b8b37907d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -26,12 +26,20 @@ class Wallets(object): def __init__(self, exchange: Exchange) -> None: self.exchange = exchange - if self.exchange._conf['dry_run']: - self.wallets: Dict[str, Any] = {'BTC': Wallet('Bittrex', 'BTC', 999.99, 100, 1000)} - else: - self.wallets: Dict[str, Any] = {} + self.wallets: Dict[str, Any] = {} self.update() + def get_free(self, currency) -> float: + + if self.exchange._conf['dry_run']: + return 999.9 + + balance = self.wallets.get(currency) + if balance and balance['free']: + return balance['free'] + else: + return 0 + def update(self) -> None: balances = self.exchange.get_balances() From 63c2ea110acb9fe04338be56055a824566de4bee Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 16:41:17 +0100 Subject: [PATCH 10/26] Not sure why those arguments were there ! --- freqtrade/tests/test_freqtradebot.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index bcd914d0d..a36ae2cd8 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -181,11 +181,7 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock assert result == default_conf['stake_amount'] -def test_get_trade_stake_amount_no_stake_amount(default_conf, - ticker, - limit_buy_order, - fee, - mocker) -> None: +def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) From 21a093bcdbe64d3fa9c20dde208169c12d03ca3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Nov 2018 20:00:02 +0100 Subject: [PATCH 11/26] extract resolvers to IResolvers and it's own package --- freqtrade/freqtradebot.py | 4 +- freqtrade/optimize/backtesting.py | 4 +- freqtrade/optimize/edge_cli.py | 2 +- freqtrade/resolvers/__init__.py | 2 + freqtrade/resolvers/iresolver.py | 72 +++++++++++++++++++ .../strategyresolver.py} | 52 ++------------ freqtrade/tests/optimize/test_hyperopt.py | 2 +- freqtrade/tests/strategy/test_strategy.py | 32 +++++---- freqtrade/tests/test_dataframe.py | 2 +- scripts/plot_dataframe.py | 2 +- scripts/plot_profit.py | 2 +- 11 files changed, 107 insertions(+), 69 deletions(-) create mode 100644 freqtrade/resolvers/__init__.py create mode 100644 freqtrade/resolvers/iresolver.py rename freqtrade/{strategy/resolver.py => resolvers/strategyresolver.py} (74%) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a2db84a9..0526ef425 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -21,9 +21,9 @@ from freqtrade.wallets import Wallets from freqtrade.edge import Edge from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType +from freqtrade.resolvers import StrategyResolver from freqtrade.state import State -from freqtrade.strategy.interface import SellType -from freqtrade.strategy.resolver import IStrategy, StrategyResolver +from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.exchange.exchange_helpers import order_book_to_dataframe diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6fcde64fa..c6cf7276f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -20,8 +20,8 @@ from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade -from freqtrade.strategy.interface import SellType -from freqtrade.strategy.resolver import IStrategy, StrategyResolver +from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.interface import SellType, IStrategy logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 59bcbc098..a2189f6c1 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -12,7 +12,7 @@ from freqtrade.edge import Edge from freqtrade.configuration import Configuration from freqtrade.arguments import Arguments from freqtrade.exchange import Exchange -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py new file mode 100644 index 000000000..fe81b7712 --- /dev/null +++ b/freqtrade/resolvers/__init__.py @@ -0,0 +1,2 @@ +from freqtrade.resolvers.iresolver import IResolver # noqa: F401 +from freqtrade.resolvers.strategyresolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py new file mode 100644 index 000000000..ea38b30c2 --- /dev/null +++ b/freqtrade/resolvers/iresolver.py @@ -0,0 +1,72 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom hyperopts +""" +import importlib.util +import inspect +import logging +import os +from typing import Optional, Dict, Type, Any + +from freqtrade.constants import DEFAULT_HYPEROPT +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +logger = logging.getLogger(__name__) + + +class IResolver(object): + """ + This class contains all the logic to load custom hyperopt class + """ + + def __init__(self, object_type, config: Optional[Dict] = None) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + config = config or {} + + @staticmethod + def _get_valid_objects(object_type, module_path: str, + object_name: str) -> Optional[Type[Any]]: + """ + Returns a list of all possible objects for the given module_path of type oject_type + :param object_type: object_type (class) + :param module_path: absolute path to the module + :param object_name: Class name of the object + :return: Tuple with (name, class) or None + """ + + # Generate spec based on absolute path + spec = importlib.util.spec_from_file_location('unknown', module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) # type: ignore # importlib does not use typehints + + valid_objects_gen = ( + obj for name, obj in inspect.getmembers(module, inspect.isclass) + if object_name == name and object_type in obj.__bases__ + ) + return next(valid_objects_gen, None) + + @staticmethod + def _search_object(directory: str, object_type, object_name: str, + kwargs: dict) -> Optional[Any]: + """ + Search for the objectname in the given directory + :param directory: relative or absolute directory path + :return: object instance + """ + logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory) + for entry in os.listdir(directory): + # Only consider python files + if not entry.endswith('.py'): + logger.debug('Ignoring %s', entry) + continue + obj = IResolver._get_valid_objects( + object_type, os.path.abspath(os.path.join(directory, entry)), object_name + ) + if obj: + return obj(**kwargs) + return None diff --git a/freqtrade/strategy/resolver.py b/freqtrade/resolvers/strategyresolver.py similarity index 74% rename from freqtrade/strategy/resolver.py rename to freqtrade/resolvers/strategyresolver.py index 3f25e4838..a7f3b2c25 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/resolvers/strategyresolver.py @@ -3,7 +3,6 @@ """ This module load custom strategies """ -import importlib.util import inspect import logging import os @@ -11,16 +10,17 @@ import tempfile from base64 import urlsafe_b64decode from collections import OrderedDict from pathlib import Path -from typing import Dict, Optional, Type +from typing import Dict, Optional from freqtrade import constants +from freqtrade.resolvers import IResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) -class StrategyResolver(object): +class StrategyResolver(IResolver): """ This class contains all the logic to load custom strategy class """ @@ -103,7 +103,8 @@ class StrategyResolver(object): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - current_path = os.path.dirname(os.path.realpath(__file__)) + current_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'strategy') + abs_paths = [ os.path.join(os.getcwd(), 'user_data', 'strategies'), current_path, @@ -131,7 +132,8 @@ class StrategyResolver(object): for path in abs_paths: try: - strategy = self._search_strategy(path, strategy_name=strategy_name, config=config) + strategy = self._search_object(directory=path, object_type=IStrategy, + object_name=strategy_name, kwargs={'config': config}) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) strategy._populate_fun_len = len( @@ -149,43 +151,3 @@ class StrategyResolver(object): "Impossible to load Strategy '{}'. This class does not exist" " or contains Python code errors".format(strategy_name) ) - - @staticmethod - def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]: - """ - Returns a list of all possible strategies for the given module_path - :param module_path: absolute path to the module - :param strategy_name: Class name of the strategy - :return: Tuple with (name, class) or None - """ - - # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('unknown', module_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - - valid_strategies_gen = ( - obj for name, obj in inspect.getmembers(module, inspect.isclass) - if strategy_name == name and IStrategy in obj.__bases__ - ) - return next(valid_strategies_gen, None) - - @staticmethod - def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]: - """ - Search for the strategy_name in the given directory - :param directory: relative or absolute directory path - :return: name of the strategy class - """ - logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory) - for entry in os.listdir(directory): - # Only consider python files - if not entry.endswith('.py'): - logger.debug('Ignoring %s', entry) - continue - strategy = StrategyResolver._get_valid_strategies( - os.path.abspath(os.path.join(directory, entry)), strategy_name - ) - if strategy: - return strategy(config) - return None diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 85d140b6d..01d2e8b17 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -7,7 +7,7 @@ import pytest from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.hyperopt import Hyperopt, start -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index a38050f24..ac20c9cab 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -10,7 +10,7 @@ from pandas import DataFrame from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver def test_import_strategy(caplog): @@ -44,17 +44,19 @@ def test_search_strategy(): path.realpath(__file__)), '..', '..', 'strategy' ) assert isinstance( - StrategyResolver._search_strategy( - default_location, - config=default_config, - strategy_name='DefaultStrategy' + StrategyResolver._search_object( + directory=default_location, + object_type=IStrategy, + kwargs={'config': default_config}, + object_name='DefaultStrategy' ), IStrategy ) - assert StrategyResolver._search_strategy( - default_location, - config=default_config, - strategy_name='NotFoundStrategy' + assert StrategyResolver._search_object( + directory=default_location, + object_type=IStrategy, + kwargs={'config': default_config}, + object_name='NotFoundStrategy' ) is None @@ -77,7 +79,7 @@ def test_load_strategy_invalid_directory(result, caplog): resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) assert ( - 'freqtrade.strategy.resolver', + 'freqtrade.resolvers.strategyresolver', logging.WARNING, 'Path "{}" does not exist'.format(extra_dir), ) in caplog.record_tuples @@ -128,7 +130,7 @@ def test_strategy_override_minimal_roi(caplog): resolver = StrategyResolver(config) assert resolver.strategy.minimal_roi[0] == 0.5 - assert ('freqtrade.strategy.resolver', + assert ('freqtrade.resolvers.strategyresolver', logging.INFO, "Override strategy 'minimal_roi' with value in config file: {'0': 0.5}." ) in caplog.record_tuples @@ -143,7 +145,7 @@ def test_strategy_override_stoploss(caplog): resolver = StrategyResolver(config) assert resolver.strategy.stoploss == -0.5 - assert ('freqtrade.strategy.resolver', + assert ('freqtrade.resolvers.strategyresolver', logging.INFO, "Override strategy 'stoploss' with value in config file: -0.5." ) in caplog.record_tuples @@ -159,7 +161,7 @@ def test_strategy_override_ticker_interval(caplog): resolver = StrategyResolver(config) assert resolver.strategy.ticker_interval == 60 - assert ('freqtrade.strategy.resolver', + assert ('freqtrade.resolvers.strategyresolver', logging.INFO, "Override strategy 'ticker_interval' with value in config file: 60." ) in caplog.record_tuples @@ -175,7 +177,7 @@ def test_strategy_override_process_only_new_candles(caplog): resolver = StrategyResolver(config) assert resolver.strategy.process_only_new_candles - assert ('freqtrade.strategy.resolver', + assert ('freqtrade.resolvers.strategyresolver', logging.INFO, "Override process_only_new_candles 'process_only_new_candles' " "with value in config file: True." @@ -201,7 +203,7 @@ def test_strategy_override_order_types(caplog): for method in ['buy', 'sell', 'stoploss']: assert resolver.strategy.order_types[method] == order_types[method] - assert ('freqtrade.strategy.resolver', + assert ('freqtrade.resolvers.strategyresolver', logging.INFO, "Override strategy 'order_types' with value in config file:" " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}." diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index dc030d630..6afb83a3f 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -3,7 +3,7 @@ import pandas from freqtrade.optimize import load_data -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver _pairs = ['ETH/BTC'] diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 68713f296..8fd3a43bd 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -44,7 +44,7 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.exchange import Exchange from freqtrade.optimize.backtesting import setup_configuration from freqtrade.persistence import Trade -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 9c3468c74..53f14ca3c 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -27,7 +27,7 @@ import plotly.graph_objs as go from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade import constants -from freqtrade.strategy.resolver import StrategyResolver +from freqtrade.resolvers import StrategyResolver import freqtrade.optimize as optimize import freqtrade.misc as misc From 2c0d0946e6e9a953ef47ad617f8d921ede54685e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Nov 2018 20:02:29 +0100 Subject: [PATCH 12/26] Small stylistic improvements to strategyresolver --- freqtrade/resolvers/iresolver.py | 8 ++------ freqtrade/resolvers/strategyresolver.py | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index ea38b30c2..37230537e 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -1,7 +1,7 @@ # pragma pylint: disable=attribute-defined-outside-init """ -This module load custom hyperopts +This module load custom objects """ import importlib.util import inspect @@ -9,10 +9,6 @@ import logging import os from typing import Optional, Dict, Type, Any -from freqtrade.constants import DEFAULT_HYPEROPT -from freqtrade.optimize.hyperopt_interface import IHyperOpt - - logger = logging.getLogger(__name__) @@ -51,7 +47,7 @@ class IResolver(object): return next(valid_objects_gen, None) @staticmethod - def _search_object(directory: str, object_type, object_name: str, + def _search_object(directory: str, object_type, object_name: str, kwargs: dict) -> Optional[Any]: """ Search for the objectname in the given directory diff --git a/freqtrade/resolvers/strategyresolver.py b/freqtrade/resolvers/strategyresolver.py index a7f3b2c25..273effe2d 100644 --- a/freqtrade/resolvers/strategyresolver.py +++ b/freqtrade/resolvers/strategyresolver.py @@ -5,7 +5,7 @@ This module load custom strategies """ import inspect import logging -import os +from os import getcwd, path import tempfile from base64 import urlsafe_b64decode from collections import OrderedDict @@ -103,10 +103,10 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - current_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'strategy') + current_path = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'strategy') abs_paths = [ - os.path.join(os.getcwd(), 'user_data', 'strategies'), + path.join(getcwd(), 'user_data', 'strategies'), current_path, ] @@ -125,14 +125,14 @@ class StrategyResolver(IResolver): temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8')) temp.joinpath("__init__.py").touch() - strategy_name = os.path.splitext(name)[0] + strategy_name = path.splitext(name)[0] # register temp path with the bot abs_paths.insert(0, str(temp.resolve())) - for path in abs_paths: + for _path in abs_paths: try: - strategy = self._search_object(directory=path, object_type=IStrategy, + strategy = self._search_object(directory=_path, object_type=IStrategy, object_name=strategy_name, kwargs={'config': config}) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) From cc7b8209783c123c9c42ed9a9df855fb7478d807 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Nov 2018 20:14:08 +0100 Subject: [PATCH 13/26] Move hyperoptresolver to resolvers package --- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/optimize/hyperopt_resolver.py | 104 ------------------ freqtrade/resolvers/__init__.py | 3 +- freqtrade/resolvers/hyperopt_resolver.py | 64 +++++++++++ freqtrade/resolvers/iresolver.py | 2 +- ...rategyresolver.py => strategy_resolver.py} | 2 +- freqtrade/tests/strategy/test_strategy.py | 12 +- 7 files changed, 75 insertions(+), 114 deletions(-) delete mode 100644 freqtrade/optimize/hyperopt_resolver.py create mode 100644 freqtrade/resolvers/hyperopt_resolver.py rename freqtrade/resolvers/{strategyresolver.py => strategy_resolver.py} (98%) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 70d20673c..fcf35acfe 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -22,7 +22,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting -from freqtrade.optimize.hyperopt_resolver import HyperOptResolver +from freqtrade.resolvers import HyperOptResolver logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt_resolver.py b/freqtrade/optimize/hyperopt_resolver.py deleted file mode 100644 index 3d019e8df..000000000 --- a/freqtrade/optimize/hyperopt_resolver.py +++ /dev/null @@ -1,104 +0,0 @@ -# pragma pylint: disable=attribute-defined-outside-init - -""" -This module load custom hyperopts -""" -import importlib.util -import inspect -import logging -import os -from typing import Optional, Dict, Type - -from freqtrade.constants import DEFAULT_HYPEROPT -from freqtrade.optimize.hyperopt_interface import IHyperOpt - - -logger = logging.getLogger(__name__) - - -class HyperOptResolver(object): - """ - This class contains all the logic to load custom hyperopt class - """ - - __slots__ = ['hyperopt'] - - def __init__(self, config: Optional[Dict] = None) -> None: - """ - Load the custom class from config parameter - :param config: configuration dictionary or None - """ - config = config or {} - - # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt - hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT - self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) - - def _load_hyperopt( - self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: - """ - Search and loads the specified hyperopt. - :param hyperopt_name: name of the module to import - :param extra_dir: additional directory to search for the given hyperopt - :return: HyperOpt instance or None - """ - current_path = os.path.dirname(os.path.realpath(__file__)) - abs_paths = [ - os.path.join(current_path, '..', '..', 'user_data', 'hyperopts'), - current_path, - ] - - if extra_dir: - # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, extra_dir) - - for path in abs_paths: - hyperopt = self._search_hyperopt(path, hyperopt_name) - if hyperopt: - logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, path) - return hyperopt - - raise ImportError( - "Impossible to load Hyperopt '{}'. This class does not exist" - " or contains Python code errors".format(hyperopt_name) - ) - - @staticmethod - def _get_valid_hyperopts(module_path: str, hyperopt_name: str) -> Optional[Type[IHyperOpt]]: - """ - Returns a list of all possible hyperopts for the given module_path - :param module_path: absolute path to the module - :param hyperopt_name: Class name of the hyperopt - :return: Tuple with (name, class) or None - """ - - # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('user_data.hyperopts', module_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - - valid_hyperopts_gen = ( - obj for name, obj in inspect.getmembers(module, inspect.isclass) - if hyperopt_name == name and IHyperOpt in obj.__bases__ - ) - return next(valid_hyperopts_gen, None) - - @staticmethod - def _search_hyperopt(directory: str, hyperopt_name: str) -> Optional[IHyperOpt]: - """ - Search for the hyperopt_name in the given directory - :param directory: relative or absolute directory path - :return: name of the hyperopt class - """ - logger.debug('Searching for hyperopt %s in \'%s\'', hyperopt_name, directory) - for entry in os.listdir(directory): - # Only consider python files - if not entry.endswith('.py'): - logger.debug('Ignoring %s', entry) - continue - hyperopt = HyperOptResolver._get_valid_hyperopts( - os.path.abspath(os.path.join(directory, entry)), hyperopt_name - ) - if hyperopt: - return hyperopt() - return None diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index fe81b7712..84e3bcdcd 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,2 +1,3 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 -from freqtrade.resolvers.strategyresolver import StrategyResolver # noqa: F401 +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 +from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py new file mode 100644 index 000000000..38cb683c9 --- /dev/null +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -0,0 +1,64 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom hyperopts +""" +import logging +from os import path +from typing import Optional, Dict + +from freqtrade.constants import DEFAULT_HYPEROPT +from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.resolvers import IResolver + +logger = logging.getLogger(__name__) + + +class HyperOptResolver(IResolver): + """ + This class contains all the logic to load custom hyperopt class + """ + + __slots__ = ['hyperopt'] + + def __init__(self, config: Optional[Dict] = None) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + config = config or {} + + # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt + hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT + self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) + + def _load_hyperopt( + self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: + """ + Search and loads the specified hyperopt. + :param hyperopt_name: name of the module to import + :param extra_dir: additional directory to search for the given hyperopt + :return: HyperOpt instance or None + """ + current_path = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'optimize') + + abs_paths = [ + path.join(current_path, '..', '..', 'user_data', 'hyperopts'), + current_path, + ] + + if extra_dir: + # Add extra hyperopt directory on top of search paths + abs_paths.insert(0, extra_dir) + + for _path in abs_paths: + hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, + object_name=hyperopt_name) + if hyperopt: + logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, _path) + return hyperopt + + raise ImportError( + "Impossible to load Hyperopt '{}'. This class does not exist" + " or contains Python code errors".format(hyperopt_name) + ) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 37230537e..5cf6a1bc2 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -48,7 +48,7 @@ class IResolver(object): @staticmethod def _search_object(directory: str, object_type, object_name: str, - kwargs: dict) -> Optional[Any]: + kwargs: dict = {}) -> Optional[Any]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path diff --git a/freqtrade/resolvers/strategyresolver.py b/freqtrade/resolvers/strategy_resolver.py similarity index 98% rename from freqtrade/resolvers/strategyresolver.py rename to freqtrade/resolvers/strategy_resolver.py index 273effe2d..f950a6e41 100644 --- a/freqtrade/resolvers/strategyresolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -145,7 +145,7 @@ class StrategyResolver(IResolver): return import_strategy(strategy, config=config) except FileNotFoundError: - logger.warning('Path "%s" does not exist', path) + logger.warning('Path "%s" does not exist', _path) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index ac20c9cab..230ec7ab7 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -79,7 +79,7 @@ def test_load_strategy_invalid_directory(result, caplog): resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) assert ( - 'freqtrade.resolvers.strategyresolver', + 'freqtrade.resolvers.strategy_resolver', logging.WARNING, 'Path "{}" does not exist'.format(extra_dir), ) in caplog.record_tuples @@ -130,7 +130,7 @@ def test_strategy_override_minimal_roi(caplog): resolver = StrategyResolver(config) assert resolver.strategy.minimal_roi[0] == 0.5 - assert ('freqtrade.resolvers.strategyresolver', + assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override strategy 'minimal_roi' with value in config file: {'0': 0.5}." ) in caplog.record_tuples @@ -145,7 +145,7 @@ def test_strategy_override_stoploss(caplog): resolver = StrategyResolver(config) assert resolver.strategy.stoploss == -0.5 - assert ('freqtrade.resolvers.strategyresolver', + assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override strategy 'stoploss' with value in config file: -0.5." ) in caplog.record_tuples @@ -161,7 +161,7 @@ def test_strategy_override_ticker_interval(caplog): resolver = StrategyResolver(config) assert resolver.strategy.ticker_interval == 60 - assert ('freqtrade.resolvers.strategyresolver', + assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override strategy 'ticker_interval' with value in config file: 60." ) in caplog.record_tuples @@ -177,7 +177,7 @@ def test_strategy_override_process_only_new_candles(caplog): resolver = StrategyResolver(config) assert resolver.strategy.process_only_new_candles - assert ('freqtrade.resolvers.strategyresolver', + assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override process_only_new_candles 'process_only_new_candles' " "with value in config file: True." @@ -203,7 +203,7 @@ def test_strategy_override_order_types(caplog): for method in ['buy', 'sell', 'stoploss']: assert resolver.strategy.order_types[method] == order_types[method] - assert ('freqtrade.resolvers.strategyresolver', + assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override strategy 'order_types' with value in config file:" " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}." From 20de8c82e4c25aec91aa12fee76070dabc4d5f13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Nov 2018 20:39:16 +0100 Subject: [PATCH 14/26] Convert to Pathlib --- freqtrade/resolvers/hyperopt_resolver.py | 8 ++++---- freqtrade/resolvers/iresolver.py | 14 +++++++------- freqtrade/resolvers/strategy_resolver.py | 15 +++++++-------- freqtrade/tests/strategy/test_strategy.py | 5 ++--- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 38cb683c9..da7b65648 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -4,7 +4,7 @@ This module load custom hyperopts """ import logging -from os import path +from pathlib import Path from typing import Optional, Dict from freqtrade.constants import DEFAULT_HYPEROPT @@ -40,16 +40,16 @@ class HyperOptResolver(IResolver): :param extra_dir: additional directory to search for the given hyperopt :return: HyperOpt instance or None """ - current_path = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'optimize') + current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - path.join(current_path, '..', '..', 'user_data', 'hyperopts'), + current_path.parent.parent.joinpath('user_data/hyperopts'), current_path, ] if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, extra_dir) + abs_paths.insert(0, Path(extra_dir)) for _path in abs_paths: hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 5cf6a1bc2..78df32e89 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -6,7 +6,7 @@ This module load custom objects import importlib.util import inspect import logging -import os +from pathlib import Path from typing import Optional, Dict, Type, Any logger = logging.getLogger(__name__) @@ -25,7 +25,7 @@ class IResolver(object): config = config or {} @staticmethod - def _get_valid_objects(object_type, module_path: str, + def _get_valid_objects(object_type, module_path: Path, object_name: str) -> Optional[Type[Any]]: """ Returns a list of all possible objects for the given module_path of type oject_type @@ -36,7 +36,7 @@ class IResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('unknown', module_path) + spec = importlib.util.spec_from_file_location('unknown', str(module_path)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore # importlib does not use typehints @@ -47,7 +47,7 @@ class IResolver(object): return next(valid_objects_gen, None) @staticmethod - def _search_object(directory: str, object_type, object_name: str, + def _search_object(directory: Path, object_type, object_name: str, kwargs: dict = {}) -> Optional[Any]: """ Search for the objectname in the given directory @@ -55,13 +55,13 @@ class IResolver(object): :return: object instance """ logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory) - for entry in os.listdir(directory): + for entry in directory.iterdir(): # Only consider python files - if not entry.endswith('.py'): + if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue obj = IResolver._get_valid_objects( - object_type, os.path.abspath(os.path.join(directory, entry)), object_name + object_type, Path.resolve(directory.joinpath(entry)), object_name ) if obj: return obj(**kwargs) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index f950a6e41..4576d0ec8 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -5,7 +5,6 @@ This module load custom strategies """ import inspect import logging -from os import getcwd, path import tempfile from base64 import urlsafe_b64decode from collections import OrderedDict @@ -103,16 +102,16 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - current_path = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'strategy') + current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() abs_paths = [ - path.join(getcwd(), 'user_data', 'strategies'), + Path.cwd().joinpath('user_data/strategies'), current_path, ] if extra_dir: # Add extra strategy directory on top of search paths - abs_paths.insert(0, extra_dir) + abs_paths.insert(0, Path(extra_dir).resolve()) if ":" in strategy_name: logger.info("loading base64 endocded strategy") @@ -125,17 +124,17 @@ class StrategyResolver(IResolver): temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8')) temp.joinpath("__init__.py").touch() - strategy_name = path.splitext(name)[0] + strategy_name = strat[0] # register temp path with the bot - abs_paths.insert(0, str(temp.resolve())) + abs_paths.insert(0, temp.resolve()) for _path in abs_paths: try: strategy = self._search_object(directory=_path, object_type=IStrategy, object_name=strategy_name, kwargs={'config': config}) if strategy: - logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) + logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path) strategy._populate_fun_len = len( inspect.getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len( @@ -145,7 +144,7 @@ class StrategyResolver(IResolver): return import_strategy(strategy, config=config) except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path) + logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 230ec7ab7..80bd9e120 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -2,6 +2,7 @@ import logging from base64 import urlsafe_b64encode from os import path +from pathlib import Path import warnings import pytest @@ -40,9 +41,7 @@ def test_import_strategy(caplog): def test_search_strategy(): default_config = {} - default_location = path.join(path.dirname( - path.realpath(__file__)), '..', '..', 'strategy' - ) + default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() assert isinstance( StrategyResolver._search_object( directory=default_location, From a3477e07eb223217bee5b244c60e82216540008b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 09:55:36 +0100 Subject: [PATCH 15/26] Remove constructor, it's not needed in the baseclass --- freqtrade/resolvers/iresolver.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 78df32e89..4bc2159fe 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -14,16 +14,9 @@ logger = logging.getLogger(__name__) class IResolver(object): """ - This class contains all the logic to load custom hyperopt class + This class contains all the logic to load custom classes """ - def __init__(self, object_type, config: Optional[Dict] = None) -> None: - """ - Load the custom class from config parameter - :param config: configuration dictionary or None - """ - config = config or {} - @staticmethod def _get_valid_objects(object_type, module_path: Path, object_name: str) -> Optional[Type[Any]]: @@ -54,7 +47,7 @@ class IResolver(object): :param directory: relative or absolute directory path :return: object instance """ - logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory) + logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory) for entry in directory.iterdir(): # Only consider python files if not str(entry).endswith('.py'): From 1d35428c8d530567916bf98a0b30f3a15ccf225a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 10:07:06 +0100 Subject: [PATCH 16/26] Rename get_valid_objects to get_valid object it only ever returns one object ... --- freqtrade/resolvers/iresolver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 4bc2159fe..aee292926 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Optional, Dict, Type, Any +from typing import Optional, Type, Any logger = logging.getLogger(__name__) @@ -18,14 +18,14 @@ class IResolver(object): """ @staticmethod - def _get_valid_objects(object_type, module_path: Path, - object_name: str) -> Optional[Type[Any]]: + def _get_valid_object(object_type, module_path: Path, + object_name: str) -> Optional[Type[Any]]: """ - Returns a list of all possible objects for the given module_path of type oject_type + Returns the first object with matching object_type and object_name in the path given. :param object_type: object_type (class) :param module_path: absolute path to the module :param object_name: Class name of the object - :return: Tuple with (name, class) or None + :return: class or None """ # Generate spec based on absolute path @@ -53,7 +53,7 @@ class IResolver(object): if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue - obj = IResolver._get_valid_objects( + obj = IResolver._get_valid_object( object_type, Path.resolve(directory.joinpath(entry)), object_name ) if obj: From e89df448e878a208a384fcafc2c58b94752548e7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 25 Nov 2018 13:34:08 +0100 Subject: [PATCH 17/26] Update ccxt from 1.17.535 to 1.17.536 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44cfe4a07..fa550195d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.535 +ccxt==1.17.536 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From fd7184718baa99cd328c4f0dacac98c7f79cd3c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 14:31:46 +0100 Subject: [PATCH 18/26] replace lambda with Magicmock in test --- freqtrade/tests/test_freqtradebot.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index a36ae2cd8..eb5336c61 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -292,7 +292,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, freqtrade = FreqtradeBot(edge_conf) freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) @@ -332,7 +332,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, freqtrade = FreqtradeBot(edge_conf) freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) @@ -1010,7 +1010,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade, value=(True, True)) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() @@ -1066,7 +1066,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade, value=(True, False)) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True + freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.create_trade() @@ -1099,7 +1099,7 @@ def test_handle_trade_experimental( freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() @@ -1580,7 +1580,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() @@ -1612,7 +1612,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() @@ -1674,7 +1674,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() @@ -1704,7 +1704,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True + freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.create_trade() @@ -1736,7 +1736,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, default_conf['trailing_stop'] = True freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() @@ -1771,7 +1771,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets default_conf['trailing_stop_positive'] = 0.01 freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() @@ -1831,7 +1831,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, default_conf['trailing_stop_positive_offset'] = 0.011 freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() trade = Trade.query.first() @@ -1890,7 +1890,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True + freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.create_trade() From 317eba2139f7fcc352b97c1de333a76ef18d27eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 14:36:02 +0100 Subject: [PATCH 19/26] Remove dual instanciation of pairinfo named tuple --- freqtrade/tests/conftest.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 63655126c..f7fe697b8 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -4,7 +4,6 @@ import logging from datetime import datetime from functools import reduce from typing import Dict, Optional -from collections import namedtuple from unittest.mock import MagicMock, PropertyMock import arrow @@ -13,7 +12,7 @@ from telegram import Chat, Message, Update from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange import Exchange -from freqtrade.edge import Edge +from freqtrade.edge import Edge, PairInfo from freqtrade.freqtradebot import FreqtradeBot logging.getLogger('').setLevel(logging.INFO) @@ -56,13 +55,11 @@ def patch_edge(mocker) -> None: # "LTC/BTC", # "XRP/BTC", # "NEO/BTC" - pair_info = namedtuple( - 'pair_info', - 'stoploss, winrate, risk_reward_ratio, required_risk_reward, expectancy') + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ - 'NEO/BTC': pair_info(-0.20, 0.66, 3.71, 0.50, 1.71), - 'LTC/BTC': pair_info(-0.21, 0.66, 3.71, 0.50, 1.71), + 'NEO/BTC': PairInfo(-0.20, 0.66, 3.71, 0.50, 1.71, 10, 25), + 'LTC/BTC': PairInfo(-0.21, 0.66, 3.71, 0.50, 1.71, 11, 20), } )) mocker.patch('freqtrade.edge.Edge.stoploss', MagicMock(return_value=-0.20)) From 745a5177958ad16e220dd0ba2d8dc982fddfc016 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 14:40:21 +0100 Subject: [PATCH 20/26] Fix comment pointing to wrong column --- freqtrade/exchange/exchange_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_helpers.py b/freqtrade/exchange/exchange_helpers.py index 8f4b03daf..84e68d4bb 100644 --- a/freqtrade/exchange/exchange_helpers.py +++ b/freqtrade/exchange/exchange_helpers.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) def parse_ticker_dataframe(ticker: list) -> DataFrame: """ Analyses the trend for the given ticker history - :param ticker: See exchange.get_candle_history + :param ticker: ticker list, as returned by exchange.async_get_candle_history :return: DataFrame """ cols = ['date', 'open', 'high', 'low', 'close', 'volume'] From 8a4361199266c3f558e6eb6b98947155ace77d57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 14:48:15 +0100 Subject: [PATCH 21/26] Remove get_candle_history (it's now async) convert sort-test to async --- freqtrade/exchange/__init__.py | 45 ------------- freqtrade/tests/exchange/test_exchange.py | 78 ++++------------------- 2 files changed, 12 insertions(+), 111 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index ae07e36e9..0f8d8895c 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -500,51 +500,6 @@ class Exchange(object): except ccxt.BaseError as e: raise OperationalException(f'Could not fetch ticker data. Msg: {e}') - @retrier - def get_candle_history(self, pair: str, tick_interval: str, - since_ms: Optional[int] = None) -> List[Dict]: - try: - # last item should be in the time interval [now - tick_interval, now] - till_time_ms = arrow.utcnow().shift( - minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] - ).timestamp * 1000 - # it looks as if some exchanges return cached data - # and they update it one in several minute, so 10 mins interval - # is necessary to skeep downloading of an empty array when all - # chached data was already downloaded - till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) - - data: List[Dict[Any, Any]] = [] - while not since_ms or since_ms < till_time_ms: - data_part = self._api.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) - - # Because some exchange sort Tickers ASC and other DESC. - # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) - # when GDAX returns a list of tickers DESC (newest first, oldest last) - data_part = sorted(data_part, key=lambda x: x[0]) - - if not data_part: - break - - logger.debug('Downloaded data for %s time range [%s, %s]', - pair, - arrow.get(data_part[0][0] / 1000).format(), - arrow.get(data_part[-1][0] / 1000).format()) - - data.extend(data_part) - since_ms = data[-1][0] + 1 - - return data - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {self._api.name} does not support fetching historical candlestick data.' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') - @retrier def cancel_order(self, order_id: str, pair: str) -> None: if self._conf['dry_run']: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 207f14efe..7fadb4cc1 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -875,64 +875,8 @@ def make_fetch_ohlcv_mock(data): return fetch_ohlcv_mock -def test_get_candle_history(default_conf, mocker): - api_mock = MagicMock() - tick = [ - [ - 1511686200000, # unix timestamp ms - 1, # open - 2, # high - 3, # low - 4, # close - 5, # volume (in quote currency) - ] - ] - type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) - api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - - # retrieve original ticker - ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) - assert ticks[0][0] == 1511686200000 - assert ticks[0][1] == 1 - assert ticks[0][2] == 2 - assert ticks[0][3] == 3 - assert ticks[0][4] == 4 - assert ticks[0][5] == 5 - - # change ticker and ensure tick changes - new_tick = [ - [ - 1511686210000, # unix timestamp ms - 6, # open - 7, # high - 8, # low - 9, # close - 10, # volume (in quote currency) - ] - ] - api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - - ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) - assert ticks[0][0] == 1511686210000 - assert ticks[0][1] == 6 - assert ticks[0][2] == 7 - assert ticks[0][3] == 8 - assert ticks[0][4] == 9 - assert ticks[0][5] == 10 - - ccxt_exceptionhandlers(mocker, default_conf, api_mock, - "get_candle_history", "fetch_ohlcv", - pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) - - with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) - - -def test_get_candle_history_sort(default_conf, mocker): +@pytest.mark.asyncio +async def test___async_get_candle_history_sort(default_conf, mocker): api_mock = MagicMock() # GDAX use-case (real data from GDAX) @@ -949,13 +893,14 @@ def test_get_candle_history_sort(default_conf, mocker): [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687], [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] ] - type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) # Test the ticker history sort - ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + assert res[0] == 'ETH/BTC' + ticks = res[1] assert ticks[0][0] == 1527830400000 assert ticks[0][1] == 0.07649 assert ticks[0][2] == 0.07651 @@ -984,11 +929,12 @@ def test_get_candle_history_sort(default_conf, mocker): [1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244], [1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783] ] - type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) - api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) # Test the ticker history sort - ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + assert res[0] == 'ETH/BTC' + ticks = res[1] + assert ticks[0][0] == 1527827700000 assert ticks[0][1] == 0.07659999 assert ticks[0][2] == 0.0766 From ebaf58b0fe465be049d46ef891c57746946ed4a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Nov 2018 15:00:50 +0100 Subject: [PATCH 22/26] Only sort data if necessary --- freqtrade/exchange/__init__.py | 4 +++- freqtrade/tests/exchange/test_exchange.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0f8d8895c..350c730a4 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -478,7 +478,9 @@ class Exchange(object): # Because some exchange sort Tickers ASC and other DESC. # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) # when GDAX returns a list of tickers DESC (newest first, oldest last) - data = sorted(data, key=lambda x: x[0]) + # Only sort if necessary to save computing time + if data and data[0][0] > data[-1][0]: + data = sorted(data, key=lambda x: x[0]) # keeping last candle time as last refreshed time of the pair if data: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 7fadb4cc1..dbb8d4ec2 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -877,7 +877,8 @@ def make_fetch_ohlcv_mock(data): @pytest.mark.asyncio async def test___async_get_candle_history_sort(default_conf, mocker): - api_mock = MagicMock() + def sort_data(data, key): + return sorted(data, key=key) # GDAX use-case (real data from GDAX) # This ticker history is ordered DESC (newest first, oldest last) @@ -893,14 +894,15 @@ async def test___async_get_candle_history_sort(default_conf, mocker): [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687], [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] ] - api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) exchange = get_patched_exchange(mocker, default_conf) exchange._api_async.fetch_ohlcv = get_mock_coro(tick) - + sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) # Test the ticker history sort res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert res[0] == 'ETH/BTC' ticks = res[1] + + assert sort_mock.call_count == 1 assert ticks[0][0] == 1527830400000 assert ticks[0][1] == 0.07649 assert ticks[0][2] == 0.07651 @@ -930,11 +932,14 @@ async def test___async_get_candle_history_sort(default_conf, mocker): [1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783] ] exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + # Reset sort mock + sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) # Test the ticker history sort res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert res[0] == 'ETH/BTC' ticks = res[1] - + # Sorted not called again - data is already in order + assert sort_mock.call_count == 0 assert ticks[0][0] == 1527827700000 assert ticks[0][1] == 0.07659999 assert ticks[0][2] == 0.0766 From 1ad5ccdfb0462e451da44089076b6c8b82739440 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 19:48:46 +0100 Subject: [PATCH 23/26] dry run condition when sell occurs --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a2db84a9..343036d19 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -781,6 +781,10 @@ class FreqtradeBot(object): sell_type = 'sell' if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' + + if self.config.get('dry_run', False) and sell_type == 'stoploss': + limit = trade.stop_loss + # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From 16eec078d78fcdc8e5948d45998dac2536e9a902 Mon Sep 17 00:00:00 2001 From: Pan Long Date: Mon, 26 Nov 2018 09:18:29 +0800 Subject: [PATCH 24/26] Use dot to access attribute in NamedTuple This should fix the crash in #1359 --- freqtrade/wallets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index b8b37907d..bf6f8b027 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -35,8 +35,8 @@ class Wallets(object): return 999.9 balance = self.wallets.get(currency) - if balance and balance['free']: - return balance['free'] + if balance and balance.free: + return balance.free else: return 0 From ad8592f3168c6d4fa908e3d9fae615f9490f9ae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Nov 2018 06:22:53 +0100 Subject: [PATCH 25/26] Test live mode of get_free --- freqtrade/tests/test_wallets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/tests/test_wallets.py b/freqtrade/tests/test_wallets.py index e6a17ecbf..88366a869 100644 --- a/freqtrade/tests/test_wallets.py +++ b/freqtrade/tests/test_wallets.py @@ -30,6 +30,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets.wallets['GAS'].free == 0.260739 assert freqtrade.wallets.wallets['GAS'].used == 0.0 assert freqtrade.wallets.wallets['GAS'].total == 0.260739 + assert freqtrade.wallets.get_free('BNT') == 1.0 mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -56,6 +57,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets.wallets['GAS'].free == 0.270739 assert freqtrade.wallets.wallets['GAS'].used == 0.1 assert freqtrade.wallets.wallets['GAS'].total == 0.260439 + assert freqtrade.wallets.get_free('GAS') == 0.270739 def test_sync_wallet_missing_data(mocker, default_conf): @@ -84,3 +86,4 @@ def test_sync_wallet_missing_data(mocker, default_conf): assert freqtrade.wallets.wallets['GAS'].free == 0.260739 assert freqtrade.wallets.wallets['GAS'].used is None assert freqtrade.wallets.wallets['GAS'].total == 0.260739 + assert freqtrade.wallets.get_free('GAS') == 0.260739 From d3712c6e4035552e62490e31d8e422944a36abc6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 13:34:05 +0100 Subject: [PATCH 26/26] Update ccxt from 1.17.536 to 1.17.539 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa550195d..6f969be06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.536 +ccxt==1.17.539 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1