From 2d60e4b18b2c982c88578cdd6ff81f0c6b993396 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 00:32:03 +0300 Subject: [PATCH 001/102] allow comments and trailing commas in config files --- freqtrade/configuration/load_config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 25504144f..7a3ca1798 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -1,7 +1,7 @@ """ This module contain functions to load the configuration file """ -import json +import rapidjson import logging import sys from typing import Any, Dict @@ -12,6 +12,9 @@ from freqtrade import OperationalException logger = logging.getLogger(__name__) +CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path @@ -21,7 +24,7 @@ def load_config_file(path: str) -> Dict[str, Any]: try: # Read config from stdin if requested in the options with open(path) if path != '-' else sys.stdin as file: - config = json.load(file) + config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: raise OperationalException( f'Config file "{path}" not found!' From 90b75afdb1373ee3c38eeeb495f053d4f3306abd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 12 Aug 2019 00:33:34 +0300 Subject: [PATCH 002/102] test added to load config with comments and trailing commas --- freqtrade/tests/config_test_comments.json | 133 ++++++++++++++++++++++ freqtrade/tests/test_configuration.py | 11 ++ 2 files changed, 144 insertions(+) create mode 100644 freqtrade/tests/config_test_comments.json diff --git a/freqtrade/tests/config_test_comments.json b/freqtrade/tests/config_test_comments.json new file mode 100644 index 000000000..85becc3e8 --- /dev/null +++ b/freqtrade/tests/config_test_comments.json @@ -0,0 +1,133 @@ +{ + /* Single-line C-style comment */ + "max_open_trades": 3, + /* + * Multi-line C-style comment + */ + "stake_currency": "BTC", + "stake_amount": 0.05, + "fiat_display_currency": "USD", // C++-style comment + "amount_reserve_percent" : 0.05, // And more, tabs before this comment + "dry_run": false, + "ticker_interval": "5m", + "trailing_stop": false, + "trailing_stop_positive": 0.005, + "trailing_stop_positive_offset": 0.0051, + "trailing_only_offset_is_reached": false, + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.10, + "unfilledtimeout": { + "buy": 10, + "sell": 30, // Trailing comma should also be accepted now + }, + "bid_strategy": { + "use_order_book": false, + "ask_last_balance": 0.0, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy":{ + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 9 + }, + "order_types": { + "buy": "limit", + "sell": "limit", + "stoploss": "market", + "stoploss_on_exchange": false, + "stoploss_on_exchange_interval": 60 + }, + "order_time_in_force": { + "buy": "gtc", + "sell": "gtc" + }, + "pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume", + "precision_filter": false + } + }, + "exchange": { + "name": "bittrex", + "sandbox": false, + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": false, + "rateLimit": 500, + "aiohttp_trust_env": false + }, + "pair_whitelist": [ + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "DASH/BTC", + "ZEC/BTC", + "XLM/BTC", + "NXT/BTC", + "POWR/BTC", + "ADA/BTC", + "XMR/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC" + ], + "outdated_offset": 5, + "markets_refresh_interval": 60 + }, + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "capital_available_percentage": 0.5, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "experimental": { + "use_sell_signal": false, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "telegram": { +// We can now comment out some settings +// "enabled": true, + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "db_url": "sqlite:///tradesv3.sqlite", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + }, + "strategy": "DefaultStrategy", + "strategy_path": "user_data/strategies/" +} diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e325a0de2..0a8381089 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -634,6 +634,17 @@ def test_validate_tsl(default_conf): configuration._validate_config_consistency(default_conf) +def test_load_config_test_comments() -> None: + """ + Load config with comments + """ + config_file = Path(__file__).parents[0] / "config_test_comments.json" + print(config_file) + conf = load_config_file(str(config_file)) + + assert conf + + def test_load_config_default_exchange(all_conf) -> None: """ config['exchange'] subtree has required options in it From af67bbde311b249910da29ed09fca1f5ec688639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 15:43:10 +0200 Subject: [PATCH 003/102] Test timeframe_to_x --- freqtrade/tests/exchange/test_exchange.py | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e8a7201f1..245814a36 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import API_RETRY_COUNT +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, + timeframe_to_msecs, + timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1540,3 +1542,24 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): ex.get_valid_pair_combination("NOPAIR", "ETH") + + +def test_timeframe_to_minutes(): + assert timeframe_to_minutes("5m") == 5 + assert timeframe_to_minutes("10m") == 10 + assert timeframe_to_minutes("1h") == 60 + assert timeframe_to_minutes("1d") == 1440 + + +def test_timeframe_to_seconds(): + assert timeframe_to_seconds("5m") == 300 + assert timeframe_to_seconds("10m") == 600 + assert timeframe_to_seconds("1h") == 3600 + assert timeframe_to_seconds("1d") == 86400 + + +def test_timeframe_to_msecs(): + assert timeframe_to_msecs("5m") == 300000 + assert timeframe_to_msecs("10m") == 600000 + assert timeframe_to_msecs("1h") == 3600000 + assert timeframe_to_msecs("1d") == 86400000 From 933a553dd436eeadf1b7a65b2120a5fea0211779 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:07:19 +0200 Subject: [PATCH 004/102] Convert timeframe to next date --- freqtrade/exchange/exchange.py | 18 ++++++++++++--- freqtrade/tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..3d2bf07c7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -6,7 +6,7 @@ import asyncio import inspect import logging from copy import deepcopy -from datetime import datetime +from datetime import datetime, timezone from math import ceil, floor from random import randint from typing import Any, Dict, List, Optional, Tuple @@ -781,13 +781,25 @@ def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_minutes(ticker_interval: str) -> int: """ - Same as above, but returns minutes. + Same as timeframe_to_seconds, but returns minutes. """ return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 def timeframe_to_msecs(ticker_interval: str) -> int: """ - Same as above, but returns milliseconds. + Same as timeframe_to_seconds, but returns milliseconds. """ return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 + + +def timeframe_to_next_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine next candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + offset = date.timestamp() % timeframe_secs + new_timestamp = date.timestamp() + (timeframe_secs - offset) + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 245814a36..23384aebe 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, + timeframe_to_next_date, + timeframe_to_minutes, timeframe_to_msecs, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -1563,3 +1565,27 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("10m") == 600000 assert timeframe_to_msecs("1h") == 3600000 assert timeframe_to_msecs("1d") == 86400000 + + +def test_timeframe_to_next_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:25:00 + assert timeframe_to_next_date("5m", date) == datetime( + 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:30:00 + assert timeframe_to_next_date("10m", date) == datetime( + 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("1h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("2h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("4h", date) == datetime( + 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-13 00:00:00 + assert timeframe_to_next_date("1d", date) == datetime( + 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) From dd0ba183f88538f51aabc5db677d374a5ee12401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:11:43 +0200 Subject: [PATCH 005/102] Add timeframe_to_prev_candle --- freqtrade/exchange/exchange.py | 16 ++++++++++++ freqtrade/tests/exchange/test_exchange.py | 30 ++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3d2bf07c7..abb03b86d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,6 +793,20 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 +def timeframe_to_prev_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine last possible candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset based on timerame_secs + offset = date.timestamp() % timeframe_secs + # Subtract seconds passed since last offset + new_timestamp = date.timestamp() - offset + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + + def timeframe_to_next_date(timeframe: str, date: datetime = None): """ Use Timeframe and determine next candle. @@ -800,6 +814,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None): if not date: date = datetime.utcnow() timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset to prev timeframe offset = date.timestamp() % timeframe_secs + # Add remaining seconds to next timeframe new_timestamp = date.timestamp() + (timeframe_secs - offset) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23384aebe..e342af604 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,10 +14,10 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, - timeframe_to_next_date, - timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1567,6 +1567,30 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("1d") == 86400000 +def test_timeframe_to_prev_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("5m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("10m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 13:00:00 + assert timeframe_to_prev_date("1h", date) == datetime( + 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("2h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("4h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-12 00:00:00 + assert timeframe_to_prev_date("1d", date) == datetime( + 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) From 1ce63b5b423e8dfa55be28ff9c614b84a6a8cef9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:17:06 +0200 Subject: [PATCH 006/102] Reformat tests to be easier readable --- freqtrade/exchange/exchange.py | 10 +++- freqtrade/tests/exchange/test_exchange.py | 68 +++++++++++------------ 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index abb03b86d..fe5561be2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,9 +793,12 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 -def timeframe_to_prev_date(timeframe: str, date: datetime = None): +def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine last possible candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of previous candle (with utc timezone) """ if not date: date = datetime.utcnow() @@ -807,9 +810,12 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None): return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def timeframe_to_next_date(timeframe: str, date: datetime = None): +def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine next candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of next candle (with utc timezone) """ if not date: date = datetime.utcnow() diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e342af604..23604f44f 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1571,45 +1571,41 @@ def test_timeframe_to_prev_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) - # 5m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("5m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("10m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 13:00:00 - assert timeframe_to_prev_date("1h", date) == datetime( - 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("2h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("4h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-12 00:00:00 - assert timeframe_to_prev_date("1d", date) == datetime( - 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:20:00 + ("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:20:00 + ("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 13:00:00 + ("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 12:00:00 + ("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 12:00:00 + ("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-12 00:00:00 + ("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)), + ] + for interval, result in tf_list: + assert timeframe_to_prev_date(interval, date) == result def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:25:00 + ("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:30:00 + ("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 14:00:00 + ("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 14:00:00 + ("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 14:00:00 + ("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-13 00:00:00 + ("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)), + ] - # 5m -> 2019-08-12 13:25:00 - assert timeframe_to_next_date("5m", date) == datetime( - 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:30:00 - assert timeframe_to_next_date("10m", date) == datetime( - 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("1h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("2h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("4h", date) == datetime( - 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-13 00:00:00 - assert timeframe_to_next_date("1d", date) == datetime( - 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) + for interval, result in tf_list: + assert timeframe_to_next_date(interval, date) == result From c042d08bb73bbb00be88fb1afed5b2f147972035 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:29:09 +0200 Subject: [PATCH 007/102] Add lock_pairs to interface --- freqtrade/strategy/interface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 37aa97bb1..363b0739a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -107,6 +107,7 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -154,6 +155,13 @@ class IStrategy(ABC): """ return self.__class__.__name__ + def lock_pair(self, pair: str, until: datetime) -> None: + """ + Locks pair until a given timestamp happens. + Locked pairs are not analyzed, and are prevented from opening new trades. + """ + self._pair_locked_until['pair'] = until + def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame From 241d5100965ab0180a3393e06f37354c040ab2cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:34:55 +0200 Subject: [PATCH 008/102] Handle and update sell-orders immediately if they are closed --- freqtrade/freqtradebot.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..b384902b2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -875,15 +875,18 @@ class FreqtradeBot(object): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") # Execute sell and update trade record - order_id = self.exchange.sell(pair=str(trade.pair), - ordertype=self.strategy.order_types[sell_type], - amount=trade.amount, rate=limit, - time_in_force=self.strategy.order_time_in_force['sell'] - )['id'] + order = self.exchange.sell(pair=str(trade.pair), + ordertype=self.strategy.order_types[sell_type], + amount=trade.amount, rate=limit, + time_in_force=self.strategy.order_time_in_force['sell'] + ) - trade.open_order_id = order_id + trade.open_order_id = order['id'] trade.close_rate_requested = limit trade.sell_reason = sell_reason.value + # In case of market sell orders the order can be closed immediately + if order.get('status', 'unknown') == 'closed': + trade.update(order) Trade.session.flush() self._notify_sell(trade) From bb0b1600016b87796d4949c1dae5ecd4d10c7e18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:39:21 +0200 Subject: [PATCH 009/102] Remove duplicate test --- freqtrade/tests/test_freqtradebot.py | 47 ---------------------------- 1 file changed, 47 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..8c241197b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,53 +2374,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up - ) - freqtrade.config = {} - - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'profit', - 'limit': 1.172e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.172e-05, - 'profit_amount': 6.126e-05, - 'profit_percent': 0.0611052, - 'sell_reason': SellType.ROI.value - - } == last_msg - - def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 444ee274d727cfe280d03f115b56226556a70d64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:46:34 +0200 Subject: [PATCH 010/102] close dry-run orders in case of market orders --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..5bde4ce98 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -371,7 +371,7 @@ class Exchange(object): 'side': side, 'remaining': amount, 'datetime': arrow.utcnow().isoformat(), - 'status': "open", + 'status': "closed" if ordertype == "market" else "open", 'fee': None, "info": {} } From feced71a6d8fff0bf4fdd5d1a5db45b948d3182c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:47:00 +0200 Subject: [PATCH 011/102] Test closing sell-orders immediately --- freqtrade/tests/test_freqtradebot.py | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 8c241197b..9ced5c5a1 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,6 +2374,58 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 +def test_execute_sell_market_order(default_conf, ticker, fee, + ticker_sell_up, markets, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_up + ) + freqtrade.config['order_types']['sell'] = 'market' + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + + assert not trade.is_open + assert trade.close_profit == 0.0611052 + + assert rpc_mock.call_count == 2 + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'order_type': 'market', + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.0611052, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'sell_reason': SellType.ROI.value + + } == last_msg + + def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 200b6ea10faccbe0ce8f57aa221bf8301cfd8eda Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 19:50:22 +0200 Subject: [PATCH 012/102] Add is_pair_locked --- freqtrade/strategy/interface.py | 19 +++++++++++++++---- freqtrade/tests/strategy/test_interface.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 363b0739a..e887da43b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -4,7 +4,7 @@ This module defines the interface to apply for strategies """ import logging from abc import ABC, abstractmethod -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Dict, List, NamedTuple, Optional, Tuple import warnings @@ -159,8 +159,19 @@ class IStrategy(ABC): """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. + :param pair: Pair to lock + :param until: datetime in UTC until the pair should be blocked from opening new trades. + Needs to be timezone aware `datetime.now(timezone.utc)` """ - self._pair_locked_until['pair'] = until + self._pair_locked_until[pair] = until + + def is_pair_locked(self, pair: str) -> bool: + """ + Checks if a pair is currently locked + """ + if pair not in self._pair_locked_until: + return False + return self._pair_locked_until[pair] >= datetime.now(timezone.utc) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -268,8 +279,8 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if on the condition required to trigger a sell has been reached - if the threshold is reached and updates the trade record. + This function evaluate if one of the conditions required to trigger a sell + has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI :param force_stoploss: Externally provided stoploss diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 0eb7630a1..36c9ffcd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -286,3 +286,19 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert ret['sell'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) + + +def test_is_pair_locked(default_conf): + strategy = DefaultStrategy(default_conf) + # dict should be empty + assert not strategy._pair_locked_until + + pair = 'ETH/BTC' + assert not strategy.is_pair_locked(pair) + strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert strategy.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not strategy.is_pair_locked(pair) From 2600cb7b641dd6ce0ab5da9aafb5ba3056ad8274 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:04:19 +0200 Subject: [PATCH 013/102] simplify timeframe_next_date calculation --- freqtrade/exchange/__init__.py | 4 +++- freqtrade/exchange/exchange.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5c58320f6..828042911 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,8 @@ from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 available_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, - timeframe_to_msecs) + timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe5561be2..d0432a060 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -817,11 +817,9 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: :param date: date to use. Defaults to utcnow() :returns: date of next candle (with utc timezone) """ - if not date: - date = datetime.utcnow() + prevdate = timeframe_to_prev_date(timeframe, date) timeframe_secs = timeframe_to_seconds(timeframe) - # Get offset to prev timeframe - offset = date.timestamp() % timeframe_secs - # Add remaining seconds to next timeframe - new_timestamp = date.timestamp() + (timeframe_secs - offset) + + # Add one interval to previous candle + new_timestamp = prevdate.timestamp() + timeframe_secs return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) From 23a70932d299b00f6d3f44e5c95676adfca8b71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:36:45 +0200 Subject: [PATCH 014/102] Remove pointless tests (without config?? really?) --- freqtrade/tests/test_freqtradebot.py | 94 ---------------------------- 1 file changed, 94 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..41ce661b9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,100 +2374,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up - ) - freqtrade.config = {} - - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'profit', - 'limit': 1.172e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.172e-05, - 'profit_amount': 6.126e-05, - 'profit_percent': 0.0611052, - 'sell_reason': SellType.ROI.value - - } == last_msg - - -def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down - ) - - freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'loss', - 'limit': 1.044e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.044e-05, - 'profit_amount': -5.492e-05, - 'profit_percent': -0.05478342, - 'sell_reason': SellType.STOP_LOSS.value - } == last_msg - - def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From ca739f71fb17b0c0437745ab257f67afc3b9823a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:37:11 +0200 Subject: [PATCH 015/102] Fix default argument handling for timeframe_to_nextdate --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d0432a060..e36032c49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -801,7 +801,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: :returns: date of previous candle (with utc timezone) """ if not date: - date = datetime.utcnow() + date = datetime.now(timezone.utc) timeframe_secs = timeframe_to_seconds(timeframe) # Get offset based on timerame_secs offset = date.timestamp() % timeframe_secs diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23604f44f..3747a2ad3 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1588,6 +1588,9 @@ def test_timeframe_to_prev_date(): for interval, result in tf_list: assert timeframe_to_prev_date(interval, date) == result + date = datetime.now(tz=timezone.utc) + assert timeframe_to_prev_date("5m", date) < date + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 @@ -1609,3 +1612,6 @@ def test_timeframe_to_next_date(): for interval, result in tf_list: assert timeframe_to_next_date(interval, date) == result + + date = datetime.now(tz=timezone.utc) + assert timeframe_to_next_date("5m", date) > date From 59acd5ec7c0b5927f1386ab6a10ee948b9ad9612 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:39:34 +0200 Subject: [PATCH 016/102] Lock pair for the rest of the candle in case of sells --- freqtrade/freqtradebot.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..c14392752 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -284,6 +284,9 @@ class FreqtradeBot(object): # running get_signal on historical data fetched for _pair in whitelist: + if self.strategy.is_pair_locked(_pair): + logger.info(f"Pair {_pair} is currently locked.") + continue (buy, sell) = self.strategy.get_signal( _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) @@ -885,6 +888,10 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() + + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) + self._notify_sell(trade) def _notify_sell(self, trade: Trade): From 482847a99417a9c582492d685e5a225ad326150d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 00:10:33 +0300 Subject: [PATCH 017/102] docs adjusted; various fixes to bot-usage.md and configuration.md --- docs/bot-usage.md | 15 +++++++++------ docs/configuration.md | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0ca2f3cc5..f720bf554 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,7 +2,7 @@ This page explains the different parameters of the bot and how to run it. -!Note: +!!! Note: If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. @@ -43,19 +43,22 @@ optional arguments: --sd-notify Notify systemd service manager. ``` -### How to use a different configuration file? +### How to specify which configuration file be used? -The bot allows you to select which configuration file you want to use. Per -default, the bot will load the file `./config.json` +The bot allows you to select which configuration file you want to use by means of +the `-c/--config` command line option: ```bash freqtrade -c path/far/far/away/config.json ``` +Per default, the bot loads the `config.json` configuration file from the current +working directory. + ### How to use multiple configuration files? The bot allows you to use multiple configuration files by specifying multiple -`-c/--config` configuration options in the command line. Configuration parameters +`-c/--config` options in the command line. Configuration parameters defined in the last configuration file override parameters with the same name defined in the previous configuration file specified in the command line. @@ -266,7 +269,7 @@ optional arguments: ## Edge commands -To know your trade expectacny and winrate against historical data, you can use Edge. +To know your trade expectancy and winrate against historical data, you can use Edge. ``` usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] diff --git a/docs/configuration.md b/docs/configuration.md index f8dbbbbbb..b48e23eee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,15 +1,24 @@ # Configure the bot -This page explains how to configure your `config.json` file. +This page explains how to configure your configuration file. -## Setup config.json +Per default, the bot loads configuration from the `config.json` file located in the current working directory. +You can change the configuration file used by the bot with the `-c/--config` option. -We recommend to copy and use the `config.json.example` as a template +If you used the [Quick start](installation.md/#quick-start) method for installing +the bot, the installation script should have already created the default configuration file (`config.json`) for you. + +We recommend you to copy and use the `config.json.example` as a template for your bot configuration. -The table below will list all configuration parameters. +The configuration file defines the set of configuration parameters for the bot written in the JSON format. +Additionally, you may use one-line `// ...` and multi-line `/* ... */` comments. -Mandatory Parameters are marked as **Required**. +## Configuration parameters + +The table below will list all configuration parameters available. + +Mandatory parameters are marked as **Required**. | Command | Default | Description | |----------|---------|-------------| From f960ea039e472e3b29d6b3bd69842f9d7e524097 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:05:51 +0200 Subject: [PATCH 018/102] Remove duplicate test --- freqtrade/tests/test_freqtradebot.py | 47 ---------------------------- 1 file changed, 47 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9ced5c5a1..a1d5f691e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2426,53 +2426,6 @@ def test_execute_sell_market_order(default_conf, ticker, fee, } == last_msg -def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down - ) - - freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'loss', - 'limit': 1.044e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.044e-05, - 'profit_amount': -5.492e-05, - 'profit_percent': -0.05478342, - 'sell_reason': SellType.STOP_LOSS.value - } == last_msg - - def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From 2961efdc1802bba064d9ead55df125fa2c0816a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:48:21 +0200 Subject: [PATCH 019/102] Initial test for locked pair --- 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 41ce661b9..6f6a539eb 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2499,6 +2499,43 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert trade.sell_reason == SellType.SELL_SIGNAL.value +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mocker, caplog) -> None: + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Decrease the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_down + ) + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) + trade.close(ticker_sell_down()['bid']) + assert trade.pair in freqtrade.strategy._pair_locked_until + assert freqtrade.strategy.is_pair_locked(trade.pair) + + # reinit - should buy other pair. + caplog.clear() + freqtrade.create_trade() + + assert log_has(f"Pair {trade.pair} is currently locked.", caplog) + + def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 28e318b6466845900d52f73e5d07c0d25d05f7ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:47:11 +0200 Subject: [PATCH 020/102] Lock pairs for stoploss_on_exchange fills too --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c14392752..2d1c1f566 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -681,6 +681,9 @@ class FreqtradeBot(object): if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, + timeframe_to_next_date(self.config['ticker_interval'])) self._notify_sell(trade) return True From 8d813fa728a4d454aaa8cf95a84e507513ca5aa6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:36:52 +0200 Subject: [PATCH 021/102] Remove return-value for _process --- freqtrade/tests/test_freqtradebot.py | 15 +++++++-------- freqtrade/worker.py | 9 ++------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..883e4f050 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -744,8 +744,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non worker = Worker(args=None, config=default_conf) patch_get_signal(worker.freqtrade) - result = worker._process() - assert result is False + worker._process() assert sleep_mock.has_calls() @@ -763,8 +762,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> assert worker.state == State.RUNNING - result = worker._process() - assert result is False + worker._process() assert worker.state == State.STOPPED assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] @@ -786,13 +784,14 @@ def test_process_trade_handling( trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades - result = freqtrade.process() - assert result is True + freqtrade.process() + trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 1 - result = freqtrade.process() - assert result is False + # Nothing happened ... + freqtrade.process() + assert len(trades) == 1 def test_process_trade_no_whitelist_pair( diff --git a/freqtrade/worker.py b/freqtrade/worker.py index db0dba0e8..df792e35e 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -127,11 +127,10 @@ class Worker(object): time.sleep(duration) return result - def _process(self) -> bool: + def _process(self) -> None: logger.debug("========================================") - state_changed = False try: - state_changed = self.freqtrade.process() + self.freqtrade.process() except TemporaryError as error: logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") time.sleep(constants.RETRY_TIMEOUT) @@ -144,10 +143,6 @@ class Worker(object): }) logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED - # TODO: The return value of _process() is not used apart tests - # and should (could) be eliminated later. See PR #1689. -# state_changed = True - return state_changed def _reconfigure(self) -> None: """ From 4b8eaaf7aa97c6bf2a0cf339ca980978f34f84a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:37:56 +0200 Subject: [PATCH 022/102] freqtradebot.process() does not need to return anything --- freqtrade/freqtradebot.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..c4792a275 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -105,13 +105,12 @@ class FreqtradeBot(object): # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) - def process(self) -> bool: + def process(self) -> None: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. :return: True if one or more trades has been created or closed, False otherwise """ - state_changed = False # Check whether markets have to be reloaded self.exchange._reload_markets() @@ -138,19 +137,17 @@ class FreqtradeBot(object): # First process current opened trades for trade in trades: - state_changed |= self.process_maybe_execute_sell(trade) + self.process_maybe_execute_sell(trade) # Then looking for buy opportunities if len(trades) < self.config['max_open_trades']: - state_changed = self.process_maybe_execute_buy() + self.process_maybe_execute_buy() if 'unfilledtimeout' in self.config: # Check and handle any timed out open orders self.check_handle_timedout() Trade.session.flush() - return state_changed - def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): """ Extend whitelist with pairs from open trades From c29389f5f397156819e79055e4bdf21b0b64d2d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:38:21 +0200 Subject: [PATCH 023/102] Remove process() checks from tests --- freqtrade/tests/test_freqtradebot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 883e4f050..7ad9cba7a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -711,8 +711,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades - result = freqtrade.process() - assert result is True + freqtrade.process() trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 1 @@ -833,11 +832,10 @@ def test_process_trade_no_whitelist_pair( )) assert pair not in freqtrade.active_pair_whitelist - result = freqtrade.process() + freqtrade.process() assert pair in freqtrade.active_pair_whitelist # Make sure each pair is only in the list once assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist)) - assert result is True def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None: From 8873e0072c81d27015d2e0e936f509c6c7690d83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 09:42:22 +0200 Subject: [PATCH 024/102] process_maybe_execute_buy does not need to return bool --- freqtrade/freqtradebot.py | 10 +++------- freqtrade/tests/test_freqtradebot.py | 8 +++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4792a275..b18b8eac5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -426,21 +426,17 @@ class FreqtradeBot(object): return True - def process_maybe_execute_buy(self) -> bool: + def process_maybe_execute_buy(self) -> None: """ Tries to execute a buy trade in a safe way :return: True if executed """ try: # Create entity and execute trade - if self.create_trade(): - return True - - logger.info('Found no buy signals for whitelisted currencies. Trying again..') - return False + if not self.create_trade(): + logger.info('Found no buy signals for whitelisted currencies. Trying again...') except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) - return False def process_maybe_execute_sell(self, trade: Trade) -> bool: """ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 7ad9cba7a..f70b5374e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1385,14 +1385,12 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, stop_price=0.00002344 * 0.99) -def test_process_maybe_execute_buy(mocker, default_conf) -> None: +def test_process_maybe_execute_buy(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=True)) - assert freqtrade.process_maybe_execute_buy() - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) - assert not freqtrade.process_maybe_execute_buy() + freqtrade.process_maybe_execute_buy() + assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> None: From 997eb7574aba223f5d25e060b8bd3c488882efc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:01:29 +0200 Subject: [PATCH 025/102] Support creating multiple trades in one iteration --- freqtrade/freqtradebot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b18b8eac5..f5ea131e4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -279,15 +279,16 @@ class FreqtradeBot(object): logger.info("No currency pair in whitelist, but checking to sell open trades.") return False + buycount = 0 # running get_signal on historical data fetched for _pair in whitelist: (buy, sell) = self.strategy.get_signal( _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) - if buy and not sell: + if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: stake_amount = self._get_trade_stake_amount(_pair) if not stake_amount: - return False + continue logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") @@ -297,12 +298,11 @@ class FreqtradeBot(object): if (bidstrat_check_depth_of_market.get('enabled', False)) and\ (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): - return self.execute_buy(_pair, stake_amount) - else: - return False - return self.execute_buy(_pair, stake_amount) + buycount += self.execute_buy(_pair, stake_amount) - return False + buycount += self.execute_buy(_pair, stake_amount) + + return buycount > 0 def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ From a325f1ce2bb05df4d7e086c7b7fe0d8da21f9763 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:01:43 +0200 Subject: [PATCH 026/102] adapt some tests since create_trade() can now buy multiple times, we need to use execute_buy() to create a single trade --- freqtrade/tests/test_freqtradebot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index f70b5374e..ba978e7ad 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -253,13 +253,14 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, 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() + # freqtrade.create_trade() + freqtrade.execute_buy('ETH/BTC', result) result = freqtrade._get_trade_stake_amount('LTC/BTC') assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) # create 2 trades, order amount should be None - freqtrade.create_trade() + freqtrade.execute_buy('LTC/BTC', result) result = freqtrade._get_trade_stake_amount('XRP/BTC') assert result is None From 6948e0ba847bd73eafb8e47a64369c9218815706 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:12:02 +0200 Subject: [PATCH 027/102] Handle orderbook_depth check correctly --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f5ea131e4..699500628 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -299,6 +299,8 @@ class FreqtradeBot(object): (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): buycount += self.execute_buy(_pair, stake_amount) + else: + continue buycount += self.execute_buy(_pair, stake_amount) From 974d899b337e83066ab4c52bae99d65a4f9b5543 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:12:12 +0200 Subject: [PATCH 028/102] Adapt some more tests --- freqtrade/tests/test_freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index ba978e7ad..43982fa5c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -302,6 +302,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['max_open_trades'] = float('inf') # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 # Thus, if price falls 21%, stoploss should be triggered @@ -342,6 +343,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['max_open_trades'] = float('inf') # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 # Thus, if price falls 15%, stoploss should not be triggered @@ -380,6 +382,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, patch_RPCManager(mocker) patch_exchange(mocker) default_conf['stake_amount'] = 0.0000098751 + default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, @@ -1284,7 +1287,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) - + edge_conf['max_open_trades'] = float('inf') mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ From d69f7ae471ccb359aa41ba80a57c60d910b1be00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:15:31 +0200 Subject: [PATCH 029/102] Adapt final tests to support multi-trade creation --- freqtrade/tests/rpc/test_rpc_telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 3575520ad..e00903947 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -308,6 +308,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: patch_exchange(mocker) + default_conf['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0 @@ -357,9 +358,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, # Reset msg_mock msg_mock.reset_mock() + freqtradebot.config['max_open_trades'] = 2 # Add two other trades freqtradebot.create_trade() - freqtradebot.create_trade() trades = Trade.query.all() for trade in trades: @@ -832,14 +833,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker markets=PropertyMock(return_value=markets), validate_pairs=MagicMock(return_value={}) ) - + default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data - for _ in range(4): - freqtradebot.create_trade() + freqtradebot.create_trade() rpc_mock.reset_mock() update.message.text = '/forcesell all' From 0a07dfc5cf7bad17af06c1e873e363fe72dd2d46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:20:32 +0200 Subject: [PATCH 030/102] Add test verifying that multiple trades are opened in one iteration --- freqtrade/tests/test_freqtradebot.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 43982fa5c..e8245d60b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -697,6 +697,28 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: assert not freqtrade.create_trade() +@pytest.mark.parametrize("max_open", range(1, 5)) +def test_create_trade_multiple_trades(default_conf, ticker, + fee, markets, mocker, max_open) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['max_open_trades'] = max_open + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': "12355555"}), + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + freqtrade.create_trade() + + trades = Trade.get_open_trades() + assert len(trades) == max_open + + def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: patch_RPCManager(mocker) From 9d476b5ab267f4bc829f00b3de05a4b97e54e249 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 10:34:27 +0200 Subject: [PATCH 031/102] Also check 0 open trades --- freqtrade/tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index e8245d60b..acab74233 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -697,7 +697,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: assert not freqtrade.create_trade() -@pytest.mark.parametrize("max_open", range(1, 5)) +@pytest.mark.parametrize("max_open", range(0, 5)) def test_create_trade_multiple_trades(default_conf, ticker, fee, markets, mocker, max_open) -> None: patch_RPCManager(mocker) From 3d36747b920ed70e366b4ab0143c445078e4ce5d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 13 Aug 2019 21:52:50 +0300 Subject: [PATCH 032/102] preface in configuration.md reworked --- docs/configuration.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b48e23eee..66b4b6da2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,18 +1,28 @@ # Configure the bot -This page explains how to configure your configuration file. +This page explains how to configure the bot. + +## The Freqtrade configuration file + +The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file). Per default, the bot loads configuration from the `config.json` file located in the current working directory. -You can change the configuration file used by the bot with the `-c/--config` option. + +You can change the name of the configuration file used by the bot with the `-c/--config` command line option. + +In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. -We recommend you to copy and use the `config.json.example` as a template +If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template for your bot configuration. -The configuration file defines the set of configuration parameters for the bot written in the JSON format. -Additionally, you may use one-line `// ...` and multi-line `/* ... */` comments. +The Freqtrade configuration file is to be written in the JSON format. + +Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters. + +Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it. ## Configuration parameters From e35a3492299fd4961585a38b9a3dfedcf920a05b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:07:03 +0200 Subject: [PATCH 033/102] Fix spelling of interface.py docstring --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e887da43b..99f5f26de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -279,7 +279,7 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if one of the conditions required to trigger a sell + This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI From a76136c0102cffbe3e1ca24c2b8ca88e81146558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:16:43 +0200 Subject: [PATCH 034/102] Rename create_trade to create_trades --- freqtrade/freqtradebot.py | 4 +- freqtrade/tests/rpc/test_rpc.py | 22 ++-- freqtrade/tests/rpc/test_rpc_apiserver.py | 8 +- freqtrade/tests/rpc/test_rpc_telegram.py | 22 ++-- freqtrade/tests/test_freqtradebot.py | 127 +++++++++++----------- 5 files changed, 91 insertions(+), 92 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 699500628..e67a8687f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -256,7 +256,7 @@ class FreqtradeBot(object): amount_reserve_percent = max(amount_reserve_percent, 0.5) return min(min_stake_amounts) / amount_reserve_percent - def create_trade(self) -> bool: + def create_trades(self) -> bool: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created @@ -435,7 +435,7 @@ class FreqtradeBot(object): """ try: # Create entity and execute trade - if not self.create_trade(): + if not self.create_trades(): logger.info('Found no buy signals for whitelisted currencies. Trying again...') except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index d273244b0..5d3fb7920 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -44,7 +44,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_trade_status() - freqtradebot.create_trade() + freqtradebot.create_trades() results = rpc._rpc_trade_status() assert { 'trade_id': 1, @@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table() - freqtradebot.create_trade() + freqtradebot.create_trades() result = rpc._rpc_status_table() assert 'instantly' in result['Since'].all() assert 'ETH/BTC' in result['Pair'].all() @@ -151,7 +151,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -208,7 +208,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -222,7 +222,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, trade.close_date = datetime.utcnow() trade.is_open = False - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -292,7 +292,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -536,7 +536,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} - freqtradebot.create_trade() + freqtradebot.create_trades() msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} @@ -570,7 +570,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it @@ -589,7 +589,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.create_trade() + freqtradebot.create_trades() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.get_order', @@ -622,7 +622,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -660,7 +660,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: assert counts["current"] == 0 # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() counts = rpc._rpc_count() assert counts["current"] == 1 diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index a218d5622..794343a98 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -275,7 +275,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 # Create some test data - ftbot.create_trade() + ftbot.create_trades() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 @@ -329,7 +329,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} - ftbot.create_trade() + ftbot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -418,7 +418,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} - ftbot.create_trade() + ftbot.create_trades() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 @@ -548,7 +548,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} - ftbot.create_trade() + ftbot.create_trades() rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index e00903947..2f469643f 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -192,7 +192,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: # Create some test data for _ in range(3): - freqtradebot.create_trade() + freqtradebot.create_trades() telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -240,7 +240,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() # Trigger status while we have a fulfilled order for the open trade telegram._status(bot=MagicMock(), update=update) @@ -292,7 +292,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() telegram._status_table(bot=MagicMock(), update=update) @@ -332,7 +332,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -360,7 +360,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 # Add two other trades - freqtradebot.create_trade() + freqtradebot.create_trades() trades = Trade.query.all() for trade in trades: @@ -439,7 +439,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -734,7 +734,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -785,7 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() # Decrease the price and sell it mocker.patch.multiple( @@ -839,7 +839,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() rpc_mock.reset_mock() update.message.text = '/forcesell all' @@ -983,7 +983,7 @@ def test_performance_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() trade = Trade.query.first() assert trade @@ -1028,7 +1028,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non freqtradebot.state = State.RUNNING # Create some test data - freqtradebot.create_trade() + freqtradebot.create_trades() msg_mock.reset_mock() telegram._count(bot=MagicMock(), update=update) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index acab74233..d74615d6b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -253,7 +253,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, 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() freqtrade.execute_buy('ETH/BTC', result) result = freqtrade._get_trade_stake_amount('LTC/BTC') @@ -327,7 +326,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -368,7 +367,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -392,7 +391,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -400,7 +399,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, assert trade.is_open assert trade.open_date is not None - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None @@ -523,7 +522,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert result == min(8, 2 * 2) / 0.9 -def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: +def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -538,7 +537,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -556,8 +555,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke assert whitelist == default_conf['exchange']['pair_whitelist'] -def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) @@ -572,11 +571,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trade() + freqtrade.create_trades() -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -591,13 +590,13 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] assert rate * amount >= default_conf['stake_amount'] -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -613,11 +612,11 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -634,12 +633,12 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() assert freqtrade._get_trade_stake_amount('ETH/BTC') is None -def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: +def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -654,13 +653,13 @@ def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert freqtrade.create_trade() - assert not freqtrade.create_trade() + assert freqtrade.create_trades() + assert not freqtrade.create_trades() assert log_has("No currency pair in whitelist, but checking to sell open trades.", caplog) -def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: +def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -674,11 +673,11 @@ def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_orde freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() assert log_has("Whitelist is empty.", caplog) -def test_create_trade_no_signal(default_conf, fee, mocker) -> None: +def test_create_trades_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True patch_RPCManager(mocker) @@ -694,12 +693,12 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: Trade.query = MagicMock() Trade.query.filter = MagicMock() - assert not freqtrade.create_trade() + assert not freqtrade.create_trades() @pytest.mark.parametrize("max_open", range(0, 5)) -def test_create_trade_multiple_trades(default_conf, ticker, - fee, markets, mocker, max_open) -> None: +def test_create_trades_multiple_trades(default_conf, ticker, + fee, markets, mocker, max_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = max_open @@ -713,7 +712,7 @@ def test_create_trade_multiple_trades(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trades = Trade.get_open_trades() assert len(trades) == max_open @@ -1101,7 +1100,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # Fourth case: when stoploss is set and it is hit # should unset stoploss_order_id and return true # as a trade actually happened - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1176,7 +1175,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1266,7 +1265,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # setting stoploss_on_exchange_interval to 60 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1347,7 +1346,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1414,7 +1413,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, def test_process_maybe_execute_buy(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trades', MagicMock(return_value=False)) freqtrade.process_maybe_execute_buy() assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) @@ -1423,7 +1422,7 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( - 'freqtrade.freqtradebot.FreqtradeBot.create_trade', + 'freqtrade.freqtradebot.FreqtradeBot.create_trades', MagicMock(side_effect=DependencyException) ) freqtrade.process_maybe_execute_buy() @@ -1610,7 +1609,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -1650,7 +1649,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, True)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() @@ -1659,7 +1658,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, # Buy is triggering, so buying ... patch_get_signal(freqtrade, value=(True, False)) - freqtrade.create_trade() + freqtrade.create_trades() trades = Trade.query.all() nb_trades = len(trades) assert nb_trades == 1 @@ -1706,7 +1705,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True @@ -1738,7 +1737,7 @@ def test_handle_trade_experimental( freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.is_open = True @@ -1766,7 +1765,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_get_signal(freqtrade) # Create trade and sell it - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2106,7 +2105,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2152,7 +2151,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2201,7 +2200,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2258,7 +2257,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() Trade.session = MagicMock() @@ -2305,7 +2304,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2357,7 +2356,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() freqtrade.process_maybe_execute_sell(trade) assert trade @@ -2409,7 +2408,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2456,7 +2455,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2512,7 +2511,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2543,7 +2542,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2574,7 +2573,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( sell_flag=False, sell_type=SellType.NONE)) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2605,7 +2604,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2635,7 +2634,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2667,7 +2666,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert freqtrade.handle_trade(trade) is False @@ -2720,7 +2719,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2778,7 +2777,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2841,7 +2840,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2900,7 +2899,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3157,7 +3156,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is not None @@ -3191,7 +3190,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade is None @@ -3297,7 +3296,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade From a4ab42560f9494b69432addeaab5cf8eb4dc3b82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:16:59 +0200 Subject: [PATCH 035/102] improve docstring for create_trades --- freqtrade/freqtradebot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e67a8687f..e2c88376e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -258,9 +258,10 @@ class FreqtradeBot(object): def create_trades(self) -> bool: """ - Checks the implemented trading indicator(s) for a randomly picked pair, - if one pair triggers the buy_signal a new trade record gets created - :return: True if a trade object has been created and persisted, False otherwise + Checks the implemented trading strategy for buy-signals, using the active pair whitelist. + If a pair triggers the buy_signal a new trade record gets created. + Checks pairs as long as the open trade count is below `max_open_trades`. + :return: True if at least one trade has been created. """ interval = self.strategy.ticker_interval whitelist = copy.deepcopy(self.active_pair_whitelist) From d6f5f6b7ba0577b65b22216cac558a565a68051b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:21:15 +0200 Subject: [PATCH 036/102] Add test with preexisting trades --- freqtrade/tests/test_freqtradebot.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d74615d6b..868cda4df 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -718,6 +718,33 @@ def test_create_trades_multiple_trades(default_conf, ticker, assert len(trades) == max_open +def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['max_open_trades'] = 4 + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': "12355555"}), + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create 2 existing trades + freqtrade.execute_buy('ETH/BTC', default_conf['stake_amount']) + freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount']) + + assert len(Trade.get_open_trades()) == 2 + + # Create 2 new trades using create_trades + assert freqtrade.create_trades() + + trades = Trade.get_open_trades() + assert len(trades) == 4 + + def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: patch_RPCManager(mocker) From 4da2bfefb73d7a9158e2979f941a3c9209c5a581 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 09:36:28 +0200 Subject: [PATCH 037/102] Improve docstring for some downloading methods --- freqtrade/exchange/exchange.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0e23cf8b8..33b250955 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -551,6 +551,11 @@ class Exchange(object): """ Gets candle history using asyncio and returns the list of candles. Handles all async doing. + Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. + :param pair: Pair to download + :param ticker_interval: Interval to get + :param since_ms: Timestamp in milliseconds to get history from + :returns List of tickers """ return asyncio.get_event_loop().run_until_complete( self._async_get_history(pair=pair, ticker_interval=ticker_interval, @@ -585,6 +590,9 @@ class Exchange(object): def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: """ Refresh in-memory ohlcv asyncronously and set `_klines` with the result + Loops asyncroneously over pair_list and dowloads all pairs async (semi-parallel). + :param pair_list: List of 2 element tuples containing pair,interval to refresh + :return: Returns a List of ticker-dataframes. """ logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) From 06fa07e73e998b208c8af7b2ea9d26f36d36fed6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:07:14 +0200 Subject: [PATCH 038/102] Move parse_timerange to TimeRange class --- freqtrade/configuration/arguments.py | 58 +------------------------ freqtrade/configuration/timerange.py | 65 ++++++++++++++++++++++++++++ freqtrade/tests/test_arguments.py | 26 +---------- freqtrade/tests/test_timerange.py | 28 ++++++++++++ 4 files changed, 95 insertions(+), 82 deletions(-) create mode 100644 freqtrade/configuration/timerange.py create mode 100644 freqtrade/tests/test_timerange.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index dd5a4290e..c129a7e47 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -2,10 +2,8 @@ This module contains the argument manager class """ import argparse -import re -from typing import List, NamedTuple, Optional +from typing import List, Optional -import arrow from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade import constants @@ -43,18 +41,6 @@ ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) -class TimeRange(NamedTuple): - """ - NamedTuple defining timerange inputs. - [start/stop]type defines if [start/stop]ts shall be used. - if *type is None, don't use corresponding startvalue. - """ - starttype: Optional[str] = None - stoptype: Optional[str] = None - startts: int = 0 - stopts: int = 0 - - class Arguments(object): """ Arguments Class. Manage the arguments received by the cli @@ -133,45 +119,3 @@ class Arguments(object): ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) - - @staticmethod - def parse_timerange(text: Optional[str]) -> TimeRange: - """ - Parse the value of the argument --timerange to determine what is the range desired - :param text: value from --timerange - :return: Start and End range period - """ - if text is None: - return TimeRange(None, None, 0, 0) - syntax = [(r'^-(\d{8})$', (None, 'date')), - (r'^(\d{8})-$', ('date', None)), - (r'^(\d{8})-(\d{8})$', ('date', 'date')), - (r'^-(\d{10})$', (None, 'date')), - (r'^(\d{10})-$', ('date', None)), - (r'^(\d{10})-(\d{10})$', ('date', 'date')), - (r'^(-\d+)$', (None, 'line')), - (r'^(\d+)-$', ('line', None)), - (r'^(\d+)-(\d+)$', ('index', 'index'))] - for rex, stype in syntax: - # Apply the regular expression to text - match = re.match(rex, text) - if match: # Regex has matched - rvals = match.groups() - index = 0 - start: int = 0 - stop: int = 0 - if stype[0]: - starts = rvals[index] - 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' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').timestamp - else: - stop = int(stops) - return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py new file mode 100644 index 000000000..b44704682 --- /dev/null +++ b/freqtrade/configuration/timerange.py @@ -0,0 +1,65 @@ +""" +This module contains the argument manager class +""" +import re +from typing import Optional + +import arrow + + +class TimeRange(): + """ + object defining timerange inputs. + [start/stop]type defines if [start/stop]ts shall be used. + if *type is None, don't use corresponding startvalue. + """ + + def __init__(self, starttype: Optional[str], stoptype: Optional[str], + startts: int, stopts: int): + + self.starttype: Optional[str] = starttype + self.stoptype: Optional[str] = stoptype + self.startts: int = startts + self.stopts: int = stopts + + @staticmethod + def parse_timerange(text: Optional[str]): + """ + Parse the value of the argument --timerange to determine what is the range desired + :param text: value from --timerange + :return: Start and End range period + """ + if text is None: + return TimeRange(None, None, 0, 0) + syntax = [(r'^-(\d{8})$', (None, 'date')), + (r'^(\d{8})-$', ('date', None)), + (r'^(\d{8})-(\d{8})$', ('date', 'date')), + (r'^-(\d{10})$', (None, 'date')), + (r'^(\d{10})-$', ('date', None)), + (r'^(\d{10})-(\d{10})$', ('date', 'date')), + (r'^(-\d+)$', (None, 'line')), + (r'^(\d+)-$', ('line', None)), + (r'^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + match = re.match(rex, text) + if match: # Regex has matched + rvals = match.groups() + index = 0 + start: int = 0 + stop: int = 0 + if stype[0]: + starts = rvals[index] + 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' and len(stops) == 8: + stop = arrow.get(stops, 'YYYYMMDD').timestamp + else: + stop = int(stops) + return TimeRange(stype[0], stype[1], start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index bf744f72b..2cb7ff6d7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import Arguments from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME from freqtrade.configuration.cli_options import check_int_positive @@ -86,30 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None: Arguments(['--strategy-path'], '').get_parsed_arg() -def test_parse_timerange_incorrect() -> None: - assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200') - assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-') - assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500') - - assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-') - assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522') - timerange = Arguments.parse_timerange('20100522-20150730') - assert timerange == TimeRange('date', 'date', 1274486400, 1438214400) - - # Added test for unix timestamp - BTC genesis date - assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-') - assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000') - timerange = Arguments.parse_timerange('1231006505-1233360000') - assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange - - # TODO: Find solution for the following case (passing timestamp in ms) - timerange = Arguments.parse_timerange('1231006505000-1233360000000') - assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange - - with pytest.raises(Exception, match=r'Incorrect syntax.*'): - Arguments.parse_timerange('-') - - def test_parse_args_backtesting_invalid() -> None: with pytest.raises(SystemExit, match=r'2'): Arguments(['backtesting --ticker-interval'], '').get_parsed_arg() diff --git a/freqtrade/tests/test_timerange.py b/freqtrade/tests/test_timerange.py new file mode 100644 index 000000000..6599472fb --- /dev/null +++ b/freqtrade/tests/test_timerange.py @@ -0,0 +1,28 @@ +# pragma pylint: disable=missing-docstring, C0103 +import pytest + +from freqtrade.configuration import TimeRange + + +def test_parse_timerange_incorrect() -> None: + assert TimeRange(None, 'line', 0, -200) == TimeRange.parse_timerange('-200') + assert TimeRange('line', None, 200, 0) == TimeRange.parse_timerange('200-') + assert TimeRange('index', 'index', 200, 500) == TimeRange.parse_timerange('200-500') + + assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-') + assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522') + timerange = TimeRange.parse_timerange('20100522-20150730') + assert timerange == TimeRange('date', 'date', 1274486400, 1438214400) + + # Added test for unix timestamp - BTC genesis date + assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-') + assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000') + timerange = TimeRange.parse_timerange('1231006505-1233360000') + assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange + + # TODO: Find solution for the following case (passing timestamp in ms) + timerange = TimeRange.parse_timerange('1231006505000-1233360000000') + assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange + + with pytest.raises(Exception, match=r'Incorrect syntax.*'): + TimeRange.parse_timerange('-') From 51c3a31bb58c01696634360b5547fa09b3fc0341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:07:32 +0200 Subject: [PATCH 039/102] Correct imports and calls to parse_timerange --- freqtrade/configuration/__init__.py | 3 ++- freqtrade/edge/__init__.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/edge_cli.py | 4 ++-- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/plot/plotting.py | 4 ++-- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/test_plotting.py | 6 +++--- scripts/download_backtest_data.py | 2 +- 9 files changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 548b508a7..7b476d173 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,2 +1,3 @@ -from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 +from freqtrade.configuration.arguments import Arguments # noqa: F401 +from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 7085663d6..2d3097ec4 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -10,7 +10,7 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade import constants, OperationalException -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.strategy.interface import SellType @@ -75,7 +75,7 @@ class Edge(): self._stoploss_range_step ) - self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift( + self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( days=-1 * self._since_number_of_days).format('YYYYMMDD')) self.fee = self.exchange.get_fee() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 252175269..8f40a6582 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes @@ -404,7 +404,7 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - timerange = Arguments.parse_timerange(None if self.config.get( + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = history.load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 8d1fa381b..7e0d60843 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -9,7 +9,7 @@ from tabulate import tabulate from freqtrade import constants from freqtrade.edge import Edge -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver @@ -41,7 +41,7 @@ class EdgeCli(object): self.edge = Edge(config, self.exchange, self.strategy) self.edge._refresh_pairs = self.config.get('refresh_pairs', False) - self.timerange = Arguments.parse_timerange(None if self.config.get( + self.timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) self.edge._timerange = self.timerange diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 550f13e28..772b4a10f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,7 +20,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow users import from this file @@ -310,7 +310,7 @@ class Hyperopt(Backtesting): ) def start(self) -> None: - timerange = Arguments.parse_timerange(None if self.config.get( + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d03d3ae53..947b3003c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional import pandas as pd -from freqtrade.configuration import Arguments +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, load_trades) @@ -42,7 +42,7 @@ def init_plotscript(config): pairs = config["exchange"]["pair_whitelist"] # Set timerange to use - timerange = Arguments.parse_timerange(config.get("timerange")) + timerange = TimeRange.parse_timerange(config.get("timerange")) tickers = history.load_data( datadir=Path(str(config.get("datadir"))), diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index bad8db66f..cf8cae566 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,7 +4,7 @@ import pytest from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, combine_tickers_with_mean, create_cum_profit, @@ -121,7 +121,7 @@ def test_combine_tickers_with_mean(): def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', datadir=None, timerange=timerange) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index bfdd72215..cd72160f8 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import plotly.graph_objects as go from plotly.subplots import make_subplots -from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, @@ -222,7 +222,7 @@ def test_generate_plot_file(mocker, caplog): def test_add_profit(): filename = history.make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', datadir=None, timerange=timerange) @@ -242,7 +242,7 @@ def test_add_profit(): def test_generate_profit_graph(): filename = history.make_testdata_path(None) / "backtest-result_test.json" trades = load_backtest_data(filename) - timerange = Arguments.parse_timerange("20180110-20180112") + timerange = TimeRange.parse_timerange("20180110-20180112") pairs = ["POWR/BTC", "XLM/BTC"] tickers = history.load_data(datadir=None, diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 580592294..f77ad7422 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -105,7 +105,7 @@ if not pairs or args.pairs_file: timerange = TimeRange() if args.days: time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") - timerange = arguments.parse_timerange(f'{time_since}-') + timerange = TimeRange.parse_timerange(f'{time_since}-') logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') From 84baef922c131f17532ecf3637ee618a5e2e57d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:14:54 +0200 Subject: [PATCH 040/102] Rename get_history to get_historic_ohlcv --- freqtrade/data/history.py | 9 +++++---- freqtrade/exchange/exchange.py | 14 +++++++------- freqtrade/tests/data/test_history.py | 10 +++++----- freqtrade/tests/exchange/test_exchange.py | 4 ++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index f600615df..899c6d0c8 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -252,10 +252,11 @@ def download_pair_history(datadir: Optional[Path], logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval, - since_ms=since_ms if since_ms - else - int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) + new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + since_ms=since_ms if since_ms + else + int(arrow.utcnow().shift( + days=-30).float_timestamp) * 1000) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 33b250955..65cb409dc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -546,8 +546,8 @@ class Exchange(object): logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] - def get_history(self, pair: str, ticker_interval: str, - since_ms: int) -> List: + def get_historic_ohlcv(self, pair: str, ticker_interval: str, + since_ms: int) -> List: """ Gets candle history using asyncio and returns the list of candles. Handles all async doing. @@ -558,12 +558,12 @@ class Exchange(object): :returns List of tickers """ return asyncio.get_event_loop().run_until_complete( - self._async_get_history(pair=pair, ticker_interval=ticker_interval, - since_ms=since_ms)) + self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, + since_ms=since_ms)) - async def _async_get_history(self, pair: str, - ticker_interval: str, - since_ms: int) -> List: + async def _async_get_historic_ohlcv(self, pair: str, + ticker_interval: str, + since_ms: int) -> List: one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit logger.debug( diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 00f4738f7..4ba65e470 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -80,7 +80,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') _backup_file(file, copy_file=True) history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC']) @@ -96,7 +96,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau """ Test load_pair_history() with 1 min ticker """ - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) exchange = get_patched_exchange(mocker, default_conf) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') @@ -266,7 +266,7 @@ def test_load_cached_data_for_updating(mocker) -> None: def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) exchange = get_patched_exchange(mocker, default_conf) file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') @@ -319,7 +319,7 @@ def test_download_pair_history2(mocker, default_conf) -> None: [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) - mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') @@ -327,7 +327,7 @@ def test_download_pair_history2(mocker, default_conf) -> None: def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_history', + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', side_effect=Exception('File Error')) exchange = get_patched_exchange(mocker, default_conf) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e8a7201f1..ed80edfce 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -996,7 +996,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_history(default_conf, mocker, caplog, exchange_name): +def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) tick = [ [ @@ -1017,7 +1017,7 @@ def test_get_history(default_conf, mocker, caplog, exchange_name): # one_call calculation * 1.8 should do 2 calls since = 5 * 60 * 500 * 1.8 print(f"since = {since}") - ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 # Returns twice the above tick From 096a6426dbf1142c5443371f9569e4f51870d148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 10:22:54 +0200 Subject: [PATCH 041/102] Override equality operator --- freqtrade/configuration/timerange.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index b44704682..f980b71ea 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -14,14 +14,19 @@ class TimeRange(): if *type is None, don't use corresponding startvalue. """ - def __init__(self, starttype: Optional[str], stoptype: Optional[str], - startts: int, stopts: int): + def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None, + startts: int = 0, stopts: int = 0): self.starttype: Optional[str] = starttype self.stoptype: Optional[str] = stoptype self.startts: int = startts self.stopts: int = stopts + def __eq__(self, other): + """Override the default Equals behavior""" + return (self.starttype == other.starttype and self.stoptype == other.stoptype + and self.startts == other.startts and self.stopts == other.stopts) + @staticmethod def parse_timerange(text: Optional[str]): """ From 0ffb184ebad3631320e7f3279746e441d3c591a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:45:24 +0200 Subject: [PATCH 042/102] Change some docstrings and formatting from history --- freqtrade/data/history.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index f600615df..363495796 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -57,10 +57,8 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] -def load_tickerdata_file( - datadir: Optional[Path], pair: str, - ticker_interval: str, - timerange: Optional[TimeRange] = None) -> Optional[list]: +def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: str, + timerange: Optional[TimeRange] = None) -> Optional[list]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccesful @@ -68,7 +66,7 @@ def load_tickerdata_file( filename = pair_data_filename(datadir, pair, ticker_interval) pairdata = misc.file_load_json(filename) if not pairdata: - return None + return [] if timerange: pairdata = trim_tickerlist(pairdata, timerange) @@ -182,6 +180,7 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str, Optional[int]]: """ Load cached data and choose what part of the data should be updated + Only used by download_pair_history(). """ since_ms = None From 91d1061c7304a598caa1be3a38c055530920956d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:48:42 +0200 Subject: [PATCH 043/102] Abstract tickerdata storing --- freqtrade/data/history.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 363495796..849b882b2 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -73,6 +73,15 @@ def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: st return pairdata +def store_tickerdata_file(datadir: Optional[Path], pair: str, + ticker_interval: str, data: list, is_zip: bool = False): + """ + Stores tickerdata to file + """ + filename = pair_data_filename(datadir, pair, ticker_interval) + misc.file_dump_json(filename, data, is_zip=is_zip) + + def load_pair_history(pair: str, ticker_interval: str, datadir: Optional[Path], @@ -175,7 +184,7 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) return filename -def load_cached_data_for_updating(filename: Path, ticker_interval: str, +def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -194,12 +203,10 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str, since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - if filename.is_file(): - with open(filename, "rt") as file: - data = misc.json_load(file) - # remove the last item, could be incomplete candle - if data: - data.pop() + data = load_tickerdata_file(datadir, pair, ticker_interval, TimeRange) + # remove the last item, could be incomplete candle + if data: + data.pop() else: data = [] @@ -238,14 +245,12 @@ def download_pair_history(datadir: Optional[Path], ) try: - filename = pair_data_filename(datadir, pair, ticker_interval) - logger.info( f'Download history data for pair: "{pair}", interval: {ticker_interval} ' f'and store in {datadir}.' ) - data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange) + data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') @@ -260,7 +265,7 @@ def download_pair_history(datadir: Optional[Path], logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) - misc.file_dump_json(filename, data) + store_tickerdata_file(datadir, pair, ticker_interval, data=data) return True except Exception as e: From 9d3322df8c00d4d18f4b182ff0d494055c002013 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 20:49:13 +0200 Subject: [PATCH 044/102] Adapt history-tests to new load_cached_data header --- freqtrade/tests/data/test_history.py | 40 +++++++--------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 00f4738f7..a06c5aa23 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -178,53 +178,41 @@ def test_load_cached_data_for_updating(mocker) -> None: # timeframe starts earlier than the cached data # should fully update data timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == [] assert start_ts == test_data[0][0] - 1000 # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120 - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - TimeRange(None, 'line', 0, -num_lines)) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', TimeRange(None, 'line', 0, -num_lines)) assert data == [] assert start_ts < test_data[0][0] - 1 # timeframe starts in the center of the cached data # should return the chached data w/o the last item timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # timeframe starts after the chached data # should return the chached data w/o the last item timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # same with 'line' timeframe num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] @@ -232,35 +220,27 @@ def test_load_cached_data_for_updating(mocker) -> None: # should return the chached data w/o the last item num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename, - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] # no datafile exist # should return timestamp start time timerange = TimeRange('date', None, now_ts - 10000, 0) - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange) assert data == [] assert start_ts == (now_ts - 10000) * 1000 # same with 'line' timeframe num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - timerange) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange) assert data == [] assert start_ts == (now_ts - num_lines * 60) * 1000 # no datafile exist, no timeframe is set # should return an empty array and None - data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), - '1m', - None) + data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', None) assert data == [] assert start_ts is None From b2a22f1afb4e10d635c33623acddd22a8dc4d9ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 21:39:53 +0200 Subject: [PATCH 045/102] Fix samll errors --- freqtrade/data/history.py | 4 ++-- freqtrade/tests/data/test_history.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 849b882b2..c6d731afa 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -184,7 +184,7 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) return filename -def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, +def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -203,7 +203,7 @@ def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - data = load_tickerdata_file(datadir, pair, ticker_interval, TimeRange) + data = load_tickerdata_file(datadir, pair, ticker_interval, timerange) # remove the last item, could be incomplete candle if data: data.pop() diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index a06c5aa23..164ebe01a 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -184,7 +184,8 @@ def test_load_cached_data_for_updating(mocker) -> None: # same with 'line' timeframe num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120 - data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', TimeRange(None, 'line', 0, -num_lines)) + data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', + TimeRange(None, 'line', 0, -num_lines)) assert data == [] assert start_ts < test_data[0][0] - 1 From f3e6bcb20c166bc7287caced4e3559604a39f0c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:35:50 +0200 Subject: [PATCH 046/102] Avoid using negative indexes --- freqtrade/data/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index c6d731afa..5471767f6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -43,7 +43,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: start_index += 1 if timerange.stoptype == 'line': - start_index = len(tickerlist) + timerange.stopts + start_index = max(len(tickerlist) + timerange.stopts, 0) if timerange.stoptype == 'index': stop_index = timerange.stopts elif timerange.stoptype == 'date': From 11790fbf0174f565a1a86dda2eb4fb283ea5d93d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:37:26 +0200 Subject: [PATCH 047/102] Fix typos in docstrings --- freqtrade/exchange/exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 65cb409dc..a2558d3d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -589,9 +589,9 @@ class Exchange(object): def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: """ - Refresh in-memory ohlcv asyncronously and set `_klines` with the result - Loops asyncroneously over pair_list and dowloads all pairs async (semi-parallel). - :param pair_list: List of 2 element tuples containing pair,interval to refresh + Refresh in-memory ohlcv asynchronously and set `_klines` with the result + Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). + :param pair_list: List of 2 element tuples containing pair, interval to refresh :return: Returns a List of ticker-dataframes. """ logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) @@ -640,7 +640,7 @@ class Exchange(object): async def _async_get_candle_history(self, pair: str, ticker_interval: str, since_ms: Optional[int] = None) -> Tuple[str, str, List]: """ - Asyncronously gets candle histories using fetch_ohlcv + Asynchronously gets candle histories using fetch_ohlcv returns tuple: (pair, ticker_interval, ohlcv_list) """ try: From f5e437d8c789dc9e383ae2dddfe1c249ffbc956c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:59:45 +0200 Subject: [PATCH 048/102] Change create_trade to create_trades for new test --- freqtrade/tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d6d547c29..24d070d2d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2611,7 +2611,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2630,7 +2630,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock # reinit - should buy other pair. caplog.clear() - freqtrade.create_trade() + freqtrade.create_trades() assert log_has(f"Pair {trade.pair} is currently locked.", caplog) From fd77f699dfbda3bf72ea9c42de13abc46531809a Mon Sep 17 00:00:00 2001 From: Ashton Honnecke Date: Thu, 15 Aug 2019 10:41:02 -0600 Subject: [PATCH 049/102] f the string --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..046a4320b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -290,7 +290,7 @@ class Configuration(object): if not self.runmode: # Handle real mode, infer dry/live from config self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - logger.info("Runmode set to {self.runmode}.") + logger.info(f"Runmode set to {self.runmode}.") config.update({'runmode': self.runmode}) From a94a89086f7b37638a4eec19c93482db9d05a731 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:09:00 +0200 Subject: [PATCH 050/102] Don't forward timerange to load_ticker_file when loading cached data for updating. We always want to get all data, not just a fraction (we would end up overwriting the non-loaded part of the data). --- freqtrade/data/history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 5471767f6..14749925f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -203,7 +203,8 @@ def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_int since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file - data = load_tickerdata_file(datadir, pair, ticker_interval, timerange) + # Intentionally don't pass timerange in - since we need to load the full dataset. + data = load_tickerdata_file(datadir, pair, ticker_interval) # remove the last item, could be incomplete candle if data: data.pop() From 12677f2d42f8d3f789cd84b42f693bb142c6f9ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:13:19 +0200 Subject: [PATCH 051/102] Adjust docstring to match functioning of load_cached_data --- freqtrade/data/history.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 14749925f..af7d8cc7c 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -188,7 +188,9 @@ def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_int timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ - Load cached data and choose what part of the data should be updated + Load cached data to download more data. + If timerange is passed in, checks wether data from an before the stored data will be downloaded. + If that's the case than what's available should be completely overwritten. Only used by download_pair_history(). """ From 69eff890496df3cc374ae1f8eb260aa2d421db52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 20:28:32 +0200 Subject: [PATCH 052/102] Improve comment in test_history to explain what is tested --- freqtrade/tests/data/test_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 164ebe01a..f238046d7 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -210,7 +210,8 @@ def test_load_cached_data_for_updating(mocker) -> None: assert data == test_data[:-1] assert test_data[-2][0] < start_ts < test_data[-1][0] - # same with 'line' timeframe + # Try loading last 30 lines. + # Not supported by load_cached_data_for_updating, we always need to get the full data. num_lines = 30 timerange = TimeRange(None, 'line', 0, -num_lines) data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange) From 4fa92ec0fa468f759d1d8b602cfbcbfd881b35de Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 15 Aug 2019 21:39:04 +0300 Subject: [PATCH 053/102] hyperopt: --print-json option added --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 6 +++ freqtrade/configuration/configuration.py | 3 ++ freqtrade/optimize/hyperopt.py | 53 +++++++++++++++++------- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c129a7e47..926d02f8f 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -22,7 +22,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", - "print_colorized", "hyperopt_jobs", + "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_continue", "hyperopt_loss"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index b098fa8bc..84686d1e6 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -198,6 +198,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), + "print_json": Arg( + '--print-json', + help='Print best result detailization in JSON format.', + action='store_true', + default=False, + ), "hyperopt_jobs": Arg( '-j', '--job-workers', help='The number of concurrently running jobs for hyperoptimization ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 587c8757e..eaba0b4da 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -242,6 +242,9 @@ class Configuration(object): else: config.update({'print_colorized': True}) + self._args_to_config(config, argname='print_json', + logstring='Parameter --print-json detected ...') + self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 772b4a10f..7664608ce 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,11 +8,14 @@ import logging import os import sys +from collections import OrderedDict from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List, Optional +import rapidjson + from colorama import init as colorama_init from colorama import Fore, Style from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count @@ -133,22 +136,44 @@ class Hyperopt(Backtesting): results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] params = best_result['params'] - log_str = self.format_results_logstring(best_result) print(f"\nBest result:\n\n{log_str}\n") - if self.has_space('buy'): - print('Buy hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, - indent=4) - if self.has_space('sell'): - print('Sell hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, - indent=4) - if self.has_space('roi'): - print("ROI table:") - pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) - if self.has_space('stoploss'): - print(f"Stoploss: {params.get('stoploss')}") + + if self.config.get('print_json'): + result_dict = {} + if self.has_space('buy') or self.has_space('sell'): + result_dict['params'] = {} + if self.has_space('buy'): + result_dict['params'].update({p.name: params.get(p.name) + for p in self.hyperopt_space('buy')}) + if self.has_space('sell'): + result_dict['params'].update({p.name: params.get(p.name) + for p in self.hyperopt_space('sell')}) + if self.has_space('roi'): + min_roi = self.custom_hyperopt.generate_roi_table(params) + # Convert keys in min_roi dict to strings because + # rapidjson cannot dump dicts with integer keys... + # OrderedDict is used to keep the numeric order of the items + # in the dict. + min_roi = OrderedDict((str(k),v) for k,v in min_roi.items()) + result_dict['minimal_roi'] = min_roi + if self.has_space('stoploss'): + result_dict['stoploss'] = params.get('stoploss') + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + else: + if self.has_space('buy'): + print('Buy hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, + indent=4) + if self.has_space('sell'): + print('Sell hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, + indent=4) + if self.has_space('roi'): + print("ROI table:") + pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + if self.has_space('stoploss'): + print(f"Stoploss: {params.get('stoploss')}") def log_results(self, results) -> None: """ From e525275d102872a68c323000d0ab5d1fd69f5740 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 15 Aug 2019 23:13:46 +0300 Subject: [PATCH 054/102] make flake and mypy happy --- freqtrade/optimize/hyperopt.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7664608ce..05b12e653 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -140,23 +140,23 @@ class Hyperopt(Backtesting): print(f"\nBest result:\n\n{log_str}\n") if self.config.get('print_json'): - result_dict = {} + result_dict: Dict = {} if self.has_space('buy') or self.has_space('sell'): result_dict['params'] = {} if self.has_space('buy'): result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('buy')}) + for p in self.hyperopt_space('buy')}) if self.has_space('sell'): result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('sell')}) + for p in self.hyperopt_space('sell')}) if self.has_space('roi'): - min_roi = self.custom_hyperopt.generate_roi_table(params) # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... # OrderedDict is used to keep the numeric order of the items # in the dict. - min_roi = OrderedDict((str(k),v) for k,v in min_roi.items()) - result_dict['minimal_roi'] = min_roi + result_dict['minimal_roi'] = OrderedDict( + (str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() + ) if self.has_space('stoploss'): result_dict['stoploss'] = params.get('stoploss') print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) From 2a842778e36bdef97af38ba955b0baad71cdd624 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 16 Aug 2019 00:49:49 +0300 Subject: [PATCH 055/102] tests added --- freqtrade/tests/optimize/test_hyperopt.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 0317bb37d..b77ea1c84 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -618,3 +618,69 @@ def test_continue_hyperopt(mocker, default_conf, caplog): assert unlinkmock.call_count == 0 assert log_has(f"Continuing on previous hyperopt results.", caplog) + + +def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + 'print_json': True, + }) + + hyperopt = Hyperopt(default_conf) + hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 + + +def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'roi stoploss', + 'hyperopt_jobs': 1, + 'print_json': True, + }) + + hyperopt = Hyperopt(default_conf) + hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert '{"minimal_roi":{},"stoploss":null}' in out From b94f3e80c40d4d817a32e13e63f467763d2a9eed Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 16 Aug 2019 04:20:12 +0300 Subject: [PATCH 056/102] tests fixed --- freqtrade/tests/optimize/test_hyperopt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index b77ea1c84..1c4e2445c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -621,6 +621,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog): def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -651,9 +652,13 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: out, err = capsys.readouterr() assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -684,3 +689,6 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> out, err = capsys.readouterr() assert '{"minimal_roi":{},"stoploss":null}' in out + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 From 8d206f83083fe880293c035ff6019ccc1cb50113 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 06:29:19 +0200 Subject: [PATCH 057/102] Fix wrong warning box --- docs/strategy-customization.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 15f44955b..0d08bdd02 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -309,8 +309,10 @@ if self.dp: dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_ask'] = ob['asks'][0][0] ``` -!Warning The order book is not part of the historic data which means backtesting and hyperopt will not work if this - method is used. + +!!! Warning + The order book is not part of the historic data which means backtesting and hyperopt will not work if this + method is used. #### Available Pairs From 53db382695b236d404a9557b05200e1bd9616e12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 10:19:06 +0200 Subject: [PATCH 058/102] Update dockerfile python version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7a0298719..8677b54de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.3-slim-stretch +FROM python:3.7.4-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ From 09286d49182fb98553dd2d137fc85b3d6b742a50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 13:04:07 +0200 Subject: [PATCH 059/102] file_dump_json accepts Path - so we should feed it that --- freqtrade/misc.py | 8 ++++---- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/tests/optimize/test_backtesting.py | 5 +++-- freqtrade/tests/test_misc.py | 5 +++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 05946e008..d01d6a254 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -5,11 +5,11 @@ import gzip import logging import re from datetime import datetime +from pathlib import Path import numpy as np import rapidjson - logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def file_dump_json(filename, data, is_zip=False) -> None: +def file_dump_json(filename: Path, data, is_zip=False) -> None: """ Dump JSON data into a file :param filename: file to create @@ -49,8 +49,8 @@ def file_dump_json(filename, data, is_zip=False) -> None: logger.info(f'dumping json to "{filename}"') if is_zip: - if not filename.endswith('.gz'): - filename = filename + '.gz' + if filename.suffix != '.gz': + filename = filename.with_suffix('.gz') with gzip.open(filename, 'w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) else: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 252175269..d321affeb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -190,7 +190,7 @@ class Backtesting(object): return tabulate(tabular_data, headers=headers, # type: ignore floatfmt=floatfmt, tablefmt="pipe") - def _store_backtest_result(self, recordfilename: str, results: DataFrame, + def _store_backtest_result(self, recordfilename: Path, results: DataFrame, strategyname: Optional[str] = None) -> None: records = [(t.pair, t.profit_percent, t.open_time.timestamp(), @@ -201,10 +201,10 @@ class Backtesting(object): if records: if strategyname: # Inject strategyname to filename - recname = Path(recordfilename) - recordfilename = str(Path.joinpath( - recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix)) - logger.info('Dumping backtest results to %s', recordfilename) + recordfilename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix) + logger.info(f'Dumping backtest results to {recordfilename}') file_dump_json(recordfilename, records) def _get_ticker_list(self, processed) -> Dict[str, DataFrame]: @@ -458,7 +458,7 @@ class Backtesting(object): for strategy, results in all_results.items(): if self.config.get('export', False): - self._store_backtest_result(self.config['exportfilename'], results, + self._store_backtest_result(Path(self.config['exportfilename']), results, strategy if len(self.strategylist) > 1 else None) print(f"Result for strategy {strategy}") diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 9ed7e7296..02e9a9c28 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -2,6 +2,7 @@ import math import random +from pathlib import Path from unittest.mock import MagicMock import numpy as np @@ -785,10 +786,10 @@ def test_backtest_record(default_conf, fee, mocker): # reset test to test with strategy name names = [] records = [] - backtesting._store_backtest_result("backtest-result.json", results, "DefStrat") + backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat") assert len(results) == 4 # Assert file_dump_json was only called once - assert names == ['backtest-result-DefStrat.json'] + assert names == [Path('backtest-result-DefStrat.json')] records = records[0] # Ensure records are of correct type assert len(records) == 4 diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 1a6b2a92d..c55083e64 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring,C0103 import datetime +from pathlib import Path from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe @@ -34,12 +35,12 @@ def test_datesarray_to_datetimearray(ticker_history_list): def test_file_dump_json(mocker) -> None: file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) - file_dump_json('somefile', [1, 2, 3]) + file_dump_json(Path('somefile'), [1, 2, 3]) assert file_open.call_count == 1 assert json_dump.call_count == 1 file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) - file_dump_json('somefile', [1, 2, 3], True) + file_dump_json(Path('somefile'), [1, 2, 3], True) assert file_open.call_count == 1 assert json_dump.call_count == 1 From 91886120a7d17071a4663cd39c36d378b7d563c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:37:10 +0200 Subject: [PATCH 060/102] use nargs for --pairs argument --- freqtrade/configuration/cli_options.py | 6 ++++-- freqtrade/constants.py | 1 - freqtrade/plot/plotting.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 84686d1e6..d39013737 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -254,7 +254,8 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', + help='Show profits for only these pairs. Pairs are space-separated.', + nargs='+', ), # Download data "pairs_file": Arg( @@ -276,9 +277,10 @@ AVAILABLE_CLI_OPTIONS = { "timeframes": Arg( '-t', '--timeframes', help=f'Specify which tickers to download. Space-separated list. ' - f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', + f'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], + default=['1m', '5m'], nargs='+', ), "erase": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9b73adcfe..fbf44dec8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,6 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 -DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m' TICKER_INTERVALS = [ '1m', '3m', '5m', '15m', '30m', diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 947b3003c..e6da581a4 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -37,7 +37,7 @@ def init_plotscript(config): strategy = StrategyResolver(config).strategy if "pairs" in config: - pairs = config["pairs"].split(',') + pairs = config["pairs"] else: pairs = config["exchange"]["pair_whitelist"] From 05deb9e09bdccd0c19904ed2687cc6bd8f2bf29f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:42:44 +0200 Subject: [PATCH 061/102] Migrate download-script logic to utils.py --- freqtrade/configuration/arguments.py | 19 ++++++-- freqtrade/tests/test_arguments.py | 18 ++++---- freqtrade/utils.py | 66 ++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 926d02f8f..8fa16318a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,7 +30,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column"] -ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", @@ -40,6 +40,8 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) +NO_CONF_REQURIED = ["start_download_data"] + class Arguments(object): """ @@ -75,7 +77,10 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if not self._no_default_config and parsed_arg.config is None: + # Allow no-config for certain commands (like downloading / plotting) + if (not self._no_default_config and parsed_arg.config is None + and not (hasattr(parsed_arg, 'func') + and parsed_arg.func.__name__ in NO_CONF_REQURIED)): parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -93,7 +98,7 @@ class Arguments(object): :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import start_list_exchanges + from freqtrade.utils import start_download_data, start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') @@ -119,3 +124,11 @@ class Arguments(object): ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + + # Add download-data subcommand + download_data_cmd = subparsers.add_parser( + 'download-data', + help='Download backtesting data.' + ) + download_data_cmd.set_defaults(func=start_download_data) + self._build_args(optionlist=ARGS_DOWNLOADER, parser=download_data_cmd) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 2cb7ff6d7..31ab9dea8 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -50,10 +50,10 @@ def test_parse_args_verbose() -> None: def test_common_scripts_options() -> None: - arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments._build_args(ARGS_DOWNLOADER) - args = arguments._parse_args() - assert args.pairs == 'ETH/BTC' + args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg() + + assert args.pairs == ['ETH/BTC', 'XRP/BTC'] + assert hasattr(args, "func") def test_parse_args_version() -> None: @@ -135,14 +135,14 @@ def test_parse_args_hyperopt_custom() -> None: def test_download_data_options() -> None: args = [ - '--pairs-file', 'file_with_pairs', '--datadir', 'datadir/directory', + 'download-data', + '--pairs-file', 'file_with_pairs', '--days', '30', '--exchange', 'binance' ] - arguments = Arguments(args, '') - arguments._build_args(ARGS_DOWNLOADER) - args = arguments._parse_args() + args = Arguments(args, '').get_parsed_arg() + assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/directory' assert args.days == 30 @@ -162,7 +162,7 @@ def test_plot_dataframe_options() -> None: assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.plot_limit == 30 - assert pargs.pairs == "UNITTEST/BTC" + assert pargs.pairs == ["UNITTEST/BTC"] def test_check_int_positive() -> None: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d550ef43c..d2770ba1a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,11 +1,16 @@ import logging +import sys from argparse import Namespace +from pathlib import Path from typing import Any, Dict -from freqtrade.configuration import Configuration -from freqtrade.exchange import available_exchanges -from freqtrade.state import RunMode +import arrow +from freqtrade.configuration import Configuration, TimeRange +from freqtrade.data.history import download_pair_history +from freqtrade.exchange import available_exchanges +from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -17,7 +22,7 @@ def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any :return: Configuration """ configuration = Configuration(args, method) - config = configuration.load_config() + config = configuration.get_config() config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials @@ -39,3 +44,56 @@ def start_list_exchanges(args: Namespace) -> None: else: print(f"Exchanges supported by ccxt and available for Freqtrade: " f"{', '.join(available_exchanges())}") + + +def start_download_data(args: Namespace) -> None: + """ + Download data based + """ + config = setup_utils_configuration(args, RunMode.OTHER) + + timerange = TimeRange() + if 'days' in config: + time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") + timerange = TimeRange.parse_timerange(f'{time_since}-') + + dl_path = Path(config['datadir']) + logger.info(f'About to download pairs: {config["pairs"]}, ' + f'intervals: {config["timeframes"]} to {dl_path}') + + pairs_not_available = [] + + try: + # Init exchange + exchange = ExchangeResolver(config['exchange']['name'], config).exchange + + for pair in config["pairs"]: + if pair not in exchange._api.markets: + pairs_not_available.append(pair) + logger.info(f"Skipping pair {pair}...") + continue + for ticker_interval in config["timeframes"]: + pair_print = pair.replace('/', '_') + filename = f'{pair_print}-{ticker_interval}.json' + dl_file = dl_path.joinpath(filename) + if args.erase and dl_file.exists(): + logger.info( + f'Deleting existing data for pair {pair}, interval {ticker_interval}.') + dl_file.unlink() + + logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') + download_pair_history(datadir=dl_path, exchange=exchange, + pair=pair, ticker_interval=str(ticker_interval), + timerange=timerange) + + except KeyboardInterrupt: + sys.exit("SIGINT received, aborting ...") + + finally: + if pairs_not_available: + logger.info( + f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {config['exchange']['name']}.") + + # configuration.resolve_pairs_list() + print(config) From 8655e521d7c64812ffda7f69e95e4959a6fcf6f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:53:46 +0200 Subject: [PATCH 062/102] Adapt some tests --- freqtrade/tests/test_arguments.py | 2 +- freqtrade/tests/test_main.py | 4 +++- freqtrade/tests/test_plotting.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 31ab9dea8..601f41e63 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -4,7 +4,7 @@ import argparse import pytest from freqtrade.configuration import Arguments -from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.configuration.cli_options import check_int_positive diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index d8ec532b0..409025a3c 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring from copy import deepcopy -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest @@ -21,6 +21,7 @@ def test_parse_args_backtesting(mocker) -> None: further argument parsing is done in test_arguments.py """ backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) + backtesting_mock.__name__ = PropertyMock("start_backtesting") # it's sys.exit(0) at the end of backtesting with pytest.raises(SystemExit): main(['backtesting']) @@ -36,6 +37,7 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) + hyperopt_mock.__name__ = PropertyMock("start_hyperopt") # it's sys.exit(0) at the end of hyperopt with pytest.raises(SystemExit): main(['hyperopt']) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cd72160f8..94d40ab84 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -50,7 +50,7 @@ def test_init_plotscript(default_conf, mocker): assert "pairs" in ret assert "strategy" in ret - default_conf['pairs'] = "POWR/BTC,XLM/BTC" + default_conf['pairs'] = ["POWR/BTC", "XLM/BTC"] ret = init_plotscript(default_conf) assert "tickers" in ret assert "POWR/BTC" in ret["tickers"] From 3c15e3ebddd07d14ba085f4d226d5bea2d7a97a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:56:38 +0200 Subject: [PATCH 063/102] Default load minimal config --- freqtrade/configuration/configuration.py | 13 +++++++++++++ freqtrade/constants.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c95246fc0..e24ace34b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -52,6 +52,9 @@ class Configuration(object): # Keep this method as staticmethod, so it can be used from interactive environments config: Dict[str, Any] = {} + if not files: + return constants.MINIMAL_CONFIG + # We expect here a list of config filenames for path in files: logger.info(f'Using config: {path} ...') @@ -276,6 +279,16 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') + self._args_to_config(config, argname='timeframes', + logstring='timeframes --timeframes: {}') + + self._args_to_config(config, argname='days', + logstring='Detected --days: {}') + + if "exchange" in self.args and self.args.exchange: + config['exchange']['name'] = self.args.exchange + logger.info(f"Using exchange {config['exchange']['name']}") + def _process_runmode(self, config: Dict[str, Any]) -> None: if not self.runmode: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fbf44dec8..b73a723eb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -38,6 +38,20 @@ SUPPORTED_FIAT = [ "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT" ] +MINIMAL_CONFIG = { + 'stake_currency': '', + 'dry_run': True, + 'exchange': { + 'name': '', + 'key': '', + 'secret': '', + 'pair_whitelist': [], + 'ccxt_async_config': { + 'enableRateLimit': True, + } + } +} + # Required json-schema for user specified config CONF_SCHEMA = { 'type': 'object', From 4e308a1a3e4aef76c809e53f1d60953399fb8655 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 14:56:57 +0200 Subject: [PATCH 064/102] Resolve pairlist in configuration --- freqtrade/configuration/configuration.py | 37 ++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e24ace34b..ed12b6501 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -90,6 +90,11 @@ class Configuration(object): self._process_runmode(config) + # Check if the exchange set by the user is supported + check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) + + self._resolve_pairs_list(config) + return config def _process_logging_options(self, config: Dict[str, Any]) -> None: @@ -150,9 +155,6 @@ class Configuration(object): if 'sd_notify' in self.args and self.args.sd_notify: config['internals'].update({'sd_notify': True}) - # Check if the exchange set by the user is supported - check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) - def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load datadir configuration: @@ -348,3 +350,32 @@ class Configuration(object): logger.info(logstring.format(config[argname])) if deprecated_msg: warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) + + def _resolve_pairs_list(self, config: Dict[str, Any]) -> None: + """ + Helper for download script. + Takes first found: + * -p (pairs argument) + * --pairs-file + * whitelist from config + """ + + if "pairs" in self.args and self.args.pairs: + return + + if "pairs_file" in self.args and self.args.pairs_file: + pairs_file = self.args.pairs_file + logger.info(f'Reading pairs file "{pairs_file}".') + # Download pairs from the pairs file if no config is specified + # or if pairs file is specified explicitely + if not pairs_file.exists(): + OperationalException(f'No pairs file found with path "{pairs_file}".') + + # with pairs_file.open() as file: + # pairs = list(set(json.load(file))) + + # pairs.sort() + + if "config" in self.args: + logger.info("Using pairlist from configuration.") + config['pairs'] = config.get('exchange', {}).get('pair_whitelist') From 219d0b7fb016f51f90f5778941d6aba72f907910 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:27:33 +0200 Subject: [PATCH 065/102] Adjust documentation to removed download-script --- docs/backtesting.md | 67 +++++++++++++----------- freqtrade/configuration/configuration.py | 3 ++ freqtrade/data/history.py | 2 +- freqtrade/tests/data/test_history.py | 4 +- freqtrade/utils.py | 4 +- 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 7e9f7ff53..f666c5b49 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -3,9 +3,43 @@ This page explains how to validate your strategy performance by using Backtesting. +## Getting data for backtesting / hyperopt + +To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. + +If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. +Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. + +Alternatively, a `pairs.json` file can be used. + +If you are using Binance for example: + +- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. +- update the `pairs.json` to contain the currency pairs you are interested in. + +```bash +mkdir -p user_data/data/binance +cp freqtrade/tests/testdata/pairs.json user_data/data/binance +``` + +Then run: + +```bash +freqtrade download-data --exchange binance +``` + +This will download ticker data for all the currency pairs you defined in `pairs.json`. + +- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. +- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.) +- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. +- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days). +- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. +- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. + ## Test your strategy with Backtesting -Now you have good Buy and Sell strategies, you want to test it against +Now you have good Buy and Sell strategies and some historic data, you want to test it against real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). @@ -109,37 +143,6 @@ The full timerange specification: - Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` -#### Downloading new set of ticker data - -To download new set of backtesting ticker data, you can use a download script. - -If you are using Binance for example: - -- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. -- update the `pairs.json` to contain the currency pairs you are interested in. - -```bash -mkdir -p user_data/data/binance -cp freqtrade/tests/testdata/pairs.json user_data/data/binance -``` - -Then run: - -```bash -python scripts/download_backtest_data.py --exchange binance -``` - -This will download ticker data for all the currency pairs you defined in `pairs.json`. - -- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. -- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`. -- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. -- To download ticker data for only 10 days, use `--days 10`. -- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. -- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. - -For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). - ## Understand the backtesting result The most important in the backtesting is to understand the result. diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ed12b6501..f7c393b60 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -281,6 +281,9 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') + self._args_to_config(config, argname='erase', + logstring='Erase detected. Deleting existing data.') + self._args_to_config(config, argname='timeframes', logstring='timeframes --timeframes: {}') diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 899c6d0c8..c7b3a28b0 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -122,7 +122,7 @@ def load_pair_history(pair: str, else: logger.warning( f'No history data for pair: "{pair}", interval: {ticker_interval}. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data' ) return None diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 4ba65e470..ea56b4bec 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -74,7 +74,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert ld is None assert log_has( 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data', caplog ) @@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau assert os.path.isfile(file) is False assert log_has( 'No history data for pair: "MEME/BTC", interval: 1m. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'script to download the data', caplog ) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d2770ba1a..7ccbae81a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -48,7 +48,7 @@ def start_list_exchanges(args: Namespace) -> None: def start_download_data(args: Namespace) -> None: """ - Download data based + Download data (former download_backtest_data.py script) """ config = setup_utils_configuration(args, RunMode.OTHER) @@ -76,7 +76,7 @@ def start_download_data(args: Namespace) -> None: pair_print = pair.replace('/', '_') filename = f'{pair_print}-{ticker_interval}.json' dl_file = dl_path.joinpath(filename) - if args.erase and dl_file.exists(): + if config.get("erase") and dl_file.exists(): logger.info( f'Deleting existing data for pair {pair}, interval {ticker_interval}.') dl_file.unlink() From 89257832d721cb06c7e2f80f860f8006fc856fc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:27:59 +0200 Subject: [PATCH 066/102] Don't use internal _API methods --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 7ccbae81a..002d89738 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -68,7 +68,7 @@ def start_download_data(args: Namespace) -> None: exchange = ExchangeResolver(config['exchange']['name'], config).exchange for pair in config["pairs"]: - if pair not in exchange._api.markets: + if pair not in exchange.markets: pairs_not_available.append(pair) logger.info(f"Skipping pair {pair}...") continue From b2c215029d4937223b88b39a7d37c2373896be0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:28:11 +0200 Subject: [PATCH 067/102] Add tests for download_data entrypoint --- freqtrade/tests/test_utils.py | 51 ++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index a12b709d7..8d0e76cde 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,8 +1,11 @@ -from freqtrade.utils import setup_utils_configuration, start_list_exchanges -from freqtrade.tests.conftest import get_args -from freqtrade.state import RunMode - import re +from pathlib import Path +from unittest.mock import MagicMock, PropertyMock + +from freqtrade.state import RunMode +from freqtrade.tests.conftest import get_args, log_has, patch_exchange +from freqtrade.utils import (setup_utils_configuration, start_download_data, + start_list_exchanges) def test_setup_utils_configuration(): @@ -40,3 +43,43 @@ def test_list_exchanges(capsys): assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out) assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^bittrex$", captured.out, re.MULTILINE) + + +def test_download_data(mocker, markets, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "unlink", MagicMock()) + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--erase" + ] + start_download_data(get_args(args)) + + assert dl_mock.call_count == 4 + assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog) + assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) + + +def test_download_data_no_markets(mocker, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC" + ] + start_download_data(get_args(args)) + + assert dl_mock.call_count == 0 + assert log_has("Skipping pair ETH/BTC...", caplog) + assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) From 132f28ad44be07194ee68cda69a488c75d1b7b8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 15:52:59 +0200 Subject: [PATCH 068/102] Add tests to correctly load / override pair-lists --- freqtrade/configuration/configuration.py | 21 ++++-- freqtrade/tests/test_configuration.py | 88 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f7c393b60..cb698544d 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,6 +4,7 @@ This module contains the configuration class import logging import warnings from argparse import Namespace +from pathlib import Path from typing import Any, Callable, Dict, List, Optional from freqtrade import OperationalException, constants @@ -12,7 +13,7 @@ from freqtrade.configuration.create_datadir import create_datadir from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts +from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -363,22 +364,28 @@ class Configuration(object): * whitelist from config """ - if "pairs" in self.args and self.args.pairs: + if "pairs" in config: return if "pairs_file" in self.args and self.args.pairs_file: - pairs_file = self.args.pairs_file + pairs_file = Path(self.args.pairs_file) logger.info(f'Reading pairs file "{pairs_file}".') # Download pairs from the pairs file if no config is specified # or if pairs file is specified explicitely if not pairs_file.exists(): - OperationalException(f'No pairs file found with path "{pairs_file}".') + raise OperationalException(f'No pairs file found with path "{pairs_file}".') - # with pairs_file.open() as file: - # pairs = list(set(json.load(file))) + config['pairs'] = json_load(pairs_file) - # pairs.sort() + config['pairs'].sort() + return if "config" in self.args: logger.info("Using pairlist from configuration.") config['pairs'] = config.get('exchange', {}).get('pair_whitelist') + else: + # Fall back to /dl_path/pairs.json + pairs_file = Path(config['datadir']) / "pairs.json" + if pairs_file.exists(): + config['pairs'] = json_load(pairs_file) + diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 8cbd02ece..c351b9b72 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -704,3 +704,91 @@ def test_load_config_default_subkeys(all_conf, keys) -> None: validate_config_schema(all_conf) assert subkey in all_conf[key] assert all_conf[key][subkey] == keys[2] + + +def test_pairlist_resolving(): + arglist = [ + 'download-data', + '--pairs', 'ETH/BTC', 'XRP/BTC', + '--exchange', 'binance' + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == 'binance' + + +def test_pairlist_resolving_with_config(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + arglist = [ + '--config', 'config.json', + 'download-data', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == default_conf['exchange']['pair_whitelist'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + # Override pairs + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs', 'ETH/BTC', 'XRP/BTC', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + +def test_pairlist_resolving_with_config_pl(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs-file', 'pairs.json', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert load_mock.call_count == 1 + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == default_conf['exchange']['name'] + + +def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): + patched_configuration_load_config_file(mocker, default_conf) + mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + arglist = [ + '--config', 'config.json', + 'download-data', + '--pairs-file', 'pairs.json', + ] + + args = Arguments(arglist, '').get_parsed_arg() + + with pytest.raises(OperationalException, match=r"No pairs file found with path.*"): + configuration = Configuration(args) + configuration.get_config() From c9207bcc0070bd574bc301f984955bbf3cdda0cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Aug 2019 16:01:30 +0200 Subject: [PATCH 069/102] Remove blank line at end --- freqtrade/configuration/configuration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cb698544d..c51153f4b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -388,4 +388,3 @@ class Configuration(object): pairs_file = Path(config['datadir']) / "pairs.json" if pairs_file.exists(): config['pairs'] = json_load(pairs_file) - From 29c56f4447ad53cb1d9dc157227be4ea09b4ab29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:47:53 +0200 Subject: [PATCH 070/102] Replace download_backtest_data script with warning message --- scripts/download_backtest_data.py | 145 ++---------------------------- 1 file changed, 5 insertions(+), 140 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index f77ad7422..496f83c7d 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,144 +1,9 @@ -#!/usr/bin/env python3 -""" -This script generates json files with pairs history data -""" -import arrow -import json import sys -from pathlib import Path -from typing import Any, Dict, List -from freqtrade.configuration import Arguments, TimeRange -from freqtrade.configuration import Configuration -from freqtrade.configuration.arguments import ARGS_DOWNLOADER -from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.load_config import load_config_file -from freqtrade.data.history import download_pair_history -from freqtrade.exchange import Exchange -from freqtrade.misc import deep_merge_dicts -import logging +print("This script has been integrated into freqtrade " + "and it's functionality is available by calling `freqtrade download-data`.") +print("Please check the documentation on https://www.freqtrade.io/en/latest/backtesting/ " + "for details.") -logger = logging.getLogger('download_backtest_data') - -DEFAULT_DL_PATH = 'user_data/data' - -# Do not read the default config if config is not specified -# in the command line options explicitely -arguments = Arguments(sys.argv[1:], 'Download backtest data', - no_default_config=True) -arguments._build_args(optionlist=ARGS_DOWNLOADER) -args = arguments._parse_args() - -# Use bittrex as default exchange -exchange_name = args.exchange or 'bittrex' - -pairs: List = [] - -configuration = Configuration(args) -config: Dict[str, Any] = {} - -if args.config: - # Now expecting a list of config filenames here, not a string - for path in args.config: - logger.info(f"Using config: {path}...") - # Merge config options, overwriting old values - config = deep_merge_dicts(load_config_file(path), config) - - config['stake_currency'] = '' - # Ensure we do not use Exchange credentials - config['exchange']['dry_run'] = True - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - pairs = config['exchange']['pair_whitelist'] - - if config.get('ticker_interval'): - timeframes = args.timeframes or [config.get('ticker_interval')] - else: - timeframes = args.timeframes or ['1m', '5m'] - -else: - config = { - 'stake_currency': '', - 'dry_run': True, - 'exchange': { - 'name': exchange_name, - 'key': '', - 'secret': '', - 'pair_whitelist': [], - 'ccxt_async_config': { - 'enableRateLimit': True, - 'rateLimit': 200 - } - } - } - timeframes = args.timeframes or ['1m', '5m'] - -configuration._process_logging_options(config) - -if args.config and args.exchange: - logger.warning("The --exchange option is ignored, " - "using exchange settings from the configuration file.") - -# Check if the exchange set by the user is supported -check_exchange(config) - -configuration._process_datadir_options(config) - -dl_path = Path(config['datadir']) - -pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') - -if not pairs or args.pairs_file: - logger.info(f'Reading pairs file "{pairs_file}".') - # Download pairs from the pairs file if no config is specified - # or if pairs file is specified explicitely - if not pairs_file.exists(): - sys.exit(f'No pairs file found with path "{pairs_file}".') - - with pairs_file.open() as file: - pairs = list(set(json.load(file))) - - pairs.sort() - -timerange = TimeRange() -if args.days: - time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") - timerange = TimeRange.parse_timerange(f'{time_since}-') - -logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') - -pairs_not_available = [] - -try: - # Init exchange - exchange = Exchange(config) - - for pair in pairs: - if pair not in exchange._api.markets: - pairs_not_available.append(pair) - logger.info(f"Skipping pair {pair}...") - continue - for ticker_interval in timeframes: - pair_print = pair.replace('/', '_') - filename = f'{pair_print}-{ticker_interval}.json' - dl_file = dl_path.joinpath(filename) - if args.erase and dl_file.exists(): - logger.info( - f'Deleting existing data for pair {pair}, interval {ticker_interval}.') - dl_file.unlink() - - logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=str(ticker_interval), - timerange=timerange) - -except KeyboardInterrupt: - sys.exit("SIGINT received, aborting ...") - -finally: - if pairs_not_available: - logger.info( - f"Pairs [{','.join(pairs_not_available)}] not available " - f"on exchange {config['exchange']['name']}.") +sys.exit(1) From f7d5280f47ced954a57536cde7dc1cc6561f7203 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:48:34 +0200 Subject: [PATCH 071/102] Replace ARGS_DOWNLOADER with ARGS_DOWNLOAD_DATA --- freqtrade/configuration/arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 8fa16318a..c45e3d7ba 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,7 +30,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column"] -ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", @@ -131,4 +131,4 @@ class Arguments(object): help='Download backtesting data.' ) download_data_cmd.set_defaults(func=start_download_data) - self._build_args(optionlist=ARGS_DOWNLOADER, parser=download_data_cmd) + self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) From a53e9e3a98cf440d1f14a9d55d907a4139e46b97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 06:58:38 +0200 Subject: [PATCH 072/102] improve tests for download_module --- freqtrade/tests/test_utils.py | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 8d0e76cde..003b3b286 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -2,6 +2,8 @@ import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import pytest + from freqtrade.state import RunMode from freqtrade.tests.conftest import get_args, log_has, patch_exchange from freqtrade.utils import (setup_utils_configuration, start_download_data, @@ -58,15 +60,41 @@ def test_download_data(mocker, markets, caplog): "download-data", "--exchange", "binance", "--pairs", "ETH/BTC", "XRP/BTC", - "--erase" + "--erase", ] start_download_data(get_args(args)) assert dl_mock.call_count == 4 + assert dl_mock.call_args[1]['timerange'].starttype is None + assert dl_mock.call_args[1]['timerange'].stoptype is None assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog) assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) +def test_download_data_days(mocker, markets, caplog): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "unlink", MagicMock()) + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + ] + + start_download_data(get_args(args)) + + assert dl_mock.call_count == 4 + assert dl_mock.call_args[1]['timerange'].starttype == 'date' + + assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) + + def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) patch_exchange(mocker) @@ -76,10 +104,28 @@ def test_download_data_no_markets(mocker, caplog): args = [ "download-data", "--exchange", "binance", - "--pairs", "ETH/BTC", "XRP/BTC" + "--pairs", "ETH/BTC", "XRP/BTC", ] start_download_data(get_args(args)) assert dl_mock.call_count == 0 assert log_has("Skipping pair ETH/BTC...", caplog) assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) + + +def test_download_data_keyboardInterrupt(mocker, caplog, markets): + dl_mock = mocker.patch('freqtrade.utils.download_pair_history', + MagicMock(side_effect=KeyboardInterrupt)) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + ] + with pytest.raises(SystemExit): + start_download_data(get_args(args)) + + assert dl_mock.call_count == 1 From 7a79b292e46dfab4e19e4635c8720bac8cde7676 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 07:05:42 +0200 Subject: [PATCH 073/102] Fix bug in pairs fallback resolving --- freqtrade/configuration/configuration.py | 3 ++- freqtrade/tests/test_configuration.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c51153f4b..676b0c594 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -380,7 +380,7 @@ class Configuration(object): config['pairs'].sort() return - if "config" in self.args: + if "config" in self.args and self.args.config: logger.info("Using pairlist from configuration.") config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: @@ -388,3 +388,4 @@ class Configuration(object): pairs_file = Path(config['datadir']) / "pairs.json" if pairs_file.exists(): config['pairs'] = json_load(pairs_file) + config['pairs'].sort() diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c351b9b72..b6e8a76d9 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -792,3 +792,21 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): with pytest.raises(OperationalException, match=r"No pairs file found with path.*"): configuration = Configuration(args) configuration.get_config() + + +def test_pairlist_resolving_fallback(mocker): + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch("freqtrade.configuration.configuration.json_load", + MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) + arglist = [ + 'download-data', + '--exchange', 'binance' + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + + assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['name'] == 'binance' From 08fa5136e11843c7ea3da2c464f1a0e77d921f03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 07:19:46 +0200 Subject: [PATCH 074/102] use copy of minimal_config ... --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 676b0c594..75319ac47 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -54,7 +54,7 @@ class Configuration(object): config: Dict[str, Any] = {} if not files: - return constants.MINIMAL_CONFIG + return constants.MINIMAL_CONFIG.copy() # We expect here a list of config filenames for path in files: From 84a0f9ea42518d9e1dced4e027044365ec00a01c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 17 Aug 2019 11:43:36 +0300 Subject: [PATCH 075/102] get_pair_dataframe helper method added --- freqtrade/data/dataprovider.py | 46 ++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b87589df7..e806f5aa7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -44,36 +44,50 @@ class DataProvider(): def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: """ - get ohlcv data for the given pair as DataFrame - Please check `available_pairs` to verify which pairs are currently cached. + Get ohlcv data for the given pair as DataFrame + Please check `self.available_pairs` to verify which pairs are currently cached. :param pair: pair to get the data for - :param ticker_interval: ticker_interval to get pair for - :param copy: copy dataframe before returning. - Use false only for RO operations (where the dataframe is not modified) + :param ticker_interval: ticker interval to get data for + :param copy: copy dataframe before returning if True. + Use False only for read-only operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - if ticker_interval: - pairtick = (pair, ticker_interval) - else: - pairtick = (pair, self._config['ticker_interval']) + pairtick = (pair, ticker_interval or self._config['ticker_interval']) + if pairtick in self.available_pairs: + return self._exchange.klines(pairtick, copy=copy) - return self._exchange.klines(pairtick, copy=copy) - else: - return DataFrame() + return DataFrame() - def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame: + def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: """ - get stored historic ohlcv data + Get stored historic ohlcv data :param pair: pair to get the data for - :param ticker_interval: ticker_interval to get pair for + :param ticker_interval: ticker interval to get data for """ return load_pair_history(pair=pair, - ticker_interval=ticker_interval, + ticker_interval=ticker_interval or self._config['ticker_interval'], refresh_pairs=False, datadir=Path(self._config['datadir']) if self._config.get( 'datadir') else None ) + def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: + """ + Return pair ohlcv data, either live or cached historical -- depending + on the runmode. + :param pair: pair to get the data for + :param ticker_interval: ticker interval to get data for + """ + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + # Get live ohlcv data. + data = self.ohlcv(pair=pair, ticker_interval=ticker_interval) + else: + # Get historic ohlcv data (cached on disk). + data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) + if len(data) == 0: + logger.warning(f"No data found for pair {pair}") + return data + def ticker(self, pair: str): """ Return last ticker data From cda912bd8cc060b09168b68194b3a042b8f879ab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 17 Aug 2019 12:59:27 +0300 Subject: [PATCH 076/102] test added --- freqtrade/tests/data/test_dataprovider.py | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index 993f0b59b..54aab7052 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -51,6 +51,39 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): assert historymock.call_args_list[0][1]["ticker_interval"] == "5m" +def test_get_pair_dataframe(mocker, default_conf, ticker_history): + default_conf["runmode"] = RunMode.DRY_RUN + ticker_interval = default_conf["ticker_interval"] + exchange = get_patched_exchange(mocker, default_conf) + exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.DRY_RUN + assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history + assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty + assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + # Test with and without parameter + assert dp.get_pair_dataframe("UNITTEST/BTC", + ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC")) + + default_conf["runmode"] = RunMode.LIVE + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.LIVE + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + historymock = MagicMock(return_value=ticker_history) + mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) + default_conf["runmode"] = RunMode.BACKTEST + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.BACKTEST + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) + # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + + def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) From fce3d7586f320b1fc391d6a52b757f54ed3438d3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:13:39 +0000 Subject: [PATCH 077/102] Bump pytest from 5.0.1 to 5.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.0.1 to 5.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.0.1...5.1.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 03b37417e..6436c60e4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 mypy==0.720 -pytest==5.0.1 +pytest==5.1.0 pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-mock==1.10.4 From 4ce3cc66d5ea41be0bfbf4c6a1a1b64c5117efd7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:14:01 +0000 Subject: [PATCH 078/102] Bump sqlalchemy from 1.3.6 to 1.3.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.6 to 1.3.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 651be7611..91da6e45c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.18.1043 -SQLAlchemy==1.3.6 +SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 cachetools==3.1.1 From e0335705b2a95387f32d692f6417f55ccaf070b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 17:19:02 +0200 Subject: [PATCH 079/102] Add dependabot config yaml --- .dependabot/config.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .dependabot/config.yml diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 000000000..66b91e99f --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,17 @@ +version: 1 + +update_configs: + - package_manager: "python" + directory: "/" + update_schedule: "weekly" + allowed_updates: + - match: + update_type: "all" + target_branch: "develop" + + - package_manager: "docker" + directory: "/" + update_schedule: "daily" + allowed_updates: + - match: + update_type: "all" From 9143ea13adbeeee2fad130debaf1d0ebbd9e8a2c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2019 15:26:07 +0000 Subject: [PATCH 080/102] Bump ccxt from 1.18.1043 to 1.18.1063 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1043 to 1.18.1063. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1043...1.18.1063) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 91da6e45c..4666fc053 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.1043 +ccxt==1.18.1063 SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 From 351740fc80761176891fb7bbe463672c540e8384 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 17:27:14 +0200 Subject: [PATCH 081/102] Change pyup to every month (should ideally not find anything ...) --- .pyup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pyup.yml b/.pyup.yml index b1b721113..7ab3e5eca 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -14,7 +14,7 @@ pin: True # update schedule # default: empty # allowed: "every day", "every week", .. -schedule: "every week" +schedule: "every month" search: False From 0e87cc8c84c72a75d7889f02fb584f46036400b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 19:30:03 +0200 Subject: [PATCH 082/102] Remove pyup.yml --- .pyup.yml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .pyup.yml diff --git a/.pyup.yml b/.pyup.yml deleted file mode 100644 index 7ab3e5eca..000000000 --- a/.pyup.yml +++ /dev/null @@ -1,37 +0,0 @@ -# autogenerated pyup.io config file -# see https://pyup.io/docs/configuration/ for all available options - -# configure updates globally -# default: all -# allowed: all, insecure, False -update: all - -# configure dependency pinning globally -# default: True -# allowed: True, False -pin: True - -# update schedule -# default: empty -# allowed: "every day", "every week", .. -schedule: "every month" - - -search: False -# Specify requirement files by hand, default is empty -# default: empty -# allowed: list -requirements: - - requirements.txt - - requirements-dev.txt - - requirements-plot.txt - - requirements-common.txt - - -# configure the branch prefix the bot is using -# default: pyup- -branch_prefix: pyup/ - -# allow to close stale PRs -# default: True -close_prs: True From 7fa6d804ce5a7352ec92f6114279acc90118aab6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Aug 2019 19:48:55 +0200 Subject: [PATCH 083/102] Add note explaining how / when docker images are rebuild --- docs/docker.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docker.md b/docs/docker.md index 615d31796..923dec1e2 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -26,6 +26,10 @@ To update the image, simply run the above commands again and restart your runnin Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). +!!! Note Docker image update frequency + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + ### Prepare the configuration files Even though you will use docker, you'll still need some files from the github repository. From 5e440a4cdc6baa4ecadcc37593f373fe06aeb257 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 06:55:19 +0200 Subject: [PATCH 084/102] Improve docs to point to `freqtrade download-data` --- docs/backtesting.md | 2 +- docs/bot-usage.md | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index f666c5b49..543422fee 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -3,7 +3,7 @@ This page explains how to validate your strategy performance by using Backtesting. -## Getting data for backtesting / hyperopt +## Getting data for backtesting and hyperopt To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 31c5812ad..2873f5e8f 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -184,19 +184,11 @@ optional arguments: result.json) ``` -### How to use **--refresh-pairs-cached** parameter? +### Getting historic data for backtesting -The first time your run Backtesting, it will take the pairs you have -set in your config file and download data from the Exchange. - -If for any reason you want to update your data set, you use -`--refresh-pairs-cached` to force Backtesting to update the data it has. - -!!! Note - Use it only if you want to update your data set. You will not be able to come back to the previous version. - -To test your strategy with latest data, we recommend continuing using -the parameter `-l` or `--live`. +The first time your run Backtesting, you will need to download some historic data first. +This can be accomplished by using `freqtrade download-data`. +Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details ## Hyperopt commands From 8a2a8ab8b5da8392857898684cf8d983e3cd1881 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 12:47:19 +0300 Subject: [PATCH 085/102] docstring for ohlcv improved --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index e806f5aa7..b67aba045 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -45,7 +45,7 @@ class DataProvider(): def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: """ Get ohlcv data for the given pair as DataFrame - Please check `self.available_pairs` to verify which pairs are currently cached. + Please use the `available_pairs` method to verify which pairs are currently cached. :param pair: pair to get the data for :param ticker_interval: ticker interval to get data for :param copy: copy dataframe before returning if True. From 310e4387065bf1407c8226a0bec9f79dc62e98ec Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 12:55:31 +0300 Subject: [PATCH 086/102] logging message improved --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b67aba045..b904ba985 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -85,7 +85,7 @@ class DataProvider(): # Get historic ohlcv data (cached on disk). data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) if len(data) == 0: - logger.warning(f"No data found for pair {pair}") + logger.warning(f"No data found for ({pair}, {ticker_interval}).") return data def ticker(self, pair: str): From 407a3bca6222bd820d55656e73398dc4e6bef102 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 13:00:37 +0300 Subject: [PATCH 087/102] implementation of ohlcv optimized --- freqtrade/data/dataprovider.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b904ba985..5b71c21a8 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -52,11 +52,10 @@ class DataProvider(): Use False only for read-only operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - pairtick = (pair, ticker_interval or self._config['ticker_interval']) - if pairtick in self.available_pairs: - return self._exchange.klines(pairtick, copy=copy) - - return DataFrame() + return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']), + copy=copy) + else: + return DataFrame() def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: """ From d300964691977d34bc3618a8e75bb48b6ce863ae Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 18 Aug 2019 13:06:21 +0300 Subject: [PATCH 088/102] code formatting in test_dataprovider.py --- freqtrade/tests/data/test_dataprovider.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index 54aab7052..2272f69a3 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -13,6 +13,7 @@ def test_ohlcv(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) @@ -37,11 +38,9 @@ def test_ohlcv(mocker, default_conf, ticker_history): def test_historic_ohlcv(mocker, default_conf, ticker_history): - historymock = MagicMock(return_value=ticker_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) - # exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, None) data = dp.historic_ohlcv("UNITTEST/BTC", "5m") assert isinstance(data, DataFrame) @@ -57,6 +56,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) @@ -86,12 +86,11 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) - ticker_interval = default_conf["ticker_interval"] exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history - dp = DataProvider(default_conf, exchange) + dp = DataProvider(default_conf, exchange) assert len(dp.available_pairs) == 2 assert dp.available_pairs == [ ("XRP/BTC", ticker_interval), From 8e96ac876597b686e66df3e4a238f324a9be95d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:45:30 +0200 Subject: [PATCH 089/102] Split exception tests for create_order --- freqtrade/tests/exchange/test_exchange.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6b833054d..123f1fcfd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -779,7 +779,13 @@ def test_sell_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + + # Market orders don't require price, so the behaviour is slightly different + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) From ee7ba96e8524558d8d7ed068fb939d18aac0a711 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:46:10 +0200 Subject: [PATCH 090/102] Don't do calculations in exception handlers when one element can be None fixes #2011 --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5119e0fcd..67a79178f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -408,12 +408,12 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From 045ac1019e37359bf6d04f9989af50493122b434 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 15:48:20 +0200 Subject: [PATCH 091/102] Split test for buy-orders too --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67a79178f..6e281b9b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -472,7 +472,7 @@ class Exchange(object): order = self.create_order(pair, ordertype, 'sell', amount, rate, params) logger.info('stoploss limit order added for %s. ' - 'stop price: %s. limit: %s' % (pair, stop_price, rate)) + 'stop price: %s. limit: %s', pair, stop_price, rate) return order @retrier diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 123f1fcfd..397326258 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -656,7 +656,13 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype=order_type, + exchange.buy(pair='ETH/BTC', ordertype='limit', + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype='market', amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): From ddfadbb69ee2bf18191d94d053d3ef2301286030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:10:10 +0200 Subject: [PATCH 092/102] Validate configuration consistency after loading strategy --- freqtrade/configuration/__init__.py | 1 + .../{json_schema.py => config_validation.py} | 35 ++++++++++++++++- freqtrade/configuration/configuration.py | 38 +++---------------- freqtrade/freqtradebot.py | 2 + freqtrade/tests/test_configuration.py | 14 +++---- 5 files changed, 47 insertions(+), 43 deletions(-) rename freqtrade/configuration/{json_schema.py => config_validation.py} (53%) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 7b476d173..ac59421a7 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,3 +1,4 @@ from freqtrade.configuration.arguments import Arguments # noqa: F401 from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401 +from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401 diff --git a/freqtrade/configuration/json_schema.py b/freqtrade/configuration/config_validation.py similarity index 53% rename from freqtrade/configuration/json_schema.py rename to freqtrade/configuration/config_validation.py index 4c6f4a4a0..bda60f90b 100644 --- a/freqtrade/configuration/json_schema.py +++ b/freqtrade/configuration/config_validation.py @@ -4,7 +4,7 @@ from typing import Any, Dict from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match -from freqtrade import constants +from freqtrade import constants, OperationalException logger = logging.getLogger(__name__) @@ -51,3 +51,36 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: raise ValidationError( best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message ) + + +def validate_config_consistency(conf: Dict[str, Any]) -> None: + """ + Validate the configuration consistency. + Should be ran after loading both configuration and strategy, + since strategies can set certain configuration settings too. + :param conf: Config in JSON format + :return: Returns None if everything is ok, otherwise throw an OperationalException + """ + # validating trailing stoploss + _validate_trailing_stoploss(conf) + + +def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: + + # Skip if trailing stoploss is not activated + if not conf.get('trailing_stop', False): + return + + tsl_positive = float(conf.get('trailing_stop_positive', 0)) + tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) + tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) + + if tsl_only_offset: + if tsl_positive == 0.0: + raise OperationalException( + f'The config trailing_only_offset_is_reached needs ' + 'trailing_stop_positive_offset to be more than 0 in your config.') + if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: + raise OperationalException( + f'The config trailing_stop_positive_offset needs ' + 'to be greater than trailing_stop_positive_offset in your config.') diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c95246fc0..486857153 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -6,10 +6,11 @@ import warnings from argparse import Namespace from typing import Any, Callable, Dict, List, Optional -from freqtrade import OperationalException, constants +from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.create_datadir import create_datadir -from freqtrade.configuration.json_schema import validate_config_schema +from freqtrade.configuration.config_validation import (validate_config_schema, + validate_config_consistency) from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts @@ -77,8 +78,6 @@ class Configuration(object): # Load all configs config: Dict[str, Any] = Configuration.from_files(self.args.config) - self._validate_config_consistency(config) - self._process_common_options(config) self._process_optimize_options(config) @@ -87,6 +86,8 @@ class Configuration(object): self._process_runmode(config) + validate_config_consistency(config) + return config def _process_logging_options(self, config: Dict[str, Any]) -> None: @@ -285,35 +286,6 @@ class Configuration(object): config.update({'runmode': self.runmode}) - def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: - """ - Validate the configuration consistency - :param conf: Config in JSON format - :return: Returns None if everything is ok, otherwise throw an OperationalException - """ - # validating trailing stoploss - self._validate_trailing_stoploss(conf) - - def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: - - # Skip if trailing stoploss is not activated - if not conf.get('trailing_stop', False): - return - - tsl_positive = float(conf.get('trailing_stop_positive', 0)) - tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) - tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) - - if tsl_only_offset: - if tsl_positive == 0.0: - raise OperationalException( - f'The config trailing_only_offset_is_reached needs ' - 'trailing_stop_positive_offset to be more than 0 in your config.') - if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: - raise OperationalException( - f'The config trailing_stop_positive_offset needs ' - 'to be greater than trailing_stop_positive_offset in your config.') - def _args_to_config(self, config: Dict[str, Any], argname: str, logstring: str, logfun: Optional[Callable] = None, deprecated_msg: Optional[str] = None) -> None: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68b45d96f..af29604f5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,6 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge +from freqtrade.configuration import validate_config_consistency from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType @@ -50,6 +51,7 @@ class FreqtradeBot(object): self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + validate_config_consistency(config) self.rpc: RPCManager = RPCManager(self) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 8cbd02ece..3a0077ef2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -2,7 +2,6 @@ import json import logging import warnings -from argparse import Namespace from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock @@ -11,10 +10,10 @@ import pytest from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants -from freqtrade.configuration import Arguments, Configuration +from freqtrade.configuration import Arguments, Configuration, validate_config_consistency from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.create_datadir import create_datadir -from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.configuration.load_config import load_config_file from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -625,21 +624,18 @@ def test_validate_tsl(default_conf): with pytest.raises(OperationalException, match=r'The config trailing_only_offset_is_reached needs ' 'trailing_stop_positive_offset to be more than 0 in your config.'): - configuration = Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) default_conf['trailing_stop_positive_offset'] = 0.01 default_conf['trailing_stop_positive'] = 0.015 with pytest.raises(OperationalException, match=r'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.'): - configuration = Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) default_conf['trailing_stop_positive'] = 0.01 default_conf['trailing_stop_positive_offset'] = 0.015 - Configuration(Namespace()) - configuration._validate_config_consistency(default_conf) + validate_config_consistency(default_conf) def test_load_config_test_comments() -> None: From 611850bf91a343d056259f3bb6ab7bc9b683c59f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:19:24 +0200 Subject: [PATCH 093/102] Add edge/dynamic_whitelist validation --- freqtrade/configuration/config_validation.py | 16 ++++++++++++++++ freqtrade/tests/test_configuration.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index bda60f90b..92846b704 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -63,6 +63,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: """ # validating trailing stoploss _validate_trailing_stoploss(conf) + _validate_edge(conf) def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: @@ -84,3 +85,18 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: raise OperationalException( f'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') + + +def _validate_edge(conf: Dict[str, Any]) -> None: + """ + Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists. + """ + + if not conf.get('edge', {}).get('enabled'): + return + + if conf.get('pairlist', {}).get('method') == 'VolumePairList': + raise OperationalException( + "Edge and VolumePairList are incompatible, " + "Edge will override whatever pairs VolumePairlist selects." + ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 3a0077ef2..19d2a28ee 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -638,6 +638,22 @@ def test_validate_tsl(default_conf): validate_config_consistency(default_conf) +def test_validate_edge(edge_conf): + edge_conf.update({"pairlist": { + "method": "VolumePairList", + }}) + + with pytest.raises(OperationalException, + match="Edge and VolumePairList are incompatible, " + "Edge will override whatever pairs VolumePairlist selects."): + validate_config_consistency(edge_conf) + + edge_conf.update({"pairlist": { + "method": "StaticPairList", + }}) + validate_config_consistency(edge_conf) + + def test_load_config_test_comments() -> None: """ Load config with comments From b6462cd51f707f4160fa77d8bf2eb36c73a7695e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 16:22:18 +0200 Subject: [PATCH 094/102] Add explaining comment --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index af29604f5..e5ecef8bf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -51,6 +51,8 @@ class FreqtradeBot(object): self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + + # Check config consistency here since strategies can set certain options validate_config_consistency(config) self.rpc: RPCManager = RPCManager(self) From d785d7637085edcaf382a26df2d71cb8612f6a7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 18:06:36 +0200 Subject: [PATCH 095/102] make VolumePairlist less verbose no need to print the full whitelist on every iteration --- freqtrade/constants.py | 1 - freqtrade/pairlist/VolumePairList.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9b73adcfe..bd3ba21fd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,7 +5,6 @@ bot constants """ DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' -DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 9a2e2eac4..b9b7977ab 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -55,7 +55,6 @@ class VolumePairList(IPairList): # Generate dynamic whitelist self._whitelist = self._gen_pair_whitelist( self._config['stake_currency'], self._sort_key)[:self._number_pairs] - logger.info(f"Searching pairs: {self._whitelist}") @cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: @@ -92,4 +91,6 @@ class VolumePairList(IPairList): valid_tickers.remove(t) pairs = [s['symbol'] for s in valid_tickers] + logger.info(f"Searching pairs: {self._whitelist}") + return pairs From ea4db0ffb6dd0007b4df471a3e3f8f90d9e3599d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 18:06:57 +0200 Subject: [PATCH 096/102] Pass object-name to loader to fix logging --- freqtrade/resolvers/iresolver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 841c3cf43..310c54015 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -29,7 +29,8 @@ class IResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('unknown', str(module_path)) + # Pass object_name as first argument to have logging print a reasonable name. + spec = importlib.util.spec_from_file_location(object_name, str(module_path)) module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints From a4ede02cedd22ddc1147f1fcffd224b554af970d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Aug 2019 19:38:23 +0200 Subject: [PATCH 097/102] Gracefully handle problems with dry-run orders --- freqtrade/exchange/exchange.py | 9 +++++++-- freqtrade/tests/exchange/test_exchange.py | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5119e0fcd..7ae40381d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -696,8 +696,13 @@ class Exchange(object): @retrier def get_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - order = self._dry_run_open_orders[order_id] - return order + try: + order = self._dry_run_open_orders[order_id] + return order + except KeyError as e: + # Gracefully handle errors with dry-run orders. + raise InvalidOrderException( + f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6b833054d..6a7dfa04b 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1328,6 +1328,9 @@ def test_get_order(default_conf, mocker, exchange_name): print(exchange.get_order('X', 'TKN/BTC')) assert exchange.get_order('X', 'TKN/BTC').myid == 123 + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_order('Y', 'TKN/BTC') + default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) From 9ad9ce0da1b46a0822cba4f100257a177211530f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2019 10:52:53 +0000 Subject: [PATCH 098/102] Bump ccxt from 1.18.1063 to 1.18.1068 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1063 to 1.18.1068. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1063...1.18.1068) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4666fc053..3d80c3ef5 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.1063 +ccxt==1.18.1068 SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 From 70b1a05d976aad8a7c808ba7ee8a27156081b7b8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 20 Aug 2019 01:32:02 +0300 Subject: [PATCH 099/102] example in the docs changed --- docs/strategy-customization.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0d08bdd02..d71ebfded 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -274,27 +274,24 @@ Please always check the mode of operation to select the correct method to get da #### Possible options for DataProvider -- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval) -- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame -- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk +- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). +- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. +- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk. +- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `runmode` - Property containing the current runmode. -#### ohlcv / historic_ohlcv +#### Example: fetch live ohlcv / historic data for the first informative pair ``` python if self.dp: - if self.dp.runmode in ('live', 'dry_run'): - if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs: - data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC', - ticker_interval=self.ticker_interval) - else: - # Get historic ohlcv data (cached on disk). - history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC', - ticker_interval='1h') + inf_pair, inf_timeframe = self.informative_pairs()[0] + informative = self.dp.get_pair_dataframe(pair=inf_pair, + ticker_interval=inf_timeframe) ``` !!! Warning Warning about backtesting - Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go, + Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` + for the backtesting runmode) provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). !!! Warning Warning in hyperopt From 8d1a575a9b975e1753d312fbf72d8e4f640354f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 06:39:28 +0200 Subject: [PATCH 100/102] Reword documentation --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 543422fee..3712fddce 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV), we recommend using the `freqtrade download-data` command. +To download backtesting data (candles / OHLCV) and hyperoptimization using the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. From be308ff91428d8b4b53cc3f5a199289d5652279e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 09:45:28 +0200 Subject: [PATCH 101/102] Fix grammar error in documentation --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3712fddce..48f658ab9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV) and hyperoptimization using the `freqtrade download-data` command. +To download backtesting data (candles / OHLCV) and hyperoptimization use the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. From 210f66e48bbbd654f61d566eca9224121c9ef147 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Aug 2019 19:34:18 +0200 Subject: [PATCH 102/102] Improve wording --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 48f658ab9..90256283e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,7 +5,7 @@ Backtesting. ## Getting data for backtesting and hyperopt -To download backtesting data (candles / OHLCV) and hyperoptimization use the `freqtrade download-data` command. +To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command. If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes. Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory.