diff --git a/README.md b/README.md index 24e01531c..929d40292 100644 --- a/README.md +++ b/README.md @@ -22,33 +22,10 @@ expect. We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot. -## Table of Contents -- [Features](#features) -- [Quick start](#quick-start) -- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) -- [Support](#support) - - [Help](#help--slack) - - [Bugs](#bugs--issues) - - [Feature Requests](#feature-requests) - - [Pull Requests](#pull-requests) -- [Basic Usage](#basic-usage) - - [Bot commands](#bot-commands) - - [Telegram RPC commands](#telegram-rpc-commands) -- [Requirements](#requirements) - - [Min hardware required](#min-hardware-required) - - [Software requirements](#software-requirements) - -## Branches -The project is currently setup in two main branches: -- `develop` - This branch has often new features, but might also cause -breaking changes. -- `master` - This branch contains the latest stable release. The bot -'should' be stable on this branch, and is generally well tested. +## Exchange marketplaces supported +- [X] [Bittrex](https://bittrex.com/) +- [X] [Binance](https://www.binance.com/) +- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ## Features - [x] **Based on Python 3.6+**: For botting on any operating system - @@ -65,74 +42,50 @@ strategy parameters with real exchange data. - [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. - [x] **Performance status report**: Provide a performance status of your current trades. -### Exchange marketplaces supported -- [X] [Bittrex](https://bittrex.com/) -- [X] [Binance](https://www.binance.com/) -- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +## Table of Contents +- [Quick start](#quick-start) +- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) + - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) + - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) + - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) + - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) + - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) +- [Basic Usage](#basic-usage) + - [Bot commands](#bot-commands) + - [Telegram RPC commands](#telegram-rpc-commands) +- [Support](#support) + - [Help](#help--slack) + - [Bugs](#bugs--issues) + - [Feature Requests](#feature-requests) + - [Pull Requests](#pull-requests) +- [Requirements](#requirements) + - [Min hardware required](#min-hardware-required) + - [Software requirements](#software-requirements) ## Quick start -This quick start section is a very short explanation on how to test the -bot in dry-run. We invite you to read the -[bot documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) -to ensure you understand how the bot is working. - -### Easy installation -The script below will install all dependencies and help you to configure the bot. -```bash -./setup.sh --install -``` - -### Manual installation -The following steps are made for Linux/MacOS environment - -**1. Clone the repo** +Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. ```bash git clone git@github.com:freqtrade/freqtrade.git git checkout develop cd freqtrade +./setup.sh --install ``` -**2. Create the config file** -Switch `"dry_run": true,` -```bash -cp config.json.example config.json -vi config.json -``` -**3. Build your docker image and run it** -```bash -docker build -t freqtrade . -docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` +_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_ -### Help / Slack -For any questions not covered by the documentation or for further -information about the bot, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). +## Documentation +We invite you to read the bot documentation to ensure you understand how the bot is working. +- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) +- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) +- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) +- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md) + - [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) + - [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) + - [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) +- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) +- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) +- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) -### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -If you discover a bug in the bot, please -[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -first. If it hasn't been reported, please -[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and -ensure you follow the template guide so that our team can assist you as -quickly as possible. - -### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) -Have you a great idea to improve the bot you want to share? Please, -first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). -If it hasn't been requested, please -[create a new request](https://github.com/freqtrade/freqtrade/issues/new) -and ensure you follow the template guide so that it does not get lost -in the bug reports. - -### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) -Feel like our bot is missing a feature? We welcome your pull requests! -Please read our -[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) -to understand the requirements before sending your pull-requests. - -**Important:** Always create your PR against the `develop` branch, not -`master`. ## Basic Usage @@ -170,11 +123,7 @@ optional arguments: "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is enabled. ``` -More details on: -- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) -- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) -- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) - + ### Telegram RPC commands Telegram is not mandatory. However, this is a great way to control your bot. More details on our @@ -193,6 +142,48 @@ bot. More details on our - `/help`: Show help message - `/version`: Show version + +## Development branches +The project is currently setup in two main branches: +- `develop` - This branch has often new features, but might also cause +breaking changes. +- `master` - This branch contains the latest stable release. The bot +'should' be stable on this branch, and is generally well tested. + + +## Support +### Help / Slack +For any questions not covered by the documentation or for further +information about the bot, we encourage you to join our slack channel. +- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). + +### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +If you discover a bug in the bot, please +[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +first. If it hasn't been reported, please +[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and +ensure you follow the template guide so that our team can assist you as +quickly as possible. + +### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) +Have you a great idea to improve the bot you want to share? Please, +first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). +If it hasn't been requested, please +[create a new request](https://github.com/freqtrade/freqtrade/issues/new) +and ensure you follow the template guide so that it does not get lost +in the bug reports. + +### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) +Feel like our bot is missing a feature? We welcome your pull requests! +Please read our +[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) +to understand the requirements before sending your pull-requests. + +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. + +**Important:** Always create your PR against the `develop` branch, not +`master`. + ## Requirements ### Min hardware required diff --git a/config.json.example b/config.json.example index d3dbeb52e..e5bdc95b1 100644 --- a/config.json.example +++ b/config.json.example @@ -31,7 +31,8 @@ }, "experimental": { "use_sell_signal": false, - "sell_profit_only": false + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false }, "telegram": { "enabled": true, diff --git a/config_full.json.example b/config_full.json.example index c17d22a15..231384acc 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -38,7 +38,8 @@ }, "experimental": { "use_sell_signal": false, - "sell_profit_only": false + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false }, "telegram": { "enabled": true, diff --git a/docs/configuration.md b/docs/configuration.md index d5d53860b..5279ed06e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,7 +16,7 @@ The table below will list all configuration parameters. |----------|---------|----------|-------------| | `max_open_trades` | 3 | Yes | Number of trades open your bot will have. | `stake_currency` | BTC | Yes | Crypto-currency used for trading. -| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. +| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance. | `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. @@ -31,6 +31,7 @@ The table below will list all configuration parameters. | `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`. | `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision. +| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal` | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. @@ -43,6 +44,13 @@ The table below will list all configuration parameters. The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205). +### Understand stake_amount +`stake_amount` is an amount of crypto-currency your bot will use for each trade. +The minimal value is 0.0005. If there is not enough crypto-currency in +the account an exception is generated. +To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`. +In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`. + ### Understand minimal_roi `minimal_roi` is a JSON object where the key is a duration in minutes and the value is the minimum ROI in percent. diff --git a/docs/installation.md b/docs/installation.md index 87d3dc5cf..e5724a7dc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,6 +8,7 @@ To understand how to set up the bot please read the [Bot Configuration](https:// * [Table of Contents](#table-of-contents) * [Easy Installation - Linux Script](#easy-installation---linux-script) +* [Manual installation](#manual-installation) * [Automatic Installation - Docker](#automatic-installation---docker) * [Custom Linux MacOS Installation](#custom-installation) - [Requirements](#requirements) @@ -55,6 +56,28 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. + +## Manual installation - Linux/MacOS +The following steps are made for Linux/MacOS environment + +**1. Clone the repo** +```bash +git clone git@github.com:freqtrade/freqtrade.git +git checkout develop +cd freqtrade +``` +**2. Create the config file** +Switch `"dry_run": true,` +```bash +cp config.json.example config.json +vi config.json +``` +**3. Build your docker image and run it** +```bash +docker build -t freqtrade . +docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + ------ ## Automatic Installation - Docker diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 7cf0fa996..ac00264f0 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.17.0' +__version__ = '0.17.1' class DependencyException(BaseException): diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index cf2d5519b..36e00dd0e 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -98,6 +98,13 @@ class Analyze(object): """ return self.strategy.ticker_interval + def get_stoploss(self) -> float: + """ + Return stoploss to use + :return: Strategy stoploss value to use + """ + return self.strategy.stoploss + def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame @@ -172,33 +179,45 @@ class Analyze(object): if the threshold is reached and updates the trade record. :return: True if trade should be sold, False otherwise """ + current_profit = trade.calc_profit_percent(rate) + if self.stop_loss_reached(current_profit=current_profit): + return True + + experimental = self.config.get('experimental', {}) + + if buy and experimental.get('ignore_roi_if_buy_signal', False): + logger.debug('Buy signal still active - not selling.') + return False + # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date): + if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): logger.debug('Required profit reached. Selling..') return True - # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) - if self.config.get('experimental', {}).get('sell_profit_only', False): + if experimental.get('sell_profit_only', False): logger.debug('Checking if trade is profitable..') if trade.calc_profit(rate=rate) <= 0: return False - - if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False): + if sell and not buy and experimental.get('use_sell_signal', False): logger.debug('Sell signal received. Selling..') return True return False - def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool: + def stop_loss_reached(self, current_profit: float) -> bool: + """Based on current profit of the trade and configured stoploss, decides to sell or not""" + + if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: + logger.debug('Stop loss hit.') + return True + return False + + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ - current_profit = trade.calc_profit_percent(current_rate) - if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: - logger.debug('Stop loss hit.') - return True # Check if time matches and current rate is above threshold time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index b392fb53e..8ce7c546d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -262,17 +262,15 @@ class Arguments(object): stop: int = 0 if stype[0]: starts = rvals[index] - if stype[0] == 'date': - start = int(starts) if len(starts) == 10 \ - else arrow.get(starts, 'YYYYMMDD').timestamp + if stype[0] == 'date' and len(starts) == 8: + start = arrow.get(starts, 'YYYYMMDD').timestamp else: start = int(starts) index += 1 if stype[1]: stops = rvals[index] - if stype[1] == 'date': - stop = int(stops) if len(stops) == 10 \ - else arrow.get(stops, 'YYYYMMDD').timestamp + if stype[1] == 'date' and len(stops) == 8: + stop = arrow.get(stops, 'YYYYMMDD').timestamp else: stop = int(stops) return TimeRange(stype[0], stype[1], start, stop) @@ -336,3 +334,10 @@ class Arguments(object): nargs='+', dest='timeframes', ) + + self.parser.add_argument( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes', + dest='erase', + action='store_true' + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 256f1bfe1..8b7d29f16 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,8 @@ RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' +UNLIMITED_STAKE_AMOUNT = 'unlimited' + TICKER_INTERVAL_MINUTES = { '1m': 1, @@ -44,7 +46,11 @@ CONF_SCHEMA = { 'max_open_trades': {'type': 'integer', 'minimum': 0}, 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']}, - 'stake_amount': {'type': 'number', 'minimum': 0.0005}, + 'stake_amount': { + "type": ["number", "string"], + "minimum": 0.0005, + "pattern": UNLIMITED_STAKE_AMOUNT + }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'minimal_roi': { @@ -73,7 +79,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'} + 'sell_profit_only': {'type': 'boolean'}, + "ignore_roi_if_buy_signal_true": {'type': 'boolean'} } }, 'telegram': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02375ca80..e25ed66cf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -244,14 +244,66 @@ class FreqtradeBot(object): balance = self.config['bid_strategy']['ask_last_balance'] return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) + def _get_trade_stake_amount(self) -> Optional[float]: + stake_amount = self.config['stake_amount'] + avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) + + if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: + open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) + if open_trades >= self.config['max_open_trades']: + logger.warning('Can\'t open a new trade: max number of trades is reached') + return None + return avaliable_amount / (self.config['max_open_trades'] - open_trades) + + # Check if stake_amount is fulfilled + if avaliable_amount < stake_amount: + raise DependencyException( + 'Available balance(%f %s) is lower than stake amount(%f %s)' % ( + avaliable_amount, self.config['stake_currency'], + stake_amount, self.config['stake_currency']) + ) + + return stake_amount + + def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]: + markets = self.exchange.get_markets() + markets = [m for m in markets if m['symbol'] == pair] + if not markets: + raise ValueError(f'Can\'t get market information for symbol {pair}') + + market = markets[0] + + if 'limits' not in market: + return None + + min_stake_amounts = [] + if 'cost' in market['limits'] and 'min' in market['limits']['cost']: + min_stake_amounts.append(market['limits']['cost']['min']) + + if 'amount' in market['limits'] and 'min' in market['limits']['amount']: + min_stake_amounts.append(market['limits']['amount']['min'] * price) + + if not min_stake_amounts: + return None + + amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss + if self.analyze.get_stoploss() is not None: + amount_reserve_percent += self.analyze.get_stoploss() + # it should not be more than 50% + amount_reserve_percent = max(amount_reserve_percent, 0.5) + return min(min_stake_amounts)/amount_reserve_percent + def create_trade(self) -> bool: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :return: True if a trade object has been created and persisted, False otherwise """ - stake_amount = self.config['stake_amount'] interval = self.analyze.get_ticker_interval() + stake_amount = self._get_trade_stake_amount() + + if not stake_amount: + return False stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] exc_name = self.exchange.name @@ -261,10 +313,6 @@ class FreqtradeBot(object): stake_amount ) whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist']) - # Check if stake_amount is fulfilled - if self.exchange.get_balance(stake_currency) < stake_amount: - raise DependencyException( - f'stake amount is not fulfilled (currency={stake_currency})') # Remove currently opened and latest pairs from whitelist for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): @@ -285,8 +333,18 @@ class FreqtradeBot(object): return False pair_s = pair.replace('_', '/') pair_url = self.exchange.get_pair_detail_url(pair) + # Calculate amount buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) + + min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit) + if min_stake_amount is not None and min_stake_amount > stake_amount: + logger.warning( + f'Can\'t open a new trade for {pair_s}: stake amount' + f' is too small ({stake_amount} < {min_stake_amount})' + ) + return False + amount = stake_amount / buy_limit order_id = self.exchange.buy(pair, buy_limit, amount)['id'] @@ -423,8 +481,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ current_rate = self.exchange.get_ticker(trade.pair)['bid'] (buy, sell) = (False, False) - - if self.config.get('experimental', {}).get('use_sell_signal'): + experimental = self.config.get('experimental', {}) + if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): (buy, sell) = self.analyze.get_signal(self.exchange, trade.pair, self.analyze.get_ticker_interval()) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ffb808a24..6982b36cb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,6 +14,7 @@ from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize +from freqtrade import constants, DependencyException from freqtrade.exchange import Exchange from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments @@ -341,6 +342,10 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]: config['exchange']['key'] = '' config['exchange']['secret'] = '' + if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: + raise DependencyException('stake amount could not be "%s" for backtesting' % + constants.UNLIMITED_STAKE_AMOUNT) + return config diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 19e732d7b..7a313a3ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -39,7 +39,6 @@ class Hyperopt(Backtesting): hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: - super().__init__(config) # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 7fd8fdeb9..20a7db4bb 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -21,7 +21,6 @@ from freqtrade import OperationalException logger = logging.getLogger(__name__) -_CONF = {} _DECL_BASE: Any = declarative_base() @@ -33,9 +32,7 @@ def init(config: Dict) -> None: :param config: config to use :return: None """ - _CONF.update(config) - - db_url = _CONF.get('db_url', None) + db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -61,7 +58,7 @@ def init(config: Dict) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if _CONF.get('dry_run', False) and db_url != 'sqlite://': + if config.get('dry_run', False) and db_url != 'sqlite://': clean_dry_run_db() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e69de29bb..e1dc7bb3f 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -0,0 +1,32 @@ +import logging +from copy import deepcopy + +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +def import_strategy(strategy: IStrategy) -> IStrategy: + """ + Imports given Strategy instance to global scope + of freqtrade.strategy and returns an instance of it + """ + # Copy all attributes from base class and class + attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__}) + # Adjust module name + attr['__module__'] = 'freqtrade.strategy' + + name = strategy.__class__.__name__ + clazz = type(name, (IStrategy,), attr) + + logger.debug( + 'Imported strategy %s.%s as %s.%s', + strategy.__module__, strategy.__class__.__name__, + clazz.__module__, strategy.__class__.__name__, + ) + + # Modify global scope to declare class + globals()[name] = clazz + + return clazz() diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 3fd39bca3..0dcd3fc6a 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -11,6 +11,7 @@ from collections import OrderedDict from typing import Optional, Dict, Type from freqtrade import constants +from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -71,7 +72,7 @@ class StrategyResolver(object): """ current_path = os.path.dirname(os.path.realpath(__file__)) abs_paths = [ - os.path.join(current_path, '..', '..', 'user_data', 'strategies'), + os.path.join(os.getcwd(), 'user_data', 'strategies'), current_path, ] @@ -80,10 +81,13 @@ class StrategyResolver(object): abs_paths.insert(0, extra_dir) for path in abs_paths: - strategy = self._search_strategy(path, strategy_name) - if strategy: - logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) - return strategy + try: + strategy = self._search_strategy(path, strategy_name) + if strategy: + logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) + return import_strategy(strategy) + except FileNotFoundError: + logger.warning('Path "%s" does not exist', path) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" @@ -100,7 +104,7 @@ class StrategyResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('user_data.strategies', module_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 diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 25a9b8d00..c8f9f3c3c 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -198,7 +198,10 @@ def markets(): 'max': 1000, }, 'price': 500000, - 'cost': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, }, 'info': '', }, @@ -220,7 +223,10 @@ def markets(): 'max': 1000, }, 'price': 500000, - 'cost': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, }, 'info': '', }, @@ -242,7 +248,85 @@ def markets(): 'max': 1000, }, 'price': 500000, - 'cost': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, + }, + 'info': '', + }, + { + 'id': 'ltcbtc', + 'symbol': 'LTC/BTC', + 'base': 'LTC', + 'quote': 'BTC', + 'active': False, + 'precision': { + 'price': 8, + 'amount': 8, + 'cost': 8, + }, + 'lot': 0.00000001, + 'limits': { + 'amount': { + 'min': 0.01, + 'max': 1000, + }, + 'price': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, + }, + 'info': '', + }, + { + 'id': 'xrpbtc', + 'symbol': 'XRP/BTC', + 'base': 'XRP', + 'quote': 'BTC', + 'active': False, + 'precision': { + 'price': 8, + 'amount': 8, + 'cost': 8, + }, + 'lot': 0.00000001, + 'limits': { + 'amount': { + 'min': 0.01, + 'max': 1000, + }, + 'price': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, + }, + 'info': '', + }, + { + 'id': 'neobtc', + 'symbol': 'NEO/BTC', + 'base': 'NEO', + 'quote': 'BTC', + 'active': False, + 'precision': { + 'price': 8, + 'amount': 8, + 'cost': 8, + }, + 'lot': 0.00000001, + 'limits': { + 'amount': { + 'min': 0.01, + 'max': 1000, + }, + 'price': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, }, 'info': '', } diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 3c68e8493..53e59b34b 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -510,7 +510,6 @@ def test_cancel_order_dry_run(default_conf, mocker): # Ensure that if not dry_run, we should call API def test_cancel_order(default_conf, mocker): default_conf['dry_run'] = False - # mocker.patch.dict('freqtrade.exchange.._CONF', default_conf) api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=123) exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -673,7 +672,7 @@ def test_get_markets(default_conf, mocker, markets): exchange = get_patched_exchange(mocker, default_conf, api_mock) ret = exchange.get_markets() assert isinstance(ret, list) - assert len(ret) == 3 + assert len(ret) == 6 assert ret[0]["id"] == "ethbtc" assert ret[0]["symbol"] == "ETH/BTC" diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 65aa00a70..c3d2ad572 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,6 +3,7 @@ import json import math import random +import pytest from copy import deepcopy from typing import List from unittest.mock import MagicMock @@ -11,7 +12,7 @@ import numpy as np import pandas as pd from arrow import Arrow -from freqtrade import optimize +from freqtrade import optimize, constants, DependencyException from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments, TimeRange from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration @@ -268,6 +269,28 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non ) +def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: + """ + Test setup_configuration() function + """ + + conf = deepcopy(default_conf) + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(conf) + )) + + args = [ + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + 'backtesting' + ] + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + setup_configuration(get_args(args)) + + def test_start(mocker, fee, default_conf, caplog) -> None: """ Test start() function diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 9df4c65b3..598587fb2 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -26,7 +26,7 @@ def prec_satoshi(a, b) -> float: # Unit tests -def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: +def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: """ Test rpc_trade_status() method """ @@ -37,7 +37,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) now = arrow.utcnow() @@ -74,7 +75,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: def test_rpc_daily_profit(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, markets, mocker) -> None: """ Test rpc_daily_profit() method """ @@ -85,7 +86,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -125,7 +127,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, markets, mocker) -> None: """ Test rpc_trade_statistics() method """ @@ -140,7 +142,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -200,7 +203,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, +def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, ticker_sell_up, limit_buy_order, limit_sell_order): """ Test rpc_trade_statistics() method @@ -216,7 +219,8 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -358,7 +362,7 @@ def test_rpc_stop(mocker, default_conf) -> None: assert freqtradebot.state == State.STOPPED -def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: +def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: """ Test rpc_forcesell() method """ @@ -380,6 +384,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: } ), get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -461,7 +466,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: def test_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: + limit_sell_order, markets, mocker) -> None: """ Test rpc_performance() method """ @@ -473,7 +478,8 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, validate_pairs=MagicMock(), get_balances=MagicMock(return_value=ticker), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -499,7 +505,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert prec_satoshi(res[0]['profit'], 6.2) -def test_rpc_count(mocker, default_conf, ticker, fee) -> None: +def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: """ Test rpc_count() method """ @@ -512,6 +518,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 439a662e6..bcf7c8e32 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -185,7 +185,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: ) -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +def test_status_handle(default_conf, markets, update, ticker, fee, mocker) -> None: """ Test _status() method """ @@ -196,6 +196,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() status_table = MagicMock() @@ -231,7 +232,8 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0] -def test_status_table_handle(default_conf, limit_buy_order, update, ticker, fee, mocker) -> None: +def test_status_table_handle( + default_conf, markets, limit_buy_order, update, ticker, fee, mocker) -> None: """ Test _status_table() method """ @@ -244,6 +246,7 @@ def test_status_table_handle(default_conf, limit_buy_order, update, ticker, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -285,7 +288,7 @@ def test_status_table_handle(default_conf, limit_buy_order, update, ticker, fee, def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: + limit_sell_order, markets, mocker) -> None: """ Test _daily() method """ @@ -299,7 +302,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -397,7 +401,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, markets, mocker) -> None: """ Test _profit() method """ @@ -408,7 +412,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -661,7 +666,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: assert 'reloading config' in msg_mock.call_args_list[0][0][0] -def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: +def test_forcesell_handle(default_conf, update, ticker, fee, + ticker_sell_up, markets, mocker) -> None: """ Test _forcesell() method """ @@ -674,7 +680,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -701,7 +708,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] -def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, mocker) -> None: +def test_forcesell_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, markets, mocker) -> None: """ Test _forcesell() method """ @@ -714,7 +722,8 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -745,7 +754,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] -def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None: +def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: """ Test _forcesell() method """ @@ -759,7 +768,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -823,7 +833,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, markets, mocker) -> None: """ Test _performance() method """ @@ -839,7 +849,8 @@ def test_performance_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) @@ -887,7 +898,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None: assert 'not running' in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: +def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: """ Test _count() method """ @@ -903,7 +914,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - buy=MagicMock(return_value={'id': 'mocked_order_id'}) + buy=MagicMock(return_value={'id': 'mocked_order_id'}), + get_markets=markets ) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) freqtradebot = FreqtradeBot(default_conf) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 244910790..1e082c380 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,14 +1,39 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 - import logging import os import pytest +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 +def test_import_strategy(caplog): + caplog.set_level(logging.DEBUG) + + strategy = DefaultStrategy() + strategy.some_method = lambda *args, **kwargs: 42 + + assert strategy.__module__ == 'freqtrade.strategy.default_strategy' + assert strategy.some_method() == 42 + + imported_strategy = import_strategy(strategy) + + assert dir(strategy) == dir(imported_strategy) + + assert imported_strategy.__module__ == 'freqtrade.strategy' + assert imported_strategy.some_method() == 42 + + assert ( + 'freqtrade.strategy', + logging.DEBUG, + 'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy ' + 'as freqtrade.strategy.DefaultStrategy', + ) in caplog.record_tuples + + def test_search_strategy(): default_location = os.path.join(os.path.dirname( os.path.realpath(__file__)), '..', '..', 'strategy' @@ -20,19 +45,21 @@ def test_search_strategy(): def test_load_strategy(result): - resolver = StrategyResolver() - resolver._load_strategy('TestStrategy') + resolver = StrategyResolver({'strategy': 'TestStrategy'}) assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.populate_indicators(result) -def test_load_strategy_custom_directory(result): +def test_load_strategy_invalid_directory(result, caplog): resolver = StrategyResolver() extra_dir = os.path.join('some', 'path') - with pytest.raises( - FileNotFoundError, - match=r".*No such file or directory: '{}'".format(extra_dir)): - resolver._load_strategy('TestStrategy', extra_dir) + resolver._load_strategy('TestStrategy', extra_dir) + + assert ( + 'freqtrade.strategy.resolver', + logging.WARNING, + 'Path "{}" does not exist'.format(extra_dir), + ) in caplog.record_tuples assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.populate_indicators(result) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 019c0c09d..a4096cc01 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -55,6 +55,18 @@ def test_load_config_missing_attributes(default_conf) -> None: configuration._validate_config(conf) +def test_load_config_incorrect_stake_amount(default_conf) -> None: + """ + Test the configuration validator with a missing attribute + """ + conf = deepcopy(default_conf) + conf['stake_amount'] = 'fake' + + with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): + configuration = Configuration(Namespace()) + configuration._validate_config(conf) + + def test_load_config_file(default_conf, mocker, caplog) -> None: """ Test Configuration._load_config_file() method diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9eae2fdf5..1cb2bfca2 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -14,7 +14,7 @@ import arrow import pytest import requests -from freqtrade import DependencyException, OperationalException, TemporaryError +from freqtrade import constants, DependencyException, OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.state import State @@ -216,7 +216,210 @@ def test_refresh_whitelist() -> None: pass -def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: + """ + Test get_trade_stake_amount() method + """ + + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) + ) + + freqtrade = FreqtradeBot(default_conf) + + result = freqtrade._get_trade_stake_amount() + assert(result == default_conf['stake_amount']) + + +def test_get_trade_stake_amount_no_stake_amount(default_conf, + ticker, + limit_buy_order, + fee, + mocker) -> None: + """ + Test get_trade_stake_amount() method + """ + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) + ) + + # test defined stake amount + freqtrade = FreqtradeBot(default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade._get_trade_stake_amount() + + +def test_get_trade_stake_amount_unlimited_amount(default_conf, + ticker, + limit_buy_order, + fee, + markets, + mocker) -> None: + """ + Test get_trade_stake_amount() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + 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 + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + conf['max_open_trades'] = 2 + + freqtrade = FreqtradeBot(conf) + + # no open trades, order amount should be 'balance / max_open_trades' + result = freqtrade._get_trade_stake_amount() + assert result == default_conf['stake_amount'] / conf['max_open_trades'] + + # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' + freqtrade.create_trade() + + result = freqtrade._get_trade_stake_amount() + assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) + + # create 2 trades, order amount should be None + freqtrade.create_trade() + + result = freqtrade._get_trade_stake_amount() + assert result is None + + # set max_open_trades = None, so do not trade + conf['max_open_trades'] = 0 + freqtrade = FreqtradeBot(conf) + result = freqtrade._get_trade_stake_amount() + assert result is None + + +def test_get_min_pair_stake_amount(mocker, default_conf) -> None: + """ + Test get_trade_stake_amount() method + """ + + patch_RPCManager(mocker) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.freqtradebot.Analyze.get_stoploss', MagicMock(return_value=-0.05)) + freqtrade = FreqtradeBot(default_conf) + + # no pair found + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC' + }]) + ) + with pytest.raises(ValueError, match=r'.*get market information.*'): + freqtrade._get_min_pair_stake_amount('BNB/BTC', 1) + + # no 'limits' section + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC' + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) + assert result is None + + # empty 'limits' section + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': {} + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) + assert result is None + + # empty 'cost'/'amount' section + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {}, + 'amount': {} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) + assert result is None + + # min cost is set + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {'min': 2}, + 'amount': {} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) + assert result == 2 / 0.9 + + # min amount is set + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {}, + 'amount': {'min': 2} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) + assert result == 2 * 2 / 0.9 + + # min amount and cost are set (cost is minimal) + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {'min': 2}, + 'amount': {'min': 2} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) + assert result == min(2, 2 * 2) / 0.9 + + # min amount and cost are set (amount is minial) + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {'min': 8}, + 'amount': {'min': 2} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) + assert result == min(8, 2 * 2) / 0.9 + + +def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -229,6 +432,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) # Save state of current whitelist @@ -252,32 +456,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert whitelist == default_conf['exchange']['pair_whitelist'] -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) - patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - buy=buy_mock, - get_fee=fee, - ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = 0.0005 - freqtrade = FreqtradeBot(conf) - - freqtrade.create_trade() - rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] - assert rate * amount >= conf['stake_amount'] - - -def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -291,6 +471,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee 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 ) freqtrade = FreqtradeBot(default_conf) @@ -298,7 +479,87 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee freqtrade.create_trade() -def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: + """ + Test create_trade() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=buy_mock, + get_fee=fee, + get_markets=markets + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = 0.0005 + freqtrade = FreqtradeBot(conf) + + freqtrade.create_trade() + rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] + assert rate * amount >= conf['stake_amount'] + + +def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: + """ + Test create_trade() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=buy_mock, + get_fee=fee, + get_markets=markets + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = 0.000000005 + freqtrade = FreqtradeBot(conf) + + result = freqtrade.create_trade() + assert result is False + + +def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: + """ + Test create_trade() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + 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 + ) + conf = deepcopy(default_conf) + conf['max_open_trades'] = 0 + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + + freqtrade = FreqtradeBot(conf) + + assert freqtrade.create_trade() is False + assert freqtrade._get_trade_stake_amount() is None + + +def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -311,6 +572,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) @@ -325,7 +587,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, - limit_buy_order, fee, mocker) -> None: + limit_buy_order, fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -338,6 +600,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) @@ -616,7 +879,8 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, assert log_has('Unable to sell trade: ', caplog.record_tuples) -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None: +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, + fee, markets, mocker) -> None: """ Test check_handle() method """ @@ -632,7 +896,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), - get_fee=fee + get_fee=fee, + get_markets=markets ) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) @@ -660,7 +925,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock assert trade.close_date is not None -def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: """ Test check_handle() method """ @@ -677,6 +943,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(conf) @@ -718,7 +985,8 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, assert freqtrade.handle_trade(trades[0]) is True -def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None: +def test_handle_trade_roi(default_conf, ticker, limit_buy_order, + fee, mocker, markets, caplog) -> None: """ Test check_handle() method """ @@ -735,6 +1003,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) @@ -755,7 +1024,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca def test_handle_trade_experimental( - default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None: + default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: """ Test check_handle() method """ @@ -772,6 +1041,7 @@ def test_handle_trade_experimental( get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) @@ -789,7 +1059,8 @@ def test_handle_trade_experimental( assert log_has('Sell signal received. Selling..', caplog.record_tuples) -def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fee, mocker) -> None: +def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, + fee, markets, mocker) -> None: """ Test check_handle() method """ @@ -802,6 +1073,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fe get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1040,7 +1312,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: assert cancel_order_mock.call_count == 1 -def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: +def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: """ Test execute_sell() method with a ticker going UP """ @@ -1051,7 +1323,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) freqtrade = FreqtradeBot(default_conf) @@ -1081,7 +1354,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] -def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: +def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: """ Test execute_sell() method with a ticker going DOWN """ @@ -1093,7 +1366,8 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1122,7 +1396,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, mocker) -> None: + ticker_sell_up, markets, mocker) -> None: """ Test execute_sell() method with a ticker going DOWN and with a bot config empty """ @@ -1133,7 +1407,8 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1163,7 +1438,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, mocker) -> None: + ticker_sell_down, markets, mocker) -> None: """ Test execute_sell() method with a ticker going DOWN and with a bot config empty """ @@ -1174,7 +1449,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1201,7 +1477,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, + fee, markets, mocker) -> None: """ Test sell_profit_only feature when enabled """ @@ -1219,6 +1496,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) conf['experimental'] = { @@ -1234,7 +1512,8 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock assert freqtrade.handle_trade(trade) is True -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, + fee, markets, mocker) -> None: """ Test sell_profit_only feature when disabled """ @@ -1252,6 +1531,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) conf['experimental'] = { @@ -1267,14 +1547,14 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc assert freqtrade.handle_trade(trade) is True -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: """ Test sell_profit_only feature when enabled and we have a loss """ patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) + mocker.patch('freqtrade.freqtradebot.Analyze.stop_loss_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1285,6 +1565,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) conf['experimental'] = { @@ -1300,7 +1581,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker assert freqtrade.handle_trade(trade) is False -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: """ Test sell_profit_only feature when enabled and we have a loss """ @@ -1312,9 +1593,9 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 + 'bid': 0.0000172, + 'ask': 0.0000173, + 'last': 0.0000172 }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, @@ -1335,6 +1616,85 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke assert freqtrade.handle_trade(trade) is True +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: + """ + Test sell_profit_only feature when enabled and we have a loss + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.0000172, + 'ask': 0.0000173, + 'last': 0.0000172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + conf = deepcopy(default_conf) + conf['experimental'] = { + 'ignore_roi_if_buy_signal': True + } + + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + patch_get_signal(mocker, value=(True, True)) + assert freqtrade.handle_trade(trade) is False + + # Test if buy-signal is absent (should sell due to roi = true) + patch_get_signal(mocker, value=(False, True)) + assert freqtrade.handle_trade(trade) is True + + +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, + fee, markets, mocker) -> None: + """ + Test sell_profit_only feature when enabled and we have a loss + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.00000172, + 'ask': 0.00000173, + 'last': 0.00000172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + get_markets=markets + ) + + conf = deepcopy(default_conf) + conf['experimental'] = { + 'ignore_roi_if_buy_signal': False + } + + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + # Sell due to min_roi_reached + patch_get_signal(mocker, value=(True, True)) + assert freqtrade.handle_trade(trade) is True + + # Test if buy-signal is absent + patch_get_signal(mocker, value=(False, True)) + assert freqtrade.handle_trade(trade) is True + + def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): """ Test get_real_amount - fee in quote currency diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index c50ad7d2c..30ad239a1 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -14,9 +14,7 @@ def init_persistence(default_conf): init(default_conf) -def test_init_create_session(default_conf, mocker): - mocker.patch.dict('freqtrade.persistence._CONF', default_conf) - +def test_init_create_session(default_conf): # Check if init create a session init(default_conf) assert hasattr(Trade, 'session') @@ -29,20 +27,17 @@ def test_init_custom_db_url(default_conf, mocker): # Update path to a value other than default, but still in-memory conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' -def test_init_invalid_db_url(default_conf, mocker): +def test_init_invalid_db_url(default_conf): conf = deepcopy(default_conf) # Update path to a value other than default, but still in-memory conf.update({'db_url': 'unknown:///some.url'}) - mocker.patch.dict('freqtrade.persistence._CONF', conf) - with pytest.raises(OperationalException, match=r'.*no valid database URL*'): init(conf) @@ -53,7 +48,6 @@ def test_init_prod_db(default_conf, mocker): conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 @@ -66,7 +60,6 @@ def test_init_dryrun_db(default_conf, mocker): conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 diff --git a/requirements.txt b/requirements.txt index 23c0c83e7..33fbd1523 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.253 +ccxt==1.14.257 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 @@ -22,7 +22,7 @@ tabulate==0.8.2 coinmarketcap==5.0.3 # Required for plotting data -#plotly==2.3.0 +#plotly==2.7.0 #Added for local rest client Flask==1.0.2 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 2f76c1232..686098f94 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -3,11 +3,14 @@ """This script generate json data from bittrex""" import json import sys -import os +from pathlib import Path import arrow -from freqtrade import (arguments, misc) +from freqtrade import arguments +from freqtrade.arguments import TimeRange from freqtrade.exchange import Exchange +from freqtrade.optimize import download_backtesting_testdata + DEFAULT_DL_PATH = 'user_data/data' @@ -17,25 +20,27 @@ args = arguments.parse_args() timeframes = args.timeframes -dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange) +dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange) if args.export: - dl_path = args.export + dl_path = Path(args.export) -if not os.path.isdir(dl_path): +if not dl_path.is_dir(): sys.exit(f'Directory {dl_path} does not exist.') -pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json') -if not os.path.isfile(pairs_file): +pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') +if not pairs_file.exists(): sys.exit(f'No pairs file found with path {pairs_file}.') -with open(pairs_file) as file: +with pairs_file.open() as file: PAIRS = list(set(json.load(file))) PAIRS.sort() -since_time = None + +timerange = TimeRange() if args.days: - since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 + time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") + timerange = arguments.parse_timerange(f'{time_since}-') print(f'About to download pairs: {PAIRS} to {dl_path}') @@ -59,21 +64,18 @@ for pair in PAIRS: print(f"skipping pair {pair}") continue for tick_interval in timeframes: - print(f'downloading pair {pair}, interval {tick_interval}') - - data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time) - if not data: - print('\tNo data was downloaded') - break - - print('\tData was downloaded for period %s - %s' % ( - arrow.get(data[0][0] / 1000).format(), - arrow.get(data[-1][0] / 1000).format())) - - # save data pair_print = pair.replace('/', '_') filename = f'{pair_print}-{tick_interval}.json' - misc.file_dump_json(os.path.join(dl_path, filename), data) + dl_file = dl_path.joinpath(filename) + if args.erase and dl_file.exists(): + print(f'Deleting existing data for pair {pair}, interval {tick_interval}') + dl_file.unlink() + + print(f'downloading pair {pair}, interval {tick_interval}') + download_backtesting_testdata(str(dl_path), exchange=exchange, + pair=pair, + tick_interval=tick_interval, + timerange=timerange) if pairs_not_available: