From 20ebd744c38cfb0791b4176379cda761d11069bb Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Sat, 12 May 2018 09:43:22 +0300 Subject: [PATCH 01/86] Freqtrade 0.16.0 release --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index d1671e4c6..cd4515a3b 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.15.1' +__version__ = '0.16.0' class DependencyException(BaseException): From e1322b75a930e13615c3d3ac1c465551369efbd7 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Sat, 12 May 2018 09:50:01 +0300 Subject: [PATCH 02/86] Freqtrade 0.16.1 release Note. This is the last release that uses our own bittrex implementation for trading. After this ccxt library will be taken into use which will offer the needed exchanges (bittrex/binance) --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index cd4515a3b..ae8e57fd5 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.16.0' +__version__ = '0.16.1' class DependencyException(BaseException): From 9be98cd8f77c503f303d021eb62b531c9be698b3 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 23 May 2018 13:15:03 +0300 Subject: [PATCH 03/86] Add ability to set unlimited stake_amount --- docs/configuration.md | 2 +- freqtrade/constants.py | 5 +- freqtrade/freqtradebot.py | 25 ++++-- freqtrade/optimize/backtesting.py | 5 +- freqtrade/tests/test_freqtradebot.py | 130 ++++++++++++++++++++++----- 5 files changed, 137 insertions(+), 30 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3d36947c3..e75775f3f 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. 'unlimited' is used to allow a 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. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7731ea610..20c8a2287 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -32,7 +32,10 @@ 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']}, - 'stake_amount': {'type': 'number', 'minimum': 0.0005}, + 'stake_amount': {'anyOf': [ + {'type': 'integer', 'minimum': 0.0005}, + {'constant': 'unlimited'} + ]}, 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7c955d423..af3f9287e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -255,6 +255,24 @@ 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) -> float: + stake_amount = self.config['stake_amount'] + avaliable_amount = exchange.get_balance(self.config['stake_currency']) + + if stake_amount == 'unlimited': + open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) + if open_trades == self.config['max_open_trades']: + return 0 + return avaliable_amount / (self.config['max_open_trades'] - open_trades) + + # Check if stake_amount is fulfilled + if avaliable_amount < stake_amount: + raise DependencyException( + 'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency']) + ) + + return stake_amount + def create_trade(self) -> bool: """ Checks the implemented trading indicator(s) for a randomly picked pair, @@ -263,19 +281,14 @@ class FreqtradeBot(object): :param interval: Ticker interval used for Analyze :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() logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', stake_amount ) whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist']) - # Check if stake_amount is fulfilled - if exchange.get_balance(self.config['stake_currency']) < stake_amount: - raise DependencyException( - 'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency']) - ) # Remove currently opened and latest pairs from whitelist for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 376730d0f..eb6b25e02 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,7 @@ from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize -from freqtrade import exchange +from freqtrade import exchange, DependencyException from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -296,6 +296,9 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]: config['exchange']['key'] = '' config['exchange']['secret'] = '' + if config['stake_amount'] == 'unlimited': + raise DependencyException('stake amount could not be "unlimited" for backtesting') + return config diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index ebabc0187..d9d009a47 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -220,6 +220,115 @@ def test_refresh_whitelist() -> None: pass +def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: + """ + Test get_trade_stake_amount() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + conf = deepcopy(default_conf) + + freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + + result = freqtrade._get_trade_stake_amount() + assert(result == 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_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.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'] * 0.5), + get_fee=fee, + ) + + conf = deepcopy(default_conf) + + freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + + 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, + mocker) -> None: + """ + Test get_trade_stake_amount() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.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, + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = 'unlimited' + conf['max_open_trades'] = 2 + + freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + + # 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)) + + +def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: + """ + Test create_trade() method + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.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'] * 0.5), + get_fee=fee, + ) + freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.create_trade() + + def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: """ Test create_trade() method @@ -281,27 +390,6 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, assert rate * amount >= conf['stake_amount'] -def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) - patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.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'] * 0.5), - get_fee=fee, - ) - freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) - - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trade() - - def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocker) -> None: """ Test create_trade() method From cf5d691950c99c8c055048e1b5aa4150b136a765 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 25 May 2018 00:46:08 +0300 Subject: [PATCH 04/86] Clean the tests --- freqtrade/tests/test_freqtradebot.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d9d009a47..e47bfd007 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -224,23 +224,18 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock """ Test get_trade_stake_amount() method """ - patch_get_signal(mocker) + patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), - get_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) ) - conf = deepcopy(default_conf) - - freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) result = freqtrade._get_trade_stake_amount() - assert(result == conf['stake_amount']) + assert(result == default_conf['stake_amount']) def test_get_trade_stake_amount_no_stake_amount(default_conf, @@ -251,21 +246,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, """ Test get_trade_stake_amount() method """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.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'] * 0.5), - get_fee=fee, + get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) ) - conf = deepcopy(default_conf) - - freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade._get_trade_stake_amount() From 3427c7eb540ac7b73e7ed52578ec1c281b4b2500 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 25 May 2018 17:04:08 +0300 Subject: [PATCH 05/86] Use constants --- docs/configuration.md | 2 +- freqtrade/constants.py | 3 ++- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/tests/test_freqtradebot.py | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e75775f3f..5b527ddf7 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. 'unlimited' is used to allow a bot to use all avaliable balance. +| `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. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 20c8a2287..1039b9da4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -9,6 +9,7 @@ TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' +UNLIMITED_STAKE_AMOUNT = 'unlimited' TICKER_INTERVAL_MINUTES = { '1m': 1, @@ -34,7 +35,7 @@ CONF_SCHEMA = { 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, 'stake_amount': {'anyOf': [ {'type': 'integer', 'minimum': 0.0005}, - {'constant': 'unlimited'} + {'constant': UNLIMITED_STAKE_AMOUNT} ]}, 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'CZK', 'DKK', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index af3f9287e..431a146dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -259,7 +259,7 @@ class FreqtradeBot(object): stake_amount = self.config['stake_amount'] avaliable_amount = exchange.get_balance(self.config['stake_currency']) - if stake_amount == 'unlimited': + 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']: return 0 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eb6b25e02..520765853 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,7 @@ from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize -from freqtrade import exchange, DependencyException +from freqtrade import exchange, constants, DependencyException from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -296,8 +296,9 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]: config['exchange']['key'] = '' config['exchange']['secret'] = '' - if config['stake_amount'] == 'unlimited': - raise DependencyException('stake amount could not be "unlimited" for backtesting') + 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/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index e47bfd007..eeab3bf37 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ import pytest import requests from sqlalchemy import create_engine -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 @@ -280,7 +280,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ) conf = deepcopy(default_conf) - conf['stake_amount'] = 'unlimited' + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT conf['max_open_trades'] = 2 freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) From daa9c0c02607cf5db0a860b133ddfbf91fa6dde9 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Jun 2018 01:48:26 +0300 Subject: [PATCH 06/86] Fix review comments --- docs/configuration.md | 7 ++++++ freqtrade/constants.py | 9 +++---- freqtrade/tests/optimize/test_backtesting.py | 25 +++++++++++++++++++- freqtrade/tests/test_configuration.py | 12 ++++++++++ freqtrade/tests/test_freqtradebot.py | 6 +++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5b527ddf7..0bb417338 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,6 +42,13 @@ The table below will list all configuration parameters. The definition of each config parameters is in [misc.py](https://github.com/gcarq/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/freqtrade/constants.py b/freqtrade/constants.py index 1039b9da4..ea4fc478c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -33,10 +33,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']}, - 'stake_amount': {'anyOf': [ - {'type': 'integer', 'minimum': 0.0005}, - {'constant': UNLIMITED_STAKE_AMOUNT} - ]}, + 'stake_amount': { + "type": ["number", "string"], + "minimum": 0.0005, + "pattern": UNLIMITED_STAKE_AMOUNT + }, 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index f17a0115e..4d02cfe5f 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 from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration @@ -261,6 +262,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/test_configuration.py b/freqtrade/tests/test_configuration.py index d27405d91..fb63ca2d9 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -53,6 +53,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([]) + 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 eeab3bf37..d91dd4720 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -295,6 +295,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, result = freqtrade._get_trade_stake_amount() assert(result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1)) + # create 2 trades, order amount should be 0 + freqtrade.create_trade() + + result = freqtrade._get_trade_stake_amount() + assert(result == 0) + def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: """ From 3030bf9778f4d23d2237432e5c840c6e5c74f374 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Jun 2018 01:52:54 +0300 Subject: [PATCH 07/86] Fix types --- freqtrade/tests/test_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index df4d3f5ef..98adf19e9 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -62,7 +62,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: conf['stake_amount'] = 'fake' with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): - configuration = Configuration([]) + configuration = Configuration(Namespace()) configuration._validate_config(conf) From 12d8a8b1a35b463fc90a0af0c9ca6d1d0560538e Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 6 Jun 2018 00:14:28 +0300 Subject: [PATCH 08/86] Fix review comments --- freqtrade/arguments.py | 10 ++++------ freqtrade/freqtradebot.py | 6 ++++-- freqtrade/tests/test_freqtradebot.py | 6 ++++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 97c3d8cb2..1ae805ca5 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -239,17 +239,15 @@ class Arguments(object): stop: Optional[int] = None 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 stype, start, stop diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b1928a4d5..49c179907 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -258,14 +258,16 @@ class FreqtradeBot(object): 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']: + if open_trades >= self.config['max_open_trades']: return 0 return avaliable_amount / (self.config['max_open_trades'] - open_trades) # Check if stake_amount is fulfilled if avaliable_amount < stake_amount: raise DependencyException( - 'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency']) + '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 diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d91dd4720..581e76406 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -301,6 +301,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, result = freqtrade._get_trade_stake_amount() assert(result == 0) + # set max_open_trades = 0, so do not trade + conf['max_open_trades'] = 0 + freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + result = freqtrade._get_trade_stake_amount() + assert(result == 0) + def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: """ From b1b87731b1a97de0fd8d2cd262c22c80fed87f30 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 8 Jun 2018 00:54:46 +0300 Subject: [PATCH 09/86] Support case when _get_trade_stake_amount returns None --- freqtrade/freqtradebot.py | 6 ++++- freqtrade/tests/test_freqtradebot.py | 36 +++++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9392f5b39..70c74755e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -257,7 +257,8 @@ class FreqtradeBot(object): 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']: - return 0 + 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 @@ -281,6 +282,9 @@ class FreqtradeBot(object): interval = self.analyze.get_ticker_interval() stake_amount = self._get_trade_stake_amount() + if not stake_amount: + return False + logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', stake_amount diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index f7e84ebdd..a90b1f983 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -231,7 +231,7 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) ) - freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(default_conf) result = freqtrade._get_trade_stake_amount() assert(result == default_conf['stake_amount']) @@ -252,7 +252,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) ) - freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade._get_trade_stake_amount() @@ -282,29 +282,29 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT conf['max_open_trades'] = 2 - freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + 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']) + 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)) + assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) - # create 2 trades, order amount should be 0 + # create 2 trades, order amount should be None freqtrade.create_trade() result = freqtrade._get_trade_stake_amount() - assert(result == 0) + assert result is None - # set max_open_trades = 0, so do not trade + # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 - freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(conf) result = freqtrade._get_trade_stake_amount() - assert(result == 0) + assert result is None def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: @@ -322,7 +322,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5), get_fee=fee, ) - freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) + freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade.create_trade() @@ -389,7 +389,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, 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_limit_reached(default_conf, ticker, limit_buy_order, fee, mocker) -> None: """ Test create_trade() method """ @@ -401,13 +401,17 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5), + get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, ) - freqtrade = FreqtradeBot(default_conf) + conf = deepcopy(default_conf) + conf['max_open_trades'] = 0 + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trade() + 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, mocker) -> None: From f0456bb802257af9ddbabe088142b566908be00e Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Tue, 5 Jun 2018 00:22:44 -0700 Subject: [PATCH 10/86] Update the README structure --- README.md | 169 ++++++++++++++++++++----------------------- docs/installation.md | 23 ++++++ 2 files changed, 102 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 24e01531c..94ebb7bf8 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,46 @@ 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. + +**Important:** Always create your PR against the `develop` branch, not +`master`. + ## Requirements ### Min hardware required diff --git a/docs/installation.md b/docs/installation.md index 9818529f6..9fec6d25e 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 From 367601518481092ae92d320e0eeabaa4b2bf7b8a Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 11 Jun 2018 16:21:57 +0300 Subject: [PATCH 11/86] Fix check --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70c74755e..8c8017d0b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import logging import time import traceback from datetime import datetime -from typing import Dict, List, Optional, Any, Callable +from typing import Dict, List, Optional, Any, Callable, Optional import arrow import requests @@ -239,7 +239,7 @@ class FreqtradeBot(object): return final_list - def get_target_bid(self, ticker: Dict[str, float]) -> float: + def get_target_bid(self, ticker: Dict[str, float]) -> Optional[float]: """ Calculates bid target between current ask price and last price :param ticker: Ticker to use for getting Ask and Last Price From 90025d0ac437fafe261df6d588f0732f3698bbb4 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 11 Jun 2018 16:38:10 +0300 Subject: [PATCH 12/86] Fix check --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ab22b2826..1e32aecdf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import logging import time import traceback from datetime import datetime -from typing import Dict, List, Optional, Any, Callable, Optional +from typing import Dict, List, Optional, Any, Callable import arrow import requests From 708320318c5a340d70a161177725f670764d0bb9 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Jun 2018 01:05:43 +0300 Subject: [PATCH 13/86] Check minimal amount --- freqtrade/freqtradebot.py | 12 +++++++++--- freqtrade/tests/test_freqtradebot.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1e32aecdf..1477a931e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -234,7 +234,7 @@ class FreqtradeBot(object): return final_list - def get_target_bid(self, ticker: Dict[str, float]) -> Optional[float]: + def get_target_bid(self, ticker: Dict[str, float]) -> float: """ Calculates bid target between current ask price and last price :param ticker: Ticker to use for getting Ask and Last Price @@ -245,7 +245,7 @@ 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) -> float: + def _get_trade_stake_amount(self) -> Optional[float]: stake_amount = self.config['stake_amount'] avaliable_amount = exchange.get_balance(self.config['stake_currency']) @@ -254,7 +254,13 @@ class FreqtradeBot(object): 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) + trade_stake_amount = avaliable_amount / (self.config['max_open_trades'] - open_trades) + if trade_stake_amount < 0.0005: + raise DependencyException( + 'Available balance(%f %s) is lower than minimal' % ( + avaliable_amount, self.config['stake_currency']) + ) + return trade_stake_amount # Check if stake_amount is fulfilled if avaliable_amount < stake_amount: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 314d880b6..80b66735d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -249,11 +249,21 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, 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() + # test UNLIMITED_STAKE_AMOUNT + conf = deepcopy(default_conf) + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + conf['max_open_trades'] = 2 + freqtrade = FreqtradeBot(conf) + + with pytest.raises(DependencyException, match=r'.*is lower than minimal.*'): + freqtrade._get_trade_stake_amount() + def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, From eb909068c5c5d37b8a574b722529f47aab276ffc Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 17 Jun 2018 02:23:12 +0300 Subject: [PATCH 14/86] Add minimal pair stake amount check --- freqtrade/analyze.py | 7 + freqtrade/freqtradebot.py | 46 ++++- freqtrade/tests/conftest.py | 90 +++++++- freqtrade/tests/rpc/test_rpc.py | 36 ++-- freqtrade/tests/rpc/test_rpc_telegram.py | 46 +++-- freqtrade/tests/test_freqtradebot.py | 251 ++++++++++++++++++----- 6 files changed, 388 insertions(+), 88 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index f18ae291c..2ef3a7515 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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1477a931e..a5612371d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -254,13 +254,7 @@ class FreqtradeBot(object): if open_trades >= self.config['max_open_trades']: logger.warning('Can\'t open a new trade: max number of trades is reached') return None - trade_stake_amount = avaliable_amount / (self.config['max_open_trades'] - open_trades) - if trade_stake_amount < 0.0005: - raise DependencyException( - 'Available balance(%f %s) is lower than minimal' % ( - avaliable_amount, self.config['stake_currency']) - ) - return trade_stake_amount + return avaliable_amount / (self.config['max_open_trades'] - open_trades) # Check if stake_amount is fulfilled if avaliable_amount < stake_amount: @@ -272,6 +266,34 @@ class FreqtradeBot(object): return stake_amount + def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]: + markets = 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, @@ -314,8 +336,16 @@ class FreqtradeBot(object): pair_url = exchange.get_pair_detail_url(pair) # Calculate amount buy_limit = self.get_target_bid(exchange.get_ticker(pair)) - amount = stake_amount / buy_limit + 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 = exchange.buy(pair, buy_limit, amount)['id'] stake_amount_fiat = self.fiat_converter.convert_amount( diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 1311687b7..5eed0a08f 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -174,7 +174,10 @@ def markets(): 'max': 1000, }, 'price': 500000, - 'cost': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, }, 'info': '', }, @@ -196,7 +199,10 @@ def markets(): 'max': 1000, }, 'price': 500000, - 'cost': 500000, + 'cost': { + 'min': 1, + 'max': 500000, + }, }, 'info': '', }, @@ -218,7 +224,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/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 5cdd22c7a..f947cc9f0 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -23,7 +23,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 """ @@ -34,7 +34,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -72,7 +73,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: assert trade.find('[ETH/BTC]') >= 0 -def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: +def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: """ Test rpc_status_table() method """ @@ -83,7 +84,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -107,7 +109,7 @@ def test_rpc_status_table(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 """ @@ -118,7 +120,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -160,7 +163,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 """ @@ -175,7 +178,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -237,7 +241,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 @@ -253,7 +257,8 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -400,7 +405,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 """ @@ -422,6 +427,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: } ), get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -519,7 +525,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 """ @@ -531,7 +537,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) @@ -558,7 +565,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 """ @@ -571,6 +578,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 0919455ad..d10abd8b5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -233,7 +233,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: ) -def test_status(default_conf, update, mocker, fee, ticker) -> None: +def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: """ Test _status() method """ @@ -250,6 +250,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None: get_ticker=ticker, get_pair_detail_url=MagicMock(), get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() status_table = MagicMock() @@ -278,7 +279,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None: assert status_table.call_count == 1 -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: """ Test _status() method """ @@ -289,6 +290,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() @@ -324,7 +326,7 @@ 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, update, ticker, fee, mocker) -> None: +def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: """ Test _status_table() method """ @@ -336,6 +338,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -377,7 +380,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: 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 """ @@ -391,7 +394,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -489,7 +493,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 """ @@ -500,7 +504,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) msg_mock = MagicMock() mocker.patch.multiple( @@ -768,7 +773,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 """ @@ -781,7 +787,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -808,7 +815,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 """ @@ -821,7 +829,8 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -852,7 +861,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 """ @@ -866,7 +875,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtradebot = FreqtradeBot(default_conf) @@ -930,7 +940,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 """ @@ -946,7 +956,8 @@ def test_performance_handle(default_conf, update, ticker, fee, 'freqtrade.freqtradebot.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) @@ -994,7 +1005,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 """ @@ -1010,7 +1021,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: 'freqtrade.freqtradebot.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.optimize.backtesting.exchange.get_fee', fee) freqtradebot = FreqtradeBot(default_conf) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 80b66735d..badc1fc3d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -255,20 +255,12 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade._get_trade_stake_amount() - # test UNLIMITED_STAKE_AMOUNT - conf = deepcopy(default_conf) - conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - conf['max_open_trades'] = 2 - freqtrade = FreqtradeBot(conf) - - with pytest.raises(DependencyException, match=r'.*is lower than minimal.*'): - 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 @@ -283,6 +275,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, 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) @@ -314,28 +307,119 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, assert result is None -def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_get_min_pair_stake_amount(mocker, default_conf) -> None: """ - Test create_trade() method + Test get_trade_stake_amount() method """ - patch_get_signal(mocker) + patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.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'] * 0.5), - get_fee=fee, - ) + mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.freqtradebot.Analyze.get_stoploss', MagicMock(return_value=-0.05)) freqtrade = FreqtradeBot(default_conf) - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trade() + # no pair found + mocker.patch( + 'freqtrade.freqtradebot.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.freqtradebot.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.freqtradebot.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.freqtradebot.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.freqtradebot.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.freqtradebot.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.freqtradebot.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.freqtradebot.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, mocker) -> None: +def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -348,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 @@ -371,7 +456,31 @@ 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: +def test_create_trade_no_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) + mocker.patch.multiple( + 'freqtrade.freqtradebot.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'] * 0.5), + get_fee=fee, + get_markets=markets + ) + freqtrade = FreqtradeBot(default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.create_trade() + + +def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -385,6 +494,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=buy_mock, get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) @@ -396,7 +506,34 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, assert rate * amount >= conf['stake_amount'] -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +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.freqtradebot.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 """ @@ -410,6 +547,7 @@ def test_create_trade_limit_reached(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']), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) conf['max_open_trades'] = 0 @@ -421,7 +559,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, fee, assert freqtrade._get_trade_stake_amount() is None -def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: """ Test create_trade() method """ @@ -434,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) @@ -448,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 """ @@ -461,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) @@ -736,7 +876,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 """ @@ -752,7 +893,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}) @@ -780,7 +922,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 """ @@ -797,6 +940,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) @@ -838,7 +982,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 """ @@ -855,6 +1000,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) @@ -875,7 +1021,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 """ @@ -892,6 +1038,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) @@ -909,7 +1056,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 """ @@ -922,6 +1070,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) @@ -1160,7 +1309,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 """ @@ -1171,7 +1320,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'freqtrade.freqtradebot.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) @@ -1201,7 +1351,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 """ @@ -1213,7 +1363,8 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1242,7 +1393,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 """ @@ -1253,7 +1404,8 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1283,7 +1435,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 """ @@ -1294,7 +1446,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - get_fee=fee + get_fee=fee, + get_markets=markets ) freqtrade = FreqtradeBot(default_conf) @@ -1321,7 +1474,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 """ @@ -1339,6 +1493,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'] = { @@ -1354,7 +1509,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 """ @@ -1372,6 +1528,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'] = { @@ -1387,7 +1544,7 @@ 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 """ @@ -1405,6 +1562,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'] = { @@ -1420,7 +1578,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 """ @@ -1438,6 +1596,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) From c73b9f5c77ab9f55b9d51326f9c372dc02af02b1 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 21:04:07 +0300 Subject: [PATCH 15/86] avoid calling exchange.get_fee inside loop --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a19d1412..ffb808a24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -62,6 +62,7 @@ class Backtesting(object): self.config['exchange']['uid'] = '' self.config['dry_run'] = True self.exchange = Exchange(self.config) + self.fee = self.exchange.get_fee() @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: @@ -130,14 +131,13 @@ class Backtesting(object): stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) - fee = self.exchange.get_fee() trade = Trade( open_rate=buy_row.close, open_date=buy_row.date, stake_amount=stake_amount, amount=stake_amount / buy_row.open, - fee_open=fee, - fee_close=fee + fee_open=self.fee, + fee_close=self.fee ) # calculate win/lose forwards from buy point From 8a44dff595da271b35e8fbc4696f15bd8271b80e Mon Sep 17 00:00:00 2001 From: xmatthias Date: Fri, 22 Jun 2018 20:10:05 +0200 Subject: [PATCH 16/86] don't sell if buy is still active --- config.json.example | 3 +- config_full.json.example | 3 +- docs/configuration.md | 1 + freqtrade/analyze.py | 4 ++ freqtrade/constants.py | 3 +- freqtrade/freqtradebot.py | 3 +- freqtrade/tests/test_freqtradebot.py | 77 ++++++++++++++++++++++++++++ 7 files changed, 90 insertions(+), 4 deletions(-) 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..c7ba9febe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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`. diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index cf2d5519b..b986b9611 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -172,6 +172,10 @@ class Analyze(object): if the threshold is reached and updates the trade record. :return: True if trade should be sold, False otherwise """ + if buy and self.config.get('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): logger.debug('Required profit reached. Selling..') diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5be01f977..bf661aecc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -73,7 +73,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..f06460706 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -424,7 +424,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ (buy, sell) = (False, False) - if self.config.get('experimental', {}).get('use_sell_signal'): + if (self.config.get('experimental', {}).get('use_sell_signal') + or self.config.get('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/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9eae2fdf5..c18950177 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1335,6 +1335,83 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke assert freqtrade.handle_trade(trade) is True +def 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.00000172, + 'ask': 0.00000173, + 'last': 0.00000172 + }), + 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, 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, + ) + + 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 From cbfee51f321992ba17501605514e87cf8a6361f3 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Fri, 22 Jun 2018 20:51:21 +0200 Subject: [PATCH 17/86] introduce experimental variable and fix test naming --- freqtrade/analyze.py | 9 ++++----- freqtrade/freqtradebot.py | 5 ++--- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index b986b9611..fc81f3fb9 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -172,7 +172,8 @@ class Analyze(object): if the threshold is reached and updates the trade record. :return: True if trade should be sold, False otherwise """ - if buy and self.config.get('experimental', {}).get('ignore_roi_if_buy_signal', False): + 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 @@ -181,13 +182,11 @@ class Analyze(object): 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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f06460706..221d32e9e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -423,9 +423,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') - or self.config.get('experimental', {}).get('ignore_roi_if_buy_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/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index c18950177..bad6d9502 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1335,7 +1335,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke assert freqtrade.handle_trade(trade) is True -def ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: +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 """ From f7e5d2c3a572d7f1af3f28c164341ba942d480d9 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 21:55:09 +0300 Subject: [PATCH 18/86] check that we set fee on backtesting init --- freqtrade/tests/optimize/test_backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 1702818b1..22536e42f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -298,6 +298,7 @@ def test_backtesting_init(mocker, default_conf) -> None: Test Backtesting._init() method """ patch_exchange(mocker) + get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) assert backtesting.config == default_conf assert isinstance(backtesting.analyze, Analyze) @@ -305,6 +306,8 @@ def test_backtesting_init(mocker, default_conf) -> None: assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.populate_buy_trend) assert callable(backtesting.populate_sell_trend) + get_fee.assert_called() + assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: From e2a2a0be9b7bed68504d7adac5048c4c41d5980a Mon Sep 17 00:00:00 2001 From: xmatthias Date: Fri, 22 Jun 2018 21:21:34 +0200 Subject: [PATCH 19/86] extract stop_loss_reached to allow check before ignore_roi_if_buy_signal --- freqtrade/analyze.py | 20 ++++++++++++++------ freqtrade/tests/test_freqtradebot.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index fc81f3fb9..4f4b4f391 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -172,13 +172,17 @@ 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) experimental = self.config.get('experimental', {}) + if self.stop_loss_reached(current_profit=current_profit): + return True + 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 @@ -192,16 +196,20 @@ class Analyze(object): 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/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index bad6d9502..33d9ae2d9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1274,7 +1274,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker 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(), From 2be7b3d9eb71b08543b5757947ded059e9eb4e4c Mon Sep 17 00:00:00 2001 From: xmatthias Date: Fri, 22 Jun 2018 21:24:21 +0200 Subject: [PATCH 20/86] fix mocked bid-value to match limt_buy_order config --- freqtrade/tests/test_freqtradebot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 33d9ae2d9..0d4256c42 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1312,9 +1312,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, @@ -1347,9 +1347,9 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> '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, From 9a07d57ed7b8f45352cf78ae7f6d45cb01acfa54 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 07:58:25 +0300 Subject: [PATCH 21/86] fix flake8 --- freqtrade/tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 22536e42f..65aa00a70 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -307,7 +307,7 @@ def test_backtesting_init(mocker, default_conf) -> None: assert callable(backtesting.populate_buy_trend) assert callable(backtesting.populate_sell_trend) get_fee.assert_called() - assert backtesting.fee == 0.5 + assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: From 3360bf400125cf4d058534e9edfacd8ac3e54c1e Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 10:25:03 +0200 Subject: [PATCH 22/86] wrap strategies with HyperoptStrategy for module lookups with pickle --- freqtrade/optimize/hyperopt.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 19e732d7b..e52581491 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,6 +11,7 @@ import pickle import signal import sys from argparse import Namespace +from copy import deepcopy from functools import reduce from math import exp from operator import itemgetter @@ -26,10 +27,25 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting +from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) +HyperoptStrategy = None + + +def wrap_strategy(strategy: IStrategy) -> Optional[HyperoptStrategy]: + """Wraps a given Strategy instance to HyperoptStrategy""" + global HyperoptStrategy + + attr = deepcopy(dict(strategy.__class__.__dict__)) + # Patch module name to make it compatible with pickle + attr['__module__'] = 'freqtrade.optimize.hyperopt' + HyperoptStrategy = type('HyperoptStrategy', (IStrategy,), attr) + return HyperoptStrategy() + + class Hyperopt(Backtesting): """ Hyperopt class, this class contains all the logic to run a hyperopt simulation @@ -39,7 +55,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 @@ -57,6 +72,9 @@ class Hyperopt(Backtesting): # check that the reported Σ% values do not exceed this! self.expected_max_profit = 3.0 + # Wrap strategy to make it compatible with pickle + self.analyze.strategy = wrap_strategy(self.analyze.strategy) + # Configuration and data used by hyperopt self.processed: Optional[Dict[str, Any]] = None From c40e6a12d1ca4f302f876c10a3a107dcb5aae1c9 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:13:49 +0200 Subject: [PATCH 23/86] move logic from hyperopt to freqtrade.strategy --- freqtrade/optimize/hyperopt.py | 19 ------------------- freqtrade/strategy/__init__.py | 32 ++++++++++++++++++++++++++++++++ freqtrade/strategy/resolver.py | 5 +++-- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e52581491..7a313a3ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,7 +11,6 @@ import pickle import signal import sys from argparse import Namespace -from copy import deepcopy from functools import reduce from math import exp from operator import itemgetter @@ -27,25 +26,10 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting -from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) -HyperoptStrategy = None - - -def wrap_strategy(strategy: IStrategy) -> Optional[HyperoptStrategy]: - """Wraps a given Strategy instance to HyperoptStrategy""" - global HyperoptStrategy - - attr = deepcopy(dict(strategy.__class__.__dict__)) - # Patch module name to make it compatible with pickle - attr['__module__'] = 'freqtrade.optimize.hyperopt' - HyperoptStrategy = type('HyperoptStrategy', (IStrategy,), attr) - return HyperoptStrategy() - - class Hyperopt(Backtesting): """ Hyperopt class, this class contains all the logic to run a hyperopt simulation @@ -72,9 +56,6 @@ class Hyperopt(Backtesting): # check that the reported Σ% values do not exceed this! self.expected_max_profit = 3.0 - # Wrap strategy to make it compatible with pickle - self.analyze.strategy = wrap_strategy(self.analyze.strategy) - # Configuration and data used by hyperopt self.processed: Optional[Dict[str, Any]] = None 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..3c7836291 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 @@ -83,7 +84,7 @@ class StrategyResolver(object): strategy = self._search_strategy(path, strategy_name) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) - return strategy + return import_strategy(strategy) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" @@ -100,7 +101,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 From 4bd61df3a7e7405c821e38f5bce4f06ae2fe4917 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:14:31 +0200 Subject: [PATCH 24/86] implement test for import_strategy --- freqtrade/tests/strategy/test_strategy.py | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 244910790..26cd798f4 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -5,10 +5,34 @@ 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 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,8 +44,7 @@ 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) From 818a6b12ed6cad47a4b7e758534b7c5721365e06 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:57:26 +0200 Subject: [PATCH 25/86] tests: add dir() assertion --- freqtrade/tests/strategy/test_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 26cd798f4..13eac3261 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 - import logging import os @@ -22,6 +21,8 @@ def test_import_strategy(caplog): imported_strategy = import_strategy(strategy) + assert dir(strategy) == dir(imported_strategy) + assert imported_strategy.__module__ == 'freqtrade.strategy' assert imported_strategy.some_method() == 42 From fc219b4e940d84e1d0e45efb5b48bc1a2631858f Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 13:10:08 +0200 Subject: [PATCH 26/86] move experimental eval below stop_loss_reached to improve performance --- freqtrade/analyze.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 4f4b4f391..a0f133b22 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -173,10 +173,11 @@ class Analyze(object): :return: True if trade should be sold, False otherwise """ current_profit = trade.calc_profit_percent(rate) - experimental = self.config.get('experimental', {}) 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 From d23cd73ba82abb0d670ccdbccfb4dcd821a557c9 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 13:12:36 +0200 Subject: [PATCH 27/86] update plotly --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a31976c8c..e26c9c8f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,4 @@ tabulate==0.8.2 coinmarketcap==5.0.3 # Required for plotting data -#plotly==2.3.0 +#plotly==2.7.0 From 295dfe26523ba0cd959bb953e95c620650d31a63 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 13:50:22 +0200 Subject: [PATCH 28/86] persistence: remove obsolete global _CONF variable --- freqtrade/persistence.py | 7 ++----- freqtrade/tests/test_persistence.py | 11 ++--------- 2 files changed, 4 insertions(+), 14 deletions(-) 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/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 From 0b3e4f6bcd07d416f5ef3508ebfc76fd4cdfb2c5 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 13:50:49 +0200 Subject: [PATCH 29/86] remove dead code --- freqtrade/tests/exchange/test_exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 3c68e8493..620113be1 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) From 0440a1917169eb9ed2e6fde713903fe9baec311a Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 14:19:50 +0200 Subject: [PATCH 30/86] export open/close rate for backtesting too preparation to allow plotting of backtest results --- freqtrade/optimize/backtesting.py | 22 +++++++++++++------- freqtrade/tests/optimize/test_backtesting.py | 18 +++++++++++----- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ffb808a24..b5ed9b167 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -37,6 +37,8 @@ class BacktestResult(NamedTuple): close_index: int trade_duration: float open_at_end: bool + open_rate: float + close_rate: float class Backtesting(object): @@ -115,11 +117,13 @@ class Backtesting(object): def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: - records = [(trade_entry.pair, trade_entry.profit_percent, - trade_entry.open_time.timestamp(), - trade_entry.close_time.timestamp(), - trade_entry.open_index - 1, trade_entry.trade_duration) - for index, trade_entry in results.iterrows()] + # columns = ["pair", "profit", "opents", "closets", "index", "duration", + # "open_rate", "close_rate", "open_at_end"] + + records = [(t.pair, t.profit_percent, t.open_time.timestamp(), + t.close_time.timestamp(), t.open_index - 1, t.trade_duration, + t.open_rate, t.close_rate, t.open_at_end) + for index, t in results.iterrows()] if records: logger.info('Dumping backtest results to %s', recordfilename) @@ -158,7 +162,9 @@ class Backtesting(object): trade_duration=(sell_row.date - buy_row.date).seconds // 60, open_index=buy_row.Index, close_index=sell_row.Index, - open_at_end=False + open_at_end=False, + open_rate=buy_row.close, + close_rate=sell_row.close ) if partial_ticker: # no sell condition found - trade stil open at end of backtest period @@ -171,7 +177,9 @@ class Backtesting(object): trade_duration=(sell_row.date - buy_row.date).seconds // 60, open_index=buy_row.Index, close_index=sell_row.Index, - open_at_end=True + open_at_end=True, + open_rate=buy_row.close, + close_rate=sell_row.close ) logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, btr.profit_percent, btr.profit_abs) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 65aa00a70..4dda82463 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -604,9 +604,13 @@ def test_backtest_record(default_conf, fee, mocker): Arrow(2017, 11, 14, 22, 10, 00).datetime, Arrow(2017, 11, 14, 22, 43, 00).datetime, Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "open_index": [1, 119, 153, 185], "close_index": [118, 151, 184, 199], - "trade_duration": [123, 34, 31, 14]}) + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True] + }) backtesting._store_backtest_result("backtest-result.json", results) assert len(results) == 4 # Assert file_dump_json was only called once @@ -617,12 +621,16 @@ def test_backtest_record(default_conf, fee, mocker): # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # Below follows just a typecheck of the schema/type of trade-records oix = None - for (pair, profit, date_buy, date_sell, buy_index, dur) in records: + for (pair, profit, date_buy, date_sell, buy_index, dur, + openr, closer, open_at_end) in records: assert pair == 'UNITTEST/BTC' - isinstance(profit, float) + assert isinstance(profit, float) # FIX: buy/sell should be converted to ints - isinstance(date_buy, str) - isinstance(date_sell, str) + assert isinstance(date_buy, float) + assert isinstance(date_sell, float) + assert isinstance(openr, float) + assert isinstance(closer, float) + assert isinstance(open_at_end, bool) isinstance(buy_index, pd._libs.tslib.Timestamp) if oix: assert buy_index > oix From 925b9b0c1920c86c1e0e5df0abcb93602673b824 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 23 Jun 2018 14:23:07 +0200 Subject: [PATCH 31/86] Update ccxt from 1.14.253 to 1.14.256 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e26c9c8f6..51312791e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.253 +ccxt==1.14.256 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 9c66c258909df4b7673300cec43477d54a5dee4d Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 14:34:36 +0200 Subject: [PATCH 32/86] resolver: use current folder instead of script folder to find user_data --- freqtrade/strategy/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 3c7836291..73abb7cee 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -72,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, ] From 4ea5fcc6613b229345e576098dc0dc6a133a4029 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 14:42:22 +0200 Subject: [PATCH 33/86] resolver: don't fail if user_data can't be found --- freqtrade/strategy/resolver.py | 11 +++++++---- freqtrade/tests/strategy/test_strategy.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 73abb7cee..0dcd3fc6a 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -81,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 import_strategy(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" diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 13eac3261..1e082c380 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -50,13 +50,16 @@ def test_load_strategy(result): 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) From 9bad75f37daccbc32ba43dfa45ab58cedf8e1a74 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Sat, 23 Jun 2018 08:36:32 -0500 Subject: [PATCH 34/86] README: note to open an issue before starting major feature work --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 94ebb7bf8..929d40292 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ 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`. From 3384679bad36a5b199abe39b54d3b5a080da2095 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Sat, 23 Jun 2018 09:38:20 -0500 Subject: [PATCH 35/86] bump develop to 0.17.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 3cedace2f6fce431cd5df784b5bcf5ce499bf02d Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 19:54:27 +0200 Subject: [PATCH 36/86] add plotting for backtested trades --- scripts/plot_dataframe.py | 49 +++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7f9641222..cd41f51b1 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -27,9 +27,12 @@ Example of usage: import logging import os import sys +import json +from pathlib import Path from argparse import Namespace from typing import Dict, List, Any +import pandas as pd import plotly.graph_objs as go from plotly import tools from plotly.offline import plot @@ -103,10 +106,42 @@ def plot_analyzed_dataframe(args: Namespace) -> None: exit() # Get trades already made from the DB - trades: List[Trade] = [] + trades: pd.DataFrame = pd.DataFrame() if args.db_url: persistence.init(_CONF) - trades = Trade.query.filter(Trade.pair.is_(pair)).all() + trades_ = Trade.query.filter(Trade.pair.is_(pair)).all() + # columns = ["pair", "profit", "opents", "closets", "index", "duration"] + columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] + + trades = pd.DataFrame([(t.pair, t.calc_profit(), + t.open_date, t.close_date, + t.open_rate, t.close_rate, + t.close_date.timestamp() - t.open_date.timestamp()) + for t in trades_], columns=columns) + + if args.exportfilename: + file = Path(args.exportfilename) + # must align with columns in backtest.py + columns = ["pair", "profit", "opents", "closets", "index", "duration", + "open_rate", "close_rate", "open_at_end"] + with file.open() as f: + data = json.load(f) + trades = pd.DataFrame(data, columns=columns) + trades = trades.loc[trades["pair"] == pair] + if timerange: + if timerange.starttype == 'date': + trades = trades.loc[trades["opents"] >= timerange.startts] + if timerange.stoptype == 'date': + trades = trades.loc[trades["opents"] <= timerange.stopts] + + trades['opents'] = pd.to_datetime(trades['opents'], + unit='s', + utc=True, + infer_datetime_format=True) + trades['closets'] = pd.to_datetime(trades['closets'], + unit='s', + utc=True, + infer_datetime_format=True) dataframes = analyze.tickerdata_to_dataframe(tickers) dataframe = dataframes[pair] @@ -126,7 +161,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html')) -def generate_graph(pair, trades, data, args) -> tools.make_subplots: +def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots: """ Generate the graph from the data generated by Backtesting or from DB :param pair: Pair to Display on the graph @@ -187,8 +222,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots: ) trade_buys = go.Scattergl( - x=[t.open_date.isoformat() for t in trades], - y=[t.open_rate for t in trades], + x=trades["opents"], + y=trades["open_rate"], mode='markers', name='trade_buy', marker=dict( @@ -199,8 +234,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots: ) ) trade_sells = go.Scattergl( - x=[t.close_date.isoformat() for t in trades], - y=[t.close_rate for t in trades], + x=trades["closets"], + y=trades["close_rate"], mode='markers', name='trade_sell', marker=dict( From f506ebcd62bf26b522afdd658dddd1fa3371deac Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 19:58:28 +0200 Subject: [PATCH 37/86] use Pathlib in the whole script --- scripts/plot_dataframe.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index cd41f51b1..763b8d9e4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -25,7 +25,6 @@ Example of usage: --indicators2 fastk,fastd """ import logging -import os import sys import json from pathlib import Path @@ -158,7 +157,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: args=args ) - plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html')) + plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html'))) def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots: From 5055563458597316bee1fbfafee1d42ba5eff673 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 20:14:15 +0200 Subject: [PATCH 38/86] add --plot-limit --- scripts/plot_dataframe.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 763b8d9e4..ba4da444a 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -104,19 +104,20 @@ def plot_analyzed_dataframe(args: Namespace) -> None: if tickers == {}: exit() + if args.db_url and args.exportfilename: + logger.critical("Can only specify --db-url or --export-filename") # Get trades already made from the DB trades: pd.DataFrame = pd.DataFrame() if args.db_url: persistence.init(_CONF) - trades_ = Trade.query.filter(Trade.pair.is_(pair)).all() - # columns = ["pair", "profit", "opents", "closets", "index", "duration"] columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] trades = pd.DataFrame([(t.pair, t.calc_profit(), t.open_date, t.close_date, t.open_rate, t.close_rate, t.close_date.timestamp() - t.open_date.timestamp()) - for t in trades_], columns=columns) + for t in Trade.query.filter(Trade.pair.is_(pair)).all()], + columns=columns) if args.exportfilename: file = Path(args.exportfilename) @@ -147,13 +148,15 @@ def plot_analyzed_dataframe(args: Namespace) -> None: dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe) - if len(dataframe.index) > 750: - logger.warning('Ticker contained more than 750 candles, clipping.') - + if len(dataframe.index) > args.plot_limit: + logger.warning('Ticker contained more than %s candles as defined ' + 'with --plot-limit, clipping.', args.plot_limit) + dataframe = dataframe.tail(args.plot_limit) + trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']] fig = generate_graph( pair=pair, trades=trades, - data=dataframe.tail(750), + data=dataframe, args=args ) @@ -333,11 +336,17 @@ def plot_parse_args(args: List[str]) -> Namespace: default='macd', dest='indicators2', ) - + arguments.parser.add_argument( + '--plot-limit', + help='Specify tick limit for plotting - too high values cause huge files - ' + 'Default: %(default)s', + dest='plot_limit', + default=750, + type=str, + ) arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) arguments.backtesting_options(arguments.parser) - return arguments.parse_args() From d8cb63efdde8d118b4939dda1d4e7a796a6402a6 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 20:19:07 +0200 Subject: [PATCH 39/86] extract load_trades --- scripts/plot_dataframe.py | 77 +++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ba4da444a..e7e38fcd3 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -39,7 +39,7 @@ from plotly.offline import plot import freqtrade.optimize as optimize from freqtrade import persistence from freqtrade.analyze import Analyze -from freqtrade.arguments import Arguments +from freqtrade.arguments import Arguments, TimeRange from freqtrade.exchange import Exchange from freqtrade.optimize.backtesting import setup_configuration from freqtrade.persistence import Trade @@ -48,6 +48,45 @@ logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} +def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: + trades: pd.DataFrame = pd.DataFrame() + if args.db_url: + persistence.init(_CONF) + columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] + + trades = pd.DataFrame([(t.pair, t.calc_profit(), + t.open_date, t.close_date, + t.open_rate, t.close_rate, + t.close_date.timestamp() - t.open_date.timestamp()) + for t in Trade.query.filter(Trade.pair.is_(pair)).all()], + columns=columns) + + if args.exportfilename: + file = Path(args.exportfilename) + # must align with columns in backtest.py + columns = ["pair", "profit", "opents", "closets", "index", "duration", + "open_rate", "close_rate", "open_at_end"] + with file.open() as f: + data = json.load(f) + trades = pd.DataFrame(data, columns=columns) + trades = trades.loc[trades["pair"] == pair] + if timerange: + if timerange.starttype == 'date': + trades = trades.loc[trades["opents"] >= timerange.startts] + if timerange.stoptype == 'date': + trades = trades.loc[trades["opents"] <= timerange.stopts] + + trades['opents'] = pd.to_datetime(trades['opents'], + unit='s', + utc=True, + infer_datetime_format=True) + trades['closets'] = pd.to_datetime(trades['closets'], + unit='s', + utc=True, + infer_datetime_format=True) + return trades + + def plot_analyzed_dataframe(args: Namespace) -> None: """ Calls analyze() and plots the returned dataframe @@ -107,41 +146,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: if args.db_url and args.exportfilename: logger.critical("Can only specify --db-url or --export-filename") # Get trades already made from the DB - trades: pd.DataFrame = pd.DataFrame() - if args.db_url: - persistence.init(_CONF) - columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] - - trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date, t.close_date, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp()) - for t in Trade.query.filter(Trade.pair.is_(pair)).all()], - columns=columns) - - if args.exportfilename: - file = Path(args.exportfilename) - # must align with columns in backtest.py - columns = ["pair", "profit", "opents", "closets", "index", "duration", - "open_rate", "close_rate", "open_at_end"] - with file.open() as f: - data = json.load(f) - trades = pd.DataFrame(data, columns=columns) - trades = trades.loc[trades["pair"] == pair] - if timerange: - if timerange.starttype == 'date': - trades = trades.loc[trades["opents"] >= timerange.startts] - if timerange.stoptype == 'date': - trades = trades.loc[trades["opents"] <= timerange.stopts] - - trades['opents'] = pd.to_datetime(trades['opents'], - unit='s', - utc=True, - infer_datetime_format=True) - trades['closets'] = pd.to_datetime(trades['closets'], - unit='s', - utc=True, - infer_datetime_format=True) + trades = load_trades(args, pair, timerange) dataframes = analyze.tickerdata_to_dataframe(tickers) dataframe = dataframes[pair] From 660ec6f443046f6a2f02a2ec23ccd2a4ad4f9658 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 24 Jun 2018 13:43:27 +0200 Subject: [PATCH 40/86] fix parameter type --- scripts/plot_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index e7e38fcd3..1cc6b818a 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -347,7 +347,7 @@ def plot_parse_args(args: List[str]) -> Namespace: 'Default: %(default)s', dest='plot_limit', default=750, - type=str, + type=int, ) arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) From 5e7e977ffab0b4f60f31ade8087bb038ec7c5ca6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 24 Jun 2018 14:23:05 +0200 Subject: [PATCH 41/86] Update ccxt from 1.14.256 to 1.14.257 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51312791e..437bb5533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.256 +ccxt==1.14.257 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From e70cb963f7cc9f82336c2fa07875b97d31e74c94 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 24 Jun 2018 17:00:00 +0200 Subject: [PATCH 42/86] document what to do with exported backtest results --- docs/backtesting.md | 28 ++++++++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 3 --- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 1efb46b43..172969ae2 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -70,6 +70,34 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ python3 ./freqtrade/main.py backtesting --export trades ``` +The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder. + +``` python +import json +from pathlib import Path +import pandas as pd + +filename=Path('user_data/backtest_data/backtest-result.json') + +with filename.open() as file: + data = json.load(file) + +columns = ["pair", "profit", "opents", "closets", "index", "duration", + "open_rate", "close_rate", "open_at_end"] +df = pd.DataFrame(data, columns=columns) + +df['opents'] = pd.to_datetime(df['opents'], + unit='s', + utc=True, + infer_datetime_format=True + ) +df['closets'] = pd.to_datetime(df['closets'], + unit='s', + utc=True, + infer_datetime_format=True + ) +``` + #### Exporting trades to file specifying a custom filename ```bash diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b5ed9b167..70f6168a4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -117,9 +117,6 @@ class Backtesting(object): def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: - # columns = ["pair", "profit", "opents", "closets", "index", "duration", - # "open_rate", "close_rate", "open_at_end"] - records = [(t.pair, t.profit_percent, t.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration, t.open_rate, t.close_rate, t.open_at_end) From 43f1a1d264765f106ad9f94b5fd1f3ffb70d20de Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 24 Jun 2018 19:52:12 +0200 Subject: [PATCH 43/86] rework download_backtest script --- freqtrade/arguments.py | 7 +++++ scripts/download_backtest_data.py | 48 ++++++++++++++++--------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 31232f1ff..8ce7c546d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -334,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/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: From 4f1fa28658d8c5d1ffa556f69022d2a56f8fdda8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 Jun 2018 14:23:06 +0200 Subject: [PATCH 44/86] Update ccxt from 1.14.257 to 1.14.267 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 437bb5533..c9d3a6529 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.257 +ccxt==1.14.267 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 7c2a50cef9b51edb4b5ef9e4ddb442096fed3a17 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 26 Jun 2018 14:23:06 +0200 Subject: [PATCH 45/86] Update ccxt from 1.14.267 to 1.14.272 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9d3a6529..d4f8b2f06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.267 +ccxt==1.14.272 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 54f52fb36664c375ab8b2c4ce64ea08c40cc7e48 Mon Sep 17 00:00:00 2001 From: Gert Wohlgemuth Date: Fri, 1 Jun 2018 20:50:40 -0700 Subject: [PATCH 46/86] Create stoploss.md --- docs/stoploss.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/stoploss.md diff --git a/docs/stoploss.md b/docs/stoploss.md new file mode 100644 index 000000000..ff93fd1d8 --- /dev/null +++ b/docs/stoploss.md @@ -0,0 +1,50 @@ +# Stop Loss support + +at this stage the bot contains the following stoploss support modes: + +1. static stop loss, defined in either the strategy or configuration + +2. trailing stop loss, defined in the configuration + +3. trailing stop loss, custom positive loss, defined in configuration + +## Static Stop Loss + +This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which +will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss. + +## Trail Stop Loss + +The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. +To enable this Feauture all you have to do, is to define the configuration element: + +``` +"trailing_stop" : True +``` +This will now actiave an algorithm, whihch automatically moves up your stop loss, every time the price of your asset increases. + +For example, simplified math, + +* you buy an asset at a price of 100$ +* your stop loss is defined at 2% +* which means your stop loss, gets triggered once your asset dropped below 98$ +* assuming your asset now increases in proce to 102$ +* your stop loss, will now be 2% of 102$ or 99.96$ +* now your asset drops in value to 101$, your stop loss, will still be 99.96$ + +basically what this means, is that your stop loss will be adjusted to be always be 2% of the highest observed price + +### Custom positive loss + +due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive, +the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the +black, it will be changed to be only a 1% stop loss + +this can be configured in the main confiuration file, the following way: + +``` + "trailing_stop": { + "positive" : 0.01 + }, +``` +The 0.01 would translate to a 1% stop loss, once you hit profit. From 257e1847b121dce297f5dc540ca52e3656c74517 Mon Sep 17 00:00:00 2001 From: peterkorodi Date: Sun, 17 Jun 2018 11:00:34 +0200 Subject: [PATCH 47/86] Update stoploss.md --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index ff93fd1d8..110cfa0d1 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -40,7 +40,7 @@ due to demand, it is possible to have a default stop loss, when you are in the r the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the black, it will be changed to be only a 1% stop loss -this can be configured in the main confiuration file, the following way: +this can be configured in the main configuration file, the following way: ``` "trailing_stop": { From 9ac3c559b6028bf0b382ed4a8adc8e0786f6b57c Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 20:26:28 +0200 Subject: [PATCH 48/86] fix some stoploss documentation --- docs/stoploss.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 110cfa0d1..cf6a2bcab 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -1,11 +1,9 @@ # Stop Loss support -at this stage the bot contains the following stoploss support modes: +At this stage the bot contains the following stoploss support modes: 1. static stop loss, defined in either the strategy or configuration - 2. trailing stop loss, defined in the configuration - 3. trailing stop loss, custom positive loss, defined in configuration ## Static Stop Loss @@ -16,35 +14,37 @@ will overwrite the strategy definition. This will basically try to sell your ass ## Trail Stop Loss The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. -To enable this Feauture all you have to do, is to define the configuration element: +To enable this Feauture all you have to do is to define the configuration element: -``` +``` json "trailing_stop" : True ``` -This will now actiave an algorithm, whihch automatically moves up your stop loss, every time the price of your asset increases. + +This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases. For example, simplified math, * you buy an asset at a price of 100$ * your stop loss is defined at 2% * which means your stop loss, gets triggered once your asset dropped below 98$ -* assuming your asset now increases in proce to 102$ +* assuming your asset now increases to 102$ * your stop loss, will now be 2% of 102$ or 99.96$ * now your asset drops in value to 101$, your stop loss, will still be 99.96$ -basically what this means, is that your stop loss will be adjusted to be always be 2% of the highest observed price +basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price ### Custom positive loss -due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive, +Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive, the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the black, it will be changed to be only a 1% stop loss this can be configured in the main configuration file, the following way: -``` +``` json "trailing_stop": { "positive" : 0.01 }, ``` + The 0.01 would translate to a 1% stop loss, once you hit profit. From 243c36b39bda5e126cd9ca6f805e70b15a57deab Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 20:49:07 +0200 Subject: [PATCH 49/86] get persistence.py for stop_loss --- freqtrade/persistence.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 20a7db4bb..76a499eed 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -151,6 +151,12 @@ class Trade(_DECL_BASE): open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) open_order_id = Column(String) + # absolute value of the stop loss + stop_loss = Column(Float, nullable=True, default=0.0) + # absolute value of the initial stop loss + initial_stop_loss = Column(Float, nullable=True, default=0.0) + # absolute value of the highest reached price + max_rate = Column(Float, nullable=True, default=0.0) def __repr__(self): return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( @@ -161,6 +167,49 @@ class Trade(_DECL_BASE): arrow.get(self.open_date).humanize() if self.is_open else 'closed' ) + def adjust_stop_loss(self, current_price, stoploss): + """ + this adjusts the stop loss to it's most recently observed + setting + :param current_price: + :param stoploss: + :return: + """ + + new_loss = Decimal(current_price * (1 - abs(stoploss))) + + # keeping track of the highest observed rate for this trade + if self.max_rate is None: + self.max_rate = current_price + else: + if current_price > self.max_rate: + self.max_rate = current_price + + # no stop loss assigned yet + if self.stop_loss is None or self.stop_loss == 0: + logger.debug("assigning new stop loss") + self.stop_loss = new_loss + self.initial_stop_loss = new_loss + + # evaluate if the stop loss needs to be updated + else: + if new_loss > self.stop_loss: # stop losses only walk up, never down! + self.stop_loss = new_loss + logger.debug("adjusted stop loss") + else: + logger.debug("keeping current stop loss") + + logger.debug( + "{} - current price {:.8f}, bought at {:.8f} and calculated " + "stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} " + "and max observed rate was {:.8f}".format( + self.pair, current_price, self.open_rate, + self.initial_stop_loss, + self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss), + self.max_rate + + )) + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. From 5015bc9bb09d8904131e37d7d26addd47a025e3a Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 22:41:28 +0200 Subject: [PATCH 50/86] slight update to persistence --- freqtrade/persistence.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 76a499eed..db97fc858 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -176,7 +176,7 @@ class Trade(_DECL_BASE): :return: """ - new_loss = Decimal(current_price * (1 - abs(stoploss))) + new_loss = float(current_price * (1 - abs(stoploss))) # keeping track of the highest observed rate for this trade if self.max_rate is None: @@ -200,15 +200,13 @@ class Trade(_DECL_BASE): logger.debug("keeping current stop loss") logger.debug( - "{} - current price {:.8f}, bought at {:.8f} and calculated " - "stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} " - "and max observed rate was {:.8f}".format( - self.pair, current_price, self.open_rate, - self.initial_stop_loss, - self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss), - self.max_rate - - )) + f"{self.pair} - current price {current_price:.8f}, " + f"bought at {self.open_rate:.8f} and calculated " + f"stop loss is at: {self.initial_stop_loss:.8f} initial " + f"stop at {self.stop_loss:.8f}. " + f"trailing stop loss saved us: " + f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} " + f"and max observed rate was {self.max_rate:.8f}") def update(self, order: Dict) -> None: """ From 3e167e11705b204bb2dcc504bf511bd7b2d9f6ca Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 22:41:38 +0200 Subject: [PATCH 51/86] update sample configs --- config.json.example | 5 ++++- config_full.json.example | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index e5bdc95b1..7b53ffa99 100644 --- a/config.json.example +++ b/config.json.example @@ -5,6 +5,9 @@ "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, + "trailing_stop": { + "positive" : 0.005 + }, "unfilledtimeout": 600, "bid_strategy": { "ask_last_balance": 0.0 @@ -32,7 +35,7 @@ "experimental": { "use_sell_signal": false, "sell_profit_only": false, - "ignore_roi_if_buy_signal": false + "sell_fullfilled_at_roi": false }, "telegram": { "enabled": true, diff --git a/config_full.json.example b/config_full.json.example index 231384acc..d7d8f7369 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -5,6 +5,7 @@ "fiat_display_currency": "USD", "dry_run": false, "ticker_interval": "5m", + "trailing_stop": true, "minimal_roi": { "40": 0.0, "30": 0.01, From da5be9fbd0ea132a9490ecca007d2e8adf568c34 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 23:06:27 +0200 Subject: [PATCH 52/86] add stop_loss based on work from @berlinguyinca --- config.json.example | 4 +--- config_full.json.example | 3 ++- docs/stoploss.md | 6 ++---- freqtrade/analyze.py | 42 ++++++++++++++++++++++++++++++++++++---- freqtrade/constants.py | 2 ++ 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/config.json.example b/config.json.example index 7b53ffa99..f92849ec9 100644 --- a/config.json.example +++ b/config.json.example @@ -5,9 +5,7 @@ "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, - "trailing_stop": { - "positive" : 0.005 - }, + "trailing_stop": false, "unfilledtimeout": 600, "bid_strategy": { "ask_last_balance": 0.0 diff --git a/config_full.json.example b/config_full.json.example index d7d8f7369..244daabac 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -5,7 +5,8 @@ "fiat_display_currency": "USD", "dry_run": false, "ticker_interval": "5m", - "trailing_stop": true, + "trailing_stop": false, + "trailing_stop_positive": 0.005, "minimal_roi": { "40": 0.0, "30": 0.01, diff --git a/docs/stoploss.md b/docs/stoploss.md index cf6a2bcab..db4433a02 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -39,12 +39,10 @@ Due to demand, it is possible to have a default stop loss, when you are in the r the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the black, it will be changed to be only a 1% stop loss -this can be configured in the main configuration file, the following way: +This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. ``` json - "trailing_stop": { - "positive" : 0.01 - }, + "trailing_stop_positive": 0.01, ``` The 0.01 would translate to a 1% stop loss, once you hit profit. diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 36e00dd0e..b283e3ae9 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -180,7 +180,7 @@ class Analyze(object): :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): + if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date): return True experimental = self.config.get('experimental', {}) @@ -204,12 +204,46 @@ class Analyze(object): return False - def stop_loss_reached(self, current_profit: float) -> bool: - """Based on current profit of the trade and configured stoploss, decides to sell or not""" + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime) -> bool: + """ + Based on current profit of the trade and configured (trailing) stoploss, + decides to sell or not + """ + + current_profit = trade.calc_profit_percent(current_rate) + trailing_stop = self.config.get('trailing_stop', False) + if trade.stop_loss is None: + # initially adjust the stop loss to the base value + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss) + + # evaluate if the stoploss was hit + if self.strategy.stoploss is not None and trade.stop_loss >= current_rate: + + if trailing_stop: + logger.debug( + f"HIT STOP: current price at {current_rate:.6f}, " + f"stop loss is {trade.stop_loss:.6f}, " + f"initial stop loss was at {trade.initial_stop_loss:.6f}, " + f"trade opened at {trade.open_rate:.6f}") + logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}") - if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: logger.debug('Stop loss hit.') return True + + # update the stop loss afterwards, after all by definition it's supposed to be hanging + if trailing_stop: + + # check if we have a special stop loss for positive condition + # and if profit is positive + stop_loss_value = self.strategy.stoploss + if 'trailing_stop_positive' in self.config and current_profit > 0: + + stop_loss_value = self.config.get('trailing_stop_positive') + logger.debug(f"using positive stop loss mode: {stop_loss_value} " + f"since we have profit {current_profit}") + + trade.adjust_stop_loss(current_rate, stop_loss_value) + return False def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0f12905e3..4ba1a7094 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -61,6 +61,8 @@ CONF_SCHEMA = { 'minProperties': 1 }, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, + 'trailing_stop': {'type': 'boolean'}, + 'trailing_stop_positive': {'type': 'number', 'minimum': 0}, 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, 'bid_strategy': { 'type': 'object', From 03005bc0f14f9f7747652a13980c8f8a209ded11 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 23:14:12 +0200 Subject: [PATCH 53/86] update documentation --- docs/configuration.md | 42 ++++++++++++++++++++++++++++++++++-------- docs/index.md | 2 ++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5279ed06e..f24adb61e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,12 +1,15 @@ # Configure the bot + This page explains how to configure your `config.json` file. ## Table of Contents + - [Bot commands](#bot-commands) - [Backtesting commands](#backtesting-commands) - [Hyperopt commands](#hyperopt-commands) ## Setup config.json + We recommend to copy and use the `config.json.example` as a template for your bot configuration. @@ -22,6 +25,8 @@ The table below will list all configuration parameters. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. +| `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). +| `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached. | `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). @@ -41,10 +46,10 @@ The table below will list all configuration parameters. | `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder). | `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second. -The definition of each config parameters is in -[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205). +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. @@ -52,10 +57,12 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set 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. See the example below: -``` + +``` json "minimal_roi": { "40": 0.0, # Sell after 40 minutes if the profit is not negative "30": 0.01, # Sell after 30 minutes if there is at least 1% profit @@ -69,6 +76,7 @@ value. This parameter is optional. If you use it, it will take over the `minimal_roi` value from the strategy file. ### Understand stoploss + `stoploss` is loss in percentage that should trigger a sale. For example value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. @@ -77,82 +85,100 @@ Most of the strategy files already include the optimal `stoploss` value. This parameter is optional. If you use it, it will take over the `stoploss` value from the strategy file. +### Understand trailing stoploss + +Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss. + ### Understand initial_state + `initial_state` is an optional field that defines the initial application state. Possible values are `running` or `stopped`. (default=`running`) If the value is `stopped` the bot has to be started with `/start` first. ### Understand process_throttle_secs + `process_throttle_secs` is an optional field that defines in seconds how long the bot should wait before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or the static list of pairs) if we should buy. ### Understand ask_last_balance + `ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary. ### What values for exchange.name? + Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested with only Bittrex and Binance. The bot was tested with the following exchanges: + - [Bittrex](https://bittrex.com/): "bittrex" - [Binance](https://www.binance.com/): "binance" Feel free to test other exchanges and submit your PR to improve the bot. ### What values for fiat_display_currency? + `fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram. The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD". In addition to central bank currencies, a range of cryto currencies are supported. The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT". ## Switch to dry-run mode + We recommend starting the bot in dry-run mode to see how your bot will behave and how is the performance of your strategy. In Dry-run mode the bot does not engage your money. It only runs a live simulation without creating trades. ### To switch your bot in Dry-run mode: + 1. Edit your `config.json` file 2. Switch dry-run to true and specify db_url for a persistent db + ```json "dry_run": true, "db_url": "sqlite///tradesv3.dryrun.sqlite", ``` 3. Remove your Exchange API key (change them by fake api credentials) + ```json "exchange": { "name": "bittrex", "key": "key", "secret": "secret", ... -} +} ``` Once you will be happy with your bot performance, you can switch it to production mode. ## Switch to production mode + In production mode, the bot will engage your money. Be careful a wrong strategy can lose all your money. Be aware of what you are doing when you run it in production mode. ### To switch your bot in production mode: + 1. Edit your `config.json` file 2. Switch dry-run to false and don't forget to adapt your database URL if set + ```json "dry_run": false, ``` 3. Insert your Exchange API key (change them by fake api keys) + ```json "exchange": { "name": "bittrex", @@ -160,10 +186,10 @@ you run it in production mode. "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", ... } + ``` -If you have not your Bittrex API key yet, -[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md). +If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md). ## Next step -Now you have configured your config.json, the next step is to -[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). + +Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). diff --git a/docs/index.md b/docs/index.md index afde2d5eb..fd6bf4378 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ # freqtrade documentation + Welcome to freqtrade documentation. Please feel free to contribute to this documentation if you see it became outdated by sending us a Pull-request. Do not hesitate to reach us on @@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on if you do not find the answer to your questions. ## Table of Contents + - [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md) - [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account) - [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot) From a3708bc56e17912af630cd2258f2920ec9d4ec17 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 23:40:20 +0200 Subject: [PATCH 54/86] add missing test --- freqtrade/tests/test_analyze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 89dac21c6..02225acca 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -42,6 +42,7 @@ def test_analyze_object() -> None: assert hasattr(Analyze, 'get_signal') assert hasattr(Analyze, 'should_sell') assert hasattr(Analyze, 'min_roi_reached') + assert hasattr(Analyze, 'stop_loss_reached') def test_dataframe_correct_length(result): From 8bec505bbe23919da0035f86627d63a150ab445d Mon Sep 17 00:00:00 2001 From: xmatthias Date: Tue, 26 Jun 2018 23:40:36 +0200 Subject: [PATCH 55/86] add test for trailing_stoploss --- freqtrade/tests/test_freqtradebot.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1cb2bfca2..3568dad1d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1654,6 +1654,43 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> assert freqtrade.handle_trade(trade) is True +def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, 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['trailing_stop'] = True + print(limit_buy_order) + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + trade.stop_loss = 3.2 + caplog.set_level(logging.DEBUG) + assert freqtrade.handle_trade(trade) is True + assert log_has( + f'HIT STOP: current price at 0.000017, stop loss is {trade.stop_loss:.6f}, ' + f'initial stop loss was at 0.000000, trade opened at 0.000011', caplog.record_tuples) + + + def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: """ From 88b898cce4a434cd15567581f42663056fe05da0 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 00:16:19 +0200 Subject: [PATCH 56/86] add test for moving stoploss --- freqtrade/tests/test_freqtradebot.py | 63 ++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 3568dad1d..468f5f8aa 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1661,14 +1661,14 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) + mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 + 'bid': 0.00000102, + 'ask': 0.00000103, + 'last': 0.00000102 }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, @@ -1682,14 +1682,61 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) trade = Trade.query.first() trade.update(limit_buy_order) - trade.stop_loss = 3.2 caplog.set_level(logging.DEBUG) + # Sell as trailing-stop is reached + assert freqtrade.handle_trade(trade) is True + assert log_has( + f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, ' + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + + +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, 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.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.0000182, + 'ask': 0.0000183, + 'last': 0.0000182 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + conf = deepcopy(default_conf) + conf['trailing_stop'] = True + conf['trailing_stop_positive'] = 0.01 + print(limit_buy_order) + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + caplog.set_level(logging.DEBUG) + # stop-loss not reached + assert freqtrade.handle_trade(trade) is False + # Adjusted stoploss + assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.64779142', + caplog.record_tuples) + assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert trade.stop_loss == 0.000018018 + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': 0.0000172, + 'ask': 0.0000173, + 'last': 0.0000172 + })) assert freqtrade.handle_trade(trade) is True assert log_has( f'HIT STOP: current price at 0.000017, stop loss is {trade.stop_loss:.6f}, ' - f'initial stop loss was at 0.000000, trade opened at 0.000011', caplog.record_tuples) - - + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: From e9d5bceeb98b12aa35c020fea29d84bf24088ed4 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 00:18:50 +0200 Subject: [PATCH 57/86] cleanly check if stop_loss is initialized --- freqtrade/analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index b283e3ae9..b8caa45b7 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -212,7 +212,7 @@ class Analyze(object): current_profit = trade.calc_profit_percent(current_rate) trailing_stop = self.config.get('trailing_stop', False) - if trade.stop_loss is None: + if trade.stop_loss is None or trade.stop_loss == 0: # initially adjust the stop loss to the base value trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss) From a91d75b3b2acdcce30a5e7bc9745430072da00d3 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 06:23:49 +0200 Subject: [PATCH 58/86] Add test for adjust_stop-loss --- freqtrade/tests/test_persistence.py | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 30ad239a1..6ffcbbf65 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -453,3 +453,37 @@ def test_migrate_new(mocker, default_conf, fee): assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" + + +def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + ) + + # Get percent of profit with a custom rate (Higher than open rate) + trade.adjust_stop_loss(1, 0.05) + assert trade.stop_loss == 0.95 + assert trade.max_rate == 1 + assert trade.initial_stop_loss == 0.95 + + trade.adjust_stop_loss(1.3, -0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.max_rate == 1.3 + assert trade.initial_stop_loss == 0.95 + + # current rate lower ... should not change + trade.adjust_stop_loss(1.2, 0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.max_rate == 1.3 + assert trade.initial_stop_loss == 0.95 + + # current rate higher... should raise stoploss + trade.adjust_stop_loss(1.4, 0.1) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.max_rate == 1.4 + assert trade.initial_stop_loss == 0.95 From c997aa9864ef0aec7b89bfb59982f7b99a609e39 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 06:38:49 +0200 Subject: [PATCH 59/86] move initial logic to persistence --- freqtrade/analyze.py | 5 ++--- freqtrade/persistence.py | 12 ++++++++---- freqtrade/tests/test_persistence.py | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index b8caa45b7..d74f9566a 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -212,9 +212,8 @@ class Analyze(object): current_profit = trade.calc_profit_percent(current_rate) trailing_stop = self.config.get('trailing_stop', False) - if trade.stop_loss is None or trade.stop_loss == 0: - # initially adjust the stop loss to the base value - trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss) + + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) # evaluate if the stoploss was hit if self.strategy.stoploss is not None and trade.stop_loss >= current_rate: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index db97fc858..4fc83a18f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -167,15 +167,19 @@ class Trade(_DECL_BASE): arrow.get(self.open_date).humanize() if self.is_open else 'closed' ) - def adjust_stop_loss(self, current_price, stoploss): + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool =False): """ - this adjusts the stop loss to it's most recently observed - setting + this adjusts the stop loss to it's most recently observed setting :param current_price: - :param stoploss: + :param stoploss in percent: + :param initial: :return: """ + if initial and not (self.stop_loss is None or self.stop_loss == 0): + # Don't modify if called with initial and nothing to do + return + new_loss = float(current_price * (1 - abs(stoploss))) # keeping track of the highest observed rate for this trade diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 6ffcbbf65..45957853f 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -465,18 +465,24 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): open_rate=1, ) - # Get percent of profit with a custom rate (Higher than open rate) - trade.adjust_stop_loss(1, 0.05) + trade.adjust_stop_loss(trade.open_rate, 0.05, True) assert trade.stop_loss == 0.95 assert trade.max_rate == 1 assert trade.initial_stop_loss == 0.95 + # Get percent of profit with a lowre rate + trade.adjust_stop_loss(0.96, 0.05) + assert trade.stop_loss == 0.95 + assert trade.max_rate == 1 + assert trade.initial_stop_loss == 0.95 + + # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(1.3, -0.1) assert round(trade.stop_loss, 8) == 1.17 assert trade.max_rate == 1.3 assert trade.initial_stop_loss == 0.95 - # current rate lower ... should not change + # current rate lower again ... should not change trade.adjust_stop_loss(1.2, 0.1) assert round(trade.stop_loss, 8) == 1.17 assert trade.max_rate == 1.3 @@ -487,3 +493,9 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): assert round(trade.stop_loss, 8) == 1.26 assert trade.max_rate == 1.4 assert trade.initial_stop_loss == 0.95 + + # Initial is true but stop_loss set - so doesn't do anything + trade.adjust_stop_loss(1.7, 0.1, True) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.max_rate == 1.4 + assert trade.initial_stop_loss == 0.95 From 78e6c9fdf680307c262e1246d02e4d76a877c90a Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 06:51:48 +0200 Subject: [PATCH 60/86] add tests for trailing stoploss --- freqtrade/tests/test_freqtradebot.py | 41 ++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 468f5f8aa..7b2786421 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1694,6 +1694,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, """ Test sell_profit_only feature when enabled and we have a loss """ + buy_price = limit_buy_order['price'] patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) @@ -1702,9 +1703,9 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ - 'bid': 0.0000182, - 'ask': 0.0000183, - 'last': 0.0000182 + 'bid': buy_price - 0.000001, + 'ask': buy_price - 0.000001, + 'last': buy_price - 0.000001 }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, @@ -1713,7 +1714,6 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, conf = deepcopy(default_conf) conf['trailing_stop'] = True conf['trailing_stop_positive'] = 0.01 - print(limit_buy_order) freqtrade = FreqtradeBot(conf) freqtrade.create_trade() @@ -1722,22 +1722,35 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, caplog.set_level(logging.DEBUG) # stop-loss not reached assert freqtrade.handle_trade(trade) is False - # Adjusted stoploss - assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.64779142', - caplog.record_tuples) - assert log_has(f'adjusted stop loss', caplog.record_tuples) - assert trade.stop_loss == 0.000018018 + + # Raise ticker above buy price mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - })) + 'bid': buy_price + 0.000003, + 'ask': buy_price + 0.000003, + 'last': buy_price + 0.000003 + })) + # stop-loss not reached, adjusted stoploss + assert freqtrade.handle_trade(trade) is False + assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643', + caplog.record_tuples) + assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert trade.stop_loss == 0.0000138501 + + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': buy_price + 0.000002, + 'ask': buy_price + 0.000002, + 'last': buy_price + 0.000002 + })) + # Lower price again (but still positive) assert freqtrade.handle_trade(trade) is True assert log_has( - f'HIT STOP: current price at 0.000017, stop loss is {trade.stop_loss:.6f}, ' + f'HIT STOP: current price at {buy_price + 0.000002:.6f}, ' + f'stop loss is {trade.stop_loss:.6f}, ' f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: """ From e6e868a03c3f4b6f94d89449509b8c618f53b84f Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 06:54:29 +0200 Subject: [PATCH 61/86] remove markdown code type as it is not valid json --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index f24adb61e..984f2529b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,7 +62,7 @@ In this case a trade amount is calclulated as `currency_balanse / (max_open_trad in minutes and the value is the minimum ROI in percent. See the example below: -``` json +``` "minimal_roi": { "40": 0.0, # Sell after 40 minutes if the profit is not negative "30": 0.01, # Sell after 30 minutes if there is at least 1% profit From 8ecdae67e1417de14177da25ce30df8a28c7ad66 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 06:57:41 +0200 Subject: [PATCH 62/86] add mypy ignore (and comment as to why) --- freqtrade/analyze.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index d74f9566a..bf6b89b17 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -237,7 +237,8 @@ class Analyze(object): stop_loss_value = self.strategy.stoploss if 'trailing_stop_positive' in self.config and current_profit > 0: - stop_loss_value = self.config.get('trailing_stop_positive') + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore logger.debug(f"using positive stop loss mode: {stop_loss_value} " f"since we have profit {current_profit}") From 19beb0941f4354e00a89238ff1630d86d5df9050 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 27 Jun 2018 14:23:07 +0200 Subject: [PATCH 63/86] Update ccxt from 1.14.272 to 1.14.288 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d4f8b2f06..62e2e3983 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.272 +ccxt==1.14.288 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 860b270e309a022e607d789754eb3ab338475d58 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 19:49:08 +0200 Subject: [PATCH 64/86] update db migrate script to work for more changes --- freqtrade/persistence.py | 33 +++++++++++++++++++---------- freqtrade/tests/test_persistence.py | 3 +++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 4fc83a18f..c72974c61 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -66,6 +66,10 @@ def has_column(columns, searchname: str) -> bool: return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 +def get_column_def(columns, column: str, default: str) -> str: + return default if not has_column(columns, column) else column + + def check_migrate(engine) -> None: """ Checks if migration is necessary and migrates if necessary @@ -74,17 +78,26 @@ def check_migrate(engine) -> None: cols = inspector.get_columns('trades') - if not has_column(cols, 'fee_open'): + # Check for latest column + if not has_column(cols, 'max_rate'): + open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') + close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') + stop_loss = get_column_def(cols, 'stop_loss', '0.0') + initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + max_rate = get_column_def(cols, 'max_rate', '0.0') + # Schema migration necessary engine.execute("alter table trades rename to trades_bak") # let SQLAlchemy create the schema as required _DECL_BASE.metadata.create_all(engine) # Copy data back - following the correct schema - engine.execute("""insert into trades + engine.execute(f"""insert into trades (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, - stake_amount, amount, open_date, close_date, open_order_id) + stake_amount, amount, open_date, close_date, open_order_id, + stop_loss, initial_stop_loss, max_rate + ) select id, lower(exchange), case when instr(pair, '_') != 0 then @@ -94,9 +107,12 @@ def check_migrate(engine) -> None: end pair, is_open, fee fee_open, fee fee_close, - open_rate, null open_rate_requested, close_rate, - null close_rate_requested, close_profit, - stake_amount, amount, open_date, close_date, open_order_id + open_rate, {open_rate_requested} open_rate_requested, close_rate, + {close_rate_requested} close_rate_requested, close_profit, + stake_amount, amount, open_date, close_date, open_order_id, + {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, + {max_rate} max_rate + from trades_bak """) @@ -104,11 +120,6 @@ def check_migrate(engine) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') - if not has_column(cols, 'open_rate_requested'): - engine.execute("alter table trades add open_rate_requested float") - if not has_column(cols, 'close_rate_requested'): - engine.execute("alter table trades add close_rate_requested float") - def cleanup() -> None: """ diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 45957853f..0cd2d089c 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -400,6 +400,9 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" assert trade.exchange == "bittrex" + assert trade.max_rate == 0.0 + assert trade.stop_loss == 0.0 + assert trade.initial_stop_loss == 0.0 def test_migrate_new(mocker, default_conf, fee): From d5ad066f8d4af30340da2e12eaef61a88bf48224 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Wed, 27 Jun 2018 20:15:25 +0200 Subject: [PATCH 65/86] support multiple db transitions by keeping the backup-table dynamic --- freqtrade/persistence.py | 12 +++++++++--- freqtrade/tests/test_persistence.py | 10 +++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index c72974c61..2b226e53a 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -77,6 +77,13 @@ def check_migrate(engine) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') + tabs = inspector.get_table_names() + table_back_name = 'trades_bak' + i = 0 + while table_back_name in tabs: + i += 1 + table_back_name = f'trades_bak{i}' + logger.info(f'trying {table_back_name}') # Check for latest column if not has_column(cols, 'max_rate'): @@ -87,7 +94,7 @@ def check_migrate(engine) -> None: max_rate = get_column_def(cols, 'max_rate', '0.0') # Schema migration necessary - engine.execute("alter table trades rename to trades_bak") + engine.execute(f"alter table trades rename to {table_back_name}") # let SQLAlchemy create the schema as required _DECL_BASE.metadata.create_all(engine) @@ -112,8 +119,7 @@ def check_migrate(engine) -> None: stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, {max_rate} max_rate - - from trades_bak + from {table_back_name} """) # Reread columns - the above recreated the table! diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 0cd2d089c..7ef4d2c25 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -7,6 +7,7 @@ from sqlalchemy import create_engine from freqtrade import constants, OperationalException from freqtrade.persistence import Trade, init, clean_dry_run_db +from freqtrade.tests.conftest import log_has @pytest.fixture(scope='function') @@ -405,7 +406,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.initial_stop_loss == 0.0 -def test_migrate_new(mocker, default_conf, fee): +def test_migrate_new(mocker, default_conf, fee, caplog): """ Test Database migration (starting with new pairformat) """ @@ -442,6 +443,9 @@ def test_migrate_new(mocker, default_conf, fee): # Create table using the old format engine.execute(create_table_old) engine.execute(insert_table_old) + + # fake previous backup + engine.execute("create table trades_bak as select * from trades") # Run init to test migration init(default_conf) @@ -456,6 +460,10 @@ def test_migrate_new(mocker, default_conf, fee): assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" + assert trade.max_rate == 0.0 + assert trade.stop_loss == 0.0 + assert trade.initial_stop_loss == 0.0 + assert log_has("trying trades_bak1", caplog.record_tuples) def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): From 7cecae52799eb480e2265802e41ef0bd863d5a1c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 28 Jun 2018 14:23:07 +0200 Subject: [PATCH 66/86] Update ccxt from 1.14.288 to 1.14.289 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62e2e3983..76dba3c49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.288 +ccxt==1.14.289 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 2d4ce593b511e3f9cd977a307c002307b4c6a052 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 19:48:05 +0200 Subject: [PATCH 67/86] catch crash with cobinhood fixes #959 --- freqtrade/freqtradebot.py | 11 +++++++---- freqtrade/tests/test_freqtradebot.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e25ed66cf..985ecb370 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -277,11 +277,14 @@ class FreqtradeBot(object): return None min_stake_amounts = [] - if 'cost' in market['limits'] and 'min' in market['limits']['cost']: - min_stake_amounts.append(market['limits']['cost']['min']) + limits = market['limits'] + if ('cost' in limits and 'min' in limits['cost'] + and limits['cost']['min'] is not None): + min_stake_amounts.append(limits['cost']['min']) - if 'amount' in market['limits'] and 'min' in market['limits']['amount']: - min_stake_amounts.append(market['limits']['amount']['min'] * price) + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_stake_amounts.append(limits['amount']['min'] * price) if not min_stake_amounts: return None diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1cb2bfca2..ff1b7161d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -348,6 +348,34 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) assert result is None + # no cost Min + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {"min": None}, + 'amount': {} + } + }]) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) + assert result is None + + # no amount Min + mocker.patch( + 'freqtrade.exchange.Exchange.get_markets', + MagicMock(return_value=[{ + 'symbol': 'ETH/BTC', + 'limits': { + 'cost': {}, + 'amount': {"min": None} + } + }]) + ) + 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', From 8ec9a0974978242fb1bd47587151eec6a0a5bda6 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 21:22:43 +0200 Subject: [PATCH 68/86] Standardize retrier exception testing --- freqtrade/tests/exchange/test_exchange.py | 141 ++++++---------------- 1 file changed, 39 insertions(+), 102 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 53e59b34b..4d46d7e61 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,6 +14,22 @@ from freqtrade.exchange import Exchange, API_RETRY_COUNT from freqtrade.tests.conftest import log_has, get_patched_exchange +def validate_exceptionhandlers(mocker, default_conf, api_mock, fun, innerfun, **kwargs): + """Function to test exception handling """ + + with pytest.raises(TemporaryError): + api_mock.__dict__[innerfun] = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[innerfun].call_count == API_RETRY_COUNT + 1 + + with pytest.raises(OperationalException): + api_mock.__dict__[innerfun] = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[innerfun].call_count == 1 + + def test_init(default_conf, mocker, caplog): caplog.set_level(logging.INFO) get_patched_exchange(mocker, default_conf) @@ -243,17 +259,8 @@ def test_get_balances_prod(default_conf, mocker): assert exchange.get_balances()['1ST']['total'] == 10.0 assert exchange.get_balances()['1ST']['used'] == 0.0 - with pytest.raises(TemporaryError): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_balances() - assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1 - - with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_balances() - assert api_mock.fetch_balance.call_count == 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + "get_balances", "fetch_balance") def test_get_tickers(default_conf, mocker): @@ -282,15 +289,8 @@ def test_get_tickers(default_conf, mocker): assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 - with pytest.raises(TemporaryError): # test retrier - api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_tickers() - - with pytest.raises(OperationalException): - api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_tickers() + validate_exceptionhandlers(mocker, default_conf, api_mock, + "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) @@ -345,15 +345,9 @@ def test_get_ticker(default_conf, mocker): exchange.get_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 - with pytest.raises(TemporaryError): # test retrier - api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_ticker(pair='ETH/BTC', refresh=True) - - with pytest.raises(OperationalException): - api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_ticker(pair='ETH/BTC', refresh=True) + validate_exceptionhandlers(mocker, default_conf, api_mock, + "get_ticker", "fetch_ticker", + pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -416,17 +410,9 @@ def test_get_ticker_history(default_conf, mocker): assert ticks[0][4] == 9 assert ticks[0][5] == 10 - with pytest.raises(TemporaryError): # test retrier - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - # new symbol to get around cache - exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval']) - - with pytest.raises(OperationalException): - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - # new symbol to get around cache - exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval']) + validate_exceptionhandlers(mocker, default_conf, api_mock, + "get_ticker_history", "fetch_ohlcv", + pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) def test_get_ticker_history_sort(default_conf, mocker): @@ -515,24 +501,15 @@ def test_cancel_order(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 - with pytest.raises(TemporaryError): - api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError) - - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.cancel_order(order_id='_', pair='TKN/BTC') - assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 - with pytest.raises(DependencyException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 - with pytest.raises(OperationalException): - api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.cancel_order(order_id='_', pair='TKN/BTC') - assert api_mock.cancel_order.call_count == 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + "cancel_order", "cancel_order", + order_id='_', pair='TKN/BTC') def test_get_order(default_conf, mocker): @@ -550,23 +527,15 @@ def test_get_order(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) assert exchange.get_order('X', 'TKN/BTC') == 456 - with pytest.raises(TemporaryError): - api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 - with pytest.raises(DependencyException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 - with pytest.raises(OperationalException): - api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + 'get_order', 'fetch_order', + order_id='_', pair='TKN/BTC') def test_name(default_conf, mocker): @@ -651,19 +620,9 @@ def test_get_trades_for_order(default_conf, mocker): assert len(orders) == 1 assert orders[0]['price'] == 165 - # test Exceptions - with pytest.raises(OperationalException): - api_mock = MagicMock() - api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_trades_for_order(order_id, 'LTC/BTC', since) - - with pytest.raises(TemporaryError): - api_mock = MagicMock() - api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_trades_for_order(order_id, 'LTC/BTC', since) - assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + 'get_trades_for_order', 'fetch_my_trades', + order_id=order_id, pair='LTC/BTC', since=since) def test_get_markets(default_conf, mocker, markets): @@ -677,19 +636,8 @@ def test_get_markets(default_conf, mocker, markets): assert ret[0]["id"] == "ethbtc" assert ret[0]["symbol"] == "ETH/BTC" - # test Exceptions - with pytest.raises(OperationalException): - api_mock = MagicMock() - api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_markets() - - with pytest.raises(TemporaryError): - api_mock = MagicMock() - api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_markets() - assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + 'get_markets', 'fetch_markets') def test_get_fee(default_conf, mocker): @@ -704,19 +652,8 @@ def test_get_fee(default_conf, mocker): assert exchange.get_fee() == 0.025 - # test Exceptions - with pytest.raises(OperationalException): - api_mock = MagicMock() - api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_fee() - - with pytest.raises(TemporaryError): - api_mock = MagicMock() - api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_fee() - assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1 + validate_exceptionhandlers(mocker, default_conf, api_mock, + 'get_fee', 'calculate_fee') def test_get_amount_lots(default_conf, mocker): From ebbfc720b24c6a378dcf79b5b1c5205f8dd95dea Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 21:51:59 +0200 Subject: [PATCH 69/86] increase test coverage --- freqtrade/tests/exchange/test_exchange.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 4d46d7e61..75394ca84 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -232,6 +232,12 @@ def test_get_balance_prod(default_conf, mocker): exchange.get_balance(currency='BTC') + with pytest.raises(TemporaryError): + # api_mock.fetch_balance = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) + exchange.get_balance(currency='BTC') + def test_get_balances_dry_run(default_conf, mocker): default_conf['dry_run'] = True @@ -624,6 +630,9 @@ def test_get_trades_for_order(default_conf, mocker): 'get_trades_for_order', 'fetch_my_trades', order_id=order_id, pair='LTC/BTC', since=since) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == [] + def test_get_markets(default_conf, mocker, markets): api_mock = MagicMock() From fe8a21681e3fd8af90093a9fd004e7acd2c51f77 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 21:56:37 +0200 Subject: [PATCH 70/86] add test for Not supported --- freqtrade/tests/exchange/test_exchange.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 75394ca84..012c55356 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -232,8 +232,7 @@ def test_get_balance_prod(default_conf, mocker): exchange.get_balance(currency='BTC') - with pytest.raises(TemporaryError): - # api_mock.fetch_balance = MagicMock(return_value={}) + with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): exchange = get_patched_exchange(mocker, default_conf, api_mock) mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) exchange.get_balance(currency='BTC') @@ -420,6 +419,11 @@ def test_get_ticker_history(default_conf, mocker): "get_ticker_history", "fetch_ohlcv", pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + def test_get_ticker_history_sort(default_conf, mocker): api_mock = MagicMock() From 15c7854e7f79c03132e86d789f7725108aec4529 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 22:11:45 +0200 Subject: [PATCH 71/86] add test for exchange_has --- freqtrade/tests/exchange/test_exchange.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 012c55356..25c6fa0c2 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -113,6 +113,20 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog): Exchange(conf) +def test_exchangehas(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf) + assert not exchange.exchange_has('ASDFASDF') + api_mock = MagicMock() + + type(api_mock).has = PropertyMock(return_value={'deadbeef': True}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + assert exchange.exchange_has("deadbeef") + + type(api_mock).has = PropertyMock(return_value={'deadbeef': False}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + assert not exchange.exchange_has("deadbeef") + + def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf) From dcdc18a33868a5630b5b747eb2773819154fb88c Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 22:18:38 +0200 Subject: [PATCH 72/86] rename test-function --- freqtrade/tests/exchange/test_exchange.py | 50 +++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 25c6fa0c2..712205893 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,8 +14,8 @@ from freqtrade.exchange import Exchange, API_RETRY_COUNT from freqtrade.tests.conftest import log_has, get_patched_exchange -def validate_exceptionhandlers(mocker, default_conf, api_mock, fun, innerfun, **kwargs): - """Function to test exception handling """ +def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, innerfun, **kwargs): + """Function to test ccxt exception handling """ with pytest.raises(TemporaryError): api_mock.__dict__[innerfun] = MagicMock(side_effect=ccxt.NetworkError) @@ -278,8 +278,8 @@ def test_get_balances_prod(default_conf, mocker): assert exchange.get_balances()['1ST']['total'] == 10.0 assert exchange.get_balances()['1ST']['used'] == 0.0 - validate_exceptionhandlers(mocker, default_conf, api_mock, - "get_balances", "fetch_balance") + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + "get_balances", "fetch_balance") def test_get_tickers(default_conf, mocker): @@ -308,8 +308,8 @@ def test_get_tickers(default_conf, mocker): assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 - validate_exceptionhandlers(mocker, default_conf, api_mock, - "get_tickers", "fetch_tickers") + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) @@ -364,9 +364,9 @@ def test_get_ticker(default_conf, mocker): exchange.get_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 - validate_exceptionhandlers(mocker, default_conf, api_mock, - "get_ticker", "fetch_ticker", - pair='ETH/BTC', refresh=True) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + "get_ticker", "fetch_ticker", + pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -429,9 +429,9 @@ def test_get_ticker_history(default_conf, mocker): assert ticks[0][4] == 9 assert ticks[0][5] == 10 - validate_exceptionhandlers(mocker, default_conf, api_mock, - "get_ticker_history", "fetch_ohlcv", - pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + "get_ticker_history", "fetch_ohlcv", + pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) @@ -531,9 +531,9 @@ def test_cancel_order(default_conf, mocker): exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 - validate_exceptionhandlers(mocker, default_conf, api_mock, - "cancel_order", "cancel_order", - order_id='_', pair='TKN/BTC') + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + "cancel_order", "cancel_order", + order_id='_', pair='TKN/BTC') def test_get_order(default_conf, mocker): @@ -557,9 +557,9 @@ def test_get_order(default_conf, mocker): exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 - validate_exceptionhandlers(mocker, default_conf, api_mock, - 'get_order', 'fetch_order', - order_id='_', pair='TKN/BTC') + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + 'get_order', 'fetch_order', + order_id='_', pair='TKN/BTC') def test_name(default_conf, mocker): @@ -644,9 +644,9 @@ def test_get_trades_for_order(default_conf, mocker): assert len(orders) == 1 assert orders[0]['price'] == 165 - validate_exceptionhandlers(mocker, default_conf, api_mock, - 'get_trades_for_order', 'fetch_my_trades', - order_id=order_id, pair='LTC/BTC', since=since) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + 'get_trades_for_order', 'fetch_my_trades', + order_id=order_id, pair='LTC/BTC', since=since) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == [] @@ -663,8 +663,8 @@ def test_get_markets(default_conf, mocker, markets): assert ret[0]["id"] == "ethbtc" assert ret[0]["symbol"] == "ETH/BTC" - validate_exceptionhandlers(mocker, default_conf, api_mock, - 'get_markets', 'fetch_markets') + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + 'get_markets', 'fetch_markets') def test_get_fee(default_conf, mocker): @@ -679,8 +679,8 @@ def test_get_fee(default_conf, mocker): assert exchange.get_fee() == 0.025 - validate_exceptionhandlers(mocker, default_conf, api_mock, - 'get_fee', 'calculate_fee') + ccxt_exceptionhandlers(mocker, default_conf, api_mock, + 'get_fee', 'calculate_fee') def test_get_amount_lots(default_conf, mocker): From cf6b1a637ac0a2312c15d926093851a3a1adf4f1 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Thu, 28 Jun 2018 22:32:28 +0200 Subject: [PATCH 73/86] increase exchange code coverage --- freqtrade/tests/exchange/test_exchange.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 712205893..99b960885 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -36,7 +36,7 @@ def test_init(default_conf, mocker, caplog): assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) -def test_init_exception(default_conf): +def test_init_exception(default_conf, mocker): default_conf['exchange']['name'] = 'wrong_exchange_name' with pytest.raises( @@ -44,6 +44,13 @@ def test_init_exception(default_conf): match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): Exchange(default_conf) + default_conf['exchange']['name'] = 'binance' + with pytest.raises( + OperationalException, + match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): + mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError)) + Exchange(default_conf) + def test_validate_pairs(default_conf, mocker): api_mock = MagicMock() From 8a941f3aa81ee6aad4778be0e1ae0952ce3108c9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 29 Jun 2018 14:23:06 +0200 Subject: [PATCH 74/86] Update ccxt from 1.14.289 to 1.14.295 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76dba3c49..6995c2513 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.289 +ccxt==1.14.295 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 98108a78f1ff5ccd0ca4390a20885137f2d65637 Mon Sep 17 00:00:00 2001 From: Nullart Date: Thu, 14 Jun 2018 09:32:52 +0800 Subject: [PATCH 75/86] separating unfulfilled timeouts for buy and sell --- config.json.example | 5 ++++- config_full.json.example | 5 ++++- freqtrade/constants.py | 9 ++++++++- freqtrade/freqtradebot.py | 19 ++++++++++++------- freqtrade/tests/conftest.py | 5 ++++- freqtrade/tests/test_freqtradebot.py | 8 ++++---- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/config.json.example b/config.json.example index e5bdc95b1..336a0c374 100644 --- a/config.json.example +++ b/config.json.example @@ -5,7 +5,10 @@ "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, - "unfilledtimeout": 600, + "unfilledtimeout": { + "buy":10, + "sell":30 + } "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/config_full.json.example b/config_full.json.example index 231384acc..1985edc70 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -12,7 +12,10 @@ "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": 600, + "unfilledtimeout": { + "buy":10, + "sell":30 + } "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0f12905e3..44bd64fe9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -61,7 +61,14 @@ CONF_SCHEMA = { 'minProperties': 1 }, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, - 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, + 'unfilledtimeout': { + 'type': 'object', + 'properties': { + 'use_book_order': {'type': 'boolean'}, + 'buy': {'type': 'number', 'minimum': 3}, + 'sell': {'type': 'number', 'minimum': 10} + } + }, 'bid_strategy': { 'type': 'object', 'properties': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e25ed66cf..c5f932119 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -160,7 +160,7 @@ class FreqtradeBot(object): if 'unfilledtimeout' in self.config: # Check and handle any timed out open orders - self.check_handle_timedout(self.config['unfilledtimeout']) + self.check_handle_timedout() Trade.session.flush() except TemporaryError as error: @@ -492,13 +492,16 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False - def check_handle_timedout(self, timeoutvalue: int) -> None: + def check_handle_timedout(self) -> None: """ Check if any orders are timed out and cancel if neccessary :param timeoutvalue: Number of minutes until order is considered timed out :return: None """ - timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime + buy_timeout = self.config['unfilledtimeout']['buy'] + sell_timeout = self.config['unfilledtimeout']['sell'] + buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime + sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): try: @@ -521,10 +524,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ if int(order['remaining']) == 0: continue - if order['side'] == 'buy' and ordertime < timeoutthreashold: - self.handle_timedout_limit_buy(trade, order) - elif order['side'] == 'sell' and ordertime < timeoutthreashold: - self.handle_timedout_limit_sell(trade, order) + # Check if trade is still actually open + if (order['status'] == 'open'): + if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: + self.handle_timedout_limit_buy(trade, order) + elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold: + self.handle_timedout_limit_sell(trade, order) # FIX: 20180110, why is cancel.order unconditionally here, whereas # it is conditionally called in the diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index d9ccaa325..4877fe556 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -100,7 +100,10 @@ def default_conf(): "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": 600, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1cb2bfca2..fb7579d45 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1124,7 +1124,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe Trade.session.add(trade_buy) # check it does cancel buy orders over the time limit - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() @@ -1165,7 +1165,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, Trade.session.add(trade_sell) # check it does cancel sell orders over the time limit - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert trade_sell.is_open is True @@ -1205,7 +1205,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() @@ -1256,7 +1256,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - 'recent call last):\n.*' ) - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert filter(regexp.match, caplog.record_tuples) From c447644fd16386868c6a4a1aa6b8a5e139af8c22 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 30 Jun 2018 14:23:06 +0200 Subject: [PATCH 76/86] Update ccxt from 1.14.295 to 1.14.301 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6995c2513..72decf95a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.295 +ccxt==1.14.301 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 5a591e01c08fa0c60db152da2c04ec04dfd4e58a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 30 Jun 2018 14:23:07 +0200 Subject: [PATCH 77/86] Update sqlalchemy from 1.2.8 to 1.2.9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 72decf95a..50441879a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ccxt==1.14.301 -SQLAlchemy==1.2.8 +SQLAlchemy==1.2.9 python-telegram-bot==10.1.0 arrow==0.12.1 cachetools==2.1.0 From 14e12bd3c09c42a4ed20c170765a55826e304b04 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 30 Jun 2018 17:37:34 +0200 Subject: [PATCH 78/86] Fix missing comma in example.json --- config.json.example | 2 +- config_full.json.example | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json.example b/config.json.example index 336a0c374..606167ea3 100644 --- a/config.json.example +++ b/config.json.example @@ -8,7 +8,7 @@ "unfilledtimeout": { "buy":10, "sell":30 - } + }, "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/config_full.json.example b/config_full.json.example index 1985edc70..4bb794937 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -15,7 +15,7 @@ "unfilledtimeout": { "buy":10, "sell":30 - } + }, "bid_strategy": { "ask_last_balance": 0.0 }, From 9e3e900f78d5105f6d145d80fd5d97dbea0b9cac Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 30 Jun 2018 17:49:46 +0200 Subject: [PATCH 79/86] Add get_markets mock to new tests --- freqtrade/tests/test_freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1cb2bfca2..f66864946 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1599,6 +1599,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets ) conf = deepcopy(default_conf) @@ -1616,7 +1617,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert freqtrade.handle_trade(trade) is True -def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: +def test_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 """ @@ -1634,6 +1635,7 @@ def test_ignore_roi_if_buy_signal(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) From 8f49d5eb1002230fb424e4b505ec5f9561e778ea Mon Sep 17 00:00:00 2001 From: Nullart Date: Thu, 14 Jun 2018 18:49:43 +0800 Subject: [PATCH 80/86] documentation updates --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5279ed06e..ea3e99f15 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -22,7 +22,8 @@ The table below will list all configuration parameters. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. -| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. +| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. +| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. From c66f858b988bd18ee24045839211f4248640b097 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 19:37:55 +0200 Subject: [PATCH 81/86] rename innerfun to mock_ccxt_fun --- freqtrade/tests/exchange/test_exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 99b960885..6cf7f11b0 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,20 +14,20 @@ from freqtrade.exchange import Exchange, API_RETRY_COUNT from freqtrade.tests.conftest import log_has, get_patched_exchange -def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, innerfun, **kwargs): +def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): """Function to test ccxt exception handling """ with pytest.raises(TemporaryError): - api_mock.__dict__[innerfun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) exchange = get_patched_exchange(mocker, default_conf, api_mock) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[innerfun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[innerfun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) exchange = get_patched_exchange(mocker, default_conf, api_mock) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[innerfun].call_count == 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 def test_init(default_conf, mocker, caplog): From 2dc881558d65b19dd6596d3a8847da618f7d6885 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 19:41:19 +0200 Subject: [PATCH 82/86] address PR comments --- config.json.example | 4 ++-- config_full.json.example | 4 ++-- freqtrade/constants.py | 1 - freqtrade/freqtradebot.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config.json.example b/config.json.example index 606167ea3..79ba2ac64 100644 --- a/config.json.example +++ b/config.json.example @@ -6,8 +6,8 @@ "ticker_interval" : "5m", "dry_run": false, "unfilledtimeout": { - "buy":10, - "sell":30 + "buy": 10, + "sell": 30 }, "bid_strategy": { "ask_last_balance": 0.0 diff --git a/config_full.json.example b/config_full.json.example index 4bb794937..d40c11a91 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -13,8 +13,8 @@ }, "stoploss": -0.10, "unfilledtimeout": { - "buy":10, - "sell":30 + "buy": 10, + "sell": 30 }, "bid_strategy": { "ask_last_balance": 0.0 diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 44bd64fe9..04d4a44ac 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -64,7 +64,6 @@ CONF_SCHEMA = { 'unfilledtimeout': { 'type': 'object', 'properties': { - 'use_book_order': {'type': 'boolean'}, 'buy': {'type': 'number', 'minimum': 3}, 'sell': {'type': 'number', 'minimum': 10} } diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c5f932119..4d6352e55 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -525,7 +525,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ continue # Check if trade is still actually open - if (order['status'] == 'open'): + if order['status'] == 'open': if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: self.handle_timedout_limit_buy(trade, order) elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold: From e39d88ef65e72387f18198da269a2d7660a2d6c6 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 19:54:26 +0200 Subject: [PATCH 83/86] Address some PR comments --- freqtrade/constants.py | 2 +- freqtrade/persistence.py | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4ba1a7094..d80eea6f4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -62,7 +62,7 @@ CONF_SCHEMA = { }, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'trailing_stop': {'type': 'boolean'}, - 'trailing_stop_positive': {'type': 'number', 'minimum': 0}, + 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, 'bid_strategy': { 'type': 'object', diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 2b226e53a..ad90d3f92 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -184,14 +184,8 @@ class Trade(_DECL_BASE): arrow.get(self.open_date).humanize() if self.is_open else 'closed' ) - def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool =False): - """ - this adjusts the stop loss to it's most recently observed setting - :param current_price: - :param stoploss in percent: - :param initial: - :return: - """ + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): + """this adjusts the stop loss to it's most recently observed setting""" if initial and not (self.stop_loss is None or self.stop_loss == 0): # Don't modify if called with initial and nothing to do @@ -207,7 +201,7 @@ class Trade(_DECL_BASE): self.max_rate = current_price # no stop loss assigned yet - if self.stop_loss is None or self.stop_loss == 0: + if not self.stop_loss: logger.debug("assigning new stop loss") self.stop_loss = new_loss self.initial_stop_loss = new_loss From 937644a04b96adbdb26ed54c5969945e09f314c2 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 19:55:51 +0200 Subject: [PATCH 84/86] change while-loop to enumerate - add intensified test for this scenario --- freqtrade/tests/test_persistence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 7ef4d2c25..12ae6579f 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -446,6 +446,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog): # fake previous backup engine.execute("create table trades_bak as select * from trades") + + engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration init(default_conf) @@ -464,6 +466,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 assert log_has("trying trades_bak1", caplog.record_tuples) + assert log_has("trying trades_bak2", caplog.record_tuples) def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): From 782570e71eb6fca9c5a70cf36896d692661a14a7 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 20:03:07 +0200 Subject: [PATCH 85/86] Address PR comment --- config.json.example | 2 +- freqtrade/persistence.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json.example b/config.json.example index 87c27090e..e8473e919 100644 --- a/config.json.example +++ b/config.json.example @@ -36,7 +36,7 @@ "experimental": { "use_sell_signal": false, "sell_profit_only": false, - "sell_fullfilled_at_roi": false + "ignore_roi_if_buy_signal": false }, "telegram": { "enabled": true, diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ad90d3f92..dfeb6145c 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -80,7 +80,7 @@ def check_migrate(engine) -> None: tabs = inspector.get_table_names() table_back_name = 'trades_bak' i = 0 - while table_back_name in tabs: + for i, table_back_name in enumerate(tabs): i += 1 table_back_name = f'trades_bak{i}' logger.info(f'trying {table_back_name}') From 3c5be55eb907878c73f0cfb26a4f21b3af2a4745 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 1 Jul 2018 20:17:30 +0200 Subject: [PATCH 86/86] remove unnecessary variable --- freqtrade/persistence.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index dfeb6145c..aa33f7063 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -79,9 +79,7 @@ def check_migrate(engine) -> None: cols = inspector.get_columns('trades') tabs = inspector.get_table_names() table_back_name = 'trades_bak' - i = 0 for i, table_back_name in enumerate(tabs): - i += 1 table_back_name = f'trades_bak{i}' logger.info(f'trying {table_back_name}')