Merge branch 'develop' of https://github.com/freqtrade/freqtrade into develop

This commit is contained in:
misagh 2018-11-25 19:48:51 +01:00
commit 59fc67f85b
20 changed files with 202 additions and 203 deletions

View File

@ -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 <yourexchange> 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

View File

@ -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
@ -337,9 +337,7 @@ 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
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())

View File

@ -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__)

View File

@ -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__)

View File

@ -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__)

View File

@ -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

View File

@ -0,0 +1,3 @@
from freqtrade.resolvers.iresolver import IResolver # noqa: F401
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401

View File

@ -0,0 +1,64 @@
# pragma pylint: disable=attribute-defined-outside-init
"""
This module load custom hyperopts
"""
import logging
from pathlib 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(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [
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, Path(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)
)

View File

@ -0,0 +1,61 @@
# pragma pylint: disable=attribute-defined-outside-init
"""
This module load custom objects
"""
import importlib.util
import inspect
import logging
from pathlib import Path
from typing import Optional, Type, Any
logger = logging.getLogger(__name__)
class IResolver(object):
"""
This class contains all the logic to load custom classes
"""
@staticmethod
def _get_valid_object(object_type, module_path: Path,
object_name: str) -> Optional[Type[Any]]:
"""
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: class or None
"""
# Generate spec based on absolute 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
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: Path, 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 directory.iterdir():
# Only consider python files
if not str(entry).endswith('.py'):
logger.debug('Ignoring %s', entry)
continue
obj = IResolver._get_valid_object(
object_type, Path.resolve(directory.joinpath(entry)), object_name
)
if obj:
return obj(**kwargs)
return None

View File

@ -3,24 +3,23 @@
"""
This module load custom strategies
"""
import importlib.util
import inspect
import logging
import os
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,15 +102,16 @@ 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 = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = [
os.path.join(os.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")
@ -124,16 +124,17 @@ class StrategyResolver(object):
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 = 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:
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)
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(
@ -143,49 +144,9 @@ class StrategyResolver(object):
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"
" 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

View File

@ -45,6 +45,12 @@ def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
return exchange
def patch_wallet(mocker, free=999.9) -> None:
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(
return_value=free
))
def patch_edge(mocker) -> None:
# "ETH/BTC",
# "LTC/BTC",

View File

@ -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

View File

@ -2,6 +2,7 @@
import logging
from base64 import urlsafe_b64encode
from os import path
from pathlib import Path
import warnings
import pytest
@ -10,7 +11,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):
@ -40,21 +41,21 @@ 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_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 +78,7 @@ def test_load_strategy_invalid_directory(result, caplog):
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert (
'freqtrade.strategy.resolver',
'freqtrade.resolvers.strategy_resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
@ -128,7 +129,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.strategy_resolver',
logging.INFO,
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
) in caplog.record_tuples
@ -143,7 +144,7 @@ def test_strategy_override_stoploss(caplog):
resolver = StrategyResolver(config)
assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.strategy.resolver',
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'stoploss' with value in config file: -0.5."
) in caplog.record_tuples
@ -159,7 +160,7 @@ def test_strategy_override_ticker_interval(caplog):
resolver = StrategyResolver(config)
assert resolver.strategy.ticker_interval == 60
assert ('freqtrade.strategy.resolver',
assert ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"Override strategy 'ticker_interval' with value in config file: 60."
) in caplog.record_tuples
@ -175,7 +176,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.strategy_resolver',
logging.INFO,
"Override process_only_new_candles 'process_only_new_candles' "
"with value in config file: True."
@ -201,7 +202,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.strategy_resolver',
logging.INFO,
"Override strategy 'order_types' with value in config file:"
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}."

View File

@ -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']

View File

@ -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
@ -181,17 +181,10 @@ 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)
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 +199,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 +514,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
)

View File

@ -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={

View File

@ -29,6 +29,17 @@ class Wallets(object):
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()

View File

@ -1,4 +1,4 @@
ccxt==1.17.522
ccxt==1.17.536
SQLAlchemy==1.2.14
python-telegram-bot==11.1.0
arrow==0.12.1
@ -7,13 +7,13 @@ 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
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

View File

@ -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] = {}

View File

@ -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