From 7166a474ae9b58259a25c356d8e16f01213b63d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 19:54:16 +0100 Subject: [PATCH 001/928] Add min_rate - always update min/max rates --- freqtrade/persistence.py | 24 ++++++++++++++---------- freqtrade/tests/test_persistence.py | 1 + 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 10aff72ec..f6cdc815f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -83,7 +83,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stoploss_last_update'): + if not has_column(cols, 'min_rate'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -95,6 +95,7 @@ def check_migrate(engine) -> None: stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', '0.0') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') @@ -113,7 +114,7 @@ def check_migrate(engine) -> None: open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update, - max_rate, sell_reason, strategy, + max_rate, min_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -130,7 +131,7 @@ def check_migrate(engine) -> None: stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {sell_reason} sell_reason, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -191,6 +192,8 @@ class Trade(_DECL_BASE): stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) @@ -201,6 +204,14 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + def adjust_high_low(self, current_price): + """ + Adjust the max_rate and min_rate. + """ + logger.info("Adjusting high/low") + self.max_rate = max(current_price, self.max_rate) + self.min_rate = min(current_price, self.min_rate) + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): """this adjusts the stop loss to it's most recently observed setting""" @@ -210,13 +221,6 @@ class Trade(_DECL_BASE): new_loss = float(current_price * (1 - abs(stoploss))) - # keeping track of the highest observed rate for this trade - if self.max_rate is None: - self.max_rate = current_price - else: - if current_price > self.max_rate: - self.max_rate = current_price - # no stop loss assigned yet if not self.stop_loss: logger.debug("assigning new stop loss") diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index a9519e693..7e809de0d 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -510,6 +510,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" assert trade.max_rate == 0.0 + assert trade.min_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 assert trade.sell_reason is None From 738ed932217daa6c8fc13114e710cb8ea0211506 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 19:54:34 +0100 Subject: [PATCH 002/928] call new function --- freqtrade/strategy/interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 41dcb8c57..d65c12895 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -254,6 +254,8 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) + trade.adjust_high_low(current_rate) + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss) From 68a9b14eca297386b8494f44c31439ef27994099 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 20:04:39 +0100 Subject: [PATCH 003/928] Min-rate should not default to 0 --- freqtrade/persistence.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index f6cdc815f..ad85d5efd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -95,7 +95,7 @@ def check_migrate(engine) -> None: stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') - min_rate = get_column_def(cols, 'min_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') @@ -193,7 +193,7 @@ class Trade(_DECL_BASE): # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached - min_rate = Column(Float, nullable=True, default=0.0) + min_rate = Column(Float, nullable=True) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) @@ -204,13 +204,13 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') - def adjust_high_low(self, current_price): + def adjust_high_low(self, current_price: float): """ Adjust the max_rate and min_rate. """ - logger.info("Adjusting high/low") - self.max_rate = max(current_price, self.max_rate) - self.min_rate = min(current_price, self.min_rate) + logger.debug("Adjusting min/max rates") + self.max_rate = max(current_price, self.max_rate or 0.0) + self.min_rate = min(current_price, self.min_rate or 10000000.0) def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): """this adjusts the stop loss to it's most recently observed setting""" From 01733c94fa01d628fcf357fe09ca5d77d716badc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 20:04:55 +0100 Subject: [PATCH 004/928] Split up tests for adjust_stoploss and adjust_highlow --- freqtrade/tests/test_persistence.py | 87 +++++++++++++++++++---------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 7e809de0d..c4d07b3f9 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -510,7 +510,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" assert trade.max_rate == 0.0 - assert trade.min_rate == 0.0 + assert trade.min_rate is None assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 assert trade.sell_reason is None @@ -586,7 +586,48 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): caplog.record_tuples) -def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): +def test_adjust_stop_loss(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + max_rate=1, + ) + + trade.adjust_stop_loss(trade.open_rate, 0.05, True) + assert trade.stop_loss == 0.95 + assert trade.initial_stop_loss == 0.95 + + # Get percent of profit with a lower rate + trade.adjust_stop_loss(0.96, 0.05) + assert trade.stop_loss == 0.95 + assert trade.initial_stop_loss == 0.95 + + # Get percent of profit with a custom rate (Higher than open rate) + trade.adjust_stop_loss(1.3, -0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.initial_stop_loss == 0.95 + + # current rate lower again ... should not change + trade.adjust_stop_loss(1.2, 0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.initial_stop_loss == 0.95 + + # current rate higher... should raise stoploss + trade.adjust_stop_loss(1.4, 0.1) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.initial_stop_loss == 0.95 + + # Initial is true but stop_loss set - so doesn't do anything + trade.adjust_stop_loss(1.7, 0.1, True) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.initial_stop_loss == 0.95 + + +def test_adjust_high_low(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -596,40 +637,24 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): open_rate=1, ) - trade.adjust_stop_loss(trade.open_rate, 0.05, True) - assert trade.stop_loss == 0.95 + trade.adjust_high_low(trade.open_rate) assert trade.max_rate == 1 - assert trade.initial_stop_loss == 0.95 + assert trade.min_rate == 1 - # Get percent of profit with a lowre rate - trade.adjust_stop_loss(0.96, 0.05) - assert trade.stop_loss == 0.95 + # check min adjusted, max remained + trade.adjust_high_low(0.96) assert trade.max_rate == 1 - assert trade.initial_stop_loss == 0.95 + assert trade.min_rate == 0.96 - # Get percent of profit with a custom rate (Higher than open rate) - trade.adjust_stop_loss(1.3, -0.1) - assert round(trade.stop_loss, 8) == 1.17 - assert trade.max_rate == 1.3 - assert trade.initial_stop_loss == 0.95 + # check max adjusted, min remains + trade.adjust_high_low(1.05) + assert trade.max_rate == 1.05 + assert trade.min_rate == 0.96 - # current rate lower again ... should not change - trade.adjust_stop_loss(1.2, 0.1) - assert round(trade.stop_loss, 8) == 1.17 - assert trade.max_rate == 1.3 - assert trade.initial_stop_loss == 0.95 - - # current rate higher... should raise stoploss - trade.adjust_stop_loss(1.4, 0.1) - assert round(trade.stop_loss, 8) == 1.26 - assert trade.max_rate == 1.4 - assert trade.initial_stop_loss == 0.95 - - # Initial is true but stop_loss set - so doesn't do anything - trade.adjust_stop_loss(1.7, 0.1, True) - assert round(trade.stop_loss, 8) == 1.26 - assert trade.max_rate == 1.4 - assert trade.initial_stop_loss == 0.95 + # current rate "in the middle" - no adjustment + trade.adjust_high_low(1.03) + assert trade.max_rate == 1.05 + assert trade.min_rate == 0.96 def test_get_open(default_conf, fee): From fc360608b7f6dfd7d8f1206d95ed48b6d040f5b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 20:06:15 +0100 Subject: [PATCH 005/928] Rename function to adjust_min_max --- freqtrade/persistence.py | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/tests/test_persistence.py | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ad85d5efd..ebafe7355 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -204,7 +204,7 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') - def adjust_high_low(self, current_price: float): + def adjust_min_max_rates(self, current_price: float): """ Adjust the max_rate and min_rate. """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d65c12895..85d4d8c13 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -254,7 +254,7 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - trade.adjust_high_low(current_rate) + trade.adjust_min_max_rates(current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index c4d07b3f9..042237ce7 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -627,7 +627,7 @@ def test_adjust_stop_loss(fee): assert trade.initial_stop_loss == 0.95 -def test_adjust_high_low(fee): +def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -637,22 +637,22 @@ def test_adjust_high_low(fee): open_rate=1, ) - trade.adjust_high_low(trade.open_rate) + trade.adjust_min_max_rates(trade.open_rate) assert trade.max_rate == 1 assert trade.min_rate == 1 # check min adjusted, max remained - trade.adjust_high_low(0.96) + trade.adjust_min_max_rates(0.96) assert trade.max_rate == 1 assert trade.min_rate == 0.96 # check max adjusted, min remains - trade.adjust_high_low(1.05) + trade.adjust_min_max_rates(1.05) assert trade.max_rate == 1.05 assert trade.min_rate == 0.96 # current rate "in the middle" - no adjustment - trade.adjust_high_low(1.03) + trade.adjust_min_max_rates(1.03) assert trade.max_rate == 1.05 assert trade.min_rate == 0.96 From 2d4a2fd10bd4517dd653a4a2754f056440009b22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 13:12:04 +0100 Subject: [PATCH 006/928] Use oppen_rate instead of artificial defaults --- freqtrade/persistence.py | 4 ++-- freqtrade/strategy/interface.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ebafe7355..b807e10e1 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -209,8 +209,8 @@ class Trade(_DECL_BASE): Adjust the max_rate and min_rate. """ logger.debug("Adjusting min/max rates") - self.max_rate = max(current_price, self.max_rate or 0.0) - self.min_rate = min(current_price, self.min_rate or 10000000.0) + self.max_rate = max(current_price, self.max_rate or self.open_rate) + self.min_rate = min(current_price, self.min_rate or self.open_rate) def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): """this adjusts the stop loss to it's most recently observed setting""" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 85d4d8c13..b844c1e58 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -301,8 +301,9 @@ class IStrategy(ABC): # evaluate if the stoploss was hit if stoploss is not on exchange if ((self.stoploss is not None) and - (trade.stop_loss >= current_rate) and - (not self.order_types.get('stoploss_on_exchange'))): + (trade.stop_loss >= current_rate) and + (not self.order_types.get('stoploss_on_exchange'))): + selltype = SellType.STOP_LOSS # If Trailing stop (and max-rate did move above open rate) if trailing_stop and trade.open_rate != trade.max_rate: From 7b99daebd766d986287143ddca0d5b62cac49968 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 13:18:29 +0100 Subject: [PATCH 007/928] Update docstring for adjust_stoploss --- freqtrade/persistence.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index b807e10e1..a1ac65c04 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -213,7 +213,13 @@ class Trade(_DECL_BASE): self.min_rate = min(current_price, self.min_rate or self.open_rate) def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): - """this adjusts the stop loss to it's most recently observed setting""" + """ + This adjusts the stop loss to it's most recently observed setting + :param current_price: Current rate the asset is traded + :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price). + :param initial: Called to initiate stop_loss. + Skips everything if self.stop_loss is already set. + """ if initial and not (self.stop_loss is None or self.stop_loss == 0): # Don't modify if called with initial and nothing to do From a77d51351342913611fb7cc7b703d098c0e15683 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 13:27:32 +0100 Subject: [PATCH 008/928] Fix backteest detail numbering ... --- .../tests/optimize/test_backtest_detail.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index e8514e76f..d6295b778 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,10 +14,10 @@ from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataf from freqtrade.tests.conftest import patch_exchange -# Test 0 Minus 8% Close +# Test 1 Minus 8% Close # Test with Stop-loss at 1% # TC1: Stop-Loss Triggered 1% loss -tc0 = BTContainer(data=[ +tc1 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -30,10 +30,10 @@ tc0 = BTContainer(data=[ ) -# Test 1 Minus 4% Low, minus 1% close +# Test 2 Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% # TC2: Stop-Loss Triggered 3% Loss -tc1 = BTContainer(data=[ +tc2 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -53,7 +53,7 @@ tc1 = BTContainer(data=[ # Test with Stop-Loss at 2% # TC3: Trade-A: Stop-Loss Triggered 2% Loss # Trade-B: Stop-Loss Triggered 2% Loss -tc2 = BTContainer(data=[ +tc3 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -71,7 +71,7 @@ tc2 = BTContainer(data=[ # Candle Data for test 3 – Candle drops 3% Closed 15% up # Test with Stop-loss at 2% ROI 6% # TC4: Stop-Loss Triggered 2% Loss -tc3 = BTContainer(data=[ +tc4 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -83,10 +83,10 @@ tc3 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 4 / Drops 0.5% Closes +20% +# Test 5 / Drops 0.5% Closes +20% # Set stop-loss at 1% ROI 3% # TC5: ROI triggers 3% Gain -tc4 = BTContainer(data=[ +tc5 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4980, 4987, 6172, 1, 0], [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -102,7 +102,7 @@ tc4 = BTContainer(data=[ # Candle Data for test 6 # Set stop-loss at 2% ROI at 5% # TC6: Stop-Loss triggers 2% Loss -tc5 = BTContainer(data=[ +tc6 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -118,7 +118,7 @@ tc5 = BTContainer(data=[ # Candle Data for test 7 # Set stop-loss at 2% ROI at 3% # TC7: ROI Triggers 3% Gain -tc6 = BTContainer(data=[ +tc7 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], @@ -131,13 +131,13 @@ tc6 = BTContainer(data=[ ) TESTS = [ - tc0, tc1, tc2, tc3, tc4, tc5, tc6, + tc7, ] From a830bee9c7a84f63f1b99346db27f2564d649f55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 15:28:04 +0100 Subject: [PATCH 009/928] Enable trailing_stop for BTContainer tests --- freqtrade/tests/optimize/__init__.py | 1 + freqtrade/tests/optimize/test_backtest_detail.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 129a09f40..075938a61 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -28,6 +28,7 @@ class BTContainer(NamedTuple): roi: float trades: List[BTrade] profit_perc: float + trailing_stop: bool = False def _get_frame_time_from_offset(offset): diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index d6295b778..b4ca4ee26 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -148,8 +148,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = {"0": data.roi} - default_conf['ticker_interval'] = tests_ticker_interval - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0)) + default_conf["ticker_interval"] = tests_ticker_interval + default_conf["trailing_stop"] = data.trailing_stop + mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) @@ -157,7 +158,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting.advise_sell = lambda a, m: frame caplog.set_level(logging.DEBUG) - pair = 'UNITTEST/BTC' + pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} min_date, max_date = get_timeframe({pair: frame}) From f0e5113a7ffe919f632ec66817f7eaaf9096c48c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 15:39:05 +0100 Subject: [PATCH 010/928] Use Magicmock instead of lambda for mocking --- freqtrade/tests/test_freqtradebot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index fc7c48663..a5de283a5 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2259,9 +2259,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = \ - lambda current_rate, trade, current_time, force_stoploss, current_profit: SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE) + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_flag=False, sell_type=SellType.NONE)) freqtrade.create_trade() trade = Trade.query.first() From 8c7e8255bb3f68c91925b68ea1229785155feb5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 16:01:34 +0100 Subject: [PATCH 011/928] Add detailed test for trailing stop --- .../tests/optimize/test_backtest_detail.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index b4ca4ee26..f33f56efc 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -130,6 +130,22 @@ tc7 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) + +# Test 8 - trailing_stop should raise so candle 3 causes a stoploss. +# Candle Data for test 8 +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC8: Trailing stoploss - stoploss should be adjusted to 94.5 after candle 1 +tc8 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5250, 4750, 4850, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.055, trailing_stop=True, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ tc1, tc2, @@ -138,6 +154,7 @@ TESTS = [ tc5, tc6, tc7, + tc8, ] From 05ab1c2e0a833bfcbaac15447bb2c5bdecedf110 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 16:02:13 +0100 Subject: [PATCH 012/928] Fix some comments --- freqtrade/strategy/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b844c1e58..6e2f35cc8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -247,6 +247,9 @@ class IStrategy(ABC): """ 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. + :param low: Only used during backtesting to simulate stoploss + :param high: Only used during backtesting, to simulate ROI + :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ @@ -263,7 +266,7 @@ class IStrategy(ABC): if stoplossflag.sell_flag: return stoplossflag - # Set current rate to low for backtesting sell + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_percent(current_rate) experimental = self.config.get('experimental', {}) From a7b60f6780e2230909400fd8437311c07a018ae2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 16:03:44 +0100 Subject: [PATCH 013/928] update trailing_stop with high in case of backtesting --- freqtrade/strategy/interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6e2f35cc8..a086139c7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -257,11 +257,11 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - trade.adjust_min_max_rates(current_rate) + trade.adjust_min_max_rates(high or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + force_stoploss=force_stoploss, high=high) if stoplossflag.sell_flag: return stoplossflag @@ -291,7 +291,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, - current_profit: float, force_stoploss: float) -> SellCheckTuple: + current_profit: float, force_stoploss: float, high) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not @@ -322,6 +322,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=True, sell_type=selltype) # update the stop loss afterwards, after all by definition it's supposed to be hanging + # TODO: Maybe this needs to be moved to the start of this function. check #1575 for details if trailing_stop: # check if we have a special stop loss for positive condition @@ -342,7 +343,7 @@ class IStrategy(ABC): # we update trailing stoploss only if offset is reached. tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) if not (tsl_only_offset and current_profit < sl_offset): - trade.adjust_stop_loss(current_rate, stop_loss_value) + trade.adjust_stop_loss(high or current_rate, stop_loss_value) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) From 39232cbcbbe440368f2389c7d03271542b6cca4f Mon Sep 17 00:00:00 2001 From: iuvbio Date: Tue, 5 Mar 2019 21:23:55 +0100 Subject: [PATCH 014/928] loop over whitelist only instead of all markets --- freqtrade/pairlist/IPairList.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5559c582f..f9a66fe89 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -65,29 +65,26 @@ class IPairList(ABC): :return: the list of pairs the user wants to trade without the one unavailable or black_listed """ - sanitized_whitelist = whitelist markets = self._freqtrade.exchange.markets # Filter to markets in stake currency - markets = [markets[pair] for pair in markets if - markets[pair]['quote'] == self._config['stake_currency']] - known_pairs = set() + stake_pairs = [pair for pair in markets if + pair.endswith(self._config['stake_currency'])] - # TODO: we should loop over whitelist instead of all markets - for market in markets: - pair = market['symbol'] + sanitized_whitelist = [] + for pair in whitelist: # pair is not in the generated dynamic market, or in the blacklist ... ignore it - if pair not in whitelist or pair in self.blacklist: + if pair in self.blacklist or pair not in stake_pairs: continue - # else the pair is valid - known_pairs.add(pair) - # Market is not active + # Check if market is active + market = markets[pair] if not market['active']: - sanitized_whitelist.remove(pair) logger.info( 'Ignoring %s from whitelist. Market is not active.', pair ) + continue + sanitized_whitelist.append(pair) # We need to remove pairs that are unknown - return [x for x in sanitized_whitelist if x in known_pairs] + return sanitized_whitelist From a241e950f292a7da931ae9a04a87896acd6dfc94 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Wed, 6 Mar 2019 22:41:46 +0100 Subject: [PATCH 015/928] prune validate_pairs --- freqtrade/exchange/exchange.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 33f62f2f7..e4838b74c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -239,13 +239,9 @@ class Exchange(object): logger.warning('Unable to validate pairs (assuming they are correct).') # return - stake_cur = self._conf['stake_currency'] for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # TODO: add a support for having coins in BTC/USDT format - if not pair.endswith(stake_cur): - raise OperationalException( - f'Pair {pair} not compatible with stake_currency: {stake_cur}') if self.markets and pair not in self.markets: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' From c907e80c10d5bc8c85e79b356e45333aebe79be4 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Wed, 6 Mar 2019 22:42:05 +0100 Subject: [PATCH 016/928] make sure no dups --- freqtrade/pairlist/IPairList.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index f9a66fe89..8e3b11ff4 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,10 +68,10 @@ class IPairList(ABC): markets = self._freqtrade.exchange.markets # Filter to markets in stake currency - stake_pairs = [pair for pair in markets if - pair.endswith(self._config['stake_currency'])] + stake_pairs = [pair for pair in markets if # pair.endswith(self._config['stake_currency']) + markets[pair]["quote"] == self._config['stake_currency']] - sanitized_whitelist = [] + sanitized_whitelist = set() for pair in whitelist: # pair is not in the generated dynamic market, or in the blacklist ... ignore it if pair in self.blacklist or pair not in stake_pairs: @@ -84,7 +84,7 @@ class IPairList(ABC): pair ) continue - sanitized_whitelist.append(pair) + sanitized_whitelist.add(pair) # We need to remove pairs that are unknown - return sanitized_whitelist + return list(sanitized_whitelist) From e38a3051a14a8faca1ef3aac0dcb368c0742b45c Mon Sep 17 00:00:00 2001 From: iuvbio Date: Mon, 11 Mar 2019 21:10:22 +0100 Subject: [PATCH 017/928] update docstring --- freqtrade/pairlist/IPairList.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 8e3b11ff4..841699251 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -60,9 +60,8 @@ class IPairList(ABC): def _validate_whitelist(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary - :param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to - trade - :return: the list of pairs the user wants to trade without the one unavailable or + :param whitelist: the sorted list of pairs the user might want to trade + :return: the list of pairs the user wants to trade without those unavailable or black_listed """ markets = self._freqtrade.exchange.markets From d4543be8eb2f2d227291857156aba69ea318b5a4 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Mon, 11 Mar 2019 21:48:55 +0100 Subject: [PATCH 018/928] edit comment --- freqtrade/pairlist/IPairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 841699251..7960e82fc 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -66,7 +66,7 @@ class IPairList(ABC): """ markets = self._freqtrade.exchange.markets - # Filter to markets in stake currency + # keep only pairs with stake currency as quote stake_pairs = [pair for pair in markets if # pair.endswith(self._config['stake_currency']) markets[pair]["quote"] == self._config['stake_currency']] From d4d37667e1d1b912804008a5d248ba8c20b6b10a Mon Sep 17 00:00:00 2001 From: iuvbio Date: Tue, 12 Mar 2019 18:26:47 +0100 Subject: [PATCH 019/928] use pairname for stake cur comparison --- freqtrade/pairlist/IPairList.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 7960e82fc..fcc129814 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -67,8 +67,8 @@ class IPairList(ABC): markets = self._freqtrade.exchange.markets # keep only pairs with stake currency as quote - stake_pairs = [pair for pair in markets if # pair.endswith(self._config['stake_currency']) - markets[pair]["quote"] == self._config['stake_currency']] + stake_pairs = [pair for pair in markets if pair.endswith(self._config['stake_currency'])] + # markets[pair]["quote"] == self._config['stake_currency'] sanitized_whitelist = set() for pair in whitelist: From 7f9c76a6fcfb31591c0568adadcb86da09a49987 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 14 Mar 2019 21:02:21 +0100 Subject: [PATCH 020/928] move stake check to the same condition as the other checks --- freqtrade/pairlist/IPairList.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index fcc129814..2564c484c 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -66,14 +66,11 @@ class IPairList(ABC): """ markets = self._freqtrade.exchange.markets - # keep only pairs with stake currency as quote - stake_pairs = [pair for pair in markets if pair.endswith(self._config['stake_currency'])] - # markets[pair]["quote"] == self._config['stake_currency'] - sanitized_whitelist = set() for pair in whitelist: # pair is not in the generated dynamic market, or in the blacklist ... ignore it - if pair in self.blacklist or pair not in stake_pairs: + if (pair in self.blacklist or pair not in markets + or not pair.endswith(self._config['stake_currency'])): continue # Check if market is active market = markets[pair] From 8386496456b7d51943b7c4a5bfc710aa1312018a Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 14 Mar 2019 21:53:42 +0100 Subject: [PATCH 021/928] remove tests that are no longer applicable --- freqtrade/tests/exchange/test_exchange.py | 29 ----------------------- 1 file changed, 29 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 7c757df09..736f2298a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -305,19 +305,6 @@ def test_validate_pairs_not_available(default_conf, mocker): Exchange(default_conf) -def test_validate_pairs_not_compatible(default_conf, mocker): - api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' - }) - default_conf['stake_currency'] = 'ETH' - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - with pytest.raises(OperationalException, match=r'not compatible'): - Exchange(default_conf) - - def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -337,22 +324,6 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.record_tuples) -def test_validate_pairs_stake_exception(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - default_conf['stake_currency'] = 'ETH' - api_mock = MagicMock() - api_mock.name = MagicMock(return_value='binance') - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - - with pytest.raises( - OperationalException, - match=r'Pair ETH/BTC not compatible with stake_currency: ETH' - ): - Exchange(default_conf) - - def test_validate_timeframes(default_conf, mocker): default_conf["ticker_interval"] = "5m" api_mock = MagicMock() From 8afce7e65105c07364991b4ab48ee718e10530eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 16:26:38 +0100 Subject: [PATCH 022/928] Add testcase for Testcase 2 --- .../tests/optimize/test_backtest_detail.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index f33f56efc..b98369533 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -49,7 +49,6 @@ tc2 = BTContainer(data=[ # Test 3 Candle drops 4%, Recovers 1%. # Entry Criteria Met # Candle drops 20% -# Candle Data for test 3 # Test with Stop-Loss at 2% # TC3: Trade-A: Stop-Loss Triggered 2% Loss # Trade-B: Stop-Loss Triggered 2% Loss @@ -99,7 +98,6 @@ tc5 = BTContainer(data=[ ) # Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve -# Candle Data for test 6 # Set stop-loss at 2% ROI at 5% # TC6: Stop-Loss triggers 2% Loss tc6 = BTContainer(data=[ @@ -115,7 +113,6 @@ tc6 = BTContainer(data=[ ) # Test 7 - 6% Positive / 1% Negative / Close 1% Positve -# Candle Data for test 7 # Set stop-loss at 2% ROI at 3% # TC7: ROI Triggers 3% Gain tc7 = BTContainer(data=[ @@ -132,11 +129,10 @@ tc7 = BTContainer(data=[ # Test 8 - trailing_stop should raise so candle 3 causes a stoploss. -# Candle Data for test 8 # Set stop-loss at 10%, ROI at 10% (should not apply) -# TC8: Trailing stoploss - stoploss should be adjusted to 94.5 after candle 1 +# TC8: Trailing stoploss - stoploss should be adjusted candle 2 tc8 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5000, 6172, 0, 0], [2, 5000, 5250, 4750, 4850, 6172, 0, 0], @@ -146,6 +142,22 @@ tc8 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) + +# Test 9 - trailing_stop should raise - high and low in same candle. +# Candle Data for test 9 +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC9: Trailing stoploss - stoploss should be adjusted candle 2 +tc9 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5050, 4950, 5000, 6172, 0, 0], + [3, 5000, 5200, 4550, 4850, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.064, trailing_stop=True, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ tc1, tc2, @@ -155,6 +167,7 @@ TESTS = [ tc6, tc7, tc8, + tc9, ] From 4de4a70be7f1b63a9eaf180c5b26e1066f83f7e6 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Sun, 17 Mar 2019 18:18:35 +0100 Subject: [PATCH 023/928] update log messages --- freqtrade/pairlist/IPairList.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 2564c484c..d08bd2587 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -71,13 +71,15 @@ class IPairList(ABC): # pair is not in the generated dynamic market, or in the blacklist ... ignore it if (pair in self.blacklist or pair not in markets or not pair.endswith(self._config['stake_currency'])): + logger.warning(f"Pair {pair} is not compatible with exchange " + f"{self._freqtrade.exchange.name} or contained in " + f"your blacklist. Removing it from whitelist..") continue # Check if market is active market = markets[pair] if not market['active']: logger.info( - 'Ignoring %s from whitelist. Market is not active.', - pair + f"Ignoring {pair} from whitelist. Market is not active." ) continue sanitized_whitelist.add(pair) From c2076af43b00ea040cd3b7db9cdf0b013879139b Mon Sep 17 00:00:00 2001 From: iuvbio Date: Sun, 17 Mar 2019 18:18:44 +0100 Subject: [PATCH 024/928] update tests --- freqtrade/tests/pairlist/test_pairlist.py | 74 +++++++++++------------ 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py index 52f44c41b..5fe2dfc08 100644 --- a/freqtrade/tests/pairlist/test_pairlist.py +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock from freqtrade import OperationalException from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.resolvers import PairListResolver -from freqtrade.tests.conftest import get_patched_freqtradebot +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has import pytest # whitelist, blacklist @@ -107,7 +107,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): assert set(whitelist) == set(pairslist) -def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) -> None: +@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ + (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'BTT/BTC']), + (False, "BTC", "bidVolume", ['BTT/BTC', 'TKN/BTC', 'ETH/BTC']), + (False, "USDT", "quoteVolume", ['ETH/USDT', 'LTC/USDT']), + (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange + (True, "BTC", "quoteVolume", ["ETH/BTC", "TKN/BTC"]), + (True, "BTC", "bidVolume", ["TKN/BTC", "ETH/BTC"]) +]) +def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key, + whitelist_result, precision_filter) -> None: whitelist_conf['pairlist']['method'] = 'VolumePairList' mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) @@ -115,32 +124,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8)) - # Test to retrieved BTC sorted on quoteVolume (default) - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume') - assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BTT/BTC'] - - # Test to retrieve BTC sorted on bidVolume - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume') - assert whitelist == ['BTT/BTC', 'TKN/BTC', 'ETH/BTC'] - - # Test with USDT sorted on quoteVolume (default) - freqtrade.config['stake_currency'] = 'USDT' # this has to be set, otherwise markets are removed - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='USDT', key='quoteVolume') - assert whitelist == ['ETH/USDT', 'LTC/USDT'] - - # Test with ETH (our fixture does not have ETH, so result should be empty) - freqtrade.config['stake_currency'] = 'ETH' - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='ETH', key='quoteVolume') - assert whitelist == [] - - freqtrade.pairlists._precision_filter = True - freqtrade.config['stake_currency'] = 'BTC' - # Retest First 2 test-cases to make sure BTT is not in it (too low priced) - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume') - assert whitelist == ['ETH/BTC', 'TKN/BTC'] - - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume') - assert whitelist == ['TKN/BTC', 'ETH/BTC'] + freqtrade.pairlists._precision_filter = precision_filter + freqtrade.config['stake_currency'] = base_currency + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) + assert whitelist == whitelist_result def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: @@ -166,17 +153,24 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): assert isinstance(freqtrade.pairlists.whitelist, list) assert isinstance(freqtrade.pairlists.blacklist, list) - whitelist = ['ETH/BTC', 'TKN/BTC'] + +@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) +@pytest.mark.parametrize("whitelist,log_message", [ + (['ETH/BTC', 'TKN/BTC'], ""), + (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake + (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available + (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist + (['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], "Market is not active") # LTC/BTC is inactive +]) +def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, + log_message): + whitelist_conf['pairlist']['method'] = pairlist + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + caplog.clear() + new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - assert set(whitelist) == set(new_whitelist) - - whitelist = ['ETH/BTC', 'TKN/BTC', 'TRX/ETH'] - new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - # TRX/ETH was removed - assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) - - whitelist = ['ETH/BTC', 'TKN/BTC', 'BLK/BTC'] - new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - # BLK/BTC is in blacklist ... - assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) + assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) + assert log_message in caplog.text From 937399606e98e0110d27e0e0e804128622adbde3 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Sun, 17 Mar 2019 18:24:29 +0100 Subject: [PATCH 025/928] fix flake8 --- freqtrade/tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py index 5fe2dfc08..38a8d78c7 100644 --- a/freqtrade/tests/pairlist/test_pairlist.py +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock from freqtrade import OperationalException from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.resolvers import PairListResolver -from freqtrade.tests.conftest import get_patched_freqtradebot, log_has +from freqtrade.tests.conftest import get_patched_freqtradebot import pytest # whitelist, blacklist From 0eff324ce04d6b19e41908edf6b00065b1fcd20b Mon Sep 17 00:00:00 2001 From: Gianluca Puglia Date: Wed, 20 Mar 2019 18:38:10 +0100 Subject: [PATCH 026/928] Use dedicated index for every pair --- freqtrade/optimize/backtesting.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 031b490c8..8d0768d43 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional -from pandas import DataFrame +from pandas import DataFrame, Timestamp from tabulate import tabulate from freqtrade import optimize @@ -325,19 +325,29 @@ class Backtesting(object): pairs.append(pair) lock_pair_until: Dict = {} + indexes: Dict = {} tmp = start_date + timedelta(minutes=self.ticker_interval_mins) - index = 0 + # Loop timerange and test per pair while tmp < end_date: # print(f"time: {tmp}") + for i, pair in enumerate(ticker): + if pair not in indexes: + indexes[pair] = 0 + try: - row = ticker[pair][index] + row = ticker[pair][indexes[pair]] except IndexError: # missing Data for one pair ... # Warnings for this are shown by `validate_backtest_data` continue + if row.date > Timestamp(tmp.datetime): + continue + + indexes[pair] += 1 + if row.buy == 0 or row.sell == 1: continue # skip rows where no buy signal or that would immediately sell off @@ -351,7 +361,7 @@ class Backtesting(object): trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:], + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], trade_count_lock, args) if trade_entry: @@ -359,11 +369,9 @@ class Backtesting(object): trades.append(trade_entry) else: # Set lock_pair_until to end of testing period if trade could not be closed - # This happens only if the buy-signal was with the last candle - lock_pair_until[pair] = end_date + lock_pair_until[pair] = Timestamp(end_date.datetime) tmp += timedelta(minutes=self.ticker_interval_mins) - index += 1 return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: From 6b89e86a97de6fc81cd0212e82e3267a339fc9d0 Mon Sep 17 00:00:00 2001 From: Gianluca Puglia Date: Wed, 20 Mar 2019 19:44:59 +0100 Subject: [PATCH 027/928] Removed Timestamp cast --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8d0768d43..f54560a0e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional -from pandas import DataFrame, Timestamp +from pandas import DataFrame from tabulate import tabulate from freqtrade import optimize @@ -343,7 +343,7 @@ class Backtesting(object): # Warnings for this are shown by `validate_backtest_data` continue - if row.date > Timestamp(tmp.datetime): + if row.date > tmp.datetime: continue indexes[pair] += 1 @@ -369,7 +369,7 @@ class Backtesting(object): trades.append(trade_entry) else: # Set lock_pair_until to end of testing period if trade could not be closed - lock_pair_until[pair] = Timestamp(end_date.datetime) + lock_pair_until[pair] = end_date.datetime tmp += timedelta(minutes=self.ticker_interval_mins) return DataFrame.from_records(trades, columns=BacktestResult._fields) From 00821036bb21c9d80265d5a5ba0e0a310f9b666b Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 20 Mar 2019 23:57:49 +0300 Subject: [PATCH 028/928] docs for dry_run_wallet --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index e7ad9c9bf..4b8d990fe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,6 +20,7 @@ Mandatory Parameters are marked as **Required**. | `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. | `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. +| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). | `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). From 7fdb099097c0afb373ee4233036403c095249d54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Mar 2019 06:14:43 +0100 Subject: [PATCH 029/928] Reformat log statement --- freqtrade/pairlist/IPairList.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index d08bd2587..a112c63b4 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -78,9 +78,7 @@ class IPairList(ABC): # Check if market is active market = markets[pair] if not market['active']: - logger.info( - f"Ignoring {pair} from whitelist. Market is not active." - ) + logger.info(f"Ignoring {pair} from whitelist. Market is not active.") continue sanitized_whitelist.add(pair) From 89145a7711da5914c473146e4c5023e509261802 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 22 Mar 2019 13:35:06 +0100 Subject: [PATCH 030/928] Update ccxt from 1.18.385 to 1.18.386 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3a25bb8bc..53e81aad3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.385 +ccxt==1.18.386 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From 60afba559274a13e33408c9f7fb0db24763b9594 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 22 Mar 2019 20:16:54 +0300 Subject: [PATCH 031/928] move worker stuff to main.py --- freqtrade/freqtradebot.py | 113 +++-------------------- freqtrade/main.py | 190 +++++++++++++++++++++++++++++++------- 2 files changed, 169 insertions(+), 134 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6f1fb2c99..ef00ba21d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,11 +7,10 @@ import logging import time import traceback from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import arrow from requests.exceptions import RequestException -import sdnotify from freqtrade import (DependencyException, OperationalException, TemporaryError, __version__, constants, persistence) @@ -24,6 +23,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListReso from freqtrade.state import State from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.wallets import Wallets +from freqtrade.main import Worker logger = logging.getLogger(__name__) @@ -35,26 +35,18 @@ class FreqtradeBot(object): This is from here the bot start its logic. """ - def __init__(self, config: Dict[str, Any]) -> None: + def __init__(self, config: Dict[str, Any], worker: Worker) -> None: """ Init all variables and objects the bot needs to work :param config: configuration dict, you can use Configuration.get_config() to get the config dict. """ - logger.info( - 'Starting freqtrade %s', - __version__, - ) - - # Init bot states - self.state = State.STOPPED + logger.info('Starting freqtrade %s', __version__) # Init objects self.config = config - - self._sd_notify = sdnotify.SystemdNotifier() if \ - self.config.get('internals', {}).get('sd_notify', False) else None + self._worker: Worker = worker self.strategy: IStrategy = StrategyResolver(self.config).strategy @@ -79,29 +71,16 @@ class FreqtradeBot(object): self.config.get('edge', {}).get('enabled', False) else None self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - self._init_modules() - - # Tell the systemd that we completed initialization phase - if self._sd_notify: - logger.debug("sd_notify: READY=1") - self._sd_notify.notify("READY=1") - - def _init_modules(self) -> None: - """ - Initializes all modules and updates the config - :return: None - """ - # Initialize all modules persistence.init(self.config) - # Set initial application state - initial_state = self.config.get('initial_state') + @property + def state(self) -> State: + return self._worker.state - if initial_state: - self.state = State[initial_state.upper()] - else: - self.state = State.STOPPED + @state.setter + def state(self, value: State): + self._worker.state = value def cleanup(self) -> None: """ @@ -113,75 +92,7 @@ class FreqtradeBot(object): self.rpc.cleanup() persistence.cleanup() - def stopping(self) -> None: - # Tell systemd that we are exiting now - if self._sd_notify: - logger.debug("sd_notify: STOPPING=1") - self._sd_notify.notify("STOPPING=1") - - def reconfigure(self) -> None: - # Tell systemd that we initiated reconfiguring - if self._sd_notify: - logger.debug("sd_notify: RELOADING=1") - self._sd_notify.notify("RELOADING=1") - - def worker(self, old_state: State = None) -> State: - """ - Trading routine that must be run at each loop - :param old_state: the previous service state from the previous call - :return: current service state - """ - # Log state transition - state = self.state - if state != old_state: - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'{state.name.lower()}' - }) - logger.info('Changing state to: %s', state.name) - if state == State.RUNNING: - self.rpc.startup_messages(self.config, self.pairlists) - - throttle_secs = self.config.get('internals', {}).get( - 'process_throttle_secs', - constants.PROCESS_THROTTLE_SECS - ) - - if state == State.STOPPED: - # Ping systemd watchdog before sleeping in the stopped state - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.") - - time.sleep(throttle_secs) - - elif state == State.RUNNING: - # Ping systemd watchdog before throttling - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.") - - self._throttle(func=self._process, min_secs=throttle_secs) - - return state - - def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: - """ - Throttles the given callable that it - takes at least `min_secs` to finish execution. - :param func: Any callable - :param min_secs: minimum execution time in seconds - :return: Any - """ - start = time.time() - result = func(*args, **kwargs) - end = time.time() - duration = max(min_secs - (end - start), 0.0) - logger.debug('Throttling %s for %.2f seconds', func.__name__, duration) - time.sleep(duration) - return result - - def _process(self) -> bool: + def process(self) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. diff --git a/freqtrade/main.py b/freqtrade/main.py index c41d54f0e..48ae17f9d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -5,10 +5,12 @@ Read the documentation to know what cli arguments you need. """ import logging import sys +import time from argparse import Namespace -from typing import List +from typing import Any, Callable, List +import sdnotify -from freqtrade import OperationalException +from freqtrade import (constants, OperationalException, __version__) from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.freqtradebot import FreqtradeBot @@ -35,20 +37,11 @@ def main(sysargv: List[str]) -> None: args.func(args) return - freqtrade = None return_code = 1 try: - # Load and validate configuration - config = Configuration(args, None).get_config() - - # Init the bot - freqtrade = FreqtradeBot(config) - - state = None - while True: - state = freqtrade.worker(old_state=state) - if state == State.RELOAD_CONF: - freqtrade = reconfigure(freqtrade, args) + # Load and run worker + worker = Worker(args) + worker.run() except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') @@ -59,32 +52,163 @@ def main(sysargv: List[str]) -> None: except BaseException: logger.exception('Fatal exception!') finally: - if freqtrade: - freqtrade.stopping() - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'process died' - }) - freqtrade.cleanup() + if worker is not None: + worker.exit() sys.exit(return_code) -def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: +class Worker(object): """ - Cleans up current instance, reloads the configuration and returns the new instance + Freqtradebot worker class """ - freqtrade.reconfigure() - # Clean up current modules - freqtrade.cleanup() + def __init__(self, args: Namespace) -> None: + """ + Init all variables and objects the bot needs to work + """ + logger.info('Starting worker %s', __version__) - # Create new instance - freqtrade = FreqtradeBot(Configuration(args, None).get_config()) - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'config reloaded' - }) - return freqtrade + self._args = args + self._init() + + # Tell systemd that we completed initialization phase + if self._sd_notify: + logger.debug("sd_notify: READY=1") + self._sd_notify.notify("READY=1") + + def _init(self): + """ + Also called from the _reconfigure() method. + """ + # Load configuration + self._config = Configuration(self._args, None).get_config() + + # Init the instance of the bot + self.freqtrade = FreqtradeBot(self._config, self) + + # Set initial bot state + initial_state = self._config.get('initial_state') + if initial_state: + self._state = State[initial_state.upper()] + else: + self._state = State.STOPPED + + self._throttle_secs = self._config.get('internals', {}).get( + 'process_throttle_secs', + constants.PROCESS_THROTTLE_SECS + ) + + self._sd_notify = sdnotify.SystemdNotifier() if \ + self._config.get('internals', {}).get('sd_notify', False) else None + + @property + def state(self) -> State: + return self._state + + @state.setter + def state(self, value: State): + self._state = value + + def run(self): + state = None + while True: + state = self._worker(old_state=state, throttle_secs=self._throttle_secs) + if state == State.RELOAD_CONF: + self.freqtrade = self._reconfigure() + + def _worker(self, old_state: State, throttle_secs: float) -> State: + """ + Trading routine that must be run at each loop + :param old_state: the previous service state from the previous call + :return: current service state + """ + state = self._state + + # Log state transition + if state != old_state: + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'{state.name.lower()}' + }) + logger.info('Changing state to: %s', state.name) + if state == State.RUNNING: + self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists) + + if state == State.STOPPED: + # Ping systemd watchdog before sleeping in the stopped state + if self._sd_notify: + logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.") + self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.") + + time.sleep(throttle_secs) + + elif state == State.RUNNING: + # Ping systemd watchdog before throttling + if self._sd_notify: + logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.") + self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.") + + self._throttle(func=self._process, min_secs=throttle_secs) + + return state + + def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: + """ + Throttles the given callable that it + takes at least `min_secs` to finish execution. + :param func: Any callable + :param min_secs: minimum execution time in seconds + :return: Any + """ + start = time.time() + result = func(*args, **kwargs) + end = time.time() + duration = max(min_secs - (end - start), 0.0) + logger.debug('Throttling %s for %.2f seconds', func.__name__, duration) + time.sleep(duration) + return result + + def _process(self) -> bool: + return self.freqtrade.process() + + def _reconfigure(self): + """ + Cleans up current freqtradebot instance, reloads the configuration and + returns the new instance + """ + # Tell systemd that we initiated reconfiguration + if self._sd_notify: + logger.debug("sd_notify: RELOADING=1") + self._sd_notify.notify("RELOADING=1") + + # Clean up current freqtrade modules + self.freqtrade.cleanup() + + # Load and validate config and create new instance of the bot + self._init() + + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'config reloaded' + }) + + # Tell systemd that we completed reconfiguration + if self._sd_notify: + logger.debug("sd_notify: READY=1") + self._sd_notify.notify("READY=1") + + def exit(self): + # Tell systemd that we are exiting now + if self._sd_notify: + logger.debug("sd_notify: STOPPING=1") + self._sd_notify.notify("STOPPING=1") + + if self.freqtrade: + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'process died' + }) + self.freqtrade.cleanup() if __name__ == '__main__': From be6836b0ef912541c336e8c7ccdd29de36dce908 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 22 Mar 2019 21:49:19 +0300 Subject: [PATCH 032/928] resolve python module circular dependency --- freqtrade/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 48ae17f9d..e1b4d80fa 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -13,7 +13,6 @@ import sdnotify from freqtrade import (constants, OperationalException, __version__) from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers -from freqtrade.freqtradebot import FreqtradeBot from freqtrade.state import State from freqtrade.rpc import RPCMessageType @@ -83,6 +82,10 @@ class Worker(object): # Load configuration self._config = Configuration(self._args, None).get_config() + # Import freqtradebot here in order to avoid python circular + # dependency error, damn! + from freqtrade.freqtradebot import FreqtradeBot + # Init the instance of the bot self.freqtrade = FreqtradeBot(self._config, self) From b4488902100408a90beb33acd79bc43fe272235b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 22 Mar 2019 22:03:15 +0300 Subject: [PATCH 033/928] test_main.py adjusted (only beginning) --- freqtrade/tests/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 51c95a4a9..188b9b9bd 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -8,7 +8,7 @@ import pytest from freqtrade import OperationalException from freqtrade.arguments import Arguments from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.main import main, reconfigure +from freqtrade.main import main, Worker from freqtrade.state import State from freqtrade.tests.conftest import log_has, patch_exchange @@ -126,7 +126,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: # Raise exception as side effect to avoid endless loop reconfigure_mock = mocker.patch( - 'freqtrade.main.reconfigure', MagicMock(side_effect=Exception) + 'freqtrade.main.Worker._reconfigure', MagicMock(side_effect=Exception) ) with pytest.raises(SystemExit): From e35daf95c04571a4297769e1ac6d1c2f5965b2c3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 22 Mar 2019 23:41:48 +0300 Subject: [PATCH 034/928] minor cleanup --- freqtrade/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index e1b4d80fa..923230b7f 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -36,6 +36,7 @@ def main(sysargv: List[str]) -> None: args.func(args) return + worker = None return_code = 1 try: # Load and run worker @@ -51,7 +52,7 @@ def main(sysargv: List[str]) -> None: except BaseException: logger.exception('Fatal exception!') finally: - if worker is not None: + if worker: worker.exit() sys.exit(return_code) From 158cb307f6268c37b711b1bc1ac5d2196bca3647 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 23 Mar 2019 00:20:20 +0300 Subject: [PATCH 035/928] further refactoring of FreqtradeBot.process() --- freqtrade/freqtradebot.py | 71 ++++++++++++++++----------------------- freqtrade/main.py | 22 ++++++++++-- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ef00ba21d..784c0b938 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging -import time import traceback from datetime import datetime from typing import Any, Dict, List, Optional, Tuple @@ -13,7 +12,7 @@ import arrow from requests.exceptions import RequestException from freqtrade import (DependencyException, OperationalException, - TemporaryError, __version__, constants, persistence) + __version__, constants, persistence) from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge @@ -99,55 +98,43 @@ class FreqtradeBot(object): :return: True if one or more trades has been created or closed, False otherwise """ state_changed = False - try: - # Check whether markets have to be reloaded - self.exchange._reload_markets() - # Refresh whitelist - self.pairlists.refresh_pairlist() - self.active_pair_whitelist = self.pairlists.whitelist + # Check whether markets have to be reloaded + self.exchange._reload_markets() - # Calculating Edge positioning - if self.edge: - self.edge.calculate() - self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) + # Refresh whitelist + self.pairlists.refresh_pairlist() + self.active_pair_whitelist = self.pairlists.whitelist - # Query trades from persistence layer - trades = Trade.get_open_trades() + # Calculating Edge positioning + if self.edge: + self.edge.calculate() + self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) - # Extend active-pair whitelist with pairs from open trades - # It ensures that tickers are downloaded for open trades - self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) + # Query trades from persistence layer + trades = Trade.get_open_trades() - # Refreshing candles - self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), - self.strategy.informative_pairs()) + # Extend active-pair whitelist with pairs from open trades + # It ensures that tickers are downloaded for open trades + self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) - # First process current opened trades - for trade in trades: - state_changed |= self.process_maybe_execute_sell(trade) + # Refreshing candles + self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), + self.strategy.informative_pairs()) - # Then looking for buy opportunities - if len(trades) < self.config['max_open_trades']: - state_changed = self.process_maybe_execute_buy() + # First process current opened trades + for trade in trades: + state_changed |= self.process_maybe_execute_sell(trade) - if 'unfilledtimeout' in self.config: - # Check and handle any timed out open orders - self.check_handle_timedout() - Trade.session.flush() + # Then looking for buy opportunities + if len(trades) < self.config['max_open_trades']: + state_changed = 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() - except TemporaryError as error: - logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") - time.sleep(constants.RETRY_TIMEOUT) - except OperationalException: - tb = traceback.format_exc() - hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'OperationalException:\n```\n{tb}```{hint}' - }) - logger.exception('OperationalException. Stopping trader ...') - self.state = State.STOPPED return state_changed def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): diff --git a/freqtrade/main.py b/freqtrade/main.py index 923230b7f..9331206fc 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -6,11 +6,13 @@ Read the documentation to know what cli arguments you need. import logging import sys import time +import traceback from argparse import Namespace from typing import Any, Callable, List import sdnotify -from freqtrade import (constants, OperationalException, __version__) +from freqtrade import (constants, OperationalException, TemporaryError, + __version__) from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.state import State @@ -173,7 +175,23 @@ class Worker(object): return result def _process(self) -> bool: - return self.freqtrade.process() + state_changed = False + try: + state_changed = self.freqtrade.process() + + except TemporaryError as error: + logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") + time.sleep(constants.RETRY_TIMEOUT) + except OperationalException: + tb = traceback.format_exc() + hint = 'Issue `/start` if you think it is safe to restart.' + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'OperationalException:\n```\n{tb}```{hint}' + }) + logger.exception('OperationalException. Stopping trader ...') + self.state = State.STOPPED + return state_changed def _reconfigure(self): """ From 34ff946f4dc3042056a3dfa03a648b42d8e932d0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 23 Mar 2019 13:35:03 +0100 Subject: [PATCH 036/928] Update ccxt from 1.18.386 to 1.18.387 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53e81aad3..9c083a675 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.386 +ccxt==1.18.387 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From 05466d318a2f3971bff4e234ecbc7aa00e57f419 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 14:50:18 +0100 Subject: [PATCH 037/928] Modify test to check for this condition --- freqtrade/tests/optimize/test_backtesting.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 40754cfbc..531f73916 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -683,18 +683,20 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): assert len(results.loc[results.open_at_end]) == 0 -def test_backtest_multi_pair(default_conf, fee, mocker): +@pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC']) +@pytest.mark.parametrize("tres", [0, 20, 30]) +def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): def _trend_alternate_hold(dataframe=None, metadata=None): """ - Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit) + Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)flake """ - multi = 8 + if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): + multi = 20 + else: + multi = 18 dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) - if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): - dataframe['buy'] = dataframe['buy'].shift(-4) - dataframe['sell'] = dataframe['sell'].shift(-4) return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) @@ -702,6 +704,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker): pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs) data = trim_dictlist(data, -500) + data[pair] = data[pair][tres:] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} default_conf['ticker_interval'] = '5m' From 00e6749d8bf8e7fc9c8924d0525d855acc8a72ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 15:00:07 +0100 Subject: [PATCH 038/928] Refactor backtest() to be a bit more concise --- freqtrade/optimize/backtesting.py | 47 ++++++++++++++++++------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f54560a0e..f3661ab32 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -202,6 +202,32 @@ class Backtesting(object): logger.info('Dumping backtest results to %s', recordfilename) file_dump_json(recordfilename, records) + def _get_ticker_list(self, processed) -> Dict[str, DataFrame]: + """ + Helper function to convert a processed tickerlist into a list for performance reasons. + + Used by backtest() - so keep this optimized for performance. + """ + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + ticker: Dict = {} + # Create ticker dict + for pair, pair_data in processed.items(): + pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + + ticker_data = self.advise_sell( + self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + + # to avoid using data from future, we buy/sell with signal from previous candle + ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) + ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + + ticker_data.drop(ticker_data.head(1).index, inplace=True) + + # Convert from Pandas to list for performance reasons + # (Looping Pandas is slow.) + ticker[pair] = [x for x in ticker_data.itertuples()] + return ticker + def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]: @@ -296,7 +322,6 @@ class Backtesting(object): position_stacking: do we allow position stacking? (default: False) :return: DataFrame """ - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) @@ -304,25 +329,7 @@ class Backtesting(object): end_date = args['end_date'] trades = [] trade_count_lock: Dict = {} - ticker: Dict = {} - pairs = [] - # Create ticker dict - for pair, pair_data in processed.items(): - pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run - - ticker_data = self.advise_sell( - self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - - # to avoid using data from future, we buy/sell with signal from previous candle - ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) - - ticker_data.drop(ticker_data.head(1).index, inplace=True) - - # Convert from Pandas to list for performance reasons - # (Looping Pandas is slow.) - ticker[pair] = [x for x in ticker_data.itertuples()] - pairs.append(pair) + ticker: Dict = self._get_ticker_list(processed) lock_pair_until: Dict = {} indexes: Dict = {} From 40899d08dd76aca924c52ce02be418747468ef25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 15:24:11 +0100 Subject: [PATCH 039/928] Fix failing test (all timezones are in UTC, so we should not convert to None) --- freqtrade/tests/edge/test_edge.py | 4 ++-- freqtrade/tests/optimize/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index c1c1b49cd..af8674188 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -122,8 +122,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.exit_type == trade.sell_reason - assert res.open_time == _get_frame_time_from_offset(trade.open_tick) - assert res.close_time == _get_frame_time_from_offset(trade.close_tick) + assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick) + assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick) def test_adjust(mocker, edge_conf): diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 129a09f40..b1bf55f50 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -32,7 +32,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): return ticker_start_time.shift(minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval]) - ).datetime.replace(tzinfo=None) + ).datetime def _build_backtest_dataframe(ticker_with_signals): From 7307084dfd5c0ec955278e285d464203d71a13bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 16:21:58 +0100 Subject: [PATCH 040/928] Move stoploss-adjustment to the top --- freqtrade/strategy/interface.py | 47 ++++++++++++++-------------- freqtrade/tests/test_freqtradebot.py | 9 ++---- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a086139c7..542b6bab0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -302,6 +302,29 @@ class IStrategy(ABC): trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss else self.stoploss, initial=True) + # update the stop loss afterwards, after all by definition it's supposed to be hanging + # TODO: Maybe this needs to be moved to the start of this function. check #1575 for details + if trailing_stop: + + # check if we have a special stop loss for positive condition + # and if profit is positive + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + + sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 + + if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore + logger.debug(f"using positive stop loss: {stop_loss_value} " + f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") + + # if trailing_only_offset_is_reached is true, + # we update trailing stoploss only if offset is reached. + tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) + if not (tsl_only_offset and current_profit < sl_offset): + trade.adjust_stop_loss(high or current_rate, stop_loss_value) + # evaluate if the stoploss was hit if stoploss is not on exchange if ((self.stoploss is not None) and (trade.stop_loss >= current_rate) and @@ -321,30 +344,6 @@ class IStrategy(ABC): logger.debug('Stop loss hit.') return SellCheckTuple(sell_flag=True, sell_type=selltype) - # update the stop loss afterwards, after all by definition it's supposed to be hanging - # TODO: Maybe this needs to be moved to the start of this function. check #1575 for details - if trailing_stop: - - # check if we have a special stop loss for positive condition - # and if profit is positive - stop_loss_value = force_stoploss if force_stoploss else self.stoploss - - sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 - - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: - - # Ignore mypy error check in configuration that this is a float - stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore - logger.debug(f"using positive stop loss mode: {stop_loss_value} " - f"with offset {sl_offset:.4g} " - f"since we have profit {current_profit:.4f}%") - - # if trailing_only_offset_is_reached is true, - # we update trailing stoploss only if offset is reached. - tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) - if not (tsl_only_offset and current_profit < sl_offset): - trade.adjust_stop_loss(high or current_rate, stop_loss_value) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index a5de283a5..561df7c05 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2406,8 +2406,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 with offset 0 ' - f'since we have profit 0.2666%', + assert log_has(f'using positive stop loss: 0.01 offset: 0 profit: 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -2466,8 +2465,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 with offset 0.011 ' - f'since we have profit 0.2666%', + assert log_has(f'using positive stop loss: 0.01 offset: 0.011 profit: 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -2546,8 +2544,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, })) assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.05 with offset 0.055 ' - f'since we have profit 0.1218%', + assert log_has(f'using positive stop loss: 0.05 offset: 0.055 profit: 0.1218%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000117705 From b1fe8c532549d1c0552390e68d59f105d83a7649 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 16:23:32 +0100 Subject: [PATCH 041/928] Simplify stoploss_reached --- freqtrade/strategy/interface.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 542b6bab0..a9925b654 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -299,18 +299,16 @@ class IStrategy(ABC): """ trailing_stop = self.config.get('trailing_stop', False) - trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss - else self.stoploss, initial=True) + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + + trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - # update the stop loss afterwards, after all by definition it's supposed to be hanging - # TODO: Maybe this needs to be moved to the start of this function. check #1575 for details if trailing_stop: - # check if we have a special stop loss for positive condition # and if profit is positive - stop_loss_value = force_stoploss if force_stoploss else self.stoploss sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 + tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) if 'trailing_stop_positive' in self.config and current_profit > sl_offset: @@ -321,7 +319,6 @@ class IStrategy(ABC): # if trailing_only_offset_is_reached is true, # we update trailing stoploss only if offset is reached. - tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) if not (tsl_only_offset and current_profit < sl_offset): trade.adjust_stop_loss(high or current_rate, stop_loss_value) From c404e9ffd06e94d320b95ba2387d011cd5eab70d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 16:48:17 +0100 Subject: [PATCH 042/928] Simplify trailing_stop logic --- freqtrade/strategy/interface.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a9925b654..eb9ed825e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -301,6 +301,7 @@ class IStrategy(ABC): trailing_stop = self.config.get('trailing_stop', False) stop_loss_value = force_stoploss if force_stoploss else self.stoploss + # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if trailing_stop: @@ -310,16 +311,15 @@ class IStrategy(ABC): sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: - - # Ignore mypy error check in configuration that this is a float - stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore - logger.debug(f"using positive stop loss: {stop_loss_value} " - f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") - - # if trailing_only_offset_is_reached is true, - # we update trailing stoploss only if offset is reached. + # Don't update stoploss if trailing_only_offset_is_reached is true. if not (tsl_only_offset and current_profit < sl_offset): + + if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore + logger.debug(f"using positive stop loss: {stop_loss_value} " + f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") + trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange From 9a632d9b7ca09bd0c85ad58f05e2583dc2951804 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 16:51:36 +0100 Subject: [PATCH 043/928] Formatting --- freqtrade/strategy/interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index eb9ed825e..fcb27d7bd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -290,8 +290,9 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, - current_profit: float, force_stoploss: float, high) -> SellCheckTuple: + def stop_loss_reached(self, current_rate: float, trade: Trade, + current_time: datetime, current_profit: float, + force_stoploss: float, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not @@ -305,15 +306,14 @@ class IStrategy(ABC): trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if trailing_stop: - # check if we have a special stop loss for positive condition - # and if profit is positive + # trailing stoploss handling sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (tsl_only_offset and current_profit < sl_offset): - + # Specific handling for trailing_stop_positive if 'trailing_stop_positive' in self.config and current_profit > sl_offset: # Ignore mypy error check in configuration that this is a float stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore From 184b13f2fba7a690919d85ca815fdfb4d48e1ee8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 19:18:10 +0100 Subject: [PATCH 044/928] Flake8 for scripts --- .travis.yml | 4 ++-- scripts/get_market_pairs.py | 2 -- scripts/plot_dataframe.py | 6 ++---- scripts/plot_profit.py | 10 ++++------ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index d24ffcf1b..014eb00ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ jobs: script: - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ # Allow failure for coveralls - - coveralls || true + - coveralls || true name: pytest - script: - cp config.json.example config.json @@ -39,7 +39,7 @@ jobs: - cp config.json.example config.json - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - - script: flake8 freqtrade + - script: flake8 freqtrade scripts name: flake8 - script: mypy freqtrade name: mypy diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py index 6ee6464d3..1a4c593f9 100644 --- a/scripts/get_market_pairs.py +++ b/scripts/get_market_pairs.py @@ -51,7 +51,6 @@ try: id = sys.argv[1] # get exchange id from command line arguments - # check if the exchange is supported by ccxt exchange_found = id in ccxt.exchanges @@ -90,4 +89,3 @@ except Exception as e: dump('[' + type(e).__name__ + ']', str(e)) dump("Usage: python " + sys.argv[0], green('id')) print_supported_exchanges() - diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 581518b12..2eccaca18 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,24 +24,22 @@ Example of usage: > python3 scripts/plot_dataframe.py --pairs BTC/EUR,XRP/BTC -d user_data/data/ --indicators1 sma,ema3 --indicators2 fastk,fastd """ -import json import logging import sys from argparse import Namespace from pathlib import Path -from typing import Dict, List, Any +from typing import Any, Dict, List import pandas as pd import plotly.graph_objs as go import pytz - from plotly import tools from plotly.offline import plot from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_backtest_data, BT_DATA_COLUMNS +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data from freqtrade.exchange import Exchange from freqtrade.optimize.backtesting import setup_configuration from freqtrade.persistence import Trade diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index e2f85932f..8361e8bf6 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -12,26 +12,24 @@ Optional Cli parameters --timerange: specify what timerange of data to use --export-filename: Specify where the backtest export is located. """ +import json import logging import sys -import json from argparse import Namespace from pathlib import Path from typing import List, Optional -import numpy as np +import numpy as np +import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -import plotly.graph_objs as go +from freqtrade import constants, misc from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade import constants from freqtrade.data import history from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode -import freqtrade.misc as misc - logger = logging.getLogger(__name__) From 83a2427a61b7025d58e74c49100dc3b65d4c1088 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 19:28:06 +0100 Subject: [PATCH 045/928] Fix mypy in scripts --- .travis.yml | 2 +- docs/plotting.md | 2 +- scripts/plot_dataframe.py | 2 +- scripts/plot_profit.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 014eb00ad..71432ea0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ jobs: name: hyperopt - script: flake8 freqtrade scripts name: flake8 - - script: mypy freqtrade + - script: mypy freqtrade scripts name: mypy - stage: docker diff --git a/docs/plotting.md b/docs/plotting.md index a9b191e75..60c642ab3 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -84,5 +84,5 @@ The `-p` pair argument, can be used to plot a single pair Example ``` -python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC +python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC ``` diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 2eccaca18..14d57265e 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -146,7 +146,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): tickers[pair] = exchange.klines((pair, tick_interval)) else: tickers = history.load_data( - datadir=Path(_CONF.get("datadir")), + datadir=Path(str(_CONF.get("datadir"))), pairs=pairs, ticker_interval=tick_interval, refresh_pairs=_CONF.get('refresh_pairs', False), diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 8361e8bf6..394f02116 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) # data:: [ pair, profit-%, enter, exit, time, duration] # data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65] def make_profit_array(data: List, px: int, min_date: int, - interval: int, + interval: str, filter_pairs: Optional[List] = None) -> np.ndarray: pg = np.zeros(px) filter_pairs = filter_pairs or [] @@ -120,7 +120,7 @@ def plot_profit(args: Namespace) -> None: logger.info('Filter, keep pairs %s' % pairs) tickers = history.load_data( - datadir=Path(config.get('datadir')), + datadir=Path(str(config.get('datadir'))), pairs=pairs, ticker_interval=tick_interval, refresh_pairs=False, @@ -178,7 +178,7 @@ def plot_profit(args: Namespace) -> None: fig.append_trace(profit, 2, 1) for pair in pairs: - pg = make_profit_array(data, num_iterations, min_date, tick_interval, pair) + pg = make_profit_array(data, num_iterations, min_date, tick_interval, [pair]) pair_profit = go.Scattergl( x=dates, y=pg, From a95f30ce452b06d16a854ccf04e182d1f350c2b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 19:40:52 +0100 Subject: [PATCH 046/928] Fix custom boxes on documentation --- docs/bot-optimization.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 8592f6cca..00ed470f5 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -250,17 +250,17 @@ class Awesomestrategy(IStrategy): self.cust_info[metadata["pair"]["crosstime"] = 1 ``` -!!! Warning: +!!! Warning The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. -!!! Note: +!!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ### Additional data (DataProvider) The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. -!!!Note: +!!! Note The DataProvier is currently not available during backtesting / hyperopt, but this is planned for the future. All methods return `None` in case of failure (do not raise an exception). @@ -288,7 +288,7 @@ if self.dp: ticker_interval='1h') ``` -!!! Warning: Warning about backtesting +!!! Warning Warning about backtesting Be carefull when using dataprovider in backtesting. `historic_ohlcv()` 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). @@ -317,7 +317,7 @@ def informative_pairs(self): ] ``` -!!! Warning: +!!! Warning As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short. All intervals and all pairs can be specified as long as they are available (and active) on the used exchange. It is however better to use resampling to longer time-intervals when possible @@ -327,7 +327,7 @@ def informative_pairs(self): The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. -!!!NOTE: +!!! Note Wallets is not available during backtesting / hyperopt. Please always check if `Wallets` is available to avoid failures during backtesting. From 0dc96210b629af0157136f2e31d1763c45cfde61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 19:43:23 +0100 Subject: [PATCH 047/928] Fix formatting of boxes 2 --- docs/bot-optimization.md | 2 +- docs/hyperopt.md | 4 ++-- docs/installation.md | 6 +++--- docs/sql_cheatsheet.md | 4 ++-- docs/telegram-usage.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 00ed470f5..d1ab08635 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -47,7 +47,7 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) file as reference.** -!!! Note: Strategies and Backtesting +!!! Note Strategies and Backtesting To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware that during backtesting the full time-interval is passed to the `populate_*()` methods at once. It is therefore best to use vectorized operations (across the whole dataframe, not loops) and diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6e52be47f..14021a341 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -293,8 +293,8 @@ You can overwrite position stacking in the configuration by explicitly setting ` Enabling the market-position for hyperopt is currently not possible. -!!! Note: -Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. ## Next Step diff --git a/docs/installation.md b/docs/installation.md index bd6c50c5a..02fdf5b8f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -437,9 +437,9 @@ when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd as the watchdog. -!!! Note: -The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a -Docker container. +!!! Note + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a + Docker container. ------ diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index e85aceec8..54f9b8213 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -65,11 +65,11 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange -!!! Warning: +!!! Warning Manually selling on the exchange should not be done by default, since the bot does not detect this and will try to sell anyway. /foresell should accomplish the same thing. -!!! Note: +!!! Note This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. ```sql diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index face22404..92d60c7ed 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -55,8 +55,8 @@ Once all positions are sold, run `/stop` to completely stop the bot. `/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. -!!! warning: -The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. +!!! warning + The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. ### /status From 06f4e627fcf8ba33ed58dd4ed390b98a3c5428a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 20:40:07 +0100 Subject: [PATCH 048/928] Add stake_currency to strategy, fix documentation typo --- docs/bot-optimization.md | 10 +++++----- freqtrade/resolvers/strategy_resolver.py | 2 ++ freqtrade/tests/strategy/test_strategy.py | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 8592f6cca..89d35848e 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -278,13 +278,13 @@ Please always check if the `DataProvider` is available to avoid failures during ``` python if self.dp: - if dp.runmode == 'live': - if ('ETH/BTC', ticker_interval) in self.dp.available_pairs: - data_eth = self.dp.ohlcv(pair='ETH/BTC', - ticker_interval=ticker_interval) + if self.dp.runmode == 'live': + 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='ETH/BTC', + history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC', ticker_interval='1h') ``` diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index cece5a5d1..44cc3fe76 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -56,6 +56,8 @@ class StrategyResolver(IResolver): ("process_only_new_candles", None, False), ("order_types", None, False), ("order_time_in_force", None, False), + ("stake_currency", None, False), + ("stake_amount", None, False), ("use_sell_signal", False, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 602ea5dbe..b63180d1e 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -194,11 +194,13 @@ def test_strategy_override_ticker_interval(caplog): config = { 'strategy': 'DefaultStrategy', - 'ticker_interval': 60 + 'ticker_interval': 60, + 'stake_currency': 'ETH' } resolver = StrategyResolver(config) assert resolver.strategy.ticker_interval == 60 + assert resolver.strategy.stake_currency == 'ETH' assert ('freqtrade.resolvers.strategy_resolver', logging.INFO, "Override strategy 'ticker_interval' with value in config file: 60." From e644493e02b4877e160eb95a638dfc9c58a82715 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 24 Mar 2019 13:35:03 +0100 Subject: [PATCH 049/928] Update ccxt from 1.18.387 to 1.18.395 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9c083a675..73c65aff6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.387 +ccxt==1.18.395 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From e60d1788b28a92f75959a4d096cceb58440d3e57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:06:17 +0100 Subject: [PATCH 050/928] Add new options to docu --- docs/configuration.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 4b8d990fe..11b941220 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,8 +14,8 @@ Mandatory Parameters are marked as **Required**. | Command | Default | Description | |----------|---------|-------------| | `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. -| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. +| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). +| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). | `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. | `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. @@ -77,8 +77,10 @@ Mandatory Parameters are marked as **Required**. The following parameters can be set in either configuration file or strategy. Values set in the configuration file always overwrite values set in the strategy. -* `minimal_roi` +* `stake_currency` +* `stake_amount` * `ticker_interval` +* `minimal_roi` * `stoploss` * `trailing_stop` * `trailing_stop_positive` From f7fc9adc638367cde602e3d2222530dd46030101 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:13:03 +0100 Subject: [PATCH 051/928] Run travis with freqtrade, not main.py --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d24ffcf1b..a711290ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,15 +29,15 @@ jobs: script: - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ # Allow failure for coveralls - - coveralls || true + - coveralls || true name: pytest - script: - cp config.json.example config.json - - python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting + - python freqtrade --datadir freqtrade/tests/testdata backtesting name: backtest - script: - cp config.json.example config.json - - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 + - python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - script: flake8 freqtrade name: flake8 From 1bba9fcc53bed652cca3e55d6596f51b5a0791f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:13:17 +0100 Subject: [PATCH 052/928] Update documentation to use freqtrade, not freqtrade/main.py fixes #1521 --- docs/backtesting.md | 18 +++++++++--------- docs/bot-optimization.md | 6 +++--- docs/bot-usage.md | 12 ++++++------ docs/deprecated.md | 4 ++-- docs/edge.md | 22 +++++++++++++++++----- docs/faq.md | 10 ++++++---- docs/hyperopt.md | 4 ++-- docs/installation.md | 2 +- setup.sh | 2 +- 9 files changed, 47 insertions(+), 33 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 932783160..a25d3c1d5 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -24,37 +24,37 @@ The backtesting is very easy with freqtrade. #### With 5 min tickers (Per default) ```bash -python3 ./freqtrade/main.py backtesting +python3 freqtrade backtesting ``` #### With 1 min tickers ```bash -python3 ./freqtrade/main.py backtesting --ticker-interval 1m +python3 freqtrade backtesting --ticker-interval 1m ``` #### Update cached pairs with the latest data ```bash -python3 ./freqtrade/main.py backtesting --refresh-pairs-cached +python3 freqtrade backtesting --refresh-pairs-cached ``` #### With live data (do not alter your testdata files) ```bash -python3 ./freqtrade/main.py backtesting --live +python3 freqtrade backtesting --live ``` #### Using a different on-disk ticker-data source ```bash -python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 +python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 ``` #### With a (custom) strategy file ```bash -python3 ./freqtrade/main.py -s TestStrategy backtesting +python3 freqtrade -s TestStrategy backtesting ``` Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory @@ -62,7 +62,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ #### Exporting trades to file ```bash -python3 ./freqtrade/main.py backtesting --export trades +python3 freqtrade backtesting --export trades ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder. @@ -70,7 +70,7 @@ The exported trades can be used for [further analysis](#further-backtest-result- #### Exporting trades to file specifying a custom filename ```bash -python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json +python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json ``` #### Running backtest with smaller testset @@ -81,7 +81,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 ./freqtrade/main.py backtesting --timerange=-200 +python3 freqtrade backtesting --timerange=-200 ``` #### Advanced use of timerange diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index d1ab08635..511be18fa 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -14,7 +14,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +python3 freqtrade --strategy AwesomeStrategy ``` ## Change your strategy @@ -41,7 +41,7 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us You can test it with the parameter: `--strategy TestStrategy` ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +python3 freqtrade --strategy AwesomeStrategy ``` **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) @@ -355,7 +355,7 @@ The default buy strategy is located in the file If you want to use a strategy from a different folder you can pass `--strategy-path` ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder ``` ### Further strategy ideas diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 739a89c07..35e4a776d 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -47,7 +47,7 @@ The bot allows you to select which configuration file you want to use. Per default, the bot will load the file `./config.json` ```bash -python3 ./freqtrade/main.py -c path/far/far/away/config.json +python3 freqtrade -c path/far/far/away/config.json ``` ### How to use multiple configuration files? @@ -63,13 +63,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua require them): ```bash -python3 ./freqtrade/main.py -c ./config.json +python3 freqtrade -c ./config.json ``` and specify both configuration files when running in the normal Live Trade Mode: ```bash -python3 ./freqtrade/main.py -c ./config.json -c path/to/secrets/keys.config.json +python3 freqtrade -c ./config.json -c path/to/secrets/keys.config.json ``` This could help you hide your private Exchange key and Exchange secrete on you local machine @@ -95,7 +95,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +python3 freqtrade --strategy AwesomeStrategy ``` If the bot does not find your strategy file, it will display in an error @@ -109,7 +109,7 @@ Learn more about strategy file in This parameter allows you to add an additional strategy lookup path, which gets checked before the default locations (The passed path must be a folder!): ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder ``` #### How to install a strategy? @@ -136,7 +136,7 @@ using `--db-url`. This can also be used to specify a custom database in production mode. Example command: ```bash -python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite +python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` ## Backtesting commands diff --git a/docs/deprecated.md b/docs/deprecated.md index 25043d981..c218bd360 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -13,14 +13,14 @@ on BaseVolume. This value can be changed when you run the script. Get the 20 currencies based on BaseVolume. ```bash -python3 ./freqtrade/main.py --dynamic-whitelist +python3 freqtrade --dynamic-whitelist ``` **Customize the number of currencies to retrieve** Get the 30 currencies based on BaseVolume. ```bash -python3 ./freqtrade/main.py --dynamic-whitelist 30 +python3 freqtrade --dynamic-whitelist 30 ``` **Exception** diff --git a/docs/edge.md b/docs/edge.md index 7372e3373..b0e0b2d42 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -146,16 +146,19 @@ Percentage of allowed risk per trade. (defaults to 0.01 so 1%) #### stoploss_range_min + Minimum stoploss. (defaults to -0.01) #### stoploss_range_max + Maximum stoploss. (defaults to -0.10) #### stoploss_range_step + As an example if this is set to -0.01 then Edge will test the strategy for \[-0.01, -0,02, -0,03 ..., -0.09, -0.10\] ranges. Note than having a smaller step means having a bigger range which could lead to slow calculation. @@ -164,6 +167,7 @@ If you set this parameter to -0.001, you then slow down the Edge calculation by (defaults to -0.01) #### minimum_winrate + It filters out pairs which don't have at least minimum_winrate. This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. @@ -171,6 +175,7 @@ This comes handy if you want to be conservative and don't comprise win rate in f (defaults to 0.60) #### minimum_expectancy + It filters out pairs which have the expectancy lower than this number. Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. @@ -178,6 +183,7 @@ Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ re (defaults to 0.20) #### min_trade_number + When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. @@ -185,6 +191,7 @@ Having a win rate of 100% on a single trade doesn't mean anything at all. But ha (defaults to 10, it is highly recommended not to decrease this number) #### max_trade_duration_minute + Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign. **NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.). @@ -192,15 +199,17 @@ Edge will filter out trades with long duration. If a trade is profitable after 1 (defaults to 1 day, i.e. to 60 * 24 = 1440 minutes) #### remove_pumps + Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off. (defaults to false) - ## Running Edge independently + You can run Edge independently in order to see in details the result. Here is an example: + ```bash -python3 ./freqtrade/main.py edge +python3 freqtrade edge ``` An example of its output: @@ -224,18 +233,21 @@ An example of its output: | NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | ### Update cached pairs with the latest data + ```bash -python3 ./freqtrade/main.py edge --refresh-pairs-cached +python3 freqtrade edge --refresh-pairs-cached ``` ### Precising stoploss range + ```bash -python3 ./freqtrade/main.py edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step +python3 freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step ``` ### Advanced use of timerange + ```bash -python3 ./freqtrade/main.py edge --timerange=20181110-20181113 +python3 freqtrade edge --timerange=20181110-20181113 ``` Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. diff --git a/docs/faq.md b/docs/faq.md index 127f69e9f..60c1509e0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -46,22 +46,24 @@ have to run it for 10.000 or more. But it will take an eternity to compute. We recommend you to run it at least 10.000 epochs: + ```bash -python3 ./freqtrade/main.py hyperopt -e 10000 +python3 freqtrade hyperopt -e 10000 ``` or if you want intermediate result to see + ```bash -for i in {1..100}; do python3 ./freqtrade/main.py hyperopt -e 100; done +for i in {1..100}; do python3 freqtrade hyperopt -e 100; done ``` #### Why it is so long to run hyperopt? + Finding a great Hyperopt results takes time. If you wonder why it takes a while to find great hyperopt results -This answer was written during the under the release 0.15.1, when we had -: +This answer was written during the under the release 0.15.1, when we had: - 8 triggers - 9 guards: let's say we evaluate even 10 values from each - 1 stoploss calculation: let's say we want 10 values from that too to diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 14021a341..e25f35c35 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -152,7 +152,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -python3 ./freqtrade/main.py -c config.json hyperopt --customhyperopt -e 5000 --spaces all +python3 freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -178,7 +178,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 ./freqtrade/main.py hyperopt --timerange -200 +python3 freqtrade hyperopt --timerange -200 ``` ### Running Hyperopt with Smaller Search Space diff --git a/docs/installation.md b/docs/installation.md index 02fdf5b8f..995bc561b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -407,7 +407,7 @@ pip3 install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3.6 ./freqtrade/main.py -c config.json +python3.6 freqtrade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. diff --git a/setup.sh b/setup.sh index 66d449037..11df6820e 100755 --- a/setup.sh +++ b/setup.sh @@ -235,7 +235,7 @@ function install() { echo "-------------------------" echo "Run the bot !" echo "-------------------------" - echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'." + echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade'." } function plot() { From 3a8b69d69becf6aa7711a29d1dc7ce6c2c3791b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:37:58 +0100 Subject: [PATCH 053/928] also support dry_run --- docs/bot-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 89d35848e..276c992f1 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -278,7 +278,7 @@ Please always check if the `DataProvider` is available to avoid failures during ``` python if self.dp: - if self.dp.runmode == 'live': + 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) From 684727b32ea11c8f1b4a4e8a120db215bb32fa7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:08:48 +0100 Subject: [PATCH 054/928] Add black blacklist handler (ro) --- freqtrade/rpc/rpc.py | 10 +++++++++- freqtrade/rpc/telegram.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a0ffff107..687ee9375 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -456,7 +456,15 @@ class RPC(object): def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name, - 'length': len(self._freqtrade.pairlists.whitelist), + 'length': len(self._freqtrade.active_pair_whitelist), 'whitelist': self._freqtrade.active_pair_whitelist } return res + + def _rpc_blacklist(self) -> Dict: + """ Returns the currently active blacklist""" + res = {'method': self._freqtrade.pairlists.name, + 'length': len(self._freqtrade.pairlists.blacklist), + 'blacklist': self._freqtrade.pairlists.blacklist + } + return res diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6771ec803..903efe7d1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,6 +93,7 @@ class Telegram(RPC): CommandHandler('reload_conf', self._reload_conf), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), + CommandHandler('blacklist', self._blacklist), CommandHandler('help', self._help), CommandHandler('version', self._version), ] @@ -470,6 +471,23 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e), bot=bot) + @authorized_only + def _blacklist(self, bot: Bot, update: Update) -> None: + """ + Handler for /blacklist + Shows the currently active blacklist + """ + try: + blacklist = self._rpc_blacklist() + + message = f"Using blacklist `{blacklist['method']}` with {blacklist['length']} pairs\n" + message += f"`{', '.join(blacklist['blacklist'])}`" + + logger.debug(message) + self._send_msg(message) + except RPCException as e: + self._send_msg(str(e), bot=bot) + @authorized_only def _help(self, bot: Bot, update: Update) -> None: """ @@ -497,6 +515,7 @@ class Telegram(RPC): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ + "*/blacklist:* `Show current blacklist` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" From ffdca7eea7d62969e9c580fe9a4599453241e541 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:09:04 +0100 Subject: [PATCH 055/928] Add blacklist to default_config --- freqtrade/tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 26262cb4b..c0f8e49b7 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -171,6 +171,10 @@ def default_conf(): "LTC/BTC", "XRP/BTC", "NEO/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC", + "HOT/BTC", ] }, "telegram": { From 8b2174d249135b285932892cbd417af5c470747a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:09:20 +0100 Subject: [PATCH 056/928] Add tests for /blacklist handler --- freqtrade/tests/rpc/test_rpc.py | 12 ++++++++++++ freqtrade/tests/rpc/test_rpc_telegram.py | 20 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index baddc0685..b1fbd27ff 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -693,3 +693,15 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: assert ret['method'] == 'VolumePairList' assert ret['length'] == 4 assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] + + +def test_rpc_blacklist(mocker, default_conf) -> None: + patch_coinmarketcap(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + ret = rpc._rpc_blacklist() + assert ret['method'] == 'StaticPairList' + assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 8e8d1f1bb..fec2e508c 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], " \ - "['stopbuy'], ['whitelist'], ['help'], ['version']]" + "['stopbuy'], ['whitelist'], ['blacklist'], ['help'], ['version']]" assert log_has(message_str, caplog.record_tuples) @@ -1074,6 +1074,24 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: in msg_mock.call_args_list[0][0][0]) +def test_blacklist_static(default_conf, update, mocker) -> None: + patch_coinmarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + + telegram = Telegram(freqtradebot) + + telegram._blacklist(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert ('Using blacklist `StaticPairList` with 2 pairs\n`DOGE/BTC, HOT/BTC`' + in msg_mock.call_args_list[0][0][0]) + + def test_help_handle(default_conf, update, mocker) -> None: patch_coinmarketcap(mocker) msg_mock = MagicMock() From 7b99d5ebcbb742d724232c606ca5a87691e9bcdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:16:39 +0100 Subject: [PATCH 057/928] Add blacklist and whitelist commands to telegram docs --- docs/telegram-usage.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 92d60c7ed..671229242 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -28,6 +28,8 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | | Show performance of each finished trade grouped by pair | `/balance` | | Show account balance per currency | `/daily ` | 7 | Shows profit or loss per day, over the last n days +| `/whitelist` | | Show the current whitelist +| `/blacklist` | | Show the current blacklist | `/help` | | Show help message | `/version` | | Show version @@ -160,6 +162,21 @@ Day Profit BTC Profit USD 2018-01-01 0.00269130 BTC 34.986 USD ``` +### /whitelist + +Shows the current whitelist + +> Using whitelist `StaticPairList` with 22 pairs +> `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` + + +### /blacklist + +Shows the current blacklist + +> Using blacklist `StaticPairList` with 2 pairs +>`DODGE/BTC`, `HOT/BTC`. + ### /version > **Version:** `0.14.3` From 9d6f629f6a50f29d8f497b2baaf8623995f87dcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:28:14 +0100 Subject: [PATCH 058/928] Support adding pairs to blacklist --- freqtrade/rpc/rpc.py | 5 ++++- freqtrade/rpc/telegram.py | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 687ee9375..cacca4e3c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -461,8 +461,11 @@ class RPC(object): } return res - def _rpc_blacklist(self) -> Dict: + def _rpc_blacklist(self, add: List[str]) -> Dict: """ Returns the currently active blacklist""" + if add: + self._freqtrade.pairlists.blacklist.extend(add) + res = {'method': self._freqtrade.pairlists.name, 'length': len(self._freqtrade.pairlists.blacklist), 'blacklist': self._freqtrade.pairlists.blacklist diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 903efe7d1..92108ded9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,7 @@ This module manage Telegram communication """ import logging -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update @@ -93,7 +93,7 @@ class Telegram(RPC): CommandHandler('reload_conf', self._reload_conf), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), - CommandHandler('blacklist', self._blacklist), + CommandHandler('blacklist', self._blacklist, pass_args=True), CommandHandler('help', self._help), CommandHandler('version', self._version), ] @@ -472,15 +472,16 @@ class Telegram(RPC): self._send_msg(str(e), bot=bot) @authorized_only - def _blacklist(self, bot: Bot, update: Update) -> None: + def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None: """ Handler for /blacklist Shows the currently active blacklist """ try: - blacklist = self._rpc_blacklist() - message = f"Using blacklist `{blacklist['method']}` with {blacklist['length']} pairs\n" + blacklist = self._rpc_blacklist(args) + + message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) From f0d3901b6b77976836a0fb300b7980b7f3b9fdfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:29:58 +0100 Subject: [PATCH 059/928] Add blacklist-pair to documentation --- docs/telegram-usage.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 671229242..d0ee2849d 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -29,7 +29,7 @@ official commands. You can ask at any moment for help with `/help`. | `/balance` | | Show account balance per currency | `/daily ` | 7 | Shows profit or loss per day, over the last n days | `/whitelist` | | Show the current whitelist -| `/blacklist` | | Show the current blacklist +| `/blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. | `/help` | | Show help message | `/version` | | Show version @@ -169,10 +169,11 @@ Shows the current whitelist > Using whitelist `StaticPairList` with 22 pairs > `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` +### /blacklist [pair] -### /blacklist - -Shows the current blacklist +Shows the current blacklist. +If Pair is set, then this pair will be added to the pairlist. +Use `/reload_conf` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. From 042354d00f2ede1a9052ceb935f6c3edf2dffb73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:30:11 +0100 Subject: [PATCH 060/928] Test blacklist-adding --- freqtrade/tests/rpc/test_rpc_telegram.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index fec2e508c..6adcda77b 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1086,11 +1086,19 @@ def test_blacklist_static(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) - telegram._blacklist(bot=MagicMock(), update=update) + telegram._blacklist(bot=MagicMock(), update=update, args=[]) assert msg_mock.call_count == 1 - assert ('Using blacklist `StaticPairList` with 2 pairs\n`DOGE/BTC, HOT/BTC`' + assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" in msg_mock.call_args_list[0][0][0]) + msg_mock.reset_mock() + telegram._blacklist(bot=MagicMock(), update=update, args=["ETH/BTC"]) + assert msg_mock.call_count == 1 + assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" + in msg_mock.call_args_list[0][0][0]) + assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] + + def test_help_handle(default_conf, update, mocker) -> None: patch_coinmarketcap(mocker) From 49559f1a1a4b553476ae986bc6bd677dc7f49890 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 16:32:56 +0100 Subject: [PATCH 061/928] Improve documentation and help message --- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 3 ++- freqtrade/tests/rpc/test_rpc_telegram.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index d0ee2849d..2f7e9ada8 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -173,6 +173,7 @@ Shows the current whitelist Shows the current blacklist. If Pair is set, then this pair will be added to the pairlist. +Also supports multiple pairs, seperated by a space. Use `/reload_conf` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92108ded9..553ac4ed9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -516,7 +516,8 @@ class Telegram(RPC): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ - "*/blacklist:* `Show current blacklist` \n" \ + "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ + "to the blacklist.` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 6adcda77b..dd49b0000 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1099,7 +1099,6 @@ def test_blacklist_static(default_conf, update, mocker) -> None: assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] - def test_help_handle(default_conf, update, mocker) -> None: patch_coinmarketcap(mocker) msg_mock = MagicMock() From 14167f826ba8924c629438461294ab2fb4179690 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 19:44:52 +0100 Subject: [PATCH 062/928] Fix typehints --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 553ac4ed9..2c419e417 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') -def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]: +def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler From 29b9bb96f3bc745a0ee88b83b39db7584e732175 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 19:49:49 +0100 Subject: [PATCH 063/928] Fix test to support adding things to pairlist --- freqtrade/tests/rpc/test_rpc.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index b1fbd27ff..e6f7ea41e 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -702,6 +702,14 @@ def test_rpc_blacklist(mocker, default_conf) -> None: freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) - ret = rpc._rpc_blacklist() + ret = rpc._rpc_blacklist(None) assert ret['method'] == 'StaticPairList' + assert len(ret['blacklist']) == 2 assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] + assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC'] + + ret = rpc._rpc_blacklist(["ETH/BTC"]) + assert ret['method'] == 'StaticPairList' + assert len(ret['blacklist']) == 3 + assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] + assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] From 1dfbf6eed646b6fe3575d8703f0f101053f3aad9 Mon Sep 17 00:00:00 2001 From: Misagh Date: Sun, 24 Mar 2019 22:36:33 +0100 Subject: [PATCH 064/928] darfting edge rpc messages --- freqtrade/rpc/rpc.py | 15 +++++++++++++++ freqtrade/rpc/telegram.py | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a0ffff107..3c0fbeba9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -460,3 +460,18 @@ class RPC(object): 'whitelist': self._freqtrade.active_pair_whitelist } return res + + def _rpc_edge(self) -> Dict: + """ Returns information related to Edge """ + if not self._freqtrade.edge: + raise RPCException(f'Edge is not enabled.') + + for pair in self._freqtrade.edge._cached_pairs: + res = { + 'pair': pair, + 'winrate': self._freqtrade.edge._cached_pairs[pair].winrate, + 'expectancy': self._freqtrade.edge._cached_pairs[pair].expectancy, + 'stoploss': self._freqtrade.edge._cached_pairs[pair].stoploss, + } + + return res diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6771ec803..edc380f92 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -470,6 +470,29 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e), bot=bot) + @authorized_only + def _edge(self, bot: Bot, update: Update) -> None: + """ + Handler for /edge + Shows informaton related to Edge + """ + try: + edge_pairs = self._rpc_edge() + edge_pairs_tab = tabulate(edge_pairs, + headers=[ + 'Pair', + f'Winrate', + f'Expectancy', + f'Stoploss' + ], + tablefmt='simple') + + message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' + self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e), bot=bot) + + @authorized_only def _help(self, bot: Bot, update: Update) -> None: """ From a8be277ca022ddf9f41e32a904e3490101e9b6c2 Mon Sep 17 00:00:00 2001 From: Misagh Date: Sun, 24 Mar 2019 22:56:42 +0100 Subject: [PATCH 065/928] cached pairs iteration fixed + help added --- freqtrade/rpc/rpc.py | 16 ++++++++-------- freqtrade/rpc/telegram.py | 13 ++++--------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3c0fbeba9..fb06c7d86 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -466,12 +466,12 @@ class RPC(object): if not self._freqtrade.edge: raise RPCException(f'Edge is not enabled.') - for pair in self._freqtrade.edge._cached_pairs: - res = { - 'pair': pair, - 'winrate': self._freqtrade.edge._cached_pairs[pair].winrate, - 'expectancy': self._freqtrade.edge._cached_pairs[pair].expectancy, - 'stoploss': self._freqtrade.edge._cached_pairs[pair].stoploss, + return [ + { + 'Pair': pair, + 'Winrate': self._freqtrade.edge._cached_pairs[pair].winrate, + 'Expectancy': self._freqtrade.edge._cached_pairs[pair].expectancy, + 'Stoploss': self._freqtrade.edge._cached_pairs[pair].stoploss, } - - return res + for pair in self._freqtrade.edge._cached_pairs + ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index edc380f92..64c35078d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,6 +93,7 @@ class Telegram(RPC): CommandHandler('reload_conf', self._reload_conf), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), + CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), ] @@ -478,15 +479,8 @@ class Telegram(RPC): """ try: edge_pairs = self._rpc_edge() - edge_pairs_tab = tabulate(edge_pairs, - headers=[ - 'Pair', - f'Winrate', - f'Expectancy', - f'Stoploss' - ], - tablefmt='simple') - + print(edge_pairs) + edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) except RPCException as e: @@ -520,6 +514,7 @@ class Telegram(RPC): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ + "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" From fd7278517dd7613c9ce7f0f4ba72b24ad4ed21c9 Mon Sep 17 00:00:00 2001 From: Misagh Date: Mon, 25 Mar 2019 09:48:41 +0100 Subject: [PATCH 066/928] using items() --- freqtrade/rpc/rpc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fb06c7d86..6416a97eb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -468,10 +468,10 @@ class RPC(object): return [ { - 'Pair': pair, - 'Winrate': self._freqtrade.edge._cached_pairs[pair].winrate, - 'Expectancy': self._freqtrade.edge._cached_pairs[pair].expectancy, - 'Stoploss': self._freqtrade.edge._cached_pairs[pair].stoploss, + 'Pair': k, + 'Winrate': v.winrate, + 'Expectancy': v.expectancy, + 'Stoploss': v.stoploss, } - for pair in self._freqtrade.edge._cached_pairs + for k, v in self._freqtrade.edge._cached_pairs.items() ] From 66f1e0f4cd82597305bfa5ff74d8224973fd1338 Mon Sep 17 00:00:00 2001 From: Misagh Date: Mon, 25 Mar 2019 10:25:07 +0100 Subject: [PATCH 067/928] help added --- docs/telegram-usage.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 2f7e9ada8..6788bec90 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -30,6 +30,7 @@ official commands. You can ask at any moment for help with `/help`. | `/daily ` | 7 | Shows profit or loss per day, over the last n days | `/whitelist` | | Show the current whitelist | `/blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. +| `/edge` | | Show validated pairs by Edge if it is enabled. | `/help` | | Show help message | `/version` | | Show version @@ -55,7 +56,7 @@ Prevents the bot from opening new trades by temporarily setting "max_open_trades After this, give the bot time to close off open trades (can be checked via `/status table`). Once all positions are sold, run `/stop` to completely stop the bot. -`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. +`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. !!! warning The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. @@ -166,7 +167,7 @@ Day Profit BTC Profit USD Shows the current whitelist -> Using whitelist `StaticPairList` with 22 pairs +> Using whitelist `StaticPairList` with 22 pairs > `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` ### /blacklist [pair] @@ -176,7 +177,7 @@ If Pair is set, then this pair will be added to the pairlist. Also supports multiple pairs, seperated by a space. Use `/reload_conf` to reset the blacklist. -> Using blacklist `StaticPairList` with 2 pairs +> Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. ### /version From 904b3008a97eda34648809ef1906eca7d7887adf Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 Mar 2019 13:36:04 +0100 Subject: [PATCH 068/928] Update ccxt from 1.18.395 to 1.18.398 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73c65aff6..feed42b52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.395 +ccxt==1.18.398 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From fe9322ecd53eea54ec7a97fbe31f82df4fc96a14 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 Mar 2019 13:36:06 +0100 Subject: [PATCH 069/928] Update pytest-mock from 1.10.1 to 1.10.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0aaf9461..56d7964c3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==4.3.1 -pytest-mock==1.10.1 +pytest-mock==1.10.2 pytest-asyncio==0.10.0 pytest-cov==2.6.1 coveralls==1.7.0 From c8b0c9af0a5e28b18d7a8f604a5f3d3db836e992 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 25 Mar 2019 17:45:03 +0300 Subject: [PATCH 070/928] Worker moved to new worker.py --- freqtrade/freqtradebot.py | 2 +- freqtrade/main.py | 190 ++----------------------------------- freqtrade/worker.py | 192 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 185 deletions(-) create mode 100755 freqtrade/worker.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 784c0b938..94c1bca8a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListReso from freqtrade.state import State from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.wallets import Wallets -from freqtrade.main import Worker +from freqtrade.worker import Worker logger = logging.getLogger(__name__) diff --git a/freqtrade/main.py b/freqtrade/main.py index 9331206fc..877e2921d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -5,18 +5,14 @@ Read the documentation to know what cli arguments you need. """ import logging import sys -import time -import traceback from argparse import Namespace -from typing import Any, Callable, List -import sdnotify +from typing import List -from freqtrade import (constants, OperationalException, TemporaryError, - __version__) +from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration, set_loggers -from freqtrade.state import State -from freqtrade.rpc import RPCMessageType +from freqtrade.configuration import set_loggers +from freqtrade.worker import Worker + logger = logging.getLogger('freqtrade') @@ -30,7 +26,7 @@ def main(sysargv: List[str]) -> None: sysargv, 'Free, open source crypto trading bot' ) - args = arguments.get_parsed_arg() + args: Namespace = arguments.get_parsed_arg() # A subcommand has been issued. # Means if Backtesting or Hyperopt have been called we exit the bot @@ -59,180 +55,6 @@ def main(sysargv: List[str]) -> None: sys.exit(return_code) -class Worker(object): - """ - Freqtradebot worker class - """ - - def __init__(self, args: Namespace) -> None: - """ - Init all variables and objects the bot needs to work - """ - logger.info('Starting worker %s', __version__) - - self._args = args - self._init() - - # Tell systemd that we completed initialization phase - if self._sd_notify: - logger.debug("sd_notify: READY=1") - self._sd_notify.notify("READY=1") - - def _init(self): - """ - Also called from the _reconfigure() method. - """ - # Load configuration - self._config = Configuration(self._args, None).get_config() - - # Import freqtradebot here in order to avoid python circular - # dependency error, damn! - from freqtrade.freqtradebot import FreqtradeBot - - # Init the instance of the bot - self.freqtrade = FreqtradeBot(self._config, self) - - # Set initial bot state - initial_state = self._config.get('initial_state') - if initial_state: - self._state = State[initial_state.upper()] - else: - self._state = State.STOPPED - - self._throttle_secs = self._config.get('internals', {}).get( - 'process_throttle_secs', - constants.PROCESS_THROTTLE_SECS - ) - - self._sd_notify = sdnotify.SystemdNotifier() if \ - self._config.get('internals', {}).get('sd_notify', False) else None - - @property - def state(self) -> State: - return self._state - - @state.setter - def state(self, value: State): - self._state = value - - def run(self): - state = None - while True: - state = self._worker(old_state=state, throttle_secs=self._throttle_secs) - if state == State.RELOAD_CONF: - self.freqtrade = self._reconfigure() - - def _worker(self, old_state: State, throttle_secs: float) -> State: - """ - Trading routine that must be run at each loop - :param old_state: the previous service state from the previous call - :return: current service state - """ - state = self._state - - # Log state transition - if state != old_state: - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'{state.name.lower()}' - }) - logger.info('Changing state to: %s', state.name) - if state == State.RUNNING: - self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists) - - if state == State.STOPPED: - # Ping systemd watchdog before sleeping in the stopped state - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.") - - time.sleep(throttle_secs) - - elif state == State.RUNNING: - # Ping systemd watchdog before throttling - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.") - - self._throttle(func=self._process, min_secs=throttle_secs) - - return state - - def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: - """ - Throttles the given callable that it - takes at least `min_secs` to finish execution. - :param func: Any callable - :param min_secs: minimum execution time in seconds - :return: Any - """ - start = time.time() - result = func(*args, **kwargs) - end = time.time() - duration = max(min_secs - (end - start), 0.0) - logger.debug('Throttling %s for %.2f seconds', func.__name__, duration) - time.sleep(duration) - return result - - def _process(self) -> bool: - state_changed = False - try: - state_changed = self.freqtrade.process() - - except TemporaryError as error: - logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") - time.sleep(constants.RETRY_TIMEOUT) - except OperationalException: - tb = traceback.format_exc() - hint = 'Issue `/start` if you think it is safe to restart.' - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'OperationalException:\n```\n{tb}```{hint}' - }) - logger.exception('OperationalException. Stopping trader ...') - self.state = State.STOPPED - return state_changed - - def _reconfigure(self): - """ - Cleans up current freqtradebot instance, reloads the configuration and - returns the new instance - """ - # Tell systemd that we initiated reconfiguration - if self._sd_notify: - logger.debug("sd_notify: RELOADING=1") - self._sd_notify.notify("RELOADING=1") - - # Clean up current freqtrade modules - self.freqtrade.cleanup() - - # Load and validate config and create new instance of the bot - self._init() - - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'config reloaded' - }) - - # Tell systemd that we completed reconfiguration - if self._sd_notify: - logger.debug("sd_notify: READY=1") - self._sd_notify.notify("READY=1") - - def exit(self): - # Tell systemd that we are exiting now - if self._sd_notify: - logger.debug("sd_notify: STOPPING=1") - self._sd_notify.notify("STOPPING=1") - - if self.freqtrade: - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'process died' - }) - self.freqtrade.cleanup() - - if __name__ == '__main__': set_loggers() main(sys.argv[1:]) diff --git a/freqtrade/worker.py b/freqtrade/worker.py new file mode 100755 index 000000000..9a7e67424 --- /dev/null +++ b/freqtrade/worker.py @@ -0,0 +1,192 @@ +""" +Main Freqtrade worker class. +""" +import logging +import time +import traceback +from argparse import Namespace +from typing import Any, Callable +import sdnotify + +from freqtrade import (constants, OperationalException, TemporaryError, + __version__) +from freqtrade.configuration import Configuration +from freqtrade.state import State +from freqtrade.rpc import RPCMessageType + + +logger = logging.getLogger(__name__) + + +class Worker(object): + """ + Freqtradebot worker class + """ + + def __init__(self, args: Namespace) -> None: + """ + Init all variables and objects the bot needs to work + """ + logger.info('Starting worker %s', __version__) + + self._args = args + self._init() + + # Tell systemd that we completed initialization phase + if self._sd_notify: + logger.debug("sd_notify: READY=1") + self._sd_notify.notify("READY=1") + + def _init(self): + """ + Also called from the _reconfigure() method. + """ + # Load configuration + self._config = Configuration(self._args, None).get_config() + + # Import freqtradebot here in order to avoid python circular + # dependency error, damn! + from freqtrade.freqtradebot import FreqtradeBot + + # Init the instance of the bot + self.freqtrade = FreqtradeBot(self._config, self) + + # Set initial bot state + initial_state = self._config.get('initial_state') + if initial_state: + self._state = State[initial_state.upper()] + else: + self._state = State.STOPPED + + self._throttle_secs = self._config.get('internals', {}).get( + 'process_throttle_secs', + constants.PROCESS_THROTTLE_SECS + ) + + self._sd_notify = sdnotify.SystemdNotifier() if \ + self._config.get('internals', {}).get('sd_notify', False) else None + + @property + def state(self) -> State: + return self._state + + @state.setter + def state(self, value: State): + self._state = value + + def run(self): + state = None + while True: + state = self._worker(old_state=state, throttle_secs=self._throttle_secs) + if state == State.RELOAD_CONF: + self.freqtrade = self._reconfigure() + + def _worker(self, old_state: State, throttle_secs: float) -> State: + """ + Trading routine that must be run at each loop + :param old_state: the previous service state from the previous call + :return: current service state + """ + state = self._state + + # Log state transition + if state != old_state: + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'{state.name.lower()}' + }) + logger.info('Changing state to: %s', state.name) + if state == State.RUNNING: + self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists) + + if state == State.STOPPED: + # Ping systemd watchdog before sleeping in the stopped state + if self._sd_notify: + logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.") + self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.") + + time.sleep(throttle_secs) + + elif state == State.RUNNING: + # Ping systemd watchdog before throttling + if self._sd_notify: + logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.") + self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.") + + self._throttle(func=self._process, min_secs=throttle_secs) + + return state + + def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: + """ + Throttles the given callable that it + takes at least `min_secs` to finish execution. + :param func: Any callable + :param min_secs: minimum execution time in seconds + :return: Any + """ + start = time.time() + result = func(*args, **kwargs) + end = time.time() + duration = max(min_secs - (end - start), 0.0) + logger.debug('Throttling %s for %.2f seconds', func.__name__, duration) + time.sleep(duration) + return result + + def _process(self) -> bool: + state_changed = False + try: + state_changed = self.freqtrade.process() + + except TemporaryError as error: + logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") + time.sleep(constants.RETRY_TIMEOUT) + except OperationalException: + tb = traceback.format_exc() + hint = 'Issue `/start` if you think it is safe to restart.' + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'OperationalException:\n```\n{tb}```{hint}' + }) + logger.exception('OperationalException. Stopping trader ...') + self.state = State.STOPPED + return state_changed + + def _reconfigure(self): + """ + Cleans up current freqtradebot instance, reloads the configuration and + returns the new instance + """ + # Tell systemd that we initiated reconfiguration + if self._sd_notify: + logger.debug("sd_notify: RELOADING=1") + self._sd_notify.notify("RELOADING=1") + + # Clean up current freqtrade modules + self.freqtrade.cleanup() + + # Load and validate config and create new instance of the bot + self._init() + + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'config reloaded' + }) + + # Tell systemd that we completed reconfiguration + if self._sd_notify: + logger.debug("sd_notify: READY=1") + self._sd_notify.notify("READY=1") + + def exit(self): + # Tell systemd that we are exiting now + if self._sd_notify: + logger.debug("sd_notify: STOPPING=1") + self._sd_notify.notify("STOPPING=1") + + if self.freqtrade: + self.freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'process died' + }) + self.freqtrade.cleanup() From bd29b7d03136b3905152b079f872d030284e7a3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:21:05 +0100 Subject: [PATCH 071/928] Test that dataprovider is loaded to strategy --- freqtrade/tests/test_freqtradebot.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index e4f0415f7..fa50b36e6 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -13,12 +13,14 @@ import requests from freqtrade import (DependencyException, OperationalException, TemporaryError, constants) +from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import SellType, SellCheckTuple -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange, patch_edge, patch_wallet +from freqtrade.strategy.interface import SellCheckTuple, SellType +from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, + patch_exchange, patch_wallet) # Functions for recurrent object patching @@ -88,6 +90,10 @@ def test_worker_running(mocker, default_conf, caplog) -> None: assert state is State.RUNNING assert log_has('Changing state to: RUNNING', caplog.record_tuples) assert mock_throttle.call_count == 1 + # Check strategy is loaded, and received a dataprovider object + assert freqtrade.strategy + assert freqtrade.strategy.dp + assert isinstance(freqtrade.strategy.dp, DataProvider) def test_worker_stopped(mocker, default_conf, caplog) -> None: From 226fc3d99b97b9a62a2c2a8246c60d8753c367e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:23:14 +0100 Subject: [PATCH 072/928] Check that dataprovider is part of strategy --- freqtrade/tests/optimize/test_backtesting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 40754cfbc..d0b21b8f4 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -16,6 +16,7 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.dataprovider import DataProvider from freqtrade.optimize import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) @@ -346,6 +347,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.advise_buy) assert callable(backtesting.advise_sell) + assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 assert not backtesting.strategy.order_types["stoploss_on_exchange"] From 0ae81d41156f84828d9bab0ec22f0686fb4d75a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Mar 2019 15:24:47 +0100 Subject: [PATCH 073/928] Provide dataprovider access during backtesting --- freqtrade/optimize/backtesting.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 031b490c8..7c98bd3a7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,6 +18,7 @@ from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history +from freqtrade.data.dataprovider import DataProvider from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -64,6 +65,13 @@ class Backtesting(object): self.config['exchange']['uid'] = '' self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] + + exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() + self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.fee = self.exchange.get_fee() + self.dataprovider = DataProvider(self.config, self.exchange) + IStrategy.dp = self.dataprovider + if self.config.get('strategy_list', None): # Force one interval self.ticker_interval = str(self.config.get('ticker_interval')) @@ -78,15 +86,13 @@ class Backtesting(object): self.strategylist.append(StrategyResolver(self.config).strategy) # Load one strategy self._set_strategy(self.strategylist[0]) - exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange - self.fee = self.exchange.get_fee() def _set_strategy(self, strategy): """ Load strategy into backtesting """ self.strategy = strategy + self.ticker_interval = self.config.get('ticker_interval') self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe From 4cf7282027e56277f79a317a8d8a7aa3092f17be Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Mar 2019 19:31:10 +0100 Subject: [PATCH 074/928] Update dataprovider docs --- docs/bot-optimization.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index c01a0a03d..ae6377bf5 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -260,12 +260,9 @@ class Awesomestrategy(IStrategy): The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. -!!! Note - The DataProvier is currently not available during backtesting / hyperopt, but this is planned for the future. - All methods return `None` in case of failure (do not raise an exception). -Please always check if the `DataProvider` is available to avoid failures during backtesting. +Please always the mode of operation to select the correct method to get data (samples see below). #### Possible options for DataProvider @@ -292,6 +289,9 @@ if self.dp: Be carefull when using dataprovider in backtesting. `historic_ohlcv()` 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 + This option should only be used in `populate_indicators()` - since it pulls the historic data from disk each time, which would be a huge performance penalty during hyperopt. + #### Available Pairs ``` python From f26ed1c8c1503457293dbdae6f0fb53c8251a317 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Mar 2019 19:40:21 +0100 Subject: [PATCH 075/928] Check if added pair has correct stake-currency --- freqtrade/rpc/rpc.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index cacca4e3c..a5601a502 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -464,10 +464,14 @@ class RPC(object): def _rpc_blacklist(self, add: List[str]) -> Dict: """ Returns the currently active blacklist""" if add: - self._freqtrade.pairlists.blacklist.extend(add) + stake_currency = self._freqtrade.config.get('stake_currency') + for pair in add: + if (pair.endswith(stake_currency) + and pair not in self._freqtrade.pairlists.blacklist): + self._freqtrade.pairlists.blacklist.append(pair) res = {'method': self._freqtrade.pairlists.name, 'length': len(self._freqtrade.pairlists.blacklist), - 'blacklist': self._freqtrade.pairlists.blacklist + 'blacklist': self._freqtrade.pairlists.blacklist, } return res From e085fd9e9538c18a6e4e6ea51c0dd151eb31c15e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Mar 2019 19:49:58 +0100 Subject: [PATCH 076/928] Disable dataprovider from hyperopt. Dataprovider uses weak links to initialize, which cannot be pickled, and therefore cannot be used during hyperopt. --- docs/bot-optimization.md | 2 +- freqtrade/optimize/backtesting.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index ae6377bf5..f018d92a7 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -290,7 +290,7 @@ if self.dp: 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 - This option should only be used in `populate_indicators()` - since it pulls the historic data from disk each time, which would be a huge performance penalty during hyperopt. + This option cannot currently be used during hyperopt. #### Available Pairs diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7c98bd3a7..293511fc0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -69,8 +69,10 @@ class Backtesting(object): exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() self.exchange = ExchangeResolver(exchange_name, self.config).exchange self.fee = self.exchange.get_fee() - self.dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = self.dataprovider + + if self.config.get('runmode') != RunMode.HYPEROPT: + self.dataprovider = DataProvider(self.config, self.exchange) + IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): # Force one interval From 85ac99aee021522e60f2b1823b2c6a2998d2a144 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 21 Mar 2019 19:11:47 +0100 Subject: [PATCH 077/928] move exchange urls to constants --- freqtrade/constants.py | 6 ++++++ freqtrade/exchange/exchange.py | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 02062acc4..bd021c327 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,6 +22,12 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 +# Urls to exchange markets, insert quote and base with .format() +_EXCHANGE_URLS = { + "Bittrex": '/Market/Index?MarketName={quote}-{base}', + "Binance": '/tradeDetail.html?symbol={base}_{quote}', +} + TICKER_INTERVAL_MINUTES = { '1m': 1, '3m': 3, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea8bcfac1..7781c85e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -21,13 +21,6 @@ logger = logging.getLogger(__name__) API_RETRY_COUNT = 4 -# Urls to exchange markets, insert quote and base with .format() -_EXCHANGE_URLS = { - ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}', - ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}', -} - - def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) From 4005b8d1d254f94840cc00034521e2c7421c17a5 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 21 Mar 2019 19:12:15 +0100 Subject: [PATCH 078/928] remove the if condition for binance --- freqtrade/exchange/binance.py | 6 ++++++ freqtrade/exchange/exchange.py | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 127f4e916..1d633be2b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -24,3 +24,9 @@ class Binance(Exchange): limit = min(list(filter(lambda x: limit <= x, limit_range))) return super().get_order_book(pair, limit) + + def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: + """ + Checks if order time in force configured in strategy/config are supported + """ + pass diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7781c85e4..d422e2bbf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -13,7 +13,7 @@ import ccxt import ccxt.async_support as ccxt_async from pandas import DataFrame -from freqtrade import constants, OperationalException, DependencyException, TemporaryError +from freqtrade import constants, DependencyException, OperationalException, TemporaryError from freqtrade.data.converter import parse_ticker_dataframe logger = logging.getLogger(__name__) @@ -269,9 +269,8 @@ class Exchange(object): Checks if order time in force configured in strategy/config are supported """ if any(v != 'gtc' for k, v in order_time_in_force.items()): - if self.name != 'Binance': - raise OperationalException( - f'Time in force policies are not supporetd for {self.name} yet.') + raise OperationalException( + f'Time in force policies are not supporetd for {self.name} yet.') def exchange_has(self, endpoint: str) -> bool: """ From 8dea640e9a671b315675f27557361e46e5eee084 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Mon, 25 Mar 2019 23:58:02 +0100 Subject: [PATCH 079/928] remove exchange urls --- freqtrade/constants.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bd021c327..02062acc4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,12 +22,6 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 -# Urls to exchange markets, insert quote and base with .format() -_EXCHANGE_URLS = { - "Bittrex": '/Market/Index?MarketName={quote}-{base}', - "Binance": '/tradeDetail.html?symbol={base}_{quote}', -} - TICKER_INTERVAL_MINUTES = { '1m': 1, '3m': 3, From e15f2ef11ad07c4f71e5086d0edaad1c22c80e11 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Tue, 26 Mar 2019 00:49:39 +0100 Subject: [PATCH 080/928] add order_time_in_force in _ft_has and revert binance --- freqtrade/exchange/binance.py | 7 +------ freqtrade/exchange/exchange.py | 6 ++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 1d633be2b..18e754e3f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -11,6 +11,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, + "order_time_in_force": ['gtc', 'fok', 'ioc'], } def get_order_book(self, pair: str, limit: int = 100) -> dict: @@ -24,9 +25,3 @@ class Binance(Exchange): limit = min(list(filter(lambda x: limit <= x, limit_range))) return super().get_order_book(pair, limit) - - def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: - """ - Checks if order time in force configured in strategy/config are supported - """ - pass diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d422e2bbf..2ec5c0e25 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -65,8 +65,9 @@ class Exchange(object): # Dict to specify which options each exchange implements # TODO: this should be merged with attributes from subclasses # To avoid having to copy/paste this to all subclasses. - _ft_has = { + _ft_has: Dict = { "stoploss_on_exchange": False, + "order_time_in_force": ["gtc"], } def __init__(self, config: dict) -> None: @@ -268,7 +269,8 @@ class Exchange(object): """ Checks if order time in force configured in strategy/config are supported """ - if any(v != 'gtc' for k, v in order_time_in_force.items()): + if any(v not in self._ft_has["order_time_in_force"] + for k, v in order_time_in_force.items()): raise OperationalException( f'Time in force policies are not supporetd for {self.name} yet.') From 5161e1abb3fd2fa5d5ea69751447d14cd3c58007 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Mar 2019 11:07:02 +0300 Subject: [PATCH 081/928] Allow to pass config into worker, as it's used in the tests --- freqtrade/freqtradebot.py | 8 ++++++-- freqtrade/worker.py | 24 ++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 94c1bca8a..7c92ac29a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -34,7 +34,7 @@ class FreqtradeBot(object): This is from here the bot start its logic. """ - def __init__(self, config: Dict[str, Any], worker: Worker) -> None: + def __init__(self, config: Dict[str, Any], worker: Optional[Worker] = None) -> None: """ Init all variables and objects the bot needs to work :param config: configuration dict, you can use Configuration.get_config() @@ -45,7 +45,7 @@ class FreqtradeBot(object): # Init objects self.config = config - self._worker: Worker = worker + self._worker = worker self.strategy: IStrategy = StrategyResolver(self.config).strategy @@ -75,10 +75,14 @@ class FreqtradeBot(object): @property def state(self) -> State: + if self._worker is None: + raise DependencyException("No Worker is available") return self._worker.state @state.setter def state(self, value: State): + if self._worker is None: + raise DependencyException("No Worker is available") self._worker.state = value def cleanup(self) -> None: diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 9a7e67424..a6fba1460 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -5,7 +5,7 @@ import logging import time import traceback from argparse import Namespace -from typing import Any, Callable +from typing import Any, Callable, Optional import sdnotify from freqtrade import (constants, OperationalException, TemporaryError, @@ -23,26 +23,28 @@ class Worker(object): Freqtradebot worker class """ - def __init__(self, args: Namespace) -> None: + def __init__(self, args: Optional[Namespace] = None, config = None) -> None: """ Init all variables and objects the bot needs to work """ logger.info('Starting worker %s', __version__) self._args = args - self._init() + self._config = config + self._init(False) # Tell systemd that we completed initialization phase if self._sd_notify: logger.debug("sd_notify: READY=1") self._sd_notify.notify("READY=1") - def _init(self): + def _init(self, reconfig: bool): """ - Also called from the _reconfigure() method. + Also called from the _reconfigure() method (with reconfig=True). """ - # Load configuration - self._config = Configuration(self._args, None).get_config() + if reconfig or self._config is None: + # Load configuration + self._config = Configuration(self._args, None).get_config() # Import freqtradebot here in order to avoid python circular # dependency error, damn! @@ -77,17 +79,19 @@ class Worker(object): def run(self): state = None while True: - state = self._worker(old_state=state, throttle_secs=self._throttle_secs) + state = self._worker(old_state=state) if state == State.RELOAD_CONF: self.freqtrade = self._reconfigure() - def _worker(self, old_state: State, throttle_secs: float) -> State: + def _worker(self, old_state: State, throttle_secs: Optional[float] = None) -> State: """ Trading routine that must be run at each loop :param old_state: the previous service state from the previous call :return: current service state """ state = self._state + if throttle_secs is None: + throttle_secs = self._throttle_secs # Log state transition if state != old_state: @@ -166,7 +170,7 @@ class Worker(object): self.freqtrade.cleanup() # Load and validate config and create new instance of the bot - self._init() + self._init(True) self.freqtrade.rpc.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, From 5ccd618189f2bc3a303148a487f114d4b5df66be Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Mar 2019 11:07:24 +0300 Subject: [PATCH 082/928] tests adjusted --- freqtrade/tests/conftest.py | 11 +- freqtrade/tests/rpc/test_rpc.py | 74 ++++++++++--- freqtrade/tests/rpc/test_rpc_telegram.py | 134 +++++++++++++++-------- freqtrade/tests/test_freqtradebot.py | 93 ++++++++++------ freqtrade/tests/test_main.py | 45 +++----- 5 files changed, 228 insertions(+), 129 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 26262cb4b..772602f6d 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -15,6 +15,7 @@ from freqtrade import constants from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.edge import Edge, PairInfo +from freqtrade.worker import Worker from freqtrade.freqtradebot import FreqtradeBot from freqtrade.resolvers import ExchangeResolver @@ -88,7 +89,7 @@ def get_patched_edge(mocker, config) -> Edge: # Functions for recurrent object patching -def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: +def patch_freqtradebot(mocker, config) -> None: """ This function patch _init_modules() to not call dependencies :param mocker: a Mocker object to apply patches @@ -102,9 +103,17 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) + +def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: + patch_freqtradebot(mocker, config) return FreqtradeBot(config) +def get_patched_worker(mocker, config) -> Worker: + patch_freqtradebot(mocker, config) + return Worker(args=None, config=config) + + def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None: """ Mocker to coinmarketcap to speed up tests diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index baddc0685..69b428693 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -8,6 +8,7 @@ import pytest from numpy import isnan from freqtrade import TemporaryError, DependencyException +from freqtrade.worker import Worker from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException @@ -37,7 +38,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -93,7 +96,9 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -129,7 +134,9 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -183,7 +190,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -271,7 +280,9 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -340,7 +351,9 @@ def test_rpc_balance_handle(default_conf, mocker): get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() @@ -368,7 +381,9 @@ def test_rpc_start(mocker, default_conf) -> None: get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -391,7 +406,9 @@ def test_rpc_stop(mocker, default_conf) -> None: get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -415,7 +432,9 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -447,7 +466,9 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -539,7 +560,9 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -575,7 +598,9 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -605,7 +630,9 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order buy=buy_mm ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -630,7 +657,10 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order # Test not buying default_conf['stake_amount'] = 0.0000001 - freqtradebot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'TKN/BTC' @@ -645,7 +675,9 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -658,7 +690,9 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -671,7 +705,9 @@ def test_rpc_whitelist(mocker, default_conf) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() assert ret['method'] == 'StaticPairList' @@ -687,7 +723,9 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() assert ret['method'] == 'VolumePairList' diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 8e8d1f1bb..1452267ea 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -13,13 +13,14 @@ from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ +from freqtrade.worker import Worker from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.strategy.interface import SellType from freqtrade.state import State -from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, +from freqtrade.tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, patch_exchange) from freqtrade.tests.test_freqtradebot import patch_get_signal from freqtrade.tests.conftest import patch_coinmarketcap @@ -98,7 +99,10 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False - bot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + bot = worker.freqtrade + patch_get_signal(bot, (True, False)) dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) @@ -125,7 +129,10 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False - bot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + bot = worker.freqtrade + patch_get_signal(bot, (True, False)) dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) @@ -153,7 +160,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: default_conf['telegram']['enabled'] = False - bot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + bot = worker.freqtrade + patch_get_signal(bot, (True, False)) dummy = DummyCls(bot) @@ -208,7 +217,9 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -244,19 +255,21 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED + worker.state = State.STOPPED # Status is also enabled when stopped telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - freqtradebot.state = State.RUNNING + worker.state = State.RUNNING telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] @@ -290,19 +303,22 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) default_conf['stake_amount'] = 15.0 - freqtradebot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED + worker.state = State.STOPPED # Status table is also enabled when stopped telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active order' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - freqtradebot.state = State.RUNNING + worker.state = State.RUNNING telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active order' in msg_mock.call_args_list[0][0][0] @@ -344,7 +360,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -409,13 +427,15 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Try invalid data msg_mock.reset_mock() - freqtradebot.state = State.RUNNING + worker.state = State.RUNNING update.message.text = '/daily -2' telegram._daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -423,7 +443,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() - freqtradebot.state = State.RUNNING + worker.state = State.RUNNING update.message.text = '/daily today' telegram._daily(bot=MagicMock(), update=update) assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] @@ -448,7 +468,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -593,13 +615,14 @@ def test_start_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED - assert freqtradebot.state == State.STOPPED + worker.state = State.STOPPED + assert worker.state == State.STOPPED telegram._start(bot=MagicMock(), update=update) - assert freqtradebot.state == State.RUNNING + assert worker.state == State.RUNNING assert msg_mock.call_count == 1 @@ -611,13 +634,14 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade telegram = Telegram(freqtradebot) - freqtradebot.state = State.RUNNING - assert freqtradebot.state == State.RUNNING + worker.state = State.RUNNING + assert worker.state == State.RUNNING telegram._start(bot=MagicMock(), update=update) - assert freqtradebot.state == State.RUNNING + assert worker.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] @@ -631,13 +655,14 @@ def test_stop_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade telegram = Telegram(freqtradebot) - freqtradebot.state = State.RUNNING - assert freqtradebot.state == State.RUNNING + worker.state = State.RUNNING + assert worker.state == State.RUNNING telegram._stop(bot=MagicMock(), update=update) - assert freqtradebot.state == State.STOPPED + assert worker.state == State.STOPPED assert msg_mock.call_count == 1 assert 'stopping trader' in msg_mock.call_args_list[0][0][0] @@ -651,13 +676,14 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED - assert freqtradebot.state == State.STOPPED + worker.state = State.STOPPED + assert worker.state == State.STOPPED telegram._stop(bot=MagicMock(), update=update) - assert freqtradebot.state == State.STOPPED + assert worker.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] @@ -691,13 +717,14 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade telegram = Telegram(freqtradebot) - freqtradebot.state = State.RUNNING - assert freqtradebot.state == State.RUNNING + worker.state = State.RUNNING + assert worker.state == State.RUNNING telegram._reload_conf(bot=MagicMock(), update=update) - assert freqtradebot.state == State.RELOAD_CONF + assert worker.state == State.RELOAD_CONF assert msg_mock.call_count == 1 assert 'reloading config' in msg_mock.call_args_list[0][0][0] @@ -717,7 +744,9 @@ def test_forcesell_handle(default_conf, update, ticker, fee, validate_pairs=MagicMock(return_value={}) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -768,7 +797,9 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, validate_pairs=MagicMock(return_value={}) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -822,7 +853,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker validate_pairs=MagicMock(return_value={}) ) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -865,7 +898,9 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: ) patch_exchange(mocker) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -907,7 +942,9 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - freqtradebot = FreqtradeBot(default_conf) + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -941,7 +978,10 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non markets=PropertyMock(markets), validate_pairs=MagicMock(return_value={}) ) - freqtradebot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -970,7 +1010,10 @@ def test_performance_handle(default_conf, update, ticker, fee, validate_pairs=MagicMock(return_value={}) ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -1009,7 +1052,10 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non markets=PropertyMock(markets) ) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - freqtradebot = FreqtradeBot(default_conf) + + worker = Worker(args=None, config=default_conf) + freqtradebot = worker.freqtrade + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index e4f0415f7..276cdc0b9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -13,6 +13,7 @@ import requests from freqtrade import (DependencyException, OperationalException, TemporaryError, constants) +from freqtrade.worker import Worker from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -22,9 +23,9 @@ from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange, patch_ # Functions for recurrent object patching -def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: +def patch_freqtradebot(mocker, config) -> None: """ - This function patch _init_modules() to not call dependencies + This function patches _init_modules() to not call dependencies :param mocker: a Mocker object to apply patches :param config: Config to pass to the bot :return: None @@ -33,9 +34,29 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) patch_exchange(mocker) + +def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: FreqtradeBot + """ + patch_freqtradebot(mocker, config) return FreqtradeBot(config) +def get_patched_worker(mocker, config) -> Worker: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: FreqtradeBot + """ + patch_freqtradebot(mocker, config) + return Worker(args=None, config=config) + + def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: """ :param mocker: mocker to patch IStrategy class @@ -61,12 +82,12 @@ def patch_RPCManager(mocker) -> MagicMock: def test_freqtradebot(mocker, default_conf, markets) -> None: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.state is State.RUNNING + worker = get_patched_worker(mocker, default_conf) + assert worker.state is State.RUNNING default_conf.pop('initial_state') - freqtrade = FreqtradeBot(default_conf) - assert freqtrade.state is State.STOPPED + worker = Worker(args=None, config=default_conf) + assert worker.state is State.STOPPED def test_cleanup(mocker, default_conf, caplog) -> None: @@ -80,11 +101,11 @@ def test_cleanup(mocker, default_conf, caplog) -> None: def test_worker_running(mocker, default_conf, caplog) -> None: mock_throttle = MagicMock() - mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - freqtrade = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) - state = freqtrade.worker(old_state=None) + state = worker._worker(old_state=None) assert state is State.RUNNING assert log_has('Changing state to: RUNNING', caplog.record_tuples) assert mock_throttle.call_count == 1 @@ -92,12 +113,12 @@ def test_worker_running(mocker, default_conf, caplog) -> None: def test_worker_stopped(mocker, default_conf, caplog) -> None: mock_throttle = MagicMock() - mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) mock_sleep = mocker.patch('time.sleep', return_value=None) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - freqtrade.state = State.STOPPED - state = freqtrade.worker(old_state=State.RUNNING) + worker = get_patched_worker(mocker, default_conf) + worker.state = State.STOPPED + state = worker._worker(old_state=State.RUNNING) assert state is State.STOPPED assert log_has('Changing state to: STOPPED', caplog.record_tuples) assert mock_throttle.call_count == 0 @@ -109,17 +130,17 @@ def test_throttle(mocker, default_conf, caplog) -> None: return 42 caplog.set_level(logging.DEBUG) - freqtrade = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) start = time.time() - result = freqtrade._throttle(throttled_func, min_secs=0.1) + result = worker._throttle(throttled_func, min_secs=0.1) end = time.time() assert result == 42 assert end - start > 0.1 assert log_has('Throttling throttled_func for 0.10 seconds', caplog.record_tuples) - result = freqtrade._throttle(throttled_func, min_secs=-1) + result = worker._throttle(throttled_func, min_secs=-1) assert result == 42 @@ -127,12 +148,12 @@ def test_throttle_with_assets(mocker, default_conf) -> None: def throttled_func(nb_assets=-1): return nb_assets - freqtrade = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) - result = freqtrade._throttle(throttled_func, min_secs=0.1, nb_assets=666) + result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666) assert result == 666 - result = freqtrade._throttle(throttled_func, min_secs=0.1) + result = worker._throttle(throttled_func, min_secs=0.1) assert result == -1 @@ -218,7 +239,7 @@ def test_edge_called_in_process(mocker, edge_conf) -> None: freqtrade = FreqtradeBot(edge_conf) freqtrade.pairlists._validate_whitelist = _refresh_whitelist patch_get_signal(freqtrade) - freqtrade._process() + freqtrade.process() assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC'] @@ -652,7 +673,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() + result = freqtrade.process() assert result is True trades = Trade.query.filter(Trade.is_open.is_(True)).all() @@ -683,10 +704,10 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) + worker = get_patched_worker(mocker, default_conf) + patch_get_signal(worker.freqtrade) - result = freqtrade._process() + result = worker.freqtrade.process() assert result is False assert sleep_mock.has_calls() @@ -700,14 +721,14 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=OperationalException) ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) + worker = get_patched_worker(mocker, default_conf) + patch_get_signal(worker.freqtrade) - assert freqtrade.state == State.RUNNING + assert worker.state == State.RUNNING - result = freqtrade._process() + result = worker.freqtrade.process() assert result is False - assert freqtrade.state == State.STOPPED + assert worker.state == State.STOPPED assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] @@ -728,18 +749,18 @@ def test_process_trade_handling( trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades - result = freqtrade._process() + result = freqtrade.process() assert result is True trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 1 - result = freqtrade._process() + result = freqtrade.process() assert result is False def test_process_trade_no_whitelist_pair( default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: - """ Test _process with trade not in pair list """ + """ Test process with trade not in pair list """ patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -776,7 +797,7 @@ def test_process_trade_no_whitelist_pair( )) assert pair not in freqtrade.active_pair_whitelist - result = freqtrade._process() + result = 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)) @@ -806,7 +827,7 @@ def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) freqtrade.strategy.informative_pairs = inf_pairs # patch_get_signal(freqtrade) - freqtrade._process() + freqtrade.process() assert inf_pairs.call_count == 1 assert refresh_mock.call_count == 1 assert ("BTC/ETH", "1m") in refresh_mock.call_args[0][0] @@ -2992,5 +3013,5 @@ def test_startup_messages(default_conf, mocker): 'config': {'number_assets': 20} } mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.state is State.RUNNING + worker = get_patched_worker(mocker, default_conf) + assert worker.state is State.RUNNING diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 188b9b9bd..cce02bf8b 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -43,17 +43,14 @@ def test_main_start_hyperopt(mocker) -> None: def test_main_fatal_exception(mocker, default_conf, caplog) -> None: patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - _init_modules=MagicMock(), - worker=MagicMock(side_effect=Exception), - cleanup=MagicMock(), - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) + mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['-c', 'config.json.example'] @@ -66,17 +63,14 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - _init_modules=MagicMock(), - worker=MagicMock(side_effect=KeyboardInterrupt), - cleanup=MagicMock(), - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) + mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['-c', 'config.json.example'] @@ -89,17 +83,14 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: def test_main_operational_exception(mocker, default_conf, caplog) -> None: patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - _init_modules=MagicMock(), - worker=MagicMock(side_effect=OperationalException('Oh snap!')), - cleanup=MagicMock(), - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) + mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!'))) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['-c', 'config.json.example'] @@ -112,17 +103,14 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_reload_conf(mocker, default_conf, caplog) -> None: patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - _init_modules=MagicMock(), - worker=MagicMock(return_value=State.RELOAD_CONF), - cleanup=MagicMock(), - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) + mocker.patch('freqtrade.worker.Worker._worker', MagicMock(return_value=State.RELOAD_CONF)) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) # Raise exception as side effect to avoid endless loop reconfigure_mock = mocker.patch( @@ -138,17 +126,14 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: def test_reconfigure(mocker, default_conf) -> None: patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - _init_modules=MagicMock(), - worker=MagicMock(side_effect=OperationalException('Oh snap!')), - cleanup=MagicMock(), - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) + mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!'))) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) freqtrade = FreqtradeBot(default_conf) From 8aee009a0acce1d86cd4be84f3696cdb6e654d19 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Mar 2019 12:42:19 +0300 Subject: [PATCH 083/928] test _reconfigure() adjusted --- freqtrade/tests/test_main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index cce02bf8b..96cf2834d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -7,8 +7,9 @@ import pytest from freqtrade import OperationalException from freqtrade.arguments import Arguments +from freqtrade.worker import Worker from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.main import main, Worker +from freqtrade.main import main from freqtrade.state import State from freqtrade.tests.conftest import log_has, patch_exchange @@ -135,7 +136,9 @@ def test_reconfigure(mocker, default_conf) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - freqtrade = FreqtradeBot(default_conf) + args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg() + worker = Worker(args=args, config=default_conf) + freqtrade = worker.freqtrade # Renew mock to return modified data conf = deepcopy(default_conf) @@ -145,11 +148,10 @@ def test_reconfigure(mocker, default_conf) -> None: lambda *args, **kwargs: conf ) + worker._config = conf # reconfigure should return a new instance - freqtrade2 = reconfigure( - freqtrade, - Arguments(['-c', 'config.json.example'], '').get_parsed_arg() - ) + worker._reconfigure() + freqtrade2 = worker.freqtrade # Verify we have a new instance with the new config assert freqtrade is not freqtrade2 From c6d2c1e52095dc3d4fbfaed5daa067b63131ede7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Mar 2019 12:45:19 +0300 Subject: [PATCH 084/928] rest of telegram tests adjusted --- freqtrade/tests/rpc/test_rpc_telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 1452267ea..578e45e24 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -697,7 +697,9 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + worker = get_patched_worker(mocker, default_conf) + freqtradebot = worker.freqtrade + telegram = Telegram(freqtradebot) assert freqtradebot.config['max_open_trades'] != 0 From 1f50bc79bc0ee098f6974ab40c7bfd530ceca914 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 26 Mar 2019 13:37:03 +0100 Subject: [PATCH 085/928] Update ccxt from 1.18.398 to 1.18.400 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index feed42b52..345d32c0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.398 +ccxt==1.18.400 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From f5744cc9bf3d7b0febc35dade539ba1f7b546bab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 26 Mar 2019 18:34:50 +0300 Subject: [PATCH 086/928] fix in the tests --- freqtrade/tests/test_freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 276cdc0b9..ece07f6fd 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -51,7 +51,7 @@ def get_patched_worker(mocker, config) -> Worker: This function patches _init_modules() to not call dependencies :param mocker: a Mocker object to apply patches :param config: Config to pass to the bot - :return: FreqtradeBot + :return: Worker """ patch_freqtradebot(mocker, config) return Worker(args=None, config=config) @@ -707,7 +707,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non worker = get_patched_worker(mocker, default_conf) patch_get_signal(worker.freqtrade) - result = worker.freqtrade.process() + result = worker._process() assert result is False assert sleep_mock.has_calls() @@ -726,7 +726,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> assert worker.state == State.RUNNING - result = worker.freqtrade.process() + result = worker._process() assert result is False assert worker.state == State.STOPPED assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] From b2c2b42408753c0d2a976fbeb91aac094fbeef0f Mon Sep 17 00:00:00 2001 From: Gianluca Puglia Date: Tue, 26 Mar 2019 18:53:16 +0100 Subject: [PATCH 087/928] Removed unwanted comment --- freqtrade/tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 531f73916..64a33fae2 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -689,7 +689,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): def _trend_alternate_hold(dataframe=None, metadata=None): """ - Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)flake + Buy every xth candle - sell every other xth -2 (hold on to pairs a bit) """ if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): multi = 20 From 3bdc7b9a8880a23cf9ae0925a0194f373e027c27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Mar 2019 10:51:13 +0100 Subject: [PATCH 088/928] add missed "check" in docs --- docs/bot-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index f018d92a7..9e754c213 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -262,7 +262,7 @@ The strategy provides access to the `DataProvider`. This allows you to get addit All methods return `None` in case of failure (do not raise an exception). -Please always the mode of operation to select the correct method to get data (samples see below). +Please always check the mode of operation to select the correct method to get data (samples see below). #### Possible options for DataProvider From 4e57969e4e2d3e1d49967f46844e744f2f6e2b8d Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 27 Mar 2019 12:54:00 +0100 Subject: [PATCH 089/928] documentation added --- docs/telegram-usage.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 6788bec90..fa10969db 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -180,6 +180,21 @@ Use `/reload_conf` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. +### /edge + +Shows pairs accepted by pais along with their corresponding winrate, expectancy and stoploss values. + +> **Edge only validated following pairs:** +``` +Pair Winrate Expectancy Stoploss +-------- --------- ------------ ---------- +DOCK/ETH 0.522727 0.881821 -0.03 +PHX/ETH 0.677419 0.560488 -0.03 +HOT/ETH 0.733333 0.490492 -0.03 +HC/ETH 0.588235 0.280988 -0.02 +ARDR/ETH 0.366667 0.143059 -0.01 +``` + ### /version > **Version:** `0.14.3` From 955e2d28262fd8572cd8983196a0a0bcbdd099f6 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 27 Mar 2019 12:59:59 +0100 Subject: [PATCH 090/928] Update test_rpc_telegram.py telegram test_init fixed --- freqtrade/tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index dd49b0000..24e99b1c5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], " \ - "['stopbuy'], ['whitelist'], ['blacklist'], ['help'], ['version']]" + "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" assert log_has(message_str, caplog.record_tuples) From cc32566c92f046dfe943e80dcd36fdcdcffa2e6d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 27 Mar 2019 12:38:05 +0000 Subject: [PATCH 091/928] Update ccxt from 1.18.400 to 1.18.406 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 345d32c0d..2bf329bd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.400 +ccxt==1.18.406 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From 8641da13b908747d49b0b5f39610e8cd104ecdfd Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 27 Mar 2019 14:02:37 +0100 Subject: [PATCH 092/928] added RPC tests in case of edge enabled/disabled --- freqtrade/tests/rpc/test_rpc.py | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index e6f7ea41e..a5ee4a26c 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -2,19 +2,20 @@ # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments from datetime import datetime -from unittest.mock import MagicMock, ANY, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import pytest from numpy import isnan -from freqtrade import TemporaryError, DependencyException +from freqtrade import DependencyException, TemporaryError +from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.test_freqtradebot import patch_get_signal from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange +from freqtrade.tests.test_freqtradebot import patch_get_signal # Functions for recurrent object patching @@ -713,3 +714,32 @@ def test_rpc_blacklist(mocker, default_conf) -> None: assert len(ret['blacklist']) == 3 assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] + +def test_rpc_edge_disabled(mocker, default_conf) -> None: + patch_coinmarketcap(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + with pytest.raises(RPCException, match=r'Edge is not enabled.'): + ret = rpc._rpc_edge() + +def test_rpc_edge_enabled(mocker, edge_conf) -> None: + patch_coinmarketcap(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + freqtradebot = FreqtradeBot(edge_conf) + + rpc = RPC(freqtradebot) + ret = rpc._rpc_edge() + + assert len(ret) == 1 + assert ret[0]['Pair'] == 'E/F' + assert ret[0]['Winrate'] == 0.66 + assert ret[0]['Expectancy'] == 1.71 + assert ret[0]['Stoploss'] == -0.02 \ No newline at end of file From 0687051ffbd86b821cfdccb228680f6d51df1bd7 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 27 Mar 2019 14:04:33 +0100 Subject: [PATCH 093/928] Update test_rpc.py flake8 --- freqtrade/tests/rpc/test_rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index a5ee4a26c..8e8e7fc50 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -715,6 +715,7 @@ def test_rpc_blacklist(mocker, default_conf) -> None: assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] + def test_rpc_edge_disabled(mocker, default_conf) -> None: patch_coinmarketcap(mocker) patch_exchange(mocker) @@ -722,7 +723,8 @@ def test_rpc_edge_disabled(mocker, default_conf) -> None: freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) with pytest.raises(RPCException, match=r'Edge is not enabled.'): - ret = rpc._rpc_edge() + rpc._rpc_edge() + def test_rpc_edge_enabled(mocker, edge_conf) -> None: patch_coinmarketcap(mocker) @@ -742,4 +744,4 @@ def test_rpc_edge_enabled(mocker, edge_conf) -> None: assert ret[0]['Pair'] == 'E/F' assert ret[0]['Winrate'] == 0.66 assert ret[0]['Expectancy'] == 1.71 - assert ret[0]['Stoploss'] == -0.02 \ No newline at end of file + assert ret[0]['Stoploss'] == -0.02 From 4038cdf70a25deae3ab1f50399dbd7cbd94d34ce Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 27 Mar 2019 16:04:05 +0100 Subject: [PATCH 094/928] "Edge" test for rpc telegram --- freqtrade/tests/rpc/test_rpc_telegram.py | 47 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 24e99b1c5..8118266cc 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -13,16 +13,16 @@ from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ +from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only -from freqtrade.strategy.interface import SellType from freqtrade.state import State +from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, - patch_exchange) + patch_coinmarketcap, patch_exchange) from freqtrade.tests.test_freqtradebot import patch_get_signal -from freqtrade.tests.conftest import patch_coinmarketcap class DummyCls(Telegram): @@ -1099,6 +1099,47 @@ def test_blacklist_static(default_conf, update, mocker) -> None: assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] +def test_edge_disabled(default_conf, update, mocker) -> None: + patch_coinmarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + + telegram = Telegram(freqtradebot) + + telegram._edge(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] + + +def test_edge_enabled(edge_conf, update, mocker) -> None: + patch_coinmarketcap(mocker) + msg_mock = MagicMock() + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, edge_conf) + + telegram = Telegram(freqtradebot) + + telegram._edge(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
+    assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
+
 def test_help_handle(default_conf, update, mocker) -> None:
     patch_coinmarketcap(mocker)
     msg_mock = MagicMock()

From 1e37d8ccb3266be217847865e4fa9154d3d83583 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 16:58:53 +0100
Subject: [PATCH 095/928] flake8

---
 freqtrade/tests/rpc/test_rpc_telegram.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index 8118266cc..c6a5fff54 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -1140,6 +1140,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
     assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
 
+
 def test_help_handle(default_conf, update, mocker) -> None:
     patch_coinmarketcap(mocker)
     msg_mock = MagicMock()

From 753b03d581d2f435f70c44ca96fcbcd4589a45a7 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 18:19:42 +0100
Subject: [PATCH 096/928] rolback on removing MD whitespaces

---
 docs/telegram-usage.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index fa10969db..d68183537 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -56,7 +56,7 @@ Prevents the bot from opening new trades by temporarily setting "max_open_trades
 After this, give the bot time to close off open trades (can be checked via `/status table`).
 Once all positions are sold, run `/stop` to completely stop the bot.
 
-`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
+`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. 
 
 !!! warning
    The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
@@ -167,7 +167,7 @@ Day         Profit BTC      Profit USD
 
 Shows the current whitelist
 
-> Using whitelist `StaticPairList` with 22 pairs
+> Using whitelist `StaticPairList` with 22 pairs  
 > `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC`
 
 ### /blacklist [pair]
@@ -177,7 +177,7 @@ If Pair is set, then this pair will be added to the pairlist.
 Also supports multiple pairs, seperated by a space.
 Use `/reload_conf` to reset the blacklist.
 
-> Using blacklist `StaticPairList` with 2 pairs
+> Using blacklist `StaticPairList` with 2 pairs  
 >`DODGE/BTC`, `HOT/BTC`.
 
 ### /edge

From 9b22d5cab135d743d7c5b3841427a24636de11ee Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 27 Mar 2019 20:51:55 +0100
Subject: [PATCH 097/928] Fix typo, add test for validate_order_tif

---
 freqtrade/exchange/exchange.py            |  2 +-
 freqtrade/tests/exchange/test_exchange.py | 22 ++++++++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 2ec5c0e25..011be58e5 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -272,7 +272,7 @@ class Exchange(object):
         if any(v not in self._ft_has["order_time_in_force"]
                for k, v in order_time_in_force.items()):
             raise OperationalException(
-                f'Time in force policies are not supporetd for  {self.name} yet.')
+                f'Time in force policies are not supported for {self.name} yet.')
 
     def exchange_has(self, endpoint: str) -> bool:
         """
diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py
index 736f2298a..eed16d39b 100644
--- a/freqtrade/tests/exchange/test_exchange.py
+++ b/freqtrade/tests/exchange/test_exchange.py
@@ -139,6 +139,28 @@ def test_exchange_resolver(default_conf, mocker, caplog):
                           caplog.record_tuples)
 
 
+def test_validate_order_time_in_force(default_conf, mocker, caplog):
+    caplog.set_level(logging.INFO)
+    # explicitly test bittrex, exchanges implementing other policies need seperate tests
+    ex = get_patched_exchange(mocker, default_conf, id="bittrex")
+    tif = {
+        "buy": "gtc",
+        "sell": "gtc",
+    }
+
+    ex.validate_order_time_in_force(tif)
+    tif2 = {
+        "buy": "fok",
+        "sell": "ioc",
+    }
+    with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"):
+        ex.validate_order_time_in_force(tif2)
+
+    # Patch to see if this will pass if the values are in the ft dict
+    ex._ft_has.update({"order_time_in_force": ["gtc", "fok", "ioc"]})
+    ex.validate_order_time_in_force(tif2)
+
+
 def test_symbol_amount_prec(default_conf, mocker):
     '''
     Test rounds down to 4 Decimal places

From 6045f07a9c4fafe7d730c0d3e3d57b3e761933e5 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 21:12:57 +0100
Subject: [PATCH 098/928] telegram message concatenation refactored

---
 freqtrade/rpc/telegram.py                | 30 ++++++++++++++----------
 freqtrade/tests/rpc/test_rpc_telegram.py |  6 +++++
 2 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 2c419e417..1e1436631 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -193,21 +193,25 @@ class Telegram(RPC):
             for result in results:
                 result['date'] = result['date'].humanize()
 
-            messages = [
-                "*Trade ID:* `{trade_id}`\n"
-                "*Current Pair:* {pair}\n"
-                "*Open Since:* `{date}`\n"
-                "*Amount:* `{amount}`\n"
-                "*Open Rate:* `{open_rate:.8f}`\n"
-                "*Close Rate:* `{close_rate}`\n"
-                "*Current Rate:* `{current_rate:.8f}`\n"
-                "*Close Profit:* `{close_profit}`\n"
-                "*Current Profit:* `{current_profit:.2f}%`\n"
-                "*Open Order:* `{open_order}`".format(**result)
-                for result in results
-            ]
+            messages = []
+            for r in results:
+                lines = [
+                    "*Trade ID:* `{trade_id}`",
+                    "*Current Pair:* {pair}",
+                    "*Open Since:* `{date}`",
+                    "*Amount:* `{amount}`",
+                    "*Open Rate:* `{open_rate:.8f}`",
+                    "*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
+                    "*Current Rate:* `{current_rate:.8f}`",
+                    "*Close Profit:* `{close_profit}`",
+                    "*Current Profit:* `{current_profit:.2f}%`",
+                    "*Open Order:* `{open_order}`"
+                ]
+                messages.append("\n".join(filter(None,lines)).format(**r))
+
             for msg in messages:
                 self._send_msg(msg, bot=bot)
+
         except RPCException as e:
             self._send_msg(str(e), bot=bot)
 
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index dd49b0000..39973d5db 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -267,6 +267,12 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
     # Trigger status while we have a fulfilled order for the open trade
     telegram._status(bot=MagicMock(), update=update)
 
+    # close_rate should not be included in the message as the trade is not closed
+    # and no line should be empty
+    lines = msg_mock.call_args_list[0][0][0].split('\n')
+    assert '' not in lines
+    assert 'Close Rate' not in ''.join(lines)
+
     assert msg_mock.call_count == 1
     assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
 

From 4d9ca71c82c08270d8fcb8c102c1c52670c60f9e Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 21:20:09 +0100
Subject: [PATCH 099/928] shifting edge help message a line lower

---
 freqtrade/rpc/telegram.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 553f1a1fb..919c45757 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -533,8 +533,8 @@ class Telegram(RPC):
                   "*/reload_conf:* `Reload configuration file` \n" \
                   "*/whitelist:* `Show current whitelist` \n" \
                   "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
-                  "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \
                   "to the blacklist.` \n" \
+                  "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \
                   "*/help:* `This help message`\n" \
                   "*/version:* `Show version`"
 

From e5406ed3cfe41d75a8381ab5112c80451023bf03 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 21:22:25 +0100
Subject: [PATCH 100/928] typo in docs and comments

---
 docs/telegram-usage.md    | 2 +-
 freqtrade/rpc/telegram.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index d68183537..1ca61e54a 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -182,7 +182,7 @@ Use `/reload_conf` to reset the blacklist.
 
 ### /edge
 
-Shows pairs accepted by pais along with their corresponding winrate, expectancy and stoploss values.
+Shows pairs validated by Edge along with their corresponding winrate, expectancy and stoploss values.
 
 > **Edge only validated following pairs:**
 ```
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 919c45757..06bf5efe9 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -494,7 +494,7 @@ class Telegram(RPC):
     def _edge(self, bot: Bot, update: Update) -> None:
         """
         Handler for /edge
-        Shows informaton related to Edge
+        Shows information related to Edge
         """
         try:
             edge_pairs = self._rpc_edge()

From 1678a039aee6cd06981152205b833bd0202ab512 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 21:32:56 +0100
Subject: [PATCH 101/928] removing close profit is trade is open

---
 freqtrade/rpc/telegram.py                | 2 +-
 freqtrade/tests/rpc/test_rpc_telegram.py | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 1e1436631..0870e660b 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -203,7 +203,7 @@ class Telegram(RPC):
                     "*Open Rate:* `{open_rate:.8f}`",
                     "*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
                     "*Current Rate:* `{current_rate:.8f}`",
-                    "*Close Profit:* `{close_profit}`",
+                    "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
                     "*Open Order:* `{open_order}`"
                 ]
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index 39973d5db..d1a068100 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -272,6 +272,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
     lines = msg_mock.call_args_list[0][0][0].split('\n')
     assert '' not in lines
     assert 'Close Rate' not in ''.join(lines)
+    assert 'Close Profit' not in ''.join(lines)
 
     assert msg_mock.call_count == 1
     assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]

From 0ca3a38ba6b6de63574c23ea8355863c858d0da9 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 21:39:17 +0100
Subject: [PATCH 102/928] moved date to top and show open order only if it is
 not none

---
 freqtrade/rpc/telegram.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 0870e660b..2ba431b07 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -196,18 +196,17 @@ class Telegram(RPC):
             messages = []
             for r in results:
                 lines = [
-                    "*Trade ID:* `{trade_id}`",
+                    "*Trade ID:* `{trade_id}` (since `{date}`)",
                     "*Current Pair:* {pair}",
-                    "*Open Since:* `{date}`",
                     "*Amount:* `{amount}`",
                     "*Open Rate:* `{open_rate:.8f}`",
                     "*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
-                    "*Open Order:* `{open_order}`"
+                    "*Open Order:* `{open_order}`" if r['open_order'] else "",
                 ]
-                messages.append("\n".join(filter(None,lines)).format(**r))
+                messages.append("\n".join(filter(None ,lines)).format(**r))
 
             for msg in messages:
                 self._send_msg(msg, bot=bot)

From 941921dd0f9f741d81da890941d9a3f6294ba81c Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 27 Mar 2019 22:00:46 +0100
Subject: [PATCH 103/928] initial SL and SL added to RPC

---
 freqtrade/rpc/rpc.py            | 2 ++
 freqtrade/tests/rpc/test_rpc.py | 4 ++++
 2 files changed, 6 insertions(+)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index a5601a502..3272059a9 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -110,6 +110,8 @@ class RPC(object):
                     amount=round(trade.amount, 8),
                     close_profit=fmt_close_profit,
                     current_profit=round(current_profit * 100, 2),
+                    initial_stoploss=trade.initial_stop_loss,
+                    stoploss=trade.stop_loss,
                     open_order='({} {} rem={:.8f})'.format(
                       order['type'], order['side'], order['remaining']
                     ) if order else None,
diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index e6f7ea41e..f9862c9ca 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -58,6 +58,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'amount': 90.99181074,
         'close_profit': None,
         'current_profit': -0.59,
+        'initial_stoploss': 0.0,
+        'stoploss': 0.0,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 
@@ -78,6 +80,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'amount': 90.99181074,
         'close_profit': None,
         'current_profit': ANY,
+        'initial_stoploss': 0.0,
+        'stoploss': 0.0,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 

From 0e5b0ebda6fa26aab71a877a6c62616110a46d7d Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Thu, 28 Mar 2019 12:09:07 +0100
Subject: [PATCH 104/928] adding SL and SL percentage to telegram msg

---
 freqtrade/rpc/rpc.py                     | 8 ++++++--
 freqtrade/rpc/telegram.py                | 3 ++-
 freqtrade/tests/rpc/test_rpc_telegram.py | 2 ++
 3 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 3272059a9..553e66d85 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -97,9 +97,13 @@ class RPC(object):
                     current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
                 except DependencyException:
                     current_rate = NAN
+
                 current_profit = trade.calc_profit_percent(current_rate)
                 fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
                                     if trade.close_profit else None)
+                sl_percentage = round(((trade.stop_loss - current_rate) / current_rate) * 100, 2)
+                txt_sl_percentage = f'{sl_percentage}%'
+
                 results.append(dict(
                     trade_id=trade.id,
                     pair=trade.pair,
@@ -110,8 +114,8 @@ class RPC(object):
                     amount=round(trade.amount, 8),
                     close_profit=fmt_close_profit,
                     current_profit=round(current_profit * 100, 2),
-                    initial_stoploss=trade.initial_stop_loss,
-                    stoploss=trade.stop_loss,
+                    stop_loss=trade.stop_loss,
+                    stop_loss_percentage=txt_sl_percentage,
                     open_order='({} {} rem={:.8f})'.format(
                       order['type'], order['side'], order['remaining']
                     ) if order else None,
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 2ba431b07..8bec8e6cd 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -204,9 +204,10 @@ class Telegram(RPC):
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
+                    "*Stoploss:* `{stop_loss:.8f}` ({stop_loss_percentage})",
                     "*Open Order:* `{open_order}`" if r['open_order'] else "",
                 ]
-                messages.append("\n".join(filter(None ,lines)).format(**r))
+                messages.append("\n".join(filter(None, lines)).format(**r))
 
             for msg in messages:
                 self._send_msg(msg, bot=bot)
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index d1a068100..edd099dc4 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -201,6 +201,8 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
             'amount': 90.99181074,
             'close_profit': None,
             'current_profit': -0.59,
+            'stop_loss': 1.099e-05,
+            'stop_loss_percentage': '-2%',
             'open_order': '(limit buy rem=0.00000000)'
         }]),
         _status_table=status_table,

From daeb172ba1d481f0c46dda83576c6b5032c34c27 Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Thu, 28 Mar 2019 12:38:05 +0000
Subject: [PATCH 105/928] Update ccxt from 1.18.406 to 1.18.407

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 2bf329bd8..0fff9dacf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.406
+ccxt==1.18.407
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From e11eb4775e875533c21f21a7c2a8d63eaf506b55 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Thu, 28 Mar 2019 16:21:49 +0100
Subject: [PATCH 106/928] stoploss precentage in telegram msg removed

---
 freqtrade/rpc/rpc.py            | 3 ---
 freqtrade/rpc/telegram.py       | 2 +-
 freqtrade/tests/rpc/test_rpc.py | 6 ++----
 3 files changed, 3 insertions(+), 8 deletions(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 553e66d85..20bfe41ba 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -101,8 +101,6 @@ class RPC(object):
                 current_profit = trade.calc_profit_percent(current_rate)
                 fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
                                     if trade.close_profit else None)
-                sl_percentage = round(((trade.stop_loss - current_rate) / current_rate) * 100, 2)
-                txt_sl_percentage = f'{sl_percentage}%'
 
                 results.append(dict(
                     trade_id=trade.id,
@@ -115,7 +113,6 @@ class RPC(object):
                     close_profit=fmt_close_profit,
                     current_profit=round(current_profit * 100, 2),
                     stop_loss=trade.stop_loss,
-                    stop_loss_percentage=txt_sl_percentage,
                     open_order='({} {} rem={:.8f})'.format(
                       order['type'], order['side'], order['remaining']
                     ) if order else None,
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 8bec8e6cd..d3f3521aa 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -204,7 +204,7 @@ class Telegram(RPC):
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
-                    "*Stoploss:* `{stop_loss:.8f}` ({stop_loss_percentage})",
+                    "*Stoploss:* `{stop_loss:.8f}`",
                     "*Open Order:* `{open_order}`" if r['open_order'] else "",
                 ]
                 messages.append("\n".join(filter(None, lines)).format(**r))
diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index f9862c9ca..529bf31f3 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -58,8 +58,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'amount': 90.99181074,
         'close_profit': None,
         'current_profit': -0.59,
-        'initial_stoploss': 0.0,
-        'stoploss': 0.0,
+        'stop_loss': 0.0,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 
@@ -80,8 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'amount': 90.99181074,
         'close_profit': None,
         'current_profit': ANY,
-        'initial_stoploss': 0.0,
-        'stoploss': 0.0,
+        'stop_loss': 0.0,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 

From 2f3f5f19cdc82971525990918c08fb5d17a173f8 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Thu, 28 Mar 2019 16:26:59 +0100
Subject: [PATCH 107/928] sl percentage removed form rpc test

---
 freqtrade/rpc/rpc.py                     | 2 --
 freqtrade/tests/rpc/test_rpc_telegram.py | 1 -
 2 files changed, 3 deletions(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 20bfe41ba..55f9a302c 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -97,11 +97,9 @@ class RPC(object):
                     current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
                 except DependencyException:
                     current_rate = NAN
-
                 current_profit = trade.calc_profit_percent(current_rate)
                 fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
                                     if trade.close_profit else None)
-
                 results.append(dict(
                     trade_id=trade.id,
                     pair=trade.pair,
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index edd099dc4..b66e29143 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -202,7 +202,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
             'close_profit': None,
             'current_profit': -0.59,
             'stop_loss': 1.099e-05,
-            'stop_loss_percentage': '-2%',
             'open_order': '(limit buy rem=0.00000000)'
         }]),
         _status_table=status_table,

From a87fc5f8634d656faa0d9de020e5f0beb0bda40c Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 28 Mar 2019 19:37:50 +0100
Subject: [PATCH 108/928] Fix tests - freqtrade should not be patched in this
 case

---
 freqtrade/tests/test_freqtradebot.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index ceeae6865..8bc071071 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -112,9 +112,9 @@ def test_worker_running(mocker, default_conf, caplog) -> None:
     assert log_has('Changing state to: RUNNING', caplog.record_tuples)
     assert mock_throttle.call_count == 1
     # Check strategy is loaded, and received a dataprovider object
-    assert freqtrade.strategy
-    assert freqtrade.strategy.dp
-    assert isinstance(freqtrade.strategy.dp, DataProvider)
+    assert worker.freqtrade.strategy
+    assert worker.freqtrade.strategy.dp
+    assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
 
 
 def test_worker_stopped(mocker, default_conf, caplog) -> None:
@@ -710,7 +710,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
     )
     sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
 
-    worker = get_patched_worker(mocker, default_conf)
+    worker = Worker(args=None, config=default_conf)
     patch_get_signal(worker.freqtrade)
 
     result = worker._process()
@@ -727,7 +727,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
         markets=PropertyMock(return_value=markets),
         buy=MagicMock(side_effect=OperationalException)
     )
-    worker = get_patched_worker(mocker, default_conf)
+    worker = Worker(args=None, config=default_conf)
     patch_get_signal(worker.freqtrade)
 
     assert worker.state == State.RUNNING

From 50fc63251ecf33edc8afebfe38cc2ce8d479b6a2 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Thu, 28 Mar 2019 21:18:26 +0100
Subject: [PATCH 109/928] added SL pct to DB

---
 freqtrade/persistence.py | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
index a1ac65c04..0b46768c1 100644
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -83,7 +83,7 @@ def check_migrate(engine) -> None:
         logger.debug(f'trying {table_back_name}')
 
     # Check for latest column
-    if not has_column(cols, 'min_rate'):
+    if not has_column(cols, 'stop_loss_pct'):
         logger.info(f'Running database migration - backup available as {table_back_name}')
 
         fee_open = get_column_def(cols, 'fee_open', 'fee')
@@ -91,7 +91,9 @@ def check_migrate(engine) -> None:
         open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
         close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
         stop_loss = get_column_def(cols, 'stop_loss', '0.0')
+        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', '0.0')
         initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
+        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', '0.0')
         stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
         stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
         max_rate = get_column_def(cols, 'max_rate', '0.0')
@@ -113,7 +115,8 @@ def check_migrate(engine) -> None:
                 (id, exchange, pair, is_open, fee_open, fee_close, open_rate,
                 open_rate_requested, close_rate, close_rate_requested, close_profit,
                 stake_amount, amount, open_date, close_date, open_order_id,
-                stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update,
+                stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
+                stoploss_order_id, stoploss_last_update,
                 max_rate, min_rate, sell_reason, strategy,
                 ticker_interval
                 )
@@ -129,7 +132,9 @@ def check_migrate(engine) -> None:
                 open_rate, {open_rate_requested} open_rate_requested, close_rate,
                 {close_rate_requested} close_rate_requested, close_profit,
                 stake_amount, amount, open_date, close_date, open_order_id,
-                {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
+                {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
+                {initial_stop_loss} initial_stop_loss,
+                {initial_stop_loss_pct} initial_stop_loss_pct,
                 {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
                 {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
                 {strategy} strategy, {ticker_interval} ticker_interval
@@ -184,8 +189,12 @@ class Trade(_DECL_BASE):
     open_order_id = Column(String)
     # absolute value of the stop loss
     stop_loss = Column(Float, nullable=True, default=0.0)
+    # percentage value of the stop loss
+    stop_loss_pct = Column(Float, nullable=True, default=0.0)
     # absolute value of the initial stop loss
     initial_stop_loss = Column(Float, nullable=True, default=0.0)
+    # percentage value of the initial stop loss
+    initial_stop_loss_pct = Column(Float, nullable=True, default=0.0)
     # stoploss order id which is on exchange
     stoploss_order_id = Column(String, nullable=True, index=True)
     # last update time of the stoploss order on exchange
@@ -231,13 +240,16 @@ class Trade(_DECL_BASE):
         if not self.stop_loss:
             logger.debug("assigning new stop loss")
             self.stop_loss = new_loss
+            self.stop_loss_pct = stoploss
             self.initial_stop_loss = new_loss
+            self.initial_stop_loss_pct = stoploss
             self.stoploss_last_update = datetime.utcnow()
 
         # evaluate if the stop loss needs to be updated
         else:
             if new_loss > self.stop_loss:  # stop losses only walk up, never down!
                 self.stop_loss = new_loss
+                self.stop_loss_pct = stoploss
                 self.stoploss_last_update = datetime.utcnow()
                 logger.debug("adjusted stop loss")
             else:

From f2599ffe9029acea5a0a08137376c6e894a8fa2b Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Fri, 29 Mar 2019 08:08:29 +0100
Subject: [PATCH 110/928] pct default to None

---
 freqtrade/persistence.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
index 0b46768c1..5be61393e 100644
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -91,9 +91,9 @@ def check_migrate(engine) -> None:
         open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
         close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
         stop_loss = get_column_def(cols, 'stop_loss', '0.0')
-        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', '0.0')
+        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
         initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
-        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', '0.0')
+        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
         stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
         stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
         max_rate = get_column_def(cols, 'max_rate', '0.0')
@@ -190,11 +190,11 @@ class Trade(_DECL_BASE):
     # absolute value of the stop loss
     stop_loss = Column(Float, nullable=True, default=0.0)
     # percentage value of the stop loss
-    stop_loss_pct = Column(Float, nullable=True, default=0.0)
+    stop_loss_pct = Column(Float, nullable=True)
     # absolute value of the initial stop loss
     initial_stop_loss = Column(Float, nullable=True, default=0.0)
     # percentage value of the initial stop loss
-    initial_stop_loss_pct = Column(Float, nullable=True, default=0.0)
+    initial_stop_loss_pct = Column(Float, nullable=True)
     # stoploss order id which is on exchange
     stoploss_order_id = Column(String, nullable=True, index=True)
     # last update time of the stoploss order on exchange

From 82b344db1bd93dcda60c48cbf65c8b2936f4d935 Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Fri, 29 Mar 2019 12:38:05 +0000
Subject: [PATCH 111/928] Update ccxt from 1.18.407 to 1.18.412

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 0fff9dacf..22d3ae485 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.407
+ccxt==1.18.412
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From bb5a310aec09fd9c2f961f32a78fa86bb97bb411 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 29 Mar 2019 20:12:44 +0100
Subject: [PATCH 112/928] Add --logfile argument

---
 freqtrade/arguments.py     |  7 +++++++
 freqtrade/configuration.py | 17 ++++++++++++++++-
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py
index 604386426..8d7dac4bc 100644
--- a/freqtrade/arguments.py
+++ b/freqtrade/arguments.py
@@ -71,6 +71,13 @@ class Arguments(object):
             dest='loglevel',
             default=0,
         )
+        self.parser.add_argument(
+            '--logfile',
+            help='Log to the file specified',
+            dest='logfile',
+            type=str,
+            metavar='FILE'
+        )
         self.parser.add_argument(
             '--version',
             action='version',
diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py
index ba7a0e200..adaf7a59b 100644
--- a/freqtrade/configuration.py
+++ b/freqtrade/configuration.py
@@ -4,7 +4,9 @@ This module contains the configuration class
 import json
 import logging
 import os
+import sys
 from argparse import Namespace
+from logging.handlers import RotatingFileHandler
 from typing import Any, Dict, Optional
 
 import ccxt
@@ -12,8 +14,8 @@ from jsonschema import Draft4Validator, validate
 from jsonschema.exceptions import ValidationError, best_match
 
 from freqtrade import OperationalException, constants
-from freqtrade.state import RunMode
 from freqtrade.misc import deep_merge_dicts
+from freqtrade.state import RunMode
 
 logger = logging.getLogger(__name__)
 
@@ -116,9 +118,22 @@ class Configuration(object):
             config.update({'verbosity': self.args.loglevel})
         else:
             config.update({'verbosity': 0})
+
+        # Log to stdout, not stderr
+        log_handlers = [logging.StreamHandler(sys.stdout)]
+        if 'logfile' in self.args and self.args.logfile:
+            config.update({'logfile': self.args.logfile})
+
+        # Allow setting this as either configuration or argument
+        if 'logfile' in config:
+            log_handlers.append(RotatingFileHandler(config['logfile'],
+                                                    maxBytes=1024 * 1024,  # 1Mb
+                                                    backupCount=10))
+
         logging.basicConfig(
             level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+            handlers=log_handlers
         )
         set_loggers(config['verbosity'])
         logger.info('Verbosity set to %s', config['verbosity'])

From d4ffdaffc2a19d4a5f54a0b1cee6d00a8534e258 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 29 Mar 2019 20:16:41 +0100
Subject: [PATCH 113/928] Correctly add types

---
 freqtrade/configuration.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py
index adaf7a59b..fdd71f2f5 100644
--- a/freqtrade/configuration.py
+++ b/freqtrade/configuration.py
@@ -7,7 +7,7 @@ import os
 import sys
 from argparse import Namespace
 from logging.handlers import RotatingFileHandler
-from typing import Any, Dict, Optional
+from typing import Any, Dict, List, Optional
 
 import ccxt
 from jsonschema import Draft4Validator, validate
@@ -120,7 +120,7 @@ class Configuration(object):
             config.update({'verbosity': 0})
 
         # Log to stdout, not stderr
-        log_handlers = [logging.StreamHandler(sys.stdout)]
+        log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
         if 'logfile' in self.args and self.args.logfile:
             config.update({'logfile': self.args.logfile})
 

From e5008fbf9347d5d8234a70afc2f65c1049b695ab Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 29 Mar 2019 20:16:52 +0100
Subject: [PATCH 114/928] Add test for logfile attribute

---
 freqtrade/tests/test_configuration.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py
index 21547d205..45e539c2f 100644
--- a/freqtrade/tests/test_configuration.py
+++ b/freqtrade/tests/test_configuration.py
@@ -5,6 +5,7 @@ import logging
 from argparse import Namespace
 from copy import deepcopy
 from unittest.mock import MagicMock
+from pathlib import Path
 
 import pytest
 from jsonschema import Draft4Validator, ValidationError, validate
@@ -547,6 +548,23 @@ def test_set_loggers() -> None:
     assert logging.getLogger('telegram').level is logging.INFO
 
 
+def test_set_logfile(default_conf, mocker):
+    mocker.patch('freqtrade.configuration.open',
+                 mocker.mock_open(read_data=json.dumps(default_conf)))
+
+    arglist = [
+        '--logfile', 'test_file.log',
+    ]
+    args = Arguments(arglist, '').get_parsed_arg()
+    configuration = Configuration(args)
+    validated_conf = configuration.load_config()
+
+    assert validated_conf['logfile'] == "test_file.log"
+    f = Path("test_file.log")
+    assert f.is_file()
+    f.unlink()
+
+
 def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
     default_conf['forcebuy_enable'] = True
     mocker.patch('freqtrade.configuration.open', mocker.mock_open(

From 12066411db802e942bd990cb3c461280ed4adb51 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 29 Mar 2019 20:19:40 +0100
Subject: [PATCH 115/928] Update docs with logfile methods

---
 README.md             | 8 +++++---
 docs/bot-usage.md     | 7 ++++---
 docs/configuration.md | 1 +
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index ade62ce94..8f7578561 100644
--- a/README.md
+++ b/README.md
@@ -68,9 +68,9 @@ For any other type of installation please refer to [Installation doc](https://ww
 ### Bot commands
 
 ```
-usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
-                 [--strategy-path PATH] [--dynamic-whitelist [INT]]
-                 [--db-url PATH]
+usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH]
+                 [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]]
+                 [--db-url PATH] [--sd-notify]
                  {backtesting,edge,hyperopt} ...
 
 Free, open source crypto trading bot
@@ -84,6 +84,7 @@ positional arguments:
 optional arguments:
   -h, --help            show this help message and exit
   -v, --verbose         Verbose mode (-vv for more, -vvv to get all messages).
+  --logfile FILE        Log to the file specified
   --version             show program's version number and exit
   -c PATH, --config PATH
                         Specify configuration file (default: None). Multiple
@@ -100,6 +101,7 @@ optional arguments:
   --db-url PATH         Override trades database URL, this is useful if
                         dry_run is enabled or in custom deployments (default:
                         None).
+  --sd-notify           Notify systemd service manager.
 ```
 
 ### Telegram RPC commands
diff --git a/docs/bot-usage.md b/docs/bot-usage.md
index 35e4a776d..5ec390d5c 100644
--- a/docs/bot-usage.md
+++ b/docs/bot-usage.md
@@ -6,9 +6,9 @@ This page explains the different parameters of the bot and how to run it.
 ## Bot commands
 
 ```
-usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
-                 [--strategy-path PATH] [--dynamic-whitelist [INT]]
-                 [--db-url PATH]
+usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH]
+                 [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]]
+                 [--db-url PATH] [--sd-notify]
                  {backtesting,edge,hyperopt} ...
 
 Free, open source crypto trading bot
@@ -22,6 +22,7 @@ positional arguments:
 optional arguments:
   -h, --help            show this help message and exit
   -v, --verbose         Verbose mode (-vv for more, -vvv to get all messages).
+  --logfile FILE        Log to the file specified
   --version             show program's version number and exit
   -c PATH, --config PATH
                         Specify configuration file (default: None). Multiple
diff --git a/docs/configuration.md b/docs/configuration.md
index 11b941220..75843ef4a 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -70,6 +70,7 @@ Mandatory Parameters are marked as **Required**.
 | `strategy` | DefaultStrategy | Defines Strategy class to use.
 | `strategy_path` | null | Adds an additional strategy lookup path (must be a folder).
 | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
+| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
 | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
 
 ### Parameters in the strategy

From 208832e8471faed4427155c5b6817a6f8295aab8 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Sat, 30 Mar 2019 02:19:43 +0300
Subject: [PATCH 116/928] flake8, mypy resolved

---
 freqtrade/tests/rpc/test_rpc_telegram.py |  1 -
 freqtrade/tests/test_main.py             | 11 ++++++++---
 freqtrade/worker.py                      |  2 +-
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index b44c19688..384cdcfa0 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -14,7 +14,6 @@ from telegram.error import NetworkError
 
 from freqtrade import __version__
 from freqtrade.edge import PairInfo
-from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.persistence import Trade
 from freqtrade.rpc import RPCMessageType
 from freqtrade.rpc.telegram import Telegram, authorized_only
diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py
index 96cf2834d..fc5d2e378 100644
--- a/freqtrade/tests/test_main.py
+++ b/freqtrade/tests/test_main.py
@@ -8,7 +8,6 @@ import pytest
 from freqtrade import OperationalException
 from freqtrade.arguments import Arguments
 from freqtrade.worker import Worker
-from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.main import main
 from freqtrade.state import State
 from freqtrade.tests.conftest import log_has, patch_exchange
@@ -85,7 +84,10 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
 def test_main_operational_exception(mocker, default_conf, caplog) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
-    mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')))
+    mocker.patch(
+        'freqtrade.worker.Worker._worker',
+        MagicMock(side_effect=OperationalException('Oh snap!'))
+    )
     mocker.patch(
         'freqtrade.configuration.Configuration._load_config_file',
         lambda *args, **kwargs: default_conf
@@ -128,7 +130,10 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
 def test_reconfigure(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
-    mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')))
+    mocker.patch(
+        'freqtrade.worker.Worker._worker',
+        MagicMock(side_effect=OperationalException('Oh snap!'))
+    )
     mocker.patch(
         'freqtrade.configuration.Configuration._load_config_file',
         lambda *args, **kwargs: default_conf
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
index a6fba1460..f32e6ff49 100755
--- a/freqtrade/worker.py
+++ b/freqtrade/worker.py
@@ -23,7 +23,7 @@ class Worker(object):
     Freqtradebot worker class
     """
 
-    def __init__(self, args: Optional[Namespace] = None, config = None) -> None:
+    def __init__(self, args: Namespace, config=None) -> None:
         """
         Init all variables and objects the bot needs to work
         """

From 44142706c3cb73cb51b5dd821daf505269b802c8 Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Sat, 30 Mar 2019 12:38:03 +0000
Subject: [PATCH 117/928] Update ccxt from 1.18.412 to 1.18.415

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 22d3ae485..b2dee3542 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.412
+ccxt==1.18.415
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From 40c0b4ef2e4315df1d8fc5baa0937c66c929e147 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 30 Mar 2019 13:47:09 +0100
Subject: [PATCH 118/928] Autopatch coinmarketcap

---
 freqtrade/tests/conftest.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py
index c0f8e49b7..6478706a4 100644
--- a/freqtrade/tests/conftest.py
+++ b/freqtrade/tests/conftest.py
@@ -95,7 +95,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
     :param config: Config to pass to the bot
     :return: None
     """
-    patch_coinmarketcap(mocker, {'price_usd': 12345.0})
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
     mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
     patch_exchange(mocker, None)
@@ -105,7 +104,8 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
     return FreqtradeBot(config)
 
 
-def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
+@pytest.fixture(autouse=True)
+def patch_coinmarketcap(mocker) -> None:
     """
     Mocker to coinmarketcap to speed up tests
     :param mocker: mocker to patch coinmarketcap class

From e98c0621d33988c354bf2400052f1daf1ee7ac2a Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 30 Mar 2019 13:47:30 +0100
Subject: [PATCH 119/928] We don't need to call patch_coinmarketcap each time.

---
 freqtrade/tests/rpc/test_fiat_convert.py | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/freqtrade/tests/rpc/test_fiat_convert.py b/freqtrade/tests/rpc/test_fiat_convert.py
index fbc942432..66870efcc 100644
--- a/freqtrade/tests/rpc/test_fiat_convert.py
+++ b/freqtrade/tests/rpc/test_fiat_convert.py
@@ -8,7 +8,7 @@ import pytest
 from requests.exceptions import RequestException
 
 from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter
-from freqtrade.tests.conftest import log_has, patch_coinmarketcap
+from freqtrade.tests.conftest import log_has
 
 
 def test_pair_convertion_object():
@@ -40,7 +40,6 @@ def test_pair_convertion_object():
 
 
 def test_fiat_convert_is_supported(mocker):
-    patch_coinmarketcap(mocker)
     fiat_convert = CryptoToFiatConverter()
     assert fiat_convert._is_supported_fiat(fiat='USD') is True
     assert fiat_convert._is_supported_fiat(fiat='usd') is True
@@ -49,7 +48,6 @@ def test_fiat_convert_is_supported(mocker):
 
 
 def test_fiat_convert_add_pair(mocker):
-    patch_coinmarketcap(mocker)
 
     fiat_convert = CryptoToFiatConverter()
 
@@ -72,8 +70,6 @@ def test_fiat_convert_add_pair(mocker):
 
 
 def test_fiat_convert_find_price(mocker):
-    patch_coinmarketcap(mocker)
-
     fiat_convert = CryptoToFiatConverter()
 
     with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
@@ -93,15 +89,12 @@ def test_fiat_convert_find_price(mocker):
 
 def test_fiat_convert_unsupported_crypto(mocker, caplog):
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
-    patch_coinmarketcap(mocker)
     fiat_convert = CryptoToFiatConverter()
     assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
     assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
 
 
 def test_fiat_convert_get_price(mocker):
-    patch_coinmarketcap(mocker)
-
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                  return_value=28000.0)
 
@@ -134,21 +127,18 @@ def test_fiat_convert_get_price(mocker):
 
 
 def test_fiat_convert_same_currencies(mocker):
-    patch_coinmarketcap(mocker)
     fiat_convert = CryptoToFiatConverter()
 
     assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
 
 
 def test_fiat_convert_two_FIAT(mocker):
-    patch_coinmarketcap(mocker)
     fiat_convert = CryptoToFiatConverter()
 
     assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
 
 
 def test_loadcryptomap(mocker):
-    patch_coinmarketcap(mocker)
 
     fiat_convert = CryptoToFiatConverter()
     assert len(fiat_convert._cryptomap) == 2
@@ -174,7 +164,6 @@ def test_fiat_init_network_exception(mocker):
 
 def test_fiat_convert_without_network(mocker):
     # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
-    patch_coinmarketcap(mocker)
 
     fiat_convert = CryptoToFiatConverter()
 
@@ -205,7 +194,6 @@ def test_fiat_invalid_response(mocker, caplog):
 
 
 def test_convert_amount(mocker):
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
 
     fiat_convert = CryptoToFiatConverter()

From 87a296f728487c9bcef763bd98a46e760b07d954 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 30 Mar 2019 13:47:47 +0100
Subject: [PATCH 120/928] No need to call patch_coinmarketcap each tim

---
 freqtrade/tests/rpc/test_rpc.py          | 21 +--------------
 freqtrade/tests/rpc/test_rpc_telegram.py | 34 +-----------------------
 2 files changed, 2 insertions(+), 53 deletions(-)

diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index df89c03d4..627768ec2 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -14,7 +14,7 @@ from freqtrade.persistence import Trade
 from freqtrade.rpc import RPC, RPCException
 from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
 from freqtrade.state import State
-from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
+from freqtrade.tests.conftest import patch_exchange
 from freqtrade.tests.test_freqtradebot import patch_get_signal
 
 
@@ -28,7 +28,6 @@ def prec_satoshi(a, b) -> float:
 
 # Unit tests
 def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -86,7 +85,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
 
 
 def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -122,7 +120,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
 
 def test_rpc_daily_profit(default_conf, update, ticker, fee,
                           limit_buy_order, limit_sell_order, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -175,7 +172,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
         'freqtrade.rpc.fiat_convert.Market',
         ticker=MagicMock(return_value={'price_usd': 15000.0}),
     )
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
@@ -333,7 +329,6 @@ def test_rpc_balance_handle(default_conf, mocker):
         'freqtrade.rpc.fiat_convert.Market',
         ticker=MagicMock(return_value={'price_usd': 15000.0}),
     )
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
@@ -363,7 +358,6 @@ def test_rpc_balance_handle(default_conf, mocker):
 
 
 def test_rpc_start(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -386,7 +380,6 @@ def test_rpc_start(mocker, default_conf) -> None:
 
 
 def test_rpc_stop(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -410,7 +403,6 @@ def test_rpc_stop(mocker, default_conf) -> None:
 
 
 def test_rpc_stopbuy(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -430,7 +422,6 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
 
 
 def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
@@ -531,7 +522,6 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
 
 def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
                             limit_sell_order, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -567,7 +557,6 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
 
 
 def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
@@ -595,7 +584,6 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
 
 def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None:
     default_conf['forcebuy_enable'] = True
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
@@ -644,7 +632,6 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order
 def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
     default_conf['forcebuy_enable'] = True
     default_conf['initial_state'] = 'stopped'
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
@@ -657,7 +644,6 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
 
 
 def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
@@ -670,7 +656,6 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
 
 
 def test_rpc_whitelist(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
@@ -682,7 +667,6 @@ def test_rpc_whitelist(mocker, default_conf) -> None:
 
 
 def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     default_conf['pairlist'] = {'method': 'VolumePairList',
                                 'config': {'number_assets': 4}
@@ -699,7 +683,6 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
 
 
 def test_rpc_blacklist(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
@@ -719,7 +702,6 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
 
 
 def test_rpc_edge_disabled(mocker, default_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     freqtradebot = FreqtradeBot(default_conf)
@@ -729,7 +711,6 @@ def test_rpc_edge_disabled(mocker, default_conf) -> None:
 
 
 def test_rpc_edge_enabled(mocker, edge_conf) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index 03a6442e5..fb2d71d4f 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -20,8 +20,7 @@ from freqtrade.rpc import RPCMessageType
 from freqtrade.rpc.telegram import Telegram, authorized_only
 from freqtrade.state import State
 from freqtrade.strategy.interface import SellType
-from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
-                                      patch_coinmarketcap, patch_exchange)
+from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange)
 from freqtrade.tests.test_freqtradebot import patch_get_signal
 
 
@@ -90,7 +89,6 @@ def test_cleanup(default_conf, mocker) -> None:
 
 
 def test_authorized_only(default_conf, mocker, caplog) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker, None)
 
     chat = Chat(0, 0)
@@ -118,7 +116,6 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
 
 
 def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker, None)
     chat = Chat(0xdeadbeef, 0)
     update = Update(randint(1, 100))
@@ -145,7 +142,6 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
 
 
 def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
 
     update = Update(randint(1, 100))
@@ -178,7 +174,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
     default_conf['telegram']['enabled'] = False
     default_conf['telegram']['chat_id'] = 123
 
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -227,7 +222,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
 
 
 def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -280,7 +274,6 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
 
 
 def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -332,7 +325,6 @@ 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_coinmarketcap(mocker, value={'price_usd': 15000.0})
     patch_exchange(mocker)
     mocker.patch(
         'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
@@ -403,7 +395,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
 
 
 def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -439,7 +430,6 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
 
 def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
                        limit_buy_order, limit_sell_order, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     mocker.patch.multiple(
@@ -544,7 +534,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
             'last': 0.1,
         }
 
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
     mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
 
@@ -631,7 +620,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
 
 
 def test_stop_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -651,7 +639,6 @@ def test_stop_handle(default_conf, update, mocker) -> None:
 
 
 def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -671,7 +658,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
 
 
 def test_stopbuy_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -691,7 +677,6 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
 
 
 def test_reload_conf_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -712,7 +697,6 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
 
 def test_forcesell_handle(default_conf, update, ticker, fee,
                           ticker_sell_up, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
@@ -762,7 +746,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
 
 def test_forcesell_down_handle(default_conf, update, ticker, fee,
                                ticker_sell_down, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                  return_value=15000.0)
     rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
@@ -816,7 +799,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
 
 
 def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                  return_value=15000.0)
@@ -862,7 +844,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
 
 
 def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                  return_value=15000.0)
     msg_mock = MagicMock()
@@ -902,7 +883,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
 
 
 def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
@@ -939,7 +919,6 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
 
 
 def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None:
-    patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
     rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
@@ -962,7 +941,6 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non
 
 def test_performance_handle(default_conf, update, ticker, fee,
                             limit_buy_order, limit_sell_order, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
@@ -1002,7 +980,6 @@ def test_performance_handle(default_conf, update, ticker, fee,
 
 
 def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
-    patch_coinmarketcap(mocker)
     patch_exchange(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
@@ -1043,7 +1020,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
 
 
 def test_whitelist_static(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1061,7 +1037,6 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
 
 
 def test_whitelist_dynamic(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1083,7 +1058,6 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None:
 
 
 def test_blacklist_static(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1108,7 +1082,6 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
 
 
 def test_edge_disabled(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1126,7 +1099,6 @@ def test_edge_disabled(default_conf, update, mocker) -> None:
 
 
 def test_edge_enabled(edge_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
         return_value={
@@ -1150,7 +1122,6 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 
 
 def test_help_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1167,7 +1138,6 @@ def test_help_handle(default_conf, update, mocker) -> None:
 
 
 def test_version_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1394,7 +1364,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
 
 
 def test__send_msg(default_conf, mocker) -> None:
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
@@ -1406,7 +1375,6 @@ def test__send_msg(default_conf, mocker) -> None:
 
 
 def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))

From 1a61bf7bff0003b7632ef20609aff3e83e99bb87 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 30 Mar 2019 13:48:30 +0100
Subject: [PATCH 121/928] sort imports

---
 freqtrade/tests/conftest.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py
index 6478706a4..cba754199 100644
--- a/freqtrade/tests/conftest.py
+++ b/freqtrade/tests/conftest.py
@@ -4,7 +4,6 @@ import logging
 import re
 from datetime import datetime
 from functools import reduce
-from typing import Dict, Optional
 from unittest.mock import MagicMock, PropertyMock
 
 import arrow
@@ -13,8 +12,8 @@ from telegram import Chat, Message, Update
 
 from freqtrade import constants
 from freqtrade.data.converter import parse_ticker_dataframe
-from freqtrade.exchange import Exchange
 from freqtrade.edge import Edge, PairInfo
+from freqtrade.exchange import Exchange
 from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.resolvers import ExchangeResolver
 

From 06144a1fc4ea15f107ecbe21bca9382d1e8193c6 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Sat, 30 Mar 2019 23:33:52 +0300
Subject: [PATCH 122/928] Wording in a comment

---
 freqtrade/worker.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/worker.py b/freqtrade/worker.py
index f32e6ff49..e5ca36035 100755
--- a/freqtrade/worker.py
+++ b/freqtrade/worker.py
@@ -159,7 +159,7 @@ class Worker(object):
     def _reconfigure(self):
         """
         Cleans up current freqtradebot instance, reloads the configuration and
-        returns the new instance
+        replaces it with the new instance
         """
         # Tell systemd that we initiated reconfiguration
         if self._sd_notify:

From 9b38c04579d22bc371a6ff417a21397ae4de05ac Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Sun, 31 Mar 2019 13:15:35 +0200
Subject: [PATCH 123/928] negating SL pct and adding tests

---
 freqtrade/persistence.py            |  6 +++---
 freqtrade/tests/test_persistence.py | 10 ++++++++++
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
index 5be61393e..5a18a922a 100644
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -240,16 +240,16 @@ class Trade(_DECL_BASE):
         if not self.stop_loss:
             logger.debug("assigning new stop loss")
             self.stop_loss = new_loss
-            self.stop_loss_pct = stoploss
+            self.stop_loss_pct = -1 * abs(stoploss)
             self.initial_stop_loss = new_loss
-            self.initial_stop_loss_pct = stoploss
+            self.initial_stop_loss_pct = -1 * abs(stoploss)
             self.stoploss_last_update = datetime.utcnow()
 
         # evaluate if the stop loss needs to be updated
         else:
             if new_loss > self.stop_loss:  # stop losses only walk up, never down!
                 self.stop_loss = new_loss
-                self.stop_loss_pct = stoploss
+                self.stop_loss_pct = -1 * abs(stoploss)
                 self.stoploss_last_update = datetime.utcnow()
                 logger.debug("adjusted stop loss")
             else:
diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py
index 042237ce7..f57a466e3 100644
--- a/freqtrade/tests/test_persistence.py
+++ b/freqtrade/tests/test_persistence.py
@@ -599,32 +599,42 @@ def test_adjust_stop_loss(fee):
 
     trade.adjust_stop_loss(trade.open_rate, 0.05, True)
     assert trade.stop_loss == 0.95
+    assert trade.stop_loss_pct == -0.05
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
 
     # Get percent of profit with a lower rate
     trade.adjust_stop_loss(0.96, 0.05)
     assert trade.stop_loss == 0.95
+    assert trade.stop_loss_pct == -0.05
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
 
     # Get percent of profit with a custom rate (Higher than open rate)
     trade.adjust_stop_loss(1.3, -0.1)
     assert round(trade.stop_loss, 8) == 1.17
+    assert trade.stop_loss_pct == -0.1
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
 
     # current rate lower again ... should not change
     trade.adjust_stop_loss(1.2, 0.1)
     assert round(trade.stop_loss, 8) == 1.17
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
 
     # current rate higher... should raise stoploss
     trade.adjust_stop_loss(1.4, 0.1)
     assert round(trade.stop_loss, 8) == 1.26
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
 
     #  Initial is true but stop_loss set - so doesn't do anything
     trade.adjust_stop_loss(1.7, 0.1, True)
     assert round(trade.stop_loss, 8) == 1.26
     assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+    assert trade.stop_loss_pct == -0.1
 
 
 def test_adjust_min_max_rates(fee):

From 707a5fca919d1561413733f3182357f7cffdd8d8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 13:30:22 +0200
Subject: [PATCH 124/928] ifix typos in full_json_example

---
 config_full.json.example | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/config_full.json.example b/config_full.json.example
index 357a8f525..58d3d3ac6 100644
--- a/config_full.json.example
+++ b/config_full.json.example
@@ -39,12 +39,12 @@
         "buy": "limit",
         "sell": "limit",
         "stoploss": "market",
-        "stoploss_on_exchange": "false",
+        "stoploss_on_exchange": false,
         "stoploss_on_exchange_interval": 60
     },
     "order_time_in_force": {
         "buy": "gtc",
-        "sell": "gtc",
+        "sell": "gtc"
     },
     "pairlist": {
         "method": "VolumePairList",

From 93229fc54b840fd7f6643b59072270004ae607ac Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Sun, 31 Mar 2019 12:38:03 +0000
Subject: [PATCH 125/928] Update ccxt from 1.18.415 to 1.18.418

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index b2dee3542..6e21e879d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.415
+ccxt==1.18.418
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From c28a0374f15e6fbd2660a1dc1a08daebf8f91d76 Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Sun, 31 Mar 2019 12:38:04 +0000
Subject: [PATCH 126/928] Update pytest-mock from 1.10.2 to 1.10.3

---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 56d7964c3..0592b99e8 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,7 +5,7 @@ flake8==3.7.7
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==2.0.0
 pytest==4.3.1
-pytest-mock==1.10.2
+pytest-mock==1.10.3
 pytest-asyncio==0.10.0
 pytest-cov==2.6.1
 coveralls==1.7.0

From 4fa736114c1321e274803a4235d08c7e684f0043 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:30:22 +0200
Subject: [PATCH 127/928] Don't set order_id to none here - it's used in
 "update_open_order".

should fix bugs observed in #1371 connected to stoploss
---
 freqtrade/freqtradebot.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 6f1fb2c99..291b74913 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -472,7 +472,6 @@ class FreqtradeBot(object):
             stake_amount = order['cost']
             amount = order['amount']
             buy_limit_filled_price = order['price']
-            order_id = None
 
         self.rpc.send_msg({
             'type': RPCMessageType.BUY_NOTIFICATION,

From 8f4cca47e9a31d08015bc0093d252b4a202f80e5 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:39:41 +0200
Subject: [PATCH 128/928] Refactor update_open_order into it's own function

---
 freqtrade/freqtradebot.py | 41 ++++++++++++++++++++++-----------------
 1 file changed, 23 insertions(+), 18 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 291b74913..f78a9078e 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -530,24 +530,7 @@ class FreqtradeBot(object):
         :return: True if executed
         """
         try:
-            # Get order details for actual price per unit
-            if trade.open_order_id:
-                # Update trade with order values
-                logger.info('Found open order for %s', trade)
-                order = self.exchange.get_order(trade.open_order_id, trade.pair)
-                # Try update amount (binance-fix)
-                try:
-                    new_amount = self.get_real_amount(trade, order)
-                    if order['amount'] != new_amount:
-                        order['amount'] = new_amount
-                        # Fee was applied, so set to 0
-                        trade.fee_open = 0
-
-                except OperationalException as exception:
-                    logger.warning("Could not update trade amount: %s", exception)
-
-                # This handles both buy and sell orders!
-                trade.update(order)
+            self.update_open_order(trade)
 
             if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open:
                 result = self.handle_stoploss_on_exchange(trade)
@@ -612,6 +595,28 @@ class FreqtradeBot(object):
                         f"(from {order_amount} to {real_amount}) from Trades")
         return real_amount
 
+    def update_open_order(self, trade, action_order: dict = None):
+        """
+        Checks trades with open orders and updates the amount if necessary
+        """
+        # Get order details for actual price per unit
+        if trade.open_order_id:
+            # Update trade with order values
+            logger.info('Found open order for %s', trade)
+            order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair)
+            # Try update amount (binance-fix)
+            try:
+                new_amount = self.get_real_amount(trade, order)
+                if order['amount'] != new_amount:
+                    order['amount'] = new_amount
+                    # Fee was applied, so set to 0
+                    trade.fee_open = 0
+
+            except OperationalException as exception:
+                logger.warning("Could not update trade amount: %s", exception)
+
+            trade.update(order)
+
     def get_sell_rate(self, pair: str, refresh: bool) -> float:
         """
         Get sell rate - either using get-ticker bid or first bid based on orderbook

From f11a1b01228801dd5ea5a6877c2f819b2564d236 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:40:16 +0200
Subject: [PATCH 129/928] Call update_open_order inline with buy

captures FOK / market orders
---
 freqtrade/freqtradebot.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index f78a9078e..74c74324f 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -500,6 +500,10 @@ class FreqtradeBot(object):
             ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
         )
 
+        # Update fees if order is closed already.
+        if order_status == 'closed':
+            self.update_open_order(trade, order)
+
         Trade.session.add(trade)
         Trade.session.flush()
 

From 5c8fbe2c6ff44d36322f3268deabdbe29c6e8779 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:41:10 +0200
Subject: [PATCH 130/928] Handle exception for stoploss independently of sell
 order

---
 freqtrade/freqtradebot.py | 67 ++++++++++++++++++++-------------------
 1 file changed, 34 insertions(+), 33 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 74c74324f..a06e146f9 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -691,43 +691,44 @@ class FreqtradeBot(object):
         """
 
         result = False
+        try:
+            # If trade is open and the buy order is fulfilled but there is no stoploss,
+            # then we add a stoploss on exchange
+            if not trade.open_order_id and not trade.stoploss_order_id:
+                if self.edge:
+                    stoploss = self.edge.stoploss(pair=trade.pair)
+                else:
+                    stoploss = self.strategy.stoploss
 
-        # If trade is open and the buy order is fulfilled but there is no stoploss,
-        # then we add a stoploss on exchange
-        if not trade.open_order_id and not trade.stoploss_order_id:
-            if self.edge:
-                stoploss = self.edge.stoploss(pair=trade.pair)
-            else:
-                stoploss = self.strategy.stoploss
+                stop_price = trade.open_rate * (1 + stoploss)
 
-            stop_price = trade.open_rate * (1 + stoploss)
+                # limit price should be less than stop price.
+                # 0.99 is arbitrary here.
+                limit_price = stop_price * 0.99
 
-            # limit price should be less than stop price.
-            # 0.99 is arbitrary here.
-            limit_price = stop_price * 0.99
-
-            stoploss_order_id = self.exchange.stoploss_limit(
-                pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
-            )['id']
-            trade.stoploss_order_id = str(stoploss_order_id)
-            trade.stoploss_last_update = datetime.now()
-
-        # Or the trade open and there is already a stoploss on exchange.
-        # so we check if it is hit ...
-        elif trade.stoploss_order_id:
-            logger.debug('Handling stoploss on exchange %s ...', trade)
-            order = self.exchange.get_order(trade.stoploss_order_id, trade.pair)
-            if order['status'] == 'closed':
-                trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
-                trade.update(order)
-                self.notify_sell(trade)
-                result = True
-            elif self.config.get('trailing_stop', False):
-                # if trailing stoploss is enabled we check if stoploss value has changed
-                # in which case we cancel stoploss order and put another one with new
-                # value immediately
-                self.handle_trailing_stoploss_on_exchange(trade, order)
+                stoploss_order_id = self.exchange.stoploss_limit(
+                    pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
+                )['id']
+                trade.stoploss_order_id = str(stoploss_order_id)
+                trade.stoploss_last_update = datetime.now()
 
+            # Or the trade open and there is already a stoploss on exchange.
+            # so we check if it is hit ...
+            elif trade.stoploss_order_id:
+                logger.debug('Handling stoploss on exchange %s ...', trade)
+                order = self.exchange.get_order(trade.stoploss_order_id, trade.pair)
+                if order['status'] == 'closed':
+                    trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
+                    trade.update(order)
+                    self.notify_sell(trade)
+                    result = True
+                elif self.config.get('trailing_stop', False):
+                    # if trailing stoploss is enabled we check if stoploss value has changed
+                    # in which case we cancel stoploss order and put another one with new
+                    # value immediately
+                    self.handle_trailing_stoploss_on_exchange(trade, order)
+        except DependencyException as exception:
+            logger.warning('Unable to create stoploss order: %s', exception)
         return result
 
     def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):

From e46dac3fbd2846d7a22d1c54ca9ff1a64368d2e8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:45:22 +0200
Subject: [PATCH 131/928] Test stoploss does not raise dependencyexception

---
 freqtrade/tests/test_freqtradebot.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index 5bf0bfcbb..e53d26793 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -1031,6 +1031,13 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     assert trade.stoploss_order_id is None
     assert trade.is_open is False
 
+    mocker.patch(
+        'freqtrade.exchange.Exchange.stoploss_limit',
+        side_effect=DependencyException()
+    )
+    freqtrade.handle_stoploss_on_exchange(trade)
+    assert log_has('Unable to create stoploss order: ', caplog.record_tuples)
+
 
 def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
                                               markets, limit_buy_order, limit_sell_order) -> None:

From b2ad402df4ec8b6a4e35a2abaf368b09fce1bda8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 15:51:45 +0200
Subject: [PATCH 132/928] Split tests for update-open_order

---
 freqtrade/tests/test_freqtradebot.py | 28 +++++++++++++++++++---------
 1 file changed, 19 insertions(+), 9 deletions(-)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index e53d26793..38a6df695 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -1306,22 +1306,32 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
     trade.open_order_id = '123'
     trade.open_fee = 0.001
 
+    # Test raise of DependencyException exception
+    mocker.patch(
+        'freqtrade.freqtradebot.FreqtradeBot.update_open_order',
+        side_effect=DependencyException()
+    )
+    freqtrade.process_maybe_execute_sell(trade)
+    assert log_has('Unable to sell trade: ', caplog.record_tuples)
+
+
+def test_update_open_order_exception(mocker, default_conf,
+                                     limit_buy_order, caplog) -> None:
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
+
+    trade = MagicMock()
+    trade.open_order_id = '123'
+    trade.open_fee = 0.001
+
     # Test raise of OperationalException exception
     mocker.patch(
         'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
         side_effect=OperationalException()
     )
-    freqtrade.process_maybe_execute_sell(trade)
+    freqtrade.update_open_order(trade)
     assert log_has('Could not update trade amount: ', caplog.record_tuples)
 
-    # Test raise of DependencyException exception
-    mocker.patch(
-        'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
-        side_effect=DependencyException()
-    )
-    freqtrade.process_maybe_execute_sell(trade)
-    assert log_has('Unable to sell trade: ', caplog.record_tuples)
-
 
 def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
                       fee, markets, mocker) -> None:

From 0ddafeeabf1322fa679a1c56f77ed709cd3ac4bf Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 16:05:00 +0200
Subject: [PATCH 133/928] Split test for open_orders from maybe_sell

---
 freqtrade/tests/test_freqtradebot.py | 39 +++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index 38a6df695..a3b01c542 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -1291,7 +1291,7 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
     trade.is_open = True
     trade.open_order_id = None
     # Assert we call handle_trade() if trade is feasible for execution
-    assert freqtrade.process_maybe_execute_sell(trade)
+    freqtrade.update_open_order(trade)
 
     regexp = re.compile('Found open order for.*')
     assert filter(regexp.match, caplog.record_tuples)
@@ -1315,6 +1315,43 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
     assert log_has('Unable to sell trade: ', caplog.record_tuples)
 
 
+def test_update_open_order(mocker, default_conf, limit_buy_order, caplog) -> None:
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
+    mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
+    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
+                 return_value=limit_buy_order['amount'])
+
+    trade = Trade()
+    # Mock session away
+    Trade.session = MagicMock()
+    trade.open_order_id = '123'
+    trade.open_fee = 0.001
+    freqtrade.update_open_order(trade)
+    # Test amount not modified by fee-logic
+    assert not log_has_re(r'Applying fee to .*', caplog.record_tuples)
+    assert trade.open_order_id is None
+    assert trade.amount == limit_buy_order['amount']
+
+    trade.open_order_id = '123'
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
+    assert trade.amount != 90.81
+    # test amount modified by fee-logic
+    freqtrade.update_open_order(trade)
+    assert trade.amount == 90.81
+    assert trade.open_order_id is None
+
+    trade.is_open = True
+    trade.open_order_id = None
+    # Assert we call handle_trade() if trade is feasible for execution
+    freqtrade.update_open_order(trade)
+
+    regexp = re.compile('Found open order for.*')
+    assert filter(regexp.match, caplog.record_tuples)
+
+
 def test_update_open_order_exception(mocker, default_conf,
                                      limit_buy_order, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)

From 19d3a0cbacd21b58589abc4c5100fe0b335bb69f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 19:41:17 +0200
Subject: [PATCH 134/928] Update comment

---
 freqtrade/freqtradebot.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index a06e146f9..5b1e5625d 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -500,7 +500,7 @@ class FreqtradeBot(object):
             ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
         )
 
-        # Update fees if order is closed already.
+        # Update fees if order is closed
         if order_status == 'closed':
             self.update_open_order(trade, order)
 

From 7be90f71d379b235d2cbfa507b5d528bf42756fc Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 31 Mar 2019 19:56:01 +0200
Subject: [PATCH 135/928] Add test as called from execute_buy

---
 freqtrade/tests/test_freqtradebot.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index a3b01c542..af8fc3b09 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -1352,6 +1352,26 @@ def test_update_open_order(mocker, default_conf, limit_buy_order, caplog) -> Non
     assert filter(regexp.match, caplog.record_tuples)
 
 
+def test_update_open_order_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker):
+    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    # get_order should not be called!!
+    mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
+    patch_exchange(mocker)
+    Trade.session = MagicMock()
+    amount = sum(x['amount'] for x in trades_for_order)
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    trade = Trade(
+        pair='LTC/ETH',
+        amount=amount,
+        exchange='binance',
+        open_rate=0.245441,
+        open_order_id="123456"
+    )
+    freqtrade.update_open_order(trade, limit_buy_order)
+    assert trade.amount != amount
+    assert trade.amount == limit_buy_order['amount']
+
+
 def test_update_open_order_exception(mocker, default_conf,
                                      limit_buy_order, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)

From 7251e5bd625817e823dc4d90f14ecd04f84a4461 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Sun, 31 Mar 2019 23:39:55 +0300
Subject: [PATCH 136/928] bot state moved back to freqtradebot from worker

---
 freqtrade/freqtradebot.py | 21 +++++++--------------
 freqtrade/worker.py       | 24 +++++++-----------------
 2 files changed, 14 insertions(+), 31 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 7c92ac29a..6345dae56 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -22,7 +22,6 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListReso
 from freqtrade.state import State
 from freqtrade.strategy.interface import SellType, IStrategy
 from freqtrade.wallets import Wallets
-from freqtrade.worker import Worker
 
 
 logger = logging.getLogger(__name__)
@@ -34,7 +33,7 @@ class FreqtradeBot(object):
     This is from here the bot start its logic.
     """
 
-    def __init__(self, config: Dict[str, Any], worker: Optional[Worker] = None) -> None:
+    def __init__(self, config: Dict[str, Any]) -> None:
         """
         Init all variables and objects the bot needs to work
         :param config: configuration dict, you can use Configuration.get_config()
@@ -43,9 +42,11 @@ class FreqtradeBot(object):
 
         logger.info('Starting freqtrade %s', __version__)
 
+        # Init bot state
+        self.state = State.STOPPED
+
         # Init objects
         self.config = config
-        self._worker = worker
 
         self.strategy: IStrategy = StrategyResolver(self.config).strategy
 
@@ -73,17 +74,9 @@ class FreqtradeBot(object):
 
         persistence.init(self.config)
 
-    @property
-    def state(self) -> State:
-        if self._worker is None:
-            raise DependencyException("No Worker is available")
-        return self._worker.state
-
-    @state.setter
-    def state(self, value: State):
-        if self._worker is None:
-            raise DependencyException("No Worker is available")
-        self._worker.state = value
+        # Set initial bot state from config
+        initial_state = self.config.get('initial_state')
+        self.state = State[initial_state.upper()] if initial_state else State.STOPPED
 
     def cleanup(self) -> None:
         """
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
index e5ca36035..2440e7320 100755
--- a/freqtrade/worker.py
+++ b/freqtrade/worker.py
@@ -11,6 +11,7 @@ import sdnotify
 from freqtrade import (constants, OperationalException, TemporaryError,
                        __version__)
 from freqtrade.configuration import Configuration
+from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.state import State
 from freqtrade.rpc import RPCMessageType
 
@@ -46,19 +47,8 @@ class Worker(object):
             # Load configuration
             self._config = Configuration(self._args, None).get_config()
 
-        # Import freqtradebot here in order to avoid python circular
-        # dependency error, damn!
-        from freqtrade.freqtradebot import FreqtradeBot
-
         # Init the instance of the bot
-        self.freqtrade = FreqtradeBot(self._config, self)
-
-        # Set initial bot state
-        initial_state = self._config.get('initial_state')
-        if initial_state:
-            self._state = State[initial_state.upper()]
-        else:
-            self._state = State.STOPPED
+        self.freqtrade = FreqtradeBot(self._config)
 
         self._throttle_secs = self._config.get('internals', {}).get(
             'process_throttle_secs',
@@ -70,11 +60,11 @@ class Worker(object):
 
     @property
     def state(self) -> State:
-        return self._state
+        return self.freqtrade.state
 
     @state.setter
     def state(self, value: State):
-        self._state = value
+        self.freqtrade.state = value
 
     def run(self):
         state = None
@@ -89,7 +79,7 @@ class Worker(object):
         :param old_state: the previous service state from the previous call
         :return: current service state
         """
-        state = self._state
+        state = self.freqtrade.state
         if throttle_secs is None:
             throttle_secs = self._throttle_secs
 
@@ -141,7 +131,6 @@ class Worker(object):
         state_changed = False
         try:
             state_changed = self.freqtrade.process()
-
         except TemporaryError as error:
             logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
             time.sleep(constants.RETRY_TIMEOUT)
@@ -153,7 +142,8 @@ class Worker(object):
                 'status': f'OperationalException:\n```\n{tb}```{hint}'
             })
             logger.exception('OperationalException. Stopping trader ...')
-            self.state = State.STOPPED
+            self.freqtrade.state = State.STOPPED
+###            state_changed = True
         return state_changed
 
     def _reconfigure(self):

From f0b2798c37b7ad9dbda736cf92217a060ab514d9 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Mon, 1 Apr 2019 14:08:03 +0300
Subject: [PATCH 137/928] fix #1704

---
 freqtrade/freqtradebot.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 6f1fb2c99..c14f0d31a 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -363,7 +363,8 @@ class FreqtradeBot(object):
                 logger.debug('Ignoring %s in pair whitelist', trade.pair)
 
         if not whitelist:
-            raise DependencyException('No currency pairs in whitelist')
+            logger.info("No currency pairs left in whitelist, no trades can be created.")
+            return False
 
         # running get_signal on historical data fetched
         for _pair in whitelist:

From 77d2479c7584954fdd30568756453051d34ac87b Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Mon, 1 Apr 2019 14:08:41 +0300
Subject: [PATCH 138/928] tests adjusted

---
 freqtrade/tests/test_freqtradebot.py | 13 ++++---------
 1 file changed, 4 insertions(+), 9 deletions(-)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index e4f0415f7..4e145c354 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -545,8 +545,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    result = freqtrade.create_trade()
-    assert result is False
+    assert not freqtrade.create_trade()
 
 
 def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
@@ -567,7 +566,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    assert freqtrade.create_trade() is False
+    assert not freqtrade.create_trade()
     assert freqtrade._get_trade_stake_amount('ETH/BTC') is None
 
 
@@ -588,9 +587,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke
     patch_get_signal(freqtrade)
 
     freqtrade.create_trade()
-
-    with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
-        freqtrade.create_trade()
+    assert not freqtrade.create_trade()
 
 
 def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
@@ -610,9 +607,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
     patch_get_signal(freqtrade)
 
     freqtrade.create_trade()
-
-    with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
-        freqtrade.create_trade()
+    assert not freqtrade.create_trade()
 
 
 def test_create_trade_no_signal(default_conf, fee, mocker) -> None:

From 97b31352c2640909a4e05b330354599767732f1c Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Mon, 1 Apr 2019 12:38:06 +0000
Subject: [PATCH 139/928] Update ccxt from 1.18.418 to 1.18.420

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 6e21e879d..bdad28181 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.418
+ccxt==1.18.420
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From 061f91ba4173d5e4d329c0a5623690a2ceccb00c Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Mon, 1 Apr 2019 12:38:07 +0000
Subject: [PATCH 140/928] Update pytest from 4.3.1 to 4.4.0

---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 0592b99e8..69082587a 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
 flake8==3.7.7
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==2.0.0
-pytest==4.3.1
+pytest==4.4.0
 pytest-mock==1.10.3
 pytest-asyncio==0.10.0
 pytest-cov==2.6.1

From ab579587f29e3802f1c038e804214a228a4b1b26 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Mon, 1 Apr 2019 19:13:45 +0200
Subject: [PATCH 141/928] adding percentage to telegram status messages

---
 docs/telegram-usage.md                   | 6 ++----
 freqtrade/rpc/rpc.py                     | 3 +++
 freqtrade/rpc/telegram.py                | 6 ++++--
 freqtrade/tests/rpc/test_rpc.py          | 6 ++++++
 freqtrade/tests/rpc/test_rpc_telegram.py | 3 +++
 5 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 1ca61e54a..381a47ae9 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -65,16 +65,14 @@ Once all positions are sold, run `/stop` to completely stop the bot.
 
 For each open trade, the bot will send you the following message.
 
-> **Trade ID:** `123`
+> **Trade ID:** `123` `(since 1 days ago)`
 > **Current Pair:** CVC/BTC
 > **Open Since:** `1 days ago`
 > **Amount:** `26.64180098`
 > **Open Rate:** `0.00007489`
-> **Close Rate:** `None`
 > **Current Rate:** `0.00007489`
-> **Close Profit:** `None`
 > **Current Profit:** `12.95%`
-> **Open Order:** `None`
+> **Stoploss:** `0.00007389 (-0.02%)`
 
 ### /status table
 
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 79db08fd3..5308c9d51 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -111,6 +111,9 @@ class RPC(object):
                     close_profit=fmt_close_profit,
                     current_profit=round(current_profit * 100, 2),
                     stop_loss=trade.stop_loss,
+                    stop_loss_pct=trade.stop_loss_pct,
+                    initial_stop_loss=trade.initial_stop_loss,
+                    initial_stop_loss_pct=trade.initial_stop_loss_pct,
                     open_order='({} {} rem={:.8f})'.format(
                       order['type'], order['side'], order['remaining']
                     ) if order else None,
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 7b36e8a1f..ca9b376ec 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -197,7 +197,7 @@ class Telegram(RPC):
             messages = []
             for r in results:
                 lines = [
-                    "*Trade ID:* `{trade_id}` (since `{date}`)",
+                    "*Trade ID:* `{trade_id}` `(since {date})`",
                     "*Current Pair:* {pair}",
                     "*Amount:* `{amount}`",
                     "*Open Rate:* `{open_rate:.8f}`",
@@ -205,7 +205,9 @@ class Telegram(RPC):
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
-                    "*Stoploss:* `{stop_loss:.8f}`",
+                    "*Initial Stoploss:* `{initial_stop_loss:.8f}` `({initial_stop_loss_pct}%)`"
+                    if r['stop_loss'] != r['initial_stop_loss'] else "",
+                    "*Stoploss:* `{stop_loss:.8f}` `({stop_loss_pct}%)`",
                     "*Open Order:* `{open_order}`" if r['open_order'] else "",
                 ]
                 messages.append("\n".join(filter(None, lines)).format(**r))
diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index 627768ec2..b454f9cd8 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -59,6 +59,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'close_profit': None,
         'current_profit': -0.59,
         'stop_loss': 0.0,
+        'initial_stop_loss': 0.0,
+        'initial_stop_loss_pct': None,
+        'stop_loss_pct': None,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 
@@ -80,6 +83,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         'close_profit': None,
         'current_profit': ANY,
         'stop_loss': 0.0,
+        'initial_stop_loss': 0.0,
+        'initial_stop_loss_pct': None,
+        'stop_loss_pct': None,
         'open_order': '(limit buy rem=0.00000000)'
     } == results[0]
 
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index fb2d71d4f..b6d12fe41 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -196,7 +196,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
             'amount': 90.99181074,
             'close_profit': None,
             'current_profit': -0.59,
+            'initial_stop_loss': 1.098e-05,
             'stop_loss': 1.099e-05,
+            'initial_stop_loss_pct': -0.05,
+            'stop_loss_pct': -0.01,
             'open_order': '(limit buy rem=0.00000000)'
         }]),
         _status_table=status_table,

From 8546db9dfdccd813fc3e0400fb8cb6aff84bcf1a Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Mon, 1 Apr 2019 20:23:13 +0300
Subject: [PATCH 142/928] wording in the log message

---
 freqtrade/freqtradebot.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index c14f0d31a..e9b8b1956 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -363,7 +363,7 @@ class FreqtradeBot(object):
                 logger.debug('Ignoring %s in pair whitelist', trade.pair)
 
         if not whitelist:
-            logger.info("No currency pairs left in whitelist, no trades can be created.")
+            logger.info("No currency pair left in whitelist, no more trade can be created.")
             return False
 
         # running get_signal on historical data fetched

From a3b0135557bf221760f2f19fe9db323e9f7b2191 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Mon, 1 Apr 2019 19:25:13 +0200
Subject: [PATCH 143/928] documentation added for telegram

---
 docs/bot-usage.md      |  2 +-
 docs/telegram-usage.md | 68 +++++++++++++++++++++---------------------
 2 files changed, 35 insertions(+), 35 deletions(-)

diff --git a/docs/bot-usage.md b/docs/bot-usage.md
index 5ec390d5c..55988985a 100644
--- a/docs/bot-usage.md
+++ b/docs/bot-usage.md
@@ -79,7 +79,7 @@ prevent unintended disclosure of sensitive private data when you publish example
 of your configuration in the project issues or in the Internet.
 
 See more details on this technique with examples in the documentation page on
-[configuration](bot-configuration.md).
+[configuration](configuration.md).
 
 ### How to use **--strategy**?
 
diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 381a47ae9..4cc8eaa5c 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -65,14 +65,14 @@ Once all positions are sold, run `/stop` to completely stop the bot.
 
 For each open trade, the bot will send you the following message.
 
-> **Trade ID:** `123` `(since 1 days ago)`
-> **Current Pair:** CVC/BTC
-> **Open Since:** `1 days ago`
-> **Amount:** `26.64180098`
-> **Open Rate:** `0.00007489`
-> **Current Rate:** `0.00007489`
-> **Current Profit:** `12.95%`
-> **Stoploss:** `0.00007389 (-0.02%)`
+> **Trade ID:** `123` `(since 1 days ago)`  
+> **Current Pair:** CVC/BTC  
+> **Open Since:** `1 days ago`  
+> **Amount:** `26.64180098`  
+> **Open Rate:** `0.00007489`  
+> **Current Rate:** `0.00007489`  
+> **Current Profit:** `12.95%`  
+> **Stoploss:** `0.00007389 (-0.02%)`  
 
 ### /status table
 
@@ -97,18 +97,18 @@ current    max
 
 Return a summary of your profit/loss and performance.
 
-> **ROI:** Close trades
->   ∙ `0.00485701 BTC (258.45%)`
->   ∙ `62.968 USD`
-> **ROI:** All trades
->   ∙ `0.00255280 BTC (143.43%)`
->   ∙ `33.095 EUR`
->
-> **Total Trade Count:** `138`
-> **First Trade opened:** `3 days ago`
-> **Latest Trade opened:** `2 minutes ago`
-> **Avg. Duration:** `2:33:45`
-> **Best Performing:** `PAY/BTC: 50.23%`
+> **ROI:** Close trades  
+>   ∙ `0.00485701 BTC (258.45%)`  
+>   ∙ `62.968 USD`  
+> **ROI:** All trades  
+>   ∙ `0.00255280 BTC (143.43%)`  
+>   ∙ `33.095 EUR`  
+>  
+> **Total Trade Count:** `138`  
+> **First Trade opened:** `3 days ago`  
+> **Latest Trade opened:** `2 minutes ago`  
+> **Avg. Duration:** `2:33:45`  
+> **Best Performing:** `PAY/BTC: 50.23%`  
 
 ### /forcesell 
 
@@ -126,26 +126,26 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
 
 Return the performance of each crypto-currency the bot has sold.
 > Performance:
-> 1. `RCN/BTC 57.77%`
-> 2. `PAY/BTC 56.91%`
-> 3. `VIB/BTC 47.07%`
-> 4. `SALT/BTC 30.24%`
-> 5. `STORJ/BTC 27.24%`
-> ...
+> 1. `RCN/BTC 57.77%`  
+> 2. `PAY/BTC 56.91%`  
+> 3. `VIB/BTC 47.07%`  
+> 4. `SALT/BTC 30.24%`  
+> 5. `STORJ/BTC 27.24%`  
+> ...  
 
 ### /balance
 
 Return the balance of all crypto-currency your have on the exchange.
 
-> **Currency:** BTC
-> **Available:** 3.05890234
-> **Balance:** 3.05890234
-> **Pending:** 0.0
+> **Currency:** BTC  
+> **Available:** 3.05890234  
+> **Balance:** 3.05890234  
+> **Pending:** 0.0  
 
-> **Currency:** CVC
-> **Available:** 86.64180098
-> **Balance:** 86.64180098
-> **Pending:** 0.0
+> **Currency:** CVC  
+> **Available:** 86.64180098  
+> **Balance:** 86.64180098  
+> **Pending:** 0.0  
 
 ### /daily 
 

From 34b40500c33dbddbd60f45ae9e8dcf4e619b0b18 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Mon, 1 Apr 2019 20:45:59 +0300
Subject: [PATCH 144/928] Check whitelist fetched from config for emptiness

---
 freqtrade/freqtradebot.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index e9b8b1956..42013c85f 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -79,6 +79,9 @@ class FreqtradeBot(object):
             self.config.get('edge', {}).get('enabled', False) else None
 
         self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist']
+        if not self.active_pair_whitelist:
+            raise DependencyException('Whitelist is empty.')
+
         self._init_modules()
 
         # Tell the systemd that we completed initialization phase

From ab0e657d7711bdb4a1ff7b60652a4e2030b8cbc3 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Mon, 1 Apr 2019 21:36:53 +0300
Subject: [PATCH 145/928] Check for empty whitelist moved to _process()

---
 freqtrade/freqtradebot.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 42013c85f..e5400c1d0 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -79,8 +79,6 @@ class FreqtradeBot(object):
             self.config.get('edge', {}).get('enabled', False) else None
 
         self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist']
-        if not self.active_pair_whitelist:
-            raise DependencyException('Whitelist is empty.')
 
         self._init_modules()
 
@@ -198,6 +196,9 @@ class FreqtradeBot(object):
             # Refresh whitelist
             self.pairlists.refresh_pairlist()
             self.active_pair_whitelist = self.pairlists.whitelist
+            if not self.active_pair_whitelist:
+                logger.warning('Whitelist is empty.')
+                return False
 
             # Calculating Edge positioning
             if self.edge:

From 0cfdce0d5e69a8b79c95bbac839ed37583dcd9b8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 2 Apr 2019 07:12:48 +0200
Subject: [PATCH 146/928] Update function name from update_open_order to
 update_trade_state

---
 freqtrade/freqtradebot.py            |  6 +++---
 freqtrade/tests/test_freqtradebot.py | 28 ++++++++++------------------
 2 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 5b1e5625d..55ef6f611 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -502,7 +502,7 @@ class FreqtradeBot(object):
 
         # Update fees if order is closed
         if order_status == 'closed':
-            self.update_open_order(trade, order)
+            self.update_trade_state(trade, order)
 
         Trade.session.add(trade)
         Trade.session.flush()
@@ -534,7 +534,7 @@ class FreqtradeBot(object):
         :return: True if executed
         """
         try:
-            self.update_open_order(trade)
+            self.update_trade_state(trade)
 
             if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open:
                 result = self.handle_stoploss_on_exchange(trade)
@@ -599,7 +599,7 @@ class FreqtradeBot(object):
                         f"(from {order_amount} to {real_amount}) from Trades")
         return real_amount
 
-    def update_open_order(self, trade, action_order: dict = None):
+    def update_trade_state(self, trade, action_order: dict = None):
         """
         Checks trades with open orders and updates the amount if necessary
         """
diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index af8fc3b09..416a085ad 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -1288,14 +1288,6 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
     # test amount modified by fee-logic
     assert not freqtrade.process_maybe_execute_sell(trade)
 
-    trade.is_open = True
-    trade.open_order_id = None
-    # Assert we call handle_trade() if trade is feasible for execution
-    freqtrade.update_open_order(trade)
-
-    regexp = re.compile('Found open order for.*')
-    assert filter(regexp.match, caplog.record_tuples)
-
 
 def test_process_maybe_execute_sell_exception(mocker, default_conf,
                                               limit_buy_order, caplog) -> None:
@@ -1308,14 +1300,14 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
 
     # Test raise of DependencyException exception
     mocker.patch(
-        'freqtrade.freqtradebot.FreqtradeBot.update_open_order',
+        'freqtrade.freqtradebot.FreqtradeBot.update_trade_state',
         side_effect=DependencyException()
     )
     freqtrade.process_maybe_execute_sell(trade)
     assert log_has('Unable to sell trade: ', caplog.record_tuples)
 
 
-def test_update_open_order(mocker, default_conf, limit_buy_order, caplog) -> None:
+def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
@@ -1329,7 +1321,7 @@ def test_update_open_order(mocker, default_conf, limit_buy_order, caplog) -> Non
     Trade.session = MagicMock()
     trade.open_order_id = '123'
     trade.open_fee = 0.001
-    freqtrade.update_open_order(trade)
+    freqtrade.update_trade_state(trade)
     # Test amount not modified by fee-logic
     assert not log_has_re(r'Applying fee to .*', caplog.record_tuples)
     assert trade.open_order_id is None
@@ -1339,20 +1331,20 @@ def test_update_open_order(mocker, default_conf, limit_buy_order, caplog) -> Non
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
     assert trade.amount != 90.81
     # test amount modified by fee-logic
-    freqtrade.update_open_order(trade)
+    freqtrade.update_trade_state(trade)
     assert trade.amount == 90.81
     assert trade.open_order_id is None
 
     trade.is_open = True
     trade.open_order_id = None
     # Assert we call handle_trade() if trade is feasible for execution
-    freqtrade.update_open_order(trade)
+    freqtrade.update_trade_state(trade)
 
     regexp = re.compile('Found open order for.*')
     assert filter(regexp.match, caplog.record_tuples)
 
 
-def test_update_open_order_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker):
+def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker):
     mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
     # get_order should not be called!!
     mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
@@ -1367,13 +1359,13 @@ def test_update_open_order_withorderdict(default_conf, trades_for_order, limit_b
         open_rate=0.245441,
         open_order_id="123456"
     )
-    freqtrade.update_open_order(trade, limit_buy_order)
+    freqtrade.update_trade_state(trade, limit_buy_order)
     assert trade.amount != amount
     assert trade.amount == limit_buy_order['amount']
 
 
-def test_update_open_order_exception(mocker, default_conf,
-                                     limit_buy_order, caplog) -> None:
+def test_update_trade_state_exception(mocker, default_conf,
+                                      limit_buy_order, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
 
@@ -1386,7 +1378,7 @@ def test_update_open_order_exception(mocker, default_conf,
         'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
         side_effect=OperationalException()
     )
-    freqtrade.update_open_order(trade)
+    freqtrade.update_trade_state(trade)
     assert log_has('Could not update trade amount: ', caplog.record_tuples)
 
 

From b9b76977b69c3e846944517eb86d57d72023f041 Mon Sep 17 00:00:00 2001
From: pyup-bot 
Date: Tue, 2 Apr 2019 12:38:06 +0000
Subject: [PATCH 147/928] Update ccxt from 1.18.420 to 1.18.425

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index bdad28181..83d77b693 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-ccxt==1.18.420
+ccxt==1.18.425
 SQLAlchemy==1.3.1
 python-telegram-bot==11.1.0
 arrow==0.13.1

From 389feda65f6b25eb1344a3f37217793fc2a00129 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 18:25:17 +0200
Subject: [PATCH 148/928] Invalid order exception added

---
 freqtrade/__init__.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py
index 0d1ae9c26..30fed8c53 100644
--- a/freqtrade/__init__.py
+++ b/freqtrade/__init__.py
@@ -17,6 +17,14 @@ class OperationalException(BaseException):
     """
 
 
+class InvalidOrder(BaseException):
+    """
+    This is returned when the order is not valid. Example:
+    If stoploss on exchange order is hit, then trying to cancel the order
+    should return this exception.
+    """
+
+
 class TemporaryError(BaseException):
     """
     Temporary network or exchange related error.

From 99d256422e24fe39c7b3f0642dcec074b100daf3 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 18:31:03 +0200
Subject: [PATCH 149/928] adding InvalidOrder to exchange

---
 freqtrade/exchange/exchange.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 011be58e5..2b7e41847 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -13,7 +13,8 @@ import ccxt
 import ccxt.async_support as ccxt_async
 from pandas import DataFrame
 
-from freqtrade import constants, DependencyException, OperationalException, TemporaryError
+from freqtrade import (constants, DependencyException, OperationalException,
+                       TemporaryError, InvalidOrder)
 from freqtrade.data.converter import parse_ticker_dataframe
 
 logger = logging.getLogger(__name__)

From 40df0dcf3d5653dc8aba3cf68f724480885b2a34 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 18:45:18 +0200
Subject: [PATCH 150/928] tests fixed

---
 freqtrade/__init__.py                     | 2 +-
 freqtrade/exchange/exchange.py            | 4 ++--
 freqtrade/tests/exchange/test_exchange.py | 7 ++++---
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py
index 30fed8c53..292613297 100644
--- a/freqtrade/__init__.py
+++ b/freqtrade/__init__.py
@@ -17,7 +17,7 @@ class OperationalException(BaseException):
     """
 
 
-class InvalidOrder(BaseException):
+class InvalidOrderException(BaseException):
     """
     This is returned when the order is not valid. Example:
     If stoploss on exchange order is hit, then trying to cancel the order
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 2b7e41847..7f4ab062c 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -14,7 +14,7 @@ import ccxt.async_support as ccxt_async
 from pandas import DataFrame
 
 from freqtrade import (constants, DependencyException, OperationalException,
-                       TemporaryError, InvalidOrder)
+                       TemporaryError, InvalidOrderException)
 from freqtrade.data.converter import parse_ticker_dataframe
 
 logger = logging.getLogger(__name__)
@@ -608,7 +608,7 @@ class Exchange(object):
         try:
             return self._api.cancel_order(order_id, pair)
         except ccxt.InvalidOrder as e:
-            raise DependencyException(
+            raise InvalidOrderException(
                 f'Could not cancel order. Message: {e}')
         except (ccxt.NetworkError, ccxt.ExchangeError) as e:
             raise TemporaryError(
diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py
index eed16d39b..4deb74c67 100644
--- a/freqtrade/tests/exchange/test_exchange.py
+++ b/freqtrade/tests/exchange/test_exchange.py
@@ -11,7 +11,8 @@ import ccxt
 import pytest
 from pandas import DataFrame
 
-from freqtrade import DependencyException, OperationalException, TemporaryError
+from freqtrade import (DependencyException, OperationalException,
+                       TemporaryError, InvalidOrderException)
 from freqtrade.exchange import Binance, Exchange, Kraken
 from freqtrade.exchange.exchange import API_RETRY_COUNT
 from freqtrade.resolvers.exchange_resolver import ExchangeResolver
@@ -1233,11 +1234,11 @@ def test_cancel_order(default_conf, mocker, exchange_name):
     exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
     assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
 
-    with pytest.raises(DependencyException):
+    with pytest.raises(InvalidOrderException):
         api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
         exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
         exchange.cancel_order(order_id='_', pair='TKN/BTC')
-    assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
+    assert api_mock.cancel_order.call_count == 1
 
     ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
                            "cancel_order", "cancel_order",

From 54f11ad60388d5c9e3da5c90424174a914f1c00c Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 18:57:06 +0200
Subject: [PATCH 151/928] enriching TSL log

---
 freqtrade/freqtradebot.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 55ef6f611..fca3e346d 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -745,8 +745,8 @@ class FreqtradeBot(object):
             update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
             if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
                 # cancelling the current stoploss on exchange first
-                logger.info('Trailing stoploss: cancelling current stoploss on exchange '
-                            'in order to add another one ...')
+                logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
+                            'in order to add another one ...', order['id'])
                 if self.exchange.cancel_order(order['id'], trade.pair):
                     # creating the new one
                     stoploss_order_id = self.exchange.stoploss_limit(

From a6daf0d991469e5db365a3106bd848a22fb1f5a7 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 20:00:58 +0200
Subject: [PATCH 152/928] formatting pct

---
 freqtrade/rpc/telegram.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index ca9b376ec..9d1f9f206 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -205,9 +205,11 @@ class Telegram(RPC):
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
-                    "*Initial Stoploss:* `{initial_stop_loss:.8f}` `({initial_stop_loss_pct}%)`"
+                    "*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
+                    ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "")
                     if r['stop_loss'] != r['initial_stop_loss'] else "",
-                    "*Stoploss:* `{stop_loss:.8f}` `({stop_loss_pct}%)`",
+                    "*Stoploss:* `{stop_loss:.8f}` " +
+                    ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""),
                     "*Open Order:* `{open_order}`" if r['open_order'] else "",
                 ]
                 messages.append("\n".join(filter(None, lines)).format(**r))

From 7b39a3084fc15406cd56fc17ea4eb8a539d47d13 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Tue, 2 Apr 2019 20:08:10 +0200
Subject: [PATCH 153/928] formatting and readability

---
 freqtrade/rpc/telegram.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 9d1f9f206..2d822820f 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -205,12 +205,17 @@ class Telegram(RPC):
                     "*Current Rate:* `{current_rate:.8f}`",
                     "*Close Profit:* `{close_profit}`" if r['close_profit'] else "",
                     "*Current Profit:* `{current_profit:.2f}%`",
+
+                    # Adding initial stoploss only if it is different from stoploss
                     "*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
                     ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "")
                     if r['stop_loss'] != r['initial_stop_loss'] else "",
+
+                    # Adding stoploss and stoploss percentage only if it is not None
                     "*Stoploss:* `{stop_loss:.8f}` " +
                     ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""),
-                    "*Open Order:* `{open_order}`" if r['open_order'] else "",
+
+                    "*Open Order:* `{open_order}`" if r['open_order'] else ""
                 ]
                 messages.append("\n".join(filter(None, lines)).format(**r))
 

From 62141d3d2790c55d779b296528e224754d1fae7f Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Tue, 2 Apr 2019 21:57:52 +0300
Subject: [PATCH 154/928] test cloned, separate tests for worker and freqtrade
 states

---
 freqtrade/tests/test_freqtradebot.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index 8bc071071..250e43c3b 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -82,7 +82,17 @@ def patch_RPCManager(mocker) -> MagicMock:
 
 # Unit tests
 
-def test_freqtradebot(mocker, default_conf, markets) -> None:
+def test_freqtradebot_state(mocker, default_conf, markets) -> None:
+    mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    assert freqtrade.state is State.RUNNING
+
+    default_conf.pop('initial_state')
+    freqtrade = FreqtradeBot(config=default_conf)
+    assert freqtrade.state is State.STOPPED
+
+
+def test_worker_state(mocker, default_conf, markets) -> None:
     mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
     worker = get_patched_worker(mocker, default_conf)
     assert worker.state is State.RUNNING

From b0ddb33acc5fb2eb3d5d30068853456e44af6c81 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Tue, 2 Apr 2019 22:36:30 +0300
Subject: [PATCH 155/928] tests cleanup: Worker --> FreqtradeBot where the
 Worker object is not really needed

---
 freqtrade/tests/rpc/test_rpc.py          |  55 ++++-------
 freqtrade/tests/rpc/test_rpc_telegram.py | 116 +++++++++--------------
 2 files changed, 64 insertions(+), 107 deletions(-)

diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index 46033ec23..b7b041695 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -16,7 +16,6 @@ from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
 from freqtrade.state import State
 from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
 from freqtrade.tests.test_freqtradebot import patch_get_signal
-from freqtrade.worker import Worker
 
 
 # Functions for recurrent object patching
@@ -39,8 +38,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -97,8 +95,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -135,8 +132,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
@@ -191,8 +187,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
@@ -281,8 +276,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
@@ -352,8 +346,7 @@ def test_rpc_balance_handle(default_conf, mocker):
         get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -382,8 +375,7 @@ def test_rpc_start(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -407,8 +399,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -433,8 +424,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -467,8 +457,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -561,8 +550,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -599,8 +587,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -631,8 +618,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order
         buy=buy_mm
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -659,8 +645,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order
     # Test not buying
     default_conf['stake_amount'] = 0.0000001
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -676,8 +661,7 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -691,8 +675,7 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
@@ -706,8 +689,7 @@ def test_rpc_whitelist(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     rpc = RPC(freqtradebot)
     ret = rpc._rpc_whitelist()
@@ -724,8 +706,7 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     rpc = RPC(freqtradebot)
     ret = rpc._rpc_whitelist()
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index 384cdcfa0..c0b77076f 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -14,15 +14,15 @@ from telegram.error import NetworkError
 
 from freqtrade import __version__
 from freqtrade.edge import PairInfo
+from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.persistence import Trade
 from freqtrade.rpc import RPCMessageType
 from freqtrade.rpc.telegram import Telegram, authorized_only
 from freqtrade.state import State
 from freqtrade.strategy.interface import SellType
-from freqtrade.tests.conftest import (get_patched_freqtradebot, get_patched_worker,
+from freqtrade.tests.conftest import (get_patched_freqtradebot,
                                       log_has, patch_coinmarketcap, patch_exchange)
 from freqtrade.tests.test_freqtradebot import patch_get_signal
-from freqtrade.worker import Worker
 
 
 class DummyCls(Telegram):
@@ -99,8 +99,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
 
     default_conf['telegram']['enabled'] = False
 
-    worker = Worker(args=None, config=default_conf)
-    bot = worker.freqtrade
+    bot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
@@ -129,8 +128,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
 
     default_conf['telegram']['enabled'] = False
 
-    worker = Worker(args=None, config=default_conf)
-    bot = worker.freqtrade
+    bot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
@@ -159,8 +157,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
 
     default_conf['telegram']['enabled'] = False
 
-    worker = Worker(args=None, config=default_conf)
-    bot = worker.freqtrade
+    bot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
@@ -216,8 +213,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -254,21 +250,20 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
 
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.STOPPED
+    freqtradebot.state = State.STOPPED
     # Status is also enabled when stopped
     telegram._status(bot=MagicMock(), update=update)
     assert msg_mock.call_count == 1
     assert 'no active trade' in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
-    worker.state = State.RUNNING
+    freqtradebot.state = State.RUNNING
     telegram._status(bot=MagicMock(), update=update)
     assert msg_mock.call_count == 1
     assert 'no active trade' in msg_mock.call_args_list[0][0][0]
@@ -303,21 +298,20 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
 
     default_conf['stake_amount'] = 15.0
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
 
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.STOPPED
+    freqtradebot.state = State.STOPPED
     # Status table is also enabled when stopped
     telegram._status_table(bot=MagicMock(), update=update)
     assert msg_mock.call_count == 1
     assert 'no active order' in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
-    worker.state = State.RUNNING
+    freqtradebot.state = State.RUNNING
     telegram._status_table(bot=MagicMock(), update=update)
     assert msg_mock.call_count == 1
     assert 'no active order' in msg_mock.call_args_list[0][0][0]
@@ -359,8 +353,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -426,15 +419,14 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
     # Try invalid data
     msg_mock.reset_mock()
-    worker.state = State.RUNNING
+    freqtradebot.state = State.RUNNING
     update.message.text = '/daily -2'
     telegram._daily(bot=MagicMock(), update=update)
     assert msg_mock.call_count == 1
@@ -442,7 +434,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
 
     # Try invalid data
     msg_mock.reset_mock()
-    worker.state = State.RUNNING
+    freqtradebot.state = State.RUNNING
     update.message.text = '/daily today'
     telegram._daily(bot=MagicMock(), update=update)
     assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
@@ -467,8 +459,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -614,14 +605,13 @@ def test_start_handle(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.STOPPED
-    assert worker.state == State.STOPPED
+    freqtradebot.state = State.STOPPED
+    assert freqtradebot.state == State.STOPPED
     telegram._start(bot=MagicMock(), update=update)
-    assert worker.state == State.RUNNING
+    assert freqtradebot.state == State.RUNNING
     assert msg_mock.call_count == 1
 
 
@@ -633,14 +623,13 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.RUNNING
-    assert worker.state == State.RUNNING
+    freqtradebot.state = State.RUNNING
+    assert freqtradebot.state == State.RUNNING
     telegram._start(bot=MagicMock(), update=update)
-    assert worker.state == State.RUNNING
+    assert freqtradebot.state == State.RUNNING
     assert msg_mock.call_count == 1
     assert 'already running' in msg_mock.call_args_list[0][0][0]
 
@@ -654,14 +643,13 @@ def test_stop_handle(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.RUNNING
-    assert worker.state == State.RUNNING
+    freqtradebot.state = State.RUNNING
+    assert freqtradebot.state == State.RUNNING
     telegram._stop(bot=MagicMock(), update=update)
-    assert worker.state == State.STOPPED
+    assert freqtradebot.state == State.STOPPED
     assert msg_mock.call_count == 1
     assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
 
@@ -675,14 +663,13 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.STOPPED
-    assert worker.state == State.STOPPED
+    freqtradebot.state = State.STOPPED
+    assert freqtradebot.state == State.STOPPED
     telegram._stop(bot=MagicMock(), update=update)
-    assert worker.state == State.STOPPED
+    assert freqtradebot.state == State.STOPPED
     assert msg_mock.call_count == 1
     assert 'already stopped' in msg_mock.call_args_list[0][0][0]
 
@@ -696,9 +683,7 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
-
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
     assert freqtradebot.config['max_open_trades'] != 0
@@ -718,14 +703,13 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
         _send_msg=msg_mock
     )
 
-    worker = get_patched_worker(mocker, default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     telegram = Telegram(freqtradebot)
 
-    worker.state = State.RUNNING
-    assert worker.state == State.RUNNING
+    freqtradebot.state = State.RUNNING
+    assert freqtradebot.state == State.RUNNING
     telegram._reload_conf(bot=MagicMock(), update=update)
-    assert worker.state == State.RELOAD_CONF
+    assert freqtradebot.state == State.RELOAD_CONF
     assert msg_mock.call_count == 1
     assert 'reloading config' in msg_mock.call_args_list[0][0][0]
 
@@ -745,8 +729,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
         validate_pairs=MagicMock(return_value={})
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -798,8 +781,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
         validate_pairs=MagicMock(return_value={})
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -854,8 +836,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
         validate_pairs=MagicMock(return_value={})
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -899,8 +880,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
     )
     patch_exchange(mocker)
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -943,8 +923,7 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
     fbuy_mock = MagicMock(return_value=None)
     mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -980,8 +959,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non
         validate_pairs=MagicMock(return_value={})
     )
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -1012,8 +990,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
@@ -1054,8 +1031,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
     )
     mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
 
-    worker = Worker(args=None, config=default_conf)
-    freqtradebot = worker.freqtrade
+    freqtradebot = FreqtradeBot(config=default_conf)
 
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)

From d54acca53abdc2d8cc6752a687f84c53c9390c73 Mon Sep 17 00:00:00 2001
From: hroff-1902 
Date: Wed, 3 Apr 2019 00:55:59 +0300
Subject: [PATCH 156/928] move tests back to original codebase to minimize
 changes

---
 freqtrade/tests/rpc/test_rpc.py          | 55 ++++++++---------------
 freqtrade/tests/rpc/test_rpc_telegram.py | 57 +++++++-----------------
 freqtrade/tests/test_freqtradebot.py     |  2 +-
 3 files changed, 36 insertions(+), 78 deletions(-)

diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py
index 4a3cafbd2..b454f9cd8 100644
--- a/freqtrade/tests/rpc/test_rpc.py
+++ b/freqtrade/tests/rpc/test_rpc.py
@@ -37,8 +37,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
 
@@ -101,8 +100,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
 
@@ -137,8 +135,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
     fiat_display_currency = default_conf['fiat_display_currency']
@@ -191,8 +188,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
     fiat_display_currency = default_conf['fiat_display_currency']
@@ -280,8 +276,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     stake_currency = default_conf['stake_currency']
     fiat_display_currency = default_conf['fiat_display_currency']
@@ -349,8 +344,7 @@ def test_rpc_balance_handle(default_conf, mocker):
         get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     rpc._fiat_converter = CryptoToFiatConverter()
@@ -377,8 +371,7 @@ def test_rpc_start(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     freqtradebot.state = State.STOPPED
@@ -400,8 +393,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     freqtradebot.state = State.RUNNING
@@ -424,8 +416,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
         get_ticker=MagicMock()
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     freqtradebot.state = State.RUNNING
@@ -456,8 +447,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
 
@@ -548,8 +538,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
 
@@ -584,8 +573,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
         markets=PropertyMock(return_value=markets)
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
 
@@ -614,8 +602,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order
         buy=buy_mm
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     pair = 'ETH/BTC'
@@ -640,9 +627,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order
 
     # Test not buying
     default_conf['stake_amount'] = 0.0000001
-
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     pair = 'TKN/BTC'
@@ -656,8 +641,7 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     pair = 'ETH/BTC'
@@ -669,8 +653,7 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     pair = 'ETH/BTC'
@@ -682,8 +665,7 @@ def test_rpc_whitelist(mocker, default_conf) -> None:
     patch_exchange(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     rpc = RPC(freqtradebot)
     ret = rpc._rpc_whitelist()
     assert ret['method'] == 'StaticPairList'
@@ -698,8 +680,7 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     rpc = RPC(freqtradebot)
     ret = rpc._rpc_whitelist()
     assert ret['method'] == 'VolumePairList'
diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py
index c3ab5064c..b6d12fe41 100644
--- a/freqtrade/tests/rpc/test_rpc_telegram.py
+++ b/freqtrade/tests/rpc/test_rpc_telegram.py
@@ -96,9 +96,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
     update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
 
     default_conf['telegram']['enabled'] = False
-
-    bot = FreqtradeBot(config=default_conf)
-
+    bot = FreqtradeBot(default_conf)
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
     dummy.dummy_handler(bot=MagicMock(), update=update)
@@ -124,9 +122,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
     update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
 
     default_conf['telegram']['enabled'] = False
-
-    bot = FreqtradeBot(config=default_conf)
-
+    bot = FreqtradeBot(default_conf)
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
     dummy.dummy_handler(bot=MagicMock(), update=update)
@@ -153,8 +149,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
 
     default_conf['telegram']['enabled'] = False
 
-    bot = FreqtradeBot(config=default_conf)
-
+    bot = FreqtradeBot(default_conf)
     patch_get_signal(bot, (True, False))
     dummy = DummyCls(bot)
 
@@ -212,8 +207,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -248,8 +242,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
 
     telegram = Telegram(freqtradebot)
@@ -301,9 +294,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
     default_conf['stake_amount'] = 15.0
-
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
 
     telegram = Telegram(freqtradebot)
@@ -356,8 +347,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -421,8 +411,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -460,8 +449,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -724,8 +712,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
         validate_pairs=MagicMock(return_value={})
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -775,8 +762,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
         validate_pairs=MagicMock(return_value={})
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -829,8 +815,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
         validate_pairs=MagicMock(return_value={})
     )
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -872,8 +857,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
     )
     patch_exchange(mocker)
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -914,8 +898,7 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
     fbuy_mock = MagicMock(return_value=None)
     mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
 
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -948,9 +931,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non
         markets=PropertyMock(markets),
         validate_pairs=MagicMock(return_value={})
     )
-
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -978,9 +959,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
         validate_pairs=MagicMock(return_value={})
     )
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
-
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
@@ -1018,9 +997,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
         markets=PropertyMock(markets)
     )
     mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
-
-    freqtradebot = FreqtradeBot(config=default_conf)
-
+    freqtradebot = FreqtradeBot(default_conf)
     patch_get_signal(freqtradebot, (True, False))
     telegram = Telegram(freqtradebot)
 
diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index 6790f513c..211683d64 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -88,7 +88,7 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None:
     assert freqtrade.state is State.RUNNING
 
     default_conf.pop('initial_state')
-    freqtrade = FreqtradeBot(config=default_conf)
+    freqtrade = FreqtradeBot(default_conf)
     assert freqtrade.state is State.STOPPED
 
 

From 53eaf85969b31b282b5bf5196d7acde51145d5f7 Mon Sep 17 00:00:00 2001
From: Misagh 
Date: Wed, 3 Apr 2019 14:03:28 +0200
Subject: [PATCH 157/928] filtering edge pairs for RPC

---
 freqtrade/edge/__init__.py | 18 ++++++++++++++++++
 freqtrade/rpc/rpc.py       | 11 +----------
 freqtrade/rpc/telegram.py  |  1 -
 3 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py
index b4dfa5624..0f70df43b 100644
--- a/freqtrade/edge/__init__.py
+++ b/freqtrade/edge/__init__.py
@@ -203,6 +203,24 @@ class Edge():
 
         return self._final_pairs
 
+    def accepted_pairs(self) -> list:
+        """
+        return a list of accepted pairs along with their winrate, expectancy and stoploss
+        ex:
+        #[{'Pair': 'ADX/ETH', 'Winrate': 0.08333333333333333, 'Expectancy': -0.8105153934775888, 'Stoploss': -0.02}]
+        """
+        final = []
+        for pair, info in self._cached_pairs.items():
+            if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
+                info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
+                final.append({
+                    'Pair': pair,
+                    'Winrate': info.winrate,
+                    'Expectancy': info.expectancy,
+                    'Stoploss': info.stoploss,
+                })
+        return final
+
     def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
         """
         The result frame contains a number of columns that are calculable
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 5308c9d51..79bfffb1d 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -484,13 +484,4 @@ class RPC(object):
         """ Returns information related to Edge """
         if not self._freqtrade.edge:
             raise RPCException(f'Edge is not enabled.')
-
-        return [
-            {
-                'Pair': k,
-                'Winrate': v.winrate,
-                'Expectancy': v.expectancy,
-                'Stoploss': v.stoploss,
-            }
-            for k, v in self._freqtrade.edge._cached_pairs.items()
-        ]
+        return self._freqtrade.edge.accepted_pairs()
\ No newline at end of file
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 2d822820f..8ff59d759 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -511,7 +511,6 @@ class Telegram(RPC):
         """
         try:
             edge_pairs = self._rpc_edge()
-            print(edge_pairs)
             edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
             message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) From 5f38d5ee6309aba3852ee832b6ecbff6ce50b5fe Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 14:07:33 +0200 Subject: [PATCH 158/928] removing % sign as it is already a pct --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2d822820f..81f9dc4f0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -208,12 +208,12 @@ class Telegram(RPC): # Adding initial stoploss only if it is different from stoploss "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + - ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") + ("`({initial_stop_loss_pct:.2f})`" if r['initial_stop_loss_pct'] else "") if r['stop_loss'] != r['initial_stop_loss'] else "", # Adding stoploss and stoploss percentage only if it is not None "*Stoploss:* `{stop_loss:.8f}` " + - ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), + ("`({stop_loss_pct:.2f})`" if r['stop_loss_pct'] else ""), "*Open Order:* `{open_order}`" if r['open_order'] else "" ] From a3835b1279d0d0b7fd7df2b34d9a74d6b2db0cf8 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 14:14:47 +0200 Subject: [PATCH 159/928] flake8 --- freqtrade/edge/__init__.py | 16 +++++++--------- freqtrade/rpc/rpc.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 0f70df43b..7bda942d2 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -206,19 +206,17 @@ class Edge(): def accepted_pairs(self) -> list: """ return a list of accepted pairs along with their winrate, expectancy and stoploss - ex: - #[{'Pair': 'ADX/ETH', 'Winrate': 0.08333333333333333, 'Expectancy': -0.8105153934775888, 'Stoploss': -0.02}] """ final = [] for pair, info in self._cached_pairs.items(): if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ - info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): - final.append({ - 'Pair': pair, - 'Winrate': info.winrate, - 'Expectancy': info.expectancy, - 'Stoploss': info.stoploss, - }) + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): + final.append({ + 'Pair': pair, + 'Winrate': info.winrate, + 'Expectancy': info.expectancy, + 'Stoploss': info.stoploss, + }) return final def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 79bfffb1d..ceab00373 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -484,4 +484,4 @@ class RPC(object): """ Returns information related to Edge """ if not self._freqtrade.edge: raise RPCException(f'Edge is not enabled.') - return self._freqtrade.edge.accepted_pairs() \ No newline at end of file + return self._freqtrade.edge.accepted_pairs() From 67eeb145e189b9cc6a1adec0003334c795a6aac8 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 14:31:00 +0200 Subject: [PATCH 160/928] flake8 --- freqtrade/edge/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 7bda942d2..4801c6cb3 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -211,12 +211,12 @@ class Edge(): for pair, info in self._cached_pairs.items(): if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): - final.append({ - 'Pair': pair, - 'Winrate': info.winrate, - 'Expectancy': info.expectancy, - 'Stoploss': info.stoploss, - }) + final.append({ + 'Pair': pair, + 'Winrate': info.winrate, + 'Expectancy': info.expectancy, + 'Stoploss': info.stoploss, + }) return final def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: From eb610441b574eaac2f8603ad9f4df59a0286b932 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 3 Apr 2019 12:38:06 +0000 Subject: [PATCH 161/928] Update ccxt from 1.18.425 to 1.18.430 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 83d77b693..914c8e644 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.425 +ccxt==1.18.430 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From 92dc3c89afb525d5276c31f9e631799f45574c3f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 3 Apr 2019 12:38:07 +0000 Subject: [PATCH 162/928] Update sqlalchemy from 1.3.1 to 1.3.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 914c8e644..afb422e4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ccxt==1.18.430 -SQLAlchemy==1.3.1 +SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 From a3fe5f57579b730705390c4e6e0e2e6f4f98ac9b Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 16:28:44 +0200 Subject: [PATCH 163/928] adding stake amount to telegram message --- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/rpc/test_rpc.py | 4 ++++ freqtrade/tests/rpc/test_rpc_telegram.py | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5308c9d51..ec6c1feed 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -103,11 +103,13 @@ class RPC(object): results.append(dict( trade_id=trade.id, pair=trade.pair, + base_currency=self._freqtrade.config['stake_currency'], date=arrow.get(trade.open_date), open_rate=trade.open_rate, close_rate=trade.close_rate, current_rate=current_rate, amount=round(trade.amount, 8), + stake_amount=round(trade.amount, 8), close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), stop_loss=trade.stop_loss, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2d822820f..9a64f5197 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -199,7 +199,7 @@ class Telegram(RPC): lines = [ "*Trade ID:* `{trade_id}` `(since {date})`", "*Current Pair:* {pair}", - "*Amount:* `{amount}`", + "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index b454f9cd8..981d3edfb 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -51,11 +51,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: assert { 'trade_id': 1, 'pair': 'ETH/BTC', + 'base_currency': 'BTC', 'date': ANY, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, + 'stake_amount': 90.99181074, 'close_profit': None, 'current_profit': -0.59, 'stop_loss': 0.0, @@ -75,11 +77,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: assert { 'trade_id': 1, 'pair': 'ETH/BTC', + 'base_currency': 'BTC', 'date': ANY, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': ANY, 'amount': 90.99181074, + 'stake_amount': 90.99181074, 'close_profit': None, 'current_profit': ANY, 'stop_loss': 0.0, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b6d12fe41..8f43d7ed0 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -189,11 +189,13 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', + 'base_currency': 'BTC', 'date': arrow.utcnow(), 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, + 'stake_amount': 90.99181074, 'close_profit': None, 'current_profit': -0.59, 'initial_stop_loss': 1.098e-05, From d5498c87123e21cddb7d65e3a186eeb19e62aec0 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 19:29:44 +0200 Subject: [PATCH 164/928] adding % --- freqtrade/rpc/rpc.py | 4 ++-- freqtrade/rpc/telegram.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5308c9d51..0a6a2388a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -111,9 +111,9 @@ class RPC(object): close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), stop_loss=trade.stop_loss, - stop_loss_pct=trade.stop_loss_pct, + stop_loss_pct=(trade.stop_loss_pct * 100), initial_stop_loss=trade.initial_stop_loss, - initial_stop_loss_pct=trade.initial_stop_loss_pct, + initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100), open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 81f9dc4f0..2d822820f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -208,12 +208,12 @@ class Telegram(RPC): # Adding initial stoploss only if it is different from stoploss "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + - ("`({initial_stop_loss_pct:.2f})`" if r['initial_stop_loss_pct'] else "") + ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") if r['stop_loss'] != r['initial_stop_loss'] else "", # Adding stoploss and stoploss percentage only if it is not None "*Stoploss:* `{stop_loss:.8f}` " + - ("`({stop_loss_pct:.2f})`" if r['stop_loss_pct'] else ""), + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), "*Open Order:* `{open_order}`" if r['open_order'] else "" ] From 3c399fbe3fe1edd1ea1dab04898201edcf1026a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Apr 2019 19:51:46 +0200 Subject: [PATCH 165/928] Improve whitelist wordings --- freqtrade/freqtradebot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e5400c1d0..bf445e56d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -79,7 +79,6 @@ class FreqtradeBot(object): self.config.get('edge', {}).get('enabled', False) else None self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - self._init_modules() # Tell the systemd that we completed initialization phase @@ -196,9 +195,6 @@ class FreqtradeBot(object): # Refresh whitelist self.pairlists.refresh_pairlist() self.active_pair_whitelist = self.pairlists.whitelist - if not self.active_pair_whitelist: - logger.warning('Whitelist is empty.') - return False # Calculating Edge positioning if self.edge: @@ -360,6 +356,10 @@ class FreqtradeBot(object): interval = self.strategy.ticker_interval whitelist = copy.deepcopy(self.active_pair_whitelist) + if not whitelist: + logger.warning("Whitelist is empty.") + return False + # Remove currently opened and latest pairs from whitelist for trade in Trade.get_open_trades(): if trade.pair in whitelist: @@ -367,7 +367,7 @@ class FreqtradeBot(object): logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: - logger.info("No currency pair left in whitelist, no more trade can be created.") + logger.info("No currency pair in whitelist, but checking to sell open trades.") return False # running get_signal on historical data fetched From 1a5b0969b952603fa54a6dedd446ad93fcaf2caf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Apr 2019 19:52:06 +0200 Subject: [PATCH 166/928] Fix tests (both tests where testing the same thing) --- freqtrade/tests/test_freqtradebot.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e145c354..fc13bc0f1 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -570,7 +570,8 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, assert freqtrade._get_trade_stake_amount('ETH/BTC') is None -def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: +def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -582,16 +583,17 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke ) default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() + assert freqtrade.create_trade() assert not freqtrade.create_trade() + assert log_has("No currency pair in whitelist, but checking to sell open trades.", + caplog.record_tuples) -def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, - limit_buy_order, fee, markets, mocker) -> None: +def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, + markets, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -601,13 +603,12 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = [] freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trade() assert not freqtrade.create_trade() + assert log_has("Whitelist is empty.", caplog.record_tuples) def test_create_trade_no_signal(default_conf, fee, mocker) -> None: From 0307ba7883c55eb7b2a763feda5373df39f9210c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Apr 2019 20:04:04 +0200 Subject: [PATCH 167/928] Remove one branch - python does lazy evaluation --- freqtrade/optimize/backtesting.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f3661ab32..0fcfd11f1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -337,7 +337,6 @@ class Backtesting(object): # Loop timerange and test per pair while tmp < end_date: - # print(f"time: {tmp}") for i, pair in enumerate(ticker): if pair not in indexes: @@ -358,9 +357,9 @@ class Backtesting(object): if row.buy == 0 or row.sell == 1: continue # skip rows where no buy signal or that would immediately sell off - if not position_stacking: - if pair in lock_pair_until and row.date <= lock_pair_until[pair]: - continue + if (not position_stacking and pair in lock_pair_until + and row.date <= lock_pair_until[pair]): + continue if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date if not trade_count_lock.get(row.date, 0) < max_open_trades: From 9ee1dd99eb0e7989abb55dbbeb549ea156d4396e Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 20:28:03 +0200 Subject: [PATCH 168/928] tests fixed --- freqtrade/rpc/rpc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0a6a2388a..090eb295f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -111,11 +111,13 @@ class RPC(object): close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), stop_loss=trade.stop_loss, - stop_loss_pct=(trade.stop_loss_pct * 100), + stop_loss_pct=(trade.stop_loss_pct * 100) \ + if trade.stop_loss_pct else None, initial_stop_loss=trade.initial_stop_loss, - initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100), + initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100) \ + if trade.initial_stop_loss_pct else None, open_order='({} {} rem={:.8f})'.format( - order['type'], order['side'], order['remaining'] + order['type'], order['side'], order['remaining'] ) if order else None, )) return results From 5488c66f536672d22d529450905767c637f5cc5a Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 3 Apr 2019 20:35:37 +0200 Subject: [PATCH 169/928] flake8 --- freqtrade/rpc/rpc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 090eb295f..e59fb73f3 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -111,11 +111,11 @@ class RPC(object): close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), stop_loss=trade.stop_loss, - stop_loss_pct=(trade.stop_loss_pct * 100) \ - if trade.stop_loss_pct else None, + stop_loss_pct=(trade.stop_loss_pct * 100) + if trade.stop_loss_pct else None, initial_stop_loss=trade.initial_stop_loss, - initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100) \ - if trade.initial_stop_loss_pct else None, + initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100) + if trade.initial_stop_loss_pct else None, open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, From 65350ad55235d8fd12356dacf9ddc1dccbf56d61 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Apr 2019 22:14:42 +0300 Subject: [PATCH 170/928] final flake happy --- freqtrade/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 2440e7320..c7afe5c97 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -143,7 +143,9 @@ class Worker(object): }) logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED -### state_changed = True + # 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): From 0cdbe714d23356375c21aa7d69184e1c7e5a0a32 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 4 Apr 2019 12:06:45 +0200 Subject: [PATCH 171/928] stake amount not amount --- freqtrade/rpc/rpc.py | 2 +- freqtrade/tests/rpc/test_rpc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e3276c53f..88ade0c27 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -109,7 +109,7 @@ class RPC(object): close_rate=trade.close_rate, current_rate=current_rate, amount=round(trade.amount, 8), - stake_amount=round(trade.amount, 8), + stake_amount=round(trade.stake_amount, 8), close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), stop_loss=trade.stop_loss, diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 981d3edfb..8b10a1314 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -57,7 +57,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, - 'stake_amount': 90.99181074, + 'stake_amount': 0.001, 'close_profit': None, 'current_profit': -0.59, 'stop_loss': 0.0, @@ -83,7 +83,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'close_rate': None, 'current_rate': ANY, 'amount': 90.99181074, - 'stake_amount': 90.99181074, + 'stake_amount': 0.001, 'close_profit': None, 'current_profit': ANY, 'stop_loss': 0.0, From 6afe232c4d56f840266a7298ea963fbbd4337a5a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 4 Apr 2019 12:38:05 +0000 Subject: [PATCH 172/928] Update ccxt from 1.18.430 to 1.18.432 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index afb422e4b..eb5d564fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.430 +ccxt==1.18.432 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From ebeaf64fbbd45aa7f7e64f25286ad10aa8625de9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 4 Apr 2019 12:38:06 +0000 Subject: [PATCH 173/928] Update mypy from 0.670 to 0.700 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 69082587a..c59923ed2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,4 +9,4 @@ pytest-mock==1.10.3 pytest-asyncio==0.10.0 pytest-cov==2.6.1 coveralls==1.7.0 -mypy==0.670 +mypy==0.700 From a363d443bfd38de6934e5d1d3beab020a9771d76 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 4 Apr 2019 17:13:54 +0200 Subject: [PATCH 174/928] stoploss on exchange canceled handled --- freqtrade/freqtradebot.py | 92 +++++++++++++++++++--------- freqtrade/tests/test_freqtradebot.py | 2 +- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fca3e346d..eeefaa6d8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -690,46 +690,78 @@ class FreqtradeBot(object): is enabled. """ - result = False + logger.debug('Handling stoploss on exchange %s ...', trade) + try: - # If trade is open and the buy order is fulfilled but there is no stoploss, - # then we add a stoploss on exchange - if not trade.open_order_id and not trade.stoploss_order_id: - if self.edge: - stoploss = self.edge.stoploss(pair=trade.pair) - else: - stoploss = self.strategy.stoploss + # First we check if there is already a stoploss on exchange + stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ + if trade.stoploss_order_id else None + except DependencyException as exception: + logger.warning('Unable to fetch stoploss order: %s', exception) - stop_price = trade.open_rate * (1 + stoploss) - # limit price should be less than stop price. - # 0.99 is arbitrary here. - limit_price = stop_price * 0.99 + # If there open order id does not exist, + # it means buy order is fulfilled + buy_order_fulfilled = not trade.open_order_id + # Limit price threshold + # This is the limit price percentage below which you don't want to sell + # 0.99 is arbitrary here. + # As limit price should always be below price + limit_price_pct = 0.99 + + # If buy order is fulfilled but there is no stoploss, + # then we add a stoploss on exchange + if (buy_order_fulfilled and not stoploss_order): + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss + + stop_price = trade.open_rate * (1 + stoploss) + + # limit price should be less than stop price. + limit_price = stop_price * limit_price_pct + + try: stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price )['id'] trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_last_update = datetime.now() + return False - # Or the trade open and there is already a stoploss on exchange. - # so we check if it is hit ... - elif trade.stoploss_order_id: - logger.debug('Handling stoploss on exchange %s ...', trade) - order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) - if order['status'] == 'closed': - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - trade.update(order) - self.notify_sell(trade) - result = True - elif self.config.get('trailing_stop', False): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, order) - except DependencyException as exception: - logger.warning('Unable to create stoploss order: %s', exception) - return result + except DependencyException as exception: + logger.warning('Unable to place a stoploss order on exchange: %s', exception) + + + # If stoploss order is canceled for some reason we add it + if stoploss_order and stoploss_order['status'] == 'canceled': + try: + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, + stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct + )['id'] + trade.stoploss_order_id = str(stoploss_order_id) + return False + except DependencyException as exception: + logger.warning('Stoploss order was cancelled, but unable to recreate one: %s', exception) + + # We check if stoploss order is fulfilled + if stoploss_order and stoploss_order['status'] == 'closed': + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + trade.update(stoploss_order) + self.notify_sell(trade) + return True + + # Finally we check if stoploss on exchange should be moved up because of trailing. + if stoploss_order and self.config.get('trailing_stop', False): + # if trailing stoploss is enabled we check if stoploss value has changed + # in which case we cancel stoploss order and put another one with new + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + + return False def handle_trailing_stoploss_on_exchange(self, trade: Trade, order): """ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 416a085ad..3e8bdb001 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1036,7 +1036,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, side_effect=DependencyException() ) freqtrade.handle_stoploss_on_exchange(trade) - assert log_has('Unable to create stoploss order: ', caplog.record_tuples) + assert log_has('Unable to place a stoploss order on exchange: ', caplog.record_tuples) def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, From 31fa857319e73fff1beb48c123f8f8a65edb25d0 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 4 Apr 2019 17:15:51 +0200 Subject: [PATCH 175/928] typo --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eeefaa6d8..acff0761b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -700,7 +700,7 @@ class FreqtradeBot(object): logger.warning('Unable to fetch stoploss order: %s', exception) - # If there open order id does not exist, + # If trade open order id does not exist, # it means buy order is fulfilled buy_order_fulfilled = not trade.open_order_id From 647534a4f81b42c0e5b944fc0aa992aea53b51c3 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 4 Apr 2019 17:17:21 +0200 Subject: [PATCH 176/928] flake8 --- freqtrade/freqtradebot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index acff0761b..9bd3384fa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -699,7 +699,6 @@ class FreqtradeBot(object): except DependencyException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) - # If trade open order id does not exist, # it means buy order is fulfilled buy_order_fulfilled = not trade.open_order_id @@ -734,7 +733,6 @@ class FreqtradeBot(object): except DependencyException as exception: logger.warning('Unable to place a stoploss order on exchange: %s', exception) - # If stoploss order is canceled for some reason we add it if stoploss_order and stoploss_order['status'] == 'canceled': try: @@ -745,7 +743,8 @@ class FreqtradeBot(object): trade.stoploss_order_id = str(stoploss_order_id) return False except DependencyException as exception: - logger.warning('Stoploss order was cancelled, but unable to recreate one: %s', exception) + logger.warning('Stoploss order was cancelled, ' + 'but unable to recreate one: %s', exception) # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] == 'closed': From 7f4fd6168a272f190c4dcbf7ec65aae9a6e1ea98 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 4 Apr 2019 17:23:21 +0200 Subject: [PATCH 177/928] test for canceled SL on exchange added --- freqtrade/tests/test_freqtradebot.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 3e8bdb001..c854d99e8 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1009,7 +1009,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 - # Third case: when stoploss is set and it is hit + # Third case: when stoploss was set but it was canceled for some reason + # should set a stoploss immediately and return False + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) + mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order) + stoploss_limit.reset_mock() + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss_limit.call_count == 1 + assert trade.stoploss_order_id == "13434334" + + # 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() From 32cbb714f9bfe415a737f8c908e41a802c4ac666 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 19:44:03 +0200 Subject: [PATCH 178/928] Improve commenting on backtsting and backtest_multi_tst --- freqtrade/optimize/backtesting.py | 9 +++++++-- freqtrade/tests/optimize/test_backtesting.py | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fcfd11f1..fc007ce2b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -329,6 +329,8 @@ class Backtesting(object): end_date = args['end_date'] trades = [] trade_count_lock: Dict = {} + + # Dict of ticker-lists for performance (looping lists is a lot faster than dataframes) ticker: Dict = self._get_ticker_list(processed) lock_pair_until: Dict = {} @@ -345,10 +347,11 @@ class Backtesting(object): try: row = ticker[pair][indexes[pair]] except IndexError: - # missing Data for one pair ... + # missing Data for one pair at the end. # Warnings for this are shown by `validate_backtest_data` continue + # Waits until the time-counter reaches the start of the data for this pair. if row.date > tmp.datetime: continue @@ -359,12 +362,13 @@ class Backtesting(object): if (not position_stacking and pair in lock_pair_until and row.date <= lock_pair_until[pair]): + # without positionstacking, we can only have one open trade per pair. continue + if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date if not trade_count_lock.get(row.date, 0) < max_open_trades: continue - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], @@ -377,6 +381,7 @@ class Backtesting(object): # Set lock_pair_until to end of testing period if trade could not be closed lock_pair_until[pair] = end_date.datetime + # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.ticker_interval_mins) return DataFrame.from_records(trades, columns=BacktestResult._fields) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 64a33fae2..edfe02225 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -701,9 +701,13 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) + pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs) + # Only use 500 lines to increase performance data = trim_dictlist(data, -500) + + # Remove data for one pair from the beginning of the data data[pair] = data[pair][tres:] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} From 2aa1b43f013a29ee2a398b660cd23709eb57eed1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 4 Apr 2019 20:56:40 +0300 Subject: [PATCH 179/928] get rid of TICKER_INTERVAL_MINUTES dict, use ccxt's parse_timeframe() instead --- freqtrade/constants.py | 25 +++++++---------------- freqtrade/data/converter.py | 6 +++--- freqtrade/data/history.py | 8 +++++--- freqtrade/exchange/exchange.py | 9 +++++--- freqtrade/freqtradebot.py | 3 ++- freqtrade/misc.py | 13 ++++++++++++ freqtrade/optimize/backtesting.py | 8 ++++---- freqtrade/strategy/interface.py | 4 ++-- freqtrade/tests/optimize/__init__.py | 6 +++--- freqtrade/tests/optimize/test_optimize.py | 7 ++++--- scripts/plot_profit.py | 5 +++-- 11 files changed, 52 insertions(+), 42 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 02062acc4..5243eeb4a 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -6,7 +6,7 @@ bot constants DEFAULT_CONFIG = 'config.json' DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec -TICKER_INTERVAL = 5 # min +DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' @@ -22,22 +22,11 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 -TICKER_INTERVAL_MINUTES = { - '1m': 1, - '3m': 3, - '5m': 5, - '15m': 15, - '30m': 30, - '1h': 60, - '2h': 120, - '4h': 240, - '6h': 360, - '8h': 480, - '12h': 720, - '1d': 1440, - '3d': 4320, - '1w': 10080, -} +TICKER_INTERVALS = [ + '1m', '3m', '5m', '15m', '30m', + '1h', '2h', '4h', '6h', '8h', '12h', + '1d', '3d', '1w', +] SUPPORTED_FIAT = [ "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", @@ -52,7 +41,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': 'integer', 'minimum': -1}, - 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, + 'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { "type": ["number", "string"], diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index c32338bbe..28749293b 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -4,8 +4,8 @@ Functions to convert data from one format to another import logging import pandas as pd from pandas import DataFrame, to_datetime +from freqtrade.misc import timeframe_to_minutes -from freqtrade.constants import TICKER_INTERVAL_MINUTES logger = logging.getLogger(__name__) @@ -65,9 +65,9 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da 'close': 'last', 'volume': 'sum' } - tick_mins = TICKER_INTERVAL_MINUTES[ticker_interval] + ticker_minutes = timeframe_to_minutes(ticker_interval) # Resample to create "NAN" values - df = dataframe.resample(f'{tick_mins}min', on='date').agg(ohlc_dict) + df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) # Forwardfill close for missing columns df['close'] = df['close'].fillna(method='ffill') diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 7d89f7ad6..0ecc632e4 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -12,10 +12,12 @@ from typing import Optional, List, Dict, Tuple, Any import arrow from pandas import DataFrame -from freqtrade import misc, constants, OperationalException +from freqtrade import misc, OperationalException +from freqtrade.arguments import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange -from freqtrade.arguments import TimeRange +from freqtrade.misc import timeframe_to_minutes + logger = logging.getLogger(__name__) @@ -163,7 +165,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, if timerange.starttype == 'date': since_ms = timerange.startts * 1000 elif timerange.stoptype == 'line': - num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval] + num_minutes = timerange.stopts * timeframe_to_minutes(tick_interval) since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 011be58e5..f18a71a50 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -15,9 +15,12 @@ from pandas import DataFrame from freqtrade import constants, DependencyException, OperationalException, TemporaryError from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.misc import timeframe_to_seconds, timeframe_to_msecs + logger = logging.getLogger(__name__) + API_RETRY_COUNT = 4 @@ -502,8 +505,8 @@ class Exchange(object): # Assume exchange returns 500 candles _LIMIT = 500 - one_call = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60 * _LIMIT * 1000 - logger.debug("one_call: %s", one_call) + one_call = timeframe_to_msecs(tick_interval) * _LIMIT + logger.debug("one_call: %s msecs", one_call) input_coroutines = [self._async_get_candle_history( pair, tick_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] @@ -557,7 +560,7 @@ class Exchange(object): def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: # Calculating ticker interval in seconds - interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60 + interval_in_sec = timeframe_to_seconds(ticker_interval) return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) + interval_in_sec) >= arrow.utcnow().timestamp) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index acf47b065..e9f636ec7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,6 +16,7 @@ from freqtrade import (DependencyException, OperationalException, from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge +from freqtrade.misc import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -397,7 +398,7 @@ class FreqtradeBot(object): exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] + ticker_interval=timeframe_to_minutes(self.config['ticker_interval']) ) # Update fees if order is closed diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 38f758669..9aeaef069 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -8,6 +8,7 @@ import re from datetime import datetime from typing import Dict +from ccxt import Exchange import numpy as np from pandas import DataFrame import rapidjson @@ -131,3 +132,15 @@ def deep_merge_dicts(source, destination): destination[key] = value return destination + + +def timeframe_to_seconds(ticker_interval: str) -> int: + return Exchange.parse_timeframe(ticker_interval) + + +def timeframe_to_minutes(ticker_interval: str) -> int: + return Exchange.parse_timeframe(ticker_interval) // 60 + + +def timeframe_to_msecs(ticker_interval: str) -> int: + return Exchange.parse_timeframe(ticker_interval) * 1000 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 293511fc0..e0360cdef 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider -from freqtrade.misc import file_dump_json +from freqtrade.misc import file_dump_json, timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -77,7 +77,7 @@ class Backtesting(object): if self.config.get('strategy_list', None): # Force one interval self.ticker_interval = str(self.config.get('ticker_interval')) - self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] + self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat @@ -96,7 +96,7 @@ class Backtesting(object): self.strategy = strategy self.ticker_interval = self.config.get('ticker_interval') - self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] + self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell @@ -421,7 +421,7 @@ class Backtesting(object): min_date, max_date = optimize.get_timeframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called) optimize.validate_backtest_data(data, min_date, max_date, - constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]) + timeframe_to_minutes(self.ticker_interval)) logger.info( 'Measuring data from %s up to %s (%s days)..', min_date.isoformat(), diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index fcb27d7bd..646bd2a94 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -12,8 +12,8 @@ import warnings import arrow from pandas import DataFrame -from freqtrade import constants from freqtrade.data.dataprovider import DataProvider +from freqtrade.misc import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.wallets import Wallets @@ -221,7 +221,7 @@ class IStrategy(ABC): # Check if dataframe is out of date signal_date = arrow.get(latest['date']) - interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + interval_minutes = timeframe_to_minutes(interval) offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 075938a61..9203ec19c 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -3,11 +3,11 @@ from typing import NamedTuple, List import arrow from pandas import DataFrame +from freqtrade.misc import timeframe_to_minutes from freqtrade.strategy.interface import SellType -from freqtrade.constants import TICKER_INTERVAL_MINUTES ticker_start_time = arrow.get(2018, 10, 3) -tests_ticker_interval = "1h" +tests_ticker_interval = '1h' class BTrade(NamedTuple): @@ -32,7 +32,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): - return ticker_start_time.shift(minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval]) + return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_ticker_interval)) ).datetime.replace(tzinfo=None) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 99cd24c26..088743038 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade import optimize, constants +from freqtrade import optimize from freqtrade.arguments import TimeRange from freqtrade.data import history +from freqtrade.misc import timeframe_to_minutes from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, patch_exchange @@ -37,7 +38,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: min_date, max_date = optimize.get_timeframe(data) caplog.clear() assert optimize.validate_backtest_data(data, min_date, max_date, - constants.TICKER_INTERVAL_MINUTES["1m"]) + timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -61,5 +62,5 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: min_date, max_date = optimize.get_timeframe(data) caplog.clear() assert not optimize.validate_backtest_data(data, min_date, max_date, - constants.TICKER_INTERVAL_MINUTES["5m"]) + timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 394f02116..8330e2a88 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -28,6 +28,7 @@ from freqtrade import constants, misc from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history +from freqtrade.misc import timeframe_to_seconds from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -193,8 +194,8 @@ def define_index(min_date: int, max_date: int, interval: str) -> int: """ Return the index of a specific date """ - interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] - return int((max_date - min_date) / (interval_minutes * 60)) + interval_seconds = timeframe_to_seconds(interval) + return int((max_date - min_date) / interval_seconds) def plot_parse_args(args: List[str]) -> Namespace: From 7010c835d24bdcd5cfa3420d56d68906b2cdb60e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 20:23:10 +0200 Subject: [PATCH 180/928] Improve commentign --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc007ce2b..bbbda9b1f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -334,10 +334,11 @@ class Backtesting(object): ticker: Dict = self._get_ticker_list(processed) lock_pair_until: Dict = {} + # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = {} tmp = start_date + timedelta(minutes=self.ticker_interval_mins) - # Loop timerange and test per pair + # Loop timerange and get candle for each pair at that point in time while tmp < end_date: for i, pair in enumerate(ticker): From 6913bce6a1e84968bc4added173e82011dc75721 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 4 Apr 2019 21:30:08 +0300 Subject: [PATCH 181/928] flake8, import in script/plot_profit.py --- scripts/plot_profit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 8330e2a88..2b6360bec 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,11 +24,10 @@ import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade import constants, misc from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history -from freqtrade.misc import timeframe_to_seconds +from freqtrade.misc import common_datearray, timeframe_to_seconds from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -132,7 +131,7 @@ def plot_profit(args: Namespace) -> None: # NOTE: the dataframes are of unequal length, # 'dates' is an merged date array of them all. - dates = misc.common_datearray(dataframes) + dates = common_datearray(dataframes) min_date = int(min(dates).timestamp()) max_date = int(max(dates).timestamp()) num_iterations = define_index(min_date, max_date, tick_interval) + 1 From e3cdc0a05bcdc9ffd3901b73c64c54abf438de44 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 4 Apr 2019 20:53:28 +0200 Subject: [PATCH 182/928] typos and visual fixes --- docs/sql_cheatsheet.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 54f9b8213..803a2b48a 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -1,5 +1,5 @@ # SQL Helper -This page constains some help if you want to edit your sqlite db. +This page contains some help if you want to edit your sqlite db. ## Install sqlite3 **Ubuntu/Debian installation** @@ -66,11 +66,13 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange !!! Warning - Manually selling on the exchange should not be done by default, since the bot does not detect this and will try to sell anyway. - /foresell should accomplish the same thing. + Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. + Whenever possible, /forcesell should be used to accomplish the same thing. + + It is strongly advised to backup your database file before making any manual changes. !!! Note - This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. + This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. ```sql UPDATE trades From 7486cb7c64d6b50b5b934ffb2fa39fb3c215a8f3 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 4 Apr 2019 21:05:26 +0200 Subject: [PATCH 183/928] fix admonitions --- docs/hyperopt.md | 4 ++-- docs/sql_cheatsheet.md | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e25f35c35..b4e42de16 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -62,7 +62,7 @@ If you have updated the buy strategy, ie. changed the contents of #### Sell optimization -Similar to the buy-signal above, sell-signals can also be optimized. +Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods * Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. @@ -163,7 +163,7 @@ running at least several thousand evaluations. The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. !!! Warning -When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. + When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. ### Execute Hyperopt with Different Ticker-Data Source diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 803a2b48a..f41520bd9 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -66,9 +66,7 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange !!! Warning - Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. - Whenever possible, /forcesell should be used to accomplish the same thing. - + Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. It is strongly advised to backup your database file before making any manual changes. !!! Note From dbb1bbf1019657969d7bc68ff541f6c9ea128c0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Apr 2019 06:47:03 +0200 Subject: [PATCH 184/928] Fix webhook documentation --- docs/webhook-config.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2b5365e32..43b036c74 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -39,32 +39,30 @@ Different payloads can be configured for different events. Not all fields are ne The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. Possible parameters are: -* exchange -* pair -* limit -* stake_amount -* stake_amount_fiat -* stake_currency -* fiat_currency +* `exchange` +* `pair` +* `limit` +* `stake_amount` +* `stake_currency` +* `fiat_currency` ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: -* exchange -* pair -* gain -* limit -* amount -* open_rate -* current_rate -* profit_amount -* profit_percent -* profit_fiat -* stake_currency -* fiat_currency -* sell_reason +* `exchange` +* `pair` +* `gain` +* `limit` +* `amount` +* `open_rate` +* `current_rate` +* `profit_amount` +* `profit_percent` +* `stake_currency` +* `fiat_currency` +* `sell_reason` ### Webhookstatus From ac1964edb16dd0bf13fd1bef6bc59ce9519b229b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Apr 2019 06:49:15 +0200 Subject: [PATCH 185/928] Remove unnecessary comment --- docs/telegram-usage.md | 4 ++-- docs/webhook-config.md | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 4cc8eaa5c..9d6877318 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -1,13 +1,13 @@ # Telegram usage -This page explains how to command your bot with Telegram. - ## Prerequisite + To control your bot with Telegram, you need first to [set up a Telegram bot](installation.md) and add your Telegram API keys into your config file. ## Telegram commands + Per default, the Telegram bot shows predefined commands. Some commands are only available by sending them to the bot. The table below list the official commands. You can ask at any moment for help with `/help`. diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 43b036c74..811b57f9b 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -1,7 +1,5 @@ # Webhook usage -This page explains how to configure your bot to talk to webhooks. - ## Configuration Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`. From 13e8f25ca97843baf47b4be38515ae830f3a7a39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Apr 2019 06:51:16 +0200 Subject: [PATCH 186/928] Improve docs layout --- mkdocs.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 9a6fec851..ecac265c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,11 +3,12 @@ nav: - About: index.md - Installation: installation.md - Configuration: configuration.md - - Start the bot: bot-usage.md - - Stoploss: stoploss.md - Custom Strategy: bot-optimization.md - - Telegram: telegram-usage.md - - Web Hook: webhook-config.md + - Stoploss: stoploss.md + - Start the bot: bot-usage.md + - Control the bot: + - Telegram: telegram-usage.md + - Web Hook: webhook-config.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge positioning: edge.md From 4c5432be6f51911327a8697a357d3b7c04996207 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 5 Apr 2019 16:48:14 +0300 Subject: [PATCH 187/928] Added command line options in backtesting to override max_open_trades and stake_amount --- freqtrade/arguments.py | 16 ++++++++++++++++ freqtrade/configuration.py | 10 +++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 8d7dac4bc..b0acb4122 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -247,6 +247,22 @@ class Arguments(object): dest='timerange', ) + parser.add_argument( + '--max_open_trades', + help='Specify max_open_trades to use.', + default=None, + type=int, + dest='max_open_trades', + ) + + parser.add_argument( + '--stake_amount', + help='Specify stake_amount.', + default=None, + type=float, + dest='stake_amount', + ) + @staticmethod def hyperopt_options(parser: argparse.ArgumentParser) -> None: """ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index fdd71f2f5..e7441c18d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -217,14 +217,22 @@ class Configuration(object): config.update({'position_stacking': True}) logger.info('Parameter --enable-position-stacking detected ...') - # If --disable-max-market-positions is used we add it to the configuration + # If --disable-max-market-positions or --max_open_trades is used we update configuration if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: config.update({'use_max_market_positions': False}) logger.info('Parameter --disable-max-market-positions detected ...') logger.info('max_open_trades set to unlimited ...') + elif 'max_open_trades' in self.args and self.args.max_open_trades: + config.update({'max_open_trades': self.args.max_open_trades}) + logger.info('Parameter --max_open_trades detected, overriding max_open_trades to: %s ...', config.get('max_open_trades')) else: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + # If --stake_amount is used we update configuration + if 'stake_amount' in self.args and self.args.stake_amount: + config.update({'stake_amount': self.args.stake_amount}) + logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', config.get('stake_amount')) + # If --timerange is used we add it to the configuration if 'timerange' in self.args and self.args.timerange: config.update({'timerange': self.args.timerange}) From 2b49a11b2add88895a3bd3f2f99189bd499b84a5 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 19:46:43 +0200 Subject: [PATCH 188/928] returning InvalidOrder exception for get_order --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/tests/exchange/test_exchange.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7f4ab062c..7880ce464 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -624,8 +624,8 @@ class Exchange(object): try: return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not get order. Message: {e}') + raise InvalidOrderException( + f'Tried to get an invalid order. Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 4deb74c67..66bc47405 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1261,11 +1261,11 @@ def test_get_order(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.get_order('X', 'TKN/BTC') == 456 - with pytest.raises(DependencyException): + with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_order', 'fetch_order', From 9712fb2d57669217523e797ec5ffd81796a02981 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 19:49:02 +0200 Subject: [PATCH 189/928] removing unnecessary comment --- freqtrade/freqtradebot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9bd3384fa..a0725ad08 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -703,10 +703,7 @@ class FreqtradeBot(object): # it means buy order is fulfilled buy_order_fulfilled = not trade.open_order_id - # Limit price threshold - # This is the limit price percentage below which you don't want to sell - # 0.99 is arbitrary here. - # As limit price should always be below price + # Limit price threshold: As limit price should always be below price limit_price_pct = 0.99 # If buy order is fulfilled but there is no stoploss, From 25d8e93a90de924758d6298455b59e6facfb9e13 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 19:53:15 +0200 Subject: [PATCH 190/928] remove unnecessary comment --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a0725ad08..f04a39610 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -699,8 +699,7 @@ class FreqtradeBot(object): except DependencyException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) - # If trade open order id does not exist, - # it means buy order is fulfilled + # If trade open order id does not exist: buy order is fulfilled buy_order_fulfilled = not trade.open_order_id # Limit price threshold: As limit price should always be below price From 54d068de44ce6a4bbb21a4ca08502d6dd1f77868 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 20:20:16 +0200 Subject: [PATCH 191/928] missing test added --- freqtrade/freqtradebot.py | 9 +++++---- freqtrade/tests/test_freqtradebot.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f04a39610..3bab1758d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -13,7 +13,7 @@ import arrow from requests.exceptions import RequestException import sdnotify -from freqtrade import (DependencyException, OperationalException, +from freqtrade import (DependencyException, OperationalException, InvalidOrderException, TemporaryError, __version__, constants, persistence) from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider @@ -692,11 +692,13 @@ class FreqtradeBot(object): logger.debug('Handling stoploss on exchange %s ...', trade) + stoploss_order = None + try: # First we check if there is already a stoploss on exchange stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ if trade.stoploss_order_id else None - except DependencyException as exception: + except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) # If trade open order id does not exist: buy order is fulfilled @@ -705,8 +707,7 @@ class FreqtradeBot(object): # Limit price threshold: As limit price should always be below price limit_price_pct = 0.99 - # If buy order is fulfilled but there is no stoploss, - # then we add a stoploss on exchange + # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange if (buy_order_fulfilled and not stoploss_order): if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index c854d99e8..5896e7cf9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -12,7 +12,7 @@ import pytest import requests from freqtrade import (DependencyException, OperationalException, - TemporaryError, constants) + TemporaryError, InvalidOrderException, constants) from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -1052,6 +1052,15 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange: ', caplog.record_tuples) + #Fifth case: get_order returns InvalidOrder + # It should try to add stoploss order + trade.stoploss_order_id = 100 + stoploss_limit.reset_mock() + mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + freqtrade.handle_stoploss_on_exchange(trade) + assert stoploss_limit.call_count == 1 + def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: From a505826ec92650af7d19a1d5a05f7fd09c82b956 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 20:20:41 +0200 Subject: [PATCH 192/928] flake8 --- 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 5896e7cf9..9687fe903 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1052,7 +1052,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange: ', caplog.record_tuples) - #Fifth case: get_order returns InvalidOrder + # Fifth case: get_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss_limit.reset_mock() From acb99a03e3f3e936865f16adb56657aeb8183ae1 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 20:30:54 +0200 Subject: [PATCH 193/928] adding stoploss on exchange manual cancel note --- docs/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 75843ef4a..f7e2a07f3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -212,6 +212,10 @@ The below is the default which is used if this is not configured in either strat unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md). +!!! Note + In case of stoploss on exchange if the stoploss is cancelled manually then + the bot would recreate one. + ### Understand order_time_in_force The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange. Three commonly used time in force are: From 41ff2a927650347a78f386abc57e387dadf15185 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 5 Apr 2019 20:40:44 +0200 Subject: [PATCH 194/928] TemporaryError removed --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 346bcfce3..009e039b3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,7 +12,7 @@ import arrow from requests.exceptions import RequestException from freqtrade import (DependencyException, OperationalException, InvalidOrderException, - TemporaryError, __version__, constants, persistence) + __version__, constants, persistence) from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge From 481df98f58025a428f61b9c375155701b9068310 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 6 Apr 2019 12:38:04 +0000 Subject: [PATCH 195/928] Update ccxt from 1.18.432 to 1.18.435 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eb5d564fa..7006c52c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.432 +ccxt==1.18.435 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 7a598f32dcc0f45f326148ed98c4ea83278dce42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 19:58:45 +0200 Subject: [PATCH 196/928] Move rpc-count calculation to _rpc class --- freqtrade/rpc/rpc.py | 7 ++++++- freqtrade/rpc/telegram.py | 10 ++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 88ade0c27..d3a6dc0db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,7 +459,12 @@ class RPC(object): if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - return Trade.get_open_trades() + trades = Trade.get_open_trades() + return { + 'current': len(trades), + 'max': self._config['max_open_trades'], + 'total stake': sum((trade.open_rate * trade.amount) for trade in trades) + } def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e17f73502..ca108b17e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -456,12 +456,10 @@ class Telegram(RPC): :return: None """ try: - trades = self._rpc_count() - message = tabulate({ - 'current': [len(trades)], - 'max': [self._config['max_open_trades']], - 'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)] - }, headers=['current', 'max', 'total stake'], tablefmt='simple') + counts = self._rpc_count() + message = tabulate({k: [v] for k, v in counts.items()}, + headers=['current', 'max', 'total stake'], + tablefmt='simple') message = "
{}
".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) From 4eb0ed9f2fd3296643e45da462c7867c3a7a2bba Mon Sep 17 00:00:00 2001 From: TL Nguyen Date: Sat, 6 Apr 2019 21:11:14 +0300 Subject: [PATCH 197/928] Add Dockerfile.pi for building docker image for raspberry pi --- Dockerfile.pi | 40 ++++++++++++++++++++++++++++++++++++++++ requirements-pi.txt | 23 +++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 Dockerfile.pi create mode 100644 requirements-pi.txt diff --git a/Dockerfile.pi b/Dockerfile.pi new file mode 100644 index 000000000..5184e2d37 --- /dev/null +++ b/Dockerfile.pi @@ -0,0 +1,40 @@ +FROM balenalib/raspberrypi3-debian:stretch + +RUN [ "cross-build-start" ] + +RUN apt-get update \ + && apt-get -y install wget curl build-essential libssl-dev libffi-dev \ + && apt-get clean + +# Prepare environment +RUN mkdir /freqtrade +WORKDIR /freqtrade + +# Install TA-lib +COPY build_helpers/ta-lib-0.4.0-src.tar.gz /freqtrade/ +RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \ + && cd /freqtrade/ta-lib/ \ + && ./configure \ + && make \ + && make install \ + && rm /freqtrade/ta-lib-0.4.0-src.tar.gz + +ENV LD_LIBRARY_PATH /usr/local/lib + +# Install berryconda +RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \ + && bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \ + && rm Berryconda3-2.0.0-Linux-armv7l.sh + +# Install dependencies +COPY requirements-pi.txt /freqtrade/ +RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ + && ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir + +# Install and execute +COPY . /freqtrade/ +RUN ~/berryconda3/bin/pip install -e . --no-cache-dir + +RUN [ "cross-build-end" ] + +ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] diff --git a/requirements-pi.txt b/requirements-pi.txt new file mode 100644 index 000000000..575107e2d --- /dev/null +++ b/requirements-pi.txt @@ -0,0 +1,23 @@ +ccxt==1.18.270 +SQLAlchemy==1.2.18 +python-telegram-bot==11.1.0 +arrow==0.13.1 +cachetools==3.1.0 +requests==2.21.0 +urllib3==1.24.1 +wrapt==1.11.1 +scikit-learn==0.20.2 +joblib==0.13.2 +jsonschema==2.6.0 +TA-Lib==0.4.17 +tabulate==0.8.3 +coinmarketcap==5.0.3 + +# Required for hyperopt +scikit-optimize==0.5.2 + +# find first, C search in arrays +py_find_1st==1.1.3 + +#Load ticker files 30% faster +python-rapidjson==0.7.0 From f13917813637ca9bc64e764cc92a9876c3850e00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 20:01:29 +0200 Subject: [PATCH 198/928] rpc_counts should be in .rpc --- freqtrade/rpc/rpc.py | 6 +++--- freqtrade/tests/rpc/test_rpc.py | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d3a6dc0db..aac419fe1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -454,7 +454,7 @@ class RPC(object): for pair, rate, count in pair_rates ] - def _rpc_count(self) -> List[Trade]: + def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') @@ -462,8 +462,8 @@ class RPC(object): trades = Trade.get_open_trades() return { 'current': len(trades), - 'max': self._config['max_open_trades'], - 'total stake': sum((trade.open_rate * trade.amount) for trade in trades) + 'max': float(self._freqtrade.config['max_open_trades']), + 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) } def _rpc_whitelist(self) -> Dict: diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 8b10a1314..25d1109b2 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -581,15 +581,13 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) - trades = rpc._rpc_count() - nb_trades = len(trades) - assert nb_trades == 0 + counts = rpc._rpc_count() + assert counts["current"] == 0 # Create some test data freqtradebot.create_trade() - trades = rpc._rpc_count() - nb_trades = len(trades) - assert nb_trades == 1 + counts = rpc._rpc_count() + assert counts["current"] == 1 def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None: From d294cab933ddaa49a00ae47ac33ae87d32754648 Mon Sep 17 00:00:00 2001 From: Misagh Date: Sat, 6 Apr 2019 20:27:03 +0200 Subject: [PATCH 199/928] adding order id to invalidorder exception message --- 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 7880ce464..275b2123f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -625,7 +625,7 @@ class Exchange(object): return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Tried to get an invalid order. Message: {e}') + f'Tried to get an invalid order (id: {order_id}). Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') From dc1968b9685270d738646df3ed26145a4b96b84a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 6 Apr 2019 23:36:55 +0300 Subject: [PATCH 200/928] docstrings added --- freqtrade/misc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9aeaef069..57a5673fd 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -135,12 +135,27 @@ def deep_merge_dicts(source, destination): def timeframe_to_seconds(ticker_interval: str) -> int: + """ + This function uses ccxt to parse the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number + of seconds for one timeframe interval. + """ return Exchange.parse_timeframe(ticker_interval) def timeframe_to_minutes(ticker_interval: str) -> int: + """ + This function uses ccxt to parse the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number + of minutes for one timeframe interval. + """ return Exchange.parse_timeframe(ticker_interval) // 60 def timeframe_to_msecs(ticker_interval: str) -> int: + """ + This function uses ccxt to parse the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number + of milliseconds for one timeframe interval. + """ return Exchange.parse_timeframe(ticker_interval) * 1000 From d6d16b4696268c891efa99862309f969a35a4a1d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Apr 2019 00:22:02 +0300 Subject: [PATCH 201/928] docstrings improved --- freqtrade/misc.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 57a5673fd..d066878be 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -136,8 +136,8 @@ def deep_merge_dicts(source, destination): def timeframe_to_seconds(ticker_interval: str) -> int: """ - This function uses ccxt to parse the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number + Translates the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) to the number of seconds for one timeframe interval. """ return Exchange.parse_timeframe(ticker_interval) @@ -145,17 +145,13 @@ def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_minutes(ticker_interval: str) -> int: """ - This function uses ccxt to parse the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number - of minutes for one timeframe interval. + Same as above, but returns minutes. """ return Exchange.parse_timeframe(ticker_interval) // 60 def timeframe_to_msecs(ticker_interval: str) -> int: """ - This function uses ccxt to parse the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) and returns the number - of milliseconds for one timeframe interval. + Same as above, but returns milliseconds. """ return Exchange.parse_timeframe(ticker_interval) * 1000 From e7c8e62d751faaf3abd22742eccb52c0d77abc84 Mon Sep 17 00:00:00 2001 From: TL Nguyen Date: Sun, 7 Apr 2019 10:31:03 +0300 Subject: [PATCH 202/928] Remove requirements-pi.txt, change Dockerfile.pi to utilize the requirements.txt instead --- Dockerfile.pi | 7 ++++--- requirements-pi.txt | 23 ----------------------- 2 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 requirements-pi.txt diff --git a/Dockerfile.pi b/Dockerfile.pi index 5184e2d37..1041b3c87 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -27,9 +27,10 @@ RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryco && rm Berryconda3-2.0.0-Linux-armv7l.sh # Install dependencies -COPY requirements-pi.txt /freqtrade/ -RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ - && ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir +COPY requirements.txt /freqtrade/ +RUN sed -i -e '/^numpy==/d' -e '/^pandas==/d' -e '/^scipy==/d' ./requirements.txt \ + && ~/berryconda3/bin/conda install -y numpy pandas scipy \ + && ~/berryconda3/bin/pip install -r requirements.txt --no-cache-dir # Install and execute COPY . /freqtrade/ diff --git a/requirements-pi.txt b/requirements-pi.txt deleted file mode 100644 index 575107e2d..000000000 --- a/requirements-pi.txt +++ /dev/null @@ -1,23 +0,0 @@ -ccxt==1.18.270 -SQLAlchemy==1.2.18 -python-telegram-bot==11.1.0 -arrow==0.13.1 -cachetools==3.1.0 -requests==2.21.0 -urllib3==1.24.1 -wrapt==1.11.1 -scikit-learn==0.20.2 -joblib==0.13.2 -jsonschema==2.6.0 -TA-Lib==0.4.17 -tabulate==0.8.3 -coinmarketcap==5.0.3 - -# Required for hyperopt -scikit-optimize==0.5.2 - -# find first, C search in arrays -py_find_1st==1.1.3 - -#Load ticker files 30% faster -python-rapidjson==0.7.0 From c35e5ca7dda865243006b1dd251b8a1eeb82af00 Mon Sep 17 00:00:00 2001 From: TL Nguyen Date: Sun, 7 Apr 2019 14:05:41 +0300 Subject: [PATCH 203/928] Add back requirements-pi.txt file and put it into .pyup.yml --- .pyup.yml | 1 + requirements-pi.txt | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 requirements-pi.txt diff --git a/.pyup.yml b/.pyup.yml index 01d4bba2a..462ae5783 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -22,6 +22,7 @@ requirements: - requirements.txt - requirements-dev.txt - requirements-plot.txt + - requirements-pi.txt # configure the branch prefix the bot is using diff --git a/requirements-pi.txt b/requirements-pi.txt new file mode 100644 index 000000000..4fca8b032 --- /dev/null +++ b/requirements-pi.txt @@ -0,0 +1,23 @@ +ccxt==1.18.353 +SQLAlchemy==1.3.1 +python-telegram-bot==11.1.0 +arrow==0.13.1 +cachetools==3.1.0 +requests==2.21.0 +urllib3==1.24.1 +wrapt==1.11.1 +scikit-learn==0.20.3 +joblib==0.13.2 +jsonschema==3.0.1 +TA-Lib==0.4.17 +tabulate==0.8.3 +coinmarketcap==5.0.3 + +# Required for hyperopt +scikit-optimize==0.5.2 + +# find first, C search in arrays +py_find_1st==1.1.3 + +#Load ticker files 30% faster +python-rapidjson==0.7.0 From 3ad4d937c5731bd9ef169684ca72d68a0082ffd0 Mon Sep 17 00:00:00 2001 From: TL Nguyen Date: Sun, 7 Apr 2019 14:07:26 +0300 Subject: [PATCH 204/928] Correct Dockerfile.pi file to use requirements-pi.txt --- Dockerfile.pi | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dockerfile.pi b/Dockerfile.pi index 1041b3c87..5184e2d37 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -27,10 +27,9 @@ RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryco && rm Berryconda3-2.0.0-Linux-armv7l.sh # Install dependencies -COPY requirements.txt /freqtrade/ -RUN sed -i -e '/^numpy==/d' -e '/^pandas==/d' -e '/^scipy==/d' ./requirements.txt \ - && ~/berryconda3/bin/conda install -y numpy pandas scipy \ - && ~/berryconda3/bin/pip install -r requirements.txt --no-cache-dir +COPY requirements-pi.txt /freqtrade/ +RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ + && ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir # Install and execute COPY . /freqtrade/ From 3a81eb7d483c46afdf30deed04f1e9a065437544 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 7 Apr 2019 12:38:05 +0000 Subject: [PATCH 205/928] Update ccxt from 1.18.435 to 1.18.437 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7006c52c7..99892a544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.435 +ccxt==1.18.437 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From ebf11263516258f853573468e59c1c9bf0941f34 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Apr 2019 16:14:40 +0300 Subject: [PATCH 206/928] cosmetic: rename interval, tick_interval, etc --> ticker_interval --- freqtrade/data/dataprovider.py | 12 +++---- freqtrade/data/history.py | 20 ++++++------ freqtrade/exchange/exchange.py | 32 +++++++++--------- freqtrade/tests/data/test_dataprovider.py | 40 +++++++++++------------ freqtrade/tests/data/test_history.py | 14 ++++---- freqtrade/tests/exchange/test_exchange.py | 6 ++-- scripts/download_backtest_data.py | 10 +++--- scripts/plot_dataframe.py | 16 ++++----- scripts/plot_profit.py | 16 ++++----- 9 files changed, 83 insertions(+), 83 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 375b8bf5b..df4accf93 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -37,23 +37,23 @@ class DataProvider(object): @property def available_pairs(self) -> List[Tuple[str, str]]: """ - Return a list of tuples containing pair, tick_interval for which data is currently cached. + Return a list of tuples containing pair, ticker_interval for which data is currently cached. Should be whitelist + open trades. """ return list(self._exchange._klines.keys()) - def ohlcv(self, pair: str, tick_interval: str = None, copy: bool = True) -> DataFrame: + 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. :param pair: pair to get the data for - :param tick_interval: ticker_interval to get pair 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) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - if tick_interval: - pairtick = (pair, tick_interval) + if ticker_interval: + pairtick = (pair, ticker_interval) else: pairtick = (pair, self._config['ticker_interval']) @@ -65,7 +65,7 @@ class DataProvider(object): """ get stored historic ohlcv data :param pair: pair to get the data for - :param tick_interval: ticker_interval to get pair for + :param ticker_interval: ticker_interval to get pair for """ return load_pair_history(pair=pair, ticker_interval=ticker_interval, diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 0ecc632e4..594c85b5f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -101,7 +101,7 @@ def load_pair_history(pair: str, download_pair_history(datadir=datadir, exchange=exchange, pair=pair, - tick_interval=ticker_interval, + ticker_interval=ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) @@ -151,7 +151,7 @@ def make_testdata_path(datadir: Optional[Path]) -> Path: return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() -def load_cached_data_for_updating(filename: Path, tick_interval: str, +def load_cached_data_for_updating(filename: Path, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -165,7 +165,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, if timerange.starttype == 'date': since_ms = timerange.startts * 1000 elif timerange.stoptype == 'line': - num_minutes = timerange.stopts * timeframe_to_minutes(tick_interval) + num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file @@ -192,7 +192,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, def download_pair_history(datadir: Optional[Path], exchange: Exchange, pair: str, - tick_interval: str = '5m', + ticker_interval: str = '5m', timerange: Optional[TimeRange] = None) -> bool: """ Download the latest ticker intervals from the exchange for the pair passed in parameters @@ -202,7 +202,7 @@ def download_pair_history(datadir: Optional[Path], Based on @Rybolov work: https://github.com/rybolov/freqtrade-data :param pair: pair to download - :param tick_interval: ticker interval + :param ticker_interval: ticker interval :param timerange: range of time to download :return: bool with success state @@ -210,17 +210,17 @@ def download_pair_history(datadir: Optional[Path], try: path = make_testdata_path(datadir) filepair = pair.replace("/", "_") - filename = path.joinpath(f'{filepair}-{tick_interval}.json') + filename = path.joinpath(f'{filepair}-{ticker_interval}.json') - logger.info('Download the pair: "%s", Interval: %s', pair, tick_interval) + logger.info('Download the pair: "%s", Interval: %s', pair, ticker_interval) - data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange) + data, since_ms = load_cached_data_for_updating(filename, 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') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_history(pair=pair, tick_interval=tick_interval, + 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) @@ -233,5 +233,5 @@ def download_pair_history(datadir: Optional[Path], return True except BaseException: logger.info('Failed to download the pair: "%s", Interval: %s', - pair, tick_interval) + pair, ticker_interval) return False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f18a71a50..2e8d6b99a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -489,26 +489,26 @@ class Exchange(object): logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] - def get_history(self, pair: str, tick_interval: str, + def get_history(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. """ return asyncio.get_event_loop().run_until_complete( - self._async_get_history(pair=pair, tick_interval=tick_interval, + self._async_get_history(pair=pair, ticker_interval=ticker_interval, since_ms=since_ms)) async def _async_get_history(self, pair: str, - tick_interval: str, + ticker_interval: str, since_ms: int) -> List: # Assume exchange returns 500 candles _LIMIT = 500 - one_call = timeframe_to_msecs(tick_interval) * _LIMIT + one_call = timeframe_to_msecs(ticker_interval) * _LIMIT logger.debug("one_call: %s msecs", one_call) input_coroutines = [self._async_get_candle_history( - pair, tick_interval, since) for since in + pair, ticker_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) @@ -548,14 +548,14 @@ class Exchange(object): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue pair = res[0] - tick_interval = res[1] + ticker_interval = res[1] ticks = res[2] # keeping last candle time as last refreshed time of the pair if ticks: - self._pairs_last_refresh_time[(pair, tick_interval)] = ticks[-1][0] // 1000 + self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, tick_interval)] = parse_ticker_dataframe( - ticks, tick_interval, fill_missing=True) + self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( + ticks, ticker_interval, fill_missing=True) return tickers def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: @@ -566,17 +566,17 @@ class Exchange(object): + interval_in_sec) >= arrow.utcnow().timestamp) @retrier_async - async def _async_get_candle_history(self, pair: str, tick_interval: str, + 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 - returns tuple: (pair, tick_interval, ohlcv_list) + returns tuple: (pair, ticker_interval, ohlcv_list) """ try: # fetch ohlcv asynchronously - logger.debug("fetching %s, %s since %s ...", pair, tick_interval, since_ms) + logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms) - data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval, + data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, since=since_ms) # Because some exchange sort Tickers ASC and other DESC. @@ -588,9 +588,9 @@ class Exchange(object): data = sorted(data, key=lambda x: x[0]) except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) - return pair, tick_interval, [] - logger.debug("done fetching %s, %s ...", pair, tick_interval) - return pair, tick_interval, data + return pair, ticker_interval, [] + logger.debug("done fetching %s, %s ...", pair, ticker_interval) + return pair, ticker_interval, data except ccxt.NotSupported as e: raise OperationalException( diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index b17bba273..993f0b59b 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -9,31 +9,31 @@ from freqtrade.tests.conftest import get_patched_exchange def test_ohlcv(mocker, default_conf, ticker_history): default_conf["runmode"] = RunMode.DRY_RUN - tick_interval = default_conf["ticker_interval"] + ticker_interval = default_conf["ticker_interval"] exchange = get_patched_exchange(mocker, default_conf) - exchange._klines[("XRP/BTC", tick_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history + 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", tick_interval)) - assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) - assert dp.ohlcv("UNITTEST/BTC", tick_interval) is not ticker_history - assert dp.ohlcv("UNITTEST/BTC", tick_interval, copy=False) is ticker_history - assert not dp.ohlcv("UNITTEST/BTC", tick_interval).empty - assert dp.ohlcv("NONESENSE/AAA", tick_interval).empty + assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) + assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history + assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history + assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty + assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty # Test with and without parameter - assert dp.ohlcv("UNITTEST/BTC", tick_interval).equals(dp.ohlcv("UNITTEST/BTC")) + assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.LIVE - assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) + assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.BACKTEST - assert dp.ohlcv("UNITTEST/BTC", tick_interval).empty + assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty def test_historic_ohlcv(mocker, default_conf, ticker_history): @@ -54,15 +54,15 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) - tick_interval = default_conf["ticker_interval"] - exchange._klines[("XRP/BTC", tick_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history + 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) assert len(dp.available_pairs) == 2 assert dp.available_pairs == [ - ("XRP/BTC", tick_interval), - ("UNITTEST/BTC", tick_interval), + ("XRP/BTC", ticker_interval), + ("UNITTEST/BTC", ticker_interval), ] @@ -71,10 +71,10 @@ def test_refresh(mocker, default_conf, ticker_history): mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) exchange = get_patched_exchange(mocker, default_conf, id="binance") - tick_interval = default_conf["ticker_interval"] - pairs = [("XRP/BTC", tick_interval), ("UNITTEST/BTC", tick_interval)] + ticker_interval = default_conf["ticker_interval"] + pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)] - pairs_non_trad = [("ETH/USDT", tick_interval), ("BTC/TUSD", "1h")] + pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] dp = DataProvider(default_conf, exchange) dp.refresh(pairs) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index bc859b325..c0b1cade3 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -242,10 +242,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non assert download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='1m') + ticker_interval='1m') assert download_pair_history(datadir=None, exchange=exchange, pair='CFI/BTC', - tick_interval='1m') + ticker_interval='1m') assert not exchange._pairs_last_refresh_time assert os.path.isfile(file1_1) is True assert os.path.isfile(file2_1) is True @@ -259,10 +259,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non assert download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='5m') + ticker_interval='5m') assert download_pair_history(datadir=None, exchange=exchange, pair='CFI/BTC', - tick_interval='5m') + ticker_interval='5m') assert not exchange._pairs_last_refresh_time assert os.path.isfile(file1_5) is True assert os.path.isfile(file2_5) is True @@ -280,8 +280,8 @@ def test_download_pair_history2(mocker, default_conf) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') - download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') + download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') + download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') assert json_dump_mock.call_count == 2 @@ -298,7 +298,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def assert not download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='1m') + ticker_interval='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index eed16d39b..6997c6ba3 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -940,8 +940,8 @@ def test_get_history(default_conf, mocker, caplog, exchange_name): ] pair = 'ETH/BTC' - async def mock_candle_hist(pair, tick_interval, since_ms): - return pair, tick_interval, tick + async def mock_candle_hist(pair, ticker_interval, since_ms): + return pair, ticker_interval, tick exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls @@ -1037,7 +1037,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", - pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 5dee41bdd..50005b97b 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -92,18 +92,18 @@ for pair in PAIRS: pairs_not_available.append(pair) print(f"skipping pair {pair}") continue - for tick_interval in timeframes: + for ticker_interval in timeframes: pair_print = pair.replace('/', '_') - filename = f'{pair_print}-{tick_interval}.json' + filename = f'{pair_print}-{ticker_interval}.json' dl_file = dl_path.joinpath(filename) if args.erase and dl_file.exists(): - print(f'Deleting existing data for pair {pair}, interval {tick_interval}') + print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') dl_file.unlink() - print(f'downloading pair {pair}, interval {tick_interval}') + print(f'downloading pair {pair}, interval {ticker_interval}') download_pair_history(datadir=dl_path, exchange=exchange, pair=pair, - tick_interval=tick_interval, + ticker_interval=ticker_interval, timerange=timerange) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 14d57265e..7fdc607e0 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -82,7 +82,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram return trades -def generate_plot_file(fig, pair, tick_interval, is_last) -> None: +def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: """ Generate a plot html file from pre populated fig plotly object :return: None @@ -90,7 +90,7 @@ def generate_plot_file(fig, pair, tick_interval, is_last) -> None: logger.info('Generate plot file for %s', pair) pair_name = pair.replace("/", "_") - file_name = 'freqtrade-plot-' + pair_name + '-' + tick_interval + '.html' + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' Path("user_data/plots").mkdir(parents=True, exist_ok=True) @@ -135,20 +135,20 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): :return: dictinnary of tickers. output format: {'pair': tickersdata} """ - tick_interval = strategy.ticker_interval + ticker_interval = strategy.ticker_interval timerange = Arguments.parse_timerange(args.timerange) tickers = {} if args.live: logger.info('Downloading pairs.') - exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs]) + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) for pair in pairs: - tickers[pair] = exchange.klines((pair, tick_interval)) + tickers[pair] = exchange.klines((pair, ticker_interval)) else: tickers = history.load_data( datadir=Path(str(_CONF.get("datadir"))), pairs=pairs, - ticker_interval=tick_interval, + ticker_interval=ticker_interval, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, exchange=Exchange(_CONF) @@ -399,7 +399,7 @@ def analyse_and_plot_pairs(args: Namespace): strategy, exchange, pairs = get_trading_env(args) # Set timerange to use timerange = Arguments.parse_timerange(args.timerange) - tick_interval = strategy.ticker_interval + ticker_interval = strategy.ticker_interval tickers = get_tickers_data(strategy, exchange, pairs, args) pair_counter = 0 @@ -422,7 +422,7 @@ def analyse_and_plot_pairs(args: Namespace): ) is_last = (False, True)[pair_counter == len(tickers)] - generate_plot_file(fig, pair, tick_interval, is_last) + generate_plot_file(fig, pair, ticker_interval, is_last) logger.info('End of ploting process %s plots generated', pair_counter) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 2b6360bec..500d9fcde 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -76,7 +76,7 @@ def plot_profit(args: Namespace) -> None: in helping out to find a good algorithm. """ - # We need to use the same pairs, same tick_interval + # We need to use the same pairs, same ticker_interval # and same timeperiod as used in backtesting # to match the tickerdata against the profits-results timerange = Arguments.parse_timerange(args.timerange) @@ -112,7 +112,7 @@ def plot_profit(args: Namespace) -> None: else: filter_pairs = config['exchange']['pair_whitelist'] - tick_interval = strategy.ticker_interval + ticker_interval = strategy.ticker_interval pairs = config['exchange']['pair_whitelist'] if filter_pairs: @@ -122,7 +122,7 @@ def plot_profit(args: Namespace) -> None: tickers = history.load_data( datadir=Path(str(config.get('datadir'))), pairs=pairs, - ticker_interval=tick_interval, + ticker_interval=ticker_interval, refresh_pairs=False, timerange=timerange ) @@ -134,7 +134,7 @@ def plot_profit(args: Namespace) -> None: dates = common_datearray(dataframes) min_date = int(min(dates).timestamp()) max_date = int(max(dates).timestamp()) - num_iterations = define_index(min_date, max_date, tick_interval) + 1 + num_iterations = define_index(min_date, max_date, ticker_interval) + 1 # Make an average close price of all the pairs that was involved. # this could be useful to gauge the overall market trend @@ -154,7 +154,7 @@ def plot_profit(args: Namespace) -> None: avgclose /= num # make an profits-growth array - pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs) + pg = make_profit_array(data, num_iterations, min_date, ticker_interval, filter_pairs) # # Plot the pairs average close prices, and total profit growth @@ -178,7 +178,7 @@ def plot_profit(args: Namespace) -> None: fig.append_trace(profit, 2, 1) for pair in pairs: - pg = make_profit_array(data, num_iterations, min_date, tick_interval, [pair]) + pg = make_profit_array(data, num_iterations, min_date, ticker_interval, [pair]) pair_profit = go.Scattergl( x=dates, y=pg, @@ -189,11 +189,11 @@ def plot_profit(args: Namespace) -> None: plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html'))) -def define_index(min_date: int, max_date: int, interval: str) -> int: +def define_index(min_date: int, max_date: int, ticker_interval: str) -> int: """ Return the index of a specific date """ - interval_seconds = timeframe_to_seconds(interval) + interval_seconds = timeframe_to_seconds(ticker_interval) return int((max_date - min_date) / interval_seconds) From 91dc2b96fc3b08d849f05c15d6c30eb3d5eca562 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 8 Apr 2019 04:23:29 +0300 Subject: [PATCH 207/928] support for defaults in json.schema --- freqtrade/configuration.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index fdd71f2f5..b0dc0b212 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -10,13 +10,14 @@ from logging.handlers import RotatingFileHandler from typing import Any, Dict, List, Optional import ccxt -from jsonschema import Draft4Validator, validate +from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -33,6 +34,27 @@ def set_loggers(log_level: int = 0) -> None: logging.getLogger('telegram').setLevel(logging.INFO) +def _extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + for prop, subschema in properties.items(): + if "default" in subschema: + instance.setdefault(prop, subschema["default"]) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {"properties": set_defaults}, + ) + + +ValidatorWithDefaults = _extend_with_default(Draft4Validator) + + class Configuration(object): """ Class to read and init the bot configuration @@ -318,7 +340,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - validate(conf, constants.CONF_SCHEMA, Draft4Validator) + ValidatorWithDefaults(constants.CONF_SCHEMA).validate(conf) return conf except ValidationError as exception: logger.critical( From 4559a38172dcaf5925928e35304ef9ae39071368 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 8 Apr 2019 04:42:28 +0300 Subject: [PATCH 208/928] PoC: use defaults in json schema for some exchange options --- freqtrade/constants.py | 12 ++++++------ freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5243eeb4a..e825fbd3f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -172,11 +172,11 @@ CONF_SCHEMA = { 'exchange': { 'type': 'object', 'properties': { - 'name': {'type': 'string'}, - 'sandbox': {'type': 'boolean'}, - 'key': {'type': 'string'}, - 'secret': {'type': 'string'}, - 'password': {'type': 'string'}, + 'name': {'type': 'string', 'default': 'bittrex'}, + 'sandbox': {'type': 'boolean', 'default': False}, + 'key': {'type': 'string', 'default': ''}, + 'secret': {'type': 'string', 'default': ''}, + 'password': {'type': 'string', 'default': ''}, 'uid': {'type': 'string'}, 'pair_whitelist': { 'type': 'array', @@ -199,7 +199,7 @@ CONF_SCHEMA = { 'ccxt_config': {'type': 'object'}, 'ccxt_async_config': {'type': 'object'} }, - 'required': ['name', 'key', 'secret', 'pair_whitelist'] + 'required': ['pair_whitelist'] }, 'edge': { 'type': 'object', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a9676a64e..bceab8b23 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -53,7 +53,7 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) - exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() + exchange_name = self.config.get('exchange', {}).get('name').title() self.exchange = ExchangeResolver(exchange_name, self.config).exchange self.wallets = Wallets(self.config, self.exchange) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3e4d642cb..513498766 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -66,7 +66,7 @@ class Backtesting(object): self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] - exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() + exchange_name = self.config.get('exchange', {}).get('name').title() self.exchange = ExchangeResolver(exchange_name, self.config).exchange self.fee = self.exchange.get_fee() From cb2f422e1c4a7a1677901e90ae33f641e9cdb44f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 8 Apr 2019 11:19:45 +0300 Subject: [PATCH 209/928] make `name` option required again --- freqtrade/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e825fbd3f..619508e73 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -172,7 +172,7 @@ CONF_SCHEMA = { 'exchange': { 'type': 'object', 'properties': { - 'name': {'type': 'string', 'default': 'bittrex'}, + 'name': {'type': 'string'}, 'sandbox': {'type': 'boolean', 'default': False}, 'key': {'type': 'string', 'default': ''}, 'secret': {'type': 'string', 'default': ''}, @@ -199,7 +199,7 @@ CONF_SCHEMA = { 'ccxt_config': {'type': 'object'}, 'ccxt_async_config': {'type': 'object'} }, - 'required': ['pair_whitelist'] + 'required': ['name', 'pair_whitelist'] }, 'edge': { 'type': 'object', From 3e4dd5019d5c354abb942958535227e0e02ef75e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 8 Apr 2019 11:20:15 +0300 Subject: [PATCH 210/928] docs adjusted --- docs/bot-usage.md | 2 +- docs/configuration.md | 6 +++--- docs/telegram-usage.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 55988985a..71d4aebb8 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -188,7 +188,7 @@ optional arguments: ### How to use **--refresh-pairs-cached** parameter? The first time your run Backtesting, it will take the pairs you have -set in your config file and download data from Bittrex. +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. diff --git a/docs/configuration.md b/docs/configuration.md index f7e2a07f3..81bac42cf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,10 +40,10 @@ Mandatory Parameters are marked as **Required**. | `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). | `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). -| `exchange.name` | bittrex | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). +| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | key | API key to use for the exchange. Only required when you are in production mode. -| `exchange.secret` | secret | API secret to use for the exchange. Only required when you are in production mode. +| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. +| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `exchange.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9d6877318..3947168c5 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -116,7 +116,7 @@ Return a summary of your profit/loss and performance. ### /forcebuy -> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) +> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) Note that for this to work, `forcebuy_enable` needs to be set to true. From ffdc33d964cc08de47a7f3eb35bbea812c3603a3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Apr 2019 12:39:06 +0000 Subject: [PATCH 211/928] Update ccxt from 1.18.437 to 1.18.442 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 99892a544..de437aa67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.437 +ccxt==1.18.442 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 5c4170951a6f240f1a71f63d4edd43f23b6df58f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Apr 2019 19:59:30 +0200 Subject: [PATCH 212/928] Don't send too large messages --- freqtrade/rpc/telegram.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ca108b17e..d1ef75cf5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -20,6 +20,9 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') +MAX_TELEGRAM_MESSAGE = 4096 + + def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: """ Decorator to check if the message comes from the correct chat_id @@ -327,13 +330,20 @@ class Telegram(RPC): output = '' for currency in result['currencies']: if currency['est_btc'] > 0.0001: - output += "*{currency}:*\n" \ + curr_output = "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {pending: .8f}`\n" \ "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) else: - output += "*{currency}:* not showing <1$ amount \n".format(**currency) + curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) + + # Handle overflowing messsage length + if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE: + self._send_msg(output, bot=bot) + output = curr_output + else: + output += curr_output output += "\n*Estimated Value*:\n" \ "\t`BTC: {total: .8f}`\n" \ From ff6967de9e5ce68a9c80c1072555e94335286bac Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Apr 2019 19:59:54 +0200 Subject: [PATCH 213/928] Add test for too large balance --- freqtrade/tests/rpc/test_rpc_telegram.py | 40 ++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 8f43d7ed0..137d7b3b6 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -4,7 +4,8 @@ import re from datetime import datetime -from random import randint +from random import choice, randint +from string import ascii_uppercase from unittest.mock import MagicMock, PropertyMock import arrow @@ -20,7 +21,8 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange) +from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, + patch_exchange) from freqtrade.tests.test_freqtradebot import patch_get_signal @@ -587,6 +589,40 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: assert 'all balances are zero' in result +def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: + balances = [] + for i in range(100): + curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) + balances.append({ + 'currency': curr, + 'available': 1.0, + 'pending': 0.5, + 'balance': i, + 'est_btc': 1 + }) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ + 'currencies': balances, + 'total': 100.0, + 'symbol': 100.0, + 'value': 1000.0, + }) + + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) + assert msg_mock.call_count > 1 + + def test_start_handle(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( From 9fbe573ccadfdb81f97c6790419eac442b743ca9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 9 Apr 2019 12:27:35 +0300 Subject: [PATCH 214/928] limit usage of ccxt to freqtrade/exchange only --- freqtrade/configuration.py | 8 +++-- freqtrade/data/converter.py | 4 ++- freqtrade/data/history.py | 7 ++-- freqtrade/exchange/__init__.py | 5 +++ freqtrade/exchange/exchange.py | 39 +++++++++++++++++++++-- freqtrade/freqtradebot.py | 4 +-- freqtrade/misc.py | 26 +-------------- freqtrade/optimize/backtesting.py | 4 ++- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/interface.py | 3 +- freqtrade/tests/optimize/__init__.py | 2 +- freqtrade/tests/optimize/test_optimize.py | 2 +- scripts/get_market_pairs.py | 10 +++--- scripts/plot_profit.py | 4 ++- 14 files changed, 71 insertions(+), 50 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index fdd71f2f5..e08969a7e 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -9,14 +9,15 @@ from argparse import Namespace from logging.handlers import RotatingFileHandler from typing import Any, Dict, List, Optional -import ccxt from jsonschema import Draft4Validator, validate from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants +from freqtrade.exchange import is_exchange_supported, supported_exchanges from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -374,10 +375,11 @@ class Configuration(object): :return: True or raised an exception if the exchange if not supported """ exchange = config.get('exchange', {}).get('name').lower() - if exchange not in ccxt.exchanges: + if not is_exchange_supported(exchange): exception_msg = f'Exchange "{exchange}" not supported.\n' \ - f'The following exchanges are supported: {", ".join(ccxt.exchanges)}' + f'The following exchanges are supported: ' \ + f'{", ".join(supported_exchanges())}' logger.critical(exception_msg) raise OperationalException( diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 28749293b..77a3447da 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -2,9 +2,9 @@ Functions to convert data from one format to another """ import logging + import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.misc import timeframe_to_minutes logger = logging.getLogger(__name__) @@ -58,6 +58,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da using the previous close as price for "open", "high" "low" and "close", volume is set to 0 """ + from freqtrade.exchange import timeframe_to_minutes + ohlc_dict = { 'open': 'first', 'high': 'max', diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 594c85b5f..07a7f0b66 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -1,10 +1,10 @@ """ Handle historic data (ohlcv). -includes: + +Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ - import logging from pathlib import Path from typing import Optional, List, Dict, Tuple, Any @@ -15,8 +15,7 @@ from pandas import DataFrame from freqtrade import misc, OperationalException from freqtrade.arguments import TimeRange from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.exchange import Exchange -from freqtrade.misc import timeframe_to_minutes +from freqtrade.exchange import Exchange, timeframe_to_minutes logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index f6db04da6..3c90e69ee 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,3 +1,8 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 +from freqtrade.exchange.exchange import (is_exchange_supported, # noqa: F401 + supported_exchanges) +from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 + timeframe_to_minutes, + timeframe_to_msecs) 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 ffd574f90..dc0c197c0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1,5 +1,7 @@ # pragma pylint: disable=W0603 -""" Cryptocurrency Exchanges support """ +""" +Cryptocurrency Exchanges support +""" import logging import inspect from random import randint @@ -16,7 +18,6 @@ from pandas import DataFrame from freqtrade import (constants, DependencyException, OperationalException, TemporaryError, InvalidOrderException) from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.misc import timeframe_to_seconds, timeframe_to_msecs logger = logging.getLogger(__name__) @@ -138,7 +139,7 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if name not in ccxt_module.exchanges: + if not is_exchange_supported(name, ccxt_module): raise OperationalException(f'Exchange {name} is not supported') ex_config = { @@ -690,3 +691,35 @@ class Exchange(object): f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') except ccxt.BaseError as e: raise OperationalException(e) + + +def is_exchange_supported(exchange: str, ccxt_module=None) -> bool: + return exchange in supported_exchanges(ccxt_module) + + +def supported_exchanges(ccxt_module=None) -> str: + exchanges = ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges + return exchanges + + +def timeframe_to_seconds(ticker_interval: str) -> int: + """ + Translates the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) to the number + of seconds for one timeframe interval. + """ + return ccxt.Exchange.parse_timeframe(ticker_interval) + + +def timeframe_to_minutes(ticker_interval: str) -> int: + """ + Same as above, 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. + """ + return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a9676a64e..038510755 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.misc import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -460,7 +460,7 @@ class FreqtradeBot(object): def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Get real amount for the trade - Necessary for self.exchanges which charge fees in base currency (e.g. binance) + Necessary for exchanges which charge fees in base currency (e.g. binance) """ order_amount = order['amount'] # Only run for closed orders diff --git a/freqtrade/misc.py b/freqtrade/misc.py index d066878be..9d37214e4 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -1,18 +1,17 @@ """ Various tool function for Freqtrade and scripts """ - import gzip import logging import re from datetime import datetime from typing import Dict -from ccxt import Exchange import numpy as np from pandas import DataFrame import rapidjson + logger = logging.getLogger(__name__) @@ -132,26 +131,3 @@ def deep_merge_dicts(source, destination): destination[key] = value return destination - - -def timeframe_to_seconds(ticker_interval: str) -> int: - """ - Translates the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) to the number - of seconds for one timeframe interval. - """ - return Exchange.parse_timeframe(ticker_interval) - - -def timeframe_to_minutes(ticker_interval: str) -> int: - """ - Same as above, but returns minutes. - """ - return Exchange.parse_timeframe(ticker_interval) // 60 - - -def timeframe_to_msecs(ticker_interval: str) -> int: - """ - Same as above, but returns milliseconds. - """ - return Exchange.parse_timeframe(ticker_interval) * 1000 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3e4d642cb..9ee1cd5cd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,12 +19,14 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider -from freqtrade.misc import file_dump_json, timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes +from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType, IStrategy + logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index b29e26ef9..c62bfe5dc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -6,6 +6,7 @@ from freqtrade.strategy.interface import IStrategy # Import Default-Strategy to have hyperopt correctly resolve from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 + logger = logging.getLogger(__name__) @@ -16,7 +17,6 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: """ # Copy all attributes from base class and class - comb = {**strategy.__class__.__dict__, **strategy.__dict__} # Delete '_abc_impl' from dict as deepcopy fails on 3.7 with @@ -26,6 +26,7 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: del comb['_abc_impl'] attr = deepcopy(comb) + # Adjust module name attr['__module__'] = 'freqtrade.strategy' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 646bd2a94..caf56f13e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,10 +13,11 @@ import arrow from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider -from freqtrade.misc import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 227050770..457113cb7 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -3,7 +3,7 @@ from typing import NamedTuple, List import arrow from pandas import DataFrame -from freqtrade.misc import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType ticker_start_time = arrow.get(2018, 10, 3) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 088743038..d746aa44f 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -2,7 +2,7 @@ from freqtrade import optimize from freqtrade.arguments import TimeRange from freqtrade.data import history -from freqtrade.misc import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, patch_exchange diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py index 1a4c593f9..00639c50b 100644 --- a/scripts/get_market_pairs.py +++ b/scripts/get_market_pairs.py @@ -1,6 +1,8 @@ import os import sys +from freqtrade.exchange import is_exchange_supported, supported_exchanges + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(root + '/python') @@ -44,7 +46,7 @@ def dump(*args): def print_supported_exchanges(): - dump('Supported exchanges:', green(', '.join(ccxt.exchanges))) + dump('Supported exchanges:', green(', '.join(supported_exchanges()))) try: @@ -52,9 +54,7 @@ try: id = sys.argv[1] # get exchange id from command line arguments # check if the exchange is supported by ccxt - exchange_found = id in ccxt.exchanges - - if exchange_found: + if is_exchange_supported(id): dump('Instantiating', green(id), 'exchange') # instantiate the exchange by id @@ -79,9 +79,7 @@ try: for (k, v) in tuples: dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote'])) - else: - dump('Exchange ' + red(id) + ' not found') print_supported_exchanges() diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 500d9fcde..5f7d42c87 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -27,10 +27,12 @@ from plotly.offline import plot from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data import history -from freqtrade.misc import common_datearray, timeframe_to_seconds +from freqtrade.exchange import timeframe_to_seconds +from freqtrade.misc import common_datearray from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) From 71e671f053be5b2854b6dc738cad8afb43d65b6a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 9 Apr 2019 12:38:06 +0000 Subject: [PATCH 215/928] Update ccxt from 1.18.442 to 1.18.445 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index de437aa67..4bd9433fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.442 +ccxt==1.18.445 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From e75cdd4c27dcc6bde25f39c4986edb282fc677a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Apr 2019 06:59:10 +0200 Subject: [PATCH 216/928] Rename variable, add more tests --- freqtrade/rpc/telegram.py | 4 ++-- freqtrade/tests/rpc/test_rpc_telegram.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d1ef75cf5..4e4c080d0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') -MAX_TELEGRAM_MESSAGE = 4096 +MAX_TELEGRAM_MESSAGE_LENGTH = 4096 def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: @@ -339,7 +339,7 @@ class Telegram(RPC): curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) # Handle overflowing messsage length - if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE: + if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: self._send_msg(output, bot=bot) output = curr_output else: diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 137d7b3b6..f2f3f3945 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -621,6 +621,11 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None telegram._balance(bot=MagicMock(), update=update) assert msg_mock.call_count > 1 + # Test if wrap happens around 4000 - + # and each single currency-output is around 120 characters long so we need + # an offset to avoid random test failures + assert len(msg_mock.call_args_list[0][0][0]) < 4096 + assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) def test_start_handle(default_conf, update, mocker) -> None: From f736646ac6975d2eed5a44f9db99e2ca89b7b511 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 Apr 2019 12:38:05 +0000 Subject: [PATCH 217/928] Update ccxt from 1.18.445 to 1.18.456 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4bd9433fe..99c5104c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.445 +ccxt==1.18.456 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From f03acce84cb37bd68f470351a031fb43cce01bb3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Apr 2019 00:07:27 +0300 Subject: [PATCH 218/928] typing of return value corrected --- freqtrade/exchange/exchange.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dc0c197c0..609c9117e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -697,9 +697,8 @@ def is_exchange_supported(exchange: str, ccxt_module=None) -> bool: return exchange in supported_exchanges(ccxt_module) -def supported_exchanges(ccxt_module=None) -> str: - exchanges = ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges - return exchanges +def supported_exchanges(ccxt_module=None) -> List[str]: + return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges def timeframe_to_seconds(ticker_interval: str) -> int: From 902ffa68539bdda660cb0258b23dfef15d8b56c9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Apr 2019 00:15:17 +0300 Subject: [PATCH 219/928] impoved argument and exception handling in scripts/get_market_pairs.py --- scripts/get_market_pairs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py index 1a4c593f9..1d1f2114b 100644 --- a/scripts/get_market_pairs.py +++ b/scripts/get_market_pairs.py @@ -1,5 +1,6 @@ import os import sys +import traceback root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(root + '/python') @@ -49,6 +50,11 @@ def print_supported_exchanges(): try: + if len(sys.argv) < 2: + dump("Usage: python " + sys.argv[0], green('id')) + print_supported_exchanges() + sys.exit(1) + id = sys.argv[1] # get exchange id from command line arguments # check if the exchange is supported by ccxt @@ -87,5 +93,7 @@ try: except Exception as e: dump('[' + type(e).__name__ + ']', str(e)) + dump(traceback.format_exc()) dump("Usage: python " + sys.argv[0], green('id')) print_supported_exchanges() + sys.exit(1) From c2ca899c7eb66dc83d1d2bda5adbe44f8381bb97 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Apr 2019 00:59:53 +0300 Subject: [PATCH 220/928] fixed printed message; cosmetic changes in the code in scripts/download_backtest_data.py --- scripts/download_backtest_data.py | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 50005b97b..2acadc8b6 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 - -"""This script generate json data""" +""" +This script generates json data +""" import json import sys from pathlib import Path @@ -35,7 +36,7 @@ if args.config: config: Dict[str, Any] = {} # Now expecting a list of config filenames here, not a string for path in args.config: - print('Using config: %s ...', path) + print(f"Using config: {path}...") # Merge config options, overwriting old values config = deep_merge_dicts(configuration._load_config_file(path), config) @@ -44,18 +45,19 @@ if args.config: config['exchange']['key'] = '' config['exchange']['secret'] = '' else: - config = {'stake_currency': '', - 'dry_run': True, - 'exchange': { - 'name': args.exchange, - 'key': '', - 'secret': '', - 'pair_whitelist': [], - 'ccxt_async_config': { - "enableRateLimit": False - } - } - } + config = { + 'stake_currency': '', + 'dry_run': True, + 'exchange': { + 'name': args.exchange, + 'key': '', + 'secret': '', + 'pair_whitelist': [], + 'ccxt_async_config': { + 'enableRateLimit': False + } + } + } dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name']) From 12ca103f9f4d98af2b68a5e532d9b2fbac80513d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 11 Apr 2019 12:38:06 +0000 Subject: [PATCH 221/928] Update ccxt from 1.18.456 to 1.18.458 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 99c5104c9..ed6e5e236 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.456 +ccxt==1.18.458 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 8bdbfbf19487263f2e41730e4e50af31ec47d0d5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Apr 2019 18:07:51 +0300 Subject: [PATCH 222/928] tests for options added --- freqtrade/tests/conftest.py | 65 +++++++++++++++++++++ freqtrade/tests/test_configuration.py | 83 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0bff1d5e9..b19518ee7 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -197,6 +197,71 @@ def default_conf(): return configuration +@pytest.fixture(scope="function") +def all_conf(): + """ Returns validated configuration with all options """ + configuration = { + "max_open_trades": 1, + "stake_currency": "BTC", + "stake_amount": 0.001, + "fiat_display_currency": "USD", + "ticker_interval": '5m', + "dry_run": True, + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.10, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": False, + "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": 1 + }, + "exchange": { + "name": "bittrex", + "sandbox": False, + "enabled": True, + "key": "key", + "secret": "secret", + "password": "password", + "pair_whitelist": [ + "ETH/BTC", + "LTC/BTC", + "XRP/BTC", + "NEO/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC", + "HOT/BTC", + ] + }, + "telegram": { + "enabled": True, + "token": "token", + "chat_id": "0" + }, + "initial_state": "running", + "db_url": "sqlite://", + "loglevel": logging.DEBUG, + } + return configuration + + @pytest.fixture def update(): _update = Update(0) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 45e539c2f..edc6111da 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -617,3 +617,86 @@ def test_validate_tsl(default_conf): default_conf['trailing_stop_positive_offset'] = 0.015 Configuration(Namespace()) configuration._validate_config_consistency(default_conf) + + +# config['exchange'] subtree has required options in it +# so it cannot be omitted in the config +def test_load_config_default_exchange(all_conf) -> None: + del all_conf['exchange'] + + assert 'exchange' not in all_conf + + with pytest.raises(ValidationError, + match=r'\'exchange\' is a required property'): + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) +### assert 'exchange' in all_conf + + +# config['exchange']['name'] option is required +# so it cannot be omitted in the config +def test_load_config_default_exchange_name(all_conf) -> None: + del all_conf['exchange']['name'] + + assert 'name' not in all_conf['exchange'] + + with pytest.raises(ValidationError, + match=r'\'name\' is a required property'): + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) + + +# config['exchange']['sandbox'] option has default value: False +# so it can be omitted in the config and the default value +# should be present in the config as the option value +def test_load_config_default_exchange_sandbox(all_conf) -> None: + del all_conf['exchange']['sandbox'] + + assert 'sandbox' not in all_conf['exchange'] + + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) + assert 'sandbox' in all_conf['exchange'] + assert all_conf['exchange']['sandbox'] is False + + +# config['exchange']['key'] option has default value: '' +# so it can be omitted in the config and the default value +# should be present in the config as the option value +def test_load_config_default_exchange_key(all_conf) -> None: + del all_conf['exchange']['key'] + + assert 'key' not in all_conf['exchange'] + + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) + assert 'key' in all_conf['exchange'] + assert all_conf['exchange']['key'] == '' + + +# config['exchange']['secret'] option has default value: '' +# so it can be omitted in the config and the default value +# should be present in the config as the option value +def test_load_config_default_exchange_secret(all_conf) -> None: + del all_conf['exchange']['secret'] + + assert 'secret' not in all_conf['exchange'] + + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) + assert 'secret' in all_conf['exchange'] + assert all_conf['exchange']['secret'] == '' + + +# config['exchange']['password'] option has default value: '' +# so it can be omitted in the config and the default value +# should be present in the config as the option value +def test_load_config_default_exchange_password(all_conf) -> None: + del all_conf['exchange']['password'] + + assert 'password' not in all_conf['exchange'] + + configuration = Configuration(Namespace()) + configuration._validate_config_schema(all_conf) + assert 'password' in all_conf['exchange'] + assert all_conf['exchange']['password'] == '' From c3a9db6488db2acff97d00c4e1fe20fc989fd8b4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Apr 2019 22:22:33 +0300 Subject: [PATCH 223/928] change comments to docstrings --- freqtrade/tests/test_configuration.py | 45 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index edc6111da..dde9ea271 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -619,9 +619,11 @@ def test_validate_tsl(default_conf): configuration._validate_config_consistency(default_conf) -# config['exchange'] subtree has required options in it -# so it cannot be omitted in the config def test_load_config_default_exchange(all_conf) -> None: + """ + config['exchange'] subtree has required options in it + so it cannot be omitted in the config + """ del all_conf['exchange'] assert 'exchange' not in all_conf @@ -630,12 +632,13 @@ def test_load_config_default_exchange(all_conf) -> None: match=r'\'exchange\' is a required property'): configuration = Configuration(Namespace()) configuration._validate_config_schema(all_conf) -### assert 'exchange' in all_conf -# config['exchange']['name'] option is required -# so it cannot be omitted in the config def test_load_config_default_exchange_name(all_conf) -> None: + """ + config['exchange']['name'] option is required + so it cannot be omitted in the config + """ del all_conf['exchange']['name'] assert 'name' not in all_conf['exchange'] @@ -646,10 +649,12 @@ def test_load_config_default_exchange_name(all_conf) -> None: configuration._validate_config_schema(all_conf) -# config['exchange']['sandbox'] option has default value: False -# so it can be omitted in the config and the default value -# should be present in the config as the option value def test_load_config_default_exchange_sandbox(all_conf) -> None: + """ + config['exchange']['sandbox'] option has default value: False + so it can be omitted in the config and the default value + should be present in the config as the option value + """ del all_conf['exchange']['sandbox'] assert 'sandbox' not in all_conf['exchange'] @@ -660,10 +665,12 @@ def test_load_config_default_exchange_sandbox(all_conf) -> None: assert all_conf['exchange']['sandbox'] is False -# config['exchange']['key'] option has default value: '' -# so it can be omitted in the config and the default value -# should be present in the config as the option value def test_load_config_default_exchange_key(all_conf) -> None: + """ + config['exchange']['key'] option has default value: '' + so it can be omitted in the config and the default value + should be present in the config as the option value + """ del all_conf['exchange']['key'] assert 'key' not in all_conf['exchange'] @@ -674,10 +681,12 @@ def test_load_config_default_exchange_key(all_conf) -> None: assert all_conf['exchange']['key'] == '' -# config['exchange']['secret'] option has default value: '' -# so it can be omitted in the config and the default value -# should be present in the config as the option value def test_load_config_default_exchange_secret(all_conf) -> None: + """ + config['exchange']['secret'] option has default value: '' + so it can be omitted in the config and the default value + should be present in the config as the option value + """ del all_conf['exchange']['secret'] assert 'secret' not in all_conf['exchange'] @@ -688,10 +697,12 @@ def test_load_config_default_exchange_secret(all_conf) -> None: assert all_conf['exchange']['secret'] == '' -# config['exchange']['password'] option has default value: '' -# so it can be omitted in the config and the default value -# should be present in the config as the option value def test_load_config_default_exchange_password(all_conf) -> None: + """ + config['exchange']['password'] option has default value: '' + so it can be omitted in the config and the default value + should be present in the config as the option value + """ del all_conf['exchange']['password'] assert 'password' not in all_conf['exchange'] From c3b9d69919e67a0f36cdd74a54a2b438ff96cfd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Apr 2019 07:05:00 +0200 Subject: [PATCH 224/928] Add docstring explaining the source of the script --- scripts/get_market_pairs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py index 1a4c593f9..c4b9777ea 100644 --- a/scripts/get_market_pairs.py +++ b/scripts/get_market_pairs.py @@ -1,3 +1,7 @@ +""" +This script was adapted from ccxt here: +https://github.com/ccxt/ccxt/blob/master/examples/py/arbitrage-pairs.py +""" import os import sys From d87db70ed0ddb41af199e1a1c69444ca763caf26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Apr 2019 07:05:15 +0200 Subject: [PATCH 225/928] Fix missing column header --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ca108b17e..0bc877272 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -266,7 +266,8 @@ class Telegram(RPC): headers=[ 'Day', f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}' + f'Profit {fiat_disp_cur}', + f'Trades' ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' From 016e8fde89a2d1d7e5a85dde7c570e2147a07e6a Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 12 Apr 2019 10:32:43 +0300 Subject: [PATCH 226/928] wrong rendering at freqtrade.io fixed; other cosmetics in docs/ * Titles render wrong both in the docs dir and at freqtrade.io * Last list of links renders wring at freqtrade.io --- docs/configuration.md | 2 +- docs/faq.md | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f7e2a07f3..06937945d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -70,8 +70,8 @@ Mandatory Parameters are marked as **Required**. | `strategy` | DefaultStrategy | Defines Strategy class to use. | `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. -| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. +| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. ### Parameters in the strategy diff --git a/docs/faq.md b/docs/faq.md index 60c1509e0..c551e3638 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -19,8 +19,7 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -#### I’d like to change the stake amount. Can I just stop the bot with -/stop and then change the config.json and run it again? +#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more @@ -31,16 +30,16 @@ like pauses. You can stop your bot, adjust settings and start it again. That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -#### Is there a setting to only SELL the coins being held and not -perform anymore BUYS? +#### Is there a setting to only SELL the coins being held and not perform anymore BUYS? You can use the `/forcesell all` command from Telegram. ### Hyperopt module #### How many epoch do I need to get a good Hyperopt result? + Per default Hyperopts without `-e` or `--epochs` parameter will only -run 100 epochs, means 100 evals of your triggers, guards, .... Too few +run 100 epochs, means 100 evals of your triggers, guards, ... Too few to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. @@ -64,10 +63,10 @@ Finding a great Hyperopt results takes time. If you wonder why it takes a while to find great hyperopt results This answer was written during the under the release 0.15.1, when we had: + - 8 triggers - 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to -be evaluated +- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is @@ -82,10 +81,9 @@ of the search space. The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. You can find further info on expectancy, winrate, risk management and position size in the following sources: -* https://www.tradeciety.com/ultimate-math-guide-for-traders/ -* http://www.vantharp.com/tharp-concepts/expectancy.asp -* https://samuraitradingacademy.com/trading-expectancy/ -* https://www.learningmarkets.com/determining-expectancy-in-your-trading/ -* http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ -* https://www.babypips.com/trading/trade-expectancy-matter - +- https://www.tradeciety.com/ultimate-math-guide-for-traders/ +- http://www.vantharp.com/tharp-concepts/expectancy.asp +- https://samuraitradingacademy.com/trading-expectancy/ +- https://www.learningmarkets.com/determining-expectancy-in-your-trading/ +- http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ +- https://www.babypips.com/trading/trade-expectancy-matter From 9f828224bc2bf72ed2f45e7857fe1df9d50f0d52 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 12 Apr 2019 12:39:05 +0000 Subject: [PATCH 227/928] Update ccxt from 1.18.458 to 1.18.460 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed6e5e236..7ab39e78a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.458 +ccxt==1.18.460 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 2f79cf13044b1eff958db96432f1f3b4f7e1fd7e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 13 Apr 2019 12:39:05 +0000 Subject: [PATCH 228/928] Update ccxt from 1.18.460 to 1.18.466 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7ab39e78a..952ee265e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.460 +ccxt==1.18.466 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 37b1389f123730c65a2e30aa1c835ce9968ab8ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Apr 2019 10:17:06 +0200 Subject: [PATCH 229/928] Fix flake8 --- freqtrade/configuration.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index e7441c18d..ae9978f81 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -194,7 +194,7 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir - def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901 """ Extract information for sys.argv and load Backtesting configuration :return: configuration as dictionary @@ -224,14 +224,16 @@ class Configuration(object): logger.info('max_open_trades set to unlimited ...') elif 'max_open_trades' in self.args and self.args.max_open_trades: config.update({'max_open_trades': self.args.max_open_trades}) - logger.info('Parameter --max_open_trades detected, overriding max_open_trades to: %s ...', config.get('max_open_trades')) + logger.info('Parameter --max_open_trades detected, ' + 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) else: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) # If --stake_amount is used we update configuration if 'stake_amount' in self.args and self.args.stake_amount: config.update({'stake_amount': self.args.stake_amount}) - logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', config.get('stake_amount')) + logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', + config.get('stake_amount')) # If --timerange is used we add it to the configuration if 'timerange' in self.args and self.args.timerange: From 5e0e8de4f6673f0a83161f70a2abdbf86a668e32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Apr 2019 13:13:28 +0200 Subject: [PATCH 230/928] Version bump to 3.7.3 in docker file --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index aeefc0722..e36766530 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.2-slim-stretch +FROM python:3.7.3-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ From 4f6df731563b933876e377b7c6f4ac40795351ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Apr 2019 15:57:44 +0200 Subject: [PATCH 231/928] Update documentation for Raspberry install since we now have a rpi-requirements file --- docs/installation.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 995bc561b..23a6cbd23 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -315,7 +315,6 @@ Before installing FreqTrade on a Raspberry Pi running the official Raspbian Imag The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. -If you have installed it from (mini)conda, you can remove `numpy`, `scipy`, and `pandas` from `requirements.txt` before you install it with `pip`. Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). @@ -327,7 +326,7 @@ conda activate freqtrade conda install scipy pandas numpy sudo apt install libffi-dev -python3 -m pip install -r requirements.txt +python3 -m pip install -r requirements-pi.txt python3 -m pip install -e . ``` From 6be4c6af0e6ca45fdd2a9df4ce94db6b46a7777f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 15 Apr 2019 12:39:05 +0000 Subject: [PATCH 232/928] Update ccxt from 1.18.466 to 1.18.468 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 952ee265e..d8d4d84d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.466 +ccxt==1.18.468 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 0ece1688330067732424bf51ebd01191f5086a7f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 15 Apr 2019 12:39:06 +0000 Subject: [PATCH 233/928] Update ccxt from 1.18.353 to 1.18.468 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 4fca8b032..c2e3e0414 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.353 +ccxt==1.18.468 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1 From 7efab85b109251f15138f3d994ca585b7f3d3b6a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 15 Apr 2019 12:39:08 +0000 Subject: [PATCH 234/928] Update sqlalchemy from 1.3.1 to 1.3.2 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index c2e3e0414..7d7b779e9 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,5 +1,5 @@ ccxt==1.18.468 -SQLAlchemy==1.3.1 +SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 From 4f557af6cb80327dbc3b9115af8f52b1afe55fad Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:04 +0000 Subject: [PATCH 235/928] Update ccxt from 1.18.468 to 1.18.470 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d8d4d84d5..8b6e3014d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.468 +ccxt==1.18.470 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From 5cb90bdf775116bbd866045ef7bf11cf8ccbb60a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:05 +0000 Subject: [PATCH 236/928] Update ccxt from 1.18.468 to 1.18.470 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 7d7b779e9..aa1133a0f 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.468 +ccxt==1.18.470 SQLAlchemy==1.3.2 python-telegram-bot==11.1.0 arrow==0.13.1 From aa63f2be1fe3ddcdcc35fd24a851201d457130eb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:06 +0000 Subject: [PATCH 237/928] Update sqlalchemy from 1.3.2 to 1.3.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8b6e3014d..40903e9d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ccxt==1.18.470 -SQLAlchemy==1.3.2 +SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 From 87ff5ad1e09534f12a438aec05b04c973cb57da5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:07 +0000 Subject: [PATCH 238/928] Update sqlalchemy from 1.3.2 to 1.3.3 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index aa1133a0f..0fd26a3d9 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,5 +1,5 @@ ccxt==1.18.470 -SQLAlchemy==1.3.2 +SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 From c40406d26eb924356773148a5b8abc525accb10d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:09 +0000 Subject: [PATCH 239/928] Update pytest from 4.4.0 to 4.4.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c59923ed2..77f122a59 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.4.0 +pytest==4.4.1 pytest-mock==1.10.3 pytest-asyncio==0.10.0 pytest-cov==2.6.1 From b2a623ee169485bb8bf82367c38d4679a1777fb5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 16 Apr 2019 12:39:12 +0000 Subject: [PATCH 240/928] Update plotly from 3.7.1 to 3.8.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index e582fddf6..0b924b608 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.7.1 +plotly==3.8.0 From 43119efaf0b2262984611a789eb0ba7bb32d7904 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Apr 2019 19:41:02 +0200 Subject: [PATCH 241/928] Remove ccxt_rate_limit completely (was deprecated) --- freqtrade/configuration.py | 5 ----- freqtrade/exchange/exchange.py | 1 - freqtrade/tests/test_configuration.py | 9 --------- 3 files changed, 15 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index ae9978f81..7f0f3c34a 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -393,11 +393,6 @@ class Configuration(object): raise OperationalException( exception_msg ) - # Depreciation warning - if 'ccxt_rate_limit' in config.get('exchange', {}): - logger.warning("`ccxt_rate_limit` has been deprecated in favor of " - "`ccxt_config` and `ccxt_async_config` and will be removed " - "in a future version.") logger.debug('Exchange "%s" supported', exchange) return True diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ffd574f90..57a002384 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -146,7 +146,6 @@ class Exchange(object): 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'enableRateLimit': exchange_config.get('ccxt_rate_limit', True) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 45e539c2f..bcd0bd92c 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -485,15 +485,6 @@ def test_check_exchange(default_conf, caplog) -> None: ): configuration.check_exchange(default_conf) - # Test ccxt_rate_limit depreciation - default_conf.get('exchange').update({'name': 'binance'}) - default_conf['exchange']['ccxt_rate_limit'] = True - configuration.check_exchange(default_conf) - assert log_has("`ccxt_rate_limit` has been deprecated in favor of " - "`ccxt_config` and `ccxt_async_config` and will be removed " - "in a future version.", - caplog.record_tuples) - def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.configuration.open', mocker.mock_open( From 5db10bdcc7929b8b05cf69ec249cc6b537122480 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Apr 2019 19:51:42 +0200 Subject: [PATCH 242/928] Add rateLimit parameters for different exchanges --- config.json.example | 3 ++- config_binance.json.example | 3 ++- config_full.json.example | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config.json.example b/config.json.example index 323ff711e..94084434a 100644 --- a/config.json.example +++ b/config.json.example @@ -30,7 +30,8 @@ "secret": "your_exchange_secret", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false + "enableRateLimit": true, + "rateLimit": 500 }, "pair_whitelist": [ "ETH/BTC", diff --git a/config_binance.json.example b/config_binance.json.example index 3d11f317a..ab57db88f 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -30,7 +30,8 @@ "secret": "your_exchange_secret", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false + "enableRateLimit": true, + "rateLimit": 200 }, "pair_whitelist": [ "AST/BTC", diff --git a/config_full.json.example b/config_full.json.example index 58d3d3ac6..20ba10c89 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -61,6 +61,7 @@ "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": false, + "rateLimit": 500, "aiohttp_trust_env": false }, "pair_whitelist": [ From 52cc2d224e47ff86a527423f444cd8131d5626d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Apr 2019 19:51:56 +0200 Subject: [PATCH 243/928] improve documentation for exchange configuration --- docs/configuration.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 06937945d..0da14d9f6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -46,7 +46,6 @@ Mandatory Parameters are marked as **Required**. | `exchange.secret` | secret | API secret to use for the exchange. Only required when you are in production mode. | `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. -| `exchange.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange. | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. @@ -217,6 +216,7 @@ The below is the default which is used if this is not configured in either strat the bot would recreate one. ### Understand order_time_in_force + The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange. Three commonly used time in force are: @@ -252,9 +252,9 @@ The possible values are: `gtc` (default), `fok` or `ioc`. This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing. -### What values for exchange.name? +### Exchange configuration -Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency +Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested with only Bittrex and Binance. @@ -266,6 +266,30 @@ The bot was tested with the following exchanges: Feel free to test other exchanges and submit your PR to improve the bot. +#### Sample exchange configuration + +A exchange configuration for "binance" would look as follows: + +```json +"exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, +``` + +This configuration enables binance, as well as rate limiting to avoid bans from the exchange. +`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. + +!!! Note + Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. + We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. + + ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the From a7383ad35d64a08850350eb5ece372e41efef896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Apr 2019 19:54:04 +0200 Subject: [PATCH 244/928] enable ratelimit in download-backtest-data too --- scripts/download_backtest_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 2acadc8b6..42b305778 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -54,7 +54,8 @@ else: 'secret': '', 'pair_whitelist': [], 'ccxt_async_config': { - 'enableRateLimit': False + 'enableRateLimit': True, + 'rateLimit': 200 } } } From 2cee716181cd414b29d45ab46eb3d1a0d653e7fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Apr 2019 20:25:48 +0200 Subject: [PATCH 245/928] Gracefully handle pickle-errors when @staticmethod is used pOinted out in https://github.com/freqtrade/freqtrade-strategies/issues/28 --- freqtrade/resolvers/strategy_resolver.py | 11 +++++++---- freqtrade/tests/strategy/test_strategy.py | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 44cc3fe76..b2743a417 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -157,12 +157,15 @@ class StrategyResolver(IResolver): getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - - return import_strategy(strategy, config=config) + try: + return import_strategy(strategy, config=config) + except TypeError as e: + logger.warning( + f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}") except FileNotFoundError: logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) raise ImportError( - "Impossible to load Strategy '{}'. This class does not exist" - " or contains Python code errors".format(strategy_name) + f"Impossible to load Strategy '{strategy_name}'. This class does not exist" + " or contains Python code errors" ) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index b63180d1e..2ed2567f9 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,17 +1,19 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging +import warnings from base64 import urlsafe_b64encode from os import path from pathlib import Path -import warnings +from unittest.mock import Mock import pytest from pandas import DataFrame +from freqtrade.resolvers import StrategyResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy -from freqtrade.resolvers import StrategyResolver +from freqtrade.tests.conftest import log_has_re def test_import_strategy(caplog): @@ -94,6 +96,16 @@ def test_load_not_found_strategy(): strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) +def test_load_staticmethod_importerror(mocker, caplog): + mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( + side_effect=TypeError("can't pickle staticmethod objects"))) + with pytest.raises(ImportError, + match=r"Impossible to load Strategy 'DefaultStrategy'." + r" This class does not exist or contains Python code errors"): + StrategyResolver() + assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) + + def test_strategy(result): config = {'strategy': 'DefaultStrategy'} From d4947ba0ee8c54d19249cbb0e1d10b1aa57d7499 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 17 Apr 2019 12:40:07 +0000 Subject: [PATCH 246/928] Update ccxt from 1.18.470 to 1.18.472 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 40903e9d6..771102e90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.470 +ccxt==1.18.472 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 7f229bbf3950ecd9790880a1c1063b237f09419a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 17 Apr 2019 12:40:09 +0000 Subject: [PATCH 247/928] Update ccxt from 1.18.470 to 1.18.472 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 0fd26a3d9..30e4a4ce4 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.470 +ccxt==1.18.472 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 8abdbc41e16de38eec1e81edea8d230ca5e8b333 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 17 Apr 2019 12:40:10 +0000 Subject: [PATCH 248/928] Update mypy from 0.700 to 0.701 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 77f122a59..9d0e99843 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,4 +9,4 @@ pytest-mock==1.10.3 pytest-asyncio==0.10.0 pytest-cov==2.6.1 coveralls==1.7.0 -mypy==0.700 +mypy==0.701 From 795c2e4aa2f238a17d38dc087ee65644c59b8d21 Mon Sep 17 00:00:00 2001 From: Misagh Date: Thu, 18 Apr 2019 08:07:43 +0200 Subject: [PATCH 249/928] version to 0.18.5 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 292613297..f28809f33 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.18.2-dev' +__version__ = '0.18.5' class DependencyException(BaseException): From c299d9249fbefe31b487372d96f843568879b457 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 18 Apr 2019 12:40:07 +0000 Subject: [PATCH 250/928] Update ccxt from 1.18.472 to 1.18.475 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 771102e90..436ce89ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.472 +ccxt==1.18.475 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 789b445815cc782ae0ed7fa126060d0b6b59d454 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 18 Apr 2019 12:40:08 +0000 Subject: [PATCH 251/928] Update ccxt from 1.18.472 to 1.18.475 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 30e4a4ce4..3c73b6007 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.472 +ccxt==1.18.475 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 578ad903bca9afb2b98e1c3dba57d6e7906beb87 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 18 Apr 2019 12:40:10 +0000 Subject: [PATCH 252/928] Update urllib3 from 1.24.1 to 1.24.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 436ce89ee..f227603ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.24.1 +urllib3==1.24.2 wrapt==1.11.1 numpy==1.16.2 pandas==0.24.2 From 5c10e9a7fa1cdd63c5673326924da29da181b5c6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 18 Apr 2019 12:40:11 +0000 Subject: [PATCH 253/928] Update urllib3 from 1.24.1 to 1.24.2 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 3c73b6007..b0e15f39d 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -4,7 +4,7 @@ python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.24.1 +urllib3==1.24.2 wrapt==1.11.1 scikit-learn==0.20.3 joblib==0.13.2 From d82fb572233421f4bd2bd6787450d986afa1869d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 18 Apr 2019 12:40:16 +0000 Subject: [PATCH 254/928] Update pytest-mock from 1.10.3 to 1.10.4 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9d0e99843..123531f23 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==4.4.1 -pytest-mock==1.10.3 +pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.6.1 coveralls==1.7.0 From 72657758d550f780342cee1b017d09d67bc73edc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:43:12 +0200 Subject: [PATCH 255/928] Restore get_market_pairs from develop --- scripts/get_market_pairs.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py index 00639c50b..cd38bf2fa 100644 --- a/scripts/get_market_pairs.py +++ b/scripts/get_market_pairs.py @@ -1,7 +1,10 @@ +""" +This script was adapted from ccxt here: +https://github.com/ccxt/ccxt/blob/master/examples/py/arbitrage-pairs.py +""" import os import sys - -from freqtrade.exchange import is_exchange_supported, supported_exchanges +import traceback root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(root + '/python') @@ -46,15 +49,22 @@ def dump(*args): def print_supported_exchanges(): - dump('Supported exchanges:', green(', '.join(supported_exchanges()))) + dump('Supported exchanges:', green(', '.join(ccxt.exchanges))) try: + if len(sys.argv) < 2: + dump("Usage: python " + sys.argv[0], green('id')) + print_supported_exchanges() + sys.exit(1) + id = sys.argv[1] # get exchange id from command line arguments # check if the exchange is supported by ccxt - if is_exchange_supported(id): + exchange_found = id in ccxt.exchanges + + if exchange_found: dump('Instantiating', green(id), 'exchange') # instantiate the exchange by id @@ -79,11 +89,15 @@ try: for (k, v) in tuples: dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote'])) + else: + dump('Exchange ' + red(id) + ' not found') print_supported_exchanges() except Exception as e: dump('[' + type(e).__name__ + ']', str(e)) + dump(traceback.format_exc()) dump("Usage: python " + sys.argv[0], green('id')) print_supported_exchanges() + sys.exit(1) From ed6a92cd0feb802a180d0f41c55e9107739e6de8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 19 Apr 2019 12:40:05 +0000 Subject: [PATCH 256/928] Update ccxt from 1.18.475 to 1.18.480 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f227603ce..ba18bf07f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.475 +ccxt==1.18.480 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 5a65b6caee20c8ed022a98492510bee61ac6f2fe Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 19 Apr 2019 12:40:06 +0000 Subject: [PATCH 257/928] Update ccxt from 1.18.475 to 1.18.480 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index b0e15f39d..adbdb81da 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.475 +ccxt==1.18.480 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 8e8ec2fba6074e0061d738ad127563dd5b5576a6 Mon Sep 17 00:00:00 2001 From: Misagh Date: Fri, 19 Apr 2019 16:01:26 +0200 Subject: [PATCH 258/928] version to 0.18.5-dev --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 292613297..f84f5240e 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.18.2-dev' +__version__ = '0.18.5-dev' class DependencyException(BaseException): From 9b8067cbc3e99b60b6732c7deac13100ffc95473 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2019 12:48:58 +0200 Subject: [PATCH 259/928] Improve developer documentation --- docs/developer.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6fbcdc812..e7f79bc1c 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -95,9 +95,9 @@ git checkout develop git checkout -b new_release ``` -* edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) +* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) * Commit this part -* push that branch to the remote and create a PR +* push that branch to the remote and create a PR against the master branch ### create changelog from git commits @@ -108,10 +108,12 @@ git log --oneline --no-decorate --no-merges master..develop ### Create github release / tag +* Use the button "Draft a new release" in the Github UI (subsection releases) * Use the version-number specified as tag. * Use "master" as reference (this step comes after the above PR is merged). -* use the above changelog as release comment (as codeblock) +* Use the above changelog as release comment (as codeblock) ### After-release -* update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`) +* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). +* Create a PR against develop to update that branch. From 7fa50465759c30f16af487816ed96fb33aa15c7f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 20 Apr 2019 12:40:05 +0000 Subject: [PATCH 260/928] Update ccxt from 1.18.480 to 1.18.481 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ba18bf07f..76e50695b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.480 +ccxt==1.18.481 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 278e5f4cc67764c2042c6bf635b438ef941f62d2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 20 Apr 2019 12:40:06 +0000 Subject: [PATCH 261/928] Update ccxt from 1.18.480 to 1.18.481 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index adbdb81da..c0eff588b 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.480 +ccxt==1.18.481 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 395aed5f97eb22bcd9421b4c45f0104af2e513ee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 20 Apr 2019 12:40:07 +0000 Subject: [PATCH 262/928] Update plotly from 3.8.0 to 3.8.1 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 0b924b608..4cdd6ec8f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.8.0 +plotly==3.8.1 From a118003d0a7fc6a5bcc7fd6816aab682da822263 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 21 Apr 2019 12:41:04 +0000 Subject: [PATCH 263/928] Update ccxt from 1.18.481 to 1.18.483 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e50695b..859deca64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.481 +ccxt==1.18.483 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From abc4840d164f0494f776419f41889704df9762f9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 21 Apr 2019 12:41:05 +0000 Subject: [PATCH 264/928] Update ccxt from 1.18.481 to 1.18.483 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index c0eff588b..8a23b896a 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.481 +ccxt==1.18.483 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 3bcc60333d8ea9d46d6b22af0f0e9cf936d905e6 Mon Sep 17 00:00:00 2001 From: NatanNMB15 Date: Sun, 21 Apr 2019 13:49:07 -0300 Subject: [PATCH 265/928] Added command for Wallets Sync after a trade is closed in "update_trade" method in "freqtradebot" class, this will help the Wallets get updated after a trade is sold and closed, specifically LIMIT_SELL trades, then bot can work properly with new trades. --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 038510755..6a825eadd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -522,6 +522,10 @@ class FreqtradeBot(object): trade.update(order) + # Updating wallets when order is closed + if trade.is_open == False: + self.wallets.update() + def get_sell_rate(self, pair: str, refresh: bool) -> float: """ Get sell rate - either using get-ticker bid or first bid based on orderbook From 706b30f4d245da345833d9c3b88ad9ccc9283767 Mon Sep 17 00:00:00 2001 From: NatanNMB15 Date: Sun, 21 Apr 2019 14:20:28 -0300 Subject: [PATCH 266/928] Fix "if" condition with "if not" for check if trade is open. --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6a825eadd..2fe55336f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -523,7 +523,7 @@ class FreqtradeBot(object): trade.update(order) # Updating wallets when order is closed - if trade.is_open == False: + if not trade.is_open: self.wallets.update() def get_sell_rate(self, pair: str, refresh: bool) -> float: From 6b87d94bb0d50070b8c47542cb8641951ccc4dab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 22 Apr 2019 01:10:01 +0300 Subject: [PATCH 267/928] --print-all command line option added for hyperopt --- freqtrade/arguments.py | 8 +++++++- freqtrade/configuration.py | 4 ++++ freqtrade/optimize/hyperopt.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index b0acb4122..96f080bd2 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -283,7 +283,6 @@ class Arguments(object): dest='position_stacking', default=False ) - parser.add_argument( '--dmmp', '--disable-max-market-positions', help='Disable applying `max_open_trades` during backtest ' @@ -309,6 +308,13 @@ class Arguments(object): nargs='+', dest='spaces', ) + parser.add_argument( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + dest='print_all', + default=False + ) def _build_subcommands(self) -> None: """ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 78e64e27c..65a8d644e 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -320,6 +320,10 @@ class Configuration(object): config.update({'spaces': self.args.spaces}) logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) + if 'print_all' in self.args and self.args.print_all: + config.update({'print_all': self.args.print_all}) + logger.info('Parameter --print-all detected: %s', config.get('print_all')) + return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f6d39f11c..b37027244 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -115,7 +115,7 @@ class Hyperopt(Backtesting): """ Log results if it is better than any previous evaluation """ - if results['loss'] < self.current_best_loss: + if self.config.get('print_all', False) or results['loss'] < self.current_best_loss: current = results['current_tries'] total = results['total_tries'] res = results['result'] From a9de2f80f21b0b18da074db59bcefefb7fb271fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Apr 2019 13:31:07 +0200 Subject: [PATCH 268/928] Add tests to update wallets after closing a limit-sell --- freqtrade/tests/test_freqtradebot.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 103c0777e..008974a6b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1407,7 +1407,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ amount=amount, exchange='binance', open_rate=0.245441, - open_order_id="123456" + open_order_id="123456", + is_open=True, ) freqtrade.update_trade_state(trade, limit_buy_order) assert trade.amount != amount @@ -1432,6 +1433,34 @@ def test_update_trade_state_exception(mocker, default_conf, assert log_has('Could not update trade amount: ', caplog.record_tuples) +def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + # get_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + wallet_mock = MagicMock() + mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) + + patch_exchange(mocker) + Trade.session = MagicMock() + amount = limit_sell_order["amount"] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + wallet_mock.reset_mock() + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + fee_open=0.0025, + fee_close=0.0025, + open_order_id="123456", + is_open=True, + ) + freqtrade.update_trade_state(trade, limit_sell_order) + assert trade.amount == limit_sell_order['amount'] + # Wallet needs to be updated after closing a limit-sell order to reenable buying + assert wallet_mock.call_count == 1 + + def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From 676cd6ffee6aead3e9fe2bd4e451918d3744139e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Apr 2019 13:36:14 +0200 Subject: [PATCH 269/928] Add assert to make sure trade was closed --- freqtrade/tests/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 008974a6b..67b05ac3e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1459,6 +1459,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde assert trade.amount == limit_sell_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 + assert not trade.is_open def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, From 8685fcd59341bdc50162a501929b7db3a00e8bfa Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Apr 2019 12:41:06 +0000 Subject: [PATCH 270/928] Update ccxt from 1.18.483 to 1.18.485 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 859deca64..d23170cd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.483 +ccxt==1.18.485 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 42d2b24d4857b159395ed6610ac8e5b18026100b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Apr 2019 12:41:07 +0000 Subject: [PATCH 271/928] Update ccxt from 1.18.483 to 1.18.485 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 8a23b896a..6ae30354e 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.483 +ccxt==1.18.485 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 3da1b24b6ad9f1a7c2840fa84f0c0d4a25efd7ee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Apr 2019 12:41:08 +0000 Subject: [PATCH 272/928] Update numpy from 1.16.2 to 1.16.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d23170cd2..9a9b17fa0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ cachetools==3.1.0 requests==2.21.0 urllib3==1.24.2 wrapt==1.11.1 -numpy==1.16.2 +numpy==1.16.3 pandas==0.24.2 scikit-learn==0.20.3 joblib==0.13.2 From ad85ac3dde913e7d0a38b02714c5ab932b2f3590 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 22 Apr 2019 21:24:45 +0300 Subject: [PATCH 273/928] make --refresh-pairs-cached common option for optimization; added support for it into hyperopt --- freqtrade/arguments.py | 101 ++++++++-------- freqtrade/configuration.py | 5 + freqtrade/data/history.py | 3 +- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 40 +++++-- freqtrade/tests/data/test_history.py | 14 ++- freqtrade/tests/optimize/test_backtesting.py | 1 + freqtrade/tests/optimize/test_hyperopt.py | 114 ++++++++++++++++++- 8 files changed, 205 insertions(+), 74 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 96f080bd2..7960a1f5c 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -141,10 +141,53 @@ class Arguments(object): dest='sd_notify', ) + @staticmethod + def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: + """ + Parses given common arguments for Backtesting, Edge and Hyperopt modules. + :param parser: + :return: + """ + parser.add_argument( + '-i', '--ticker-interval', + help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', + dest='ticker_interval', + type=str, + ) + parser.add_argument( + '--timerange', + help='Specify what timerange of data to use.', + default=None, + type=str, + dest='timerange', + ) + parser.add_argument( + '--max_open_trades', + help='Specify max_open_trades to use.', + default=None, + type=int, + dest='max_open_trades', + ) + parser.add_argument( + '--stake_amount', + help='Specify stake_amount.', + default=None, + type=float, + dest='stake_amount', + ) + parser.add_argument( + '-r', '--refresh-pairs-cached', + help='Refresh the pairs files in tests/testdata with the latest data from the ' + 'exchange. Use it if you want to run your optimization commands with ' + 'up-to-date data.', + action='store_true', + dest='refresh_pairs', + ) + @staticmethod def backtesting_options(parser: argparse.ArgumentParser) -> None: """ - Parses given arguments for Backtesting scripts. + Parses given arguments for Backtesting module. """ parser.add_argument( '--eps', '--enable-position-stacking', @@ -167,13 +210,6 @@ class Arguments(object): action='store_true', dest='live', ) - parser.add_argument( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your backtesting with up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) parser.add_argument( '--strategy-list', help='Provide a commaseparated list of strategies to backtest ' @@ -207,15 +243,8 @@ class Arguments(object): @staticmethod def edge_options(parser: argparse.ArgumentParser) -> None: """ - Parses given arguments for Backtesting scripts. + Parses given arguments for Edge module. """ - parser.add_argument( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your edge with up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) parser.add_argument( '--stoplosses', help='Defines a range of stoploss against which edge will assess the strategy ' @@ -225,48 +254,10 @@ class Arguments(object): dest='stoploss_range', ) - @staticmethod - def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: - """ - Parses given common arguments for Backtesting and Hyperopt scripts. - :param parser: - :return: - """ - parser.add_argument( - '-i', '--ticker-interval', - help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', - dest='ticker_interval', - type=str, - ) - - parser.add_argument( - '--timerange', - help='Specify what timerange of data to use.', - default=None, - type=str, - dest='timerange', - ) - - parser.add_argument( - '--max_open_trades', - help='Specify max_open_trades to use.', - default=None, - type=int, - dest='max_open_trades', - ) - - parser.add_argument( - '--stake_amount', - help='Specify stake_amount.', - default=None, - type=float, - dest='stake_amount', - ) - @staticmethod def hyperopt_options(parser: argparse.ArgumentParser) -> None: """ - Parses given arguments for Hyperopt scripts. + Parses given arguments for Hyperopt module. """ parser.add_argument( '--customhyperopt', diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 65a8d644e..284c91260 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -324,6 +324,11 @@ class Configuration(object): config.update({'print_all': self.args.print_all}) logger.info('Parameter --print-all detected: %s', config.get('print_all')) + # If -r/--refresh-pairs-cached is used we add it to the configuration + if 'refresh_pairs' in self.args and self.args.refresh_pairs: + config.update({'refresh_pairs': True}) + logger.info('Parameter -r/--refresh-pairs-cached detected ...') + return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 07a7f0b66..4dba1b760 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -116,7 +116,8 @@ def load_pair_history(pair: str, return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing) else: logger.warning('No data for pair: "%s", Interval: %s. ' - 'Use --refresh-pairs-cached to download the data', + 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'script to download the data', pair, ticker_interval) return None diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9ee1cd5cd..e7f06687d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -516,6 +516,7 @@ def start(args: Namespace) -> None: """ # Initialize configuration config = setup_configuration(args) + logger.info('Starting freqtrade in Backtesting mode') # Initialize backtesting object diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b37027244..24857aae2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,6 +20,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension +from freqtrade import DependencyException from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data.history import load_data @@ -258,6 +259,8 @@ class Hyperopt(Backtesting): datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, timerange=timerange ) @@ -265,7 +268,10 @@ class Hyperopt(Backtesting): self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) + + # We don't need exchange instance anymore while running hyperopt self.exchange = None # type: ignore + self.load_previous_results() cpus = multiprocessing.cpu_count() @@ -295,22 +301,16 @@ class Hyperopt(Backtesting): self.log_trials_result() -def start(args: Namespace) -> None: +def setup_configuration(args: Namespace) -> Dict[str, Any]: """ - Start Backtesting script + Prepare the configuration for the Hyperopt module :param args: Cli args from Arguments() - :return: None + :return: Configuration """ - - # Remove noisy log messages - logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) - - # Initialize configuration - # Monkey patch the configuration with hyperopt_conf.py configuration = Configuration(args, RunMode.HYPEROPT) - logger.info('Starting freqtrade in Hyperopt mode') config = configuration.load_config() + # Ensure we do not use Exchange credentials config['exchange']['key'] = '' config['exchange']['secret'] = '' @@ -320,7 +320,25 @@ def start(args: Namespace) -> None: "Read the documentation at " "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " "to understand how to configure hyperopt.") - raise ValueError("--strategy configured but not supported for hyperopt") + raise DependencyException("--strategy configured but not supported for hyperopt") + + return config + + +def start(args: Namespace) -> None: + """ + Start Backtesting script + :param args: Cli args from Arguments() + :return: None + """ + # Initialize configuration + config = setup_configuration(args) + + logger.info('Starting freqtrade in Hyperopt mode') + + # Remove noisy log messages + logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + # Initialize backtesting object hyperopt = Hyperopt(config) hyperopt.start() diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index c0b1cade3..14ec99042 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -68,7 +68,10 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert ld is None assert log_has( 'No data for pair: "UNITTEST/BTC", Interval: 7m. ' - 'Use --refresh-pairs-cached to download the data', caplog.record_tuples) + 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'script to download the data', + caplog.record_tuples + ) def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: @@ -96,9 +99,12 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau refresh_pairs=False, pair='MEME/BTC') assert os.path.isfile(file) is False - assert log_has('No data for pair: "MEME/BTC", Interval: 1m. ' - 'Use --refresh-pairs-cached to download the data', - caplog.record_tuples) + assert log_has( + 'No data for pair: "MEME/BTC", Interval: 1m. ' + 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'script to download the data', + caplog.record_tuples + ) # download a new pair if refresh_pairs is set history.load_pair_history(datadir=None, diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 0596cffb5..af0b07d6f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -260,6 +260,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert 'refresh_pairs' in config assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + assert 'timerange' in config assert log_has( 'Parameter --timerange detected: {} ...'.format(config['timerange']), diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 20baee99e..7151935b2 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,16 +1,19 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime +import json import os from unittest.mock import MagicMock import pandas as pd import pytest +from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize.hyperopt import Hyperopt, start +from freqtrade.optimize.hyperopt import Hyperopt, start, setup_configuration from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.resolvers import StrategyResolver, HyperOptResolver +from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args @@ -39,6 +42,112 @@ def create_trials(mocker, hyperopt) -> None: return [{'loss': 1, 'result': 'foo', 'params': {}}] +def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf) + )) + + args = [ + '--config', 'config.json', + 'hyperopt' + ] + + config = setup_configuration(get_args(args)) + assert 'max_open_trades' in config + assert 'stake_currency' in config + assert 'stake_amount' in config + assert 'exchange' in config + assert 'pair_whitelist' in config['exchange'] + assert 'datadir' in config + assert log_has( + 'Using data folder: {} ...'.format(config['datadir']), + caplog.record_tuples + ) + assert 'ticker_interval' in config + assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + + assert 'live' not in config + assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) + + assert 'position_stacking' not in config + assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + + assert 'refresh_pairs' not in config + assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + + assert 'timerange' not in config + assert 'runmode' in config + assert config['runmode'] == RunMode.HYPEROPT + + +def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf) + )) + mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + + args = [ + '--config', 'config.json', + '--datadir', '/foo/bar', + 'hyperopt', + '--ticker-interval', '1m', + '--timerange', ':100', + '--refresh-pairs-cached', + '--enable-position-stacking', + '--disable-max-market-positions', + '--epochs', '1000', + '--spaces', 'all', + '--print-all' + ] + + config = setup_configuration(get_args(args)) + assert 'max_open_trades' in config + assert 'stake_currency' in config + assert 'stake_amount' in config + assert 'exchange' in config + assert 'pair_whitelist' in config['exchange'] + assert 'datadir' in config + assert config['runmode'] == RunMode.HYPEROPT + + assert log_has( + 'Using data folder: {} ...'.format(config['datadir']), + caplog.record_tuples + ) + assert 'ticker_interval' in config + assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert log_has( + 'Using ticker_interval: 1m ...', + caplog.record_tuples + ) + + assert 'position_stacking' in config + assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + + assert 'use_max_market_positions' in config + assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) + assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) + + assert 'refresh_pairs' in config + assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) + + assert 'timerange' in config + assert log_has( + 'Parameter --timerange detected: {} ...'.format(config['timerange']), + caplog.record_tuples + ) + + assert 'epochs' in config + assert log_has('Parameter --epochs detected ...', caplog.record_tuples) + + assert 'spaces' in config + assert log_has( + 'Parameter -s/--spaces detected: {}'.format(config['spaces']), + caplog.record_tuples + ) + assert 'print_all' in config + assert log_has('Parameter --print-all detected: True', caplog.record_tuples) + + def test_hyperoptresolver(mocker, default_conf, caplog) -> None: mocker.patch( @@ -72,7 +181,6 @@ def test_start(mocker, default_conf, caplog) -> None: args = [ '--config', 'config.json', - '--strategy', 'DefaultStrategy', 'hyperopt', '--epochs', '5' ] @@ -107,7 +215,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ] args = get_args(args) StrategyResolver({'strategy': 'DefaultStrategy'}) - with pytest.raises(ValueError): + with pytest.raises(DependencyException): start(args) assert log_has( "Please don't use --strategy for hyperopt.", From 8dad8f25cf3d8b1a1371722b5a430a9f4f1e47f9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 22 Apr 2019 22:11:56 +0300 Subject: [PATCH 274/928] docs refreshed --- docs/bot-usage.md | 49 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 55988985a..87ce67fc6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -146,9 +146,11 @@ Backtesting also uses the config specified via `-c/--config`. ``` usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--eps] [--dmmp] [-l] [-r] - [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp] + [-l] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + [--export EXPORT] [--export-filename PATH] optional arguments: -h, --help show this help message and exit @@ -156,6 +158,14 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE Specify what timerange of data to use. + --max_open_trades MAX_OPEN_TRADES + Specify max_open_trades to use. + --stake_amount STAKE_AMOUNT + Specify stake_amount. + -r, --refresh-pairs-cached + Refresh the pairs files in tests/testdata with the + latest data from the exchange. Use it if you want to + run your optimization commands with up-to-date data. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). @@ -164,10 +174,6 @@ optional arguments: (same as setting `max_open_trades` to a very high number). -l, --live Use live data. - -r, --refresh-pairs-cached - Refresh the pairs files in tests/testdata with the - latest data from the exchange. Use it if you want to - run your backtesting with up-to-date data. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a commaseparated list of strategies to backtest Please note that ticker-interval needs to be @@ -206,8 +212,11 @@ to find optimal parameter values for your stategy. ``` usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] - [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [-r] + [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] + [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--print-all] optional arguments: -h, --help show this help message and exit @@ -215,6 +224,14 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE Specify what timerange of data to use. + --max_open_trades MAX_OPEN_TRADES + Specify max_open_trades to use. + --stake_amount STAKE_AMOUNT + Specify stake_amount. + -r, --refresh-pairs-cached + Refresh the pairs files in tests/testdata with the + latest data from the exchange. Use it if you want to + run your optimization commands with up-to-date data. --customhyperopt NAME Specify hyperopt class name (default: DefaultHyperOpts). @@ -229,7 +246,7 @@ optional arguments: -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] Specify which parameters to hyperopt. Space separate list. Default: all. - + --print-all Print all results, not only the best ones. ``` ## Edge commands @@ -237,8 +254,10 @@ optional arguments: To know your trade expectacny and winrate against historical data, you can use Edge. ``` -usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-r] - [--stoplosses STOPLOSS_RANGE] +usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [-r] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -246,10 +265,14 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE Specify what timerange of data to use. + --max_open_trades MAX_OPEN_TRADES + Specify max_open_trades to use. + --stake_amount STAKE_AMOUNT + Specify stake_amount. -r, --refresh-pairs-cached Refresh the pairs files in tests/testdata with the latest data from the exchange. Use it if you want to - run your edge with up-to-date data. + run your optimization commands with up-to-date data. --stoplosses STOPLOSS_RANGE Defines a range of stoploss against which edge will assess the strategy the format is "min,max,step" From 7c8e26c717fd61550d8ffe3077b2c4d3d0a7dcb4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Apr 2019 00:30:09 +0300 Subject: [PATCH 275/928] -j/--job-workers option added for controlling the number of joblib parallel worker processes used in hyperopt docs refreshed --- docs/bot-usage.md | 19 ++++++++++++++++--- freqtrade/arguments.py | 11 +++++++++++ freqtrade/configuration.py | 18 ++++-------------- freqtrade/optimize/hyperopt.py | 16 ++++++++++------ 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 55988985a..9261294e1 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -206,8 +206,11 @@ to find optimal parameter values for your stategy. ``` usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] - [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [--customhyperopt NAME] + [--eps] [--dmmp] [-e INT] + [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--print-all] [-j JOBS] optional arguments: -h, --help show this help message and exit @@ -215,6 +218,10 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE Specify what timerange of data to use. + --max_open_trades MAX_OPEN_TRADES + Specify max_open_trades to use. + --stake_amount STAKE_AMOUNT + Specify stake_amount. --customhyperopt NAME Specify hyperopt class name (default: DefaultHyperOpts). @@ -229,7 +236,13 @@ optional arguments: -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] Specify which parameters to hyperopt. Space separate list. Default: all. - + --print-all Print all results, not only the best ones. + -j JOBS, --job-workers JOBS + The number of concurrently running jobs for + hyperoptimization (hyperopt worker processes). If -1 + (default), all CPUs are used, for -2, all CPUs but one + are used, etc. If 1 is given, no parallel computing + code is used at all. ``` ## Edge commands diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 96f080bd2..3631e6615 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -315,6 +315,17 @@ class Arguments(object): dest='print_all', default=False ) + parser.add_argument( + '-j', '--job-workers', + help='The number of concurrently running jobs for hyperoptimization ' + '(hyperopt worker processes). ' + 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' + 'If 1 is given, no parallel computing code is used at all.', + dest='hyperopt_jobs', + default=-1, + type=int, + metavar='JOBS', + ) def _build_subcommands(self) -> None: """ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 65a8d644e..a13c24f6a 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -208,17 +208,14 @@ class Configuration(object): logger.info('Parameter -i/--ticker-interval detected ...') logger.info('Using ticker_interval: %s ...', config.get('ticker_interval')) - # If -l/--live is used we add it to the configuration if 'live' in self.args and self.args.live: config.update({'live': True}) logger.info('Parameter -l/--live detected ...') - # If --enable-position-stacking is used we add it to the configuration if 'position_stacking' in self.args and self.args.position_stacking: config.update({'position_stacking': True}) logger.info('Parameter --enable-position-stacking detected ...') - # If --disable-max-market-positions or --max_open_trades is used we update configuration if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: config.update({'use_max_market_positions': False}) logger.info('Parameter --disable-max-market-positions detected ...') @@ -230,25 +227,21 @@ class Configuration(object): else: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - # If --stake_amount is used we update configuration if 'stake_amount' in self.args and self.args.stake_amount: config.update({'stake_amount': self.args.stake_amount}) logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', config.get('stake_amount')) - # If --timerange is used we add it to the configuration if 'timerange' in self.args and self.args.timerange: config.update({'timerange': self.args.timerange}) logger.info('Parameter --timerange detected: %s ...', self.args.timerange) - # If --datadir is used we add it to the configuration if 'datadir' in self.args and self.args.datadir: config.update({'datadir': self._create_datadir(config, self.args.datadir)}) else: config.update({'datadir': self._create_datadir(config, None)}) logger.info('Using data folder: %s ...', config.get('datadir')) - # If -r/--refresh-pairs-cached is used we add it to the configuration if 'refresh_pairs' in self.args and self.args.refresh_pairs: config.update({'refresh_pairs': True}) logger.info('Parameter -r/--refresh-pairs-cached detected ...') @@ -261,12 +254,10 @@ class Configuration(object): config.update({'ticker_interval': self.args.ticker_interval}) logger.info('Overriding ticker interval with Command line argument') - # If --export is used we add it to the configuration if 'export' in self.args and self.args.export: config.update({'export': self.args.export}) logger.info('Parameter --export detected: %s ...', self.args.export) - # If --export-filename is used we add it to the configuration if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename: config.update({'exportfilename': self.args.exportfilename}) logger.info('Storing backtest results to %s ...', self.args.exportfilename) @@ -279,12 +270,10 @@ class Configuration(object): :return: configuration as dictionary """ - # If --timerange is used we add it to the configuration if 'timerange' in self.args and self.args.timerange: config.update({'timerange': self.args.timerange}) logger.info('Parameter --timerange detected: %s ...', self.args.timerange) - # If --timerange is used we add it to the configuration if 'stoploss_range' in self.args and self.args.stoploss_range: txt_range = eval(self.args.stoploss_range) config['edge'].update({'stoploss_range_min': txt_range[0]}) @@ -292,7 +281,6 @@ class Configuration(object): config['edge'].update({'stoploss_range_step': txt_range[2]}) logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) - # If -r/--refresh-pairs-cached is used we add it to the configuration if 'refresh_pairs' in self.args and self.args.refresh_pairs: config.update({'refresh_pairs': True}) logger.info('Parameter -r/--refresh-pairs-cached detected ...') @@ -309,13 +297,11 @@ class Configuration(object): # Add the hyperopt file to use config.update({'hyperopt': self.args.hyperopt}) - # If --epochs is used we add it to the configuration if 'epochs' in self.args and self.args.epochs: config.update({'epochs': self.args.epochs}) logger.info('Parameter --epochs detected ...') logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs')) - # If --spaces is used we add it to the configuration if 'spaces' in self.args and self.args.spaces: config.update({'spaces': self.args.spaces}) logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) @@ -324,6 +310,10 @@ class Configuration(object): config.update({'print_all': self.args.print_all}) logger.info('Parameter --print-all detected: %s', config.get('print_all')) + if 'hyperopt_jobs' in self.args and self.args.hyperopt_jobs: + config.update({'hyperopt_jobs': self.args.hyperopt_jobs}) + logger.info('Parameter -j/--job-workers detected: %s', config.get('hyperopt_jobs')) + return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b37027244..2bcfdd499 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -270,21 +270,25 @@ class Hyperopt(Backtesting): cpus = multiprocessing.cpu_count() logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') + config_jobs = self.config.get('hyperopt_jobs', -1) + logger.info(f'Number of parallel jobs set as: {config_jobs}') - opt = self.get_optimizer(cpus) - EVALS = max(self.total_tries // cpus, 1) + opt = self.get_optimizer(config_jobs) try: - with Parallel(n_jobs=cpus) as parallel: + with Parallel(n_jobs=config_jobs) as parallel: + jobs = parallel._effective_n_jobs() + logger.info(f'Effective number of parallel workers used: {jobs}') + EVALS = max(self.total_tries // jobs, 1) for i in range(EVALS): - asked = opt.ask(n_points=cpus) + asked = opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked) opt.tell(asked, [i['loss'] for i in f_val]) self.trials += f_val - for j in range(cpus): + for j in range(jobs): self.log_results({ 'loss': f_val[j]['loss'], - 'current_tries': i * cpus + j, + 'current_tries': i * jobs + j, 'total_tries': self.total_tries, 'result': f_val[j]['result'], }) From 3e3fce5f38555057b57a1f4afbb126c938b31526 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Apr 2019 09:25:19 +0300 Subject: [PATCH 276/928] print optimizer state in debug log messages --- freqtrade/optimize/hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b37027244..287e7831a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -288,6 +288,9 @@ class Hyperopt(Backtesting): 'total_tries': self.total_tries, 'result': f_val[j]['result'], }) + logger.debug(f"Optimizer params: {f_val[j]['params']}") + for j in range(cpus): + logger.debug(f"Opimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") except KeyboardInterrupt: print('User interrupted..') From a2a70bd6d01ee7921b062c561d078011df1822b5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 23 Apr 2019 12:41:12 +0000 Subject: [PATCH 277/928] Update ccxt from 1.18.485 to 1.18.486 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9a9b17fa0..6ffda79b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.485 +ccxt==1.18.486 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 48e2bd511488d76b99b71d4508cb7fbaad49d0d1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 23 Apr 2019 12:41:13 +0000 Subject: [PATCH 278/928] Update ccxt from 1.18.485 to 1.18.486 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 6ae30354e..16dbcce42 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.485 +ccxt==1.18.486 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 9a2eb46ceabb4806d46ee38feed684de395bdf6c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 23 Apr 2019 12:41:14 +0000 Subject: [PATCH 279/928] Update urllib3 from 1.24.2 to 1.25 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ffda79b8..ea3792583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.24.2 +urllib3==1.25 wrapt==1.11.1 numpy==1.16.3 pandas==0.24.2 From 8568459c740616df056d978f0c705728a77c8cb9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 23 Apr 2019 12:41:16 +0000 Subject: [PATCH 280/928] Update urllib3 from 1.24.2 to 1.25 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 16dbcce42..80935e9ef 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -4,7 +4,7 @@ python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.24.2 +urllib3==1.25 wrapt==1.11.1 scikit-learn==0.20.3 joblib==0.13.2 From a022b1a6c147b7a9ec47064496122ad5c9575419 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Apr 2019 21:18:52 +0300 Subject: [PATCH 281/928] --random-state for optimzer to get reproducible results added --- freqtrade/arguments.py | 18 ++++++++++++++++++ freqtrade/configuration.py | 4 ++++ freqtrade/optimize/hyperopt.py | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 96f080bd2..9fda677ed 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -315,6 +315,14 @@ class Arguments(object): dest='print_all', default=False ) + parser.add_argument( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + dest='hyperopt_random_state', + default=None, + type=Arguments.check_positive, + metavar='INT', + ) def _build_subcommands(self) -> None: """ @@ -385,6 +393,16 @@ class Arguments(object): return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) + @staticmethod + def check_positive(value) -> int: + try: + uint = int(value) + if uint <= 0: + raise ValueError + except: + raise argparse.ArgumentTypeError(f"{value} is invalid for this parameter, should be a positive integer value") + return uint + def scripts_options(self) -> None: """ Parses given arguments for scripts. diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 65a8d644e..b14e515fb 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -324,6 +324,10 @@ class Configuration(object): config.update({'print_all': self.args.print_all}) logger.info('Parameter --print-all detected: %s', config.get('print_all')) + if 'hyperopt_random_state' in self.args and self.args.hyperopt_random_state is not None: + config.update({'hyperopt_random_state': self.args.hyperopt_random_state}) + logger.info("Parameter --random-state detected: %s", config.get('hyperopt_random_state')) + return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b37027244..af012d6bf 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -235,7 +235,8 @@ class Hyperopt(Backtesting): base_estimator="ET", acq_optimizer="auto", n_initial_points=30, - acq_optimizer_kwargs={'n_jobs': cpu_count} + acq_optimizer_kwargs={'n_jobs': cpu_count}, + random_state=self.config.get('hyperopt_random_state', None) ) def run_optimizer_parallel(self, parallel, asked) -> List: From cc9f899cd619e599a12eb235c0feaa0ef56b2e62 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Apr 2019 21:25:36 +0300 Subject: [PATCH 282/928] removed explicit dependency on multiprocessing module --- freqtrade/optimize/hyperopt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2bcfdd499..312bdba98 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -5,7 +5,6 @@ This module contains the hyperopt logic """ import logging -import multiprocessing import os import sys from argparse import Namespace @@ -15,7 +14,7 @@ from pathlib import Path from pprint import pprint from typing import Any, Dict, List -from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects +from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension @@ -268,7 +267,7 @@ class Hyperopt(Backtesting): self.exchange = None # type: ignore self.load_previous_results() - cpus = multiprocessing.cpu_count() + cpus = cpu_count() logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') From 2f0ad0d28c2bf1b0ee075f2282cd0d23dab3d937 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 23 Apr 2019 22:03:41 +0300 Subject: [PATCH 283/928] test adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 7151935b2..08107265d 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -314,7 +314,7 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1)) + mocker.patch('freqtrade.optimize.hyperopt.joblib.cpu_count', MagicMock(return_value=1)) parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) From a429f83f5e7b54047f15f5eb3fef9766dcc00c3e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Apr 2019 22:16:24 +0300 Subject: [PATCH 284/928] flake happy; check_positive() renamed --- freqtrade/arguments.py | 10 ++++++---- freqtrade/configuration.py | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ce84d7425..8ae3922ca 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -311,7 +311,7 @@ class Arguments(object): help='Set random state to some positive integer for reproducible hyperopt results.', dest='hyperopt_random_state', default=None, - type=Arguments.check_positive, + type=Arguments.check_int_positive, metavar='INT', ) @@ -385,13 +385,15 @@ class Arguments(object): raise Exception('Incorrect syntax for timerange "%s"' % text) @staticmethod - def check_positive(value) -> int: + def check_int_positive(value) -> int: try: uint = int(value) if uint <= 0: raise ValueError - except: - raise argparse.ArgumentTypeError(f"{value} is invalid for this parameter, should be a positive integer value") + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a positive integer value" + ) return uint def scripts_options(self) -> None: diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 9994d405d..ca3b81279 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -315,7 +315,8 @@ class Configuration(object): if 'hyperopt_random_state' in self.args and self.args.hyperopt_random_state is not None: config.update({'hyperopt_random_state': self.args.hyperopt_random_state}) - logger.info("Parameter --random-state detected: %s", config.get('hyperopt_random_state')) + logger.info("Parameter --random-state detected: %s", + config.get('hyperopt_random_state')) return config From 6d2a1cfb442de9aa6ba11a008640df99b32553a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 09:30:59 +0200 Subject: [PATCH 285/928] remove full-config in tests and load full_config file --- freqtrade/tests/conftest.py | 65 --------------------------- freqtrade/tests/test_configuration.py | 9 ++++ 2 files changed, 9 insertions(+), 65 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index b19518ee7..0bff1d5e9 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -197,71 +197,6 @@ def default_conf(): return configuration -@pytest.fixture(scope="function") -def all_conf(): - """ Returns validated configuration with all options """ - configuration = { - "max_open_trades": 1, - "stake_currency": "BTC", - "stake_amount": 0.001, - "fiat_display_currency": "USD", - "ticker_interval": '5m', - "dry_run": True, - "minimal_roi": { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - }, - "stoploss": -0.10, - "unfilledtimeout": { - "buy": 10, - "sell": 30 - }, - "bid_strategy": { - "ask_last_balance": 0.0, - "use_order_book": False, - "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": 1 - }, - "exchange": { - "name": "bittrex", - "sandbox": False, - "enabled": True, - "key": "key", - "secret": "secret", - "password": "password", - "pair_whitelist": [ - "ETH/BTC", - "LTC/BTC", - "XRP/BTC", - "NEO/BTC" - ], - "pair_blacklist": [ - "DOGE/BTC", - "HOT/BTC", - ] - }, - "telegram": { - "enabled": True, - "token": "token", - "chat_id": "0" - }, - "initial_state": "running", - "db_url": "sqlite://", - "loglevel": logging.DEBUG, - } - return configuration - - @pytest.fixture def update(): _update = Update(0) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index dde9ea271..dce76f25e 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -18,6 +18,15 @@ from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has +@pytest.fixture(scope="function") +def all_conf(): + config_file = Path(__file__).parents[2] / "config_full.json.example" + print(config_file) + configuration = Configuration(Namespace()) + conf = configuration._load_config_file(str(config_file)) + return conf + + def test_load_config_invalid_pair(default_conf) -> None: default_conf['exchange']['pair_whitelist'].append('ETH-BTC') From 65a82d7ee62fc1891ab852a633a7bccbc76efe9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 09:31:13 +0200 Subject: [PATCH 286/928] Add some missing default parameters --- config_full.json.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config_full.json.example b/config_full.json.example index 58d3d3ac6..7fc088585 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -56,8 +56,10 @@ }, "exchange": { "name": "bittrex", + "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", + "password": "", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": false, From 6a0f527e0e8cdfee8db94a641ab69adb2965527c Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 24 Apr 2019 10:35:04 +0300 Subject: [PATCH 287/928] merge --job-workers and commit printing debug log messages with the opt state --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 58bcebf3f..de2ab8a8a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -299,7 +299,7 @@ class Hyperopt(Backtesting): 'result': f_val[j]['result'], }) logger.debug(f"Optimizer params: {f_val[j]['params']}") - for j in range(cpus): + for j in range(jobs): logger.debug(f"Opimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") except KeyboardInterrupt: print('User interrupted..') From 95ebd07735df241dda21465ba14d2e5f0da70b09 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 24 Apr 2019 10:38:50 +0300 Subject: [PATCH 288/928] an attempt to fix mocking --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 08107265d..aaaa5c27d 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -314,7 +314,7 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.joblib.cpu_count', MagicMock(return_value=1)) + mocker.patch('freqtrade.optimize.hyperopt.cpu_count', MagicMock(return_value=1)) parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) From a92d5f35699e7d4bf31a0cc1e7220916e1ff7849 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 09:48:25 +0200 Subject: [PATCH 289/928] Parametrize default-param tests --- freqtrade/tests/test_configuration.py | 72 +++++++-------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index dce76f25e..af94506b7 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -658,65 +658,27 @@ def test_load_config_default_exchange_name(all_conf) -> None: configuration._validate_config_schema(all_conf) -def test_load_config_default_exchange_sandbox(all_conf) -> None: +@pytest.mark.parametrize("keys", [("exchange", "sandbox", False), + ("exchange", "key", ""), + ("exchange", "secret", ""), + ("exchange", "password", ""), + ]) +def test_load_config_default_subkeys(all_conf, keys) -> None: """ - config['exchange']['sandbox'] option has default value: False - so it can be omitted in the config and the default value + Test for parameters with default values in sub-paths + so they can be omitted in the config and the default value should be present in the config as the option value """ - del all_conf['exchange']['sandbox'] + # Get first level key + key = keys[0] + # get second level key + subkey = keys[1] - assert 'sandbox' not in all_conf['exchange'] + del all_conf[key][subkey] + + assert subkey not in all_conf[key] configuration = Configuration(Namespace()) configuration._validate_config_schema(all_conf) - assert 'sandbox' in all_conf['exchange'] - assert all_conf['exchange']['sandbox'] is False - - -def test_load_config_default_exchange_key(all_conf) -> None: - """ - config['exchange']['key'] option has default value: '' - so it can be omitted in the config and the default value - should be present in the config as the option value - """ - del all_conf['exchange']['key'] - - assert 'key' not in all_conf['exchange'] - - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) - assert 'key' in all_conf['exchange'] - assert all_conf['exchange']['key'] == '' - - -def test_load_config_default_exchange_secret(all_conf) -> None: - """ - config['exchange']['secret'] option has default value: '' - so it can be omitted in the config and the default value - should be present in the config as the option value - """ - del all_conf['exchange']['secret'] - - assert 'secret' not in all_conf['exchange'] - - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) - assert 'secret' in all_conf['exchange'] - assert all_conf['exchange']['secret'] == '' - - -def test_load_config_default_exchange_password(all_conf) -> None: - """ - config['exchange']['password'] option has default value: '' - so it can be omitted in the config and the default value - should be present in the config as the option value - """ - del all_conf['exchange']['password'] - - assert 'password' not in all_conf['exchange'] - - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) - assert 'password' in all_conf['exchange'] - assert all_conf['exchange']['password'] == '' + assert subkey in all_conf[key] + assert all_conf[key][subkey] == keys[2] From ad692c185ed35f981861eb02d9dda521481e4610 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 09:55:53 +0200 Subject: [PATCH 290/928] Improve comment --- freqtrade/tests/test_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 6fcae6aaa..09041dd3d 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -658,7 +658,7 @@ def test_load_config_default_subkeys(all_conf, keys) -> None: """ Test for parameters with default values in sub-paths so they can be omitted in the config and the default value - should be present in the config as the option value + should is added to the config. """ # Get first level key key = keys[0] From a8e787fda887dac389a4bd290a57b60784c05cea Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 24 Apr 2019 11:25:15 +0300 Subject: [PATCH 291/928] test adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index aaaa5c27d..063d0e791 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -314,7 +314,6 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.cpu_count', MagicMock(return_value=1)) parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) @@ -325,6 +324,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: default_conf.update({'epochs': 1}) default_conf.update({'timerange': None}) default_conf.update({'spaces': 'all'}) + default_conf.update({'hyperopt_jobs': 1}) hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() From eb89b65b59c411c25b678101ec02ff06d45d9c6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 14:04:12 +0200 Subject: [PATCH 292/928] Downgrade urllib3, cleanup requirements files every requirement should be there only once --- requirements-pi.txt | 7 ++++++- requirements.txt | 29 +++-------------------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 80935e9ef..8e55be5c3 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,10 +1,12 @@ +# requirements without requirements installable via conda +# mainly used for Raspberry pi installs ccxt==1.18.486 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.25 +urllib3==1.24.2 wrapt==1.11.1 scikit-learn==0.20.3 joblib==0.13.2 @@ -21,3 +23,6 @@ py_find_1st==1.1.3 #Load ticker files 30% faster python-rapidjson==0.7.0 + +# Notify systemd +sdnotify==0.3.2 diff --git a/requirements.txt b/requirements.txt index ea3792583..4c2376078 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,6 @@ -ccxt==1.18.486 -SQLAlchemy==1.3.3 -python-telegram-bot==11.1.0 -arrow==0.13.1 -cachetools==3.1.0 -requests==2.21.0 -urllib3==1.25 -wrapt==1.11.1 +# Load common requirements +-r requirements-pi.txt + numpy==1.16.3 pandas==0.24.2 -scikit-learn==0.20.3 -joblib==0.13.2 scipy==1.2.1 -jsonschema==3.0.1 -TA-Lib==0.4.17 -tabulate==0.8.3 -coinmarketcap==5.0.3 - -# Required for hyperopt -scikit-optimize==0.5.2 - -# find first, C search in arrays -py_find_1st==1.1.3 - -# Load ticker files 30% faster -python-rapidjson==0.7.0 - -# Notify systemd -sdnotify==0.3.2 From 30888cf5ca17fc05414019079b55dc4072ae80f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 14:07:55 +0200 Subject: [PATCH 293/928] have pyup ignore outdated dependency --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 8e55be5c3..f420b7201 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -6,7 +6,7 @@ python-telegram-bot==11.1.0 arrow==0.13.1 cachetools==3.1.0 requests==2.21.0 -urllib3==1.24.2 +urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 scikit-learn==0.20.3 joblib==0.13.2 From 060571290a8c46d02b87f4608484d15bdeaa60ee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 24 Apr 2019 12:41:08 +0000 Subject: [PATCH 294/928] Update ccxt from 1.18.486 to 1.18.489 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea3792583..7dc953f9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.486 +ccxt==1.18.489 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 59f905a5733c4f6c4764ede4a95868e33602d7d0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 24 Apr 2019 12:41:09 +0000 Subject: [PATCH 295/928] Update ccxt from 1.18.486 to 1.18.489 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 80935e9ef..df06b5834 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,4 +1,4 @@ -ccxt==1.18.486 +ccxt==1.18.489 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 17cf9d33cfa848a20c4b9ff4d0fdf116f4c5603f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 20:44:36 +0200 Subject: [PATCH 296/928] add _args_to_conig --- freqtrade/configuration.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 6388a97ac..172010dd8 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -216,6 +216,14 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir + def _args_to_config(self, config, argname, configname, logstring) -> bool: + + if argname in self.args and getattr(self.args, argname): + + config.update({configname: getattr(self.args, argname)}) + + logger.info(logstring.format(config[configname])) + def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901 """ Extract information for sys.argv and load Backtesting configuration @@ -232,9 +240,8 @@ class Configuration(object): config.update({'live': True}) logger.info('Parameter -l/--live detected ...') - if 'position_stacking' in self.args and self.args.position_stacking: - config.update({'position_stacking': True}) - logger.info('Parameter --enable-position-stacking detected ...') + self._args_to_config(config, 'position_stacking', 'position_stacking', + 'Parameter --enable-position-stacking detected ...') if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: config.update({'use_max_market_positions': False}) From 39f60c474002be0827987c3c6ba74d391ecacef6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:02:05 +0200 Subject: [PATCH 297/928] Add some more arguments to args_to_config --- freqtrade/configuration.py | 60 ++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 172010dd8..395653213 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -216,13 +216,19 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir - def _args_to_config(self, config, argname, configname, logstring) -> bool: - + def _args_to_config(self, config, argname, configname, logstring, logfun=None) -> bool: + """ + logfun is applied to the configuration entry before passing + that entry to the log string using .format(). + sample: logfun=len (prints the length of the found configuration instead of the content) + """ if argname in self.args and getattr(self.args, argname): config.update({configname: getattr(self.args, argname)}) - - logger.info(logstring.format(config[configname])) + if logfun: + logger.info(logstring.format(logfun(config[configname]))) + else: + logger.info(logstring.format(config[configname])) def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901 """ @@ -236,12 +242,11 @@ class Configuration(object): logger.info('Parameter -i/--ticker-interval detected ...') logger.info('Using ticker_interval: %s ...', config.get('ticker_interval')) - if 'live' in self.args and self.args.live: - config.update({'live': True}) - logger.info('Parameter -l/--live detected ...') + self._args_to_config(config, argname='live', configname='live', + logstring='Parameter -l/--live detected ...') - self._args_to_config(config, 'position_stacking', 'position_stacking', - 'Parameter --enable-position-stacking detected ...') + self._args_to_config(config, argname='position_stacking', configname='position_stacking', + logstring='Parameter --enable-position-stacking detected ...') if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: config.update({'use_max_market_positions': False}) @@ -254,14 +259,12 @@ class Configuration(object): else: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - if 'stake_amount' in self.args and self.args.stake_amount: - config.update({'stake_amount': self.args.stake_amount}) - logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', - config.get('stake_amount')) + self._args_to_config(config, argname='stake_amount', configname='stake_amount', + logstring='Parameter --stake_amount detected, ' + 'overriding stake_amount to: {} ...') - if 'timerange' in self.args and self.args.timerange: - config.update({'timerange': self.args.timerange}) - logger.info('Parameter --timerange detected: %s ...', self.args.timerange) + self._args_to_config(config, argname='timerange', configname='timerange', + logstring='Parameter --timerange detected: {} ...') if 'datadir' in self.args and self.args.datadir: config.update({'datadir': self._create_datadir(config, self.args.datadir)}) @@ -269,25 +272,20 @@ class Configuration(object): config.update({'datadir': self._create_datadir(config, None)}) logger.info('Using data folder: %s ...', config.get('datadir')) - if 'refresh_pairs' in self.args and self.args.refresh_pairs: - config.update({'refresh_pairs': True}) - logger.info('Parameter -r/--refresh-pairs-cached detected ...') + self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + logstring='Parameter -r/--refresh-pairs-cached detected ...') - if 'strategy_list' in self.args and self.args.strategy_list: - config.update({'strategy_list': self.args.strategy_list}) - logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list)) + self._args_to_config(config, argname='strategy_list', configname='strategy_list', + logstring='Using strategy list of {} Strategies', logfun=len) - if 'ticker_interval' in self.args and self.args.ticker_interval: - config.update({'ticker_interval': self.args.ticker_interval}) - logger.info('Overriding ticker interval with Command line argument') + self._args_to_config(config, argname='ticker_interval', configname='ticker_interval', + logstring='Overriding ticker interval with Command line argument') - if 'export' in self.args and self.args.export: - config.update({'export': self.args.export}) - logger.info('Parameter --export detected: %s ...', self.args.export) + self._args_to_config(config, argname='export', configname='export', + logstring='Parameter --export detected: {} ...') - if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename: - config.update({'exportfilename': self.args.exportfilename}) - logger.info('Storing backtest results to %s ...', self.args.exportfilename) + self._args_to_config(config, argname='exportfilename', configname='exportfilename', + logstring='Storing backtest results to {} ...') return config From d6276a15d2087d1bea30a1a796d8bd30a4b05724 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:12:08 +0200 Subject: [PATCH 298/928] Convert all optimize to args_to_config --- freqtrade/configuration.py | 54 ++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 395653213..2b9b02630 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -216,7 +216,7 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir - def _args_to_config(self, config, argname, configname, logstring, logfun=None) -> bool: + def _args_to_config(self, config, argname, configname, logstring, logfun=None) -> None: """ logfun is applied to the configuration entry before passing that entry to the log string using .format(). @@ -230,7 +230,7 @@ class Configuration(object): else: logger.info(logstring.format(config[configname])) - def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901 + def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ Extract information for sys.argv and load Backtesting configuration :return: configuration as dictionary @@ -295,9 +295,8 @@ class Configuration(object): :return: configuration as dictionary """ - if 'timerange' in self.args and self.args.timerange: - config.update({'timerange': self.args.timerange}) - logger.info('Parameter --timerange detected: %s ...', self.args.timerange) + self._args_to_config(config, argname='timerange', configname='timerange', + logstring='Parameter --timerange detected: {} ...') if 'stoploss_range' in self.args and self.args.stoploss_range: txt_range = eval(self.args.stoploss_range) @@ -306,9 +305,8 @@ class Configuration(object): config['edge'].update({'stoploss_range_step': txt_range[2]}) logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) - if 'refresh_pairs' in self.args and self.args.refresh_pairs: - config.update({'refresh_pairs': True}) - logger.info('Parameter -r/--refresh-pairs-cached detected ...') + self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + logstring='Parameter -r/--refresh-pairs-cached detected ...') return config @@ -318,35 +316,29 @@ class Configuration(object): :return: configuration as dictionary """ - if "hyperopt" in self.args: - # Add the hyperopt file to use - config.update({'hyperopt': self.args.hyperopt}) + self._args_to_config(config, argname='hyperopt', configname='hyperopt', + logstring='Using Hyperopt file {}') - if 'epochs' in self.args and self.args.epochs: - config.update({'epochs': self.args.epochs}) - logger.info('Parameter --epochs detected ...') - logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs')) + self._args_to_config(config, argname='epochs', configname='epochs', + logstring='Parameter --epochs detected ... ' + 'Will run Hyperopt with for {} epochs ...' + ) - if 'spaces' in self.args and self.args.spaces: - config.update({'spaces': self.args.spaces}) - logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) + self._args_to_config(config, argname='spaces', configname='spaces', + logstring='Parameter -s/--spaces detected: {}') - if 'print_all' in self.args and self.args.print_all: - config.update({'print_all': self.args.print_all}) - logger.info('Parameter --print-all detected: %s', config.get('print_all')) + self._args_to_config(config, argname='print_all', configname='print_all', + logstring='Parameter --print-all detected ...') - if 'hyperopt_jobs' in self.args and self.args.hyperopt_jobs: - config.update({'hyperopt_jobs': self.args.hyperopt_jobs}) - logger.info('Parameter -j/--job-workers detected: %s', config.get('hyperopt_jobs')) + self._args_to_config(config, argname='hyperopt_jobs', configname='hyperopt_jobs', + logstring='Parameter -j/--job-workers detected: {}') - if 'refresh_pairs' in self.args and self.args.refresh_pairs: - config.update({'refresh_pairs': True}) - logger.info('Parameter -r/--refresh-pairs-cached detected ...') + self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + logstring='Parameter -r/--refresh-pairs-cached detected ...') - if 'hyperopt_random_state' in self.args and self.args.hyperopt_random_state is not None: - config.update({'hyperopt_random_state': self.args.hyperopt_random_state}) - logger.info("Parameter --random-state detected: %s", - config.get('hyperopt_random_state')) + self._args_to_config(config, argname='hyperopt_random_state', + configname='hyperopt_random_state', + logstring='Parameter --random-state detected: {}') return config From a0413b5d910e22fbb1a168cb7949c7a6a176b27d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:12:23 +0200 Subject: [PATCH 299/928] Only log one message per call --- freqtrade/tests/optimize/test_hyperopt.py | 5 +++-- freqtrade/tests/test_configuration.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 063d0e791..d45f806e5 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -137,7 +137,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo ) assert 'epochs' in config - assert log_has('Parameter --epochs detected ...', caplog.record_tuples) + assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...', + caplog.record_tuples) assert 'spaces' in config assert log_has( @@ -145,7 +146,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo caplog.record_tuples ) assert 'print_all' in config - assert log_has('Parameter --print-all detected: True', caplog.record_tuples) + assert log_has('Parameter --print-all detected ...', caplog.record_tuples) def test_hyperoptresolver(mocker, default_conf, caplog) -> None: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 09041dd3d..663b4fbad 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -463,8 +463,8 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: assert 'epochs' in config assert int(config['epochs']) == 10 - assert log_has('Parameter --epochs detected ...', caplog.record_tuples) - assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples) + assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...', + caplog.record_tuples) assert 'spaces' in config assert config['spaces'] == ['all'] From ca3b8ef2e7f1c9d745187bcbc59c4ae6dfc343b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:13:57 +0200 Subject: [PATCH 300/928] Remove duplicate argument --- freqtrade/configuration.py | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 2b9b02630..5d4b1b9af 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -216,7 +216,7 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir - def _args_to_config(self, config, argname, configname, logstring, logfun=None) -> None: + def _args_to_config(self, config, argname, logstring, logfun=None) -> None: """ logfun is applied to the configuration entry before passing that entry to the log string using .format(). @@ -224,11 +224,11 @@ class Configuration(object): """ if argname in self.args and getattr(self.args, argname): - config.update({configname: getattr(self.args, argname)}) + config.update({argname: getattr(self.args, argname)}) if logfun: - logger.info(logstring.format(logfun(config[configname]))) + logger.info(logstring.format(logfun(config[argname]))) else: - logger.info(logstring.format(config[configname])) + logger.info(logstring.format(config[argname])) def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ @@ -242,10 +242,10 @@ class Configuration(object): logger.info('Parameter -i/--ticker-interval detected ...') logger.info('Using ticker_interval: %s ...', config.get('ticker_interval')) - self._args_to_config(config, argname='live', configname='live', + self._args_to_config(config, argname='live', logstring='Parameter -l/--live detected ...') - self._args_to_config(config, argname='position_stacking', configname='position_stacking', + self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: @@ -259,11 +259,11 @@ class Configuration(object): else: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - self._args_to_config(config, argname='stake_amount', configname='stake_amount', + self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake_amount detected, ' 'overriding stake_amount to: {} ...') - self._args_to_config(config, argname='timerange', configname='timerange', + self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') if 'datadir' in self.args and self.args.datadir: @@ -272,19 +272,19 @@ class Configuration(object): config.update({'datadir': self._create_datadir(config, None)}) logger.info('Using data folder: %s ...', config.get('datadir')) - self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') - self._args_to_config(config, argname='strategy_list', configname='strategy_list', + self._args_to_config(config, argname='strategy_list', logstring='Using strategy list of {} Strategies', logfun=len) - self._args_to_config(config, argname='ticker_interval', configname='ticker_interval', + self._args_to_config(config, argname='ticker_interval', logstring='Overriding ticker interval with Command line argument') - self._args_to_config(config, argname='export', configname='export', + self._args_to_config(config, argname='export', logstring='Parameter --export detected: {} ...') - self._args_to_config(config, argname='exportfilename', configname='exportfilename', + self._args_to_config(config, argname='exportfilename', logstring='Storing backtest results to {} ...') return config @@ -295,7 +295,7 @@ class Configuration(object): :return: configuration as dictionary """ - self._args_to_config(config, argname='timerange', configname='timerange', + self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') if 'stoploss_range' in self.args and self.args.stoploss_range: @@ -305,7 +305,7 @@ class Configuration(object): config['edge'].update({'stoploss_range_step': txt_range[2]}) logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) - self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') return config @@ -316,28 +316,27 @@ class Configuration(object): :return: configuration as dictionary """ - self._args_to_config(config, argname='hyperopt', configname='hyperopt', + self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt file {}') - self._args_to_config(config, argname='epochs', configname='epochs', + self._args_to_config(config, argname='epochs', logstring='Parameter --epochs detected ... ' 'Will run Hyperopt with for {} epochs ...' ) - self._args_to_config(config, argname='spaces', configname='spaces', + self._args_to_config(config, argname='spaces', logstring='Parameter -s/--spaces detected: {}') - self._args_to_config(config, argname='print_all', configname='print_all', + self._args_to_config(config, argname='print_all', logstring='Parameter --print-all detected ...') - self._args_to_config(config, argname='hyperopt_jobs', configname='hyperopt_jobs', + self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') - self._args_to_config(config, argname='refresh_pairs', configname='refresh_pairs', + self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') self._args_to_config(config, argname='hyperopt_random_state', - configname='hyperopt_random_state', logstring='Parameter --random-state detected: {}') return config From 87329c689d3345354dd49d6676edb8503eac9998 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:24:00 +0200 Subject: [PATCH 301/928] Change ticker_interval too --- freqtrade/configuration.py | 7 +++---- freqtrade/tests/optimize/test_backtesting.py | 17 ++++++----------- freqtrade/tests/optimize/test_edge_cli.py | 18 ++++++++---------- freqtrade/tests/optimize/test_hyperopt.py | 11 ++++------- freqtrade/tests/test_configuration.py | 14 ++++---------- 5 files changed, 25 insertions(+), 42 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 5d4b1b9af..eea0bf5ae 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -237,10 +237,9 @@ class Configuration(object): """ # This will override the strategy configuration - if 'ticker_interval' in self.args and self.args.ticker_interval: - config.update({'ticker_interval': self.args.ticker_interval}) - logger.info('Parameter -i/--ticker-interval detected ...') - logger.info('Using ticker_interval: %s ...', config.get('ticker_interval')) + self._args_to_config(config, argname='ticker_interval', + logstring='Parameter -i/--ticker-interval detected ... ' + 'Using ticker_interval: {} ...') self._args_to_config(config, argname='live', logstring='Parameter -l/--live detected ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index af0b07d6f..281ce2bfe 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -23,7 +23,7 @@ from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange def get_args(args) -> List[str]: @@ -190,7 +190,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> caplog.record_tuples ) assert 'ticker_interval' in config - assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert 'live' not in config assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -242,11 +242,8 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'live' in config assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -853,8 +850,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): start(args) # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ...', - 'Using ticker_interval: 1m ...', + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', @@ -912,8 +908,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ...', - 'Using ticker_interval: 1m ...', + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index a58620139..488d552c8 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -1,14 +1,15 @@ # pragma pylint: disable=missing-docstring, C0103, C0330 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -from unittest.mock import MagicMock import json from typing import List -from freqtrade.edge import PairInfo +from unittest.mock import MagicMock + from freqtrade.arguments import Arguments -from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start) +from freqtrade.edge import PairInfo +from freqtrade.optimize.edge_cli import EdgeCli, setup_configuration, start from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange def get_args(args) -> List[str]: @@ -40,7 +41,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> caplog.record_tuples ) assert 'ticker_interval' in config - assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert 'refresh_pairs' not in config assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -79,11 +80,8 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'refresh_pairs' in config assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index d45f806e5..aa3381e95 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -14,7 +14,7 @@ from freqtrade.optimize.hyperopt import Hyperopt, start, setup_configuration from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.resolvers import StrategyResolver, HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args @@ -64,7 +64,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca caplog.record_tuples ) assert 'ticker_interval' in config - assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert 'live' not in config assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -114,11 +114,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'position_stacking' in config assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 663b4fbad..aee0dfadd 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -360,11 +360,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'live' in config assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -425,11 +422,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'strategy_list' in config assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples) From 86313b337a6edb075066ea120d63a93a7219ce39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:27:32 +0200 Subject: [PATCH 302/928] Combine optimize configurations, eliminate duplicates --- freqtrade/configuration.py | 42 ++++++-------------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index eea0bf5ae..062c877ec 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -95,14 +95,8 @@ class Configuration(object): # Load Common configuration config = self._load_common_config(config) - # Load Backtesting - config = self._load_backtesting_config(config) - - # Load Edge - config = self._load_edge_config(config) - - # Load Hyperopt - config = self._load_hyperopt_config(config) + # Load Optimize configurations + config = self._load_optimize_config(config) # Set runmode if not self.runmode: @@ -230,9 +224,9 @@ class Configuration(object): else: logger.info(logstring.format(config[argname])) - def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ - Extract information for sys.argv and load Backtesting configuration + Extract information for sys.argv and load Optimize configuration :return: configuration as dictionary """ @@ -286,17 +280,7 @@ class Configuration(object): self._args_to_config(config, argname='exportfilename', logstring='Storing backtest results to {} ...') - return config - - def _load_edge_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """ - Extract information for sys.argv and load Edge configuration - :return: configuration as dictionary - """ - - self._args_to_config(config, argname='timerange', - logstring='Parameter --timerange detected: {} ...') - + # Edge section: if 'stoploss_range' in self.args and self.args.stoploss_range: txt_range = eval(self.args.stoploss_range) config['edge'].update({'stoploss_range_min': txt_range[0]}) @@ -304,17 +288,7 @@ class Configuration(object): config['edge'].update({'stoploss_range_step': txt_range[2]}) logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) - self._args_to_config(config, argname='refresh_pairs', - logstring='Parameter -r/--refresh-pairs-cached detected ...') - - return config - - def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """ - Extract information for sys.argv and load Hyperopt configuration - :return: configuration as dictionary - """ - + # Hyperopt section self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt file {}') @@ -332,12 +306,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_jobs', logstring='Parameter -j/--job-workers detected: {}') - self._args_to_config(config, argname='refresh_pairs', - logstring='Parameter -r/--refresh-pairs-cached detected ...') - self._args_to_config(config, argname='hyperopt_random_state', logstring='Parameter --random-state detected: {}') - return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: From b4630c403d2aa88f1be79b80ec9510006a18e388 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:32:33 +0200 Subject: [PATCH 303/928] Add typehints --- freqtrade/configuration.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 062c877ec..0eb773eb5 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -7,7 +7,7 @@ import os import sys from argparse import Namespace from logging.handlers import RotatingFileHandler -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match @@ -17,7 +17,6 @@ from freqtrade.exchange import is_exchange_supported, supported_exchanges from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode - logger = logging.getLogger(__name__) @@ -210,11 +209,16 @@ class Configuration(object): logger.info(f'Created data directory: {datadir}') return datadir - def _args_to_config(self, config, argname, logstring, logfun=None) -> None: + def _args_to_config(self, config: Dict[str, Any], argname: str, + logstring: str, logfun=Optional[Callable]) -> None: """ - logfun is applied to the configuration entry before passing - that entry to the log string using .format(). - sample: logfun=len (prints the length of the found configuration instead of the content) + :param config: Configuration dictionary + :param argname: Argumentname in self.args - will be copied to config dict. + :param logstring: Logging String + :param logfun: logfun is applied to the configuration entry before passing + that entry to the log string using .format(). + sample: logfun=len (prints the length of the found + configuration instead of the content) """ if argname in self.args and getattr(self.args, argname): From 65dcb6acea016a1248480e8eb262b89e1579695d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 21:56:24 +0200 Subject: [PATCH 304/928] Catch errors on reload_markets --- freqtrade/exchange/exchange.py | 7 +++++-- freqtrade/tests/exchange/test_exchange.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 94673fdc3..475baad67 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -222,8 +222,11 @@ class Exchange(object): > arrow.utcnow().timestamp): return None logger.debug("Performing scheduled market reload..") - self._api.load_markets(reload=True) - self._last_markets_refresh = arrow.utcnow().timestamp + try: + self._api.load_markets(reload=True) + self._last_markets_refresh = arrow.utcnow().timestamp + except ccxt.NetworkError: + logger.exception("Could not reload markets.") def validate_pairs(self, pairs: List[str]) -> None: """ diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 9e471d551..924ed538f 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -301,6 +301,20 @@ def test__reload_markets(default_conf, mocker, caplog): assert log_has('Performing scheduled market reload..', caplog.record_tuples) +def test__reload_markets_exception(default_conf, mocker, caplog): + caplog.set_level(logging.DEBUG) + + api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError) + default_conf['exchange']['markets_refresh_interval'] = 10 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + + # less than 10 minutes have passed, no reload + exchange._reload_markets() + assert exchange._last_markets_refresh == 0 + assert log_has_re(r"Could not reload markets.*", caplog.record_tuples) + + def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ From 22eb6cb5fa181e7fd1b2d64cdd53ccfc23a41325 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 22:08:56 +0200 Subject: [PATCH 305/928] Fix typo in args_to_config --- freqtrade/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 0eb773eb5..54862ff70 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -210,7 +210,7 @@ class Configuration(object): return datadir def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun=Optional[Callable]) -> None: + logstring: str, logfun: Optional[Callable] = None) -> None: """ :param config: Configuration dictionary :param argname: Argumentname in self.args - will be copied to config dict. From 45ecbc91e8c0d47398e2c20463f84befd64bb2b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Apr 2019 22:20:05 +0200 Subject: [PATCH 306/928] Use BaseError, not NetworkError in exception handler --- 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 475baad67..66857a7a5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -225,7 +225,7 @@ class Exchange(object): try: self._api.load_markets(reload=True) self._last_markets_refresh = arrow.utcnow().timestamp - except ccxt.NetworkError: + except ccxt.BaseError: logger.exception("Could not reload markets.") def validate_pairs(self, pairs: List[str]) -> None: From ea44bbff9fcd7cfab7d4907bc1d172b37c7eab25 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Apr 2019 11:11:04 +0300 Subject: [PATCH 307/928] prevent hyperopt from running simultaneously --- freqtrade/optimize/hyperopt.py | 33 +++++++++++++++++++++++++++------ requirements-pi.txt | 1 + 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index de2ab8a8a..52821d93b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -14,6 +14,7 @@ from pathlib import Path from pprint import pprint from typing import Any, Dict, List +from filelock import Timeout, FileLock from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer @@ -28,10 +29,14 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.resolvers import HyperOptResolver + logger = logging.getLogger(__name__) + MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl') +TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle') +HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock') class Hyperopt(Backtesting): @@ -63,7 +68,7 @@ class Hyperopt(Backtesting): self.expected_max_profit = 3.0 # Previous evaluations - self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle') + self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] def get_args(self, params): @@ -343,9 +348,25 @@ def start(args: Namespace) -> None: logger.info('Starting freqtrade in Hyperopt mode') - # Remove noisy log messages - logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + lock = FileLock(HYPEROPT_LOCKFILE) - # Initialize backtesting object - hyperopt = Hyperopt(config) - hyperopt.start() + try: + with lock.acquire(timeout=1): + + # Remove noisy log messages + logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + logging.getLogger('filelock').setLevel(logging.WARNING) + + # Initialize backtesting object + hyperopt = Hyperopt(config) + hyperopt.start() + + except Timeout: + logger.info("Another running instance of freqtrade Hyperopt detected.") + logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " + "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " + "or on separate machines.") + logger.info("Quitting now.") + # TODO: return False here in order to help freqtrade to exit + # with non-zero exit code... + # Same in Edge and Backtesting start() functions. diff --git a/requirements-pi.txt b/requirements-pi.txt index 672ab9767..05de03e5f 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -17,6 +17,7 @@ coinmarketcap==5.0.3 # Required for hyperopt scikit-optimize==0.5.2 +filelock==3.0.10 # find first, C search in arrays py_find_1st==1.1.3 From eaf5547b8802aeabde208f12ed62b2b002a1ac18 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 26 Apr 2019 12:42:07 +0000 Subject: [PATCH 308/928] Update ccxt from 1.18.489 to 1.18.491 --- requirements-pi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-pi.txt b/requirements-pi.txt index 672ab9767..c121d8cc9 100644 --- a/requirements-pi.txt +++ b/requirements-pi.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.489 +ccxt==1.18.491 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From bf2a39b76da9d0c0ff99e7efdc79257f79d6ffa6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 19:50:18 +0200 Subject: [PATCH 309/928] Fix add requirements-pi.txt in dockerfile earlier Avoids docker-build failure --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e36766530..202989e43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt /freqtrade/ +COPY requirements.txt requirements-pi.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements.txt --no-cache-dir From 99b08fbd13efef4645045556f2e33e143cada380 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 19:51:24 +0200 Subject: [PATCH 310/928] Remove unused Hyperopt test lines --- freqtrade/tests/optimize/test_hyperopt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 063d0e791..a777f93a8 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -from datetime import datetime import json import os +from datetime import datetime from unittest.mock import MagicMock import pandas as pd @@ -10,9 +10,10 @@ import pytest from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize.hyperopt import Hyperopt, start, setup_configuration from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.resolvers import StrategyResolver, HyperOptResolver +from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, Hyperopt, + setup_configuration, start) +from freqtrade.resolvers import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args @@ -185,7 +186,6 @@ def test_start(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - StrategyResolver({'strategy': 'DefaultStrategy'}) start(args) import pprint @@ -214,7 +214,6 @@ def test_start_failure(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - StrategyResolver({'strategy': 'DefaultStrategy'}) with pytest.raises(DependencyException): start(args) assert log_has( @@ -224,7 +223,6 @@ def test_start_failure(mocker, default_conf, caplog) -> None: def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: - StrategyResolver({'strategy': 'DefaultStrategy'}) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) From dc12cacd50114394950d195d0ab33b6785c6c507 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 19:57:09 +0200 Subject: [PATCH 311/928] Rename requirements-pi to requirements.common --- .pyup.yml | 2 +- Dockerfile | 2 +- Dockerfile.pi | 4 ++-- docs/installation.md | 2 +- requirements-pi.txt => requirements-common.txt | 0 requirements.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename requirements-pi.txt => requirements-common.txt (100%) diff --git a/.pyup.yml b/.pyup.yml index 462ae5783..3494a3fd3 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -22,7 +22,7 @@ requirements: - requirements.txt - requirements-dev.txt - requirements-plot.txt - - requirements-pi.txt + - requirements-common.txt # configure the branch prefix the bot is using diff --git a/Dockerfile b/Dockerfile index 202989e43..7a0298719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-pi.txt /freqtrade/ +COPY requirements.txt requirements-common.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements.txt --no-cache-dir diff --git a/Dockerfile.pi b/Dockerfile.pi index 5184e2d37..1b9c4c579 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -27,9 +27,9 @@ RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryco && rm Berryconda3-2.0.0-Linux-armv7l.sh # Install dependencies -COPY requirements-pi.txt /freqtrade/ +COPY requirements-common.txt /freqtrade/ RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ - && ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir + && ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir # Install and execute COPY . /freqtrade/ diff --git a/docs/installation.md b/docs/installation.md index 23a6cbd23..7060d7b39 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -326,7 +326,7 @@ conda activate freqtrade conda install scipy pandas numpy sudo apt install libffi-dev -python3 -m pip install -r requirements-pi.txt +python3 -m pip install -r requirements-common.txt python3 -m pip install -e . ``` diff --git a/requirements-pi.txt b/requirements-common.txt similarity index 100% rename from requirements-pi.txt rename to requirements-common.txt diff --git a/requirements.txt b/requirements.txt index 4c2376078..78585f8f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements --r requirements-pi.txt +-r requirements-common.txt numpy==1.16.3 pandas==0.24.2 From 40c02073776418614e8c223bbdaf2456beccebb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 19:59:05 +0200 Subject: [PATCH 312/928] revert erroneous refactor --- freqtrade/tests/optimize/test_hyperopt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a777f93a8..21db63636 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -11,8 +11,7 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, Hyperopt, - setup_configuration, start) +from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start from freqtrade.resolvers import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, patch_exchange From 21b31f11b8d3e87eceab3f9973316711ffc81c64 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 27 Apr 2019 12:42:05 +0000 Subject: [PATCH 313/928] Update ccxt from 1.18.491 to 1.18.492 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index c121d8cc9..51bc88480 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.491 +ccxt==1.18.492 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 59bd081e92cd4fdfef54e119a9a823205e1554c6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Apr 2019 12:42:12 +0000 Subject: [PATCH 314/928] Update ccxt from 1.18.492 to 1.18.493 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 51bc88480..71680cc7b 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.492 +ccxt==1.18.493 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From f71eda1c2f90b26ccbbbb5b6cd1cae71a5055ff6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Apr 2019 06:23:14 +0200 Subject: [PATCH 315/928] Have forcesell return a result --- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/rpc/telegram.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aac419fe1..007b7686d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -346,7 +346,7 @@ class RPC(object): return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} - def _rpc_forcesell(self, trade_id) -> None: + def _rpc_forcesell(self, trade_id) -> Dict[str, str]: """ Handler for forcesell . Sells the given trade at current price @@ -386,7 +386,7 @@ class RPC(object): for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.session.flush() - return + return {'result': 'Created sell orders for all open trades.'} # Query for trade trade = Trade.query.filter( @@ -401,6 +401,7 @@ class RPC(object): _exec_forcesell(trade) Trade.session.flush() + return {'result': f'Created sell orders for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c61193d29..dc0bad2b2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -413,7 +413,9 @@ class Telegram(RPC): trade_id = update.message.text.replace('/forcesell', '').strip() try: - self._rpc_forcesell(trade_id) + msg = self._rpc_forcesell(trade_id) + self._send_msg('Forcesell Result: `{result}`'.format(**msg), bot=bot) + except RPCException as e: self._send_msg(str(e), bot=bot) From 91642b2bd9608b5edee8fe9253278163ac1a885f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Apr 2019 06:25:02 +0200 Subject: [PATCH 316/928] Add tsts for forcesell-answers --- freqtrade/tests/rpc/test_rpc.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 25d1109b2..e1feffec8 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -463,12 +463,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: with pytest.raises(RPCException, match=r'.*invalid argument*'): rpc._rpc_forcesell(None) - rpc._rpc_forcesell('all') + msg = rpc._rpc_forcesell('all') + assert msg == {'result': 'Created sell orders for all open trades.'} freqtradebot.create_trade() - rpc._rpc_forcesell('all') + msg = rpc._rpc_forcesell('all') + assert msg == {'result': 'Created sell orders for all open trades.'} - rpc._rpc_forcesell('1') + msg = rpc._rpc_forcesell('1') + assert msg == {'result': 'Created sell orders for trade 1.'} freqtradebot.state = State.STOPPED with pytest.raises(RPCException, match=r'.*trader is not running*'): @@ -511,7 +514,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: } ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called - rpc._rpc_forcesell('2') + msg = rpc._rpc_forcesell('2') + assert msg == {'result': 'Created sell orders for trade 2.'} assert cancel_order_mock.call_count == 2 assert trade.amount == amount @@ -525,7 +529,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: 'side': 'sell' } ) - rpc._rpc_forcesell('3') + msg = rpc._rpc_forcesell('3') + assert msg == {'result': 'Created sell orders for trade 3.'} # status quo, no exchange calls assert cancel_order_mock.call_count == 2 From 537c03504f62c5f59a59fd11b912470ae58a4e96 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Apr 2019 10:29:49 +0300 Subject: [PATCH 317/928] fix #1810 --- freqtrade/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index c7afe5c97..60c069e11 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -71,7 +71,7 @@ class Worker(object): while True: state = self._worker(old_state=state) if state == State.RELOAD_CONF: - self.freqtrade = self._reconfigure() + self._reconfigure() def _worker(self, old_state: State, throttle_secs: Optional[float] = None) -> State: """ From 615067973657a459de85c00f069c647986d8a761 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 30 Apr 2019 12:42:06 +0000 Subject: [PATCH 318/928] Update ccxt from 1.18.493 to 1.18.496 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 71680cc7b..e91b47d2f 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.493 +ccxt==1.18.496 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 5665426e6ba752c29be6dd9230c91ab08c4046c5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Apr 2019 19:47:55 +0300 Subject: [PATCH 319/928] better type hints in worker --- freqtrade/worker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 60c069e11..19a570505 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -39,7 +39,7 @@ class Worker(object): logger.debug("sd_notify: READY=1") self._sd_notify.notify("READY=1") - def _init(self, reconfig: bool): + def _init(self, reconfig: bool) -> None: """ Also called from the _reconfigure() method (with reconfig=True). """ @@ -63,17 +63,17 @@ class Worker(object): return self.freqtrade.state @state.setter - def state(self, value: State): + def state(self, value: State) -> None: self.freqtrade.state = value - def run(self): + def run(self) -> None: state = None while True: state = self._worker(old_state=state) if state == State.RELOAD_CONF: self._reconfigure() - def _worker(self, old_state: State, throttle_secs: Optional[float] = None) -> State: + def _worker(self, old_state: Optional[State], throttle_secs: Optional[float] = None) -> State: """ Trading routine that must be run at each loop :param old_state: the previous service state from the previous call @@ -148,7 +148,7 @@ class Worker(object): # state_changed = True return state_changed - def _reconfigure(self): + def _reconfigure(self) -> None: """ Cleans up current freqtradebot instance, reloads the configuration and replaces it with the new instance @@ -174,7 +174,7 @@ class Worker(object): logger.debug("sd_notify: READY=1") self._sd_notify.notify("READY=1") - def exit(self): + def exit(self) -> None: # Tell systemd that we are exiting now if self._sd_notify: logger.debug("sd_notify: STOPPING=1") From b24bbb2cb107cb095dfc628106aac2d2c6cfaf8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Apr 2019 19:32:03 +0200 Subject: [PATCH 320/928] Improve test for reload_conf with a "realistic" workflow --- freqtrade/tests/test_main.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index fc5d2e378..e4ffc5fae 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -7,10 +7,11 @@ import pytest from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.worker import Worker +from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.worker import Worker def test_parse_args_backtesting(mocker) -> None: @@ -107,24 +108,30 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_reload_conf(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) - mocker.patch('freqtrade.worker.Worker._worker', MagicMock(return_value=State.RELOAD_CONF)) + # Simulate Running, reload, running workflow + worker_mock = MagicMock(side_effect=[State.RUNNING, + State.RELOAD_CONF, + State.RUNNING, + OperationalException("Oh snap!")]) + mocker.patch('freqtrade.worker.Worker._worker', worker_mock) mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) + reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - # Raise exception as side effect to avoid endless loop - reconfigure_mock = mocker.patch( - 'freqtrade.main.Worker._reconfigure', MagicMock(side_effect=Exception) - ) - + args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg() + worker = Worker(args=args, config=default_conf) with pytest.raises(SystemExit): main(['-c', 'config.json.example']) - assert reconfigure_mock.call_count == 1 assert log_has('Using config: config.json.example ...', caplog.record_tuples) + assert worker_mock.call_count == 4 + assert reconfigure_mock.call_count == 1 + assert isinstance(worker.freqtrade, FreqtradeBot) def test_reconfigure(mocker, default_conf) -> None: From e7b81e4d46c4852769ff35be52644affac367fce Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 1 May 2019 15:27:58 +0300 Subject: [PATCH 321/928] hyperopt --min-trades parameter --- freqtrade/arguments.py | 9 +++++++++ freqtrade/configuration.py | 4 ++++ freqtrade/optimize/hyperopt.py | 6 +++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 930342e5a..327915b61 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -325,6 +325,15 @@ class Arguments(object): type=Arguments.check_int_positive, metavar='INT', ) + parser.add_argument( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + dest='hyperopt_min_trades', + default=1, + type=Arguments.check_int_positive, + metavar='INT', + ) def _build_subcommands(self) -> None: """ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 54862ff70..c19580c36 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -312,6 +312,10 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_random_state', logstring='Parameter --random-state detected: {}') + + self._args_to_config(config, argname='hyperopt_min_trades', + logstring='Parameter --min-trades detected: {}') + return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 52821d93b..235a20156 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -204,7 +204,11 @@ class Hyperopt(Backtesting): trade_count = len(results.index) trade_duration = results.trade_duration.mean() - if trade_count == 0: + # If this evaluation contains too short small amount of trades + # to be interesting -- consider it as 'bad' (assign max. loss value) + # in order to cast this hyperspace point away from optimization + # path. We do not want to optimize 'hodl' strategies. + if trade_count < self.config['hyperopt_min_trades']: return { 'loss': MAX_LOSS, 'params': params, From 4cecf0463992e25059c85c3d76c7b2a37aa3251a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 1 May 2019 14:43:05 +0200 Subject: [PATCH 322/928] Update ccxt from 1.18.496 to 1.18.497 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 70288fc45..6fa53153d 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.496 +ccxt==1.18.497 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 269699988b7586f964826c45cfce21e45683a5e8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 1 May 2019 15:55:56 +0300 Subject: [PATCH 323/928] test adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index eb2506f00..8bd5ffd0b 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -410,6 +410,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'config': 'config.json.example'}) default_conf.update({'timerange': None}) default_conf.update({'spaces': 'all'}) + default_conf.update({'hyperopt_min_trades': 1}) trades = [ ('POWR/BTC', 0.023117, 0.000233, 100) From 46214ce7cd690609bbd1c1c171e9b6a3e7a08306 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 May 2019 16:21:14 +0200 Subject: [PATCH 324/928] Fix typo after feedback --- freqtrade/rpc/rpc.py | 2 +- freqtrade/tests/rpc/test_rpc.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 007b7686d..af384da45 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -401,7 +401,7 @@ class RPC(object): _exec_forcesell(trade) Trade.session.flush() - return {'result': f'Created sell orders for trade {trade_id}.'} + return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """ diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index e1feffec8..93de6ef89 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -471,7 +471,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: assert msg == {'result': 'Created sell orders for all open trades.'} msg = rpc._rpc_forcesell('1') - assert msg == {'result': 'Created sell orders for trade 1.'} + assert msg == {'result': 'Created sell order for trade 1.'} freqtradebot.state = State.STOPPED with pytest.raises(RPCException, match=r'.*trader is not running*'): @@ -515,7 +515,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called msg = rpc._rpc_forcesell('2') - assert msg == {'result': 'Created sell orders for trade 2.'} + assert msg == {'result': 'Created sell order for trade 2.'} assert cancel_order_mock.call_count == 2 assert trade.amount == amount @@ -530,7 +530,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: } ) msg = rpc._rpc_forcesell('3') - assert msg == {'result': 'Created sell orders for trade 3.'} + assert msg == {'result': 'Created sell order for trade 3.'} # status quo, no exchange calls assert cancel_order_mock.call_count == 2 From 6c2301ec39b314c815f5d805283bdc44345cf293 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 2 May 2019 12:43:05 +0000 Subject: [PATCH 325/928] Update ccxt from 1.18.497 to 1.18.500 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 6fa53153d..fb05388de 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.497 +ccxt==1.18.500 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From dad55fe7a85b65fa4c80a44a48fcc7fed1e40a37 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 3 May 2019 12:43:08 +0000 Subject: [PATCH 326/928] Update ccxt from 1.18.500 to 1.18.502 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index fb05388de..9a8bb6141 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.500 +ccxt==1.18.502 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 32e4b0b1b2826cec22cef36901c51a1cc1fa3cef Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 3 May 2019 12:43:09 +0000 Subject: [PATCH 327/928] Update pytest-cov from 2.6.1 to 2.7.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 123531f23..0c3dc9727 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,6 +7,6 @@ flake8-tidy-imports==2.0.0 pytest==4.4.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 -pytest-cov==2.6.1 +pytest-cov==2.7.1 coveralls==1.7.0 mypy==0.701 From 1be4c59481bf706211416f8a544635b3f35e7fbe Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 3 May 2019 16:48:07 +0300 Subject: [PATCH 328/928] qtpylib/indicators.py updated --- freqtrade/vendor/qtpylib/indicators.py | 205 ++++++++++++++----------- 1 file changed, 119 insertions(+), 86 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 3866d36c1..d5860ff5f 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -4,13 +4,13 @@ # QTPyLib: Quantitative Trading Python Library # https://github.com/ranaroussi/qtpylib # -# Copyright 2016 Ran Aroussi +# Copyright 2016-2018 Ran Aroussi # -# Licensed under the GNU Lesser General Public License, v3.0 (the "License"); +# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# https://www.gnu.org/licenses/lgpl-3.0.en.html +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -19,8 +19,8 @@ # limitations under the License. # -import sys import warnings +import sys from datetime import datetime, timedelta import numpy as np @@ -62,19 +62,20 @@ def numpy_rolling_series(func): @numpy_rolling_series def numpy_rolling_mean(data, window, as_source=False): - return np.mean(numpy_rolling_window(data, window), -1) + return np.mean(numpy_rolling_window(data, window), axis=-1) @numpy_rolling_series def numpy_rolling_std(data, window, as_source=False): - return np.std(numpy_rolling_window(data, window), -1) + return np.std(numpy_rolling_window(data, window), axis=-1, ddof=1) + # --------------------------------------------- def session(df, start='17:00', end='16:00'): """ remove previous globex day from df """ - if len(df) == 0: + if df.empty: return df # get start/end/now as decimals @@ -103,47 +104,50 @@ def session(df, start='17:00', end='16:00'): return df.copy() - # --------------------------------------------- + def heikinashi(bars): bars = bars.copy() bars['ha_close'] = (bars['open'] + bars['high'] + bars['low'] + bars['close']) / 4 - bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2 - bars.loc[:1, 'ha_open'] = bars['open'].values[0] - for x in range(2): - bars.loc[1:, 'ha_open'] = ( - (bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:] + # ha open + bars.loc[:1, 'ha_open'] = (bars['open'] + bars['close']) / 2 + prev_open = bars[:1]['ha_open'].values[0] + for idx, _ in bars[1:][['ha_open', 'ha_close']].iterrows(): + loc = bars.index.get_loc(idx) + prev_open = (prev_open + bars['ha_close'].values[loc - 1]) / 2 + bars.loc[loc:loc + 1, 'ha_open'] = prev_open bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) - return pd.DataFrame( - index=bars.index, - data={ - 'open': bars['ha_open'], - 'high': bars['ha_high'], - 'low': bars['ha_low'], - 'close': bars['ha_close']}) - + return pd.DataFrame(index=bars.index, + data={'open': bars['ha_open'], + 'high': bars['ha_high'], + 'low': bars['ha_low'], + 'close': bars['ha_close']}) # --------------------------------------------- -def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2, - rsi_signal_len=7, bollinger_std=1.6185): - rsi_series = rsi(series, rsi_len) - bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std) - signal = sma(rsi_series, rsi_signal_len) - rsi_series = sma(rsi_series, rsi_smoothing) + +def tdi(series, rsi_lookback=13, rsi_smooth_len=2, + rsi_signal_len=7, bb_lookback=34, bb_std=1.6185): + + rsi_data = rsi(series, rsi_lookback) + rsi_smooth = sma(rsi_data, rsi_smooth_len) + rsi_signal = sma(rsi_data, rsi_signal_len) + + bb_series = bollinger_bands(rsi_data, bb_lookback, bb_std) return pd.DataFrame(index=series.index, data={ - "rsi": rsi_series, - "signal": signal, - "bbupper": bb_series['upper'], - "bblower": bb_series['lower'], - "bbmid": bb_series['mid'] + "rsi": rsi_data, + "rsi_signal": rsi_signal, + "rsi_smooth": rsi_smooth, + "rsi_bb_upper": bb_series['upper'], + "rsi_bb_lower": bb_series['lower'], + "rsi_bb_mid": bb_series['mid'] }) # --------------------------------------------- @@ -163,8 +167,8 @@ def awesome_oscillator(df, weighted=False, fast=5, slow=34): # --------------------------------------------- -def nans(len=1): - mtx = np.empty(len) +def nans(length=1): + mtx = np.empty(length) mtx[:] = np.nan return mtx @@ -222,7 +226,7 @@ def crossed(series1, series2, direction=None): if isinstance(series1, np.ndarray): series1 = pd.Series(series1) - if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray): + if isinstance(series2, (float, int, np.ndarray)): series2 = pd.Series(index=series1.index, data=series2) if direction is None or direction == "above": @@ -256,7 +260,7 @@ def rolling_std(series, window=200, min_periods=None): else: try: return series.rolling(window=window, min_periods=min_periods).std() - except BaseException: + except Exception as e: return pd.Series(series).rolling(window=window, min_periods=min_periods).std() # --------------------------------------------- @@ -269,7 +273,7 @@ def rolling_mean(series, window=200, min_periods=None): else: try: return series.rolling(window=window, min_periods=min_periods).mean() - except BaseException: + except Exception as e: return pd.Series(series).rolling(window=window, min_periods=min_periods).mean() # --------------------------------------------- @@ -279,7 +283,7 @@ def rolling_min(series, window=14, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.rolling(window=window, min_periods=min_periods).min() - except BaseException: + except Exception as e: return pd.Series(series).rolling(window=window, min_periods=min_periods).min() @@ -289,7 +293,7 @@ def rolling_max(series, window=14, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.rolling(window=window, min_periods=min_periods).min() - except BaseException: + except Exception as e: return pd.Series(series).rolling(window=window, min_periods=min_periods).min() @@ -299,16 +303,17 @@ def rolling_weighted_mean(series, window=200, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.ewm(span=window, min_periods=min_periods).mean() - except BaseException: + except Exception as e: return pd.ewma(series, span=window, min_periods=min_periods) # --------------------------------------------- -def hull_moving_average(series, window=200): - wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \ - rolling_weighted_mean(series, window=window) - return rolling_weighted_mean(wma, window=np.sqrt(window)) +def hull_moving_average(series, window=200, min_periods=None): + min_periods = window if min_periods is None else min_periods + ma = (2 * rolling_weighted_mean(series, window / 2, min_periods)) - \ + rolling_weighted_mean(series, window, min_periods) + return rolling_weighted_mean(ma, np.sqrt(window), min_periods) # --------------------------------------------- @@ -325,8 +330,8 @@ def wma(series, window=200, min_periods=None): # --------------------------------------------- -def hma(series, window=200): - return hull_moving_average(series, window=window) +def hma(series, window=200, min_periods=None): + return hull_moving_average(series, window=window, min_periods=min_periods) # --------------------------------------------- @@ -361,7 +366,7 @@ def rolling_vwap(bars, window=200, min_periods=None): min_periods=min_periods).sum() right = volume.rolling(window=window, min_periods=min_periods).sum() - return pd.Series(index=bars.index, data=(left / right)) + return pd.Series(index=bars.index, data=(left / right)).replace([np.inf, -np.inf], float('NaN')).ffill() # --------------------------------------------- @@ -370,6 +375,7 @@ def rsi(series, window=14): """ compute the n period relative strength indicator """ + # 100-(100/relative_strength) deltas = np.diff(series) seed = deltas[:window + 1] @@ -406,13 +412,13 @@ def macd(series, fast=3, slow=10, smooth=16): using a fast and slow exponential moving avg' return value is emaslow, emafast, macd which are len(x) arrays """ - macd = rolling_weighted_mean(series, window=fast) - \ + macd_line = rolling_weighted_mean(series, window=fast) - \ rolling_weighted_mean(series, window=slow) - signal = rolling_weighted_mean(macd, window=smooth) - histogram = macd - signal - # return macd, signal, histogram + signal = rolling_weighted_mean(macd_line, window=smooth) + histogram = macd_line - signal + # return macd_line, signal, histogram return pd.DataFrame(index=series.index, data={ - 'macd': macd.values, + 'macd': macd_line.values, 'signal': signal.values, 'histogram': histogram.values }) @@ -421,14 +427,14 @@ def macd(series, fast=3, slow=10, smooth=16): # --------------------------------------------- def bollinger_bands(series, window=20, stds=2): - sma = rolling_mean(series, window=window) - std = rolling_std(series, window=window) - upper = sma + std * stds - lower = sma - std * stds + ma = rolling_mean(series, window=window, min_periods=1) + std = rolling_std(series, window=window, min_periods=1) + upper = ma + std * stds + lower = ma - std * stds return pd.DataFrame(index=series.index, data={ 'upper': upper, - 'mid': sma, + 'mid': ma, 'lower': lower }) @@ -454,7 +460,7 @@ def returns(series): try: res = (series / series.shift(1) - 1).replace([np.inf, -np.inf], float('NaN')) - except BaseException: + except Exception as e: res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -465,8 +471,8 @@ def returns(series): def log_returns(series): try: res = np.log(series / series.shift(1) - ).replace([np.inf, -np.inf], float('NaN')) - except BaseException: + ).replace([np.inf, -np.inf], float('NaN')) + except Exception as e: res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -477,9 +483,9 @@ def log_returns(series): def implied_volatility(series, window=252): try: logret = np.log(series / series.shift(1) - ).replace([np.inf, -np.inf], float('NaN')) + ).replace([np.inf, -np.inf], float('NaN')) res = numpy_rolling_std(logret, window) * np.sqrt(window) - except BaseException: + except Exception as e: res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -530,32 +536,52 @@ def stoch(df, window=14, d=3, k=3, fast=False): compute the n period relative strength indicator http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html """ - highs_ma = pd.concat([df['high'].shift(i) - for i in np.arange(window)], 1).apply(list, 1) - highs_ma = highs_ma.T.max().T - lows_ma = pd.concat([df['low'].shift(i) - for i in np.arange(window)], 1).apply(list, 1) - lows_ma = lows_ma.T.min().T + my_df = pd.DataFrame(index=df.index) - fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100 - fast_d = numpy_rolling_mean(fast_k, d) + my_df['rolling_max'] = df['high'].rolling(window).max() + my_df['rolling_min'] = df['low'].rolling(window).min() + + my_df['fast_k'] = 100 * (df['close'] - my_df['rolling_min'])/(my_df['rolling_max'] - my_df['rolling_min']) + my_df['fast_d'] = my_df['fast_k'].rolling(d).mean() if fast: - data = { - 'k': fast_k, - 'd': fast_d - } + return my_df.loc[:, ['fast_k', 'fast_d']] - else: - slow_k = numpy_rolling_mean(fast_k, k) - slow_d = numpy_rolling_mean(slow_k, d) - data = { - 'k': slow_k, - 'd': slow_d - } + my_df['slow_k'] = my_df['fast_k'].rolling(k).mean() + my_df['slow_d'] = my_df['slow_k'].rolling(d).mean() - return pd.DataFrame(index=df.index, data=data) + return my_df.loc[:, ['slow_k', 'slow_d']] + +# --------------------------------------------- + + +def zlma(series, window=20, min_periods=None, kind="ema"): + """ + John Ehlers' Zero lag (exponential) moving average + https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average + """ + min_periods = window if min_periods is None else min_periods + + lag = (window - 1) // 2 + series = 2 * series - series.shift(lag) + if kind in ['ewm', 'ema']: + return wma(series, lag, min_periods) + elif kind == "hma": + return hma(series, lag, min_periods) + return sma(series, lag, min_periods) + + +def zlema(series, window, min_periods=None): + return zlma(series, window, min_periods, kind="ema") + + +def zlsma(series, window, min_periods=None): + return zlma(series, window, min_periods, kind="sma") + + +def zlhma(series, window, min_periods=None): + return zlma(series, window, min_periods, kind="hma") # --------------------------------------------- @@ -571,13 +597,13 @@ def zscore(bars, window=20, stds=1, col='close'): def pvt(bars): """ Price Volume Trend """ - pvt = ((bars['close'] - bars['close'].shift(1)) / - bars['close'].shift(1)) * bars['volume'] - return pvt.cumsum() - + trend = ((bars['close'] - bars['close'].shift(1)) / + bars['close'].shift(1)) * bars['volume'] + return trend.cumsum() # ============================================= + PandasObject.session = session PandasObject.atr = atr PandasObject.bollinger_bands = bollinger_bands @@ -613,4 +639,11 @@ PandasObject.rolling_weighted_mean = rolling_weighted_mean PandasObject.sma = sma PandasObject.wma = wma +PandasObject.ema = wma PandasObject.hma = hma + +PandasObject.zlsma = zlsma +PandasObject.zlwma = zlema +PandasObject.zlema = zlema +PandasObject.zlhma = zlhma +PandasObject.zlma = zlma From 66c2bdd65a06dc06d535c903869269840c8f1c3f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 3 May 2019 16:58:51 +0300 Subject: [PATCH 329/928] flake happy --- freqtrade/vendor/qtpylib/indicators.py | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index d5860ff5f..8586968e8 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -260,7 +260,7 @@ def rolling_std(series, window=200, min_periods=None): else: try: return series.rolling(window=window, min_periods=min_periods).std() - except Exception as e: + except Exception as e: # noqa: F841 return pd.Series(series).rolling(window=window, min_periods=min_periods).std() # --------------------------------------------- @@ -273,7 +273,7 @@ def rolling_mean(series, window=200, min_periods=None): else: try: return series.rolling(window=window, min_periods=min_periods).mean() - except Exception as e: + except Exception as e: # noqa: F841 return pd.Series(series).rolling(window=window, min_periods=min_periods).mean() # --------------------------------------------- @@ -283,7 +283,7 @@ def rolling_min(series, window=14, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.rolling(window=window, min_periods=min_periods).min() - except Exception as e: + except Exception as e: # noqa: F841 return pd.Series(series).rolling(window=window, min_periods=min_periods).min() @@ -293,7 +293,7 @@ def rolling_max(series, window=14, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.rolling(window=window, min_periods=min_periods).min() - except Exception as e: + except Exception as e: # noqa: F841 return pd.Series(series).rolling(window=window, min_periods=min_periods).min() @@ -303,7 +303,7 @@ def rolling_weighted_mean(series, window=200, min_periods=None): min_periods = window if min_periods is None else min_periods try: return series.ewm(span=window, min_periods=min_periods).mean() - except Exception as e: + except Exception as e: # noqa: F841 return pd.ewma(series, span=window, min_periods=min_periods) @@ -366,7 +366,8 @@ def rolling_vwap(bars, window=200, min_periods=None): min_periods=min_periods).sum() right = volume.rolling(window=window, min_periods=min_periods).sum() - return pd.Series(index=bars.index, data=(left / right)).replace([np.inf, -np.inf], float('NaN')).ffill() + return pd.Series(index=bars.index, data=(left / right) + ).replace([np.inf, -np.inf], float('NaN')).ffill() # --------------------------------------------- @@ -460,7 +461,7 @@ def returns(series): try: res = (series / series.shift(1) - 1).replace([np.inf, -np.inf], float('NaN')) - except Exception as e: + except Exception as e: # noqa: F841 res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -471,8 +472,8 @@ def returns(series): def log_returns(series): try: res = np.log(series / series.shift(1) - ).replace([np.inf, -np.inf], float('NaN')) - except Exception as e: + ).replace([np.inf, -np.inf], float('NaN')) + except Exception as e: # noqa: F841 res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -483,9 +484,9 @@ def log_returns(series): def implied_volatility(series, window=252): try: logret = np.log(series / series.shift(1) - ).replace([np.inf, -np.inf], float('NaN')) + ).replace([np.inf, -np.inf], float('NaN')) res = numpy_rolling_std(logret, window) * np.sqrt(window) - except Exception as e: + except Exception as e: # noqa: F841 res = nans(len(series)) return pd.Series(index=series.index, data=res) @@ -542,7 +543,10 @@ def stoch(df, window=14, d=3, k=3, fast=False): my_df['rolling_max'] = df['high'].rolling(window).max() my_df['rolling_min'] = df['low'].rolling(window).min() - my_df['fast_k'] = 100 * (df['close'] - my_df['rolling_min'])/(my_df['rolling_max'] - my_df['rolling_min']) + my_df['fast_k'] = ( + 100 * (df['close'] - my_df['rolling_min']) / + (my_df['rolling_max'] - my_df['rolling_min']) + ) my_df['fast_d'] = my_df['fast_k'].rolling(d).mean() if fast: From f506644a8cabb542c176561a63b94ef879aa0164 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 May 2019 09:10:25 +0200 Subject: [PATCH 330/928] Improve docker documentation --- docs/installation.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 7060d7b39..11ddc010d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -237,14 +237,18 @@ docker run -d \ --name freqtrade \ -v /etc/localtime:/etc/localtime:ro \ -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade --db-url sqlite:///tradesv3.sqlite + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy ``` !!! Note db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` +!!! Note + All command line arguments can be added to the end of the `docker run` command. + ### 6. Monitor your Docker instance You can then use the following commands to monitor and manage your container: From 6c03246ec80ed4f430215a2079cd96dea43e2dd1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 4 May 2019 12:42:04 +0000 Subject: [PATCH 331/928] Update ccxt from 1.18.502 to 1.18.507 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 9a8bb6141..f7c6b240f 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.502 +ccxt==1.18.507 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 1e056ee415593b502dbb6277ecfc26e280956383 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 May 2019 14:07:08 +0200 Subject: [PATCH 332/928] Move trade jsonification to trade class --- freqtrade/persistence.py | 17 +++++++++++++++++ freqtrade/rpc/rpc.py | 19 ++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 5a18a922a..d25ea9fed 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -213,6 +213,23 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + def to_json(self) -> Dict[str, Any]: + + return { + 'trade_id': self.id, + 'pair': self.pair, + 'date': arrow.get(self.open_date), + 'open_rate': self.open_rate, + 'close_rate': self.close_rate, + 'amount': round(self.amount, 8), + 'stake_amount': round(self.stake_amount, 8), + 'stop_loss': self.stop_loss, + 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, + 'initial_stop_loss': self.initial_stop_loss, + 'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100) + if self.initial_stop_loss_pct else None, + } + def adjust_min_max_rates(self, current_price: float): """ Adjust the max_rate and min_rate. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index af384da45..37312318a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -100,28 +100,17 @@ class RPC(object): current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) - results.append(dict( - trade_id=trade.id, - pair=trade.pair, + trade_dict = trade.to_json() + trade_dict.update(dict( base_currency=self._freqtrade.config['stake_currency'], - date=arrow.get(trade.open_date), - open_rate=trade.open_rate, - close_rate=trade.close_rate, - current_rate=current_rate, - amount=round(trade.amount, 8), - stake_amount=round(trade.stake_amount, 8), close_profit=fmt_close_profit, + current_rate=current_rate, current_profit=round(current_profit * 100, 2), - stop_loss=trade.stop_loss, - stop_loss_pct=(trade.stop_loss_pct * 100) - if trade.stop_loss_pct else None, - initial_stop_loss=trade.initial_stop_loss, - initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100) - if trade.initial_stop_loss_pct else None, open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, )) + results.append(trade_dict) return results def _rpc_status_table(self) -> DataFrame: From 2200a0223b679fe1f0b7b1bba0c7808cdf42e7b8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 6 May 2019 00:30:21 +0300 Subject: [PATCH 333/928] fixed heikinashi --- freqtrade/vendor/qtpylib/indicators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 8586968e8..cb1b794fd 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -113,7 +113,7 @@ def heikinashi(bars): bars['low'] + bars['close']) / 4 # ha open - bars.loc[:1, 'ha_open'] = (bars['open'] + bars['close']) / 2 + bars.loc[0:1, 'ha_open'] = (bars['open'].values[0] + bars['close'].values[0]) / 2 prev_open = bars[:1]['ha_open'].values[0] for idx, _ in bars[1:][['ha_open', 'ha_close']].iterrows(): loc = bars.index.get_loc(idx) From 31d271084fe78f29a2358d719a95fb8e4e119128 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 May 2019 06:55:12 +0200 Subject: [PATCH 334/928] Move json to persistence --- freqtrade/persistence.py | 12 ++++++++---- freqtrade/rpc/telegram.py | 5 +---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index d25ea9fed..6088dba72 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -214,11 +214,15 @@ class Trade(_DECL_BASE): f'open_rate={self.open_rate:.8f}, open_since={open_since})') def to_json(self) -> Dict[str, Any]: - return { 'trade_id': self.id, 'pair': self.pair, - 'date': arrow.get(self.open_date), + 'open_date_hum': arrow.get(self.open_date).humanize(), + 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'close_date_hum': (arrow.get(self.close_date).humanize() + if self.close_date else None), + 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") + if self.close_date else None), 'open_rate': self.open_rate, 'close_rate': self.close_rate, 'amount': round(self.amount, 8), @@ -226,8 +230,8 @@ class Trade(_DECL_BASE): 'stop_loss': self.stop_loss, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'initial_stop_loss': self.initial_stop_loss, - 'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100) - if self.initial_stop_loss_pct else None, + 'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100 + if self.initial_stop_loss_pct else None), } def adjust_min_max_rates(self, current_price: float): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index dc0bad2b2..497c117ac 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -193,14 +193,11 @@ class Telegram(RPC): try: results = self._rpc_trade_status() - # pre format data - for result in results: - result['date'] = result['date'].humanize() messages = [] for r in results: lines = [ - "*Trade ID:* `{trade_id}` `(since {date})`", + "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Open Rate:* `{open_rate:.8f}`", From 2b78f73fe5e0ecb689c9d7d57b8f67bb6bc90b96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 May 2019 06:56:07 +0200 Subject: [PATCH 335/928] Adapt tests to to_json method --- freqtrade/tests/rpc/test_rpc.py | 12 +++++++++--- freqtrade/tests/rpc/test_rpc_telegram.py | 5 ++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 93de6ef89..1f3f26dc9 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -47,12 +47,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: freqtradebot.create_trade() results = rpc._rpc_trade_status() - + print(results[0]) assert { 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', - 'date': ANY, + 'open_date': ANY, + 'open_date_hum': ANY, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, @@ -78,7 +81,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', - 'date': ANY, + 'open_date': ANY, + 'open_date_hum': ANY, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': ANY, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index f2f3f3945..86d728290 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -192,7 +192,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', - 'date': arrow.utcnow(), + 'open_date': arrow.utcnow(), + 'open_date_hum': arrow.utcnow().humanize, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, From 1a677c7441e0773826a12f8000384dd485062a32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 May 2019 06:56:22 +0200 Subject: [PATCH 336/928] Add explicit test for to_json --- freqtrade/tests/rpc/test_rpc.py | 1 - freqtrade/tests/test_persistence.py | 69 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 1f3f26dc9..6ce543f3d 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -47,7 +47,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: freqtradebot.create_trade() results = rpc._rpc_trade_status() - print(results[0]) assert { 'trade_id': 1, 'pair': 'ETH/BTC', diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index f57a466e3..8c15fa8e8 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring, C0103 -from unittest.mock import MagicMock import logging +from unittest.mock import MagicMock +import arrow import pytest from sqlalchemy import create_engine @@ -710,3 +711,69 @@ def test_get_open(default_conf, fee): Trade.session.add(trade) assert len(Trade.get_open_trades()) == 2 + + +def test_to_json(default_conf, fee): + init(default_conf) + + # Simulate dry_run entries + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + open_rate=0.123, + exchange='bittrex', + open_order_id='dry_run_buy_12345' + ) + result = trade.to_json() + assert isinstance(result, dict) + print(result) + + assert result == {'trade_id': None, + 'pair': 'ETH/BTC', + 'open_date_hum': '2 hours ago', + 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'close_date_hum': None, + 'close_date': None, + 'open_rate': 0.123, + 'close_rate': None, + 'amount': 123.0, + 'stake_amount': 0.001, + 'stop_loss': None, + 'stop_loss_pct': None, + 'initial_stop_loss': None, + 'initial_stop_loss_pct': None} + + # Simulate dry_run entries + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=100.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + close_date=arrow.utcnow().shift(hours=-1).datetime, + open_rate=0.123, + close_rate=0.125, + exchange='bittrex', + ) + result = trade.to_json() + assert isinstance(result, dict) + + assert result == {'trade_id': None, + 'pair': 'XRP/BTC', + 'open_date_hum': '2 hours ago', + 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'close_date_hum': 'an hour ago', + 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_rate': 0.123, + 'close_rate': 0.125, + 'amount': 100.0, + 'stake_amount': 0.001, + 'stop_loss': None, + 'stop_loss_pct': None, + 'initial_stop_loss': None, + 'initial_stop_loss_pct': None} From c8b8806fed9e2bf3c32398b6e0ee1d1c3ab1c204 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 6 May 2019 12:42:06 +0000 Subject: [PATCH 337/928] Update ccxt from 1.18.507 to 1.18.508 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f7c6b240f..703b77b0e 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.507 +ccxt==1.18.508 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 6467d3b58e29c15157be6b264a55acefeb89f406 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 6 May 2019 18:27:05 +0300 Subject: [PATCH 338/928] check python version --- freqtrade/main.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 877e2921d..79d150441 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -22,21 +22,28 @@ def main(sysargv: List[str]) -> None: This function will initiate the bot and start the trading loop. :return: None """ - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args: Namespace = arguments.get_parsed_arg() - - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot - if hasattr(args, 'func'): - args.func(args) - return - - worker = None - return_code = 1 try: + worker = None + return_code = 1 + + # check min. python version + if sys.version_info < (3, 6): + raise SystemError("Freqtrade requires Python version >= 3.6") + + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() + + # A subcommand has been issued. + # Means if Backtesting or Hyperopt have been called we exit the bot + if hasattr(args, 'func'): + args.func(args) + # TODO: fetch return_code as returned by the command function here + return_code = 0 + return + # Load and run worker worker = Worker(args) worker.run() @@ -47,8 +54,8 @@ def main(sysargv: List[str]) -> None: except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: - logger.exception('Fatal exception!') + except BaseException as e: + logger.exception('Fatal exception! ' + str(e)) finally: if worker: worker.exit() From a8c4bed4e8b2ce99b255548f2d8f3804de15ec74 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 7 May 2019 12:42:07 +0000 Subject: [PATCH 339/928] Update ccxt from 1.18.508 to 1.18.509 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 703b77b0e..90949243c 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.508 +ccxt==1.18.509 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From db0644eddfb58e2404eef3145170e68029b0e67e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 7 May 2019 12:42:10 +0000 Subject: [PATCH 340/928] Update plotly from 3.8.1 to 3.9.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 4cdd6ec8f..23daee258 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.8.1 +plotly==3.9.0 From d642e03cd0d134bc0f9a542e9c01303b46852a53 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 7 May 2019 23:39:42 +0300 Subject: [PATCH 341/928] heikinashi performance problem resolved --- freqtrade/vendor/qtpylib/indicators.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index cb1b794fd..d16d7aa44 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -113,12 +113,11 @@ def heikinashi(bars): bars['low'] + bars['close']) / 4 # ha open - bars.loc[0:1, 'ha_open'] = (bars['open'].values[0] + bars['close'].values[0]) / 2 - prev_open = bars[:1]['ha_open'].values[0] - for idx, _ in bars[1:][['ha_open', 'ha_close']].iterrows(): - loc = bars.index.get_loc(idx) - prev_open = (prev_open + bars['ha_close'].values[loc - 1]) / 2 - bars.loc[loc:loc + 1, 'ha_open'] = prev_open + for i in range(0, len(bars)): + bars.at[i, 'ha_open'] = ( + (bars.at[0, 'open'] if i == 0 else bars.at[i - 1, 'ha_open']) + + (bars.at[0, 'close'] if i == 0 else bars.at[i - 1, 'ha_close']) + ) / 2 bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) From 2554ebf2739a7d400c2b766469a92cc6d4d2acd4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 8 May 2019 00:00:44 +0300 Subject: [PATCH 342/928] fixed: heikinashi worked in backtesting, but failed in tests with testing arrays --- freqtrade/vendor/qtpylib/indicators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index d16d7aa44..d62d062d7 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -113,11 +113,15 @@ def heikinashi(bars): bars['low'] + bars['close']) / 4 # ha open + idx = bars.index.name + bars.reset_index(inplace=True) for i in range(0, len(bars)): bars.at[i, 'ha_open'] = ( (bars.at[0, 'open'] if i == 0 else bars.at[i - 1, 'ha_open']) + (bars.at[0, 'close'] if i == 0 else bars.at[i - 1, 'ha_close']) ) / 2 + if idx: + bars.set_index(idx, inplace=True) bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) From cf1ad3fd8c4be05d2f62617e1bd02057e8a3908c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 8 May 2019 12:42:06 +0000 Subject: [PATCH 343/928] Update ccxt from 1.18.509 to 1.18.512 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 90949243c..b79729731 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.509 +ccxt==1.18.512 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 1ccc25b4860e9b48f82c30430020a0a58872b6a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 May 2019 20:33:22 +0200 Subject: [PATCH 344/928] Fix test-data indexing --- freqtrade/tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 281ce2bfe..02f8840e2 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -33,7 +33,7 @@ def get_args(args) -> List[str]: def trim_dictlist(dict_list, num): new = {} for pair, pair_data in dict_list.items(): - new[pair] = pair_data[num:] + new[pair] = pair_data[num:].reset_index() return new @@ -708,7 +708,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): data = trim_dictlist(data, -500) # Remove data for one pair from the beginning of the data - data[pair] = data[pair][tres:] + data[pair] = data[pair][tres:].reset_index() # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} default_conf['ticker_interval'] = '5m' From 45e586773639da57f93d5fa3b7767ace37568160 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 8 May 2019 23:41:45 +0300 Subject: [PATCH 345/928] heikinashi loop optimized; reset_index moved to tests --- freqtrade/vendor/qtpylib/indicators.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index d62d062d7..6edf626f0 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -113,15 +113,9 @@ def heikinashi(bars): bars['low'] + bars['close']) / 4 # ha open - idx = bars.index.name - bars.reset_index(inplace=True) - for i in range(0, len(bars)): - bars.at[i, 'ha_open'] = ( - (bars.at[0, 'open'] if i == 0 else bars.at[i - 1, 'ha_open']) + - (bars.at[0, 'close'] if i == 0 else bars.at[i - 1, 'ha_close']) - ) / 2 - if idx: - bars.set_index(idx, inplace=True) + bars.at[0, 'ha_open'] = (bars.at[0, 'open'] + bars.at[0, 'close']) / 2 + for i in range(1, len(bars)): + bars.at[i, 'ha_open'] = (bars.at[i - 1, 'ha_open'] + bars.at[i - 1, 'ha_close']) / 2 bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) From 0410654c2cac12b4b2e8c13b432dfb1bf29cdde0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 May 2019 06:51:30 +0200 Subject: [PATCH 346/928] Add printing dataframe to documentation --- docs/bot-optimization.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 9e754c213..c83e4d0b1 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -345,6 +345,30 @@ if self.wallets: - `get_used(asset)` - currently tied up balance (open orders) - `get_total(asset)` - total available balance - sum of the 2 above +### Print created dataframe + +To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. +You may also want to print the pair so it's clear what data is currently shown. + +``` python +def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + #>> whatever condition<<< + ), + 'buy'] = 1 + + # Print the Analyzed pair + print(f"result for {metadata['pair']}") + + # Inspect the last 5 rows + print(dataframe.tail()) + + return dataframe +``` + +Printing more than a few rows is possible (don't use `.tail()`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). + ### Where is the default strategy? The default buy strategy is located in the file From 909df0d7bbe2094a53c0799c4f17d42b11a2debb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 May 2019 08:56:27 +0200 Subject: [PATCH 347/928] Improve doc wording --- docs/bot-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index c83e4d0b1..5e080eab1 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -367,7 +367,7 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return dataframe ``` -Printing more than a few rows is possible (don't use `.tail()`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). +Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). ### Where is the default strategy? From f36ccdd9fa71c63743dcf513843d4bdfde960c51 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 9 May 2019 12:42:08 +0000 Subject: [PATCH 348/928] Update ccxt from 1.18.512 to 1.18.514 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b79729731..07c65ffc7 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.512 +ccxt==1.18.514 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 00383b9438a7c597f39fc74de8c3f1e3cd55e132 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 9 May 2019 12:42:09 +0000 Subject: [PATCH 349/928] Update pytest from 4.4.1 to 4.4.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0c3dc9727..3c99a87f6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.4.1 +pytest==4.4.2 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 0f43e0bb7d786f13f00f6b246851ac44c674231c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 10 May 2019 10:54:44 +0300 Subject: [PATCH 350/928] minor hyperopt output improvements --- freqtrade/optimize/hyperopt.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 235a20156..06c7ae495 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -33,6 +33,7 @@ from freqtrade.resolvers import HyperOptResolver logger = logging.getLogger(__name__) +INITIAL_POINTS = 30 MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl') TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle') @@ -120,14 +121,20 @@ class Hyperopt(Backtesting): """ Log results if it is better than any previous evaluation """ - if self.config.get('print_all', False) or results['loss'] < self.current_best_loss: - current = results['current_tries'] + print_all = self.config.get('print_all', False) + if print_all or results['loss'] < self.current_best_loss: + # Output human-friendly index here (starting from 1) + current = results['current_tries'] + 1 total = results['total_tries'] res = results['result'] loss = results['loss'] self.current_best_loss = results['loss'] - log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}' - print(log_msg) + log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' + log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}' + if print_all: + print(log_msg) + else: + print('\n' + log_msg) else: print('.', end='') sys.stdout.flush() @@ -228,13 +235,13 @@ class Hyperopt(Backtesting): Return the format result in a string """ trades = len(results.index) - avg_profit = results.profit_percent.mean() * 100.0 + avg_profit = results.profit_percent.mean() total_profit = results.profit_abs.sum() stake_cur = self.config['stake_currency'] profit = results.profit_percent.sum() duration = results.trade_duration.mean() - return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' + return (f'{trades:6d} trades. Avg profit {avg_profit: 9.6f}%. ' f'Total profit {total_profit: 11.8f} {stake_cur} ' f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.') @@ -243,7 +250,7 @@ class Hyperopt(Backtesting): self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", - n_initial_points=30, + n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, random_state=self.config.get('hyperopt_random_state', None) ) @@ -301,9 +308,11 @@ class Hyperopt(Backtesting): self.trials += f_val for j in range(jobs): + current = i * jobs + j self.log_results({ 'loss': f_val[j]['loss'], - 'current_tries': i * jobs + j, + 'current_tries': current, + 'initial_point': current < INITIAL_POINTS, 'total_tries': self.total_tries, 'result': f_val[j]['result'], }) From 349d5563394d0fada998c936f133c88a4e643d94 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 10 May 2019 12:42:13 +0000 Subject: [PATCH 351/928] Update ccxt from 1.18.514 to 1.18.516 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 07c65ffc7..da8b76550 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.514 +ccxt==1.18.516 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From ab23db2fa1bf594a5ceb7047fa98ac8b78663c3a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 10 May 2019 12:42:14 +0000 Subject: [PATCH 352/928] Update scikit-learn from 0.20.3 to 0.21.0 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index da8b76550..61c753784 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.0 requests==2.21.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 -scikit-learn==0.20.3 +scikit-learn==0.21.0 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From 75306b7a6ea559efbfcf0b79f2d4fe671f4c5c3f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 11 May 2019 10:17:46 +0300 Subject: [PATCH 353/928] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 8bd5ffd0b..325713ba7 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -249,11 +249,12 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None: 'loss': 1, 'current_tries': 1, 'total_tries': 2, - 'result': 'foo' + 'result': 'foo.', + 'initial_point': False } ) out, err = capsys.readouterr() - assert ' 1/2: foo. Loss 1.00000' in out + assert ' 2/2: foo. Objective: 1.00000' in out def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: @@ -458,7 +459,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + 'result': ' 1 trades. Avg profit 0.023117%. Total profit 0.00023300 BTC ' '(0.0231Σ%). Avg duration 100.0 mins.', 'params': optimizer_param } From 52da64b6dcc9b07f9cdf0e7b9828c1631c6a110f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 14:33:26 +0200 Subject: [PATCH 354/928] Align configuration files --- config_binance.json.example | 2 +- config_full.json.example | 2 +- config_kraken.json.example | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config_binance.json.example b/config_binance.json.example index ab57db88f..1d492fc3c 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -11,8 +11,8 @@ "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, diff --git a/config_full.json.example b/config_full.json.example index 806b5f718..4c4ad3c58 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -22,8 +22,8 @@ "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index 7a47b701f..ea3677b2d 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -5,15 +5,14 @@ "fiat_display_currency": "EUR", "ticker_interval" : "5m", "dry_run": true, - "db_url": "sqlite:///tradesv3.dryrun.sqlite", "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -60,8 +59,8 @@ }, "telegram": { "enabled": false, - "token": "", - "chat_id": "" + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" }, "initial_state": "running", "forcebuy_enable": false, From 131b232155c4c049adafc450e2b14ca7c6d57396 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 14:33:35 +0200 Subject: [PATCH 355/928] Add sample for order_types in config (slightly different syntax) --- docs/configuration.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index dc623a728..df116b3c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -191,14 +191,28 @@ If this is configured, all 4 values (`buy`, `sell`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start. The below is the default which is used if this is not configured in either strategy or configuration file. +Syntax for Strategy: + ```python -"order_types": { +order_types = { "buy": "limit", "sell": "limit", "stoploss": "market", "stoploss_on_exchange": False, "stoploss_on_exchange_interval": 60 -}, +} +``` + +Configuration: + +```json +"order_types": { + "buy": "limit", + "sell": "limit", + "stoploss": "market", + "stoploss_on_exchange": false, + "stoploss_on_exchange_interval": 60 +} ``` !!! Note From 22f902f0f77d4ee9e92d2e368b55c3057809708f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 11 May 2019 12:41:06 +0000 Subject: [PATCH 356/928] Update ccxt from 1.18.516 to 1.18.519 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 61c753784..41e79eeda 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.516 +ccxt==1.18.519 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 652914a67b2237c7fe2c04857d17fd5b16220d61 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 11 May 2019 12:41:07 +0000 Subject: [PATCH 357/928] Update python-rapidjson from 0.7.0 to 0.7.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 41e79eeda..6e5c1ea8a 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -23,7 +23,7 @@ filelock==3.0.10 py_find_1st==1.1.3 #Load ticker files 30% faster -python-rapidjson==0.7.0 +python-rapidjson==0.7.1 # Notify systemd sdnotify==0.3.2 From 46b1ecc77d53054aed6835d69f9989316422aa78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 15:27:09 +0200 Subject: [PATCH 358/928] Fix #1840 - Support balances other than USDT --- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/tests/rpc/test_rpc_telegram.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 37312318a..2189a0d17 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -276,11 +276,12 @@ class RPC(object): rate = 1.0 else: try: - if coin == 'USDT': - rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False) + if coin in('USDT', 'USD', 'EUR'): + rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False) else: rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) except (TemporaryError, DependencyException): + logger.warning(f" Could not get rate for pair {coin}.") continue est_btc: float = rate * balance['total'] total = total + est_btc diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 86d728290..69e3006cd 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -522,6 +522,11 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'total': 1.0, 'free': 1.0, 'used': 0.0 + }, + 'EUR': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 } } @@ -565,6 +570,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: assert '*BTC:*' in result assert '*ETH:*' not in result assert '*USDT:*' in result + assert '*EUR:*' in result assert 'Balance:' in result assert 'Est. BTC:' in result assert 'BTC: 12.00000000' in result From dccd6b4a91b2cc0ee34f9cf3386872b1e8121013 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 12 May 2019 12:41:05 +0000 Subject: [PATCH 359/928] Update ccxt from 1.18.519 to 1.18.522 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 6e5c1ea8a..41f1b6218 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.519 +ccxt==1.18.522 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 11dca0bd29c04aac339698e53a9a68d53bd220ea Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 12 May 2019 12:41:06 +0000 Subject: [PATCH 360/928] Update pytest from 4.4.2 to 4.5.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3c99a87f6..fa52a4869 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.4.2 +pytest==4.5.0 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 00b4501c598e23592ddc3f86e73e1ef487a6e469 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 12 May 2019 21:14:00 +0300 Subject: [PATCH 361/928] avg profit and total profit corrected (to be %, not ratio); comments cleaned up a bit; typo in the log msg fixed --- freqtrade/optimize/hyperopt.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 06c7ae495..252b76252 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -63,9 +63,11 @@ class Hyperopt(Backtesting): # if eval ends with higher value, we consider it a failed eval self.max_accepted_trade_duration = 300 - # this is expexted avg profit * expected trade count - # for example 3.5%, 1100 trades, self.expected_max_profit = 3.85 - # check that the reported Σ% values do not exceed this! + # This is assumed to be expected avg profit * expected trade count. + # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, + # self.expected_max_profit = 3.85 + # Check that the reported Σ% values do not exceed this! + # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 # Previous evaluations @@ -211,8 +213,8 @@ class Hyperopt(Backtesting): trade_count = len(results.index) trade_duration = results.trade_duration.mean() - # If this evaluation contains too short small amount of trades - # to be interesting -- consider it as 'bad' (assign max. loss value) + # If this evaluation contains too short amount of trades to be + # interesting -- consider it as 'bad' (assigned max. loss value) # in order to cast this hyperspace point away from optimization # path. We do not want to optimize 'hodl' strategies. if trade_count < self.config['hyperopt_min_trades']: @@ -235,15 +237,15 @@ class Hyperopt(Backtesting): Return the format result in a string """ trades = len(results.index) - avg_profit = results.profit_percent.mean() + avg_profit = results.profit_percent.mean() * 100.0 total_profit = results.profit_abs.sum() stake_cur = self.config['stake_currency'] - profit = results.profit_percent.sum() + profit = results.profit_percent.sum() * 100.0 duration = results.trade_duration.mean() - return (f'{trades:6d} trades. Avg profit {avg_profit: 9.6f}%. ' + return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' f'Total profit {total_profit: 11.8f} {stake_cur} ' - f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.') + f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.') def get_optimizer(self, cpu_count) -> Optimizer: return Optimizer( @@ -318,7 +320,7 @@ class Hyperopt(Backtesting): }) logger.debug(f"Optimizer params: {f_val[j]['params']}") for j in range(jobs): - logger.debug(f"Opimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") + logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") except KeyboardInterrupt: print('User interrupted..') From 003461ec96648b061a4d92127cb6634833921413 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 12 May 2019 21:19:20 +0300 Subject: [PATCH 362/928] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 325713ba7..86c5f2a34 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -459,8 +459,8 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'result': ' 1 trades. Avg profit 0.023117%. Total profit 0.00023300 BTC ' - '(0.0231Σ%). Avg duration 100.0 mins.', + 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + '( 2.31Σ%). Avg duration 100.0 mins.', 'params': optimizer_param } From 600f660f5e0b4a05615c511f5dfe687853fa050a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 13 May 2019 12:41:06 +0000 Subject: [PATCH 363/928] Update ccxt from 1.18.522 to 1.18.523 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 41f1b6218..4f7309a6a 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.522 +ccxt==1.18.523 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 1cd98665def8e19cece3f8f9c89d82265ee528a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 May 2019 19:50:56 +0200 Subject: [PATCH 364/928] Update pyup only weekly --- .pyup.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.pyup.yml b/.pyup.yml index 3494a3fd3..b1b721113 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -11,7 +11,10 @@ update: all # allowed: True, False pin: True -schedule: "every day" +# update schedule +# default: empty +# allowed: "every day", "every week", .. +schedule: "every week" search: False From 5677c4882edd9faa39efa5df5d832bed5a6b76c2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 13 May 2019 23:56:59 +0300 Subject: [PATCH 365/928] minor: add ticker data validation; log backtesting interval --- freqtrade/optimize/hyperopt.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 252b76252..5b8797d99 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -24,7 +24,8 @@ from freqtrade import DependencyException from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.data.history import load_data -from freqtrade.optimize import get_timeframe +from freqtrade.exchange import timeframe_to_minutes +from freqtrade.optimize import get_timeframe, validate_backtest_data from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.resolvers import HyperOptResolver @@ -282,9 +283,25 @@ class Hyperopt(Backtesting): timerange=timerange ) + if not data: + logger.critical("No data found. Terminating.") + return + + min_date, max_date = get_timeframe(data) + # Validate dataframe for missing values (mainly at start and end, as fillup is called) + validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes(self.ticker_interval)) + logger.info( + 'Backtesting data from %s up to %s (%s days)..', + min_date.isoformat(), + max_date.isoformat(), + (max_date - min_date).days + ) + if self.has_space('buy') or self.has_space('sell'): self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore + dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) # We don't need exchange instance anymore while running hyperopt From 90a52e46021fb100e5e298d8b495ee407fed77f4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 14 May 2019 09:23:09 +0300 Subject: [PATCH 366/928] tests adjusted; new test_start_no_data() added for hyperopt --- freqtrade/tests/optimize/test_hyperopt.py | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 86c5f2a34..f50f58e5b 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -195,6 +195,33 @@ def test_start(mocker, default_conf, caplog) -> None: assert start_mock.call_count == 1 +def test_start_no_data(mocker, default_conf, caplog) -> None: + mocker.patch( + 'freqtrade.configuration.Configuration._load_config_file', + lambda *args, **kwargs: default_conf + ) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + patch_exchange(mocker) + + args = [ + '--config', 'config.json', + 'hyperopt', + '--epochs', '5' + ] + args = get_args(args) + start(args) + + import pprint + pprint.pprint(caplog.record_tuples) + + assert log_has('No data found. Terminating.', caplog.record_tuples) + + def test_start_failure(mocker, default_conf, caplog) -> None: start_mock = MagicMock() mocker.patch( @@ -310,6 +337,11 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) 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, 'result': 'foo result', 'params': {}}]) From 8b95e12468474b9c25258d921491e08eab307b27 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 15 May 2019 12:05:35 +0300 Subject: [PATCH 367/928] log message adjusted in backtesting and hyperopt --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eefbd4e04..51122cfb2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -445,7 +445,7 @@ class Backtesting(object): optimize.validate_backtest_data(data, min_date, max_date, timeframe_to_minutes(self.ticker_interval)) logger.info( - 'Measuring data from %s up to %s (%s days)..', + 'Backtesting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5b8797d99..92589aed2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -292,7 +292,7 @@ class Hyperopt(Backtesting): validate_backtest_data(data, min_date, max_date, timeframe_to_minutes(self.ticker_interval)) logger.info( - 'Backtesting data from %s up to %s (%s days)..', + 'Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 02f8840e2..6a39deed4 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -495,7 +495,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: 'Using local backtesting data (using whitelist in given config) ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Measuring data from 2017-11-14T21:17:00+00:00 ' + 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' 'up to 2017-11-14T22:59:00+00:00 (0 days)..' ] for line in exists: @@ -858,7 +858,8 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Downloading data for all pairs in whitelist ...', - 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' ] @@ -916,7 +917,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Downloading data for all pairs in whitelist ...', - 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategy', From 2741c5c3307ffb1a89e0815ba5047d9e13403d82 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 16 May 2019 22:38:59 +0300 Subject: [PATCH 368/928] inherit freqtrade exceptions from Exception i.o. BaseException --- freqtrade/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f84f5240e..340d5b515 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -2,14 +2,14 @@ __version__ = '0.18.5-dev' -class DependencyException(BaseException): +class DependencyException(Exception): """ - Indicates that a assumed dependency is not met. + Indicates that an assumed dependency is not met. This could happen when there is currently not enough money on the account. """ -class OperationalException(BaseException): +class OperationalException(Exception): """ Requires manual intervention. This happens when an exchange returns an unexpected error during runtime @@ -17,7 +17,7 @@ class OperationalException(BaseException): """ -class InvalidOrderException(BaseException): +class InvalidOrderException(Exception): """ This is returned when the order is not valid. Example: If stoploss on exchange order is hit, then trying to cancel the order @@ -25,7 +25,7 @@ class InvalidOrderException(BaseException): """ -class TemporaryError(BaseException): +class TemporaryError(Exception): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user From e2b83624a397b85807d23f7b5353ade9727e54a3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 17 May 2019 19:05:36 +0300 Subject: [PATCH 369/928] data/history cleanup --- freqtrade/data/history.py | 38 ++++++++++++++++------------ freqtrade/tests/data/test_history.py | 29 ++++++++++++++++----- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 4dba1b760..86d3c3071 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -90,13 +90,8 @@ def load_pair_history(pair: str, :return: DataFrame with ohlcv data """ - # If the user force the refresh of pairs + # The user forced the refresh of pairs if refresh_pairs: - if not exchange: - raise OperationalException("Exchange needs to be initialized when " - "calling load_data with refresh_pairs=True") - - logger.info('Download data for pair and store them in %s', datadir) download_pair_history(datadir=datadir, exchange=exchange, pair=pair, @@ -115,10 +110,11 @@ def load_pair_history(pair: str, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing) else: - logger.warning('No data for pair: "%s", Interval: %s. ' - 'Use --refresh-pairs-cached option or download_backtest_data.py ' - 'script to download the data', - pair, ticker_interval) + logger.warning( + f'No history data for pair: "{pair}", interval: {ticker_interval}. ' + 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'script to download the data' + ) return None @@ -190,7 +186,7 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str, def download_pair_history(datadir: Optional[Path], - exchange: Exchange, + exchange: Optional[Exchange], pair: str, ticker_interval: str = '5m', timerange: Optional[TimeRange] = None) -> bool: @@ -201,18 +197,26 @@ def download_pair_history(datadir: Optional[Path], the full data will be redownloaded Based on @Rybolov work: https://github.com/rybolov/freqtrade-data + :param pair: pair to download :param ticker_interval: ticker interval :param timerange: range of time to download :return: bool with success state - """ + if not exchange: + raise OperationalException( + "Exchange needs to be initialized when downloading pair history data" + ) + try: path = make_testdata_path(datadir) filepair = pair.replace("/", "_") filename = path.joinpath(f'{filepair}-{ticker_interval}.json') - logger.info('Download the pair: "%s", Interval: %s', 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) @@ -231,7 +235,9 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True - except BaseException: - logger.info('Failed to download the pair: "%s", Interval: %s', - pair, ticker_interval) + + except Exception: + logger.error( + f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}.' + ) return False diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 14ec99042..15442f577 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -59,7 +59,11 @@ def _clean_test_file(file: str) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None: ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=None) assert isinstance(ld, DataFrame) - assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples) + assert not log_has( + 'Download history data for pair: "UNITTEST/BTC", interval: 30m ' + 'and store in None.', + caplog.record_tuples + ) def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: @@ -67,7 +71,7 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert not isinstance(ld, DataFrame) assert ld is None assert log_has( - 'No data for pair: "UNITTEST/BTC", Interval: 7m. ' + 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' 'Use --refresh-pairs-cached option or download_backtest_data.py ' 'script to download the data', caplog.record_tuples @@ -80,7 +84,11 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: _backup_file(file, copy_file=True) history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC']) assert os.path.isfile(file) is True - assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples) + assert not log_has( + 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' + 'and store in None.', + caplog.record_tuples + ) _clean_test_file(file) @@ -100,7 +108,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau pair='MEME/BTC') assert os.path.isfile(file) is False assert log_has( - 'No data for pair: "MEME/BTC", Interval: 1m. ' + 'No history data for pair: "MEME/BTC", interval: 1m. ' 'Use --refresh-pairs-cached option or download_backtest_data.py ' 'script to download the data', caplog.record_tuples @@ -113,7 +121,11 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau exchange=exchange, pair='MEME/BTC') assert os.path.isfile(file) is True - assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples) + assert log_has( + 'Download history data for pair: "MEME/BTC", interval: 1m ' + 'and store in None.', + caplog.record_tuples + ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): history.load_pair_history(datadir=None, ticker_interval='1m', @@ -293,7 +305,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', - side_effect=BaseException('File Error')) + side_effect=Exception('File Error')) exchange = get_patched_exchange(mocker, default_conf) @@ -308,7 +320,10 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) - assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples) + assert log_has( + 'Failed to download history data for pair: "MEME/BTC", interval: 1m.', + caplog.record_tuples + ) def test_load_tickerdata_file() -> None: From c3c745ca19f0bb2885bae9fa1858c731564498a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:08:24 +0200 Subject: [PATCH 370/928] Get new files from old branch --- freqtrade/rpc/api_server.py | 203 +++++++++++++++++++++++++++++ freqtrade/rpc/api_server_common.py | 74 +++++++++++ freqtrade/rpc/rest_client.py | 47 +++++++ 3 files changed, 324 insertions(+) create mode 100644 freqtrade/rpc/api_server.py create mode 100644 freqtrade/rpc/api_server_common.py create mode 100755 freqtrade/rpc/rest_client.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py new file mode 100644 index 000000000..1055a0553 --- /dev/null +++ b/freqtrade/rpc/api_server.py @@ -0,0 +1,203 @@ +import json +import threading +import logging +# import json +from typing import Dict + +from flask import Flask, request +# from flask_restful import Resource, Api +from freqtrade.rpc.rpc import RPC, RPCException +from ipaddress import IPv4Address + + +logger = logging.getLogger(__name__) +app = Flask(__name__) + + +class ApiServer(RPC): + """ + This class runs api server and provides rpc.rpc functionality to it + + This class starts a none blocking thread the api server runs within + """ + + def __init__(self, freqtrade) -> None: + """ + Init the api server, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + + self._config = freqtrade.config + + # Register application handling + self.register_rest_other() + self.register_rest_rpc_urls() + + thread = threading.Thread(target=self.run, daemon=True) + thread.start() + + def register_rest_other(self): + """ + Registers flask app URLs that are not calls to functionality in rpc.rpc. + :return: + """ + app.register_error_handler(404, self.page_not_found) + app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + + def register_rest_rpc_urls(self): + """ + Registers flask app URLs that are calls to functonality in rpc.rpc. + + First two arguments passed are /URL and 'Label' + Label can be used as a shortcut when refactoring + :return: + """ + app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) + app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) + app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) + app.add_url_rule('/status_table', 'status_table', + view_func=self.status_table, methods=['GET']) + + def run(self): + """ Method that runs flask app in its own thread forever """ + + """ + Section to handle configuration and running of the Rest server + also to check and warn if not bound to a loopback, warn on security risk. + """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] + + logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) + if not IPv4Address(rest_ip).is_loopback: + logger.info("SECURITY WARNING - Local Rest Server listening to external connections") + logger.info("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + # Run the Server + logger.info('Starting Local Rest Server') + try: + app.run(host=rest_ip, port=rest_port) + except Exception: + logger.exception("Api server failed to start, exception message is:") + + def cleanup(self) -> None: + pass + + def send_msg(self, msg: Dict[str, str]) -> None: + pass + + """ + Define the application methods here, called by app.add_url_rule + each Telegram command should have a like local substitute + """ + + def page_not_found(self, error): + """ + Return "404 not found", 404. + """ + return json.dumps({ + 'status': 'error', + 'reason': '''There's no API call for %s''' % request.base_url, + 'code': 404 + }), 404 + + def hello(self): + """ + None critical but helpful default index page. + + That lists URLs added to the flask server. + This may be deprecated at any time. + :return: index.html + """ + rest_cmds = 'Commands implemented:
' \ + 'Show 7 days of stats' \ + '
' \ + 'Stop the Trade thread' \ + '
' \ + 'Start the Traded thread' \ + '
' \ + 'Show profit summary' \ + '
' \ + 'Show status table - Open trades' \ + '
' \ + ' 404 page does not exist' \ + '
' + + return rest_cmds + + def daily(self): + """ + Returns the last X days trading stats summary. + + :return: stats + """ + try: + timescale = request.args.get('timescale') + logger.info("LocalRPC - Daily Command Called") + timescale = int(timescale) + + stats = self._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return json.dumps(stats, indent=4, sort_keys=True, default=str) + except RPCException as e: + logger.exception("API Error querying daily:", e) + return "Error querying daily" + + def profit(self): + """ + Handler for /profit. + + Returns a cumulative profit statistics + :return: stats + """ + try: + logger.info("LocalRPC - Profit Command Called") + + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return json.dumps(stats, indent=4, sort_keys=True, default=str) + except RPCException as e: + logger.exception("API Error calling profit", e) + return "Error querying closed trades - maybe there are none" + + def status_table(self): + """ + Handler for /status table. + + Returns the current TradeThread status in table format + :return: results + """ + try: + results = self._rpc_trade_status() + return json.dumps(results, indent=4, sort_keys=True, default=str) + + except RPCException as e: + logger.exception("API Error calling status table", e) + return "Error querying open trades - maybe there are none." + + def start(self): + """ + Handler for /start. + + Starts TradeThread in bot if stopped. + """ + msg = self._rpc_start() + return json.dumps(msg) + + def stop(self): + """ + Handler for /stop. + + Stops TradeThread in bot if running + """ + msg = self._rpc_stop() + return json.dumps(msg) diff --git a/freqtrade/rpc/api_server_common.py b/freqtrade/rpc/api_server_common.py new file mode 100644 index 000000000..19338a825 --- /dev/null +++ b/freqtrade/rpc/api_server_common.py @@ -0,0 +1,74 @@ +import logging +import flask +from flask import request, jsonify + +logger = logging.getLogger(__name__) + + +class MyApiApp(flask.Flask): + def __init__(self, import_name): + """ + Contains common rest routes and resource that do not need + to access to rpc.rpc functionality + """ + super(MyApiApp, self).__init__(import_name) + + """ + Registers flask app URLs that are not calls to functionality in rpc.rpc. + :return: + """ + self.before_request(self.my_preprocessing) + self.register_error_handler(404, self.page_not_found) + self.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + self.add_url_rule('/stop_api', 'stop_api', view_func=self.stop_api, methods=['GET']) + + def my_preprocessing(self): + # Do stuff to flask.request + pass + + def page_not_found(self, error): + # Return "404 not found", 404. + return jsonify({'status': 'error', + 'reason': '''There's no API call for %s''' % request.base_url, + 'code': 404}), 404 + + def hello(self): + """ + None critical but helpful default index page. + + That lists URLs added to the flask server. + This may be deprecated at any time. + :return: index.html + """ + rest_cmds = 'Commands implemented:
' \ + 'Show 7 days of stats' \ + '
' \ + 'Stop the Trade thread' \ + '
' \ + 'Start the Traded thread' \ + '
' \ + ' 404 page does not exist' \ + '
' \ + '
' \ + 'Shut down the api server - be sure' + return rest_cmds + + def stop_api(self): + """ For calling shutdown_api_server over via api server HTTP""" + self.shutdown_api_server() + return 'Api Server shutting down... ' + + def shutdown_api_server(self): + """ + Stop the running flask application + + Records the shutdown in logger.info + :return: + """ + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running the Flask Werkzeug Server') + if func is not None: + logger.info('Stopping the Local Rest Server') + func() + return diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py new file mode 100755 index 000000000..cabedebb8 --- /dev/null +++ b/freqtrade/rpc/rest_client.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Simple command line client into RPC commands +Can be used as an alternate to Telegram +""" + +import time +from requests import get +from sys import argv + +# TODO - use argparse to clean this up +# TODO - use IP and Port from config.json not hardcode + +if len(argv) == 1: + print('\nThis script accepts the following arguments') + print('- daily (int) - Where int is the number of days to report back. daily 3') + print('- start - this will start the trading thread') + print('- stop - this will start the trading thread') + print('- there will be more....\n') + +if len(argv) == 3 and argv[1] == "daily": + if str.isnumeric(argv[2]): + get_url = 'http://localhost:5002/daily?timescale=' + argv[2] + d = get(get_url).json() + print(d) + else: + print("\nThe second argument to daily must be an integer, 1,2,3 etc") + +if len(argv) == 2 and argv[1] == "start": + get_url = 'http://localhost:5002/start' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) + +if len(argv) == 2 and argv[1] == "stop": + get_url = 'http://localhost:5002/stop' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) From 26c42bd5590e70356e59963425c990cf0c379198 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:12:58 +0200 Subject: [PATCH 371/928] Add apiserver tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 freqtrade/tests/rpc/test_rpc_apiserver.py diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py new file mode 100644 index 000000000..14c35a38e --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -0,0 +1,52 @@ +""" +Unit test file for rpc/api_server.py +""" + +from unittest.mock import MagicMock + +from freqtrade.rpc.api_server import ApiServer +from freqtrade.state import State +from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver + + +def test__init__(default_conf, mocker): + """ + Test __init__() method + """ + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + assert apiserver._config == default_conf + + +def test_start_endpoint(default_conf, mocker): + """Test /start endpoint""" + patch_apiserver(mocker) + bot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(bot) + + bot.state = State.STOPPED + assert bot.state == State.STOPPED + result = apiserver.start() + assert result == '{"status": "starting trader ..."}' + assert bot.state == State.RUNNING + + result = apiserver.start() + assert result == '{"status": "already running"}' + + +def test_stop_endpoint(default_conf, mocker): + """Test /stop endpoint""" + patch_apiserver(mocker) + bot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(bot) + + bot.state = State.RUNNING + assert bot.state == State.RUNNING + result = apiserver.stop() + assert result == '{"status": "stopping trader ..."}' + assert bot.state == State.STOPPED + + result = apiserver.stop() + assert result == '{"status": "already stopped"}' From 6f67ea44dce5e81b3ebf3cd5e29f3d5e82459725 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:14 +0200 Subject: [PATCH 372/928] Enable config-check for rest server --- freqtrade/constants.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 619508e73..1b06eb726 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,19 @@ CONF_SCHEMA = { 'webhookstatus': {'type': 'object'}, }, }, + 'api_server': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'listen_ip_address': {'format': 'ipv4'}, + 'listen_port': { + 'type': 'integer', + "minimum": 1024, + "maximum": 65535 + }, + }, + 'required': ['enabled', 'listen_ip_address', 'listen_port'] + }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'forcebuy_enable': {'type': 'boolean'}, From ef2950bca2210dd2873534d62ba02a19b89f9b63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:40 +0200 Subject: [PATCH 373/928] Load api-server in rpc_manager --- freqtrade/rpc/rpc.py | 5 +++++ freqtrade/rpc/rpc_manager.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2189a0d17..5b78c8356 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -48,6 +48,11 @@ class RPCException(Exception): def __str__(self): return self.message + def __json__(self): + return { + 'msg': self.message + } + class RPC(object): """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7f0d0a5d4..fad532aa0 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -29,6 +29,12 @@ class RPCManager(object): from freqtrade.rpc.webhook import Webhook self.registered_modules.append(Webhook(freqtrade)) + # Enable local rest api server for cmd line control + if freqtrade.config.get('api_server', {}).get('enabled', False): + logger.info('Enabling rpc.api_server') + from freqtrade.rpc.api_server import ApiServer + self.registered_modules.append(ApiServer(freqtrade)) + def cleanup(self) -> None: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') From 68743012e400cde9e04a434e385eeceeded237c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:55 +0200 Subject: [PATCH 374/928] Patch api server for tests --- freqtrade/tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0bff1d5e9..98563a374 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -134,6 +134,15 @@ def patch_coinmarketcap(mocker) -> None: ) +def patch_apiserver(mocker) -> None: + mocker.patch.multiple( + 'freqtrade.rpc.api_server.ApiServer', + run=MagicMock(), + register_rest_other=MagicMock(), + register_rest_rpc_urls=MagicMock(), + ) + + @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ From 9d95ae934190ce72435dae7f1ce819de77f0bc3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 19:34:05 +0200 Subject: [PATCH 375/928] Add flask to dependencies --- requirements-common.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-common.txt b/requirements-common.txt index 4f7309a6a..9e3e2816c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -27,3 +27,6 @@ python-rapidjson==0.7.1 # Notify systemd sdnotify==0.3.2 + +# Api server +flask==1.0.2 From 6bb2fad9b07e2ffd64c2e3b22a9ab77f5f815224 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 19:34:19 +0200 Subject: [PATCH 376/928] Reorder some things --- freqtrade/rpc/api_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 1055a0553..46678466d 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -38,6 +38,12 @@ class ApiServer(RPC): thread = threading.Thread(target=self.run, daemon=True) thread.start() + def cleanup(self) -> None: + pass + + def send_msg(self, msg: Dict[str, str]) -> None: + pass + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -84,12 +90,6 @@ class ApiServer(RPC): except Exception: logger.exception("Api server failed to start, exception message is:") - def cleanup(self) -> None: - pass - - def send_msg(self, msg: Dict[str, str]) -> None: - pass - """ Define the application methods here, called by app.add_url_rule each Telegram command should have a like local substitute From 96a260b027075809d4fc09e351a5f1a4733d36bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 20:21:26 +0200 Subject: [PATCH 377/928] rest_dump --- freqtrade/rpc/api_server.py | 43 +++++++++-------- freqtrade/rpc/api_server_common.py | 74 ------------------------------ 2 files changed, 21 insertions(+), 96 deletions(-) delete mode 100644 freqtrade/rpc/api_server_common.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 46678466d..a0532a3b3 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -4,7 +4,7 @@ import logging # import json from typing import Dict -from flask import Flask, request +from flask import Flask, request, jsonify # from flask_restful import Resource, Api from freqtrade.rpc.rpc import RPC, RPCException from ipaddress import IPv4Address @@ -39,11 +39,15 @@ class ApiServer(RPC): thread.start() def cleanup(self) -> None: - pass + logger.info("Stopping API Server") def send_msg(self, msg: Dict[str, str]) -> None: pass + def rest_dump(self, return_value): + """ Helper function to jsonify object for a webserver """ + return jsonify(return_value) + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -89,6 +93,7 @@ class ApiServer(RPC): app.run(host=rest_ip, port=rest_port) except Exception: logger.exception("Api server failed to start, exception message is:") + logger.info('Starting Local Rest Server_end') """ Define the application methods here, called by app.add_url_rule @@ -99,7 +104,7 @@ class ApiServer(RPC): """ Return "404 not found", 404. """ - return json.dumps({ + return self.rest_dump({ 'status': 'error', 'reason': '''There's no API call for %s''' % request.base_url, 'code': 404 @@ -113,20 +118,14 @@ class ApiServer(RPC): This may be deprecated at any time. :return: index.html """ - rest_cmds = 'Commands implemented:
' \ - 'Show 7 days of stats' \ - '
' \ - 'Stop the Trade thread' \ - '
' \ - 'Start the Traded thread' \ - '
' \ - 'Show profit summary' \ - '
' \ - 'Show status table - Open trades' \ - '
' \ - ' 404 page does not exist' \ - '
' - + rest_cmds = ('Commands implemented:
' + 'Show 7 days of stats
' + 'Stop the Trade thread
' + 'Start the Traded thread
' + 'Show profit summary
' + 'Show status table - Open trades
' + ' 404 page does not exist
' + ) return rest_cmds def daily(self): @@ -145,7 +144,7 @@ class ApiServer(RPC): self._config['fiat_display_currency'] ) - return json.dumps(stats, indent=4, sort_keys=True, default=str) + return self.rest_dump(stats) except RPCException as e: logger.exception("API Error querying daily:", e) return "Error querying daily" @@ -164,7 +163,7 @@ class ApiServer(RPC): self._config['fiat_display_currency'] ) - return json.dumps(stats, indent=4, sort_keys=True, default=str) + return self.rest_dump(stats) except RPCException as e: logger.exception("API Error calling profit", e) return "Error querying closed trades - maybe there are none" @@ -178,7 +177,7 @@ class ApiServer(RPC): """ try: results = self._rpc_trade_status() - return json.dumps(results, indent=4, sort_keys=True, default=str) + return self.rest_dump(results) except RPCException as e: logger.exception("API Error calling status table", e) @@ -191,7 +190,7 @@ class ApiServer(RPC): Starts TradeThread in bot if stopped. """ msg = self._rpc_start() - return json.dumps(msg) + return self.rest_dump(msg) def stop(self): """ @@ -200,4 +199,4 @@ class ApiServer(RPC): Stops TradeThread in bot if running """ msg = self._rpc_stop() - return json.dumps(msg) + return self.rest_dump(msg) diff --git a/freqtrade/rpc/api_server_common.py b/freqtrade/rpc/api_server_common.py deleted file mode 100644 index 19338a825..000000000 --- a/freqtrade/rpc/api_server_common.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -import flask -from flask import request, jsonify - -logger = logging.getLogger(__name__) - - -class MyApiApp(flask.Flask): - def __init__(self, import_name): - """ - Contains common rest routes and resource that do not need - to access to rpc.rpc functionality - """ - super(MyApiApp, self).__init__(import_name) - - """ - Registers flask app URLs that are not calls to functionality in rpc.rpc. - :return: - """ - self.before_request(self.my_preprocessing) - self.register_error_handler(404, self.page_not_found) - self.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) - self.add_url_rule('/stop_api', 'stop_api', view_func=self.stop_api, methods=['GET']) - - def my_preprocessing(self): - # Do stuff to flask.request - pass - - def page_not_found(self, error): - # Return "404 not found", 404. - return jsonify({'status': 'error', - 'reason': '''There's no API call for %s''' % request.base_url, - 'code': 404}), 404 - - def hello(self): - """ - None critical but helpful default index page. - - That lists URLs added to the flask server. - This may be deprecated at any time. - :return: index.html - """ - rest_cmds = 'Commands implemented:
' \ - 'Show 7 days of stats' \ - '
' \ - 'Stop the Trade thread' \ - '
' \ - 'Start the Traded thread' \ - '
' \ - ' 404 page does not exist' \ - '
' \ - '
' \ - 'Shut down the api server - be sure' - return rest_cmds - - def stop_api(self): - """ For calling shutdown_api_server over via api server HTTP""" - self.shutdown_api_server() - return 'Api Server shutting down... ' - - def shutdown_api_server(self): - """ - Stop the running flask application - - Records the shutdown in logger.info - :return: - """ - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running the Flask Werkzeug Server') - if func is not None: - logger.info('Stopping the Local Rest Server') - func() - return From c6c2893e2cef4a058a4d5adc2c4e13755e44d876 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 21:07:44 +0200 Subject: [PATCH 378/928] Improve rest-client interface --- freqtrade/rpc/rest_client.py | 96 ++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index cabedebb8..5ae65dc4a 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -2,46 +2,80 @@ """ Simple command line client into RPC commands Can be used as an alternate to Telegram + +Should not import anything from freqtrade, +so it can be used as a standalone script. """ +import argparse +import logging import time -from requests import get from sys import argv -# TODO - use argparse to clean this up +import click + +from requests import get +from requests.exceptions import ConnectionError + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) +logger = logging.getLogger("ft_rest_client") + # TODO - use IP and Port from config.json not hardcode -if len(argv) == 1: - print('\nThis script accepts the following arguments') - print('- daily (int) - Where int is the number of days to report back. daily 3') - print('- start - this will start the trading thread') - print('- stop - this will start the trading thread') - print('- there will be more....\n') +COMMANDS_NO_ARGS = ["start", + "stop", + ] +COMMANDS_ARGS = ["daily", + ] -if len(argv) == 3 and argv[1] == "daily": - if str.isnumeric(argv[2]): - get_url = 'http://localhost:5002/daily?timescale=' + argv[2] - d = get(get_url).json() - print(d) - else: - print("\nThe second argument to daily must be an integer, 1,2,3 etc") +SERVER_URL = "http://localhost:5002" -if len(argv) == 2 and argv[1] == "start": - get_url = 'http://localhost:5002/start' - d = get(get_url).text - print(d) - if "already" not in d: - time.sleep(2) - d = get(get_url).text - print(d) +def add_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("command", + help="Positional argument defining the command to execute.") + args = parser.parse_args() + # if len(argv) == 1: + # print('\nThis script accepts the following arguments') + # print('- daily (int) - Where int is the number of days to report back. daily 3') + # print('- start - this will start the trading thread') + # print('- stop - this will start the trading thread') + # print('- there will be more....\n') + return vars(args) -if len(argv) == 2 and argv[1] == "stop": - get_url = 'http://localhost:5002/stop' - d = get(get_url).text - print(d) - if "already" not in d: - time.sleep(2) - d = get(get_url).text - print(d) +def call_authorized(url): + try: + return get(url).json() + except ConnectionError: + logger.warning("Connection error") + + +def call_command_noargs(command): + logger.info(f"Running command `{command}` at {SERVER_URL}") + r = call_authorized(f"{SERVER_URL}/{command}") + logger.info(r) + + +def main(args): + + # Call commands without arguments + if args["command"] in COMMANDS_NO_ARGS: + call_command_noargs(args["command"]) + + if args["command"] == "daily": + if str.isnumeric(argv[2]): + get_url = SERVER_URL + '/daily?timescale=' + argv[2] + d = get(get_url).json() + print(d) + else: + print("\nThe second argument to daily must be an integer, 1,2,3 etc") + + +if __name__ == "__main__": + args = add_arguments() + main(args) From 8993882dcb1434b0f938ac45eee23cb4e4c5f915 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Apr 2019 06:39:33 +0200 Subject: [PATCH 379/928] Sort imports --- freqtrade/rpc/api_server.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a0532a3b3..20850a3a1 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,14 +1,11 @@ -import json -import threading import logging -# import json +import threading +from ipaddress import IPv4Address from typing import Dict -from flask import Flask, request, jsonify -# from flask_restful import Resource, Api -from freqtrade.rpc.rpc import RPC, RPCException -from ipaddress import IPv4Address +from flask import Flask, jsonify, request +from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) app = Flask(__name__) @@ -42,6 +39,7 @@ class ApiServer(RPC): logger.info("Stopping API Server") def send_msg(self, msg: Dict[str, str]) -> None: + """We don't push to endpoints at the moment. Look at webhooks for that.""" pass def rest_dump(self, return_value): From 3cf6c6ee0c4ed3c346b80983789ee2acbc192750 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 19:58:00 +0200 Subject: [PATCH 380/928] Implement a few more methods --- freqtrade/rpc/api_server.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 20850a3a1..53520025b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -37,6 +37,8 @@ class ApiServer(RPC): def cleanup(self) -> None: logger.info("Stopping API Server") + # TODO: Gracefully shutdown - right now it'll fail on /reload_conf + # since it's not terminated correctly. def send_msg(self, msg: Dict[str, str]) -> None: """We don't push to endpoints at the moment. Look at webhooks for that.""" @@ -62,8 +64,13 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ - app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + # TODO: actions should not be GET... app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) + app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self.stopbuy, methods=['GET']) + app.add_url_rule('/reload_conf', 'reload_conf', view_func=self.reload_conf, + methods=['GET']) + app.add_url_rule('/count', 'count', view_func=self.count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) app.add_url_rule('/status_table', 'status_table', @@ -198,3 +205,31 @@ class ApiServer(RPC): """ msg = self._rpc_stop() return self.rest_dump(msg) + + def stopbuy(self): + """ + Handler for /stopbuy. + + Sets max_open_trades to 0 and gracefully sells all open trades + """ + msg = self._rpc_stopbuy() + return self.rest_dump(msg) + + def reload_conf(self): + """ + Handler for /reload_conf. + Triggers a config file reload + """ + msg = self._rpc_reload_conf() + return self.rest_dump(msg) + + def count(self): + """ + Handler for /count. + Returns the number of trades running + """ + try: + msg = self._rpc_count() + except RPCException as e: + msg = {"status": str(e)} + return self.rest_dump(msg) From 2f8088432c9f16423ea7a3625cf5128a12b478a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 20:08:01 +0200 Subject: [PATCH 381/928] All handlers should be private --- freqtrade/rpc/api_server.py | 119 +++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 53520025b..30771bdc7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -5,6 +5,7 @@ from typing import Dict from flask import Flask, jsonify, request +from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -65,16 +66,17 @@ class ApiServer(RPC): :return: """ # TODO: actions should not be GET... - app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) - app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self.stopbuy, methods=['GET']) - app.add_url_rule('/reload_conf', 'reload_conf', view_func=self.reload_conf, + app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) + app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['GET']) - app.add_url_rule('/count', 'count', view_func=self.count, methods=['GET']) - app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) - app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) + app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) + app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status_table', 'status_table', - view_func=self.status_table, methods=['GET']) + view_func=self._status_table, methods=['GET']) def run(self): """ Method that runs flask app in its own thread forever """ @@ -133,7 +135,56 @@ class ApiServer(RPC): ) return rest_cmds - def daily(self): + def _start(self): + """ + Handler for /start. + Starts TradeThread in bot if stopped. + """ + msg = self._rpc_start() + return self.rest_dump(msg) + + def _stop(self): + """ + Handler for /stop. + Stops TradeThread in bot if running + """ + msg = self._rpc_stop() + return self.rest_dump(msg) + + def _stopbuy(self): + """ + Handler for /stopbuy. + Sets max_open_trades to 0 and gracefully sells all open trades + """ + msg = self._rpc_stopbuy() + return self.rest_dump(msg) + + def _version(self): + """ + Prints the bot's version + """ + return self.rest_dump({"version": __version__}) + + def _reload_conf(self): + """ + Handler for /reload_conf. + Triggers a config file reload + """ + msg = self._rpc_reload_conf() + return self.rest_dump(msg) + + def _count(self): + """ + Handler for /count. + Returns the number of trades running + """ + try: + msg = self._rpc_count() + except RPCException as e: + msg = {"status": str(e)} + return self.rest_dump(msg) + + def _daily(self): """ Returns the last X days trading stats summary. @@ -154,7 +205,7 @@ class ApiServer(RPC): logger.exception("API Error querying daily:", e) return "Error querying daily" - def profit(self): + def _profit(self): """ Handler for /profit. @@ -173,7 +224,7 @@ class ApiServer(RPC): logger.exception("API Error calling profit", e) return "Error querying closed trades - maybe there are none" - def status_table(self): + def _status_table(self): """ Handler for /status table. @@ -187,49 +238,3 @@ class ApiServer(RPC): except RPCException as e: logger.exception("API Error calling status table", e) return "Error querying open trades - maybe there are none." - - def start(self): - """ - Handler for /start. - - Starts TradeThread in bot if stopped. - """ - msg = self._rpc_start() - return self.rest_dump(msg) - - def stop(self): - """ - Handler for /stop. - - Stops TradeThread in bot if running - """ - msg = self._rpc_stop() - return self.rest_dump(msg) - - def stopbuy(self): - """ - Handler for /stopbuy. - - Sets max_open_trades to 0 and gracefully sells all open trades - """ - msg = self._rpc_stopbuy() - return self.rest_dump(msg) - - def reload_conf(self): - """ - Handler for /reload_conf. - Triggers a config file reload - """ - msg = self._rpc_reload_conf() - return self.rest_dump(msg) - - def count(self): - """ - Handler for /count. - Returns the number of trades running - """ - try: - msg = self._rpc_count() - except RPCException as e: - msg = {"status": str(e)} - return self.rest_dump(msg) From a12e093417b3bc60c7c20b6410798e4ef3e836be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Apr 2019 13:09:53 +0200 Subject: [PATCH 382/928] Api server - custom json encoder --- freqtrade/rpc/api_server.py | 50 ++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 30771bdc7..fe2367458 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -3,13 +3,31 @@ import threading from ipaddress import IPv4Address from typing import Dict +from arrow import Arrow from flask import Flask, jsonify, request +from flask.json import JSONEncoder from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) + + +class ArrowJSONEncoder(JSONEncoder): + def default(self, obj): + try: + if isinstance(obj, Arrow): + return obj.for_json() + iterable = iter(obj) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, obj) + + app = Flask(__name__) +app.json_encoder = ArrowJSONEncoder class ApiServer(RPC): @@ -42,13 +60,19 @@ class ApiServer(RPC): # since it's not terminated correctly. def send_msg(self, msg: Dict[str, str]) -> None: - """We don't push to endpoints at the moment. Look at webhooks for that.""" + """ + We don't push to endpoints at the moment. + Take a look at webhooks for that functionality. + """ pass def rest_dump(self, return_value): """ Helper function to jsonify object for a webserver """ return jsonify(return_value) + def rest_error(self, error_msg): + return jsonify({"error": error_msg}), 502 + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -75,8 +99,7 @@ class ApiServer(RPC): app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/status_table', 'status_table', - view_func=self._status_table, methods=['GET']) + app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) def run(self): """ Method that runs flask app in its own thread forever """ @@ -180,9 +203,9 @@ class ApiServer(RPC): """ try: msg = self._rpc_count() + return self.rest_dump(msg) except RPCException as e: - msg = {"status": str(e)} - return self.rest_dump(msg) + return self.rest_error(str(e)) def _daily(self): """ @@ -202,8 +225,8 @@ class ApiServer(RPC): return self.rest_dump(stats) except RPCException as e: - logger.exception("API Error querying daily:", e) - return "Error querying daily" + logger.exception("API Error querying daily: %s", e) + return self.rest_error(f"Error querying daily {e}") def _profit(self): """ @@ -221,20 +244,19 @@ class ApiServer(RPC): return self.rest_dump(stats) except RPCException as e: - logger.exception("API Error calling profit", e) - return "Error querying closed trades - maybe there are none" + logger.exception("API Error calling profit: %s", e) + return self.rest_error("Error querying closed trades - maybe there are none") - def _status_table(self): + def _status(self): """ Handler for /status table. - Returns the current TradeThread status in table format - :return: results + Returns the current status of the trades in json format """ try: results = self._rpc_trade_status() return self.rest_dump(results) except RPCException as e: - logger.exception("API Error calling status table", e) - return "Error querying open trades - maybe there are none." + logger.exception("API Error calling status table: %s", e) + return self.rest_error("Error querying open trades - maybe there are none.") From d8549fe09ab9bd5f9410149abc4b3fd780615603 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Apr 2019 13:22:44 +0200 Subject: [PATCH 383/928] add balance handler --- freqtrade/rpc/api_server.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index fe2367458..a07057997 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,16 +90,27 @@ class ApiServer(RPC): :return: """ # TODO: actions should not be GET... + # Actions to control the bot app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['GET']) + # Info commands + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) + app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + # TODO: Implement the following + # performance + # forcebuy + # forcesell + # whitelist + # blacklist + # edge + # help (?) def run(self): """ Method that runs flask app in its own thread forever """ @@ -258,5 +269,19 @@ class ApiServer(RPC): return self.rest_dump(results) except RPCException as e: - logger.exception("API Error calling status table: %s", e) + logger.exception("API Error calling status: %s", e) return self.rest_error("Error querying open trades - maybe there are none.") + + def _balance(self): + """ + Handler for /balance table. + + Returns the current status of the trades in json format + """ + try: + results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + return self.rest_dump(results) + + except RPCException as e: + logger.exception("API Error calling status table: %s", e) + return self.rest_error(f"{e}") From 01c93a2ee36d878289c3e90fe38df4888098c8df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 06:40:15 +0200 Subject: [PATCH 384/928] Load rest-client config from file --- freqtrade/rpc/rest_client.py | 40 ++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 5ae65dc4a..51c7a88f5 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -8,11 +8,10 @@ so it can be used as a standalone script. """ import argparse +import json import logging -import time from sys import argv - -import click +from pathlib import Path from requests import get from requests.exceptions import ConnectionError @@ -23,21 +22,27 @@ logging.basicConfig( ) logger = logging.getLogger("ft_rest_client") -# TODO - use IP and Port from config.json not hardcode COMMANDS_NO_ARGS = ["start", "stop", + "stopbuy", + "reload_conf" ] COMMANDS_ARGS = ["daily", ] -SERVER_URL = "http://localhost:5002" - def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + parser.add_argument('-c', '--config', + help='Specify configuration file (default: %(default)s). ', + dest='config', + type=str, + metavar='PATH', + default='config.json' + ) args = parser.parse_args() # if len(argv) == 1: # print('\nThis script accepts the following arguments') @@ -48,6 +53,14 @@ def add_arguments(): return vars(args) +def load_config(configfile): + file = Path(configfile) + if file.is_file(): + with file.open("r") as f: + config = json.load(f) + return config + + def call_authorized(url): try: return get(url).json() @@ -55,21 +68,26 @@ def call_authorized(url): logger.warning("Connection error") -def call_command_noargs(command): - logger.info(f"Running command `{command}` at {SERVER_URL}") - r = call_authorized(f"{SERVER_URL}/{command}") +def call_command_noargs(server_url, command): + logger.info(f"Running command `{command}` at {server_url}") + r = call_authorized(f"{server_url}/{command}") logger.info(r) def main(args): + config = load_config(args["config"]) + url = config.get("api_server", {}).get("server_url", "127.0.0.1") + port = config.get("api_server", {}).get("listen_port", "8080") + server_url = f"http://{url}:{port}" + # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - call_command_noargs(args["command"]) + call_command_noargs(server_url, args["command"]) if args["command"] == "daily": if str.isnumeric(argv[2]): - get_url = SERVER_URL + '/daily?timescale=' + argv[2] + get_url = server_url + '/daily?timescale=' + argv[2] d = get(get_url).json() print(d) else: From ae8660fe06f213d7e5017d6bceb0de84b2811f33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 06:59:07 +0200 Subject: [PATCH 385/928] Extract exception handling to decorator --- freqtrade/rpc/api_server.py | 85 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a07057997..06f4ad0b9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -37,6 +37,18 @@ class ApiServer(RPC): This class starts a none blocking thread the api server runs within """ + def safe_rpc(func): + + def func_wrapper(self, *args, **kwargs): + + try: + return func(self, *args, **kwargs) + except RPCException as e: + logger.exception("API Error calling %s: %s", func.__name__, e) + return self.rest_error(f"Error querying {func.__name__}: {e}") + + return func_wrapper + def __init__(self, freqtrade) -> None: """ Init the api server, and init the super class RPC @@ -103,11 +115,11 @@ class ApiServer(RPC): app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) # TODO: Implement the following # performance # forcebuy # forcesell - # whitelist # blacklist # edge # help (?) @@ -207,38 +219,34 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) + @safe_rpc def _count(self): """ Handler for /count. Returns the number of trades running """ - try: - msg = self._rpc_count() - return self.rest_dump(msg) - except RPCException as e: - return self.rest_error(str(e)) + msg = self._rpc_count() + return self.rest_dump(msg) + @safe_rpc def _daily(self): """ Returns the last X days trading stats summary. :return: stats """ - try: - timescale = request.args.get('timescale') - logger.info("LocalRPC - Daily Command Called") - timescale = int(timescale) + timescale = request.args.get('timescale') + logger.info("LocalRPC - Daily Command Called") + timescale = int(timescale) - stats = self._rpc_daily_profit(timescale, - self._config['stake_currency'], - self._config['fiat_display_currency'] - ) + stats = self._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) - return self.rest_dump(stats) - except RPCException as e: - logger.exception("API Error querying daily: %s", e) - return self.rest_error(f"Error querying daily {e}") + return self.rest_dump(stats) + @safe_rpc def _profit(self): """ Handler for /profit. @@ -246,42 +254,39 @@ class ApiServer(RPC): Returns a cumulative profit statistics :return: stats """ - try: - logger.info("LocalRPC - Profit Command Called") + logger.info("LocalRPC - Profit Command Called") - stats = self._rpc_trade_statistics(self._config['stake_currency'], - self._config['fiat_display_currency'] - ) + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) - return self.rest_dump(stats) - except RPCException as e: - logger.exception("API Error calling profit: %s", e) - return self.rest_error("Error querying closed trades - maybe there are none") + return self.rest_dump(stats) + @safe_rpc def _status(self): """ Handler for /status table. Returns the current status of the trades in json format """ - try: - results = self._rpc_trade_status() - return self.rest_dump(results) - - except RPCException as e: - logger.exception("API Error calling status: %s", e) - return self.rest_error("Error querying open trades - maybe there are none.") + results = self._rpc_trade_status() + return self.rest_dump(results) + @safe_rpc def _balance(self): """ Handler for /balance table. Returns the current status of the trades in json format """ - try: - results = self._rpc_balance(self._config.get('fiat_display_currency', '')) - return self.rest_dump(results) + results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + return self.rest_dump(results) - except RPCException as e: - logger.exception("API Error calling status table: %s", e) - return self.rest_error(f"{e}") + @safe_rpc + def _whitelist(self): + """ + Handler for /whitelist table. + + """ + results = self._rpc_whitelist() + return self.rest_dump(results) From 99875afcc034e6a7cdb96a2be5eb71917d3e9115 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 07:08:46 +0200 Subject: [PATCH 386/928] Add default argument --- freqtrade/rpc/api_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 06f4ad0b9..693b82bec 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -235,8 +235,7 @@ class ApiServer(RPC): :return: stats """ - timescale = request.args.get('timescale') - logger.info("LocalRPC - Daily Command Called") + timescale = request.args.get('timescale', 7) timescale = int(timescale) stats = self._rpc_daily_profit(timescale, From d2c2811249022671812e9a4f8754105cc9697782 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:45:15 +0200 Subject: [PATCH 387/928] Move rest-client to scripts --- {freqtrade/rpc => scripts}/rest_client.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {freqtrade/rpc => scripts}/rest_client.py (100%) diff --git a/freqtrade/rpc/rest_client.py b/scripts/rest_client.py similarity index 100% rename from freqtrade/rpc/rest_client.py rename to scripts/rest_client.py From 5ba189ffb41a7d887cc4d74b92e0ffb708f62194 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:55:38 +0200 Subject: [PATCH 388/928] Add more commands to rest client, fix bug in config handling --- scripts/rest_client.py | 46 ++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 51c7a88f5..bd1187a3e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -26,16 +26,28 @@ logger = logging.getLogger("ft_rest_client") COMMANDS_NO_ARGS = ["start", "stop", "stopbuy", - "reload_conf" + "reload_conf", ] -COMMANDS_ARGS = ["daily", - ] +INFO_COMMANDS = {"version": [], + "count": [], + "daily": ["timescale"], + "profit": [], + "status": [], + "balance": [] + } def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + + parser.add_argument("command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[] + ) + parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -58,7 +70,8 @@ def load_config(configfile): if file.is_file(): with file.open("r") as f: config = json.load(f) - return config + return config + return {} def call_authorized(url): @@ -74,6 +87,22 @@ def call_command_noargs(server_url, command): logger.info(r) +def call_info(server_url, command, command_args): + logger.info(f"Running command `{command}` with parameters `{command_args}` at {server_url}") + call = f"{server_url}/{command}?" + args = INFO_COMMANDS[command] + if len(args) < len(command_args): + logger.error(f"Command {command} does only support {len(args)} arguments.") + return + for idx, arg in enumerate(command_args): + + call += f"{args[idx]}={arg}" + logger.debug(call) + r = call_authorized(call) + + logger.info(r) + + def main(args): config = load_config(args["config"]) @@ -85,13 +114,8 @@ def main(args): if args["command"] in COMMANDS_NO_ARGS: call_command_noargs(server_url, args["command"]) - if args["command"] == "daily": - if str.isnumeric(argv[2]): - get_url = server_url + '/daily?timescale=' + argv[2] - d = get(get_url).json() - print(d) - else: - print("\nThe second argument to daily must be an integer, 1,2,3 etc") + if args["command"] in INFO_COMMANDS: + call_info(server_url, args["command"], args["command_arguments"]) if __name__ == "__main__": From a1043121fc0a760194ad434d79371368d835386d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:56:01 +0200 Subject: [PATCH 389/928] Add blacklist handler --- freqtrade/rpc/api_server.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 693b82bec..2c151daf3 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -116,11 +116,13 @@ class ApiServer(RPC): app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) + app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET']) # TODO: Implement the following # performance # forcebuy # forcesell - # blacklist + # whitelist param + # balacklist params # edge # help (?) @@ -264,7 +266,7 @@ class ApiServer(RPC): @safe_rpc def _status(self): """ - Handler for /status table. + Handler for /status. Returns the current status of the trades in json format """ @@ -274,7 +276,7 @@ class ApiServer(RPC): @safe_rpc def _balance(self): """ - Handler for /balance table. + Handler for /balance. Returns the current status of the trades in json format """ @@ -284,8 +286,15 @@ class ApiServer(RPC): @safe_rpc def _whitelist(self): """ - Handler for /whitelist table. - + Handler for /whitelist. """ results = self._rpc_whitelist() return self.rest_dump(results) + + @safe_rpc + def _blacklist(self): + """ + Handler for /blacklist. + """ + results = self._rpc_blacklist() + return self.rest_dump(results) From a132d6e141fccc257c0814fb6e24927e41f3a028 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Apr 2019 20:32:10 +0200 Subject: [PATCH 390/928] Refactor client into class --- scripts/rest_client.py | 95 +++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index bd1187a3e..4e576d3cd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,10 +10,10 @@ so it can be used as a standalone script. import argparse import json import logging -from sys import argv +from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path -from requests import get +import requests from requests.exceptions import ConnectionError logging.basicConfig( @@ -37,6 +37,63 @@ INFO_COMMANDS = {"version": [], } +class FtRestClient(): + + def __init__(self, serverurl): + self.serverurl = serverurl + + self.session = requests.Session() + + def call(self, method, apipath, params: dict = None, data=None, files=None): + + if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): + raise ValueError('invalid method <{0}>'.format(method)) + basepath = f"{self.serverurl}/{apipath}" + + hd = {"Accept": "application/json", + "Content-Type": "application/json" + } + + # Split url + schema, netloc, path, params, query, fragment = urlparse(basepath) + # URLEncode query string + query = urlencode(params) + # recombine url + url = urlunparse((schema, netloc, path, params, query, fragment)) + print(url) + try: + + req = requests.Request(method, url, headers=hd, data=data, + # auth=self.session.auth + ) + reqp = req.prepare() + return self.session.send(reqp).json() + # return requests.get(url).json() + except ConnectionError: + logger.warning("Connection error") + + def call_command_noargs(self, command): + logger.info(f"Running command `{command}` at {self.serverurl}") + r = self.call("GET", command) + logger.info(r) + + def call_info(self, command, command_args): + logger.info( + f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") + args = INFO_COMMANDS[command] + if len(args) < len(command_args): + logger.error(f"Command {command} does only support {len(args)} arguments.") + return + params = {} + for idx, arg in enumerate(command_args): + params[args[idx]] = arg + + logger.debug(params) + r = self.call("GET", command, params) + + logger.info(r) + + def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", @@ -74,48 +131,20 @@ def load_config(configfile): return {} -def call_authorized(url): - try: - return get(url).json() - except ConnectionError: - logger.warning("Connection error") - - -def call_command_noargs(server_url, command): - logger.info(f"Running command `{command}` at {server_url}") - r = call_authorized(f"{server_url}/{command}") - logger.info(r) - - -def call_info(server_url, command, command_args): - logger.info(f"Running command `{command}` with parameters `{command_args}` at {server_url}") - call = f"{server_url}/{command}?" - args = INFO_COMMANDS[command] - if len(args) < len(command_args): - logger.error(f"Command {command} does only support {len(args)} arguments.") - return - for idx, arg in enumerate(command_args): - - call += f"{args[idx]}={arg}" - logger.debug(call) - r = call_authorized(call) - - logger.info(r) - - def main(args): config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") server_url = f"http://{url}:{port}" + client = FtRestClient(server_url) # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - call_command_noargs(server_url, args["command"]) + client.call_command_noargs(args["command"]) if args["command"] in INFO_COMMANDS: - call_info(server_url, args["command"], args["command_arguments"]) + client.call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From b0ac98a7cd069ceffda15d018792bbff70acdaf7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:08:03 +0200 Subject: [PATCH 391/928] Clean up rest client --- scripts/rest_client.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4e576d3cd..e01fc086e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -40,8 +40,8 @@ INFO_COMMANDS = {"version": [], class FtRestClient(): def __init__(self, serverurl): - self.serverurl = serverurl + self.serverurl = serverurl self.session = requests.Session() def call(self, method, apipath, params: dict = None, data=None, files=None): @@ -62,19 +62,17 @@ class FtRestClient(): url = urlunparse((schema, netloc, path, params, query, fragment)) print(url) try: - - req = requests.Request(method, url, headers=hd, data=data, - # auth=self.session.auth - ) - reqp = req.prepare() - return self.session.send(reqp).json() - # return requests.get(url).json() + resp = self.session.request(method, url, headers=hd, data=data, + # auth=self.session.auth + ) + # return resp.text + return resp.json() except ConnectionError: logger.warning("Connection error") def call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") - r = self.call("GET", command) + r = self.call("POST", command) logger.info(r) def call_info(self, command, command_args): From ebebf94750f8fd56add5520ecd0e0ef2777bbebf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:10:07 +0200 Subject: [PATCH 392/928] Change commands to post --- freqtrade/rpc/api_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 2c151daf3..d520fd255 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -103,11 +103,11 @@ class ApiServer(RPC): """ # TODO: actions should not be GET... # Actions to control the bot - app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) - app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) + app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) + app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['GET']) + methods=['POST']) # Info commands app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) From d1fffab23580ff615d3bd3fbbe532c451c2ad69a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:10:23 +0200 Subject: [PATCH 393/928] Rename internal methods to _ --- scripts/rest_client.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index e01fc086e..6843bb31d 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -44,7 +44,7 @@ class FtRestClient(): self.serverurl = serverurl self.session = requests.Session() - def call(self, method, apipath, params: dict = None, data=None, files=None): + def _call(self, method, apipath, params: dict = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) @@ -70,12 +70,12 @@ class FtRestClient(): except ConnectionError: logger.warning("Connection error") - def call_command_noargs(self, command): + def _call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") - r = self.call("POST", command) + r = self._call("POST", command) logger.info(r) - def call_info(self, command, command_args): + def _call_info(self, command, command_args): logger.info( f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") args = INFO_COMMANDS[command] @@ -87,7 +87,7 @@ class FtRestClient(): params[args[idx]] = arg logger.debug(params) - r = self.call("GET", command, params) + r = self._call("GET", command, params) logger.info(r) @@ -139,10 +139,10 @@ def main(args): # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - client.call_command_noargs(args["command"]) + client._call_command_noargs(args["command"]) if args["command"] in INFO_COMMANDS: - client.call_info(args["command"], args["command_arguments"]) + client._call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From 8f9b9d31e2f667cbaa0083b6982f01eae006d9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:12:03 +0200 Subject: [PATCH 394/928] Reorder arguments --- scripts/rest_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6843bb31d..fc3478046 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -97,12 +97,6 @@ def add_arguments(): parser.add_argument("command", help="Positional argument defining the command to execute.") - parser.add_argument("command_arguments", - help="Positional arguments for the parameters for [command]", - nargs="*", - default=[] - ) - parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -110,6 +104,13 @@ def add_arguments(): metavar='PATH', default='config.json' ) + + parser.add_argument("command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[] + ) + args = parser.parse_args() # if len(argv) == 1: # print('\nThis script accepts the following arguments') From 938d7275ba49b6cf718d6c8c5ff03958b13c4243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:27:20 +0200 Subject: [PATCH 395/928] implement some methods --- scripts/rest_client.py | 50 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index fc3478046..eb6fb97a9 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,6 +10,7 @@ so it can be used as a standalone script. import argparse import json import logging +import inspect from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path @@ -55,11 +56,11 @@ class FtRestClient(): } # Split url - schema, netloc, path, params, query, fragment = urlparse(basepath) + schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string query = urlencode(params) # recombine url - url = urlunparse((schema, netloc, path, params, query, fragment)) + url = urlunparse((schema, netloc, path, par, query, fragment)) print(url) try: resp = self.session.request(method, url, headers=hd, data=data, @@ -70,6 +71,12 @@ class FtRestClient(): except ConnectionError: logger.warning("Connection error") + def _get(self, apipath, params: dict = None): + return self._call("GET", apipath, params=params) + + def _post(self, apipath, params: dict = None, data: dict = None): + return self._call("POST", apipath, params=params, data=data) + def _call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") r = self._call("POST", command) @@ -91,6 +98,27 @@ class FtRestClient(): logger.info(r) + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + + def count(self): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("count") + + def daily(self, days=None): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("daily", params={"timescale": days} if days else None) + def add_arguments(): parser = argparse.ArgumentParser() @@ -138,12 +166,20 @@ def main(args): server_url = f"http://{url}:{port}" client = FtRestClient(server_url) - # Call commands without arguments - if args["command"] in COMMANDS_NO_ARGS: - client._call_command_noargs(args["command"]) + m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] + command = args["command"] + if command not in m: + logger.error(f"Command {command} not defined") + return - if args["command"] in INFO_COMMANDS: - client._call_info(args["command"], args["command_arguments"]) + print(getattr(client, command)(*args["command_arguments"])) + + # Call commands without arguments + # if args["command"] in COMMANDS_NO_ARGS: + # client._call_command_noargs(args["command"]) + + # if args["command"] in INFO_COMMANDS: + # client._call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From 122cf4c897268d8ba2ce7e8665f4f1a9989dda93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:11 +0200 Subject: [PATCH 396/928] Default add to None for blacklist rpc calls --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5b78c8356..048ebec63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -470,7 +470,7 @@ class RPC(object): } return res - def _rpc_blacklist(self, add: List[str]) -> Dict: + def _rpc_blacklist(self, add: List[str] = None) -> Dict: """ Returns the currently active blacklist""" if add: stake_currency = self._freqtrade.config.get('stake_currency') From 3efdd55fb8526a3c1a771bcc1c89928d191f86db Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:36 +0200 Subject: [PATCH 397/928] Support blacklist adding --- freqtrade/rpc/api_server.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d520fd255..1643bc535 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -115,14 +115,15 @@ class ApiServer(RPC): app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) - app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) + app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + methods=['GET', 'POST']) # TODO: Implement the following # performance # forcebuy # forcesell - # whitelist param - # balacklist params + # blacklist params # edge # help (?) @@ -296,5 +297,6 @@ class ApiServer(RPC): """ Handler for /blacklist. """ - results = self._rpc_blacklist() + add = request.json.get("blacklist", None) if request.method == 'POST' else None + results = self._rpc_blacklist(add) return self.rest_dump(results) From 0163edc868c3a8c2e9e4040387ea588415163d0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:52 +0200 Subject: [PATCH 398/928] rest-client more methods --- scripts/rest_client.py | 123 +++++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index eb6fb97a9..1958c1a07 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -11,6 +11,7 @@ import argparse import json import logging import inspect +from typing import List from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path @@ -24,32 +25,18 @@ logging.basicConfig( logger = logging.getLogger("ft_rest_client") -COMMANDS_NO_ARGS = ["start", - "stop", - "stopbuy", - "reload_conf", - ] -INFO_COMMANDS = {"version": [], - "count": [], - "daily": ["timescale"], - "profit": [], - "status": [], - "balance": [] - } - - class FtRestClient(): def __init__(self, serverurl): - self.serverurl = serverurl - self.session = requests.Session() + self._serverurl = serverurl + self._session = requests.Session() def _call(self, method, apipath, params: dict = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) - basepath = f"{self.serverurl}/{apipath}" + basepath = f"{self._serverurl}/{apipath}" hd = {"Accept": "application/json", "Content-Type": "application/json" @@ -58,14 +45,14 @@ class FtRestClient(): # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string - query = urlencode(params) + query = urlencode(params) if params else None # recombine url url = urlunparse((schema, netloc, path, par, query, fragment)) - print(url) + try: - resp = self.session.request(method, url, headers=hd, data=data, - # auth=self.session.auth - ) + resp = self._session.request(method, url, headers=hd, data=json.dumps(data), + # auth=self.session.auth + ) # return resp.text return resp.json() except ConnectionError: @@ -77,33 +64,12 @@ class FtRestClient(): def _post(self, apipath, params: dict = None, data: dict = None): return self._call("POST", apipath, params=params, data=data) - def _call_command_noargs(self, command): - logger.info(f"Running command `{command}` at {self.serverurl}") - r = self._call("POST", command) - logger.info(r) - - def _call_info(self, command, command_args): - logger.info( - f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") - args = INFO_COMMANDS[command] - if len(args) < len(command_args): - logger.error(f"Command {command} does only support {len(args)} arguments.") - return - params = {} - for idx, arg in enumerate(command_args): - params[args[idx]] = arg - - logger.debug(params) - r = self._call("GET", command, params) - - logger.info(r) - - def version(self): + def balance(self): """ - Returns the version of the bot - :returns: json object containing the version + get the account balance + :returns: json object """ - return self._get("version") + return self._get("balance") def count(self): """ @@ -119,12 +85,58 @@ class FtRestClient(): """ return self._get("daily", params={"timescale": days} if days else None) + def profit(self): + """ + Returns the profit summary + :returns: json object + """ + return self._get("profit") + + def status(self): + """ + Get the status of open trades + :returns: json object + """ + return self._get("status") + + def whitelist(self): + """ + Show the current whitelist + :returns: json object + """ + return self._get("whitelist") + + def blacklist(self, *args): + """ + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: json object + """ + if not args: + return self._get("blacklist") + else: + return self._post("blacklist", data={"blacklist": args}) + + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + parser.add_argument('--show', + help='Show possible methods with this client', + dest='show', + action='store_true', + default=False + ) + parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -160,6 +172,16 @@ def load_config(configfile): def main(args): + if args.get("show"): + # Print dynamic help for the different commands + client = FtRestClient(None) + print("Possible commands:") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + print(f"{x} {getattr(client, x).__doc__}") + + return + config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") @@ -174,13 +196,6 @@ def main(args): print(getattr(client, command)(*args["command_arguments"])) - # Call commands without arguments - # if args["command"] in COMMANDS_NO_ARGS: - # client._call_command_noargs(args["command"]) - - # if args["command"] in INFO_COMMANDS: - # client._call_info(args["command"], args["command_arguments"]) - if __name__ == "__main__": args = add_arguments() From 393e4ac90e0c435a37be62060c144835dae0b853 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:59:08 +0200 Subject: [PATCH 399/928] Sort methods --- freqtrade/rpc/api_server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 1643bc535..009ae7d68 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -109,16 +109,18 @@ class ApiServer(RPC): app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['POST']) # Info commands - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, - methods=['GET']) + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + + # Combined actions and infos app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) # TODO: Implement the following # performance # forcebuy From b1964851c9d9bc7153abf6b93a774bf2a4873fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:03:54 +0200 Subject: [PATCH 400/928] Add performance handlers --- freqtrade/rpc/api_server.py | 18 ++++++++++++++++-- scripts/rest_client.py | 9 ++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 009ae7d68..f63c68bf1 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -113,6 +113,8 @@ class ApiServer(RPC): app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) + app.add_url_rule('/performance', 'performance', view_func=self._performance, + methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) @@ -122,10 +124,8 @@ class ApiServer(RPC): app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) # TODO: Implement the following - # performance # forcebuy # forcesell - # blacklist params # edge # help (?) @@ -266,6 +266,20 @@ class ApiServer(RPC): return self.rest_dump(stats) + @safe_rpc + def _performance(self): + """ + Handler for /performance. + + Returns a cumulative performance statistics + :return: stats + """ + logger.info("LocalRPC - performance Command Called") + + stats = self._rpc_performance() + + return self.rest_dump(stats) + @safe_rpc def _status(self): """ diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 1958c1a07..7dbed9bc5 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -45,7 +45,7 @@ class FtRestClient(): # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string - query = urlencode(params) if params else None + query = urlencode(params) if params else "" # recombine url url = urlunparse((schema, netloc, path, par, query, fragment)) @@ -92,6 +92,13 @@ class FtRestClient(): """ return self._get("profit") + def performance(self): + """ + Returns the performance of the different coins + :returns: json object + """ + return self._get("performance") + def status(self): """ Get the status of open trades From ea8b8eec1ca178ca151796f19aaef7c48bae7624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:06:46 +0200 Subject: [PATCH 401/928] Add edge handler --- freqtrade/rpc/api_server.py | 12 +++++++++++- scripts/rest_client.py | 21 ++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f63c68bf1..bde28be73 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -112,6 +112,7 @@ class ApiServer(RPC): app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/performance', 'performance', view_func=self._performance, methods=['GET']) @@ -126,7 +127,6 @@ class ApiServer(RPC): # TODO: Implement the following # forcebuy # forcesell - # edge # help (?) def run(self): @@ -250,6 +250,16 @@ class ApiServer(RPC): return self.rest_dump(stats) + @safe_rpc + def _edge(self): + """ + Returns information related to Edge. + :return: edge stats + """ + stats = self._rpc_edge() + + return self.rest_dump(stats) + @safe_rpc def _profit(self): """ diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 7dbed9bc5..efca8adfa 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -85,6 +85,13 @@ class FtRestClient(): """ return self._get("daily", params={"timescale": days} if days else None) + def edge(self): + """ + Returns information about edge + :returns: json object + """ + return self._get("edge") + def profit(self): """ Returns the profit summary @@ -106,6 +113,13 @@ class FtRestClient(): """ return self._get("status") + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + def whitelist(self): """ Show the current whitelist @@ -124,13 +138,6 @@ class FtRestClient(): else: return self._post("blacklist", data={"blacklist": args}) - def version(self): - """ - Returns the version of the bot - :returns: json object containing the version - """ - return self._get("version") - def add_arguments(): parser = argparse.ArgumentParser() From cb271f51d1f288cebaacb45572b6948b8f763d22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:10:01 +0200 Subject: [PATCH 402/928] Add client actions for actions --- scripts/rest_client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index efca8adfa..c8f5823a1 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -64,6 +64,35 @@ class FtRestClient(): def _post(self, apipath, params: dict = None, data: dict = None): return self._call("POST", apipath, params=params, data=data) + def start(self): + """ + Start the bot if it's in stopped state. + :returns: json object + """ + return self._post("start") + + def stop(self): + """ + Stop the bot. Use start to restart + :returns: json object + """ + return self._post("stop") + + def stopbuy(self): + """ + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + """ + return self._post("stopbuy") + + def reload_conf(self): + """ + Reload configuration + :returns: json object + """ + return self._post("reload_conf") + def balance(self): """ get the account balance From bc4342b2d0119ba5b9baa97ffddabe1e5669557a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:12:39 +0200 Subject: [PATCH 403/928] small cleanup --- freqtrade/rpc/api_server.py | 7 +++---- scripts/rest_client.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index bde28be73..e158df0be 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -101,7 +101,6 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ - # TODO: actions should not be GET... # Actions to control the bot app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) @@ -114,7 +113,7 @@ class ApiServer(RPC): app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/performance', 'performance', view_func=self._performance, + app.add_url_rule('/performance', 'performance', view_func=self._performance, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) @@ -141,8 +140,8 @@ class ApiServer(RPC): logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) if not IPv4Address(rest_ip).is_loopback: - logger.info("SECURITY WARNING - Local Rest Server listening to external connections") - logger.info("SECURITY WARNING - This is insecure please set to your loopback," + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json") # Run the Server diff --git a/scripts/rest_client.py b/scripts/rest_client.py index c8f5823a1..4830d43b8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -95,7 +95,7 @@ class FtRestClient(): def balance(self): """ - get the account balance + Get the account balance :returns: json object """ return self._get("balance") From 6e4b159611128bce19111b0141ff8d5b6da8f61a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 12:50:13 +0200 Subject: [PATCH 404/928] Add forcebuy and forcesell --- freqtrade/rpc/api_server.py | 25 +++++++++++++++++++++++-- scripts/rest_client.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e158df0be..d47def4a6 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -123,9 +123,10 @@ class ApiServer(RPC): methods=['GET', 'POST']) app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) + app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) + app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) + # TODO: Implement the following - # forcebuy - # forcesell # help (?) def run(self): @@ -325,3 +326,23 @@ class ApiServer(RPC): add = request.json.get("blacklist", None) if request.method == 'POST' else None results = self._rpc_blacklist(add) return self.rest_dump(results) + + @safe_rpc + def _forcebuy(self): + """ + Handler for /forcebuy. + """ + asset = request.json.get("pair") + price = request.json.get("price", None) + trade = self._rpc_forcebuy(asset, price) + # TODO: Returns a trade, we need to jsonify that. + return self.rest_dump(trade) + + @safe_rpc + def _forcesell(self): + """ + Handler for /forcesell. + """ + tradeid = request.json.get("tradeid") + results = self._rpc_forcesell(tradeid) + return self.rest_dump(results) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4830d43b8..81c4b66cc 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -167,6 +167,27 @@ class FtRestClient(): else: return self._post("blacklist", data={"blacklist": args}) + def forcebuy(self, pair, price=None): + """ + Buy an asset + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :returns: json object of the trade + """ + data = {"pair": pair, + "price": price + } + return self._post("forcebuy", data=data) + + def forcesell(self, tradeid): + """ + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + """ + + return self._post("forcesell", data={"tradeid": tradeid}) + def add_arguments(): parser = argparse.ArgumentParser() From 0ac434da78613714dc27300fee7f5750ae872cff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 May 2019 07:12:37 +0200 Subject: [PATCH 405/928] Add forcebuy jsonification --- freqtrade/rpc/api_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d47def4a6..be2f02663 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -335,8 +335,7 @@ class ApiServer(RPC): asset = request.json.get("pair") price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) - # TODO: Returns a trade, we need to jsonify that. - return self.rest_dump(trade) + return self.rest_dump(trade.to_json()) @safe_rpc def _forcesell(self): From e0486ea68eb5e2243dbc8ae7125013e4827935af Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 May 2019 07:07:14 +0200 Subject: [PATCH 406/928] Make app a instance object --- freqtrade/rpc/api_server.py | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be2f02663..be6b20ecb 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -26,10 +26,6 @@ class ArrowJSONEncoder(JSONEncoder): return JSONEncoder.default(self, obj) -app = Flask(__name__) -app.json_encoder = ArrowJSONEncoder - - class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it @@ -58,6 +54,9 @@ class ApiServer(RPC): super().__init__(freqtrade) self._config = freqtrade.config + self.app = Flask(__name__) + + self.app.json_encoder = ArrowJSONEncoder # Register application handling self.register_rest_other() @@ -90,8 +89,8 @@ class ApiServer(RPC): Registers flask app URLs that are not calls to functionality in rpc.rpc. :return: """ - app.register_error_handler(404, self.page_not_found) - app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + self.app.register_error_handler(404, self.page_not_found) + self.app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) def register_rest_rpc_urls(self): """ @@ -102,29 +101,30 @@ class ApiServer(RPC): :return: """ # Actions to control the bot - app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) - app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['POST']) + self.app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) + self.app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) + self.app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) + self.app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, + methods=['POST']) # Info commands - app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) - app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) - app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) - app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/performance', 'performance', view_func=self._performance, - methods=['GET']) - app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + self.app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + self.app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) + self.app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) + self.app.add_url_rule('/performance', 'performance', view_func=self._performance, + methods=['GET']) + self.app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) + self.app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) # Combined actions and infos - app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, - methods=['GET', 'POST']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, - methods=['GET']) - app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) - app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) + self.app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + methods=['GET', 'POST']) + self.app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) + self.app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) + self.app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, + methods=['POST']) # TODO: Implement the following # help (?) @@ -143,12 +143,12 @@ class ApiServer(RPC): if not IPv4Address(rest_ip).is_loopback: logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") + "e.g 127.0.0.1 in config.json") # Run the Server logger.info('Starting Local Rest Server') try: - app.run(host=rest_ip, port=rest_port) + self.app.run(host=rest_ip, port=rest_port) except Exception: logger.exception("Api server failed to start, exception message is:") logger.info('Starting Local Rest Server_end') From b1a14401c2ee83cd3f61ecc6c3a99dac6cd5e9ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 May 2019 07:07:38 +0200 Subject: [PATCH 407/928] Add some initial tests for apiserver --- freqtrade/tests/rpc/test_rpc_apiserver.py | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 14c35a38e..fae278028 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -4,11 +4,37 @@ Unit test file for rpc/api_server.py from unittest.mock import MagicMock +import pytest + from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver +@pytest.fixture +def client(default_conf, mocker): + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + yield apiserver.app.test_client() + # Cleanup ... ? + + +def response_success_assert(response): + assert response.status_code == 200 + assert response.content_type == "application/json" + + +def test_start(client): + rc = client.post("/start") + response_success_assert(rc) + assert rc.json == {'status': 'already running'} + + +def test_stop(client): + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'stopping trader ...'} + + def test__init__(default_conf, mocker): """ Test __init__() method From 6ea08958032d65efec043f5b1f395bb549977ad1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 08:55:10 +0200 Subject: [PATCH 408/928] Fix docstrings --- freqtrade/rpc/api_server.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be6b20ecb..18c68e797 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -130,9 +130,7 @@ class ApiServer(RPC): # help (?) def run(self): - """ Method that runs flask app in its own thread forever """ - - """ + """ Method that runs flask app in its own thread forever. Section to handle configuration and running of the Rest server also to check and warn if not bound to a loopback, warn on security risk. """ @@ -153,11 +151,6 @@ class ApiServer(RPC): logger.exception("Api server failed to start, exception message is:") logger.info('Starting Local Rest Server_end') - """ - Define the application methods here, called by app.add_url_rule - each Telegram command should have a like local substitute - """ - def page_not_found(self, error): """ Return "404 not found", 404. From 70a3c2c648f5aa3c0c09ea650241aed7dea63c84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 08:55:21 +0200 Subject: [PATCH 409/928] Actions - Add tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 75 ++++++++++++----------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index fae278028..8062171c5 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -12,9 +12,10 @@ from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver @pytest.fixture -def client(default_conf, mocker): - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) - yield apiserver.app.test_client() +def botclient(default_conf, mocker): + ftbot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(ftbot) + yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -23,19 +24,32 @@ def response_success_assert(response): assert response.content_type == "application/json" -def test_start(client): +def test_api_stop_workflow(botclient): + ftbot, client = botclient + assert ftbot.state == State.RUNNING + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'stopping trader ...'} + assert ftbot.state == State.STOPPED + + # Stop bot again + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'already stopped'} + + # Start bot + rc = client.post("/start") + response_success_assert(rc) + assert rc.json == {'status': 'starting trader ...'} + assert ftbot.state == State.RUNNING + + # Call start again rc = client.post("/start") response_success_assert(rc) assert rc.json == {'status': 'already running'} -def test_stop(client): - rc = client.post("/stop") - response_success_assert(rc) - assert rc.json == {'status': 'stopping trader ...'} - - -def test__init__(default_conf, mocker): +def test_api__init__(default_conf, mocker): """ Test __init__() method """ @@ -46,33 +60,20 @@ def test__init__(default_conf, mocker): assert apiserver._config == default_conf -def test_start_endpoint(default_conf, mocker): - """Test /start endpoint""" - patch_apiserver(mocker) - bot = get_patched_freqtradebot(mocker, default_conf) - apiserver = ApiServer(bot) +def test_api_reloadconf(botclient): + ftbot, client = botclient - bot.state = State.STOPPED - assert bot.state == State.STOPPED - result = apiserver.start() - assert result == '{"status": "starting trader ..."}' - assert bot.state == State.RUNNING - - result = apiserver.start() - assert result == '{"status": "already running"}' + rc = client.post("/reload_conf") + response_success_assert(rc) + assert rc.json == {'status': 'reloading config ...'} + assert ftbot.state == State.RELOAD_CONF -def test_stop_endpoint(default_conf, mocker): - """Test /stop endpoint""" - patch_apiserver(mocker) - bot = get_patched_freqtradebot(mocker, default_conf) - apiserver = ApiServer(bot) +def test_api_stopbuy(botclient): + ftbot, client = botclient + assert ftbot.config['max_open_trades'] != 0 - bot.state = State.RUNNING - assert bot.state == State.RUNNING - result = apiserver.stop() - assert result == '{"status": "stopping trader ..."}' - assert bot.state == State.STOPPED - - result = apiserver.stop() - assert result == '{"status": "already stopped"}' + rc = client.post("/stopbuy") + response_success_assert(rc) + assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + assert ftbot.config['max_open_trades'] == 0 From 6b426e78f60745fec291ecd192cb00837bee3374 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:10:54 +0200 Subject: [PATCH 410/928] Tests for balance --- freqtrade/tests/conftest.py | 36 ++++++++++++++++++++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 37 +++++++++++++++++++++++ freqtrade/tests/rpc/test_rpc_telegram.py | 36 ++-------------------- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 98563a374..a4eae4300 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -970,3 +970,39 @@ def edge_conf(default_conf): } return default_conf + + +@pytest.fixture +def rpc_balance(): + return { + 'BTC': { + 'total': 12.0, + 'free': 12.0, + 'used': 0.0 + }, + 'ETH': { + 'total': 0.0, + 'free': 0.0, + 'used': 0.0 + }, + 'USDT': { + 'total': 10000.0, + 'free': 10000.0, + 'used': 0.0 + }, + 'LTC': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + 'XRP': { + 'total': 1.0, + 'free': 1.0, + 'used': 0.0 + }, + 'EUR': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + } diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 8062171c5..7e99a9510 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -77,3 +77,40 @@ def test_api_stopbuy(botclient): response_success_assert(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 + + +def test_api_balance(botclient, mocker, rpc_balance): + ftbot, client = botclient + + def mock_ticker(symbol, refresh): + if symbol == 'BTC/USDT': + return { + 'bid': 10000.00, + 'ask': 10000.00, + 'last': 10000.00, + } + elif symbol == 'XRP/BTC': + return { + 'bid': 0.00001, + 'ask': 0.00001, + 'last': 0.00001, + } + return { + 'bid': 0.1, + 'ask': 0.1, + 'last': 0.1, + } + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + + rc = client.get("/balance") + response_success_assert(rc) + assert "currencies" in rc.json + assert len(rc.json["currencies"]) == 5 + assert rc.json['currencies'][0] == { + 'currency': 'BTC', + 'available': 12.0, + 'balance': 12.0, + 'pending': 0.0, + 'est_btc': 12.0, + } diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 69e3006cd..b8e57d092 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -496,39 +496,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_telegram_balance_handle(default_conf, update, mocker) -> None: - mock_balance = { - 'BTC': { - 'total': 12.0, - 'free': 12.0, - 'used': 0.0 - }, - 'ETH': { - 'total': 0.0, - 'free': 0.0, - 'used': 0.0 - }, - 'USDT': { - 'total': 10000.0, - 'free': 10000.0, - 'used': 0.0 - }, - 'LTC': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - }, - 'XRP': { - 'total': 1.0, - 'free': 1.0, - 'used': 0.0 - }, - 'EUR': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - } - } +def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: def mock_ticker(symbol, refresh): if symbol == 'BTC/USDT': @@ -549,7 +517,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) msg_mock = MagicMock() From 88dd18e045bc0bae78c3b7203674842d4485c484 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:42:30 +0200 Subject: [PATCH 411/928] Move patch_signal to conftest --- freqtrade/tests/conftest.py | 13 +++++++++++-- freqtrade/tests/rpc/test_rpc.py | 3 +-- freqtrade/tests/rpc/test_rpc_telegram.py | 3 +-- freqtrade/tests/test_freqtradebot.py | 11 +---------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index a4eae4300..c9b98aacd 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -10,7 +10,7 @@ import arrow import pytest from telegram import Chat, Message, Update -from freqtrade import constants +from freqtrade import constants, persistence from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -96,7 +96,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + persistence.init(config) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -112,6 +112,15 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: + """ + :param mocker: mocker to patch IStrategy class + :param value: which value IStrategy.get_signal() must return + :return: None + """ + freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.exchange.refresh_latest_ohlcv = lambda p: None + @pytest.fixture(autouse=True) def patch_coinmarketcap(mocker) -> None: """ diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 6ce543f3d..f005041d9 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -14,8 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.conftest import patch_exchange -from freqtrade.tests.test_freqtradebot import patch_get_signal +from freqtrade.tests.conftest import patch_exchange, patch_get_signal # Functions for recurrent object patching diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b8e57d092..46ef15f56 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -22,8 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, - patch_exchange) -from freqtrade.tests.test_freqtradebot import patch_get_signal + patch_exchange, patch_get_signal) class DummyCls(Telegram): diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 67b05ac3e..946a9c819 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -19,7 +19,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, +from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, patch_get_signal, patch_exchange, patch_wallet) from freqtrade.worker import Worker @@ -59,15 +59,6 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: - """ - :param mocker: mocker to patch IStrategy class - :param value: which value IStrategy.get_signal() must return - :return: None - """ - freqtrade.strategy.get_signal = lambda e, s, t: value - freqtrade.exchange.refresh_latest_ohlcv = lambda p: None - def patch_RPCManager(mocker) -> MagicMock: """ From 3c468701093e67f64b663dedbc36f56ac4531e64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:44:39 +0200 Subject: [PATCH 412/928] Test /count for api-server --- freqtrade/tests/rpc/test_rpc_apiserver.py | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 7e99a9510..3590b0dc8 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -2,18 +2,23 @@ Unit test file for rpc/api_server.py """ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest +from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver +from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver, patch_get_signal @pytest.fixture def botclient(default_conf, mocker): + default_conf.update({"api_server":{"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -79,6 +84,14 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 +def test_api_version(botclient): + ftbot, client = botclient + + rc = client.get("/version") + response_success_assert(rc) + assert rc.json == {"version": __version__} + + def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient @@ -114,3 +127,27 @@ def test_api_balance(botclient, mocker, rpc_balance): 'pending': 0.0, 'est_btc': 12.0, } + + +def test_api_count(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/count") + response_success_assert(rc) + + assert rc.json["current"] == 0 + assert rc.json["max"] == 1.0 + + # Create some test data + ftbot.create_trade() + rc = client.get("/count") + response_success_assert(rc) + assert rc.json["current"] == 1.0 + assert rc.json["max"] == 1.0 From 03dc6d92aeb1371ff80562971a82700c386fac4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:10:41 +0200 Subject: [PATCH 413/928] Remove hello() --- freqtrade/rpc/api_server.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 18c68e797..5b7da902d 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,7 +90,6 @@ class ApiServer(RPC): :return: """ self.app.register_error_handler(404, self.page_not_found) - self.app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) def register_rest_rpc_urls(self): """ @@ -161,24 +160,6 @@ class ApiServer(RPC): 'code': 404 }), 404 - def hello(self): - """ - None critical but helpful default index page. - - That lists URLs added to the flask server. - This may be deprecated at any time. - :return: index.html - """ - rest_cmds = ('Commands implemented:
' - 'Show 7 days of stats
' - 'Stop the Trade thread
' - 'Start the Traded thread
' - 'Show profit summary
' - 'Show status table - Open trades
' - ' 404 page does not exist
' - ) - return rest_cmds - def _start(self): """ Handler for /start. From 557f849519ef144693135f2f5f51404c28f3bd7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:18:11 +0200 Subject: [PATCH 414/928] Improve 404 handling --- freqtrade/rpc/api_server.py | 2 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5b7da902d..573f89143 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -156,7 +156,7 @@ class ApiServer(RPC): """ return self.rest_dump({ 'status': 'error', - 'reason': '''There's no API call for %s''' % request.base_url, + 'reason': f"There's no API call for {request.base_url}.", 'code': 404 }), 404 diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 3590b0dc8..52a456d68 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -29,6 +29,18 @@ def response_success_assert(response): assert response.content_type == "application/json" +def test_api_not_found(botclient): + ftbot, client = botclient + + rc = client.post("/invalid_url") + assert rc.status_code == 404 + assert rc.content_type == "application/json" + assert rc.json == {'status': 'error', + 'reason': "There's no API call for http://localhost/invalid_url.", + 'code': 404 + } + + def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING From a146c5bf7820402470440620eae26f4f145743e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:31:48 +0200 Subject: [PATCH 415/928] Improve jsonification --- freqtrade/rpc/api_server.py | 5 +++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 573f89143..4ddda307a 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,5 +1,6 @@ import logging import threading +from datetime import datetime, date from ipaddress import IPv4Address from typing import Dict @@ -18,6 +19,10 @@ class ArrowJSONEncoder(JSONEncoder): try: if isinstance(obj, Arrow): return obj.for_json() + elif isinstance(obj, date): + return obj.strftime("%Y-%m-%d") + elif isinstance(obj, datetime): + return obj.strftime("%Y-%m-%d %H:%M:%S") iterable = iter(obj) except TypeError: pass diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 52a456d68..cefbb9acd 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -2,6 +2,7 @@ Unit test file for rpc/api_server.py """ +from datetime import datetime from unittest.mock import MagicMock, PropertyMock import pytest @@ -9,12 +10,13 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver, patch_get_signal +from freqtrade.tests.conftest import (get_patched_freqtradebot, + patch_apiserver, patch_get_signal) @pytest.fixture def botclient(default_conf, mocker): - default_conf.update({"api_server":{"enabled": True, + default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) @@ -163,3 +165,19 @@ def test_api_count(botclient, mocker, ticker, fee, markets): response_success_assert(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 + + +def test_api_daily(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/daily") + response_success_assert(rc) + assert len(rc.json) == 7 + assert rc.json[0][0] == str(datetime.utcnow().date()) From a7329e5cc945efb005e02685d66d7b8b65ed56cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:40:30 +0200 Subject: [PATCH 416/928] Test api-server start from manager --- freqtrade/tests/conftest.py | 9 ------- freqtrade/tests/rpc/test_rpc_apiserver.py | 7 +++--- freqtrade/tests/rpc/test_rpc_manager.py | 29 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index c9b98aacd..bb8bd9c95 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -143,15 +143,6 @@ def patch_coinmarketcap(mocker) -> None: ) -def patch_apiserver(mocker) -> None: - mocker.patch.multiple( - 'freqtrade.rpc.api_server.ApiServer', - run=MagicMock(), - register_rest_other=MagicMock(), - register_rest_rpc_urls=MagicMock(), - ) - - @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index cefbb9acd..34fe558f8 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -10,15 +10,14 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import (get_patched_freqtradebot, - patch_apiserver, patch_get_signal) +from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) @pytest.fixture def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, - "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 15d9c20c6..91fd2297f 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -135,3 +135,32 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] + + +def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + default_conf['telegram']['enabled'] = False + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert not log_has('Enabling rpc.api_server', caplog.record_tuples) + assert rpc_manager.registered_modules == [] + assert run_mock.call_count == 0 + + +def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + + default_conf["telegram"]["enabled"] = False + default_conf["api_server"] = {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"} + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert log_has('Enabling rpc.api_server', caplog.record_tuples) + assert len(rpc_manager.registered_modules) == 1 + assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] + assert run_mock.call_count == 1 From b9435e3ceac2342e8aa853c7c9f1247f66a8790a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 14:05:25 +0200 Subject: [PATCH 417/928] Add more tests --- freqtrade/tests/conftest.py | 1 + freqtrade/tests/rpc/test_rpc_apiserver.py | 220 +++++++++++++++++++--- freqtrade/tests/test_freqtradebot.py | 10 +- 3 files changed, 203 insertions(+), 28 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index bb8bd9c95..59989d604 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -121,6 +121,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.strategy.get_signal = lambda e, s, t: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None + @pytest.fixture(autouse=True) def patch_coinmarketcap(mocker) -> None: """ diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 34fe558f8..95ad7dbc3 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -3,7 +3,7 @@ Unit test file for rpc/api_server.py """ from datetime import datetime -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -11,6 +11,7 @@ from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) +from freqtrade.persistence import Trade @pytest.fixture @@ -25,8 +26,8 @@ def botclient(default_conf, mocker): # Cleanup ... ? -def response_success_assert(response): - assert response.status_code == 200 +def assert_response(response, expected_code=200): + assert response.status_code == expected_code assert response.content_type == "application/json" @@ -34,8 +35,7 @@ def test_api_not_found(botclient): ftbot, client = botclient rc = client.post("/invalid_url") - assert rc.status_code == 404 - assert rc.content_type == "application/json" + assert_response(rc, 404) assert rc.json == {'status': 'error', 'reason': "There's no API call for http://localhost/invalid_url.", 'code': 404 @@ -46,24 +46,24 @@ def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING rc = client.post("/stop") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again rc = client.post("/stop") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot rc = client.post("/start") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again rc = client.post("/start") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'already running'} @@ -82,7 +82,7 @@ def test_api_reloadconf(botclient): ftbot, client = botclient rc = client.post("/reload_conf") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -92,19 +92,11 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] != 0 rc = client.post("/stopbuy") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 -def test_api_version(botclient): - ftbot, client = botclient - - rc = client.get("/version") - response_success_assert(rc) - assert rc.json == {"version": __version__} - - def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient @@ -130,7 +122,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) rc = client.get("/balance") - response_success_assert(rc) + assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 assert rc.json['currencies'][0] == { @@ -153,7 +145,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) rc = client.get("/count") - response_success_assert(rc) + assert_response(rc) assert rc.json["current"] == 0 assert rc.json["max"] == 1.0 @@ -161,7 +153,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() rc = client.get("/count") - response_success_assert(rc) + assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -177,6 +169,188 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) rc = client.get("/daily") - response_success_assert(rc) + assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) + + +def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/edge") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} + + +def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client.get("/profit") + assert_response(rc, 502) + assert len(rc.json) == 1 + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + ftbot.create_trade() + trade = Trade.query.first() + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + rc = client.get("/profit") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + rc = client.get("/profit") + assert_response(rc) + assert rc.json == {'avg_duration': '0:00:00', + 'best_pair': 'ETH/BTC', + 'best_rate': 6.2, + 'first_trade_date': 'just now', + 'latest_trade_date': 'just now', + 'profit_all_coin': 6.217e-05, + 'profit_all_fiat': 0, + 'profit_all_percent': 6.2, + 'profit_closed_coin': 6.217e-05, + 'profit_closed_fiat': 0, + 'profit_closed_percent': 6.2, + 'trade_count': 1 + } + + +def test_api_performance(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + + trade = Trade( + pair='LTC/ETH', + amount=1, + exchange='binance', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + + trade = Trade( + pair='XRP/ETH', + amount=5, + stake_amount=1, + exchange='binance', + open_rate=0.412, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.391 + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + Trade.session.flush() + + rc = client.get("/performance") + assert_response(rc) + assert len(rc.json) == 2 + assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] + + +def test_api_status(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client.get("/status") + assert_response(rc, 502) + assert rc.json == {'error': 'Error querying _status: no active trade'} + + ftbot.create_trade() + rc = client.get("/status") + assert_response(rc) + assert len(rc.json) == 1 + assert rc.json == [{'amount': 90.99181074, + 'base_currency': 'BTC', + 'close_date': None, + 'close_date_hum': None, + 'close_profit': None, + 'close_rate': None, + 'current_profit': -0.59, + 'current_rate': 1.098e-05, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_order': '(limit buy rem=0.00000000)', + 'open_rate': 1.099e-05, + 'pair': 'ETH/BTC', + 'stake_amount': 0.001, + 'stop_loss': 0.0, + 'stop_loss_pct': None, + 'trade_id': 1}] + + +def test_api_version(botclient): + ftbot, client = botclient + + rc = client.get("/version") + assert_response(rc) + assert rc.json == {"version": __version__} + + +def test_api_blacklist(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + + rc = client.get("/blacklist") + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], + "length": 2, + "method": "StaticPairList"} + + # Add ETH/BTC to blacklist + rc = client.post("/blacklist", data='{"blacklist": ["ETH/BTC"]}', + content_type='application/json') + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], + "length": 3, + "method": "StaticPairList"} + + +def test_api_whitelist(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + + rc = client.get("/whitelist") + assert_response(rc) + assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], + "length": 4, + "method": "StaticPairList"} + diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 946a9c819..4407859bf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -11,16 +11,17 @@ import arrow import pytest import requests -from freqtrade import (DependencyException, OperationalException, - TemporaryError, InvalidOrderException, constants) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError, constants) from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, patch_get_signal, - patch_exchange, patch_wallet) +from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, + patch_exchange, patch_get_signal, + patch_wallet) from freqtrade.worker import Worker @@ -59,7 +60,6 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) - def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests From 39afe4c7bd5cd92d14e728051f4b27409c9fda81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2019 07:07:51 +0200 Subject: [PATCH 418/928] Test flask app .run() --- freqtrade/rpc/api_server.py | 2 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 49 +++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4ddda307a..c2d83d1fb 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -141,7 +141,7 @@ class ApiServer(RPC): rest_ip = self._config['api_server']['listen_ip_address'] rest_port = self._config['api_server']['listen_port'] - logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') if not IPv4Address(rest_ip).is_loopback: logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") logger.warning("SECURITY WARNING - This is insecure please set to your loopback," diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 95ad7dbc3..41d682d2d 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -8,10 +8,11 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from freqtrade.__init__ import __version__ +from freqtrade.persistence import Trade from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) -from freqtrade.persistence import Trade +from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, + patch_get_signal) @pytest.fixture @@ -78,6 +79,49 @@ def test_api__init__(default_conf, mocker): assert apiserver._config == default_conf +def test_api_run(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + + # Monkey patch flask app + run_mock = MagicMock() + apiserver.app = MagicMock() + apiserver.app.run = run_mock + + assert apiserver._config == default_conf + apiserver.run() + assert run_mock.call_count == 1 + assert run_mock.call_args_list[0][1]["host"] == "127.0.0.1" + assert run_mock.call_args_list[0][1]["port"] == "8080" + + assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) + assert log_has("Starting Local Rest Server", caplog.record_tuples) + + # Test binding to public + caplog.clear() + run_mock.reset_mock() + apiserver._config.update({"api_server": {"enabled": True, + "listen_ip_address": "0.0.0.0", + "listen_port": "8089"}}) + apiserver.run() + + assert run_mock.call_count == 1 + assert run_mock.call_args_list[0][1]["host"] == "0.0.0.0" + assert run_mock.call_args_list[0][1]["port"] == "8089" + assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) + assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", + caplog.record_tuples) + assert log_has("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json", + caplog.record_tuples) + + def test_api_reloadconf(botclient): ftbot, client = botclient @@ -353,4 +397,3 @@ def test_api_whitelist(botclient, mocker, ticker, fee, markets): assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, "method": "StaticPairList"} - From 350c9037930406691fc5091fd1138d583dca33da Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 06:24:22 +0200 Subject: [PATCH 419/928] Test falsk crash --- freqtrade/tests/rpc/test_rpc_apiserver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 41d682d2d..8752207d0 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -121,6 +121,13 @@ def test_api_run(default_conf, mocker, caplog): "e.g 127.0.0.1 in config.json", caplog.record_tuples) + # Test crashing flask + caplog.clear() + apiserver.app.run = MagicMock(side_effect=Exception) + apiserver.run() + assert log_has("Api server failed to start, exception message is:", + caplog.record_tuples) + def test_api_reloadconf(botclient): ftbot, client = botclient From b700c64dc26cc25c6393930770479982c2cec447 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 06:51:23 +0200 Subject: [PATCH 420/928] Test forcebuy - cleanup some tests --- freqtrade/rpc/api_server.py | 5 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 68 +++++++++++++++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index c2d83d1fb..e7b64969f 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -314,7 +314,10 @@ class ApiServer(RPC): asset = request.json.get("pair") price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) - return self.rest_dump(trade.to_json()) + if trade: + return self.rest_dump(trade.to_json()) + else: + return self.rest_dump({"status": f"Error buying pair {asset}."}) @safe_rpc def _forcesell(self): diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 8752207d0..31a56321d 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -287,7 +287,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li } -def test_api_performance(botclient, mocker, ticker, fee, markets): +def test_api_performance(botclient, mocker, ticker, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -330,7 +330,7 @@ def test_api_performance(botclient, mocker, ticker, fee, markets): {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] -def test_api_status(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): +def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -378,7 +378,7 @@ def test_api_version(botclient): assert rc.json == {"version": __version__} -def test_api_blacklist(botclient, mocker, ticker, fee, markets): +def test_api_blacklist(botclient, mocker): ftbot, client = botclient rc = client.get("/blacklist") @@ -396,7 +396,7 @@ def test_api_blacklist(botclient, mocker, ticker, fee, markets): "method": "StaticPairList"} -def test_api_whitelist(botclient, mocker, ticker, fee, markets): +def test_api_whitelist(botclient): ftbot, client = botclient rc = client.get("/whitelist") @@ -404,3 +404,63 @@ def test_api_whitelist(botclient, mocker, ticker, fee, markets): assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, "method": "StaticPairList"} + + +def test_api_forcebuy(botclient, mocker, fee): + ftbot, client = botclient + + rc = client.post("/forcebuy", content_type='application/json', + data='{"pair": "ETH/BTC"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} + + # enable forcebuy + ftbot.config["forcebuy_enable"] = True + + fbuy_mock = MagicMock(return_value=None) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + rc = client.post("/forcebuy", content_type="application/json", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {"status": "Error buying pair ETH/BTC."} + + # Test creating trae + fbuy_mock = MagicMock(return_value=Trade( + pair='ETH/ETH', + amount=1, + exchange='bittrex', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + open_date=datetime.utcnow(), + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + )) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + + rc = client.post("/forcebuy", content_type="application/json", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {'amount': 1, + 'close_date': None, + 'close_date_hum': None, + 'close_rate': 0.265441, + 'initial_stop_loss': None, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_rate': 0.245441, + 'pair': 'ETH/ETH', + 'stake_amount': 1, + 'stop_loss': None, + 'stop_loss_pct': None, + 'trade_id': None} + + +# def test_api_sellbuy(botclient): + # TODO +# ftbot, client = botclient + + # rc = client.get("/forcesell ") From 01cd68a5aa2dbef6844845cef25c6d4a5eefc2ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 07:00:17 +0200 Subject: [PATCH 421/928] Test forcesell --- freqtrade/tests/rpc/test_rpc_apiserver.py | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 31a56321d..c3a8ab27a 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -459,8 +459,25 @@ def test_api_forcebuy(botclient, mocker, fee): 'trade_id': None} -# def test_api_sellbuy(botclient): - # TODO -# ftbot, client = botclient +def test_api_forcesell(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + patch_get_signal(ftbot, (True, False)) - # rc = client.get("/forcesell ") + rc = client.post("/forcesell", content_type="application/json", + data='{"tradeid": "1"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcesell: invalid argument"} + + ftbot.create_trade() + + rc = client.post("/forcesell", content_type="application/json", + data='{"tradeid": "1"}') + assert_response(rc) + assert rc.json == {'result': 'Created sell order for trade 1.'} From 5149ff7b120bbc431426566a708c48273c06d3b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 07:12:33 +0200 Subject: [PATCH 422/928] Move api to /api/v1 --- freqtrade/rpc/api_server.py | 47 ++++++++++------- freqtrade/tests/rpc/test_rpc_apiserver.py | 62 +++++++++++------------ scripts/rest_client.py | 2 +- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e7b64969f..6213256a4 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -7,12 +7,15 @@ from typing import Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder +from werkzeug.wsgi import DispatcherMiddleware from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) +BASE_URI = "/api/v1" + class ArrowJSONEncoder(JSONEncoder): def default(self, obj): @@ -60,7 +63,6 @@ class ApiServer(RPC): self._config = freqtrade.config self.app = Flask(__name__) - self.app.json_encoder = ArrowJSONEncoder # Register application handling @@ -105,29 +107,36 @@ class ApiServer(RPC): :return: """ # Actions to control the bot - self.app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) - self.app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) - self.app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - self.app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/start', 'start', + view_func=self._start, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', + view_func=self._stopbuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf', + view_func=self._reload_conf, methods=['POST']) # Info commands - self.app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - self.app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) - self.app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) - self.app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) - self.app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - self.app.add_url_rule('/performance', 'performance', view_func=self._performance, - methods=['GET']) - self.app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - self.app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', + view_func=self._balance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', + view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', + view_func=self._performance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/status', 'status', + view_func=self._status, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/version', 'version', + view_func=self._version, methods=['GET']) # Combined actions and infos - self.app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) - self.app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) - self.app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) - self.app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, + self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy', + view_func=self._forcebuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) # TODO: Implement the following diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index c3a8ab27a..6233811fd 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -9,7 +9,7 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade -from freqtrade.rpc.api_server import ApiServer +from freqtrade.rpc.api_server import ApiServer, BASE_URI from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) @@ -35,35 +35,35 @@ def assert_response(response, expected_code=200): def test_api_not_found(botclient): ftbot, client = botclient - rc = client.post("/invalid_url") + rc = client.post(f"{BASE_URI}/invalid_url") assert_response(rc, 404) - assert rc.json == {'status': 'error', - 'reason': "There's no API call for http://localhost/invalid_url.", - 'code': 404 + assert rc.json == {"status": "error", + "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", + "code": 404 } def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING - rc = client.post("/stop") + rc = client.post(f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again - rc = client.post("/stop") + rc = client.post(f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot - rc = client.post("/start") + rc = client.post(f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again - rc = client.post("/start") + rc = client.post(f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'already running'} @@ -132,7 +132,7 @@ def test_api_run(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client.post("/reload_conf") + rc = client.post(f"{BASE_URI}/reload_conf") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -142,7 +142,7 @@ def test_api_stopbuy(botclient): ftbot, client = botclient assert ftbot.config['max_open_trades'] != 0 - rc = client.post("/stopbuy") + rc = client.post(f"{BASE_URI}/stopbuy") assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 @@ -172,7 +172,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) - rc = client.get("/balance") + rc = client.get(f"{BASE_URI}/balance") assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 @@ -195,7 +195,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/count") + rc = client.get(f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 0 @@ -203,7 +203,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() - rc = client.get("/count") + rc = client.get(f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -219,7 +219,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/daily") + rc = client.get(f"{BASE_URI}/daily") assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) @@ -235,7 +235,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/edge") + rc = client.get(f"{BASE_URI}/edge") assert_response(rc, 502) assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} @@ -251,7 +251,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li markets=PropertyMock(return_value=markets) ) - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc, 502) assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -261,7 +261,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc, 502) assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -270,7 +270,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li trade.close_date = datetime.utcnow() trade.is_open = False - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc) assert rc.json == {'avg_duration': '0:00:00', 'best_pair': 'ETH/BTC', @@ -323,7 +323,7 @@ def test_api_performance(botclient, mocker, ticker, fee): Trade.session.add(trade) Trade.session.flush() - rc = client.get("/performance") + rc = client.get(f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json) == 2 assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, @@ -341,12 +341,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) - rc = client.get("/status") + rc = client.get(f"{BASE_URI}/status") assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} ftbot.create_trade() - rc = client.get("/status") + rc = client.get(f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 90.99181074, @@ -373,7 +373,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_version(botclient): ftbot, client = botclient - rc = client.get("/version") + rc = client.get(f"{BASE_URI}/version") assert_response(rc) assert rc.json == {"version": __version__} @@ -381,14 +381,14 @@ def test_api_version(botclient): def test_api_blacklist(botclient, mocker): ftbot, client = botclient - rc = client.get("/blacklist") + rc = client.get(f"{BASE_URI}/blacklist") assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, "method": "StaticPairList"} # Add ETH/BTC to blacklist - rc = client.post("/blacklist", data='{"blacklist": ["ETH/BTC"]}', + rc = client.post(f"{BASE_URI}/blacklist", data='{"blacklist": ["ETH/BTC"]}', content_type='application/json') assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], @@ -399,7 +399,7 @@ def test_api_blacklist(botclient, mocker): def test_api_whitelist(botclient): ftbot, client = botclient - rc = client.get("/whitelist") + rc = client.get(f"{BASE_URI}/whitelist") assert_response(rc) assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, @@ -409,7 +409,7 @@ def test_api_whitelist(botclient): def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient - rc = client.post("/forcebuy", content_type='application/json', + rc = client.post(f"{BASE_URI}/forcebuy", content_type='application/json', data='{"pair": "ETH/BTC"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} @@ -419,7 +419,7 @@ def test_api_forcebuy(botclient, mocker, fee): fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post("/forcebuy", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {"status": "Error buying pair ETH/BTC."} @@ -440,7 +440,7 @@ def test_api_forcebuy(botclient, mocker, fee): )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post("/forcebuy", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, @@ -470,14 +470,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): ) patch_get_signal(ftbot, (True, False)) - rc = client.post("/forcesell", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} ftbot.create_trade() - rc = client.post("/forcesell", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 81c4b66cc..54b08a03a 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -36,7 +36,7 @@ class FtRestClient(): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) - basepath = f"{self._serverurl}/{apipath}" + basepath = f"{self._serverurl}/api/v1/{apipath}" hd = {"Accept": "application/json", "Content-Type": "application/json" From 540d4bef1e146920dce1215441e951f8a70992cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:50:19 +0200 Subject: [PATCH 423/928] gracefully shutdown flask --- freqtrade/rpc/api_server.py | 51 +++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 6213256a4..3e1bf6195 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -7,7 +7,7 @@ from typing import Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder -from werkzeug.wsgi import DispatcherMiddleware +from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException @@ -74,8 +74,31 @@ class ApiServer(RPC): def cleanup(self) -> None: logger.info("Stopping API Server") - # TODO: Gracefully shutdown - right now it'll fail on /reload_conf - # since it's not terminated correctly. + self.srv.shutdown() + + def run(self): + """ + Method that runs flask app in its own thread forever. + Section to handle configuration and running of the Rest server + also to check and warn if not bound to a loopback, warn on security risk. + """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] + + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + if not IPv4Address(rest_ip).is_loopback: + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + # Run the Server + logger.info('Starting Local Rest Server') + try: + self.srv = make_server(rest_ip, rest_port, self.app) + self.srv.serve_forever() + except Exception: + logger.exception("Api server failed to start, exception message is:") + logger.info('Starting Local Rest Server_end') def send_msg(self, msg: Dict[str, str]) -> None: """ @@ -142,28 +165,6 @@ class ApiServer(RPC): # TODO: Implement the following # help (?) - def run(self): - """ Method that runs flask app in its own thread forever. - Section to handle configuration and running of the Rest server - also to check and warn if not bound to a loopback, warn on security risk. - """ - rest_ip = self._config['api_server']['listen_ip_address'] - rest_port = self._config['api_server']['listen_port'] - - logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') - if not IPv4Address(rest_ip).is_loopback: - logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") - logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") - - # Run the Server - logger.info('Starting Local Rest Server') - try: - self.app.run(host=rest_ip, port=rest_port) - except Exception: - logger.exception("Api server failed to start, exception message is:") - logger.info('Starting Local Rest Server_end') - def page_not_found(self, error): """ Return "404 not found", 404. From bfc57a6f6d3e076ebd02ec0bd68459429604d632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:50:29 +0200 Subject: [PATCH 424/928] Adapt tests to new method of starting flask --- freqtrade/tests/rpc/test_rpc_apiserver.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 6233811fd..1842d8828 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -6,10 +6,11 @@ from datetime import datetime from unittest.mock import ANY, MagicMock, PropertyMock import pytest +from flask import Flask from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade -from freqtrade.rpc.api_server import ApiServer, BASE_URI +from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) @@ -86,18 +87,17 @@ def test_api_run(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) - - # Monkey patch flask app run_mock = MagicMock() - apiserver.app = MagicMock() - apiserver.app.run = run_mock + mocker.patch('freqtrade.rpc.api_server.make_server', run_mock) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) assert apiserver._config == default_conf apiserver.run() assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][1]["host"] == "127.0.0.1" - assert run_mock.call_args_list[0][1]["port"] == "8080" + assert run_mock.call_args_list[0][0][0] == "127.0.0.1" + assert run_mock.call_args_list[0][0][1] == "8080" + assert isinstance(run_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) @@ -111,8 +111,9 @@ def test_api_run(default_conf, mocker, caplog): apiserver.run() assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][1]["host"] == "0.0.0.0" - assert run_mock.call_args_list[0][1]["port"] == "8089" + assert run_mock.call_args_list[0][0][0] == "0.0.0.0" + assert run_mock.call_args_list[0][0][1] == "8089" + assert isinstance(run_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -123,7 +124,7 @@ def test_api_run(default_conf, mocker, caplog): # Test crashing flask caplog.clear() - apiserver.app.run = MagicMock(side_effect=Exception) + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() assert log_has("Api server failed to start, exception message is:", caplog.record_tuples) From fd5012c04e4c92cdfab04102d85099abb30e282e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:54:36 +0200 Subject: [PATCH 425/928] Add test for api cleanup --- freqtrade/tests/rpc/test_rpc_apiserver.py | 42 +++++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 1842d8828..123988288 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -87,33 +87,34 @@ def test_api_run(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.make_server', run_mock) + server_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) assert apiserver._config == default_conf apiserver.run() - assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][0][0] == "127.0.0.1" - assert run_mock.call_args_list[0][0][1] == "8080" - assert isinstance(run_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "127.0.0.1" + assert server_mock.call_args_list[0][0][1] == "8080" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert hasattr(apiserver, "srv") assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) # Test binding to public caplog.clear() - run_mock.reset_mock() + server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", "listen_port": "8089"}}) apiserver.run() - assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][0][0] == "0.0.0.0" - assert run_mock.call_args_list[0][0][1] == "8089" - assert isinstance(run_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "0.0.0.0" + assert server_mock.call_args_list[0][0][1] == "8089" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -130,6 +131,25 @@ def test_api_run(default_conf, mocker, caplog): caplog.record_tuples) +def test_api_cleanup(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver.run() + stop_mock = MagicMock() + stop_mock.shutdown = MagicMock() + apiserver.srv = stop_mock + + apiserver.cleanup() + assert stop_mock.shutdown.call_count == 1 + assert log_has("Stopping API Server", caplog.record_tuples) + + def test_api_reloadconf(botclient): ftbot, client = botclient From c272e1ccdf1d94bee785e211713bae2aedda84d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:24:01 +0200 Subject: [PATCH 426/928] Add default rest config --- config_full.json.example | 5 +++++ scripts/rest_client.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 4c4ad3c58..6603540cf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -109,6 +109,11 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080 + }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", "forcebuy_enable": false, diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 54b08a03a..4bf46e6fd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -234,17 +234,19 @@ def load_config(configfile): return {} +def print_commands(): + # Print dynamic help for the different commands + client = FtRestClient(None) + print("Possible commands:") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + print(f"{x} {getattr(client, x).__doc__}") + + def main(args): - if args.get("show"): - # Print dynamic help for the different commands - client = FtRestClient(None) - print("Possible commands:") - for x, y in inspect.getmembers(client): - if not x.startswith('_'): - print(f"{x} {getattr(client, x).__doc__}") - - return + if args.get("help"): + print_commands() config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") @@ -256,6 +258,7 @@ def main(args): command = args["command"] if command not in m: logger.error(f"Command {command} not defined") + print_commands() return print(getattr(client, command)(*args["command_arguments"])) From 70fabebcb324f38e80ce15dae7b197c2f75c3d5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:24:22 +0200 Subject: [PATCH 427/928] Document rest api --- docs/rest-api.md | 184 +++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 185 insertions(+) create mode 100644 docs/rest-api.md diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 000000000..aeb1421c1 --- /dev/null +++ b/docs/rest-api.md @@ -0,0 +1,184 @@ +# REST API Usage + +## Configuration + +Enable the rest API by adding the api_server section to your configuration and setting `api_server.enabled` to `true`. + +Sample configuration: + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080 + }, +``` + +!!! Danger: Security warning + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will be able to control your bot. + +You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. + + +### Configuration with docker + +If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + }, +``` + +Add the following to your docker command: + +``` bash + -p 127.0.0.1:8080:8080 +``` + +A complete sample-command may then look as follows: + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -p 127.0.0.1:8080:8080 \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Danger "Security warning" + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others will be able to control your bot. + +## Consuming the API + +You can consume the API by using the script `scripts/rest_client.py`. +The client script only requires the `requests` module, so FreqTrade does not need to be installed on the system. + +``` bash +python3 scripts/rest_client.py [optional parameters] +``` + +By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. + +### Minimalistic client config + +``` json +{ + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + } +} +``` + +``` bash +python3 scripts/rest_client.py --config rest_config.json [optional parameters] +``` + +## Available commands + +| Command | Default | Description | +|----------|---------|-------------| +| `start` | | Starts the trader +| `stop` | | Stops the trader +| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. +| `reload_conf` | | Reloads the configuration file +| `status` | | Lists all open trades +| `status table` | | List all open trades in a table format +| `count` | | Displays number of trades used and available +| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance +| `forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). +| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). +| `forcebuy [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `performance` | | Show performance of each finished trade grouped by pair +| `balance` | | Show account balance per currency +| `daily ` | 7 | Shows profit or loss per day, over the last n days +| `whitelist` | | Show the current whitelist +| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. +| `edge` | | Show validated pairs by Edge if it is enabled. +| `version` | | Show version + +Possible commands can be listed from the rest-client script using the `help` command. + +``` bash +python3 scripts/rest_client.py help +``` + +``` output +Possible commands: +balance + Get the account balance + :returns: json object + +blacklist + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: json object + +count + Returns the amount of open trades + :returns: json object + +daily + Returns the amount of open trades + :returns: json object + +edge + Returns information about edge + :returns: json object + +forcebuy + Buy an asset + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :returns: json object of the trade + +forcesell + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + +performance + Returns the performance of the different coins + :returns: json object + +profit + Returns the profit summary + :returns: json object + +reload_conf + Reload configuration + :returns: json object + +start + Start the bot if it's in stopped state. + :returns: json object + +status + Get the status of open trades + :returns: json object + +stop + Stop the bot. Use start to restart + :returns: json object + +stopbuy + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + +version + Returns the version of the bot + :returns: json object containing the version + +whitelist + Show the current whitelist + :returns: json object + + +``` diff --git a/mkdocs.yml b/mkdocs.yml index ecac265c1..547c527a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md + - REST API: rest-api.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge positioning: edge.md From f2e4689d0c0fb4afb007233752c9587c581288b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:29:38 +0200 Subject: [PATCH 428/928] Cleanup script --- docs/rest-api.md | 3 --- scripts/rest_client.py | 8 +------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index aeb1421c1..728941c88 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -19,7 +19,6 @@ Sample configuration: You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. - ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. @@ -179,6 +178,4 @@ version whitelist Show the current whitelist :returns: json object - - ``` diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4bf46e6fd..0669feb8c 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -216,12 +216,6 @@ def add_arguments(): ) args = parser.parse_args() - # if len(argv) == 1: - # print('\nThis script accepts the following arguments') - # print('- daily (int) - Where int is the number of days to report back. daily 3') - # print('- start - this will start the trading thread') - # print('- stop - this will start the trading thread') - # print('- there will be more....\n') return vars(args) @@ -235,7 +229,7 @@ def load_config(configfile): def print_commands(): - # Print dynamic help for the different commands + # Print dynamic help for the different commands using the commands doc-strings client = FtRestClient(None) print("Possible commands:") for x, y in inspect.getmembers(client): From 9385a27ff031d9f461a47f2d12ef3a6b95b85dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:34:30 +0200 Subject: [PATCH 429/928] Sort imports --- freqtrade/rpc/api_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3e1bf6195..fca7fa702 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,6 +1,6 @@ import logging import threading -from datetime import datetime, date +from datetime import date, datetime from ipaddress import IPv4Address from typing import Dict From 79cac36b34d0dde28a0bc6d56a0735f8cedf3b52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:36:40 +0200 Subject: [PATCH 430/928] Reference reest api in main documentation page --- docs/index.md | 12 ++++++++---- scripts/rest_client.py | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index 9abc71747..9fbc0519c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,8 @@ Freqtrade is a cryptocurrency trading bot written in Python. We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it. - ## Features + - Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux. - Persistence: Persistence is achieved through sqlite database. - Dry-run mode: Run the bot without playing money. @@ -31,17 +31,19 @@ Freqtrade is a cryptocurrency trading bot written in Python. - Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume. - Blacklist crypto-currencies: Select which crypto-currency you want to avoid. - - Manageable via Telegram: Manage the bot with Telegram. + - Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API. - Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### Up to date clock + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -56,12 +59,13 @@ To run this bot we recommend you a cloud instance with a minimum of: - virtualenv (Recommended) - Docker (Recommended) - ## Support + Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. ## Ready to try? + Begin by reading our installation guide [here](installation). diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 0669feb8c..b31a1de50 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -11,7 +11,6 @@ import argparse import json import logging import inspect -from typing import List from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path From e6ae890defcbf32ddfe1fb990e5ed3f5a948c6eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 13:36:51 +0200 Subject: [PATCH 431/928] small adjustments after first feedback --- docs/rest-api.md | 4 ++-- freqtrade/rpc/api_server.py | 16 +++++----------- freqtrade/tests/rpc/test_rpc_apiserver.py | 6 +++--- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 728941c88..95eec3020 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -15,7 +15,7 @@ Sample configuration: ``` !!! Danger: Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will be able to control your bot. + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will potentially be able to control your bot. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. @@ -50,7 +50,7 @@ docker run -d \ ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others will be able to control your bot. + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. ## Consuming the API diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index fca7fa702..923605e7b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -66,7 +66,6 @@ class ApiServer(RPC): self.app.json_encoder = ArrowJSONEncoder # Register application handling - self.register_rest_other() self.register_rest_rpc_urls() thread = threading.Thread(target=self.run, daemon=True) @@ -92,13 +91,13 @@ class ApiServer(RPC): "e.g 127.0.0.1 in config.json") # Run the Server - logger.info('Starting Local Rest Server') + logger.info('Starting Local Rest Server.') try: self.srv = make_server(rest_ip, rest_port, self.app) self.srv.serve_forever() except Exception: - logger.exception("Api server failed to start, exception message is:") - logger.info('Starting Local Rest Server_end') + logger.exception("Api server failed to start.") + logger.info('Local Rest Server started.') def send_msg(self, msg: Dict[str, str]) -> None: """ @@ -114,13 +113,6 @@ class ApiServer(RPC): def rest_error(self, error_msg): return jsonify({"error": error_msg}), 502 - def register_rest_other(self): - """ - Registers flask app URLs that are not calls to functionality in rpc.rpc. - :return: - """ - self.app.register_error_handler(404, self.page_not_found) - def register_rest_rpc_urls(self): """ Registers flask app URLs that are calls to functonality in rpc.rpc. @@ -129,6 +121,8 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ + self.app.register_error_handler(404, self.page_not_found) + # Actions to control the bot self.app.add_url_rule(f'{BASE_URI}/start', 'start', view_func=self._start, methods=['POST']) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 123988288..5b9e538b5 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -101,7 +101,7 @@ def test_api_run(default_conf, mocker, caplog): assert hasattr(apiserver, "srv") assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) - assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) # Test binding to public caplog.clear() @@ -116,7 +116,7 @@ def test_api_run(default_conf, mocker, caplog): assert server_mock.call_args_list[0][0][1] == "8089" assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) - assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", caplog.record_tuples) assert log_has("SECURITY WARNING - This is insecure please set to your loopback," @@ -127,7 +127,7 @@ def test_api_run(default_conf, mocker, caplog): caplog.clear() mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() - assert log_has("Api server failed to start, exception message is:", + assert log_has("Api server failed to start.", caplog.record_tuples) From 2cf07e218590182530f2614d96ac245e682e6064 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 13:39:12 +0200 Subject: [PATCH 432/928] rename exception handlers --- freqtrade/rpc/api_server.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 923605e7b..6792cc9a0 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -41,7 +41,7 @@ class ApiServer(RPC): This class starts a none blocking thread the api server runs within """ - def safe_rpc(func): + def rpc_catch_errors(func): def func_wrapper(self, *args, **kwargs): @@ -169,6 +169,7 @@ class ApiServer(RPC): 'code': 404 }), 404 + @rpc_catch_errors def _start(self): """ Handler for /start. @@ -177,6 +178,7 @@ class ApiServer(RPC): msg = self._rpc_start() return self.rest_dump(msg) + @rpc_catch_errors def _stop(self): """ Handler for /stop. @@ -185,6 +187,7 @@ class ApiServer(RPC): msg = self._rpc_stop() return self.rest_dump(msg) + @rpc_catch_errors def _stopbuy(self): """ Handler for /stopbuy. @@ -193,12 +196,14 @@ class ApiServer(RPC): msg = self._rpc_stopbuy() return self.rest_dump(msg) + @rpc_catch_errors def _version(self): """ Prints the bot's version """ return self.rest_dump({"version": __version__}) + @rpc_catch_errors def _reload_conf(self): """ Handler for /reload_conf. @@ -207,7 +212,7 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) - @safe_rpc + @rpc_catch_errors def _count(self): """ Handler for /count. @@ -216,7 +221,7 @@ class ApiServer(RPC): msg = self._rpc_count() return self.rest_dump(msg) - @safe_rpc + @rpc_catch_errors def _daily(self): """ Returns the last X days trading stats summary. @@ -233,7 +238,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _edge(self): """ Returns information related to Edge. @@ -243,7 +248,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _profit(self): """ Handler for /profit. @@ -259,7 +264,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _performance(self): """ Handler for /performance. @@ -273,7 +278,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _status(self): """ Handler for /status. @@ -283,7 +288,7 @@ class ApiServer(RPC): results = self._rpc_trade_status() return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _balance(self): """ Handler for /balance. @@ -293,7 +298,7 @@ class ApiServer(RPC): results = self._rpc_balance(self._config.get('fiat_display_currency', '')) return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _whitelist(self): """ Handler for /whitelist. @@ -301,7 +306,7 @@ class ApiServer(RPC): results = self._rpc_whitelist() return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _blacklist(self): """ Handler for /blacklist. @@ -310,7 +315,7 @@ class ApiServer(RPC): results = self._rpc_blacklist(add) return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _forcebuy(self): """ Handler for /forcebuy. @@ -323,7 +328,7 @@ class ApiServer(RPC): else: return self.rest_dump({"status": f"Error buying pair {asset}."}) - @safe_rpc + @rpc_catch_errors def _forcesell(self): """ Handler for /forcesell. From 8d8b4a69b759c0774e913abc26c82105c18d6f3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 May 2019 09:03:56 +0200 Subject: [PATCH 433/928] Clearly warn about using future data during strategy development --- docs/bot-optimization.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 5e080eab1..97166a736 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -53,6 +53,12 @@ file as reference.** It is therefore best to use vectorized operations (across the whole dataframe, not loops) and avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle. +!!! Warning Using future data + Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author + needs to take care to avoid having the strategy utilize data from the future. + Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour). + They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly dry-run tests. + ### Customize Indicators Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. From f93e6ad0f6461fee649cfaeeed2f2400ada8d0e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 May 2019 09:07:43 +0200 Subject: [PATCH 434/928] Rename strategy customization file --- docs/bot-usage.md | 4 ++-- docs/{bot-optimization.md => strategy-customization.md} | 0 mkdocs.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/{bot-optimization.md => strategy-customization.md} (100%) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 2b2fef640..cb98e1ea5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -103,7 +103,7 @@ If the bot does not find your strategy file, it will display in an error message the reason (File not found, or errors in your code). Learn more about strategy file in -[optimize your bot](bot-optimization.md). +[Strategy Customization](strategy-customization.md). ### How to use **--strategy-path**? @@ -296,4 +296,4 @@ in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc. ## Next step The optimal strategy of the bot will change with time depending of the market trends. The next step is to -[optimize your bot](bot-optimization.md). +[Strategy Customization](strategy-customization.md). diff --git a/docs/bot-optimization.md b/docs/strategy-customization.md similarity index 100% rename from docs/bot-optimization.md rename to docs/strategy-customization.md diff --git a/mkdocs.yml b/mkdocs.yml index ecac265c1..9932ff316 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ nav: - About: index.md - Installation: installation.md - Configuration: configuration.md - - Custom Strategy: bot-optimization.md + - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md - Start the bot: bot-usage.md - Control the bot: From fc96da869a4d0b5051eefd8e437b088dc35cf941 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 May 2019 16:07:16 +0200 Subject: [PATCH 435/928] Fix grammar messup --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 97166a736..51540f690 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -57,7 +57,7 @@ file as reference.** Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author needs to take care to avoid having the strategy utilize data from the future. Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour). - They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly dry-run tests. + They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly in dry-runs. ### Customize Indicators From e7b9bc6808be9dc54a94e43831a60a55dcc04013 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 20 May 2019 12:27:30 +0300 Subject: [PATCH 436/928] minor: remove noisy useless debug message --- freqtrade/persistence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 6088dba72..e64e0b89c 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -238,7 +238,6 @@ class Trade(_DECL_BASE): """ Adjust the max_rate and min_rate. """ - logger.debug("Adjusting min/max rates") self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate) From 703fdb2bc637efa73e1515afcf8628969cb7a3aa Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:07 +0000 Subject: [PATCH 437/928] Update scipy from 1.2.1 to 1.3.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 78585f8f5..da87f56d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ numpy==1.16.3 pandas==0.24.2 -scipy==1.2.1 +scipy==1.3.0 From de95e50804addaafb785f1392a0a10075f523a0b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:08 +0000 Subject: [PATCH 438/928] Update ccxt from 1.18.523 to 1.18.551 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4f7309a6a..8139d2cbb 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.523 +ccxt==1.18.551 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.1 From 3404bb1865b31823bd978ec3084f66c6955f63e2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:09 +0000 Subject: [PATCH 439/928] Update arrow from 0.13.1 to 0.13.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 8139d2cbb..4ec3a0092 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.551 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 -arrow==0.13.1 +arrow==0.13.2 cachetools==3.1.0 requests==2.21.0 urllib3==1.24.2 # pyup: ignore From 34c7ac8926079673cbf92f7a204f08daff38231b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:10 +0000 Subject: [PATCH 440/928] Update requests from 2.21.0 to 2.22.0 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4ec3a0092..deea30faf 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -5,7 +5,7 @@ SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 cachetools==3.1.0 -requests==2.21.0 +requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 scikit-learn==0.21.0 From 5b24ac78981946bf334d19e28c14a180add4431c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:11 +0000 Subject: [PATCH 441/928] Update scikit-learn from 0.21.0 to 0.21.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index deea30faf..4ba4426b7 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.0 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 -scikit-learn==0.21.0 +scikit-learn==0.21.1 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From 04e13eed7dbc006bd17ea50dab6da94317a39750 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 20 May 2019 15:36:13 +0000 Subject: [PATCH 442/928] Update filelock from 3.0.10 to 3.0.12 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4ba4426b7..3f755b8c0 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -17,7 +17,7 @@ coinmarketcap==5.0.3 # Required for hyperopt scikit-optimize==0.5.2 -filelock==3.0.10 +filelock==3.0.12 # find first, C search in arrays py_find_1st==1.1.3 From 96a34f753b0be9a86e09eb9fd00aeace08302bfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 19:48:12 +0200 Subject: [PATCH 443/928] Adapt test to new output from arrow --- freqtrade/tests/rpc/test_rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 6ce543f3d..c3fcd62fb 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -119,7 +119,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: freqtradebot.create_trade() result = rpc._rpc_status_table() - assert 'just now' in result['Since'].all() + assert 'instantly' in result['Since'].all() assert 'ETH/BTC' in result['Pair'].all() assert '-0.59%' in result['Profit'].all() @@ -128,7 +128,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} result = rpc._rpc_status_table() - assert 'just now' in result['Since'].all() + assert 'instantly' in result['Since'].all() assert 'ETH/BTC' in result['Pair'].all() assert 'nan%' in result['Profit'].all() From 349c0619aa30dba6fe38d70575cea7b710b23e27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 May 2019 20:06:26 +0200 Subject: [PATCH 444/928] Move startup to freqtradebot --- freqtrade/freqtradebot.py | 8 ++++++++ freqtrade/worker.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8b29d6d40..425b7e3a9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -89,6 +89,14 @@ class FreqtradeBot(object): self.rpc.cleanup() persistence.cleanup() + def startup(self) -> None: + """ + Called on startup and after reloading the bot - triggers notifications and + performs startup tasks + : return: None + """ + self.rpc.startup_messages(self.config, self.pairlists) + def process(self) -> bool: """ Queries the persistence layer for open trades and handles them, diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 19a570505..c224b4ee5 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -91,7 +91,7 @@ class Worker(object): }) logger.info('Changing state to: %s', state.name) if state == State.RUNNING: - self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists) + self.freqtrade.startup() if state == State.STOPPED: # Ping systemd watchdog before sleeping in the stopped state From 6a5daab520b4f26181abafe44a024777dfafa181 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 07:06:40 +0200 Subject: [PATCH 445/928] add logic for stoploss reinitialization after startup --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence.py | 20 ++++++++++++++++++++ freqtrade/tests/test_freqtradebot.py | 1 + 3 files changed, 23 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 425b7e3a9..7d5cddd6c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -96,6 +96,8 @@ class FreqtradeBot(object): : return: None """ self.rpc.startup_messages(self.config, self.pairlists) + # Adjust stoploss if it was changed + Trade.stoploss_reinitialization(self.strategy.stoploss) def process(self) -> bool: """ diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e64e0b89c..3fc9a189e 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -421,3 +421,23 @@ class Trade(_DECL_BASE): Query trades from persistence layer """ return Trade.query.filter(Trade.is_open.is_(True)).all() + + @staticmethod + def stoploss_reinitialization(desired_stoploss): + """ + Adjust initial Stoploss to desired stoploss for all open trades. + """ + for trade in Trade.get_open_trades(): + logger.info("Found open trade: %s", trade) + + # skip case if trailing-stop changed the stoploss already. + if (trade.stop_loss == trade.initial_stop_loss + and trade.initial_stop_loss_pct != desired_stoploss): + # Stoploss value got changed + + logger.info(f"Stoploss for {trade} needs adjustment.") + logger.info(f"Stoploss: {trade.initial_stop_loss_pct}: {desired_stoploss}") + # Force reset of stoploss + trade.stop_loss = None + trade.adjust_stop_loss(trade.open_rate, desired_stoploss) + logger.info(f"new stoploss: {trade.stop_loss}, ") diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 67b05ac3e..c683f6273 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -114,6 +114,7 @@ def test_cleanup(mocker, default_conf, caplog) -> None: def test_worker_running(mocker, default_conf, caplog) -> None: mock_throttle = MagicMock() mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) + mocker.patch('freqtrade.persistence.Trade.adjust_initial_stoploss', MagicMock()) worker = get_patched_worker(mocker, default_conf) From 9f54181494a7b211a7f0cd4213150c36f4193634 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 07:12:54 +0200 Subject: [PATCH 446/928] Add test for stoploss_reinit --- freqtrade/tests/test_persistence.py | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 8c15fa8e8..93bad0797 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -777,3 +777,64 @@ def test_to_json(default_conf, fee): 'stop_loss_pct': None, 'initial_stop_loss': None, 'initial_stop_loss_pct': None} + + +def test_stoploss_reinitialization(default_conf, fee): + init(default_conf) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=10, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + max_rate=1, + ) + + trade.adjust_stop_loss(trade.open_rate, 0.05, True) + assert trade.stop_loss == 0.95 + assert trade.stop_loss_pct == -0.05 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + Trade.session.add(trade) + + # Lower stoploss + Trade.stoploss_reinitialization(0.06) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 0.94 + assert trade_adj.stop_loss_pct == -0.06 + assert trade_adj.initial_stop_loss == 0.94 + assert trade_adj.initial_stop_loss_pct == -0.06 + + # Raise stoploss + Trade.stoploss_reinitialization(0.04) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 0.96 + assert trade_adj.stop_loss_pct == -0.04 + assert trade_adj.initial_stop_loss == 0.96 + assert trade_adj.initial_stop_loss_pct == -0.04 + + + # Trailing stoploss (move stoplos up a bit) + trade.adjust_stop_loss(1.02, 0.04) + assert trade_adj.stop_loss == 0.9792 + assert trade_adj.initial_stop_loss == 0.96 + + Trade.stoploss_reinitialization(0.04) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + # Stoploss should not change in this case. + assert trade_adj.stop_loss == 0.9792 + assert trade_adj.stop_loss_pct == -0.04 + assert trade_adj.initial_stop_loss == 0.96 + assert trade_adj.initial_stop_loss_pct == -0.04 From 53af8f331d985fc559d64e0d5808f9f57e515d70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 19:35:19 +0200 Subject: [PATCH 447/928] Deep-copy default_conf for edge config --- freqtrade/tests/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0bff1d5e9..692fda368 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -2,6 +2,7 @@ import json import logging import re +from copy import deepcopy from datetime import datetime from functools import reduce from unittest.mock import MagicMock, PropertyMock @@ -942,9 +943,10 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): - default_conf['max_open_trades'] = -1 - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - default_conf['edge'] = { + conf = deepcopy(default_conf) + conf['max_open_trades'] = -1 + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + conf['edge'] = { "enabled": True, "process_throttle_secs": 1800, "calculate_since_number_of_days": 14, @@ -960,4 +962,4 @@ def edge_conf(default_conf): "remove_pumps": False } - return default_conf + return conf From a39cdd3b2b3b59719bb9b5e53f89f1085776e7dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 19:35:48 +0200 Subject: [PATCH 448/928] Exclude Edge from startup-stoploss calc Edge would recalculate / reevaluate stoploss values on startup, so these values are not reliable --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/tests/test_freqtradebot.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7d5cddd6c..0121512ee 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -96,8 +96,9 @@ class FreqtradeBot(object): : return: None """ self.rpc.startup_messages(self.config, self.pairlists) - # Adjust stoploss if it was changed - Trade.stoploss_reinitialization(self.strategy.stoploss) + if not self.edge: + # Adjust stoploss if it was changed + Trade.stoploss_reinitialization(self.strategy.stoploss) def process(self) -> bool: """ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index c683f6273..48eb51b54 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -3136,10 +3136,27 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None: assert rate == 0.043936 -def test_startup_messages(default_conf, mocker): +def test_startup_state(default_conf, mocker): default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} } mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) worker = get_patched_worker(mocker, default_conf) assert worker.state is State.RUNNING + + +def test_startup_trade_reinit(default_conf, edge_conf, mocker): + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + reinit_mock = MagicMock() + mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', reinit_mock) + + ftbot = get_patched_freqtradebot(mocker, default_conf) + ftbot.startup() + assert reinit_mock.call_count == 1 + + reinit_mock.reset_mock() + + ftbot = get_patched_freqtradebot(mocker, edge_conf) + ftbot.startup() + assert reinit_mock.call_count == 0 From 11fd8a59af3fb92f405e59f3db47f4d82be809f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 20:06:13 +0200 Subject: [PATCH 449/928] cleanup stoploss documentations --- docs/configuration.md | 10 ++-------- docs/stoploss.md | 15 ++++++++++++--- docs/strategy-customization.md | 5 ++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index df116b3c2..d097712c6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -131,17 +131,11 @@ If it is not set in either Strategy or Configuration, a default of 1000% `{"0": ### Understand stoploss -The `stoploss` configuration parameter is loss in percentage that should trigger a sale. -For example, value `-0.10` will cause immediate sell if the -profit dips below -10% for a given trade. This parameter is optional. - -Most of the strategy files already include the optimal `stoploss` -value. This parameter is optional. If you use it in the configuration file, it will take over the -`stoploss` value from the strategy file. +Go to the [stoploss documentation](stoploss.md) for more details. ### Understand trailing stoploss -Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss. +Go to the [trailing stoploss Documentation](stoploss.md#trailing-stop-loss) for details on trailing stoploss. ### Understand initial_state diff --git a/docs/stoploss.md b/docs/stoploss.md index cbe4fd3c4..4c731940e 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -1,4 +1,14 @@ -# Stop Loss support +# Stop Loss + +The `stoploss` configuration parameter is loss in percentage that should trigger a sale. +For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. + +Most of the strategy files already include the optimal `stoploss` +value. This parameter is optional. If you use it in the configuration file, it will take over the +`stoploss` value from the strategy file. + + +## Stop Loss support At this stage the bot contains the following stoploss support modes: @@ -16,13 +26,12 @@ In case of stoploss on exchange there is another parameter called `stoploss_on_e !!! Note Stoploss on exchange is only supported for Binance as of now. - ## Static Stop Loss This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss. -## Trail Stop Loss +## Trailing Stop Loss The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. To enable this Feauture all you have to do is to define the configuration element: diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 51540f690..1256f4a32 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -218,9 +218,12 @@ stoploss = -0.10 ``` This would signify a stoploss of -10%. + +For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md). + If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). -For more information on order_types please look [here](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md#understand-order_types). +For more information on order_types please look to [here](configuration.md#understand-order_types). ### Ticker interval From 58ced364451c28ba2e6c71fbb5f9f4f6085de975 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 20:06:35 +0200 Subject: [PATCH 450/928] Add documentation for stoploss updates --- docs/stoploss.md | 10 ++++++++++ docs/strategy-customization.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 4c731940e..975c2aeb5 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -72,3 +72,13 @@ The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured`stoploss`. + +## Changing stoploss on open trades + +A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works). + +The new stoploss value will be applied to open trades (and corresponding log-messages will be generated). + +### Limitations + +Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted, or if [Edge](edge.md) is enabled (since Edge would recalculate stoploss based on the current market situation). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 1256f4a32..85d8104b0 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -223,7 +223,7 @@ For the full documentation on stoploss features, look at the dedicated [stoploss If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). -For more information on order_types please look to [here](configuration.md#understand-order_types). +For more information on order_types please look [here](configuration.md#understand-order_types). ### Ticker interval From 51aa469f67e0ed3c7504e444063032e6d60f9553 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 May 2019 20:13:01 +0200 Subject: [PATCH 451/928] Cleanups --- docs/stoploss.md | 1 - freqtrade/freqtradebot.py | 1 - freqtrade/persistence.py | 1 - freqtrade/tests/test_freqtradebot.py | 2 +- freqtrade/tests/test_persistence.py | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 975c2aeb5..f5e2f8df6 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -7,7 +7,6 @@ Most of the strategy files already include the optimal `stoploss` value. This parameter is optional. If you use it in the configuration file, it will take over the `stoploss` value from the strategy file. - ## Stop Loss support At this stage the bot contains the following stoploss support modes: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0121512ee..81c9dc5d3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -93,7 +93,6 @@ class FreqtradeBot(object): """ Called on startup and after reloading the bot - triggers notifications and performs startup tasks - : return: None """ self.rpc.startup_messages(self.config, self.pairlists) if not self.edge: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 3fc9a189e..ed09f6f22 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -436,7 +436,6 @@ class Trade(_DECL_BASE): # Stoploss value got changed logger.info(f"Stoploss for {trade} needs adjustment.") - logger.info(f"Stoploss: {trade.initial_stop_loss_pct}: {desired_stoploss}") # Force reset of stoploss trade.stop_loss = None trade.adjust_stop_loss(trade.open_rate, desired_stoploss) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 48eb51b54..2587b1b36 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -114,7 +114,7 @@ def test_cleanup(mocker, default_conf, caplog) -> None: def test_worker_running(mocker, default_conf, caplog) -> None: mock_throttle = MagicMock() mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - mocker.patch('freqtrade.persistence.Trade.adjust_initial_stoploss', MagicMock()) + mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock()) worker = get_patched_worker(mocker, default_conf) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 93bad0797..3312bc21d 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -822,7 +822,6 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.initial_stop_loss == 0.96 assert trade_adj.initial_stop_loss_pct == -0.04 - # Trailing stoploss (move stoplos up a bit) trade.adjust_stop_loss(1.02, 0.04) assert trade_adj.stop_loss == 0.9792 From 11dce9128193184623d9a4896d503c351ef37faf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 21 May 2019 20:49:02 +0300 Subject: [PATCH 452/928] data/history minor cleanup --- freqtrade/data/history.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 86d3c3071..27e68b533 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -63,12 +63,8 @@ def load_tickerdata_file( Load a pair from file, either .json.gz or .json :return tickerlist or None if unsuccesful """ - path = make_testdata_path(datadir) - pair_s = pair.replace('/', '_') - file = path.joinpath(f'{pair_s}-{ticker_interval}.json') - - pairdata = misc.file_load_json(file) - + filename = pair_data_filename(datadir, pair, ticker_interval) + pairdata = misc.file_load_json(filename) if not pairdata: return None @@ -142,11 +138,18 @@ def load_data(datadir: Optional[Path], return result -def make_testdata_path(datadir: Optional[Path]) -> Path: +def make_datadir_path(datadir: Optional[Path]) -> Path: """Return the path where testdata files are stored""" return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() +def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path: + path = make_datadir_path(datadir) + pair_s = pair.replace("/", "_") + filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') + return filename + + def load_cached_data_for_updating(filename: Path, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: @@ -209,9 +212,7 @@ def download_pair_history(datadir: Optional[Path], ) try: - path = make_testdata_path(datadir) - filepair = pair.replace("/", "_") - filename = path.joinpath(f'{filepair}-{ticker_interval}.json') + filename = pair_data_filename(datadir, pair, ticker_interval) logger.info( f'Download history data for pair: "{pair}", interval: {ticker_interval} ' @@ -236,8 +237,9 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True - except Exception: + except Exception as e: logger.error( - f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}.' + f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' + f'Error: {e}' ) return False From 7cb753754b039898c3cfae2dd8049068cda6b4a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 21 May 2019 20:49:19 +0300 Subject: [PATCH 453/928] tests adjusted --- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/data/test_history.py | 7 ++++--- freqtrade/tests/test_misc.py | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index dd7cbe0d9..4c0426b93 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -2,12 +2,12 @@ import pytest from pandas import DataFrame from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.data.history import make_testdata_path +from freqtrade.data.history import make_datadir_path def test_load_backtest_data(): - filename = make_testdata_path(None) / "backtest-result_test.json" + filename = make_datadir_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"] diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 15442f577..a37b42351 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, load_tickerdata_file, - make_testdata_path, + make_datadir_path, trim_tickerlist) from freqtrade.misc import file_dump_json from freqtrade.tests.conftest import get_patched_exchange, log_has @@ -136,7 +136,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau def test_testdata_path() -> None: - assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) + assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_datadir_path(None)) def test_load_cached_data_for_updating(mocker) -> None: @@ -321,7 +321,8 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def _clean_test_file(file1_1) _clean_test_file(file1_5) assert log_has( - 'Failed to download history data for pair: "MEME/BTC", interval: 1m.', + 'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' + 'Error: File Error', caplog.record_tuples ) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 2da6b8718..c7bcf7edf 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, file_dump_json, file_load_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file, make_testdata_path +from freqtrade.data.history import load_tickerdata_file, pair_data_filename from freqtrade.strategy.default_strategy import DefaultStrategy @@ -60,13 +60,13 @@ def test_file_dump_json(mocker) -> None: def test_file_load_json(mocker) -> None: # 7m .json does not exist - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '7m')) assert not ret # 1m json exists (but no .gz exists) - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '1m')) assert ret # 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '8m')) assert ret From 98eeec31451c844bbdabc0a8c8dacd2474122596 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:04:58 +0300 Subject: [PATCH 454/928] renaming of make_testdata_path reverted --- freqtrade/data/history.py | 4 ++-- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/data/test_history.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 27e68b533..3bec63926 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -138,13 +138,13 @@ def load_data(datadir: Optional[Path], return result -def make_datadir_path(datadir: Optional[Path]) -> Path: +def make_testdata_path(datadir: Optional[Path]) -> Path: """Return the path where testdata files are stored""" return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path: - path = make_datadir_path(datadir) + path = make_testdata_path(datadir) pair_s = pair.replace("/", "_") filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') return filename diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4c0426b93..dd7cbe0d9 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -2,12 +2,12 @@ import pytest from pandas import DataFrame from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.data.history import make_datadir_path +from freqtrade.data.history import make_testdata_path def test_load_backtest_data(): - filename = make_datadir_path(None) / "backtest-result_test.json" + filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"] diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index a37b42351..0d4210d3a 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, load_tickerdata_file, - make_datadir_path, + make_testdata_path, trim_tickerlist) from freqtrade.misc import file_dump_json from freqtrade.tests.conftest import get_patched_exchange, log_has @@ -136,7 +136,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau def test_testdata_path() -> None: - assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_datadir_path(None)) + assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) def test_load_cached_data_for_updating(mocker) -> None: From 2c9a519c5e359ba2fea353bfd29155c06c8a2d3b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:21:36 +0300 Subject: [PATCH 455/928] edge: handle properly the 'No trades' case --- freqtrade/edge/__init__.py | 1 + freqtrade/optimize/edge_cli.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4801c6cb3..5c7252d88 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -139,6 +139,7 @@ class Edge(): # If no trade found then exit if len(trades) == 0: + logger.info("No trades created.") return False # Fill missing, calculable columns, profit, duration , abs etc. diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 9b628cf2e..818c1e050 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -73,9 +73,10 @@ class EdgeCli(object): floatfmt=floatfmt, tablefmt="pipe") def start(self) -> None: - self.edge.calculate() - print('') # blank like for readability - print(self._generate_edge_table(self.edge._cached_pairs)) + result = self.edge.calculate() + if result: + print('') # blank like for readability + print(self._generate_edge_table(self.edge._cached_pairs)) def setup_configuration(args: Namespace) -> Dict[str, Any]: From 406e266bb4ea12caaaaf86b6ddde5369e7450fea Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:34:35 +0300 Subject: [PATCH 456/928] typo in comment fixed --- freqtrade/optimize/edge_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 818c1e050..d37b930b8 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -75,7 +75,7 @@ class EdgeCli(object): def start(self) -> None: result = self.edge.calculate() if result: - print('') # blank like for readability + print('') # blank line for readability print(self._generate_edge_table(self.edge._cached_pairs)) From 6e1da13920eb3b1ed7ce501cf0c13f5c11a667df Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 17:19:11 +0300 Subject: [PATCH 457/928] Log message changed --- freqtrade/edge/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 5c7252d88..053be6bc3 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -139,7 +139,7 @@ class Edge(): # If no trade found then exit if len(trades) == 0: - logger.info("No trades created.") + logger.info("No trades found.") return False # Fill missing, calculable columns, profit, duration , abs etc. From 7b074765ab059193ec75cacd8739d5e7e5ea6204 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 May 2019 19:48:22 +0200 Subject: [PATCH 458/928] Improve edge tests - cleanup test file --- freqtrade/tests/edge/test_edge.py | 128 ++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 41 deletions(-) diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index af8674188..a14e3282e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -10,10 +10,11 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime +from freqtrade import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import get_patched_freqtradebot +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has from freqtrade.tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -30,7 +31,50 @@ ticker_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} +# Helpers for this test file + +def _validate_ohlc(buy_ohlc_sell_matrice): + for index, ohlc in enumerate(buy_ohlc_sell_matrice): + # if not high < open < low or not high < close < low + if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: + raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') + return True + + +def _build_dataframe(buy_ohlc_sell_matrice): + _validate_ohlc(buy_ohlc_sell_matrice) + tickers = [] + for ohlc in buy_ohlc_sell_matrice: + ticker = { + 'date': ticker_start_time.shift( + minutes=( + ohlc[0] * + ticker_interval_in_minute)).timestamp * + 1000, + 'buy': ohlc[1], + 'open': ohlc[2], + 'high': ohlc[3], + 'low': ohlc[4], + 'close': ohlc[5], + 'sell': ohlc[6]} + tickers.append(ticker) + + frame = DataFrame(tickers) + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) + + return frame + + +def _time_on_candle(number): + return np.datetime64(ticker_start_time.shift( + minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') + + +# End helper functions # Open trade should be removed from the end tc0 = BTContainer(data=[ # D O H L C V B S @@ -203,46 +247,6 @@ def test_nonexisting_stake_amount(mocker, edge_conf): assert edge.stake_amount('N/O', 1, 2, 1) == 0.15 -def _validate_ohlc(buy_ohlc_sell_matrice): - for index, ohlc in enumerate(buy_ohlc_sell_matrice): - # if not high < open < low or not high < close < low - if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: - raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') - return True - - -def _build_dataframe(buy_ohlc_sell_matrice): - _validate_ohlc(buy_ohlc_sell_matrice) - tickers = [] - for ohlc in buy_ohlc_sell_matrice: - ticker = { - 'date': ticker_start_time.shift( - minutes=( - ohlc[0] * - ticker_interval_in_minute)).timestamp * - 1000, - 'buy': ohlc[1], - 'open': ohlc[2], - 'high': ohlc[3], - 'low': ohlc[4], - 'close': ohlc[5], - 'sell': ohlc[6]} - tickers.append(ticker) - - frame = DataFrame(tickers) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) - - return frame - - -def _time_on_candle(number): - return np.datetime64(ticker_start_time.shift( - minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') - - def test_edge_heartbeat_calculate(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -298,6 +302,40 @@ def test_edge_process_downloaded_data(mocker, edge_conf): assert edge._last_updated <= arrow.utcnow().timestamp + 2 +def test_edge_process_no_data(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No data found. Edge is stopped ...", caplog.record_tuples) + assert edge._last_updated == 0 + + +def test_edge_process_no_trades(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No trades found.", caplog.record_tuples) + + +def test_edge_init_error(mocker, edge_conf,): + edge_conf['stake_amount'] = 0.5 + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'): + get_patched_freqtradebot(mocker, edge_conf) + + def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) @@ -360,3 +398,11 @@ def test_process_expectancy(mocker, edge_conf): assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384 assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0 assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128 + + # Pop last item so no trade is profitable + trades.pop() + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + assert len(final) == 0 + assert isinstance(final, dict) From 253025c0feb173ccb304ca3d38dcfc2ac7b28477 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 May 2019 19:53:42 +0200 Subject: [PATCH 459/928] Add tests for check_int_positive --- freqtrade/tests/test_arguments.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 0952d1c5d..d71502abb 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -185,3 +185,12 @@ def test_testdata_dl_options() -> None: assert args.export == 'export/folder' assert args.days == 30 assert args.exchange == 'binance' + + +def test_check_int_positive() -> None: + assert Arguments.check_int_positive(2) == 2 + assert Arguments.check_int_positive(6) == 6 + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive(-6) + + assert Arguments.check_int_positive(2.5) == 2 From 7b968a2401dc0b82d0c2e32016f16d9c19185173 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 24 May 2019 04:04:07 +0300 Subject: [PATCH 460/928] logger.exception cleanup --- freqtrade/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 79d150441..809ab3c7a 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -54,8 +54,8 @@ def main(sysargv: List[str]) -> None: except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException as e: - logger.exception('Fatal exception! ' + str(e)) + except BaseException: + logger.exception('Fatal exception!') finally: if worker: worker.exit() From 7bbe8b24832099ca7bac9508373c232c4497f683 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 May 2019 06:22:27 +0200 Subject: [PATCH 461/928] Add a few more testcases for check_int_positive --- freqtrade/tests/test_arguments.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d71502abb..455f3dbc6 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import argparse import pytest @@ -194,3 +193,9 @@ def test_check_int_positive() -> None: Arguments.check_int_positive(-6) assert Arguments.check_int_positive(2.5) == 2 + assert Arguments.check_int_positive("3") == 3 + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("3.5") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("DeadBeef") From c3e93e7593b7f092e80464ebad25918420246bd3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 24 May 2019 23:08:56 +0300 Subject: [PATCH 462/928] fix reduce() TypeError in hyperopts --- docs/hyperopt.md | 7 ++++--- freqtrade/optimize/default_hyperopt.py | 14 ++++++++------ user_data/hyperopts/sample_hyperopt.py | 14 ++++++++------ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b4e42de16..79ea4771b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -122,9 +122,10 @@ So let's write the buy strategy using these values: dataframe['macd'], dataframe['macdsignal'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 721848d2e..7f1cb2435 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -70,9 +70,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -129,9 +130,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 54f65a7e6..7cb55378e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -79,9 +79,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -138,9 +139,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe From 469c0b6a558d3194026c97ec637f47d1e4e06eae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 13:16:00 +0200 Subject: [PATCH 463/928] Adjust check_int_positive tests --- freqtrade/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 327915b61..5afa8fa06 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -405,7 +405,7 @@ class Arguments(object): raise Exception('Incorrect syntax for timerange "%s"' % text) @staticmethod - def check_int_positive(value) -> int: + def check_int_positive(value: str) -> int: try: uint = int(value) if uint <= 0: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 455f3dbc6..ecd108b5e 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -187,13 +187,17 @@ def test_testdata_dl_options() -> None: def test_check_int_positive() -> None: - assert Arguments.check_int_positive(2) == 2 - assert Arguments.check_int_positive(6) == 6 - with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive(-6) - assert Arguments.check_int_positive(2.5) == 2 assert Arguments.check_int_positive("3") == 3 + assert Arguments.check_int_positive("1") == 1 + assert Arguments.check_int_positive("100") == 100 + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("-2") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("0") + with pytest.raises(argparse.ArgumentTypeError): Arguments.check_int_positive("3.5") From 7e952b028a6d047f0da4ce83cf0bcea2272bf582 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:11:30 +0200 Subject: [PATCH 464/928] Add basic auth to rest-api --- config_full.json.example | 4 +++- freqtrade/rpc/api_server.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 6603540cf..acecfb649 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -112,7 +112,9 @@ "api_server": { "enabled": false, "listen_ip_address": "127.0.0.1", - "listen_port": 8080 + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 6792cc9a0..5e76e148c 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -53,6 +53,19 @@ class ApiServer(RPC): return func_wrapper + def require_login(func): + + def func_wrapper(self, *args, **kwargs): + # Also works if no username/password is specified + if (request.headers.get('username') == self._config['api_server'].get('username') + and request.headers.get('password') == self._config['api_server'].get('password')): + + return func(self, *args, **kwargs) + else: + return jsonify({"error": "Unauthorized"}), 401 + + return func_wrapper + def __init__(self, freqtrade) -> None: """ Init the api server, and init the super class RPC @@ -159,6 +172,7 @@ class ApiServer(RPC): # TODO: Implement the following # help (?) + @require_login def page_not_found(self, error): """ Return "404 not found", 404. @@ -169,6 +183,7 @@ class ApiServer(RPC): 'code': 404 }), 404 + @require_login @rpc_catch_errors def _start(self): """ @@ -178,6 +193,7 @@ class ApiServer(RPC): msg = self._rpc_start() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _stop(self): """ @@ -187,6 +203,7 @@ class ApiServer(RPC): msg = self._rpc_stop() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _stopbuy(self): """ @@ -196,6 +213,7 @@ class ApiServer(RPC): msg = self._rpc_stopbuy() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _version(self): """ @@ -203,6 +221,7 @@ class ApiServer(RPC): """ return self.rest_dump({"version": __version__}) + @require_login @rpc_catch_errors def _reload_conf(self): """ @@ -212,6 +231,7 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _count(self): """ @@ -221,6 +241,7 @@ class ApiServer(RPC): msg = self._rpc_count() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _daily(self): """ @@ -238,6 +259,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _edge(self): """ @@ -248,6 +270,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _profit(self): """ @@ -264,6 +287,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _performance(self): """ @@ -278,6 +302,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _status(self): """ @@ -288,6 +313,7 @@ class ApiServer(RPC): results = self._rpc_trade_status() return self.rest_dump(results) + @require_login @rpc_catch_errors def _balance(self): """ @@ -298,6 +324,7 @@ class ApiServer(RPC): results = self._rpc_balance(self._config.get('fiat_display_currency', '')) return self.rest_dump(results) + @require_login @rpc_catch_errors def _whitelist(self): """ @@ -306,6 +333,7 @@ class ApiServer(RPC): results = self._rpc_whitelist() return self.rest_dump(results) + @require_login @rpc_catch_errors def _blacklist(self): """ @@ -315,6 +343,7 @@ class ApiServer(RPC): results = self._rpc_blacklist(add) return self.rest_dump(results) + @require_login @rpc_catch_errors def _forcebuy(self): """ @@ -328,6 +357,7 @@ class ApiServer(RPC): else: return self.rest_dump({"status": f"Error buying pair {asset}."}) + @require_login @rpc_catch_errors def _forcesell(self): """ From 04c35b465ed20284763227c0d28a0f6471e6713b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:13:59 +0200 Subject: [PATCH 465/928] Add authorization to tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 111 ++++++++++++++++------ 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 5b9e538b5..620a7c34b 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -16,11 +16,19 @@ from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) +_TEST_USER = "FreqTrader" +_TEST_PASS = "SuperSecurePassword1!" + + @pytest.fixture def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": "8080", + "username": _TEST_USER, + "password": _TEST_PASS, + }}) + ftbot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) @@ -28,6 +36,21 @@ def botclient(default_conf, mocker): # Cleanup ... ? +def client_post(client, url, data={}): + headers = {"username": _TEST_USER, + "password": _TEST_PASS} + return client.post(url, + content_type="application/json", + data=data, + headers=headers) + + +def client_get(client, url): + headers = {"username": _TEST_USER, + "password": _TEST_PASS} + return client.get(url, headers=headers) + + def assert_response(response, expected_code=200): assert response.status_code == expected_code assert response.content_type == "application/json" @@ -36,7 +59,7 @@ def assert_response(response, expected_code=200): def test_api_not_found(botclient): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/invalid_url") + rc = client_post(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json == {"status": "error", "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", @@ -44,27 +67,57 @@ def test_api_not_found(botclient): } +def test_api_unauthorized(botclient): + ftbot, client = botclient + # Don't send user/pass information + rc = client.get(f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only username + ftbot.config['api_server']['username'] = "Ftrader" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only password + ftbot.config['api_server']['username'] = _TEST_USER + ftbot.config['api_server']['password'] = "WrongPassword" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + ftbot.config['api_server']['username'] = "Ftrader" + ftbot.config['api_server']['password'] = "WrongPassword" + + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + + + def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING - rc = client.post(f"{BASE_URI}/stop") + rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again - rc = client.post(f"{BASE_URI}/stop") + rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot - rc = client.post(f"{BASE_URI}/start") + rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again - rc = client.post(f"{BASE_URI}/start") + rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'already running'} @@ -153,7 +206,7 @@ def test_api_cleanup(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/reload_conf") + rc = client_post(client, f"{BASE_URI}/reload_conf") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -163,7 +216,7 @@ def test_api_stopbuy(botclient): ftbot, client = botclient assert ftbot.config['max_open_trades'] != 0 - rc = client.post(f"{BASE_URI}/stopbuy") + rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 @@ -193,7 +246,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) - rc = client.get(f"{BASE_URI}/balance") + rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 @@ -216,7 +269,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/count") + rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 0 @@ -224,7 +277,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() - rc = client.get(f"{BASE_URI}/count") + rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -240,7 +293,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/daily") + rc = client_get(client, f"{BASE_URI}/daily") assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) @@ -256,7 +309,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/edge") + rc = client_get(client, f"{BASE_URI}/edge") assert_response(rc, 502) assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} @@ -272,7 +325,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 502) assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -282,7 +335,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 502) assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -291,7 +344,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li trade.close_date = datetime.utcnow() trade.is_open = False - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) assert rc.json == {'avg_duration': '0:00:00', 'best_pair': 'ETH/BTC', @@ -344,7 +397,7 @@ def test_api_performance(botclient, mocker, ticker, fee): Trade.session.add(trade) Trade.session.flush() - rc = client.get(f"{BASE_URI}/performance") + rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json) == 2 assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, @@ -362,12 +415,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/status") + rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} ftbot.create_trade() - rc = client.get(f"{BASE_URI}/status") + rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 90.99181074, @@ -394,7 +447,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_version(botclient): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/version") + rc = client_get(client, f"{BASE_URI}/version") assert_response(rc) assert rc.json == {"version": __version__} @@ -402,15 +455,15 @@ def test_api_version(botclient): def test_api_blacklist(botclient, mocker): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/blacklist") + rc = client_get(client, f"{BASE_URI}/blacklist") assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, "method": "StaticPairList"} # Add ETH/BTC to blacklist - rc = client.post(f"{BASE_URI}/blacklist", data='{"blacklist": ["ETH/BTC"]}', - content_type='application/json') + rc = client_post(client, f"{BASE_URI}/blacklist", + data='{"blacklist": ["ETH/BTC"]}') assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], "length": 3, @@ -420,7 +473,7 @@ def test_api_blacklist(botclient, mocker): def test_api_whitelist(botclient): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/whitelist") + rc = client_get(client, f"{BASE_URI}/whitelist") assert_response(rc) assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, @@ -430,7 +483,7 @@ def test_api_whitelist(botclient): def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/forcebuy", content_type='application/json', + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} @@ -440,7 +493,7 @@ def test_api_forcebuy(botclient, mocker, fee): fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {"status": "Error buying pair ETH/BTC."} @@ -461,7 +514,7 @@ def test_api_forcebuy(botclient, mocker, fee): )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, @@ -491,14 +544,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): ) patch_get_signal(ftbot, (True, False)) - rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} ftbot.create_trade() - rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} From 1fab884a2fcae07293a2d3139889d7dcb4462fe0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:14:09 +0200 Subject: [PATCH 466/928] use Authorization for client --- scripts/rest_client.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b31a1de50..ca3da737e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -26,10 +26,12 @@ logger = logging.getLogger("ft_rest_client") class FtRestClient(): - def __init__(self, serverurl): + def __init__(self, serverurl, username=None, password=None): self._serverurl = serverurl self._session = requests.Session() + self._username = username + self._password = password def _call(self, method, apipath, params: dict = None, data=None, files=None): @@ -41,6 +43,12 @@ class FtRestClient(): "Content-Type": "application/json" } + if self._username: + hd.update({"username": self._username}) + + if self._password: + hd.update({"password": self._password}) + # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string @@ -244,8 +252,11 @@ def main(args): config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") + username = config.get("api_server", {}).get("username") + password = config.get("api_server", {}).get("password") + server_url = f"http://{url}:{port}" - client = FtRestClient(server_url) + client = FtRestClient(server_url, username, password) m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] command = args["command"] From 5bbd3c61581cac89741fdc65b8786078610afd75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:16:59 +0200 Subject: [PATCH 467/928] Add documentation --- docs/rest-api.md | 9 +++++++-- freqtrade/rpc/api_server.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 95eec3020..535163da4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -10,12 +10,17 @@ Sample configuration: "api_server": { "enabled": true, "listen_ip_address": "127.0.0.1", - "listen_port": 8080 + "listen_port": 8080, + "username": "Freqtrader", + "password": "SuperSecret1!" }, ``` !!! Danger: Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will potentially be able to control your bot. + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. + +!!! Danger: Password selection + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5e76e148c..d2001e91a 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -56,7 +56,7 @@ class ApiServer(RPC): def require_login(func): def func_wrapper(self, *args, **kwargs): - # Also works if no username/password is specified + # Also accepts empty username/password if it's missing in both config and request if (request.headers.get('username') == self._config['api_server'].get('username') and request.headers.get('password') == self._config['api_server'].get('password')): From 2da7145132956d4c0f02488e28133b9e35d16772 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:25:16 +0200 Subject: [PATCH 468/928] Switch auth to real basic auth --- freqtrade/rpc/api_server.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d2001e91a..14b15a3df 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -53,13 +53,16 @@ class ApiServer(RPC): return func_wrapper + def check_auth(self, username, password): + return (username == self._config['api_server'].get('username') and + password == self._config['api_server'].get('password')) + def require_login(func): def func_wrapper(self, *args, **kwargs): - # Also accepts empty username/password if it's missing in both config and request - if (request.headers.get('username') == self._config['api_server'].get('username') - and request.headers.get('password') == self._config['api_server'].get('password')): + auth = request.authorization + if auth and self.check_auth(auth.username, auth.password): return func(self, *args, **kwargs) else: return jsonify({"error": "Unauthorized"}), 401 From febcc3dddc043b568f9d067ca6ad1dc5c60bed7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:25:36 +0200 Subject: [PATCH 469/928] Adapt tests and rest_client to basic_auth --- freqtrade/tests/rpc/test_rpc_apiserver.py | 11 +++-------- scripts/rest_client.py | 13 ++----------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 620a7c34b..4c3aea89a 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -7,6 +7,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from flask import Flask +from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade @@ -37,18 +38,14 @@ def botclient(default_conf, mocker): def client_post(client, url, data={}): - headers = {"username": _TEST_USER, - "password": _TEST_PASS} return client.post(url, content_type="application/json", data=data, - headers=headers) + headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) def client_get(client, url): - headers = {"username": _TEST_USER, - "password": _TEST_PASS} - return client.get(url, headers=headers) + return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) def assert_response(response, expected_code=200): @@ -95,8 +92,6 @@ def test_api_unauthorized(botclient): assert rc.json == {'error': 'Unauthorized'} - - def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ca3da737e..2261fba0b 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -30,8 +30,7 @@ class FtRestClient(): self._serverurl = serverurl self._session = requests.Session() - self._username = username - self._password = password + self._session.auth = (username, password) def _call(self, method, apipath, params: dict = None, data=None, files=None): @@ -43,12 +42,6 @@ class FtRestClient(): "Content-Type": "application/json" } - if self._username: - hd.update({"username": self._username}) - - if self._password: - hd.update({"password": self._password}) - # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string @@ -57,9 +50,7 @@ class FtRestClient(): url = urlunparse((schema, netloc, path, par, query, fragment)) try: - resp = self._session.request(method, url, headers=hd, data=json.dumps(data), - # auth=self.session.auth - ) + resp = self._session.request(method, url, headers=hd, data=json.dumps(data)) # return resp.text return resp.json() except ConnectionError: From 90ece09ee98900b62e29562d4ccdd6f5d77d777e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:42:13 +0200 Subject: [PATCH 470/928] require username/password for API server --- freqtrade/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1b06eb726..4772952fc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -166,8 +166,10 @@ CONF_SCHEMA = { "minimum": 1024, "maximum": 65535 }, + 'username': {'type': 'string'}, + 'password': {'type': 'string'}, }, - 'required': ['enabled', 'listen_ip_address', 'listen_port'] + 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, From b6484cb2b42e9f7df977f5717180e2442ce42e5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:54:35 +0200 Subject: [PATCH 471/928] Replace technical link --- Dockerfile.technical | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.technical b/Dockerfile.technical index 5339eb232..9431e72d0 100644 --- a/Dockerfile.technical +++ b/Dockerfile.technical @@ -3,4 +3,4 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/berlinguyinca/technical + && pip install git+https://github.com/freqtrade/technical From 4394701de37853183b84d9d982745d29f8fe8a77 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:13:18 +0200 Subject: [PATCH 472/928] Seperate docker-documentation --- README.md | 1 - docs/docker.md | 204 +++++++++++++++++++++++++++++++++++++++++ docs/index.md | 7 +- docs/installation.md | 210 ++++--------------------------------------- 4 files changed, 226 insertions(+), 196 deletions(-) create mode 100644 docs/docker.md diff --git a/README.md b/README.md index 8f7578561..98dad1d2e 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ The project is currently setup in two main branches: - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - ## A note on Binance For Binance, please add `"BNB/"` to your blacklist to avoid issues. diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 000000000..767cabf01 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,204 @@ +# Using FreqTrade with Docker + +## Install Docker + +Start by downloading and installing Docker CE for your platform: + +* [Mac](https://docs.docker.com/docker-for-mac/install/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) +* [Linux](https://docs.docker.com/install/) + +Once you have Docker installed, simply create the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. + +## Download the official FreqTrade docker image + +Pull the image from docker hub. + +Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +```bash +docker pull freqtradeorg/freqtrade:develop +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:develop freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +Production + +```bash +touch tradesv3.sqlite +```` + +Dry-Run + +```bash +touch tradesv3.dryrun.sqlite +``` + +!!! Note + Make sure to use the path to this file when starting the bot in docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libaries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Note + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +Should you find this irritating please add the following to your docker commands: + +##### Linux + +``` bash +-v /etc/timezone:/etc/timezone:ro + +# Complete command: +docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +##### MacOS + +There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. + +```bash +docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. + To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` + +!!! Note + All available command line arguments can be added to the end of the `docker run` command. + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade --strategy AwsomelyProfitableStrategy backtesting +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/index.md b/docs/index.md index 9abc71747..a6ae6312d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,12 +36,14 @@ Freqtrade is a cryptocurrency trading bot written in Python. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### Up to date clock + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -58,10 +61,12 @@ To run this bot we recommend you a cloud instance with a minimum of: ## Support + Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. ## Ready to try? + Begin by reading our installation guide [here](installation). diff --git a/docs/installation.md b/docs/installation.md index 11ddc010d..ed38c1340 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,7 +1,9 @@ # Installation + This page explains how to prepare your environment for running the bot. ## Prerequisite + Before running your bot in production you will need to setup few external API. In production mode, the bot required valid Bittrex API credentials and a Telegram bot (optional but recommended). @@ -10,9 +12,11 @@ credentials and a Telegram bot (optional but recommended). - [Backtesting commands](#setup-your-telegram-bot) ### Setup your exchange account + *To be completed, please feel free to complete this section.* ### Setup your Telegram bot + The only things you need is a working Telegram bot and its API token. Below we explain how to create your Telegram Bot, and how to get your Telegram user id. @@ -35,7 +39,9 @@ Good. Now let's choose a username for your bot. It must end in `bot`. Like this, **1.5. Father bot will return you the token (API key)**
Copy it and keep it you will use it for the config parameter `token`. + *BotFather response:* + ```hl_lines="4" Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. @@ -44,15 +50,18 @@ Use this token to access the HTTP API: For a description of the Bot API, see this page: https://core.telegram.org/bots/api ``` + **1.6. Don't forget to start the conversation with your bot, by clicking /START button** ### 2. Get your user id + **2.1. Talk to https://telegram.me/userinfobot** **2.2. Get your "Id", you will use it for the config parameter `chat_id`.** -
+ ## Quick start + Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. ```bash @@ -61,9 +70,10 @@ cd freqtrade git checkout develop ./setup.sh --install ``` + !!! Note Windows installation is explained [here](#windows). -
+ ## Easy Installation - Linux Script If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. @@ -101,193 +111,6 @@ Config parameter is a `config.json` configurator. This script will ask you quest ------ -## Automatic Installation - Docker - -Start by downloading Docker for your platform: - -* [Mac](https://www.docker.com/products/docker#/mac) -* [Windows](https://www.docker.com/products/docker#/windows) -* [Linux](https://www.docker.com/products/docker#/linux) - -Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo. - -### 1. Prepare the Bot - -**1.1. Clone the git repository** - -Linux/Mac/Windows with WSL -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -**1.2. (Optional) Checkout the develop branch** - -```bash -git checkout develop -``` - -**1.3. Go into the new directory** - -```bash -cd freqtrade -``` - -**1.4. Copy `config.json.example` to `config.json`** - -```bash -cp -n config.json.example config.json -``` - -> To edit the config please refer to the [Bot Configuration](configuration.md) page. - -**1.5. Create your database file *(optional - the bot will create it if it is missing)** - -Production - -```bash -touch tradesv3.sqlite -```` - -Dry-Run - -```bash -touch tradesv3.dryrun.sqlite -``` - -### 2. Download or build the docker image - -Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used. - -Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -**2.1. Download the docker image** - -Pull the image from docker hub and (optionally) change the name of the image - -```bash -docker pull freqtradeorg/freqtrade:develop -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:develop freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -**2.2. Build the Docker image** - -```bash -cd freqtrade -docker build -t freqtrade . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f ./Dockerfile.develop -t freqtrade-dev . -``` - -For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. - -### 3. Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -### 4. Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. - -```bash -docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. - -### 5. Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -**5.1. Move your config file and database** - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -**5.2. Run the docker image** - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. - To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` - -!!! Note - All command line arguments can be added to the end of the `docker run` command. - -### 6. Monitor your Docker instance - -You can then use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### 7. Backtest with docker - -The following assumes that the above steps (1-4) have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade --strategy AwsomelyProfitableStrategy backtesting -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional parameters can be appended after the image name (`freqtrade` in the above example). - ------- - ## Custom Installation We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. @@ -413,7 +236,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run python3.6 freqtrade -c config.json ``` -*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. +*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. #### 7. [Optional] Configure `freqtrade` as a `systemd` service @@ -441,14 +264,13 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co as the watchdog. !!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a - Docker container. + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ ## Windows -We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure). +We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. If that is not available on your system, feel free to try the instructions below, which led to success for some. @@ -492,7 +314,7 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. --- From 3e0a71f69f99ef82ebc6ad4730259ed4e780d106 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:27:18 +0200 Subject: [PATCH 473/928] Add docker install script to mkdocs index --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 9932ff316..489107f2e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ site_name: Freqtrade nav: - About: index.md - Installation: installation.md + - Installation Docker: docker.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md From 26a8cdcc031d6fa7cba4dc2a3f7ada3807d5a297 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:27:36 +0200 Subject: [PATCH 474/928] Move telegram-setup to telegram page --- docs/installation.md | 52 +++--------------------------------------- docs/telegram-usage.md | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index ed38c1340..d215dc8d6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,60 +5,14 @@ This page explains how to prepare your environment for running the bot. ## Prerequisite Before running your bot in production you will need to setup few -external API. In production mode, the bot required valid Bittrex API -credentials and a Telegram bot (optional but recommended). +external API. In production mode, the bot will require valid Exchange API +credentials. We also reccomend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - [Setup your exchange account](#setup-your-exchange-account) -- [Backtesting commands](#setup-your-telegram-bot) ### Setup your exchange account -*To be completed, please feel free to complete this section.* - -### Setup your Telegram bot - -The only things you need is a working Telegram bot and its API token. -Below we explain how to create your Telegram Bot, and how to get your -Telegram user id. - -### 1. Create your Telegram bot - -**1.1. Start a chat with https://telegram.me/BotFather** - -**1.2. Send the message `/newbot`. ** *BotFather response:* -``` -Alright, a new bot. How are we going to call it? Please choose a name for your bot. -``` - -**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)** -*BotFather response:* -``` -Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. -``` -**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")** - -**1.5. Father bot will return you the token (API key)**
-Copy it and keep it you will use it for the config parameter `token`. - -*BotFather response:* - -```hl_lines="4" -Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. - -Use this token to access the HTTP API: -521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0 - -For a description of the Bot API, see this page: https://core.telegram.org/bots/api -``` - -**1.6. Don't forget to start the conversation with your bot, by clicking /START button** - -### 2. Get your user id - -**2.1. Talk to https://telegram.me/userinfobot** - -**2.2. Get your "Id", you will use it for the config parameter -`chat_id`.** +You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. ## Quick start diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 3947168c5..e06d4fdfc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -1,10 +1,45 @@ # Telegram usage -## Prerequisite +## Setup your Telegram bot -To control your bot with Telegram, you need first to -[set up a Telegram bot](installation.md) -and add your Telegram API keys into your config file. +Below we explain how to create your Telegram Bot, and how to get your +Telegram user id. + +### 1. Create your Telegram bot + +Start a chat with the [Telegram BotFather](https://telegram.me/BotFather) + +Send the message `/newbot`. + +*BotFather response:* + +> Alright, a new bot. How are we going to call it? Please choose a name for your bot. + +Choose the public name of your bot (e.x. `Freqtrade bot`) + +*BotFather response:* + +> Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. + +Choose the name id of your bot and send it to the BotFather (e.g. "`My_own_freqtrade_bot`") + +*BotFather response:* + +> Done! Congratulations on your new bot. You will find it at `t.me/yourbots_name_bot`. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. + +> Use this token to access the HTTP API: `22222222:APITOKEN` + +> For a description of the Bot API, see this page: https://core.telegram.org/bots/api Father bot will return you the token (API key) + +Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it for the config parameter `token`. + +Don't forget to start the conversation with your bot, by clicking `/START` button + +### 2. Get your user id + +Talk to the [userinfobot](https://telegram.me/userinfobot) + +Get your "Id", you will use it for the config parameter `chat_id`. ## Telegram commands From 9225cdea8a770453c59b203fb8c2b8f8fbb40fe2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:51:52 +0200 Subject: [PATCH 475/928] Move validate_backtest_data and get_timeframe to histoyr --- freqtrade/data/history.py | 44 ++++++++++++++++++++++++++++--- freqtrade/edge/__init__.py | 4 +-- freqtrade/optimize/__init__.py | 48 ---------------------------------- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3bec63926..e0f9f67db 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -5,19 +5,21 @@ Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ + import logging +import operator +from datetime import datetime from pathlib import Path -from typing import Optional, List, Dict, Tuple, Any +from typing import Any, Dict, List, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade import misc, OperationalException +from freqtrade import OperationalException, misc from freqtrade.arguments import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange, timeframe_to_minutes - logger = logging.getLogger(__name__) @@ -243,3 +245,39 @@ def download_pair_history(datadir: Optional[Path], f'Error: {e}' ) return False + + +def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: + """ + Get the maximum timeframe for the given backtest data + :param data: dictionary with preprocessed backtesting data + :return: tuple containing min_date, max_date + """ + timeframe = [ + (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + for frame in data.values() + ] + return min(timeframe, key=operator.itemgetter(0))[0], \ + max(timeframe, key=operator.itemgetter(1))[1] + + +def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, + max_date: datetime, ticker_interval_mins: int) -> bool: + """ + Validates preprocessed backtesting data for missing values and shows warnings about it that. + + :param data: dictionary with preprocessed backtesting data + :param min_date: start-date of the data + :param max_date: end-date of the data + :param ticker_interval_mins: ticker interval in minutes + """ + # total difference in minutes / interval-minutes + expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + found_missing = False + for pair, df in data.items(): + dflen = len(df) + if dflen < expected_frames: + found_missing = True + logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", + pair, expected_frames, dflen, expected_frames - dflen) + return found_missing diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 053be6bc3..3ddff4772 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -13,7 +13,6 @@ from freqtrade import constants, OperationalException from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.data import history -from freqtrade.optimize import get_timeframe from freqtrade.strategy.interface import SellType @@ -49,7 +48,6 @@ class Edge(): self.strategy = strategy self.ticker_interval = self.strategy.ticker_interval self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.get_timeframe = get_timeframe self.advise_sell = self.strategy.advise_sell self.advise_buy = self.strategy.advise_buy @@ -117,7 +115,7 @@ class Edge(): preprocessed = self.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) + min_date, max_date = history.get_timeframe(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 19b8dd90a..f4f31720b 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1,49 +1 @@ -# pragma pylint: disable=missing-docstring - -import logging -from datetime import datetime -from typing import Dict, Tuple -import operator - -import arrow -from pandas import DataFrame - from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401 - -logger = logging.getLogger(__name__) - - -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: - """ - Get the maximum timeframe for the given backtest data - :param data: dictionary with preprocessed backtesting data - :return: tuple containing min_date, max_date - """ - timeframe = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) - for frame in data.values() - ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] - - -def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, - max_date: datetime, ticker_interval_mins: int) -> bool: - """ - Validates preprocessed backtesting data for missing values and shows warnings about it that. - - :param data: dictionary with preprocessed backtesting data - :param min_date: start-date of the data - :param max_date: end-date of the data - :param ticker_interval_mins: ticker interval in minutes - """ - # total difference in minutes / interval-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) - found_missing = False - for pair, df in data.items(): - dflen = len(df) - if dflen < expected_frames: - found_missing = True - logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", - pair, expected_frames, dflen, expected_frames - dflen) - return found_missing From b38c43141c44d26702cb69f88ce58b85c111e577 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:53:35 +0200 Subject: [PATCH 476/928] Adjust imports to new location --- freqtrade/optimize/backtesting.py | 7 +++---- freqtrade/optimize/hyperopt.py | 3 +-- freqtrade/tests/data/test_converter.py | 3 +-- freqtrade/tests/optimize/test_backtest_detail.py | 12 ++++++------ freqtrade/tests/optimize/test_backtesting.py | 6 +++--- freqtrade/tests/optimize/test_hyperopt.py | 2 +- freqtrade/tests/optimize/test_optimize.py | 15 +++++++-------- 7 files changed, 22 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 51122cfb2..c7ce29120 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,6 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import optimize from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -440,10 +439,10 @@ class Backtesting(object): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called) - optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) + history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes(self.ticker_interval)) logger.info( 'Backtesting with data from %s up to %s (%s days)..', min_date.isoformat(), diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 92589aed2..68c7b2508 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,9 +23,8 @@ from skopt.space import Dimension from freqtrade import DependencyException from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.data.history import load_data +from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data from freqtrade.exchange import timeframe_to_minutes -from freqtrade.optimize import get_timeframe, validate_backtest_data from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.resolvers import HyperOptResolver diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 46d564003..4c8de575d 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -2,8 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history -from freqtrade.optimize import validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe from freqtrade.tests.conftest import log_has diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index b98369533..32c6bd09b 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -2,17 +2,17 @@ import logging from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame - -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType -from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe, - _get_frame_time_from_offset, tests_ticker_interval) from freqtrade.tests.conftest import patch_exchange - +from freqtrade.tests.optimize import (BTContainer, BTrade, + _build_backtest_dataframe, + _get_frame_time_from_offset, + tests_ticker_interval) # Test 1 Minus 8% Close # Test with Stop-loss at 1% diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 6a39deed4..07ad7eaff 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) from freqtrade.state import RunMode @@ -472,7 +472,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -507,7 +507,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index f50f58e5b..9d1789171 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -12,7 +12,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start -from freqtrade.resolvers import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index d746aa44f..401592b53 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade import optimize from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.exchange import timeframe_to_minutes @@ -18,7 +17,7 @@ def test_get_timeframe(default_conf, mocker) -> None: pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -35,10 +34,10 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: fill_up_missing=False ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) caplog.clear() - assert optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) + assert history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -59,8 +58,8 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) caplog.clear() - assert not optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) + assert not history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 From 236c392d282fc1516f05531651546c732b520b84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:00:31 +0200 Subject: [PATCH 477/928] Don't load hyperopts / optimize dependency tree if that module is not used --- freqtrade/arguments.py | 6 +- freqtrade/optimize/__init__.py | 100 +++++++++++++++++++++++++++++- freqtrade/optimize/backtesting.py | 42 +------------ freqtrade/optimize/hyperopt.py | 66 +------------------- freqtrade/resolvers/__init__.py | 3 +- 5 files changed, 106 insertions(+), 111 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 5afa8fa06..e79d0c6d4 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,13 +340,13 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import backtesting, hyperopt, edge_cli + from freqtrade.optimize import start_backtesting, start_hyperopt, edge_cli subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') - backtesting_cmd.set_defaults(func=backtesting.start) + backtesting_cmd.set_defaults(func=start_backtesting) self.optimizer_shared_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) @@ -358,7 +358,7 @@ class Arguments(object): # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') - hyperopt_cmd.set_defaults(func=hyperopt.start) + hyperopt_cmd.set_defaults(func=start_hyperopt) self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index f4f31720b..34076ee43 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1 +1,99 @@ -from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401 +import logging +from argparse import Namespace +from typing import Any, Dict + +from filelock import FileLock, Timeout + +from freqtrade import DependencyException, constants +from freqtrade.configuration import Configuration +from freqtrade.state import RunMode + +logger = logging.getLogger(__name__) + + +def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: + """ + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration + """ + configuration = Configuration(args, method) + config = configuration.load_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + if method == RunMode.BACKTEST: + if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: + raise DependencyException('stake amount could not be "%s" for backtesting' % + constants.UNLIMITED_STAKE_AMOUNT) + + if method == RunMode.HYPEROPT: + # Special cases for Hyperopt + if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': + logger.error("Please don't use --strategy for hyperopt.") + logger.error( + "Read the documentation at " + "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " + "to understand how to configure hyperopt.") + raise DependencyException("--strategy configured but not supported for hyperopt") + + return config + + +def start_backtesting(args: Namespace) -> None: + """ + Start Backtesting script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading backtesting module when it's not used + from freqtrade.optimize.backtesting import Backtesting + + # Initialize configuration + config = setup_configuration(args, RunMode.BACKTEST) + + logger.info('Starting freqtrade in Backtesting mode') + + # Initialize backtesting object + backtesting = Backtesting(config) + backtesting.start() + + +def start_hyperopt(args: Namespace) -> None: + """ + Start hyperopt script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading hyperopt module when it's not used + from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE + + # Initialize configuration + config = setup_configuration(args, RunMode.HYPEROPT) + + logger.info('Starting freqtrade in Hyperopt mode') + + lock = FileLock(HYPEROPT_LOCKFILE) + + try: + with lock.acquire(timeout=1): + + # Remove noisy log messages + logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + logging.getLogger('filelock').setLevel(logging.WARNING) + + # Initialize backtesting object + hyperopt = Hyperopt(config) + hyperopt.start() + + except Timeout: + logger.info("Another running instance of freqtrade Hyperopt detected.") + logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " + "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " + "or on separate machines.") + logger.info("Quitting now.") + # TODO: return False here in order to help freqtrade to exit + # with non-zero exit code... + # Same in Edge and Backtesting start() functions. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c7ce29120..bdd42943b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,7 +4,6 @@ This module contains the backtesting logic """ import logging -from argparse import Namespace from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path @@ -13,9 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes @@ -23,8 +20,7 @@ from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType, IStrategy - +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -485,39 +481,3 @@ class Backtesting(object): print(' Strategy Summary '.center(133, '=')) print(self._generate_text_table_strategy(all_results)) print('\nFor more details, please look at the detail tables above') - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.BACKTEST) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: - raise DependencyException('stake amount could not be "%s" for backtesting' % - constants.UNLIMITED_STAKE_AMOUNT) - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Backtesting mode') - - # Initialize backtesting object - backtesting = Backtesting(config) - backtesting.start() diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 68c7b2508..d19d54031 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,27 +7,22 @@ This module contains the hyperopt logic import logging import os import sys -from argparse import Namespace from math import exp from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List -from filelock import Timeout, FileLock from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade import DependencyException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data from freqtrade.exchange import timeframe_to_minutes from freqtrade.optimize.backtesting import Backtesting -from freqtrade.state import RunMode -from freqtrade.resolvers import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver logger = logging.getLogger(__name__) @@ -342,62 +337,3 @@ class Hyperopt(Backtesting): self.save_trials() self.log_trials_result() - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the Hyperopt module - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.HYPEROPT) - config = configuration.load_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': - logger.error("Please don't use --strategy for hyperopt.") - logger.error( - "Read the documentation at " - "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " - "to understand how to configure hyperopt.") - raise DependencyException("--strategy configured but not supported for hyperopt") - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Hyperopt mode') - - lock = FileLock(HYPEROPT_LOCKFILE) - - try: - with lock.acquire(timeout=1): - - # Remove noisy log messages - logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) - logging.getLogger('filelock').setLevel(logging.WARNING) - - # Initialize backtesting object - hyperopt = Hyperopt(config) - hyperopt.start() - - except Timeout: - logger.info("Another running instance of freqtrade Hyperopt detected.") - logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " - "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " - "or on separate machines.") - logger.info("Quitting now.") - # TODO: return False here in order to help freqtrade to exit - # with non-zero exit code... - # Same in Edge and Backtesting start() functions. diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 5cf6c616a..8f79349fe 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,5 +1,6 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 +# Don't import HyperoptResolver to avoid loading the whole Optimize tree +# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 From 65a4862d1f420b9a1df673ba6bfe32a6457db639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:01:43 +0200 Subject: [PATCH 478/928] Adapt tests to load start_* methods from optimize --- freqtrade/tests/optimize/test_backtesting.py | 16 ++++++++-------- freqtrade/tests/optimize/test_hyperopt.py | 13 +++++++------ freqtrade/tests/test_main.py | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 07ad7eaff..5b42cae34 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -18,8 +18,8 @@ from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timeframe -from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, - start) +from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType @@ -178,7 +178,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'backtesting' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -228,7 +228,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--export-filename', 'foo_bar.json' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -290,7 +290,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog ] with pytest.raises(DependencyException, match=r'.*stake amount.*'): - setup_configuration(get_args(args)) + setup_configuration(get_args(args), RunMode.BACKTEST) def test_start(mocker, fee, default_conf, caplog) -> None: @@ -307,7 +307,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: 'backtesting' ] args = get_args(args) - start(args) + start_backtesting(args) assert log_has( 'Starting freqtrade in Backtesting mode', caplog.record_tuples @@ -847,7 +847,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--disable-max-market-positions' ] args = get_args(args) - start(args) + start_backtesting(args) # check the logs, that will contain the backtest result exists = [ 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', @@ -901,7 +901,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'TestStrategy', ] args = get_args(args) - start(args) + start_backtesting(args) # 2 backtests, 4 tables assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9d1789171..9128efa0c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -11,7 +11,8 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start +from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -52,7 +53,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca 'hyperopt' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -100,7 +101,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--print-all' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -183,7 +184,7 @@ def test_start(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -214,7 +215,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -239,7 +240,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ] args = get_args(args) with pytest.raises(DependencyException): - start(args) + start_hyperopt(args) assert log_has( "Please don't use --strategy for hyperopt.", caplog.record_tuples diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e4ffc5fae..1292fd24d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -19,7 +19,7 @@ def test_parse_args_backtesting(mocker) -> None: Test that main() can start backtesting and also ensure we can pass some specific arguments further argument parsing is done in test_arguments.py """ - backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) + backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] @@ -32,7 +32,7 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: - hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) + hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] From 104f1212e6134973ff89b00f042552c389499bb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:05:19 +0200 Subject: [PATCH 479/928] Move edge_cli_start to optimize --- freqtrade/arguments.py | 4 ++-- freqtrade/optimize/__init__.py | 16 ++++++++++++++++ freqtrade/optimize/edge_cli.py | 34 ---------------------------------- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index e79d0c6d4..d6f0063d0 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,7 +340,7 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import start_backtesting, start_hyperopt, edge_cli + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edgecli subparsers = self.parser.add_subparsers(dest='subparser') @@ -352,7 +352,7 @@ class Arguments(object): # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=edge_cli.start) + edge_cmd.set_defaults(func=start_edgecli) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 34076ee43..cb01950b4 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -97,3 +97,19 @@ def start_hyperopt(args: Namespace) -> None: # TODO: return False here in order to help freqtrade to exit # with non-zero exit code... # Same in Edge and Backtesting start() functions. + + +def start_edgecli(args: Namespace) -> None: + """ + Start Edge script + :param args: Cli args from Arguments() + :return: None + """ + from freqtrade.optimize.edge_cli import EdgeCli + # Initialize configuration + config = setup_configuration(args, RunMode.EDGECLI) + logger.info('Starting freqtrade in Edge mode') + + # Initialize Edge object + edge_cli = EdgeCli(config) + edge_cli.start() diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index d37b930b8..8232c79c9 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,16 +4,13 @@ This module contains the edge backtesting interface """ import logging -from argparse import Namespace from typing import Dict, Any from tabulate import tabulate from freqtrade.edge import Edge from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -77,34 +74,3 @@ class EdgeCli(object): if result: print('') # blank line for readability print(self._generate_edge_table(self.edge._cached_pairs)) - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for edge backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.EDGECLI) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - return config - - -def start(args: Namespace) -> None: - """ - Start Edge script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - logger.info('Starting freqtrade in Edge mode') - - # Initialize Edge object - edge_cli = EdgeCli(config) - edge_cli.start() From 8ad30e262578f076c7b9bf8f2399fdbcadc1aa06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:06:18 +0200 Subject: [PATCH 480/928] Adapt tests --- freqtrade/tests/optimize/test_edge_cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 488d552c8..dc40cc85c 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,7 +7,8 @@ from unittest.mock import MagicMock from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize.edge_cli import EdgeCli, setup_configuration, start +from freqtrade.optimize import start_edgecli, setup_configuration +from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -27,7 +28,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.EDGECLI) assert config['runmode'] == RunMode.EDGECLI assert 'max_open_trades' in config @@ -67,7 +68,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.EDGECLI) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -106,7 +107,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: 'edge' ] args = get_args(args) - start(args) + start_edgecli(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples From 71447e55aac787dce881adf32da776c747838791 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:14:31 +0200 Subject: [PATCH 481/928] Update missing import --- scripts/plot_dataframe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7fdc607e0..8a87d971c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -41,9 +41,10 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data from freqtrade.exchange import Exchange -from freqtrade.optimize.backtesting import setup_configuration +from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} @@ -107,7 +108,7 @@ def get_trading_env(args: Namespace): global _CONF # Load the configuration - _CONF.update(setup_configuration(args)) + _CONF.update(setup_configuration(args, RunMode.BACKTEST)) print(_CONF) pairs = args.pairs.split(',') From 201e02e73fbb39da0ee2b9a0687339edc489b49a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:25:59 +0200 Subject: [PATCH 482/928] Add test for Timeout - move tests to test_history --- freqtrade/tests/data/test_history.py | 70 +++++++++++++++++++++-- freqtrade/tests/optimize/test_hyperopt.py | 25 +++++++- freqtrade/tests/optimize/test_optimize.py | 65 --------------------- 3 files changed, 89 insertions(+), 71 deletions(-) delete mode 100644 freqtrade/tests/optimize/test_optimize.py diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 0d4210d3a..4d70d4cdd 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -2,24 +2,25 @@ import json import os -from pathlib import Path import uuid +from pathlib import Path from shutil import copyfile import arrow -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade import OperationalException from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, - load_tickerdata_file, - make_testdata_path, + load_tickerdata_file, make_testdata_path, trim_tickerlist) +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json -from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import get_patched_exchange, log_has, patch_exchange # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -495,3 +496,62 @@ def test_file_dump_json_tofile() -> None: # Remove the file _clean_test_file(file) + + +def test_get_timeframe(default_conf, mocker) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + ) + ) + min_date, max_date = history.get_timeframe(data) + assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + + +def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'], + fill_up_missing=False + ) + ) + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('1m')) + assert len(caplog.record_tuples) == 1 + assert log_has( + "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + caplog.record_tuples) + + +def test_validate_backtest_data(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + timerange = TimeRange('index', 'index', 200, 250) + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='5m', + pairs=['UNITTEST/BTC'], + timerange=timerange + ) + ) + + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert not history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('5m')) + assert len(caplog.record_tuples) == 0 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9128efa0c..b41f8ac36 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -3,6 +3,7 @@ import json import os from datetime import datetime from unittest.mock import MagicMock +from filelock import Timeout import pandas as pd import pytest @@ -11,7 +12,7 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -247,6 +248,28 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ) +def test_start_filelock(mocker, default_conf, caplog) -> None: + start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) + mocker.patch( + 'freqtrade.configuration.Configuration._load_config_file', + lambda *args, **kwargs: default_conf + ) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) + patch_exchange(mocker) + + args = [ + '--config', 'config.json', + 'hyperopt', + '--epochs', '5' + ] + args = get_args(args) + start_hyperopt(args) + assert log_has( + "Another running instance of freqtrade Hyperopt detected.", + caplog.record_tuples + ) + + def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py deleted file mode 100644 index 401592b53..000000000 --- a/freqtrade/tests/optimize/test_optimize.py +++ /dev/null @@ -1,65 +0,0 @@ -# pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade.arguments import TimeRange -from freqtrade.data import history -from freqtrade.exchange import timeframe_to_minutes -from freqtrade.strategy.default_strategy import DefaultStrategy -from freqtrade.tests.conftest import log_has, patch_exchange - - -def test_get_timeframe(default_conf, mocker) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'] - ) - ) - min_date, max_date = history.get_timeframe(data) - assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' - - -def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'], - fill_up_missing=False - ) - ) - min_date, max_date = history.get_timeframe(data) - caplog.clear() - assert history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) - assert len(caplog.record_tuples) == 1 - assert log_has( - "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", - caplog.record_tuples) - - -def test_validate_backtest_data(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - timerange = TimeRange('index', 'index', 200, 250) - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='5m', - pairs=['UNITTEST/BTC'], - timerange=timerange - ) - ) - - min_date, max_date = history.get_timeframe(data) - caplog.clear() - assert not history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) - assert len(caplog.record_tuples) == 0 From 0e228acbfb5f2605c099696e37915e8d8a8fe005 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 25 May 2019 22:42:17 +0300 Subject: [PATCH 483/928] minor: exchange debug logging humanized --- freqtrade/exchange/exchange.py | 19 +++++++++++++++---- freqtrade/tests/exchange/test_exchange.py | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 66857a7a5..72a0efb1f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -510,7 +510,11 @@ class Exchange(object): _LIMIT = 500 one_call = timeframe_to_msecs(ticker_interval) * _LIMIT - logger.debug("one_call: %s msecs", one_call) + logger.debug( + "one_call: %s msecs (%s)", + one_call, + arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) + ) input_coroutines = [self._async_get_candle_history( pair, ticker_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] @@ -541,7 +545,10 @@ class Exchange(object): or self._now_is_time_to_refresh(pair, ticker_interval)): input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) else: - logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval) + logger.debug( + "Using cached ohlcv data for pair %s, interval %s ...", + pair, ticker_interval + ) tickers = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) @@ -578,7 +585,11 @@ class Exchange(object): """ try: # fetch ohlcv asynchronously - logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms) + s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' + logger.debug( + "Fetching pair %s, interval %s, since %s %s...", + pair, ticker_interval, since_ms, s + ) data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, since=since_ms) @@ -593,7 +604,7 @@ class Exchange(object): except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) return pair, ticker_interval, [] - logger.debug("done fetching %s, %s ...", pair, ticker_interval) + logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) return pair, ticker_interval, data except ccxt.NotSupported as e: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 924ed538f..fda9c8241 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1016,7 +1016,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert log_has(f"Using cached ohlcv data for {pairs[0][0]}, {pairs[0][1]} ...", + assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", caplog.record_tuples) From e335e6c480350e8b7c91939cfae99e5ec9c91170 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 13:40:07 +0200 Subject: [PATCH 484/928] Fix some wordings --- docs/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 767cabf01..939ab3f7d 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -8,7 +8,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -Once you have Docker installed, simply create the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. +Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. ## Download the official FreqTrade docker image @@ -74,7 +74,7 @@ touch tradesv3.dryrun.sqlite Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. -To add additional libaries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. ```bash docker build -t freqtrade -f Dockerfile.technical . @@ -164,7 +164,7 @@ docker run -d \ To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` !!! Note - All available command line arguments can be added to the end of the `docker run` command. + All available bot command line parameters can be added to the end of the `docker run` command. ### Monitor your Docker instance @@ -201,4 +201,4 @@ docker run -d \ Head over to the [Backtesting Documentation](backtesting.md) for more details. !!! Note - Additional parameters can be appended after the image name (`freqtrade` in the above example). + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). From dab4307e04dd3ccbe3f860113a4ace43463d4fb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 14:40:03 +0200 Subject: [PATCH 485/928] Add secure way to genreate password, warn if no password is defined --- docs/rest-api.md | 7 +++++++ freqtrade/rpc/api_server.py | 4 ++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 10 +++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 535163da4..0508f83e4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -24,6 +24,13 @@ Sample configuration: You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. +To generate a secure password, either use a password manager, or use the below code snipped. + +``` python +import secrets +secrets.token_hex() +``` + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 14b15a3df..711202b27 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -106,6 +106,10 @@ class ApiServer(RPC): logger.warning("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json") + if not self._config['api_server'].get('password'): + logger.warning("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!") + # Run the Server logger.info('Starting Local Rest Server.') try: diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 4c3aea89a..b7721fd8e 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -156,7 +156,9 @@ def test_api_run(default_conf, mocker, caplog): server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", - "listen_port": "8089"}}) + "listen_port": "8089", + "password": "", + }}) apiserver.run() assert server_mock.call_count == 1 @@ -170,13 +172,15 @@ def test_api_run(default_conf, mocker, caplog): assert log_has("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json", caplog.record_tuples) + assert log_has("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!", + caplog.record_tuples) # Test crashing flask caplog.clear() mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() - assert log_has("Api server failed to start.", - caplog.record_tuples) + assert log_has("Api server failed to start.", caplog.record_tuples) def test_api_cleanup(default_conf, mocker, caplog): From 1988662607842601b7d1813c074a07bcf9b48a13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 20:19:06 +0200 Subject: [PATCH 486/928] Update plot-script to work with exported trades --- scripts/plot_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7fdc607e0..cc54bfff2 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -74,7 +74,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram file = Path(args.exportfilename) if file.exists(): - load_backtest_data(file) + trades = load_backtest_data(file) else: trades = pd.DataFrame([], columns=BT_DATA_COLUMNS) From 196a1bcc267696379efb539136a37f29399c92d9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:06 +0000 Subject: [PATCH 487/928] Update ccxt from 1.18.551 to 1.18.578 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3f755b8c0..1defdf19e 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.551 +ccxt==1.18.578 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 From bfb6dc4a8ea4eb031ef554c0b299fc4b9d380327 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:07 +0000 Subject: [PATCH 488/928] Update cachetools from 3.1.0 to 3.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 1defdf19e..e885e3e0f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.18.578 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 -cachetools==3.1.0 +cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 From 09e037c96ec2a0b1edaefef1f3641f0b7e8b6bc5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:09 +0000 Subject: [PATCH 489/928] Update scikit-learn from 0.21.1 to 0.21.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e885e3e0f..b149abacd 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 -scikit-learn==0.21.1 +scikit-learn==0.21.2 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From f7766d305b73d01ee90d7f45bfca5bbee30dfefd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 May 2019 19:42:12 +0200 Subject: [PATCH 490/928] Improve plotting documentation --- docs/plotting.md | 55 +++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 60c642ab3..20183ab9c 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,63 +1,79 @@ # Plotting + This page explains how to plot prices, indicator, profits. ## Installation Plotting scripts use Plotly library. Install/upgrade it with: -``` -pip install --upgrade plotly +``` bash +pip install -U -r requirements-plot.txt ``` At least version 2.3.0 is required. ## Plot price and indicators + Usage for the price plotter: -``` -script/plot_dataframe.py [-h] [-p pairs] [--live] +``` bash +python3 script/plot_dataframe.py [-h] [-p pairs] [--live] ``` Example -``` -python scripts/plot_dataframe.py -p BTC/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH ``` The `-p` pairs argument, can be used to specify pairs you would like to plot. -**Advanced use** +### Advanced use To plot multiple pairs, separate them with a comma: -``` -python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH ``` To plot the current live price use the `--live` flag: -``` -python scripts/plot_dataframe.py -p BTC/ETH --live + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --live ``` To plot a timerange (to zoom in): + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 ``` -python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 -``` + Timerange doesn't work with live data. To plot trades stored in a database use `--db-url` argument: + +``` bash +python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH + +To polt trades from a backtesting result, use `--export-filename ` + +``` bash +python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH ``` To plot a test strategy the strategy should have first be backtested. The results may then be plotted with the -s argument: -``` -python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// + +``` bash +python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// ``` ## Plot profit The profit plotter show a picture with three plots: + 1) Average closing price for all pairs 2) The summarized profit made by backtesting. Note that this is not the real-world profit, but @@ -76,13 +92,14 @@ that makes profit spikes. Usage for the profit plotter: -``` -script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] +``` bash +python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] ``` The `-p` pair argument, can be used to plot a single pair Example -``` + +``` bash python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC ``` From 8b028068bbe22b3aa0988c830aeb85316ee844e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 07:06:26 +0200 Subject: [PATCH 491/928] Fix typos, add section for custom indicators --- docs/plotting.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 20183ab9c..eb72b0502 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,6 +1,6 @@ # Plotting -This page explains how to plot prices, indicator, profits. +This page explains how to plot prices, indicators and profits. ## Installation @@ -10,8 +10,6 @@ Plotting scripts use Plotly library. Install/upgrade it with: pip install -U -r requirements-plot.txt ``` -At least version 2.3.0 is required. - ## Plot price and indicators Usage for the price plotter: @@ -26,8 +24,14 @@ Example python3 scripts/plot_dataframe.py -p BTC/ETH ``` -The `-p` pairs argument, can be used to specify -pairs you would like to plot. +The `-p` pairs argument can be used to specify pairs you would like to plot. + +Specify custom indicators. +Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd +``` ### Advanced use @@ -57,13 +61,13 @@ To plot trades stored in a database use `--db-url` argument: python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -To polt trades from a backtesting result, use `--export-filename ` +To plot trades from a backtesting result, use `--export-filename ` ``` bash python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH ``` -To plot a test strategy the strategy should have first be backtested. +To plot a custom strategy the strategy should have first be backtested. The results may then be plotted with the -s argument: ``` bash @@ -72,7 +76,7 @@ python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_dat ## Plot profit -The profit plotter show a picture with three plots: +The profit plotter shows a picture with three plots: 1) Average closing price for all pairs 2) The summarized profit made by backtesting. From 89f44c10a1d17e673a105b062d98857e0ee948ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 19:20:41 +0200 Subject: [PATCH 492/928] Fix grammar error --- docs/plotting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index eb72b0502..6dc3d13b1 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -87,7 +87,7 @@ The profit plotter shows a picture with three plots: The first graph is good to get a grip of how the overall market progresses. -The second graph will show how you algorithm works or doesnt. +The second graph will show how your algorithm works or doesn't. Perhaps you want an algorithm that steadily makes small profits, or one that acts less seldom, but makes big swings. From 55bdd2643954a38cfe6ebb144edfc04b84339894 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 19:25:01 +0200 Subject: [PATCH 493/928] Edgecli -> Edge for Runmode and start_edge() --- freqtrade/arguments.py | 4 ++-- freqtrade/optimize/__init__.py | 4 ++-- freqtrade/state.py | 4 ++-- freqtrade/tests/optimize/test_edge_cli.py | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index d6f0063d0..ddc0dc489 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,7 +340,7 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import start_backtesting, start_hyperopt, start_edgecli + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge subparsers = self.parser.add_subparsers(dest='subparser') @@ -352,7 +352,7 @@ class Arguments(object): # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=start_edgecli) + edge_cmd.set_defaults(func=start_edge) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index cb01950b4..475aaa82f 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -99,7 +99,7 @@ def start_hyperopt(args: Namespace) -> None: # Same in Edge and Backtesting start() functions. -def start_edgecli(args: Namespace) -> None: +def start_edge(args: Namespace) -> None: """ Start Edge script :param args: Cli args from Arguments() @@ -107,7 +107,7 @@ def start_edgecli(args: Namespace) -> None: """ from freqtrade.optimize.edge_cli import EdgeCli # Initialize configuration - config = setup_configuration(args, RunMode.EDGECLI) + config = setup_configuration(args, RunMode.EDGE) logger.info('Starting freqtrade in Edge mode') # Initialize Edge object diff --git a/freqtrade/state.py b/freqtrade/state.py index b69c26cb5..ce2683a77 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -18,11 +18,11 @@ class State(Enum): class RunMode(Enum): """ Bot running mode (backtest, hyperopt, ...) - can be "live", "dry-run", "backtest", "edgecli", "hyperopt". + can be "live", "dry-run", "backtest", "edge", "hyperopt". """ LIVE = "live" DRY_RUN = "dry_run" BACKTEST = "backtest" - EDGECLI = "edgecli" + EDGE = "edge" HYPEROPT = "hyperopt" OTHER = "other" # Used for plotting scripts and test diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index dc40cc85c..5d16b0f2d 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize import start_edgecli, setup_configuration +from freqtrade.optimize import start_edge, setup_configuration from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -28,8 +28,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args), RunMode.EDGECLI) - assert config['runmode'] == RunMode.EDGECLI + config = setup_configuration(get_args(args), RunMode.EDGE) + assert config['runmode'] == RunMode.EDGE assert 'max_open_trades' in config assert 'stake_currency' in config @@ -68,14 +68,14 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args), RunMode.EDGECLI) + config = setup_configuration(get_args(args), RunMode.EDGE) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert config['runmode'] == RunMode.EDGECLI + assert config['runmode'] == RunMode.EDGE assert log_has( 'Using data folder: {} ...'.format(config['datadir']), caplog.record_tuples @@ -107,7 +107,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: 'edge' ] args = get_args(args) - start_edgecli(args) + start_edge(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples From 536c8fa4549715ab6bd63d35ef7b0b4a1223f385 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:04:39 +0300 Subject: [PATCH 494/928] move python version check to the top --- freqtrade/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 809ab3c7a..35fbccfa3 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,8 +3,14 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ -import logging + import sys +# check min. python version +if sys.version_info < (3, 6): + sys.exit("Freqtrade requires Python version >= 3.6") + +# flake8: noqa E402 +import logging from argparse import Namespace from typing import List @@ -26,10 +32,6 @@ def main(sysargv: List[str]) -> None: worker = None return_code = 1 - # check min. python version - if sys.version_info < (3, 6): - raise SystemError("Freqtrade requires Python version >= 3.6") - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' From 58477dcd8205784ba58c69233229f285fb272c20 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:25:19 +0300 Subject: [PATCH 495/928] cleanup: return after cmd removed in main() --- freqtrade/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 35fbccfa3..d8c447800 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -44,11 +44,10 @@ def main(sysargv: List[str]) -> None: args.func(args) # TODO: fetch return_code as returned by the command function here return_code = 0 - return - - # Load and run worker - worker = Worker(args) - worker.run() + else: + # Load and run worker + worker = Worker(args) + worker.run() except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') From db2e6f2d1c9a25c077f660c1738bc65b64f26a7e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:25:53 +0300 Subject: [PATCH 496/928] tests adjusted --- freqtrade/tests/test_main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e4ffc5fae..9e7cc4a66 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -20,7 +20,9 @@ def test_parse_args_backtesting(mocker) -> None: further argument parsing is done in test_arguments.py """ backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) - main(['backtesting']) + # it's sys.exit(0) at the end of backtesting + with pytest.raises(SystemExit): + main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] @@ -33,7 +35,9 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) - main(['hyperopt']) + # it's sys.exit(0) at the end of hyperopt + with pytest.raises(SystemExit): + main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == ['config.json'] From ea83b2b1d0542adc299513c2876963a433cea987 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 29 May 2019 14:17:09 +0200 Subject: [PATCH 497/928] legacy code removed. --- user_data/strategies/test_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 3cb78842f..66a5f8c09 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -51,7 +51,7 @@ class TestStrategy(IStrategy): ticker_interval = '5m' # run "populate_indicators" only for new candle - ta_on_candle = False + process_only_new_candles = False # Experimental settings (configuration will overide these if set) use_sell_signal = False From 7406edfd8fcad1b5104e470f43c862cf70ed4cc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:51:50 +0200 Subject: [PATCH 498/928] Move set_loggers to main() --- freqtrade/__main__.py | 1 - freqtrade/main.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 7d271dfd1..628fc930f 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -11,5 +11,4 @@ import sys from freqtrade import main if __name__ == '__main__': - main.set_loggers() main.main(sys.argv[1:]) diff --git a/freqtrade/main.py b/freqtrade/main.py index d8c447800..d0e783808 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -28,7 +28,24 @@ def main(sysargv: List[str]) -> None: This function will initiate the bot and start the trading loop. :return: None """ + set_loggers() + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() + + # A subcommand has been issued. + # Means if Backtesting or Hyperopt have been called we exit the bot + if hasattr(args, 'func'): + args.func(args) + return + + worker = None + return_code = 1 try: + set_loggers() + worker = None return_code = 1 @@ -64,5 +81,4 @@ def main(sysargv: List[str]) -> None: if __name__ == '__main__': - set_loggers() main(sys.argv[1:]) From 17d614c66a4ab39484b13582061e846d637d7b4f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:07:52 +0200 Subject: [PATCH 499/928] Remove binary script - allow None arguemnts --- bin/freqtrade | 7 ------- freqtrade/__main__.py | 4 +--- freqtrade/main.py | 4 ++-- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100755 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade deleted file mode 100755 index e7ae7a4ca..000000000 --- a/bin/freqtrade +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from freqtrade.main import main, set_loggers -set_loggers() -main(sys.argv[1:]) diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 628fc930f..97ed9ae67 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -6,9 +6,7 @@ To launch Freqtrade as a module > python -m freqtrade (with Python >= 3.6) """ -import sys - from freqtrade import main if __name__ == '__main__': - main.main(sys.argv[1:]) + main.main() diff --git a/freqtrade/main.py b/freqtrade/main.py index d0e783808..9fc8c9d0c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -23,7 +23,7 @@ from freqtrade.worker import Worker logger = logging.getLogger('freqtrade') -def main(sysargv: List[str]) -> None: +def main(sysargv: List[str] = None) -> None: """ This function will initiate the bot and start the trading loop. :return: None @@ -81,4 +81,4 @@ def main(sysargv: List[str]) -> None: if __name__ == '__main__': - main(sys.argv[1:]) + main() From c5ef700eb700dd30d58d6b4d0236e1ec352e361d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:08:35 +0200 Subject: [PATCH 500/928] Use autogenerated entrypoint --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35fdb2938..ca2f81d1f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ setup(name='freqtrade', author_email='michael.egger@tsn.at', license='GPLv3', packages=['freqtrade'], - scripts=['bin/freqtrade'], setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ @@ -43,6 +42,11 @@ setup(name='freqtrade', ], include_package_data=True, zip_safe=False, + entry_points={ + 'console_scripts': [ + 'freqtrade = freqtrade.main:main', + ], + }, classifiers=[ 'Programming Language :: Python :: 3.6', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', From 22144d89fc3cf4c69a7bef6db3b9a33b0dd9e6ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:31:30 +0200 Subject: [PATCH 501/928] Fix mypy error --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ddc0dc489..89b587c6f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -27,7 +27,7 @@ class Arguments(object): Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: List[str], description: str) -> None: + def __init__(self, args: Optional[List[str]], description: str) -> None: self.args = args self.parsed_arg: Optional[argparse.Namespace] = None self.parser = argparse.ArgumentParser(description=description) From 9e4dd6f37f7f698c4ae92a91b2293bd9fa2a46ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 May 2019 19:27:24 +0200 Subject: [PATCH 502/928] Read bin/freqtrade with deprecation warning --- bin/freqtrade | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade new file mode 100755 index 000000000..b9e3a7008 --- /dev/null +++ b/bin/freqtrade @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import sys +import warnings + +from freqtrade.main import main, set_loggers + +set_loggers() + +warnings.warn( + "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", + DeprecationWarning) +main(sys.argv[1:]) From 7b367818fc79366c883ee06a6de6b8b57b75e95e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 19:46:46 +0200 Subject: [PATCH 503/928] Remove duplicate code --- freqtrade/main.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 9fc8c9d0c..4b1decdc5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -28,21 +28,7 @@ def main(sysargv: List[str] = None) -> None: This function will initiate the bot and start the trading loop. :return: None """ - set_loggers() - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args: Namespace = arguments.get_parsed_arg() - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot - if hasattr(args, 'func'): - args.func(args) - return - - worker = None - return_code = 1 try: set_loggers() From d7bebc4385cf833aa69732343ec7d62fd36f6762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 19:54:47 +0200 Subject: [PATCH 504/928] persistence.init does not need the config dict --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence.py | 5 ++--- freqtrade/tests/test_persistence.py | 24 ++++++++++++------------ scripts/plot_dataframe.py | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8b29d6d40..1a5187d8c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,7 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config) + persistence.init(self.config.get('db_url', None), self.config.get('dry_run', False)) # Set initial bot state from config initial_state = self.config.get('initial_state') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e64e0b89c..5218b793a 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,7 +25,7 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(config: Dict) -> None: +def init(db_url: str, dry_run: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers @@ -33,7 +33,6 @@ def init(config: Dict) -> None: :param config: config to use :return: None """ - db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -57,7 +56,7 @@ def init(config: Dict) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if config.get('dry_run', False) and db_url != 'sqlite://': + if dry_run and db_url != 'sqlite://': clean_dry_run_db() diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 8c15fa8e8..7e47abf39 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -13,12 +13,12 @@ from freqtrade.tests.conftest import log_has @pytest.fixture(scope='function') def init_persistence(default_conf): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_create_session(default_conf): # Check if init create a session - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'Session' in type(Trade.session).__name__ @@ -28,7 +28,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -37,7 +37,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -46,7 +46,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -57,7 +57,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' @@ -336,8 +336,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit_percent(fee=0.003) == 0.06147824 +@pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -424,7 +424,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(create_table_old) engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -497,7 +497,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -566,7 +566,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -668,8 +668,8 @@ def test_adjust_min_max_rates(fee): assert trade.min_rate == 0.96 +@pytest.mark.usefixtures("init_persistence") def test_get_open(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -713,8 +713,8 @@ def test_get_open(default_conf, fee): assert len(Trade.get_open_trades()) == 2 +@pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ba549deb5..49ae857b6 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,7 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(_CONF) + persistence.init(args.db_url, True) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From c2f6897d8b722a1c92cd845f63adfe1270c7bff7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 20:10:48 +0200 Subject: [PATCH 505/928] Move download of live data to load_data Avoids code duplication in backtesting and plot_dataframe --- freqtrade/data/history.py | 26 +++++++++++++++++--------- freqtrade/optimize/backtesting.py | 29 +++++++++++------------------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index e0f9f67db..29a7f3478 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -122,21 +122,29 @@ def load_data(datadir: Optional[Path], refresh_pairs: bool = False, exchange: Optional[Exchange] = None, timerange: TimeRange = TimeRange(None, None, 0, 0), - fill_up_missing: bool = True) -> Dict[str, DataFrame]: + fill_up_missing: bool = True, + live: bool = False + ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs the given parameters :return: dict(:) """ result = {} + if live: + logger.info('Live: Downloading data for all defined pairs ...') + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) + result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + else: + logger.info('Using local backtesting data ...') - for pair in pairs: - hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, - datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, - fill_up_missing=fill_up_missing) - if hist is not None: - result[pair] = hist + for pair in pairs: + hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, + datadir=datadir, timerange=timerange, + refresh_pairs=refresh_pairs, + exchange=exchange, + fill_up_missing=fill_up_missing) + if hist is not None: + result[pair] = hist return result diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bdd42943b..76c6556fa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -401,24 +401,17 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - if self.config.get('live'): - logger.info('Downloading data for all pairs in whitelist ...') - self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs]) - data = {key[0]: value for key, value in self.exchange._klines.items()} - - else: - logger.info('Using local backtesting data (using whitelist in given config) ...') - - timerange = Arguments.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, - pairs=pairs, - ticker_interval=self.ticker_interval, - refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, - timerange=timerange - ) + timerange = Arguments.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, + pairs=pairs, + ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, + timerange=timerange, + live=self.config.get('live', False) + ) if not data: logger.critical("No data found. Terminating.") From 15984b5c432b0f19fc1a8ace8ab6f36427b16e02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 20:25:07 +0200 Subject: [PATCH 506/928] Adjust some tests - implement new "live" method to plot_script --- freqtrade/data/history.py | 13 ++++++--- freqtrade/tests/data/test_history.py | 29 +++++++++++++++++++- freqtrade/tests/optimize/test_backtesting.py | 7 ++--- scripts/plot_dataframe.py | 24 ++++++---------- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 29a7f3478..2dacce8c6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -129,11 +129,16 @@ def load_data(datadir: Optional[Path], Loads ticker history data for a list of pairs the given parameters :return: dict(:) """ - result = {} + result: Dict[str, DataFrame] = {} if live: - logger.info('Live: Downloading data for all defined pairs ...') - exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) - result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + if exchange: + logger.info('Live: Downloading data for all defined pairs ...') + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) + result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + else: + raise OperationalException( + "Exchange needs to be initialized when using live data." + ) else: logger.info('Using local backtesting data ...') diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 4d70d4cdd..a13bc34af 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -5,6 +5,7 @@ import os import uuid from pathlib import Path from shutil import copyfile +from unittest.mock import MagicMock import arrow import pytest @@ -20,7 +21,8 @@ from freqtrade.data.history import (download_pair_history, from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.strategy.default_strategy import DefaultStrategy -from freqtrade.tests.conftest import get_patched_exchange, log_has, patch_exchange +from freqtrade.tests.conftest import (get_patched_exchange, log_has, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -136,6 +138,31 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau _clean_test_file(file) +def test_load_data_live(default_conf, mocker, caplog) -> None: + refresh_mock = MagicMock() + mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) + exchange = get_patched_exchange(mocker, default_conf) + + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + live=True, + exchange=exchange) + assert refresh_mock.call_count == 1 + assert len(refresh_mock.call_args_list[0][0][0]) == 2 + assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples) + + +def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None: + + with pytest.raises(OperationalException, + match=r'Exchange needs to be initialized when using live data.'): + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + exchange=None, + live=True, + ) + + def test_testdata_path() -> None: assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5b42cae34..3f88a8d6c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -105,7 +105,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None): + timerange=None, exchange=None, live=False): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)} return pairdata @@ -492,7 +492,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result exists = [ - 'Using local backtesting data (using whitelist in given config) ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' @@ -857,7 +856,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' @@ -916,7 +915,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ba549deb5..27932b559 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -139,21 +139,15 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): ticker_interval = strategy.ticker_interval timerange = Arguments.parse_timerange(args.timerange) - tickers = {} - if args.live: - logger.info('Downloading pairs.') - exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) - for pair in pairs: - tickers[pair] = exchange.klines((pair, ticker_interval)) - else: - tickers = history.load_data( - datadir=Path(str(_CONF.get("datadir"))), - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), - timerange=timerange, - exchange=Exchange(_CONF) - ) + tickers = history.load_data( + datadir=Path(str(_CONF.get("datadir"))), + pairs=pairs, + ticker_interval=ticker_interval, + refresh_pairs=_CONF.get('refresh_pairs', False), + timerange=timerange, + exchange=Exchange(_CONF), + live=args.live, + ) # No ticker found, impossible to download, len mismatch for pair, data in tickers.copy().items(): From fb88953be330d3e7bbb0d8f15de39087e4737096 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 29 May 2019 21:57:14 +0300 Subject: [PATCH 507/928] refactoring download_backtest_data.py --- freqtrade/arguments.py | 47 +++++++----- freqtrade/configuration.py | 31 +++++--- freqtrade/tests/test_arguments.py | 8 +-- scripts/download_backtest_data.py | 116 +++++++++++++++++------------- 4 files changed, 119 insertions(+), 83 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ddc0dc489..c94c02a8b 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -47,7 +47,7 @@ class Arguments(object): return self.parsed_arg - def parse_args(self) -> argparse.Namespace: + def parse_args(self, no_default_config: bool = False) -> argparse.Namespace: """ Parses given arguments and returns an argparse Namespace instance. """ @@ -55,7 +55,7 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if parsed_arg.config is None: + if parsed_arg.config is None and not no_default_config: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -427,26 +427,24 @@ class Arguments(object): default=None ) - def testdata_dl_options(self) -> None: + def download_data_options(self) -> None: """ Parses given arguments for testdata download """ self.parser.add_argument( - '--pairs-file', - help='File containing a list of pairs to download.', - dest='pairs_file', - default=None, - metavar='PATH', + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + dest='loglevel', + default=0, ) - self.parser.add_argument( - '--export', - help='Export files to given dir.', - dest='export', - default=None, - metavar='PATH', + '--logfile', + help='Log to the file specified', + dest='logfile', + type=str, + metavar='FILE' ) - self.parser.add_argument( '-c', '--config', help='Specify configuration file (default: %(default)s). ' @@ -456,7 +454,21 @@ class Arguments(object): type=str, metavar='PATH', ) - + self.parser.add_argument( + '-d', '--datadir', + help='Path to backtest data.', + dest='datadir', + default=None, + type=str, + metavar='PATH', + ) + self.parser.add_argument( + '--pairs-file', + help='File containing a list of pairs to download.', + dest='pairs_file', + default=None, + metavar='PATH', + ) self.parser.add_argument( '--days', help='Download data for given number of days.', @@ -465,7 +477,6 @@ class Arguments(object): metavar='INT', default=None ) - self.parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', @@ -473,7 +484,6 @@ class Arguments(object): type=str, default='bittrex' ) - self.parser.add_argument( '-t', '--timeframes', help='Specify which tickers to download. Space separated list. \ @@ -484,7 +494,6 @@ class Arguments(object): nargs='+', dest='timeframes', ) - self.parser.add_argument( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index c19580c36..58fd1d6d9 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -122,12 +122,11 @@ class Configuration(object): return conf - def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_logging_config(self, config: Dict[str, Any]) -> None: """ - Extract information for sys.argv and load common configuration - :return: configuration as dictionary + Extract information for sys.argv and load logging configuration: + the --loglevel, --logfile options """ - # Log level if 'loglevel' in self.args and self.args.loglevel: config.update({'verbosity': self.args.loglevel}) @@ -153,6 +152,13 @@ class Configuration(object): set_loggers(config['verbosity']) logger.info('Verbosity set to %s', config['verbosity']) + def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract information for sys.argv and load common configuration + :return: configuration as dictionary + """ + self._load_logging_config(config) + # Support for sd_notify if self.args.sd_notify: config['internals'].update({'sd_notify': True}) @@ -228,6 +234,17 @@ class Configuration(object): else: logger.info(logstring.format(config[argname])) + def _load_datadir_config(self, config: Dict[str, Any]) -> None: + """ + Extract information for sys.argv and load datadir configuration: + the --datadir option + """ + if 'datadir' in self.args and self.args.datadir: + config.update({'datadir': self._create_datadir(config, self.args.datadir)}) + else: + config.update({'datadir': self._create_datadir(config, None)}) + logger.info('Using data folder: %s ...', config.get('datadir')) + def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ Extract information for sys.argv and load Optimize configuration @@ -263,11 +280,7 @@ class Configuration(object): self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') - if 'datadir' in self.args and self.args.datadir: - config.update({'datadir': self._create_datadir(config, self.args.datadir)}) - else: - config.update({'datadir': self._create_datadir(config, None)}) - logger.info('Using data folder: %s ...', config.get('datadir')) + self._load_datadir_config(config) self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index ecd108b5e..afa42f287 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -170,18 +170,18 @@ def test_parse_args_hyperopt_custom() -> None: assert call_args.func is not None -def test_testdata_dl_options() -> None: +def test_download_data_options() -> None: args = [ '--pairs-file', 'file_with_pairs', - '--export', 'export/folder', + '--datadir', 'datadir/folder', '--days', '30', '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.testdata_dl_options() + arguments.download_data_options() args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' - assert args.export == 'export/folder' + assert args.datadir == 'datadir/folder' assert args.days == 30 assert args.exchange == 'binance' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 42b305778..acf86d0d8 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -1,39 +1,39 @@ #!/usr/bin/env python3 """ -This script generates json data +This script generates json files with pairs history data """ +import arrow import json import sys from pathlib import Path -import arrow -from typing import Any, Dict +from typing import Any, Dict, List -from freqtrade.arguments import Arguments -from freqtrade.arguments import TimeRange -from freqtrade.exchange import Exchange +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.configuration import Configuration from freqtrade.data.history import download_pair_history -from freqtrade.configuration import Configuration, set_loggers +from freqtrade.exchange import Exchange from freqtrade.misc import deep_merge_dicts import logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -) -set_loggers(0) + +logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' arguments = Arguments(sys.argv[1:], 'download utility') -arguments.testdata_dl_options() -args = arguments.parse_args() +arguments.download_data_options() + +# Do not read the default config if config is not specified +# in the command line options explicitely +args = arguments.parse_args(no_default_config=True) timeframes = args.timeframes +pairs: List = [] + +configuration = Configuration(args) +config: Dict[str, Any] = {} if args.config: - configuration = Configuration(args) - - config: Dict[str, Any] = {} # Now expecting a list of config filenames here, not a string for path in args.config: print(f"Using config: {path}...") @@ -42,9 +42,19 @@ if args.config: config['stake_currency'] = '' # Ensure we do not use Exchange credentials + config['exchange']['dry_run'] = True config['exchange']['key'] = '' config['exchange']['secret'] = '' + + if args.exchange: + config['exchange']['name'] = args.exchange + + pairs = config['exchange']['pair_whitelist'] + timeframes = [config['ticker_interval']] + else: + if not args.exchange: + sys.exit("No exchange specified.") config = { 'stake_currency': '', 'dry_run': True, @@ -60,55 +70,59 @@ else: } } +configuration._load_logging_config(config) +configuration._load_datadir_config(config) -dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name']) -if args.export: - dl_path = Path(args.export) - -if not dl_path.is_dir(): - sys.exit(f'Directory {dl_path} does not exist.') +dl_path = Path(config['datadir']) pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') -if not pairs_file.exists(): - sys.exit(f'No pairs file found with path {pairs_file}.') -with pairs_file.open() as file: - PAIRS = list(set(json.load(file))) +if not pairs or args.pairs_file: + print(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}".') -PAIRS.sort() + 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 = arguments.parse_timerange(f'{time_since}-') +print(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') -print(f'About to download pairs: {PAIRS} to {dl_path}') - -# Init exchange -exchange = Exchange(config) pairs_not_available = [] -for pair in PAIRS: - if pair not in exchange._api.markets: - pairs_not_available.append(pair) - print(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(): - print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') - dl_file.unlink() +try: + # Init exchange + exchange = Exchange(config) - print(f'downloading pair {pair}, interval {ticker_interval}') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, - ticker_interval=ticker_interval, - timerange=timerange) + for pair in pairs: + if pair not in exchange._api.markets: + pairs_not_available.append(pair) + print(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(): + print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') + dl_file.unlink() + print(f'downloading pair {pair}, interval {ticker_interval}') + download_pair_history(datadir=dl_path, exchange=exchange, + pair=pair, ticker_interval=ticker_interval, + timerange=timerange) -if pairs_not_available: - print(f"Pairs [{','.join(pairs_not_available)}] not availble.") +except KeyboardInterrupt: + sys.exit("SIGINT received, aborting ...") + +finally: + if pairs_not_available: + print(f"Pairs [{','.join(pairs_not_available)}] not availble.") From d6cf3144813a6f1d14a50c195f109a1ff027c9bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 06:30:06 +0200 Subject: [PATCH 508/928] Don't default to false for init() --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1a5187d8c..dfffc21b3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,7 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config.get('db_url', None), self.config.get('dry_run', False)) + persistence.init(self.config.get('db_url', None), self.config.get('dry_run')) # Set initial bot state from config initial_state = self.config.get('initial_state') From b6e8fecbf50b3f22c7efdbabe7e3f75230d8c166 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 06:31:34 +0200 Subject: [PATCH 509/928] Change persistence.init parameter It should describe what it does --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence.py | 8 +++++--- scripts/plot_dataframe.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dfffc21b3..3f8c1e106 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,8 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config.get('db_url', None), self.config.get('dry_run')) + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) # Set initial bot state from config initial_state = self.config.get('initial_state') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 5218b793a..3d86d4f4d 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,12 +25,14 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(db_url: str, dry_run: bool = False) -> None: +def init(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates - :param config: config to use + :param db_url: Database to use + :param clean_open_orders: Remove open orders from the database. + Useful for dry-run or if all orders have been reset on the exchange. :return: None """ kwargs = {} @@ -56,7 +58,7 @@ def init(db_url: str, dry_run: bool = False) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if dry_run and db_url != 'sqlite://': + if clean_open_orders and db_url != 'sqlite://': clean_dry_run_db() diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 49ae857b6..74e8573e5 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,7 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(args.db_url, True) + persistence.init(args.db_url, False) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From f463817c883a8510054bbc486c5c18bfe8da9eab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 09:51:32 +0300 Subject: [PATCH 510/928] change metavar for --pairs-file --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index c94c02a8b..711975fd0 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -467,7 +467,7 @@ class Arguments(object): help='File containing a list of pairs to download.', dest='pairs_file', default=None, - metavar='PATH', + metavar='FILE', ) self.parser.add_argument( '--days', From 11f535e79f74d0fd41632768c65773595356c75d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 09:54:58 +0300 Subject: [PATCH 511/928] change prints to logging --- scripts/download_backtest_data.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index acf86d0d8..acdfb25ac 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -36,7 +36,7 @@ config: Dict[str, Any] = {} if args.config: # Now expecting a list of config filenames here, not a string for path in args.config: - print(f"Using config: {path}...") + logger.info(f"Using config: {path}...") # Merge config options, overwriting old values config = deep_merge_dicts(configuration._load_config_file(path), config) @@ -78,7 +78,7 @@ 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: - print(f'Reading pairs file "{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(): @@ -94,7 +94,7 @@ if args.days: time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") timerange = arguments.parse_timerange(f'{time_since}-') -print(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') +logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}') pairs_not_available = [] @@ -105,17 +105,17 @@ try: for pair in pairs: if pair not in exchange._api.markets: pairs_not_available.append(pair) - print(f"skipping pair {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(): - print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') + logger.info(f'Deleting existing data for pair {pair}, interval {ticker_interval}') dl_file.unlink() - print(f'downloading pair {pair}, interval {ticker_interval}') + logger.info(f'downloading pair {pair}, interval {ticker_interval}') download_pair_history(datadir=dl_path, exchange=exchange, pair=pair, ticker_interval=ticker_interval, timerange=timerange) @@ -125,4 +125,4 @@ except KeyboardInterrupt: finally: if pairs_not_available: - print(f"Pairs [{','.join(pairs_not_available)}] not availble.") + logger.info(f"Pairs [{','.join(pairs_not_available)}] not availble.") From 39932627bd25d5fde92242a6e307574846e372c4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 11:03:17 +0300 Subject: [PATCH 512/928] typo in log message fixed --- scripts/download_backtest_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index acdfb25ac..b7c302437 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -125,4 +125,4 @@ except KeyboardInterrupt: finally: if pairs_not_available: - logger.info(f"Pairs [{','.join(pairs_not_available)}] not availble.") + logger.info(f"Pairs [{','.join(pairs_not_available)}] not available.") From ef15f2bdc67c65ec2d05f730006502f536a8cde4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 11:07:31 +0300 Subject: [PATCH 513/928] log messages slightly improved --- scripts/download_backtest_data.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index b7c302437..196af5bed 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -105,17 +105,18 @@ try: for pair in pairs: if pair not in exchange._api.markets: pairs_not_available.append(pair) - logger.info(f"skipping pair {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}') + logger.info( + f'Deleting existing data for pair {pair}, interval {ticker_interval}.') dl_file.unlink() - logger.info(f'downloading pair {pair}, interval {ticker_interval}') + logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') download_pair_history(datadir=dl_path, exchange=exchange, pair=pair, ticker_interval=ticker_interval, timerange=timerange) @@ -125,4 +126,6 @@ except KeyboardInterrupt: finally: if pairs_not_available: - logger.info(f"Pairs [{','.join(pairs_not_available)}] not available.") + logger.info( + f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {config['exchange']['name']}.") From 6b144150c757e81cda5db8e66f681859fce2f02c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 20:38:04 +0300 Subject: [PATCH 514/928] fix handling of SystemExit --- freqtrade/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 4b1decdc5..f693bba5c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -52,13 +52,15 @@ def main(sysargv: List[str] = None) -> None: worker = Worker(args) worker.run() + except SystemExit as e: + return_code = e except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: + except Exception: logger.exception('Fatal exception!') finally: if worker: From e4e22167bb575e608775caac2b93281d199891c8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 21:00:16 +0300 Subject: [PATCH 515/928] make mypy happy --- freqtrade/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index f693bba5c..6f073f5d4 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -12,7 +12,7 @@ if sys.version_info < (3, 6): # flake8: noqa E402 import logging from argparse import Namespace -from typing import List +from typing import Any, List from freqtrade import OperationalException from freqtrade.arguments import Arguments @@ -29,12 +29,11 @@ def main(sysargv: List[str] = None) -> None: :return: None """ + return_code: Any = 1 + worker = None try: set_loggers() - worker = None - return_code = 1 - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' From 1add432673aa76c378bed6030ce0f53939353443 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 23:00:19 +0300 Subject: [PATCH 516/928] docs adjusted --- docs/backtesting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index a25d3c1d5..5a25bc255 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -123,11 +123,12 @@ 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 folder than the exchange specific default, use `--export user_data/data/some_directory`. +- To use a different folder 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 folder, 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). From 338f2a2322b87affc4c34f63517f31773e4ef131 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Jun 2019 06:26:03 +0200 Subject: [PATCH 517/928] Use kwarg to call persistence.init() --- scripts/plot_dataframe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 74e8573e5..9316c953b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,8 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(args.db_url, False) + persistence.init(args.db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From 199426460a4b1ba6bae242eff2b7b07e9f5ddfcc Mon Sep 17 00:00:00 2001 From: Yuliyan Perfanov Date: Sun, 2 Jun 2019 13:25:09 +0300 Subject: [PATCH 518/928] implemented DataProvider.orderbook() --- freqtrade/data/dataprovider.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index df4accf93..2852cbcb0 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -85,8 +85,7 @@ class DataProvider(object): """ return latest orderbook data """ - # TODO: Implement me - pass + return self._exchange.get_order_book(pair, max) @property def runmode(self) -> RunMode: From c68fe7a6857468c8c39c4aec1110c393663b2883 Mon Sep 17 00:00:00 2001 From: Yuliyan Perfanov Date: Sun, 2 Jun 2019 13:27:44 +0300 Subject: [PATCH 519/928] example how to use best bid and ask in strategy --- user_data/strategies/test_strategy.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 66a5f8c09..7edaead14 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -253,6 +253,15 @@ class TestStrategy(IStrategy): dataframe['ha_low'] = heikinashi['low'] """ + # Retrieve best bid and best ask + # ------------------------------------ + """ + ob = self.dp.orderbook(metadata['pair'], 1) + print(ob) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 36dae7cc6c513e590ff8cbeb36be5b82b8393664 Mon Sep 17 00:00:00 2001 From: Misagh Date: Sun, 2 Jun 2019 13:27:31 +0200 Subject: [PATCH 520/928] trailing stoploss reason fixed --- freqtrade/strategy/interface.py | 5 +++-- freqtrade/tests/test_freqtradebot.py | 32 ++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index caf56f13e..db266d95f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -329,8 +329,9 @@ class IStrategy(ABC): (not self.order_types.get('stoploss_on_exchange'))): selltype = SellType.STOP_LOSS - # If Trailing stop (and max-rate did move above open rate) - if trailing_stop and trade.open_rate != trade.max_rate: + + # If initial stoploss is not the same as current one then it is trailing. + if trade.initial_stop_loss != trade.stop_loss: selltype = SellType.TRAILING_STOP_LOSS logger.debug( f"HIT STOP: current price at {current_rate:.6f}, " diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 67b05ac3e..790e90641 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2493,9 +2493,9 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ - 'bid': 0.00000102, - 'ask': 0.00000103, - 'last': 0.00000102 + 'bid': 0.00001099, + 'ask': 0.00001099, + 'last': 0.00001099 }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, @@ -2507,15 +2507,33 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trade() - trade = Trade.query.first() - trade.update(limit_buy_order) - trade.max_rate = trade.open_rate * 1.003 + assert freqtrade.handle_trade(trade) is False + + # Raise ticker above buy price + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': 0.00001099 * 1.5, + 'ask': 0.00001099 * 1.5, + 'last': 0.00001099 * 1.5 + })) + + # Stoploss should be adjusted + assert freqtrade.handle_trade(trade) is False + + # Price fell + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': 0.00001099 * 1.1, + 'ask': 0.00001099 * 1.1, + 'last': 0.00001099 * 1.1 + })) + caplog.set_level(logging.DEBUG) # Sell as trailing-stop is reached assert freqtrade.handle_trade(trade) is True assert log_has( - f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, ' + f'HIT STOP: current price at 0.000012, stop loss is 0.000015, ' f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value From 107c3beb200fefe805f97b02223b3e0159935af2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Jun 2019 15:28:29 +0200 Subject: [PATCH 521/928] Fix test-failure introduced in #1891 --- freqtrade/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 59989d604..a907b33ed 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -96,7 +96,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - persistence.init(config) + persistence.init(config['db_url']) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) From bd8edd61fd29f304bdfdb6c03fcc81b6a45cfaf4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:12 +0200 Subject: [PATCH 522/928] Update numpy from 1.16.3 to 1.16.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da87f56d9..52442fb19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Load common requirements -r requirements-common.txt -numpy==1.16.3 +numpy==1.16.4 pandas==0.24.2 scipy==1.3.0 From c04a8a102469a099230e09c85b8fb4e48798c5d0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:13 +0200 Subject: [PATCH 523/928] Update ccxt from 1.18.578 to 1.18.615 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 434944aad..7620ddada 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.578 +ccxt==1.18.615 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 From 51113dae0e2d21fee9095ddaa5b03ddf931ff86d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:16 +0200 Subject: [PATCH 524/928] Update sqlalchemy from 1.3.3 to 1.3.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 7620ddada..ee8b08b75 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.615 -SQLAlchemy==1.3.3 +SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.13.2 cachetools==3.1.1 From 4ef8a74977acf7fb55a8350269f2be00e7a616fd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:19 +0200 Subject: [PATCH 525/928] Update arrow from 0.13.2 to 0.14.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index ee8b08b75..bf21c6e59 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.615 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 -arrow==0.13.2 +arrow==0.14.1 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From 3c1ae07f92d1399cbc336f37d32db1afb9801050 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:20 +0200 Subject: [PATCH 526/928] Update flask from 1.0.2 to 1.0.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index bf21c6e59..9e854e4af 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.1 sdnotify==0.3.2 # Api server -flask==1.0.2 +flask==1.0.3 From a132517f0abeb60071608ba39b405cdaa7838f60 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:24 +0200 Subject: [PATCH 527/928] Update pytest from 4.5.0 to 4.6.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fa52a4869..531d99940 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.5.0 +pytest==4.6.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From f75e97e9b0e15cc114101051ab4d57a038b7ddc3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:25 +0200 Subject: [PATCH 528/928] Update coveralls from 1.7.0 to 1.8.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 531d99940..effa714e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,5 @@ pytest==4.6.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 -coveralls==1.7.0 +coveralls==1.8.0 mypy==0.701 From 7134273918e927d010283a4626380c7cdcf532c5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:26 +0200 Subject: [PATCH 529/928] Update plotly from 3.9.0 to 3.10.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 23daee258..d4e4fc165 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.9.0 +plotly==3.10.0 From 2e6ded06a91d8b51930d25ec9b77380c115b15a2 Mon Sep 17 00:00:00 2001 From: Yuliyan Perfanov Date: Thu, 6 Jun 2019 18:25:58 +0300 Subject: [PATCH 530/928] removed redundant print() --- user_data/strategies/test_strategy.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 7edaead14..6b7770a8f 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -253,15 +253,16 @@ class TestStrategy(IStrategy): dataframe['ha_low'] = heikinashi['low'] """ - # Retrieve best bid and best ask + # Retrieve best bid and best ask # ------------------------------------ """ - ob = self.dp.orderbook(metadata['pair'], 1) - print(ob) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] + # first check if dataprovider is available + if self.dp: + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] """ - + return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From a9ed5da369d606da23223b16bf7fcc834bb13949 Mon Sep 17 00:00:00 2001 From: Yuliyan Perfanov Date: Thu, 6 Jun 2019 18:48:26 +0300 Subject: [PATCH 531/928] added doc for DataProvider.orderbook() --- docs/strategy-customization.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 51540f690..b44775dfb 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -298,6 +298,18 @@ if self.dp: !!! Warning Warning in hyperopt This option cannot currently be used during hyperopt. +#### Orderbook + +``` python +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + 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. + #### Available Pairs ``` python @@ -306,6 +318,7 @@ if self.dp: print(f"available {pair}, {ticker}") ``` + #### Get data for non-tradeable pairs Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. From f9fe2663644cd003224919540cc95bddbee8fc36 Mon Sep 17 00:00:00 2001 From: Yuliyan Perfanov Date: Thu, 6 Jun 2019 18:52:14 +0300 Subject: [PATCH 532/928] check for runmode before retrieving the orderbook --- user_data/strategies/test_strategy.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 6b7770a8f..2415e43eb 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -258,9 +258,10 @@ class TestStrategy(IStrategy): """ # first check if dataprovider is available if self.dp: - ob = self.dp.orderbook(metadata['pair'], 1) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] """ return dataframe From 5273540a93fd8ce33b998a2674024a0a1d133702 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jun 2019 19:32:31 +0200 Subject: [PATCH 533/928] Fix test failure (double-trailing newlines are removed now) --- freqtrade/tests/optimize/test_hyperopt.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index b41f8ac36..a51d74dbb 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -372,20 +372,21 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: ) patch_exchange(mocker) - default_conf.update({'config': 'config.json.example'}) - default_conf.update({'epochs': 1}) - default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) - default_conf.update({'hyperopt_jobs': 1}) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() parallel.assert_called_once() - - assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text + assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples) assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 def test_format_results(hyperopt): From adc12ed043320a9aee2884cdfad6aa5e86cb3069 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jun 2019 20:26:25 +0200 Subject: [PATCH 534/928] Fix new test after develop merge --- freqtrade/tests/test_persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 57f054dee..bb00fa8f4 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -780,7 +780,7 @@ def test_to_json(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee): - init(default_conf) + init(default_conf['db_url']) trade = Trade( pair='ETH/BTC', stake_amount=0.001, From d7c63347e12f41a99e3caaed498669f0230ea16e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 13:19:01 +0200 Subject: [PATCH 535/928] Use kwarg for parse_ticker_dataframe --- freqtrade/data/history.py | 2 +- freqtrade/tests/conftest.py | 4 ++-- freqtrade/tests/strategy/test_interface.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 2dacce8c6..72f7111f6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -106,7 +106,7 @@ def load_pair_history(pair: str, logger.warning('Missing data at end for pair %s, data ends at %s', pair, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) - return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing) + return parse_ticker_dataframe(pairdata, ticker_interval, fill_missing=fill_up_missing) else: logger.warning( f'No history data for pair: "{pair}", interval: {ticker_interval}. ' diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index a907b33ed..dcc69fcb1 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -649,7 +649,7 @@ def ticker_history_list(): @pytest.fixture def ticker_history(ticker_history_list): - return parse_ticker_dataframe(ticker_history_list, "5m", True) + return parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True) @pytest.fixture @@ -854,7 +854,7 @@ def tickers(): @pytest.fixture def result(): with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: - return parse_ticker_dataframe(json.load(data_file), '1m', True) + return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True) # FIX: # Create an fixture/function diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index d6ef0c8e7..e384003dc 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -111,7 +111,7 @@ def test_tickerdata_to_dataframe(default_conf) -> None: timerange = TimeRange(None, 'line', 0, -100) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)} data = strategy.tickerdata_to_dataframe(tickerlist) assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed From 9c497bf15c597921bb1c5509236f50414bbc4df4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:04:19 +0200 Subject: [PATCH 536/928] Improve docstring for deep_merge_dicts --- freqtrade/misc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9d37214e4..460e20e91 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -117,6 +117,8 @@ def format_ms_time(date: int) -> str: def deep_merge_dicts(source, destination): """ + Values from Source override destination, destination is returned (and modified!!) + Sample: >>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } } >>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } } >>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } } From 7108a2e57d557b4fc2580ec0defb2693c7e96164 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:05:36 +0200 Subject: [PATCH 537/928] Add deep_merge for _ft_has and test --- freqtrade/exchange/exchange.py | 31 +++++++++++++++-------- freqtrade/tests/exchange/test_exchange.py | 27 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 72a0efb1f..07a5e9037 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2,23 +2,24 @@ """ Cryptocurrency Exchanges support """ -import logging +import asyncio import inspect -from random import randint -from typing import List, Dict, Tuple, Any, Optional +import logging +from copy import deepcopy from datetime import datetime -from math import floor, ceil +from math import ceil, floor +from random import randint +from typing import Any, Dict, List, Optional, Tuple import arrow -import asyncio import ccxt import ccxt.async_support as ccxt_async from pandas import DataFrame -from freqtrade import (constants, DependencyException, OperationalException, - TemporaryError, InvalidOrderException) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError, constants) from freqtrade.data.converter import parse_ticker_dataframe - +from freqtrade.misc import deep_merge_dicts logger = logging.getLogger(__name__) @@ -68,12 +69,13 @@ class Exchange(object): _params: Dict = {} # Dict to specify which options each exchange implements - # TODO: this should be merged with attributes from subclasses - # To avoid having to copy/paste this to all subclasses. - _ft_has: Dict = { + # This defines defaults, which can be selectively overridden by subclasses using _ft_has + # or by specifying them in the configuration. + _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], } + _ft_has: Dict = {} def __init__(self, config: dict) -> None: """ @@ -100,6 +102,13 @@ class Exchange(object): logger.info('Instance is running with dry_run enabled') exchange_config = config['exchange'] + + # Deep merge ft_has with default ft_has options + self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) + if exchange_config.get("_ft_has_params"): + self._ft_has = deep_merge_dicts(exchange_config.get("_ft_has_params"), + self._ft_has) + self._api: ccxt.Exchange = self._init_ccxt( exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) self._api_async: ccxt_async.Exchange = self._init_ccxt( diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index fda9c8241..f0dc96626 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1435,3 +1435,30 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): assert order['type'] == order_type assert order['price'] == 220 assert order['amount'] == 1 + + +def test_merge_ft_has_dict(default_conf, mocker): + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + ex = Exchange(default_conf) + assert ex._ft_has == Exchange._ft_has_default + + ex = Kraken(default_conf) + assert ex._ft_has == Exchange._ft_has_default + + # Binance defines different values + ex = Binance(default_conf) + assert ex._ft_has != Exchange._ft_has_default + assert ex._ft_has['stoploss_on_exchange'] + assert ex._ft_has['order_time_in_force'] == ['gtc', 'fok', 'ioc'] + + conf = copy.deepcopy(default_conf) + conf['exchange']['_ft_has_params'] = {"DeadBeef": 20, + "stoploss_on_exchange": False} + # Use settings from configuration (overriding stoploss_on_exchange) + ex = Binance(conf) + assert ex._ft_has != Exchange._ft_has_default + assert not ex._ft_has['stoploss_on_exchange'] + assert ex._ft_has['DeadBeef'] == 20 From 3fe5388d4cdce72a03f7420a5585eaf8edddbd21 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:13:03 +0200 Subject: [PATCH 538/928] Document _ft_has_params override --- docs/configuration.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index df116b3c2..bbadb87e8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -301,8 +301,24 @@ This configuration enables binance, as well as rate limiting to avoid bans from !!! Note Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. - We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. + We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. +#### Advanced FreqTrade Exchange configuration + +Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours. + +Available options are listed in the exchange-class as `_ft_has_default`. + +For example, to test the order type `FOK` with Kraken: + +```json +"exchange": { + "name": "kraken", + "_ft_has_params": {"order_time_in_force": ["gtc", "fok"]} +``` + +!!! Warning + Please make sure to fully understand the impacts of these settings before modifying them. ### What values can be used for fiat_display_currency? From fdbbefdddd8442f1439256b84ddb24f804e1490c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:35:58 +0200 Subject: [PATCH 539/928] Make drop_incomplete optional --- freqtrade/data/converter.py | 12 ++++++++---- freqtrade/exchange/exchange.py | 8 +++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 77a3447da..dc566070d 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,14 +10,16 @@ from pandas import DataFrame, to_datetime logger = logging.getLogger(__name__) -def parse_ticker_dataframe(ticker: list, ticker_interval: str, - fill_missing: bool = True) -> DataFrame: +def parse_ticker_dataframe(ticker: list, ticker_interval: str, *, + fill_missing: bool = True, + drop_incomplete: bool = True) -> DataFrame: """ Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe :param ticker: ticker list, as returned by exchange.async_get_candle_history :param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data :param fill_missing: fill up missing candles with 0 candles (see ohlcv_fill_up_missing_data for details) + :param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete :return: DataFrame """ logger.debug("Parsing tickerlist to dataframe") @@ -43,8 +45,10 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, 'close': 'last', 'volume': 'max', }) - frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle - logger.debug('Dropping last candle') + # eliminate partial candle + if drop_incomplete: + frame.drop(frame.tail(1).index, inplace=True) + logger.debug('Dropping last candle') if fill_missing: return ohlcv_fill_up_missing_data(frame, ticker_interval) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 07a5e9037..401a3571f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -74,6 +74,7 @@ class Exchange(object): _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "ohlcv_partial_candle": True, } _ft_has: Dict = {} @@ -108,7 +109,12 @@ class Exchange(object): if exchange_config.get("_ft_has_params"): self._ft_has = deep_merge_dicts(exchange_config.get("_ft_has_params"), self._ft_has) + logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) + # Assign this directly for easy access + self._drop_incomplete = self._ft_has['ohlcv_partial_candle'] + + # Initialize ccxt objects self._api: ccxt.Exchange = self._init_ccxt( exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) self._api_async: ccxt_async.Exchange = self._init_ccxt( @@ -575,7 +581,7 @@ class Exchange(object): self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( - ticks, ticker_interval, fill_missing=True) + ticks, ticker_interval, fill_missing=True, drop_incomplete=self._drop_incomplete) return tickers def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: From 6ad94684d501af503cf070edb714e21f54ce2886 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:36:08 +0200 Subject: [PATCH 540/928] Add WIP document of steps to test a new exchange --- docs/developer.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index e7f79bc1c..ccd5cdd8f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -81,6 +81,50 @@ Please also run `self._validate_whitelist(pairs)` and to check and remove pairs This is a simple method used by `VolumePairList` - however serves as a good example. It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider. +## Implement a new Exchange (WIP) + +!!! Note + This section is a Work in Progress and is not a complete guide on how to test a new exchange with FreqTrade. + +Most exchanges supported by CCXT should work out of the box. + +### Stoploss On Exchange + +Check if the new exchange supports Stoploss on Exchange orders through their API. + +Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselfs. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects. + +### Incomplete candles + +While fetching OHLCV data, we're may end up getting incomplete candles (Depending on the exchange). +To demonstrate this, we'll use daily candles (`"1d"`) to keep things simple. +We query the api (`ct.fetch_ohlcv()`) for the timeframe and look at the date of the last entry. If this entry changes or shows the date of a "incomplete" candle, then we should drop this since having incomplete candles is problematic because indicators assume that only complete candles are passed to them, and will generate a lot of false buy signals. By default, we're therefore removing the last candle assuming it's incomplete. + +To check how the new exchange behaves, you can use the following snippet: + +``` python +import ccxt +from datetime import datetime +from freqtrade.data.converter import parse_ticker_dataframe +ct = ccxt.binance() +timeframe = "1d" +raw = ct.fetch_ohlcv(pair, timeframe=timeframe) + +# convert to dataframe +df1 = parse_ticker_dataframe(raw, timeframe, drop_incomplete=False) + +print(df1["date"].tail(1)) +print(datetime.utcnow()) +``` + +``` output +19 2019-06-08 00:00:00+00:00 +2019-06-09 12:30:27.873327 +``` + +The output will show the last entry from the Exchange as well as the current UTC date. +If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above). + ## Creating a release This part of the documentation is aimed at maintainers, and shows how to create a release. From ce317b62f9b3d523ccaa9e8474f99be2c36966a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:40:45 +0200 Subject: [PATCH 541/928] Add docstrings to load_pair_history --- freqtrade/data/history.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 72f7111f6..67f942119 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -81,10 +81,20 @@ def load_pair_history(pair: str, timerange: TimeRange = TimeRange(None, None, 0, 0), refresh_pairs: bool = False, exchange: Optional[Exchange] = None, - fill_up_missing: bool = True + fill_up_missing: bool = True, + drop_incomplete: bool = True ) -> DataFrame: """ Loads cached ticker history for the given pair. + :param pair: Pair to load data for + :param ticker_interval: Ticker-interval (e.g. "5m") + :param datadir: Path to the data storage location. + :param timerange: Limit data to be loaded to this timerange + :param refresh_pairs: Refresh pairs from exchange. + (Note: Requires exchange to be passed as well.) + :param exchange: Exchange object (needed when using "refresh_pairs") + :param fill_up_missing: Fill missing values with "No action"-candles + :param drop_incomplete: Drop last candle assuming it may be incomplete. :return: DataFrame with ohlcv data """ @@ -106,7 +116,9 @@ def load_pair_history(pair: str, logger.warning('Missing data at end for pair %s, data ends at %s', pair, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) - return parse_ticker_dataframe(pairdata, ticker_interval, fill_missing=fill_up_missing) + return parse_ticker_dataframe(pairdata, ticker_interval, + fill_missing=fill_up_missing, + drop_incomplete=drop_incomplete) else: logger.warning( f'No history data for pair: "{pair}", interval: {ticker_interval}. ' From 3380543878f664089038357d7554d947cafcfd4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:51:58 +0200 Subject: [PATCH 542/928] Add test for drop_incomplete option --- freqtrade/tests/data/test_converter.py | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 4c8de575d..8a0761f1c 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -96,3 +96,50 @@ def test_ohlcv_fill_up_missing_data2(caplog): assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}", caplog.record_tuples) + + +def test_ohlcv_drop_incomplete(caplog): + ticker_interval = '1d' + ticks = [[ + 1559750400000, # 2019-06-04 + 8.794e-05, # open + 8.948e-05, # high + 8.794e-05, # low + 8.88e-05, # close + 2255, # volume (in quote currency) + ], + [ + 1559836800000, # 2019-06-05 + 8.88e-05, + 8.942e-05, + 8.88e-05, + 8.893e-05, + 9911, + ], + [ + 1559923200000, # 2019-06-06 + 8.891e-05, + 8.893e-05, + 8.875e-05, + 8.877e-05, + 2251 + ], + [ + 1560009600000, # 2019-06-07 + 8.877e-05, + 8.883e-05, + 8.895e-05, + 8.817e-05, + 123551 + ] + ] + caplog.set_level(logging.DEBUG) + data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False, drop_incomplete=False) + assert len(data) == 4 + assert not log_has("Dropping last candle", caplog.record_tuples) + + # Drop last candle + data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False, drop_incomplete=True) + assert len(data) == 3 + + assert log_has("Dropping last candle", caplog.record_tuples) From 9f2e0b11d19772cfbe165ad6f8e67099ab98c175 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 14:52:17 +0200 Subject: [PATCH 543/928] Parametrize ohlcv_candle_limit (per call) --- docs/configuration.md | 7 +++++-- freqtrade/exchange/exchange.py | 11 ++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index bbadb87e8..98953d73f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -309,12 +309,15 @@ Advanced options can be configured using the `_ft_has_params` setting, which wil Available options are listed in the exchange-class as `_ft_has_default`. -For example, to test the order type `FOK` with Kraken: +For example, to test the order type `FOK` with Kraken, and modify candle_limit to 200 (so you only get 200 candles per call): ```json "exchange": { "name": "kraken", - "_ft_has_params": {"order_time_in_force": ["gtc", "fok"]} + "_ft_has_params": { + "order_time_in_force": ["gtc", "fok"], + "ohlcv_candle_limit": 200 + } ``` !!! Warning diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 401a3571f..ea6996efb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -74,6 +74,7 @@ class Exchange(object): _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, } _ft_has: Dict = {} @@ -112,7 +113,8 @@ class Exchange(object): logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) # Assign this directly for easy access - self._drop_incomplete = self._ft_has['ohlcv_partial_candle'] + self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit'] + self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] # Initialize ccxt objects self._api: ccxt.Exchange = self._init_ccxt( @@ -521,10 +523,8 @@ class Exchange(object): async def _async_get_history(self, pair: str, ticker_interval: str, since_ms: int) -> List: - # Assume exchange returns 500 candles - _LIMIT = 500 - one_call = timeframe_to_msecs(ticker_interval) * _LIMIT + one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit logger.debug( "one_call: %s msecs (%s)", one_call, @@ -581,7 +581,8 @@ class Exchange(object): self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( - ticks, ticker_interval, fill_missing=True, drop_incomplete=self._drop_incomplete) + ticks, ticker_interval, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) return tickers def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: From 792390e8159a7f6f6e211f0a10146de4cc3741f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jun 2019 15:03:26 +0200 Subject: [PATCH 544/928] Add missing parameter for exchange-verify snippet --- docs/developer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/developer.md b/docs/developer.md index ccd5cdd8f..6ecb7f156 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -108,6 +108,7 @@ from datetime import datetime from freqtrade.data.converter import parse_ticker_dataframe ct = ccxt.binance() timeframe = "1d" +pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange! raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe From 90b0f1daa860fc27d2edb37ed3fa2eefddd5f0f9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 10 Jun 2019 02:08:54 +0300 Subject: [PATCH 545/928] minor optimize cleanup --- freqtrade/edge/__init__.py | 12 ++++-------- freqtrade/optimize/backtesting.py | 9 +++------ freqtrade/optimize/hyperopt.py | 5 +++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 3ddff4772..4bc8bb493 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -46,10 +46,6 @@ class Edge(): self.config = config self.exchange = exchange self.strategy = strategy - self.ticker_interval = self.strategy.ticker_interval - self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.advise_sell = self.strategy.advise_sell - self.advise_buy = self.strategy.advise_buy self.edge_config = self.config.get('edge', {}) self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs @@ -100,7 +96,7 @@ class Edge(): data = history.load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=pairs, - ticker_interval=self.ticker_interval, + ticker_interval=self.strategy.ticker_interval, refresh_pairs=self._refresh_pairs, exchange=self.exchange, timerange=self._timerange @@ -112,7 +108,7 @@ class Edge(): logger.critical("No data found. Edge is stopped ...") return False - preprocessed = self.tickerdata_to_dataframe(data) + preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe min_date, max_date = history.get_timeframe(preprocessed) @@ -130,8 +126,8 @@ class Edge(): pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - ticker_data = self.advise_sell( - self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + ticker_data = self.strategy.advise_sell( + self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 76c6556fa..47933668c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -72,18 +72,16 @@ class Backtesting(object): IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): - # Force one interval - self.ticker_interval = str(self.config.get('ticker_interval')) - self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append(StrategyResolver(stratconf).strategy) else: - # only one strategy + # No strategy list specified, only one strategy self.strategylist.append(StrategyResolver(self.config).strategy) - # Load one strategy + + # Load one (first) strategy self._set_strategy(self.strategylist[0]) def _set_strategy(self, strategy): @@ -94,7 +92,6 @@ class Backtesting(object): self.ticker_interval = self.config.get('ticker_interval') self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) - self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell # Set stoploss_on_exchange to false for backtesting, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d19d54031..28b9ce789 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -45,7 +45,6 @@ class Hyperopt(Backtesting): """ def __init__(self, config: Dict[str, Any]) -> None: super().__init__(config) - self.config = config self.custom_hyperopt = HyperOptResolver(self.config).hyperopt # set TARGET_TRADES to suit your number concurrent trades so its realistic @@ -296,7 +295,9 @@ class Hyperopt(Backtesting): self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore - dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) + preprocessed = self.strategy.tickerdata_to_dataframe(data) + + dump(preprocessed, TICKERDATA_PICKLE) # We don't need exchange instance anymore while running hyperopt self.exchange = None # type: ignore From 4dc3a0ca1d3d8900bc78cca3ea31a4ca3f2e32b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jun 2019 16:20:19 +0200 Subject: [PATCH 546/928] Small cleanup to reduce dict lookups during backtesting/hyperopt --- freqtrade/optimize/backtesting.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 47933668c..6cc78ad2b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -232,10 +232,9 @@ class Backtesting(object): def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, - partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]: + partial_ticker: List, trade_count_lock: Dict, + stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: - stake_amount = args['stake_amount'] - max_open_trades = args.get('max_open_trades', 0) trade = Trade( open_rate=buy_row.open, open_date=buy_row.date, @@ -251,8 +250,7 @@ class Backtesting(object): # Increase trade_count_lock for every iteration trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 - buy_signal = sell_row.buy - sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal, + sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: @@ -325,6 +323,7 @@ class Backtesting(object): :return: DataFrame """ processed = args['processed'] + stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) start_date = args['start_date'] @@ -375,7 +374,8 @@ class Backtesting(object): trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], - trade_count_lock, args) + trade_count_lock, stake_amount, + max_open_trades) if trade_entry: lock_pair_until[pair] = trade_entry.close_time From 5c5b0effc18275bbae44c65fe9f62a6d4dd11f3a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Jun 2019 15:19:05 +0000 Subject: [PATCH 547/928] Update ccxt from 1.18.615 to 1.18.667 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 9e854e4af..d24ac4336 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.615 +ccxt==1.18.667 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.14.1 From 9961c0e15b6ccab9edd1f202be314c387aaba2f2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Jun 2019 15:19:06 +0000 Subject: [PATCH 548/928] Update arrow from 0.14.1 to 0.14.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index d24ac4336..5100f3235 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.667 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 -arrow==0.14.1 +arrow==0.14.2 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From 1a41d4e6cd5f1cf74dc79300e2e8c1e471651c7a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Jun 2019 15:19:08 +0000 Subject: [PATCH 549/928] Update python-rapidjson from 0.7.1 to 0.7.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 5100f3235..8b44e9d28 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -23,7 +23,7 @@ filelock==3.0.12 py_find_1st==1.1.3 #Load ticker files 30% faster -python-rapidjson==0.7.1 +python-rapidjson==0.7.2 # Notify systemd sdnotify==0.3.2 From 6636f0c71bc4097b2d2afc3b1c96b8b9402b6b91 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Jun 2019 15:19:09 +0000 Subject: [PATCH 550/928] Update pytest from 4.6.1 to 4.6.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index effa714e9..315033847 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.6.1 +pytest==4.6.2 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From dc0326db2737ba1bc213bab7f76bbffcc922c810 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:09:30 +0300 Subject: [PATCH 551/928] fix handling --exchange --- freqtrade/arguments.py | 8 +------- scripts/download_backtest_data.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 711975fd0..0d4288ef3 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -443,7 +443,7 @@ class Arguments(object): help='Log to the file specified', dest='logfile', type=str, - metavar='FILE' + metavar='FILE', ) self.parser.add_argument( '-c', '--config', @@ -458,15 +458,12 @@ class Arguments(object): '-d', '--datadir', help='Path to backtest data.', dest='datadir', - default=None, - type=str, metavar='PATH', ) self.parser.add_argument( '--pairs-file', help='File containing a list of pairs to download.', dest='pairs_file', - default=None, metavar='FILE', ) self.parser.add_argument( @@ -475,14 +472,11 @@ class Arguments(object): dest='days', type=int, metavar='INT', - default=None ) self.parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', dest='exchange', - type=str, - default='bittrex' ) self.parser.add_argument( '-t', '--timeframes', diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 196af5bed..278879fb0 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -27,6 +27,9 @@ arguments.download_data_options() # in the command line options explicitely args = arguments.parse_args(no_default_config=True) +# Use bittrex as default exchange +exchange_name = args.exchange or 'bittrex' + timeframes = args.timeframes pairs: List = [] @@ -46,20 +49,15 @@ if args.config: config['exchange']['key'] = '' config['exchange']['secret'] = '' - if args.exchange: - config['exchange']['name'] = args.exchange - pairs = config['exchange']['pair_whitelist'] timeframes = [config['ticker_interval']] else: - if not args.exchange: - sys.exit("No exchange specified.") config = { 'stake_currency': '', 'dry_run': True, 'exchange': { - 'name': args.exchange, + 'name': exchange_name, 'key': '', 'secret': '', 'pair_whitelist': [], @@ -71,6 +69,13 @@ else: } configuration._load_logging_config(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 +configuration.check_exchange(config) + configuration._load_datadir_config(config) dl_path = Path(config['datadir']) From cd60d6d99adc61ca030f418d3a60de9bfbd43fd1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:10:21 +0300 Subject: [PATCH 552/928] make --days positive int only --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 0d4288ef3..09fea5e63 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -470,7 +470,7 @@ class Arguments(object): '--days', help='Download data for given number of days.', dest='days', - type=int, + type=Arguments.check_int_positive, metavar='INT', ) self.parser.add_argument( From d55f2be942b5f9a58cd085e815c5a11311f802c0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:21:59 +0300 Subject: [PATCH 553/928] make flake happy --- scripts/download_backtest_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 278879fb0..76b2c415b 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -71,7 +71,8 @@ else: configuration._load_logging_config(config) if args.config and args.exchange: - logger.warning("The --exchange option is ignored, using exchange settings from the configuration file.") + logger.warning("The --exchange option is ignored, " + "using exchange settings from the configuration file.") # Check if the exchange set by the user is supported configuration.check_exchange(config) From 4801af4c77afbb10a8ec756e6c22955521b08a17 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:42:14 +0300 Subject: [PATCH 554/928] debug logging for IStrategy.advise_*() added --- freqtrade/strategy/interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index db266d95f..4e2d96c55 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -378,6 +378,7 @@ class IStrategy(ABC): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ + logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -393,6 +394,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with buy column """ + logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -408,6 +410,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with sell column """ + logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.") if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) From 7322a34fa426d0eb42f7ad5711b997ad356c19fa Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 10:58:19 +0300 Subject: [PATCH 555/928] fix metadata in tests --- freqtrade/tests/strategy/test_strategy.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 2ed2567f9..b96f9c79e 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -63,15 +63,14 @@ def test_search_strategy(): def test_load_strategy(result): resolver = StrategyResolver({'strategy': 'TestStrategy'}) - metadata = {'pair': 'ETH/BTC'} - assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata) + assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_byte64(result): with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) - assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC') + assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_invalid_directory(result, caplog): @@ -371,7 +370,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC') + indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -380,7 +379,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_buy(indicators, 'ETH/BTC') + resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -389,7 +388,7 @@ def test_deprecate_populate_indicators(result): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_sell(indicators, 'ETH_BTC') + resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ From 676e7300132e15aa55a8cdb844e2f3b582140184 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:18:35 +0300 Subject: [PATCH 556/928] enhance check_exchange --- freqtrade/configuration.py | 41 +++++++++++++++++++++------ freqtrade/exchange/__init__.py | 6 ++-- freqtrade/exchange/exchange.py | 18 ++++++++---- freqtrade/tests/test_configuration.py | 2 +- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index c19580c36..90393cae2 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -13,7 +13,8 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants -from freqtrade.exchange import is_exchange_supported, supported_exchanges +from freqtrade.exchange import (is_exchange_bad, is_exchange_known, + is_exchange_officially_supported, known_exchanges) from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -375,22 +376,44 @@ class Configuration(object): return self.config - def check_exchange(self, config: Dict[str, Any]) -> bool: + def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool: """ Check if the exchange name in the config file is supported by Freqtrade - :return: True or raised an exception if the exchange if not supported + :param check_for_bad: if True, check the exchange against the list of known 'bad' + exchanges + :return: False if exchange is 'bad', i.e. is known to work with the bot with + critical issues or does not work at all, crashes, etc. True otherwise. + raises an exception if the exchange if not supported by ccxt + and thus is not known for the Freqtrade at all. """ - exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_supported(exchange): + logger.info("Checking exchange...") - exception_msg = f'Exchange "{exchange}" not supported.\n' \ - f'The following exchanges are supported: ' \ - f'{", ".join(supported_exchanges())}' + exchange = config.get('exchange', {}).get('name').lower() + if not is_exchange_known(exchange): + exception_msg = f'Exchange "{exchange}" is not supported by ccxt ' \ + f'and not known for the bot.\n' \ + f'The following exchanges are supported by ccxt: ' \ + f'{", ".join(known_exchanges())}' logger.critical(exception_msg) raise OperationalException( exception_msg ) - logger.debug('Exchange "%s" supported', exchange) + logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') + + if is_exchange_officially_supported(exchange): + logger.info(f'Exchange "{exchange}" is officially supported ' + f'by the Freqtrade development team.') + else: + logger.warning(f'Exchange "{exchange}" is not officially supported ' + f'by the Freqtrade development team. ' + f'It may work with serious issues or not work at all. ' + f'Use it at your own discretion.') + + if check_for_bad and is_exchange_bad(exchange): + logger.warning(f'Exchange "{exchange}" is known to not work with Freqtrade yet. ' + f'Use it only for development and testing purposes.') + return False + return True diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 3c90e69ee..29e50e29c 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,8 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 -from freqtrade.exchange.exchange import (is_exchange_supported, # noqa: F401 - supported_exchanges) +from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 + is_exchange_known, + is_exchange_officially_supported, + known_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, timeframe_to_msecs) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6996efb..1196f5efe 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,8 +156,8 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if not is_exchange_supported(name, ccxt_module): - raise OperationalException(f'Exchange {name} is not supported') +# if not is_exchange_supported(name, ccxt_module): +# raise OperationalException(f'Exchange {name} is not supported') ex_config = { 'apiKey': exchange_config.get('key'), @@ -722,11 +722,19 @@ class Exchange(object): raise OperationalException(e) -def is_exchange_supported(exchange: str, ccxt_module=None) -> bool: - return exchange in supported_exchanges(ccxt_module) +def is_exchange_bad(exchange: str) -> bool: + return exchange in ['bitmex'] -def supported_exchanges(ccxt_module=None) -> List[str]: +def is_exchange_known(exchange: str, ccxt_module=None) -> bool: + return exchange in known_exchanges(ccxt_module) + + +def is_exchange_officially_supported(exchange: str) -> bool: + return exchange in ['bittrex', 'binance'] + + +def known_exchanges(ccxt_module=None) -> List[str]: return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index aee0dfadd..03f0c004c 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -484,7 +484,7 @@ def test_check_exchange(default_conf, caplog) -> None: with pytest.raises( OperationalException, - match=r'.*Exchange "unknown_exchange" not supported.*' + match=r'.*Exchange "unknown_exchange" is not supported by ccxt and not known for the bot.*' ): configuration.check_exchange(default_conf) From db6ccef6bdad38d3438bc08554ee91d500a514c1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:43:29 +0300 Subject: [PATCH 557/928] return back check in init_ccxt() --- 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 1196f5efe..0d4ed364d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,8 +156,8 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] -# if not is_exchange_supported(name, ccxt_module): -# raise OperationalException(f'Exchange {name} is not supported') + if not is_exchange_known(name, ccxt_module): + raise OperationalException(f'Exchange {name} is not supported by ccxt') ex_config = { 'apiKey': exchange_config.get('key'), From dc7f8837518e280ee950b811c9d2ee8e7826d810 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 11 Jun 2019 13:47:04 +0300 Subject: [PATCH 558/928] no need to duplicate this long error message --- freqtrade/configuration.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 90393cae2..bd4717894 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -390,14 +390,11 @@ class Configuration(object): exchange = config.get('exchange', {}).get('name').lower() if not is_exchange_known(exchange): - exception_msg = f'Exchange "{exchange}" is not supported by ccxt ' \ - f'and not known for the bot.\n' \ - f'The following exchanges are supported by ccxt: ' \ - f'{", ".join(known_exchanges())}' - - logger.critical(exception_msg) raise OperationalException( - exception_msg + f'Exchange "{exchange}" is not supported by ccxt ' + f'and not known for the bot.\n' + f'The following exchanges are supported by ccxt: ' + f'{", ".join(known_exchanges())}' ) logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') From 9c64965808d02fc8e100a48e76f34022858f9a9e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 12:33:20 +0300 Subject: [PATCH 559/928] list-exchanges subcommand added --- freqtrade/arguments.py | 18 +++++++++++++++++ freqtrade/utils.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 freqtrade/utils.py diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 89b587c6f..101d927bf 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -335,12 +335,25 @@ class Arguments(object): metavar='INT', ) + @staticmethod + def list_exchanges_options(parser: argparse.ArgumentParser) -> None: + """ + Parses given arguments for the list-exchanges command. + """ + parser.add_argument( + '-1', + help='Print exchanges in one column', + action='store_true', + dest='print_one_column', + ) + def _build_subcommands(self) -> None: """ Builds and attaches all subcommands :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge + from freqtrade.utils import start_list_exchanges subparsers = self.parser.add_subparsers(dest='subparser') @@ -362,6 +375,11 @@ class Arguments(object): self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) + # Add list-exchanges subcommand + list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='List known exchanges.') + list_exchanges_cmd.set_defaults(func=start_list_exchanges) + self.list_exchanges_options(list_exchanges_cmd) + @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: """ diff --git a/freqtrade/utils.py b/freqtrade/utils.py new file mode 100644 index 000000000..944287001 --- /dev/null +++ b/freqtrade/utils.py @@ -0,0 +1,44 @@ +import logging +from argparse import Namespace +from typing import Any, Dict + +from freqtrade.configuration import Configuration +from freqtrade.exchange import supported_exchanges +from freqtrade.state import RunMode + + +logger = logging.getLogger(__name__) + + +def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: + """ + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration + """ + configuration = Configuration(args, method) + config = configuration.load_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + return config + + +def start_list_exchanges(args: Namespace) -> None: + """ + Start listing known exchanges + :param args: Cli args from Arguments() + :return: None + """ + + # Initialize configuration + config = setup_configuration(args, RunMode.OTHER) + + logger.debug('Starting freqtrade in cli-util mode') + + if args.print_one_column: + print('\n'.join(supported_exchanges())) + else: + print(f"Supported exchanges: {', '.join(supported_exchanges())}") From 8df40a6ff945fbf679ad3484bc8aec93b46d271b Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 12 Jun 2019 22:40:50 +0300 Subject: [PATCH 560/928] make flake happy --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 944287001..394507059 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -34,7 +34,7 @@ def start_list_exchanges(args: Namespace) -> None: """ # Initialize configuration - config = setup_configuration(args, RunMode.OTHER) + _ = setup_configuration(args, RunMode.OTHER) logger.debug('Starting freqtrade in cli-util mode') From 0cc2210f222d54127ba58b7fdf7173b32fe0746d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 22:37:43 +0300 Subject: [PATCH 561/928] wording fixed --- freqtrade/configuration.py | 20 ++++++++++---------- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/exchange.py | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index bd4717894..b85345b45 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -13,8 +13,8 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants -from freqtrade.exchange import (is_exchange_bad, is_exchange_known, - is_exchange_officially_supported, known_exchanges) +from freqtrade.exchange import (is_exchange_bad, is_exchange_available, + is_exchange_officially_supported, available_exchanges) from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -389,27 +389,27 @@ class Configuration(object): logger.info("Checking exchange...") exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_known(exchange): + if not is_exchange_available(exchange): raise OperationalException( f'Exchange "{exchange}" is not supported by ccxt ' - f'and not known for the bot.\n' + f'and therefore not available for the bot.\n' f'The following exchanges are supported by ccxt: ' - f'{", ".join(known_exchanges())}' + f'{", ".join(available_exchanges())}' ) - logger.info(f'Exchange "{exchange}" is supported by ccxt and known for the bot.') + logger.info(f'Exchange "{exchange}" is supported by ccxt ' + f'and therefore available for the bot.') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' f'by the Freqtrade development team.') else: - logger.warning(f'Exchange "{exchange}" is not officially supported ' - f'by the Freqtrade development team. ' - f'It may work with serious issues or not work at all. ' + logger.warning(f'Exchange "{exchange}" is not officially supported. ' + f'It may work flawlessly (please report back) or have serious issues. ' f'Use it at your own discretion.') if check_for_bad and is_exchange_bad(exchange): - logger.warning(f'Exchange "{exchange}" is known to not work with Freqtrade yet. ' + logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' f'Use it only for development and testing purposes.') return False diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 29e50e29c..5c58320f6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,8 +1,8 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 - is_exchange_known, + is_exchange_available, is_exchange_officially_supported, - known_exchanges) + available_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, timeframe_to_msecs) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0d4ed364d..194e1d883 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -156,7 +156,7 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if not is_exchange_known(name, ccxt_module): + if not is_exchange_available(name, ccxt_module): raise OperationalException(f'Exchange {name} is not supported by ccxt') ex_config = { @@ -726,15 +726,15 @@ def is_exchange_bad(exchange: str) -> bool: return exchange in ['bitmex'] -def is_exchange_known(exchange: str, ccxt_module=None) -> bool: - return exchange in known_exchanges(ccxt_module) +def is_exchange_available(exchange: str, ccxt_module=None) -> bool: + return exchange in available_exchanges(ccxt_module) def is_exchange_officially_supported(exchange: str) -> bool: return exchange in ['bittrex', 'binance'] -def known_exchanges(ccxt_module=None) -> List[str]: +def available_exchanges(ccxt_module=None) -> List[str]: return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges From a65c89f09074c7c2eba664afe6335f3186788b03 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 12 Jun 2019 23:03:16 +0300 Subject: [PATCH 562/928] test adjusted --- freqtrade/tests/test_configuration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 03f0c004c..c8beb3512 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -484,7 +484,8 @@ def test_check_exchange(default_conf, caplog) -> None: with pytest.raises( OperationalException, - match=r'.*Exchange "unknown_exchange" is not supported by ccxt and not known for the bot.*' + match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' + r'and therefore not available for the bot.*' ): configuration.check_exchange(default_conf) From a4d8424268f0d2ed8fdb03d6cc3f16b51b888215 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 19:34:46 +0200 Subject: [PATCH 563/928] trailing_stop_positive should only be set when needed, and none/undefined otherwise --- user_data/strategies/test_strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 66a5f8c09..1dd6406b4 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -44,8 +44,8 @@ class TestStrategy(IStrategy): # trailing stoploss trailing_stop = False - trailing_stop_positive = 0.01 - trailing_stop_positive_offset = 0.0 # Disabled / not configured + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured # Optimal ticker interval for the strategy ticker_interval = '5m' From b64b6a2583f55f1f1d139a45a59f70bc633c4c61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 19:35:20 +0200 Subject: [PATCH 564/928] Support trailing_stop_positive options in BTContainer --- freqtrade/tests/optimize/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 457113cb7..41500051f 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -29,6 +29,10 @@ class BTContainer(NamedTuple): trades: List[BTrade] profit_perc: float trailing_stop: bool = False + trailing_only_offset_is_reached: bool = False + trailing_stop_positive: float = None + trailing_stop_positive_offset: float = 0.0 + use_sell_signal: bool = False def _get_frame_time_from_offset(offset): From 578180f45b986aaa5dade8425102aee0baa59dbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:00:56 +0200 Subject: [PATCH 565/928] Add test for sell-signal sell --- .../tests/optimize/test_backtest_detail.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 32c6bd09b..35f705e16 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,6 +14,21 @@ from freqtrade.tests.optimize import (BTContainer, BTrade, _get_frame_time_from_offset, tests_ticker_interval) +# Test 0 Sell signal sell +# Test with Stop-loss at 1% +# TC0: Sell signal in candle 3 +tc0 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], # exit with stoploss hit + [3, 5010, 5000, 4980, 5010, 6172, 0, 1], + [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi=1, profit_perc=0.002, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + # Test 1 Minus 8% Close # Test with Stop-loss at 1% # TC1: Stop-Loss Triggered 1% loss @@ -146,7 +161,7 @@ tc8 = BTContainer(data=[ # Test 9 - trailing_stop should raise - high and low in same candle. # Candle Data for test 9 # Set stop-loss at 10%, ROI at 10% (should not apply) -# TC9: Trailing stoploss - stoploss should be adjusted candle 2 +# TC9: Trailing stoploss - stoploss should be adjusted candle 3 tc9 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -159,6 +174,7 @@ tc9 = BTContainer(data=[ ) TESTS = [ + tc0, tc1, tc2, tc3, @@ -180,6 +196,13 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: default_conf["minimal_roi"] = {"0": data.roi} default_conf["ticker_interval"] = tests_ticker_interval default_conf["trailing_stop"] = data.trailing_stop + default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached + # Only add this to configuration If it's necessary + if data.trailing_stop_positive: + default_conf["trailing_stop_positive"] = data.trailing_stop_positive + default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset + default_conf["experimental"] = {"use_sell_signal": data.use_sell_signal} + mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) From 160894c0312cf5c778a3d04b8968f62b28aecd4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:04:52 +0200 Subject: [PATCH 566/928] Calculate profit_high to make sure stoploss_positive_offset is correct --- freqtrade/strategy/interface.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index db266d95f..8570c354f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -308,14 +308,16 @@ class IStrategy(ABC): if trailing_stop: # trailing stoploss handling - sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) + # Make sure current_profit is calculated using high for backtesting. + high_profit = current_profit if not high else trade.calc_profit_percent(high) + # Don't update stoploss if trailing_only_offset_is_reached is true. - if not (tsl_only_offset and current_profit < sl_offset): + if not (tsl_only_offset and high_profit < sl_offset): # Specific handling for trailing_stop_positive - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + if 'trailing_stop_positive' in self.config and high_profit > sl_offset: # Ignore mypy error check in configuration that this is a float stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore logger.debug(f"using positive stop loss: {stop_loss_value} " From 550fbad53e7fc64074744945fea988273c0ebad8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:05:49 +0200 Subject: [PATCH 567/928] Add test-cases with trailing_stop_offsets --- .../tests/optimize/test_backtest_detail.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 35f705e16..402e22391 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -173,6 +173,57 @@ tc9 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 10 - trailing_stop should raise so candle 3 causes a stoploss +# without applying trailing_stop_positive since stoploss_offset is at 10%. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC10: Trailing stoploss - stoploss should be adjusted candle 2 +tc10 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.1, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] +) + +# Test 11 - trailing_stop should raise so candle 3 causes a stoploss +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC11: Trailing stoploss - stoploss should be adjusted candle 2, +tc11 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + +# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC12: Trailing stoploss - stoploss should be adjusted candle 2, +tc12 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] +) + TESTS = [ tc0, tc1, @@ -184,6 +235,9 @@ TESTS = [ tc7, tc8, tc9, + tc10, + tc11, + tc12, ] From e08fda074ad2ea504b830c8760ad1a5e198e9b0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:26:47 +0200 Subject: [PATCH 568/928] Fix bug with timeframe handling --- scripts/download_backtest_data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 76b2c415b..8493c0872 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -30,7 +30,6 @@ args = arguments.parse_args(no_default_config=True) # Use bittrex as default exchange exchange_name = args.exchange or 'bittrex' -timeframes = args.timeframes pairs: List = [] configuration = Configuration(args) @@ -50,7 +49,9 @@ if args.config: config['exchange']['secret'] = '' pairs = config['exchange']['pair_whitelist'] - timeframes = [config['ticker_interval']] + + # Don't fail if ticker_interval is not in the configuration + timeframes = [config.get('ticker_interval')] else: config = { @@ -68,6 +69,8 @@ else: } } +timeframes = args.timeframes + configuration._load_logging_config(config) if args.config and args.exchange: From 9657b1a17f454fcd81f66f91c2c8949c7495acfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jun 2019 20:37:17 +0200 Subject: [PATCH 569/928] explict parse to string for ticker-interval --- scripts/download_backtest_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 8493c0872..a39500d0e 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -127,7 +127,7 @@ try: logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=ticker_interval, + pair=pair, ticker_interval=str(ticker_interval), timerange=timerange) except KeyboardInterrupt: From 04ea66c97722901caeae519fd0b38feb886bae1e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 02:58:34 +0300 Subject: [PATCH 570/928] fix handling timeframes --- freqtrade/arguments.py | 1 - scripts/download_backtest_data.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 09fea5e63..46fb9e7b3 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -484,7 +484,6 @@ class Arguments(object): Default: %(default)s.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], - default=['1m', '5m'], nargs='+', dest='timeframes', ) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index a39500d0e..6263d0e2f 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -50,8 +50,10 @@ if args.config: pairs = config['exchange']['pair_whitelist'] - # Don't fail if ticker_interval is not in the configuration - timeframes = [config.get('ticker_interval')] + if config.get('ticker_interval'): + timeframes = args.timeframes or [config.get('ticker_interval')] + else: + timeframes = args.timeframes or ['1m', '5m'] else: config = { @@ -68,8 +70,7 @@ else: } } } - -timeframes = args.timeframes + timeframes = args.timeframes or ['1m', '5m'] configuration._load_logging_config(config) From ee113ab8edb29269221fd0a10830c1a5339cafde Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 18:40:02 +0300 Subject: [PATCH 571/928] log messages aligned --- freqtrade/configuration.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index b85345b45..28faacf90 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -397,20 +397,19 @@ class Configuration(object): f'{", ".join(available_exchanges())}' ) - logger.info(f'Exchange "{exchange}" is supported by ccxt ' - f'and therefore available for the bot.') - - if is_exchange_officially_supported(exchange): - logger.info(f'Exchange "{exchange}" is officially supported ' - f'by the Freqtrade development team.') - else: - logger.warning(f'Exchange "{exchange}" is not officially supported. ' - f'It may work flawlessly (please report back) or have serious issues. ' - f'Use it at your own discretion.') - if check_for_bad and is_exchange_bad(exchange): logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' f'Use it only for development and testing purposes.') return False + if is_exchange_officially_supported(exchange): + logger.info(f'Exchange "{exchange}" is officially supported ' + f'by the Freqtrade development team.') + else: + logger.warning(f'Exchange "{exchange}" is supported by ccxt ' + f'and therefore available for the bot but not officially supported ' + f'by the Freqtrade development team. ' + f'It may work flawlessly (please report back) or have serious issues. ' + f'Use it at your own discretion.') + return True From 941fb4ebbb3c7f3d53bc3741e20d086b7545b072 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 18:40:25 +0300 Subject: [PATCH 572/928] tests added --- freqtrade/tests/test_configuration.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c8beb3512..82167125c 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -470,15 +470,27 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None: configuration = Configuration(Namespace()) - # Test a valid exchange + # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) assert configuration.check_exchange(default_conf) - # Test a valid exchange + # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert configuration.check_exchange(default_conf) - # Test a invalid exchange + # Test an available exchange, supported by ccxt + default_conf.get('exchange').update({'name': 'kraken'}) + assert configuration.check_exchange(default_conf) + + # Test a 'bad' exchange, which known to have serious problems + default_conf.get('exchange').update({'name': 'bitmex'}) + assert not configuration.check_exchange(default_conf) + + # Test a 'bad' exchange with check_for_bad=False + default_conf.get('exchange').update({'name': 'bitmex'}) + assert configuration.check_exchange(default_conf, False) + + # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) configuration.config = default_conf From 1afe6c1437c7e7704834f55badf1b40fa0408a4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Jun 2019 19:37:54 +0200 Subject: [PATCH 573/928] Don't run validation per strategy, it's only eneded once --- freqtrade/optimize/backtesting.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cc78ad2b..b9d874fc0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -421,20 +421,21 @@ class Backtesting(object): max_open_trades = 0 all_results = {} + min_date, max_date = history.get_timeframe(data) + # Validate dataframe for missing values (mainly at start and end, as fillup is called) + history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes(self.ticker_interval)) + logger.info( + 'Backtesting with data from %s up to %s (%s days)..', + min_date.isoformat(), + max_date.isoformat(), + (max_date - min_date).days + ) + for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) - min_date, max_date = history.get_timeframe(data) - # Validate dataframe for missing values (mainly at start and end, as fillup is called) - history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) - logger.info( - 'Backtesting with data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days - ) # need to reprocess data every time to populate signals preprocessed = self.strategy.tickerdata_to_dataframe(data) From cedd38455f0e2da85a25cbae32b6bf11e02a9490 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 21:54:38 +0300 Subject: [PATCH 574/928] remove configuration from list-exchanges --- freqtrade/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 394507059..45bd9fd3f 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -28,17 +28,13 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: def start_list_exchanges(args: Namespace) -> None: """ - Start listing known exchanges + Print available exchanges :param args: Cli args from Arguments() :return: None """ - # Initialize configuration - _ = setup_configuration(args, RunMode.OTHER) - - logger.debug('Starting freqtrade in cli-util mode') - if args.print_one_column: print('\n'.join(supported_exchanges())) else: - print(f"Supported exchanges: {', '.join(supported_exchanges())}") + print(f"Exchanges supported by ccxt and available for Freqtrade: " + f"{', '.join(supported_exchanges())}") From 1af988711b9020d78866c6bbdcb065fe58eeeaf2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 21:59:16 +0300 Subject: [PATCH 575/928] add --one-column as an alias option --- freqtrade/arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 101d927bf..a8e8af2ad 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -341,7 +341,7 @@ class Arguments(object): Parses given arguments for the list-exchanges command. """ parser.add_argument( - '-1', + '-1', '--one-column', help='Print exchanges in one column', action='store_true', dest='print_one_column', @@ -376,7 +376,7 @@ class Arguments(object): self.hyperopt_options(hyperopt_cmd) # Add list-exchanges subcommand - list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='List known exchanges.') + list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='Print available exchanges.') list_exchanges_cmd.set_defaults(func=start_list_exchanges) self.list_exchanges_options(list_exchanges_cmd) From 09cd7db9b17f251212d9db7dc53660089b08b933 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 14 Jun 2019 22:04:29 +0300 Subject: [PATCH 576/928] make flake happy --- freqtrade/arguments.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index a8e8af2ad..4f512e40e 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -376,7 +376,10 @@ class Arguments(object): self.hyperopt_options(hyperopt_cmd) # Add list-exchanges subcommand - list_exchanges_cmd = subparsers.add_parser('list-exchanges', help='Print available exchanges.') + list_exchanges_cmd = subparsers.add_parser( + 'list-exchanges', + help='Print available exchanges.' + ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) self.list_exchanges_options(list_exchanges_cmd) From ad9dc349e46fc0cc6438939f944cedc12aef07cc Mon Sep 17 00:00:00 2001 From: Misagh Date: Sat, 15 Jun 2019 12:20:32 +0200 Subject: [PATCH 577/928] edge cli should override stake_amount --- freqtrade/optimize/edge_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 8232c79c9..231493e4d 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -6,6 +6,7 @@ This module contains the edge backtesting interface import logging from typing import Dict, Any from tabulate import tabulate +from freqtrade import constants from freqtrade.edge import Edge from freqtrade.arguments import Arguments @@ -32,6 +33,7 @@ class EdgeCli(object): self.config['exchange']['secret'] = '' self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' + self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['dry_run'] = True self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy From 707118a63684290287fbfc53c6dcfd60e7e7f5e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:04:15 +0200 Subject: [PATCH 578/928] Test stake changed to unlimited --- freqtrade/tests/optimize/test_edge_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 5d16b0f2d..49d3cdd55 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -117,8 +117,10 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: def test_edge_init(mocker, edge_conf) -> None: patch_exchange(mocker) + edge_conf['stake_amount'] = 20 edge_cli = EdgeCli(edge_conf) assert edge_cli.config == edge_conf + assert edge_cli.config['stake_amount'] == 'unlimited' assert callable(edge_cli.edge.calculate) From a77d75eb431c29123b537b7dc1056bca1809560c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:14:07 +0200 Subject: [PATCH 579/928] Check log output since that's whats shown to users --- freqtrade/tests/test_configuration.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 82167125c..38f17fbea 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -15,7 +15,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, log_has_re @pytest.fixture(scope="function") @@ -473,22 +473,40 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog.record_tuples) + caplog.clear() # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog.record_tuples) + caplog.clear() # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'kraken'}) assert configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " + r"by the Freqtrade development team\. .*", + caplog.record_tuples) + caplog.clear() # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) assert not configuration.check_exchange(default_conf) + assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " + r"Use it only for development and testing purposes\.", + caplog.record_tuples) + caplog.clear() # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) assert configuration.check_exchange(default_conf, False) + assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " + r"by the Freqtrade development team\. .*", + caplog.record_tuples) + caplog.clear() # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) From 36dd061be759167f1b376fbf94ec6bd93e77cfd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:19:18 +0200 Subject: [PATCH 580/928] Update slack link since the old one expired --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/index.md | 2 +- docs/strategy-customization.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c511f44d..e15059f56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Few pointers for contributions: - Create your PR against the `develop` branch, not `master`. - New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100). -If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) +If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index 98dad1d2e..240b4f917 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). +- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -172,7 +172,7 @@ to understand the requirements before sending your pull-requests. Coding is not a neccessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `master`. diff --git a/docs/developer.md b/docs/developer.md index 6ecb7f156..7f3dc76f6 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) where you can ask questions. ## Documentation diff --git a/docs/index.md b/docs/index.md index 9fbc0519c..63d6be75e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ To run this bot we recommend you a cloud instance with a minimum of: Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. +Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) to join Slack channel. ## Ready to try? diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fd9760bda..57c646aed 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -410,7 +410,7 @@ To get additional Ideas for strategies, head over to our [strategy repository](h Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new Strategies to that repo. -We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas. +We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) which is a great place to get and/or share ideas. ## Next step From 01b5ece64247cc906e18023fba285824fc82ca6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:31:14 +0200 Subject: [PATCH 581/928] Log missing data filllup if necessary --- freqtrade/data/converter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index dc566070d..d1b8277ea 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -84,7 +84,10 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da 'low': df['close'], }) df.reset_index(inplace=True) - logger.debug(f"Missing data fillup: before: {len(dataframe)} - after: {len(df)}") + len_before = len(dataframe) + len_after = len(df) + if len_before != len_after: + logger.info(f"Missing data fillup: before: {len_before} - after: {len_after}") return df From cd4cf215e1f83ae52d8c02ddfbb0985dc9a9bba7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:31:27 +0200 Subject: [PATCH 582/928] Convert validate_backtest_data to take dataframe directly --- freqtrade/data/history.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 67f942119..83ea84284 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -286,12 +286,13 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] max(timeframe, key=operator.itemgetter(1))[1] -def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, +def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, max_date: datetime, ticker_interval_mins: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. - :param data: dictionary with preprocessed backtesting data + :param data: preprocessed backtesting data (as DataFrame) + :param pair: pair used for log output. :param min_date: start-date of the data :param max_date: end-date of the data :param ticker_interval_mins: ticker interval in minutes @@ -299,10 +300,9 @@ def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, # total difference in minutes / interval-minutes expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) found_missing = False - for pair, df in data.items(): - dflen = len(df) - if dflen < expected_frames: - found_missing = True - logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", - pair, expected_frames, dflen, expected_frames - dflen) + dflen = len(data) + if dflen < expected_frames: + found_missing = True + logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", + pair, expected_frames, dflen, expected_frames - dflen) return found_missing From d047a9d8361d5ce1a93d9b2179dd57876c4b106f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:32:05 +0200 Subject: [PATCH 583/928] Adapt tests for new validate_backtest signature --- freqtrade/tests/data/test_converter.py | 4 ++-- freqtrade/tests/data/test_history.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 8a0761f1c..032f32390 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -37,8 +37,8 @@ def test_ohlcv_fill_up_missing_data(caplog): # Test fillup actually fixes invalid backtest data min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) - assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1) - assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1) + assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1) + assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1) def test_ohlcv_fill_up_missing_data2(caplog): diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index a13bc34af..46bcf06c4 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -555,8 +555,8 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: ) min_date, max_date = history.get_timeframe(data) caplog.clear() - assert history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) + assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -579,6 +579,6 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: min_date, max_date = history.get_timeframe(data) caplog.clear() - assert not history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) + assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 From 55079831a13af3d95913343f025bdde2b1e7ecca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:45:50 +0200 Subject: [PATCH 584/928] Don't explicitly validate backtest data (it's done while loading now). --- freqtrade/optimize/backtesting.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b9d874fc0..bb72cbada 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -350,7 +350,7 @@ class Backtesting(object): row = ticker[pair][indexes[pair]] except IndexError: # missing Data for one pair at the end. - # Warnings for this are shown by `validate_backtest_data` + # Warnings for this are shown during data loading continue # Waits until the time-counter reaches the start of the data for this pair. @@ -422,9 +422,7 @@ class Backtesting(object): all_results = {} min_date, max_date = history.get_timeframe(data) - # Validate dataframe for missing values (mainly at start and end, as fillup is called) - history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) + logger.info( 'Backtesting with data from %s up to %s (%s days)..', min_date.isoformat(), From 89ff614e1d7e5deb04347849ba5b2f3060b180de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:46:19 +0200 Subject: [PATCH 585/928] Add pair as parameter, and warn when fillup was necessary --- freqtrade/data/converter.py | 8 ++++---- freqtrade/data/history.py | 2 +- freqtrade/exchange/exchange.py | 2 +- freqtrade/optimize/hyperopt.py | 7 ++----- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index d1b8277ea..43c91a843 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,7 @@ from pandas import DataFrame, to_datetime logger = logging.getLogger(__name__) -def parse_ticker_dataframe(ticker: list, ticker_interval: str, *, +def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: """ @@ -51,12 +51,12 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, *, logger.debug('Dropping last candle') if fill_missing: - return ohlcv_fill_up_missing_data(frame, ticker_interval) + return ohlcv_fill_up_missing_data(frame, ticker_interval, pair) else: return frame -def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> DataFrame: +def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: str) -> DataFrame: """ Fills up missing data with 0 volume rows, using the previous close as price for "open", "high" "low" and "close", volume is set to 0 @@ -87,7 +87,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da len_before = len(dataframe) len_after = len(df) if len_before != len_after: - logger.info(f"Missing data fillup: before: {len_before} - after: {len_after}") + logger.info(f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}") return df diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 83ea84284..655034b33 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -116,7 +116,7 @@ def load_pair_history(pair: str, logger.warning('Missing data at end for pair %s, data ends at %s', pair, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) - return parse_ticker_dataframe(pairdata, ticker_interval, + return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, fill_missing=fill_up_missing, drop_incomplete=drop_incomplete) else: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6996efb..db264d1bc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -581,7 +581,7 @@ class Exchange(object): self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( - ticks, ticker_interval, fill_missing=True, + ticks, ticker_interval, pair=pair, fill_missing=True, drop_incomplete=self._ohlcv_partial_candle) return tickers diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 28b9ce789..7fd9bf5d9 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -19,8 +19,7 @@ from skopt import Optimizer from skopt.space import Dimension from freqtrade.arguments import Arguments -from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver @@ -281,9 +280,7 @@ class Hyperopt(Backtesting): return min_date, max_date = get_timeframe(data) - # Validate dataframe for missing values (mainly at start and end, as fillup is called) - validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) + logger.info( 'Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), From 4a916125a0ad7b1fe5af327b230f7dfa718c4166 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 13:47:20 +0200 Subject: [PATCH 586/928] Tests need to pass pair to parse_ticker_dataframe --- docs/developer.md | 2 +- freqtrade/data/converter.py | 1 + freqtrade/tests/conftest.py | 5 +++-- freqtrade/tests/data/test_converter.py | 20 +++++++++++-------- freqtrade/tests/edge/test_edge.py | 6 +++--- freqtrade/tests/optimize/test_backtesting.py | 9 ++++++--- freqtrade/tests/optimize/test_hyperopt.py | 6 ++++-- .../tests/strategy/test_default_strategy.py | 3 ++- freqtrade/tests/strategy/test_interface.py | 3 ++- freqtrade/tests/test_misc.py | 6 ++++-- 10 files changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6ecb7f156..74535234d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -112,7 +112,7 @@ pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange! raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe -df1 = parse_ticker_dataframe(raw, timeframe, drop_incomplete=False) +df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) print(df1["date"].tail(1)) print(datetime.utcnow()) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 43c91a843..b530b3bce 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -17,6 +17,7 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe :param ticker: ticker list, as returned by exchange.async_get_candle_history :param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data + :param pair: Pair this data is for (used to warn if fillup was necessary) :param fill_missing: fill up missing candles with 0 candles (see ohlcv_fill_up_missing_data for details) :param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index dd0148bd8..808d128ad 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -650,7 +650,7 @@ def ticker_history_list(): @pytest.fixture def ticker_history(ticker_history_list): - return parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True) + return parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", fill_missing=True) @pytest.fixture @@ -855,7 +855,8 @@ def tickers(): @pytest.fixture def result(): with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: - return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True) + return parse_ticker_dataframe(json.load(data_file), '1m', + pair="UNITTEST/BTC", fill_missing=True) # FIX: # Create an fixture/function diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 032f32390..e64b4e84c 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -15,7 +15,8 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog): caplog.set_level(logging.DEBUG) # Test file with BV data - dataframe = parse_ticker_dataframe(ticker_history_list, '5m', fill_missing=True) + dataframe = parse_ticker_dataframe(ticker_history_list, '5m', + pair="UNITTEST/BTC", fill_missing=True) assert dataframe.columns.tolist() == columns assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples) @@ -27,12 +28,13 @@ def test_ohlcv_fill_up_missing_data(caplog): pair='UNITTEST/BTC', fill_up_missing=False) caplog.set_level(logging.DEBUG) - data2 = ohlcv_fill_up_missing_data(data, '1m') + data2 = ohlcv_fill_up_missing_data(data, '1m', 'UNITTEST/BTC') assert len(data2) > len(data) # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}", + assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}", caplog.record_tuples) # Test fillup actually fixes invalid backtest data @@ -78,10 +80,10 @@ def test_ohlcv_fill_up_missing_data2(caplog): ] # Generate test-data without filling missing - data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False) + data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False) assert len(data) == 3 caplog.set_level(logging.DEBUG) - data2 = ohlcv_fill_up_missing_data(data, ticker_interval) + data2 = ohlcv_fill_up_missing_data(data, ticker_interval, "UNITTEST/BTC") assert len(data2) == 4 # 3rd candle has been filled row = data2.loc[2, :] @@ -94,7 +96,7 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}", + assert log_has(f"Missing data fillup for UNITTEST/BTC: before: {len(data)} - after: {len(data2)}", caplog.record_tuples) @@ -134,12 +136,14 @@ def test_ohlcv_drop_incomplete(caplog): ] ] caplog.set_level(logging.DEBUG) - data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False, drop_incomplete=False) + data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", + fill_missing=False, drop_incomplete=False) assert len(data) == 4 assert not log_has("Dropping last candle", caplog.record_tuples) # Drop last candle - data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False, drop_incomplete=True) + data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", + fill_missing=False, drop_incomplete=True) assert len(data) == 3 assert log_has("Dropping last candle", caplog.record_tuples) diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index a14e3282e..45b8e609e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -263,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals hz = 0.1 base = 0.001 - ETHBTC = [ + NEOBTC = [ [ ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, @@ -285,8 +285,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals 123.45 ] for x in range(0, 500)] - pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True), - 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', fill_missing=True)} + pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True), + 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)} return pairdata diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 3f88a8d6c..98e8808a8 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -78,7 +78,8 @@ def load_data_test(what): pair[x][5] # Keep old volume ] for x in range(0, datalen) ] - return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', fill_missing=True)} + return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', pair="UNITTEST/BTC", + fill_missing=True)} def simple_backtest(config, contour, num_results, mocker) -> None: @@ -107,7 +108,8 @@ def simple_backtest(config, contour, num_results, mocker) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, timerange=None, exchange=None, live=False): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) - pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)} + pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", + fill_missing=True)} return pairdata @@ -355,7 +357,8 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", + fill_missing=True)} backtesting = Backtesting(default_conf) data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a51d74dbb..2c601e0fa 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -427,7 +427,8 @@ def test_has_space(hyperopt): def test_populate_indicators(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", + fill_missing=True)} dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -440,7 +441,8 @@ def test_populate_indicators(hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", + fill_missing=True)} dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index be514f2d1..74c81882a 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -10,7 +10,8 @@ from freqtrade.strategy.default_strategy import DefaultStrategy @pytest.fixture def result(): with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file: - return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True) + return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC", + fill_missing=True) def test_default_strategy_structure(): diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index e384003dc..fe7fd2193 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -111,7 +111,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None: timerange = TimeRange(None, 'line', 0, -100) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", + fill_missing=True)} data = strategy.tickerdata_to_dataframe(tickerlist) assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index c7bcf7edf..7a7b15cf2 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -17,7 +17,8 @@ def test_shorten_date() -> None: def test_datesarray_to_datetimearray(ticker_history_list): - dataframes = parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True) + dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", + fill_missing=True) dates = datesarray_to_datetimearray(dataframes['date']) assert isinstance(dates[0], datetime.datetime) @@ -34,7 +35,8 @@ def test_datesarray_to_datetimearray(ticker_history_list): def test_common_datearray(default_conf) -> None: strategy = DefaultStrategy(default_conf) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", fill_missing=True)} + tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", pair="UNITTEST/BTC", + fill_missing=True)} dataframes = strategy.tickerdata_to_dataframe(tickerlist) dates = common_datearray(dataframes) From 472e7f80a0f6add32d2e212fd8f2ed9b57f3fcb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jun 2019 16:58:17 +0200 Subject: [PATCH 587/928] Fix Line too long error --- freqtrade/tests/data/test_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index e64b4e84c..f68224e0e 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -96,7 +96,8 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: {len(data)} - after: {len(data2)}", + assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}", caplog.record_tuples) From e6cab6d7106d00bce437fa728475e8603fbfb81d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:12 +0200 Subject: [PATCH 588/928] Move get_args from multiple locations to conftest --- freqtrade/tests/conftest.py | 6 ++++++ freqtrade/tests/optimize/test_backtesting.py | 9 ++------- freqtrade/tests/optimize/test_edge_cli.py | 10 ++-------- freqtrade/tests/optimize/test_hyperopt.py | 3 +-- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index dcc69fcb1..5693d4d80 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -4,6 +4,7 @@ import logging import re from datetime import datetime from functools import reduce +from typing import List from unittest.mock import MagicMock, PropertyMock import arrow @@ -11,6 +12,7 @@ import pytest from telegram import Chat, Message, Update from freqtrade import constants, persistence +from freqtrade.arguments import Arguments from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -35,6 +37,10 @@ def log_has_re(line, logs): False) +def get_args(args) -> List[str]: + return Arguments(args, '').get_parsed_arg() + + def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 3f88a8d6c..cf32934c7 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,7 +3,6 @@ import json import math import random -from typing import List from unittest.mock import MagicMock import numpy as np @@ -12,7 +11,7 @@ import pytest from arrow import Arrow from freqtrade import DependencyException, constants -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe @@ -23,11 +22,7 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange def trim_dictlist(dict_list, num): diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 5d16b0f2d..b46f353d8 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -2,19 +2,13 @@ # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments import json -from typing import List from unittest.mock import MagicMock -from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize import start_edge, setup_configuration +from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a51d74dbb..baa5da545 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,8 +16,7 @@ from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange -from freqtrade.tests.optimize.test_backtesting import get_args +from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange @pytest.fixture(scope='function') From 442339cd279118dba4292a78ef2ca4fe2d25f144 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:24 +0200 Subject: [PATCH 589/928] Add tests for utils.py --- freqtrade/tests/test_utils.py | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 freqtrade/tests/test_utils.py diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py new file mode 100644 index 000000000..3526026e6 --- /dev/null +++ b/freqtrade/tests/test_utils.py @@ -0,0 +1,41 @@ +from freqtrade.utils import setup_configuration, start_list_exchanges +from freqtrade.tests.conftest import get_args, log_has, log_has_re +from freqtrade.state import RunMode + +import re + + +def test_setup_configuration(): + args = [ + '--config', 'config.json.example', + ] + + config = setup_configuration(get_args(args), RunMode.OTHER) + assert "exchange" in config + assert config['exchange']['key'] == '' + assert config['exchange']['secret'] == '' + + +def test_list_exchanges(capsys): + + args = [ + "list-exchanges", + ] + + start_list_exchanges(get_args(args)) + captured = capsys.readouterr() + assert re.match(r"Exchanges supported by ccxt and available.*", captured.out) + assert re.match(r".*binance,.*", captured.out) + assert re.match(r".*bittrex,.*", captured.out) + + # Test with --one-column + args = [ + "list-exchanges", + "--one-column", + ] + + start_list_exchanges(get_args(args)) + captured = capsys.readouterr() + 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) From 114de8a0259914804750fc5b341ac0cc45832e34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:13:56 +0200 Subject: [PATCH 590/928] Remove unused imports --- freqtrade/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 3526026e6..7550efb23 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,5 +1,5 @@ from freqtrade.utils import setup_configuration, start_list_exchanges -from freqtrade.tests.conftest import get_args, log_has, log_has_re +from freqtrade.tests.conftest import get_args from freqtrade.state import RunMode import re From 9035e0b695e710b4bab719e35b57e5620c450cf7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:39:43 +0200 Subject: [PATCH 591/928] Update function due to merge of #1926 --- freqtrade/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 45bd9fd3f..324b54a4e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -3,7 +3,7 @@ from argparse import Namespace from typing import Any, Dict from freqtrade.configuration import Configuration -from freqtrade.exchange import supported_exchanges +from freqtrade.exchange import available_exchanges from freqtrade.state import RunMode @@ -34,7 +34,7 @@ def start_list_exchanges(args: Namespace) -> None: """ if args.print_one_column: - print('\n'.join(supported_exchanges())) + print('\n'.join(available_exchanges())) else: print(f"Exchanges supported by ccxt and available for Freqtrade: " - f"{', '.join(supported_exchanges())}") + f"{', '.join(available_exchanges())}") From 583d70ec9c535d6b2ad6275818096bcb2b9fac49 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 14:18:30 +0200 Subject: [PATCH 592/928] add plot module proto --- freqtrade/plot/__init__.py | 0 freqtrade/plot/plotting.py | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 freqtrade/plot/__init__.py create mode 100644 freqtrade/plot/plotting.py diff --git a/freqtrade/plot/__init__.py b/freqtrade/plot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py new file mode 100644 index 000000000..e04b51726 --- /dev/null +++ b/freqtrade/plot/plotting.py @@ -0,0 +1,13 @@ +import logging + + +logger = logging.getLogger(__name__) + + +try: + from plotly import tools + from plotly.offline import plot + import plotly.graph_objs as go +except ImportError: + logger.exception("Module plotly not found \n Please install using `pip install plotly`") + exit() From 68af6d415157183a33472eb8dfd2070d3db2e3f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 07:00:57 +0200 Subject: [PATCH 593/928] Move plot-functions to plotting module --- freqtrade/plot/plotting.py | 177 +++++++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 170 ++--------------------------------- 2 files changed, 185 insertions(+), 162 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e04b51726..7e815bdd7 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,5 +1,7 @@ import logging +from typing import List +import pandas as pd logger = logging.getLogger(__name__) @@ -11,3 +13,178 @@ try: except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") exit() + + +def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: + """ + Generator all the indicator selected by the user for a specific row + :param fig: Plot figure to append to + :param row: row number for this plot + :param indicators: List of indicators present in the dataframe + :param data: candlestick DataFrame + """ + for indicator in indicators: + if indicator in data: + # TODO: Replace all Scatter with Scattergl for performance!! + scattergl = go.Scatter( + x=data['date'], + y=data[indicator], + mode='lines', + name=indicator + ) + fig.append_trace(scattergl, row, 1) + else: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not found ' + 'in your strategy.', + indicator + ) + + return fig + + +def plot_trades(fig, trades: pd.DataFrame): + """ + Plot trades to "fig" + """ + # Trades can be empty + if trades is not None: + trade_buys = go.Scatter( + x=trades["open_time"], + y=trades["open_rate"], + mode='markers', + name='trade_buy', + marker=dict( + symbol='square-open', + size=11, + line=dict(width=2), + color='green' + ) + ) + trade_sells = go.Scatter( + x=trades["close_time"], + y=trades["close_rate"], + mode='markers', + name='trade_sell', + marker=dict( + symbol='square-open', + size=11, + line=dict(width=2), + color='red' + ) + ) + fig.append_trace(trade_buys, 1, 1) + fig.append_trace(trade_sells, 1, 1) + return fig + + +def generate_graph( + pair: str, + data: pd.DataFrame, + trades: pd.DataFrame = None, + indicators1: List[str] = [], + indicators2: List[str] = [], +) -> tools.make_subplots: + """ + Generate the graph from the data generated by Backtesting or from DB + Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators + :param pair: Pair to Display on the graph + :param data: OHLCV DataFrame containing indicators and buy/sell signals + :param trades: All trades created + :param indicators1: List containing Main plot indicators + :param indicators2: List containing Sub plot indicators + :return: None + """ + + # Define the graph + fig = tools.make_subplots( + rows=3, + cols=1, + shared_xaxes=True, + row_width=[1, 1, 4], + vertical_spacing=0.0001, + ) + fig['layout'].update(title=pair) + fig['layout']['yaxis1'].update(title='Price') + fig['layout']['yaxis2'].update(title='Volume') + fig['layout']['yaxis3'].update(title='Other') + fig['layout']['xaxis']['rangeslider'].update(visible=False) + + # Common information + candles = go.Candlestick( + x=data.date, + open=data.open, + high=data.high, + low=data.low, + close=data.close, + name='Price' + ) + fig.append_trace(candles, 1, 1) + + if 'buy' in data.columns: + df_buy = data[data['buy'] == 1] + buys = go.Scatter( + x=df_buy.date, + y=df_buy.close, + mode='markers', + name='buy', + marker=dict( + symbol='triangle-up-dot', + size=9, + line=dict(width=1), + color='green', + ) + ) + fig.append_trace(buys, 1, 1) + + if 'sell' in data.columns: + df_sell = data[data['sell'] == 1] + sells = go.Scatter( + x=df_sell.date, + y=df_sell.close, + mode='markers', + name='sell', + marker=dict( + symbol='triangle-down-dot', + size=9, + line=dict(width=1), + color='red', + ) + ) + fig.append_trace(sells, 1, 1) + + if 'bb_lowerband' in data and 'bb_upperband' in data: + bb_lower = go.Scatter( + x=data.date, + y=data.bb_lowerband, + name='BB lower', + line={'color': 'rgba(255,255,255,0)'}, + ) + bb_upper = go.Scatter( + x=data.date, + y=data.bb_upperband, + name='BB upper', + fill="tonexty", + fillcolor="rgba(0,176,246,0.2)", + line={'color': 'rgba(255,255,255,0)'}, + ) + fig.append_trace(bb_lower, 1, 1) + fig.append_trace(bb_upper, 1, 1) + + # Add indicators to main plot + fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) + + fig = plot_trades(fig, trades) + + # Volume goes to row 2 + volume = go.Bar( + x=data['date'], + y=data['volume'], + name='Volume' + ) + fig.append_trace(volume, 2, 1) + + # Add indicators to seperate row + fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) + + return fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4f8ffb32b..897b0c917 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,15 +31,14 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd -import plotly.graph_objs as go import pytz -from plotly import tools from plotly.offline import plot from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.plot.plotting import generate_graph from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade @@ -96,7 +95,10 @@ def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: Path("user_data/plots").mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), auto_open=False) + plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), + auto_open=False, + include_plotlyjs='https://cdn.plot.ly/plotly-1.47.4.min.js' + ) if is_last: plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) @@ -186,162 +188,6 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: return trades -def generate_graph( - pair: str, - trades: pd.DataFrame, - data: pd.DataFrame, - indicators1: str, - indicators2: str - ) -> tools.make_subplots: - """ - Generate the graph from the data generated by Backtesting or from DB - :param pair: Pair to Display on the graph - :param trades: All trades created - :param data: Dataframe - :indicators1: String Main plot indicators - :indicators2: String Sub plot indicators - :return: None - """ - - # Define the graph - fig = tools.make_subplots( - rows=3, - cols=1, - shared_xaxes=True, - row_width=[1, 1, 4], - vertical_spacing=0.0001, - ) - fig['layout'].update(title=pair) - fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Volume') - fig['layout']['yaxis3'].update(title='Other') - fig['layout']['xaxis']['rangeslider'].update(visible=False) - - # Common information - candles = go.Candlestick( - x=data.date, - open=data.open, - high=data.high, - low=data.low, - close=data.close, - name='Price' - ) - - df_buy = data[data['buy'] == 1] - buys = go.Scattergl( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', - ) - ) - df_sell = data[data['sell'] == 1] - sells = go.Scattergl( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', - ) - ) - - trade_buys = go.Scattergl( - x=trades["open_time"], - y=trades["open_rate"], - mode='markers', - name='trade_buy', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='green' - ) - ) - trade_sells = go.Scattergl( - x=trades["close_time"], - y=trades["close_rate"], - mode='markers', - name='trade_sell', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='red' - ) - ) - - # Row 1 - fig.append_trace(candles, 1, 1) - - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - name='BB lower', - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='BB upper', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.append_trace(bb_lower, 1, 1) - fig.append_trace(bb_upper, 1, 1) - - fig = generate_row(fig=fig, row=1, raw_indicators=indicators1, data=data) - fig.append_trace(buys, 1, 1) - fig.append_trace(sells, 1, 1) - fig.append_trace(trade_buys, 1, 1) - fig.append_trace(trade_sells, 1, 1) - - # Row 2 - volume = go.Bar( - x=data['date'], - y=data['volume'], - name='Volume' - ) - fig.append_trace(volume, 2, 1) - - # Row 3 - fig = generate_row(fig=fig, row=3, raw_indicators=indicators2, data=data) - - return fig - - -def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots: - """ - Generator all the indicator selected by the user for a specific row - """ - for indicator in raw_indicators.split(','): - if indicator in data: - scattergl = go.Scattergl( - x=data['date'], - y=data[indicator], - name=indicator - ) - fig.append_trace(scattergl, row, 1) - else: - logger.info( - 'Indicator "%s" ignored. Reason: This indicator is not found ' - 'in your strategy.', - indicator - ) - - return fig - - def plot_parse_args(args: List[str]) -> Namespace: """ Parse args passed to the script @@ -411,10 +257,10 @@ def analyse_and_plot_pairs(args: Namespace): fig = generate_graph( pair=pair, - trades=trades, data=dataframe, - indicators1=args.indicators1, - indicators2=args.indicators2 + trades=trades, + indicators1=args.indicators1.split(","), + indicators2=args.indicators2.split(",") ) is_last = (False, True)[pair_counter == len(tickers)] From 6df0b39f8151c31b89d4acbf06a9654fa6c56976 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 20:02:17 +0200 Subject: [PATCH 594/928] Cleanup plot_dataframe a bit --- scripts/plot_dataframe.py | 99 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 897b0c917..1d3b24449 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -51,7 +51,7 @@ _CONF: Dict[str, Any] = {} timeZone = pytz.UTC -def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: +def load_trades(args: Namespace, pair: str) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: persistence.init(args.db_url, clean_open_orders=False) @@ -96,9 +96,7 @@ def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: Path("user_data/plots").mkdir(parents=True, exist_ok=True) plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), - auto_open=False, - include_plotlyjs='https://cdn.plot.ly/plotly-1.47.4.min.js' - ) + auto_open=False) if is_last: plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) @@ -133,14 +131,13 @@ def get_trading_env(args: Namespace): return [strategy, exchange, pairs] -def get_tickers_data(strategy, exchange, pairs: List[str], args): +def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, live: bool): """ Get tickers data for each pairs on live or local, option defined in args - :return: dictinnary of tickers. output format: {'pair': tickersdata} + :return: dictionary of tickers. output format: {'pair': tickersdata} """ ticker_interval = strategy.ticker_interval - timerange = Arguments.parse_timerange(args.timerange) tickers = history.load_data( datadir=Path(str(_CONF.get("datadir"))), @@ -184,10 +181,53 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: Compare trades and backtested pair DataFrames to get trades performed on backtested period :return: the DataFrame of a trades of period """ - trades = trades.loc[trades['open_time'] >= dataframe.iloc[0]['date']] + trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & + (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades +def analyse_and_plot_pairs(args: Namespace): + """ + From arguments provided in cli: + -Initialise backtest env + -Get tickers data + -Generate Dafaframes populated with indicators and signals + -Load trades excecuted on same periods + -Generate Plotly plot objects + -Generate plot files + :return: None + """ + strategy, exchange, pairs = get_trading_env(args) + # Set timerange to use + timerange = Arguments.parse_timerange(args.timerange) + ticker_interval = strategy.ticker_interval + + tickers = get_tickers_data(strategy, exchange, pairs, timerange, args.live) + pair_counter = 0 + for pair, data in tickers.items(): + pair_counter += 1 + logger.info("analyse pair %s", pair) + tickers = {} + tickers[pair] = data + dataframe = generate_dataframe(strategy, tickers, pair) + + trades = load_trades(args, pair) + trades = extract_trades_of_period(dataframe, trades) + + fig = generate_graph( + pair=pair, + data=dataframe, + trades=trades, + indicators1=args.indicators1.split(","), + indicators2=args.indicators2.split(",") + ) + + is_last = (False, True)[pair_counter == len(tickers)] + generate_plot_file(fig, pair, ticker_interval, is_last) + + logger.info('End of ploting process %s plots generated', pair_counter) + + def plot_parse_args(args: List[str]) -> Namespace: """ Parse args passed to the script @@ -226,49 +266,6 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.backtesting_options(arguments.parser) return arguments.parse_args() - -def analyse_and_plot_pairs(args: Namespace): - """ - From arguments provided in cli: - -Initialise backtest env - -Get tickers data - -Generate Dafaframes populated with indicators and signals - -Load trades excecuted on same periods - -Generate Plotly plot objects - -Generate plot files - :return: None - """ - strategy, exchange, pairs = get_trading_env(args) - # Set timerange to use - timerange = Arguments.parse_timerange(args.timerange) - ticker_interval = strategy.ticker_interval - - tickers = get_tickers_data(strategy, exchange, pairs, args) - pair_counter = 0 - for pair, data in tickers.items(): - pair_counter += 1 - logger.info("analyse pair %s", pair) - tickers = {} - tickers[pair] = data - dataframe = generate_dataframe(strategy, tickers, pair) - - trades = load_trades(args, pair, timerange) - trades = extract_trades_of_period(dataframe, trades) - - fig = generate_graph( - pair=pair, - data=dataframe, - trades=trades, - indicators1=args.indicators1.split(","), - indicators2=args.indicators2.split(",") - ) - - is_last = (False, True)[pair_counter == len(tickers)] - generate_plot_file(fig, pair, ticker_interval, is_last) - - logger.info('End of ploting process %s plots generated', pair_counter) - - def main(sysargv: List[str]) -> None: """ This function will initiate the bot and start the trading loop. From e0a1e5417fbb8e36bc947e29398f05754b565959 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 20:23:16 +0200 Subject: [PATCH 595/928] sanity checks before plotting, cleanup --- freqtrade/plot/plotting.py | 63 ++++++++++++++++++++------------------ scripts/plot_dataframe.py | 29 +++++++++++------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7e815bdd7..a18b7bf70 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -25,8 +25,7 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m """ for indicator in indicators: if indicator in data: - # TODO: Replace all Scatter with Scattergl for performance!! - scattergl = go.Scatter( + scattergl = go.Scattergl( x=data['date'], y=data[indicator], mode='lines', @@ -48,7 +47,7 @@ def plot_trades(fig, trades: pd.DataFrame): Plot trades to "fig" """ # Trades can be empty - if trades is not None: + if trades is not None and len(trades) > 0: trade_buys = go.Scatter( x=trades["open_time"], y=trades["open_rate"], @@ -123,44 +122,50 @@ def generate_graph( if 'buy' in data.columns: df_buy = data[data['buy'] == 1] - buys = go.Scatter( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', + if len(df_buy) > 0: + buys = go.Scattergl( + x=df_buy.date, + y=df_buy.close, + mode='markers', + name='buy', + marker=dict( + symbol='triangle-up-dot', + size=9, + line=dict(width=1), + color='green', + ) ) - ) - fig.append_trace(buys, 1, 1) + fig.append_trace(buys, 1, 1) + else: + logger.warning("No buy-signals found.") if 'sell' in data.columns: df_sell = data[data['sell'] == 1] - sells = go.Scatter( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', + if len(df_sell) > 0: + sells = go.Scattergl( + x=df_sell.date, + y=df_sell.close, + mode='markers', + name='sell', + marker=dict( + symbol='triangle-down-dot', + size=9, + line=dict(width=1), + color='red', + ) ) - ) - fig.append_trace(sells, 1, 1) + fig.append_trace(sells, 1, 1) + else: + logger.warning("No sell-signals found.") if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( + bb_lower = go.Scattergl( x=data.date, y=data.bb_lowerband, name='BB lower', line={'color': 'rgba(255,255,255,0)'}, ) - bb_upper = go.Scatter( + bb_upper = go.Scattergl( x=data.date, y=data.bb_upperband, name='BB upper', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 1d3b24449..84e18e5cd 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -51,11 +51,18 @@ _CONF: Dict[str, Any] = {} timeZone = pytz.UTC -def load_trades(args: Namespace, pair: str) -> pd.DataFrame: - trades: pd.DataFrame = pd.DataFrame() - if args.db_url: - persistence.init(args.db_url, clean_open_orders=False) +def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: + """ + Load trades, either from a DB (using dburl) or via a backtest export file. + :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :param exportfilename: Path to a file exported from backtesting + :returns: Dataframe containing Trades + """ + # TODO: Document and move to btanalysis + trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + if db_url: + persistence.init(db_url, clean_open_orders=False) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] @@ -68,18 +75,15 @@ def load_trades(args: Namespace, pair: str) -> pd.DataFrame: t.open_rate, t.close_rate, t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) - for t in Trade.query.filter(Trade.pair.is_(pair)).all()], + for t in Trade.query.all()], columns=columns) - elif args.exportfilename: + elif exportfilename: - file = Path(args.exportfilename) + file = Path(exportfilename) if file.exists(): trades = load_backtest_data(file) - else: - trades = pd.DataFrame([], columns=BT_DATA_COLUMNS) - return trades @@ -181,6 +185,7 @@ def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: Compare trades and backtested pair DataFrames to get trades performed on backtested period :return: the DataFrame of a trades of period """ + # TODO: Document and move to btanalysis (?) trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades @@ -211,7 +216,9 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(args, pair) + trades = load_trades(pair, db_url=args.db_url, + exportfilename=args.exportfilename) + trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) fig = generate_graph( From b1a01345f9db4bf45b128e31464498fd14af003e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 07:19:21 +0200 Subject: [PATCH 596/928] Add better hover tip --- freqtrade/plot/plotting.py | 19 +++++++++++++------ scripts/plot_dataframe.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a18b7bf70..a8bd032ab 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,7 @@ import logging - from typing import List + +import arrow import pandas as pd logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ try: import plotly.graph_objs as go except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") - exit() + exit(1) def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: @@ -25,9 +26,10 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m """ for indicator in indicators: if indicator in data: - scattergl = go.Scattergl( + # TODO: Figure out why scattergl causes problems + scattergl = go.Scatter( x=data['date'], - y=data[indicator], + y=data[indicator].values, mode='lines', name=indicator ) @@ -60,9 +62,14 @@ def plot_trades(fig, trades: pd.DataFrame): color='green' ) ) + # Create description for sell summarizing the trade + desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " + f"{row['duration']}min", + axis=1) trade_sells = go.Scatter( x=trades["close_time"], y=trades["close_rate"], + text=desc, mode='markers', name='trade_sell', marker=dict( @@ -123,7 +130,7 @@ def generate_graph( if 'buy' in data.columns: df_buy = data[data['buy'] == 1] if len(df_buy) > 0: - buys = go.Scattergl( + buys = go.Scatter( x=df_buy.date, y=df_buy.close, mode='markers', @@ -142,7 +149,7 @@ def generate_graph( if 'sell' in data.columns: df_sell = data[data['sell'] == 1] if len(df_sell) > 0: - sells = go.Scattergl( + sells = go.Scatter( x=df_sell.date, y=df_sell.close, mode='markers', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 84e18e5cd..7457aadc4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -216,7 +216,7 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(pair, db_url=args.db_url, + trades = load_trades(db_url=args.db_url, exportfilename=args.exportfilename) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) From 6347161975591e6cb0d9721df52de98003c13ef2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 20:26:46 +0200 Subject: [PATCH 597/928] don't use print in plot_dataframe --- scripts/plot_dataframe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7457aadc4..a20d8abae 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -67,7 +67,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: "open_rate", "close_rate", "duration"] for x in Trade.query.all(): - print("date: {}".format(x.open_date)) + logger.info("date: {}".format(x.open_date)) trades = pd.DataFrame([(t.pair, t.calc_profit(), t.open_date.replace(tzinfo=timeZone), @@ -114,7 +114,6 @@ def get_trading_env(args: Namespace): # Load the configuration _CONF.update(setup_configuration(args, RunMode.BACKTEST)) - print(_CONF) pairs = args.pairs.split(',') if pairs is None: @@ -161,7 +160,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, if data.empty: del tickers[pair] logger.info( - 'An issue occured while retreiving datas of %s pair, please retry ' + 'An issue occured while retreiving data of %s pair, please retry ' 'using -l option for live or --refresh-pairs-cached', pair) return tickers @@ -273,6 +272,7 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.backtesting_options(arguments.parser) return arguments.parse_args() + def main(sysargv: List[str]) -> None: """ This function will initiate the bot and start the trading loop. From cae218546087552ba890cb39425e6f6820058ad7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2019 06:41:55 +0200 Subject: [PATCH 598/928] Move generate_plot to plotting.py --- freqtrade/plot/plotting.py | 20 ++++++++++++++++++++ scripts/plot_dataframe.py | 22 ++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a8bd032ab..037516a27 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -3,6 +3,7 @@ from typing import List import arrow import pandas as pd +from pathlib import Path logger = logging.getLogger(__name__) @@ -200,3 +201,22 @@ def generate_graph( fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) return fig + + +def generate_plot_file(fig, pair, ticker_interval) -> None: + """ + Generate a plot html file from pre populated fig plotly object + :param fig: Plotly Figure to plot + :param pair: Pair to plot (used as filename and Plot title) + :param ticker_interval: Used as part of the filename + :return: None + """ + logger.info('Generate plot file for %s', pair) + + pair_name = pair.replace("/", "_") + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + + Path("user_data/plots").mkdir(parents=True, exist_ok=True) + + plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), + auto_open=False) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a20d8abae..bccf98261 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -38,7 +38,7 @@ from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.plot.plotting import generate_graph +from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade @@ -87,23 +87,6 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: return trades -def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: - """ - Generate a plot html file from pre populated fig plotly object - :return: None - """ - logger.info('Generate plot file for %s', pair) - - pair_name = pair.replace("/", "_") - file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' - - Path("user_data/plots").mkdir(parents=True, exist_ok=True) - - plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), - auto_open=False) - if is_last: - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False) - def get_trading_env(args: Namespace): """ @@ -228,8 +211,7 @@ def analyse_and_plot_pairs(args: Namespace): indicators2=args.indicators2.split(",") ) - is_last = (False, True)[pair_counter == len(tickers)] - generate_plot_file(fig, pair, ticker_interval, is_last) + generate_plot_file(fig, pair, ticker_interval) logger.info('End of ploting process %s plots generated', pair_counter) From 2891d7cccbc61835cc28d40f62fb9cce3bda8720 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jun 2019 20:17:23 +0200 Subject: [PATCH 599/928] Add initial plotting test --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 52 ++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 4 +-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 freqtrade/tests/test_plotting.py diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 037516a27..b1e32c4fb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -91,7 +91,7 @@ def generate_graph( trades: pd.DataFrame = None, indicators1: List[str] = [], indicators2: List[str] = [], -) -> tools.make_subplots: +) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py new file mode 100644 index 000000000..1f4873f4a --- /dev/null +++ b/freqtrade/tests/test_plotting.py @@ -0,0 +1,52 @@ + +from unittest.mock import MagicMock + +import plotly.graph_objs as go + +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.data import history +from freqtrade.plot.plotting import (generate_graph, generate_plot_file, + generate_row, plot_trades) + + +def fig_generating_mock(fig, *args, **kwargs): + """ Return Fig - used to mock generate_row and plot_trades""" + return fig + + +def test_generate_row(): + # TODO: implement me + pass + + +def test_plot_trades(): + # TODO: implement me + pass + + +def test_generate_graph(default_conf, mocker): + row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + MagicMock(side_effect=fig_generating_mock)) + trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', + MagicMock(side_effect=fig_generating_mock)) + + timerange = TimeRange(None, 'line', 0, -100) + data = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + datadir=None, timerange=timerange) + + indicators1 = [] + indicators2 = [] + fig = generate_graph(pair="UNITTEST/BTC", data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) + assert isinstance(fig, go.Figure) + assert fig.layout.title.text == "UNITTEST/BTC" + figure = fig.layout.figure + # Candlesticks are plotted first + assert isinstance(figure.data[0], go.Candlestick) + assert figure.data[0].name == "Price" + + assert isinstance(figure.data[1], go.Bar) + assert figure.data[1].name == "Volume" + + assert row_mock.call_count == 2 + assert trades_mock.call_count == 1 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index bccf98261..8bed81985 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -32,7 +32,6 @@ from typing import Any, Dict, List import pandas as pd import pytz -from plotly.offline import plot from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange @@ -87,7 +86,6 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: return trades - def get_trading_env(args: Namespace): """ Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter @@ -132,7 +130,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, exchange=Exchange(_CONF), - live=args.live, + live=live, ) # No ticker found, impossible to download, len mismatch From 6db4e05aef7a97531041b20417448c0551974753 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jun 2019 06:45:36 +0200 Subject: [PATCH 600/928] Improve plotting tests --- freqtrade/tests/test_plotting.py | 83 +++++++++++++++++++++++++++----- requirements-dev.txt | 1 + 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 1f4873f4a..d9fb3b338 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -7,13 +7,19 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.plot.plotting import (generate_graph, generate_plot_file, generate_row, plot_trades) - +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import log_has, log_has_re def fig_generating_mock(fig, *args, **kwargs): """ Return Fig - used to mock generate_row and plot_trades""" return fig +def find_trace_in_fig_data(data, search_string: str): + matches = filter(lambda x: x.name == search_string, data) + return next(matches) + + def test_generate_row(): # TODO: implement me pass @@ -24,29 +30,84 @@ def test_plot_trades(): pass -def test_generate_graph(default_conf, mocker): +def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) - timerange = TimeRange(None, 'line', 0, -100) - data = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + data = history.load_pair_history(pair=pair, ticker_interval='1m', datadir=None, timerange=timerange) + data['buy'] = 0 + data['sell'] = 0 indicators1 = [] indicators2 = [] - fig = generate_graph(pair="UNITTEST/BTC", data=data, trades=None, + fig = generate_graph(pair=pair, data=data, trades=None, indicators1=indicators1, indicators2=indicators2) assert isinstance(fig, go.Figure) - assert fig.layout.title.text == "UNITTEST/BTC" + assert fig.layout.title.text == pair figure = fig.layout.figure - # Candlesticks are plotted first - assert isinstance(figure.data[0], go.Candlestick) - assert figure.data[0].name == "Price" - assert isinstance(figure.data[1], go.Bar) - assert figure.data[1].name == "Volume" + assert len(figure.data) == 2 + # Candlesticks are plotted first + candles = find_trace_in_fig_data(figure.data, "Price") + assert isinstance(candles, go.Candlestick) + + volume = find_trace_in_fig_data(figure.data, "Volume") + assert isinstance(volume, go.Bar) + + assert row_mock.call_count == 2 + assert trades_mock.call_count == 1 + + assert log_has("No buy-signals found.", caplog.record_tuples) + assert log_has("No sell-signals found.", caplog.record_tuples) + + +def test_generate_graph_no_trades(default_conf, mocker): + row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + MagicMock(side_effect=fig_generating_mock)) + trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', + MagicMock(side_effect=fig_generating_mock)) + pair = 'UNITTEST/BTC' + timerange = TimeRange(None, 'line', 0, -1000) + data = history.load_pair_history(pair=pair, ticker_interval='1m', + datadir=None, timerange=timerange) + + # Generate buy/sell signals and indicators + strat = DefaultStrategy(default_conf) + data = strat.analyze_ticker(data, {'pair': pair}) + + indicators1 = [] + indicators2 = [] + fig = generate_graph(pair=pair, data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) + assert isinstance(fig, go.Figure) + assert fig.layout.title.text == pair + figure = fig.layout.figure + + assert len(figure.data) == 6 + # Candlesticks are plotted first + candles = find_trace_in_fig_data(figure.data, "Price") + assert isinstance(candles, go.Candlestick) + + volume = find_trace_in_fig_data(figure.data, "Volume") + assert isinstance(volume, go.Bar) + + buy = find_trace_in_fig_data(figure.data, "buy") + assert isinstance(buy, go.Scatter) + # All buy-signals should be plotted + assert int(data.buy.sum()) == len(buy.x) + + sell = find_trace_in_fig_data(figure.data, "sell") + assert isinstance(sell, go.Scatter) + # All buy-signals should be plotted + assert int(data.sell.sum()) == len(sell.x) + + assert find_trace_in_fig_data(figure.data, "BB lower") + assert find_trace_in_fig_data(figure.data, "BB upper") assert row_mock.call_count == 2 assert trades_mock.call_count == 1 diff --git a/requirements-dev.txt b/requirements-dev.txt index 315033847..c8dd8b0b9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ # Include all requirements to run the bot. -r requirements.txt +-r requirements-plot.txt flake8==3.7.7 flake8-type-annotations==0.1.0 From 9f5ca82f485bece4452b46c5a1c4ea7d2315cdb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:32:12 +0200 Subject: [PATCH 601/928] Add more tests --- freqtrade/tests/test_plotting.py | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index d9fb3b338..e264ef6b3 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -1,7 +1,9 @@ from unittest.mock import MagicMock +from plotly import tools import plotly.graph_objs as go +from copy import deepcopy from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history @@ -10,6 +12,7 @@ from freqtrade.plot.plotting import (generate_graph, generate_plot_file, from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re + def fig_generating_mock(fig, *args, **kwargs): """ Return Fig - used to mock generate_row and plot_trades""" return fig @@ -20,14 +23,55 @@ def find_trace_in_fig_data(data, search_string: str): return next(matches) -def test_generate_row(): - # TODO: implement me - pass +def generage_empty_figure(): + return tools.make_subplots( + rows=3, + cols=1, + shared_xaxes=True, + row_width=[1, 1, 4], + vertical_spacing=0.0001, + ) + +def test_generate_row(default_conf, caplog): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = history.load_pair_history(pair=pair, ticker_interval='1m', + datadir=None, timerange=timerange) + indicators1 = ["ema10"] + indicators2 = ["macd"] + + # Generate buy/sell signals and indicators + strat = DefaultStrategy(default_conf) + data = strat.analyze_ticker(data, {'pair': pair}) + fig = generage_empty_figure() + + # Row 1 + fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) + figure = fig1.layout.figure + ema10 = find_trace_in_fig_data(figure.data, "ema10") + assert isinstance(ema10, go.Scatter) + assert ema10.yaxis == "y" + + fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) + figure = fig2.layout.figure + macd = find_trace_in_fig_data(figure.data, "macd") + assert isinstance(macd, go.Scatter) + assert macd.yaxis == "y3" + + # No indicator found + fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) + assert fig == fig3 + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) def test_plot_trades(): - # TODO: implement me - pass + fig1 = generage_empty_figure() + # nothing happens when no trades are available + fig = plot_trades(fig1, None) + assert fig == fig1 + + # TODO: implement tests that do something def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): From c7643e142b7e6ba0c7e9cbc422409caa38608952 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:41:05 +0200 Subject: [PATCH 602/928] Move load_trades to bt_anlaysis --- freqtrade/data/btanalysis.py | 43 ++++++++++++++++++++++++++++++++++++ scripts/plot_dataframe.py | 43 +----------------------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6fce4361b..fb120e521 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -1,12 +1,18 @@ """ Helpers when analyzing backtest data """ +import logging from pathlib import Path import numpy as np import pandas as pd +import pytz +from freqtrade import persistence from freqtrade.misc import json_load +from freqtrade.persistence import Trade + +logger = logging.getLogger(__name__) # must align with columns in backtest.py BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration", @@ -65,3 +71,40 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() return df_final[df_final['pair'] > max_open_trades] + + +def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: + """ + Load trades, either from a DB (using dburl) or via a backtest export file. + :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :param exportfilename: Path to a file exported from backtesting + :returns: Dataframe containing Trades + """ + timeZone = pytz.UTC + + trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + + if db_url: + persistence.init(db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", + "open_rate", "close_rate", "duration"] + + for x in Trade.query.all(): + logger.info("date: {}".format(x.open_date)) + + trades = pd.DataFrame([(t.pair, t.calc_profit(), + t.open_date.replace(tzinfo=timeZone), + t.close_date.replace(tzinfo=timeZone) if t.close_date else None, + t.open_rate, t.close_rate, + t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None) + for t in Trade.query.all()], + columns=columns) + + elif exportfilename: + + file = Path(exportfilename) + if file.exists(): + trades = load_backtest_data(file) + + return trades diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 8bed81985..005148d3b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,60 +31,19 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd -import pytz -from freqtrade import persistence from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.data.btanalysis import load_trades from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration -from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} -timeZone = pytz.UTC - - -def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: - """ - Load trades, either from a DB (using dburl) or via a backtest export file. - :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) - :param exportfilename: Path to a file exported from backtesting - :returns: Dataframe containing Trades - """ - # TODO: Document and move to btanalysis - trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) - - if db_url: - persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration"] - - for x in Trade.query.all(): - logger.info("date: {}".format(x.open_date)) - - trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date.replace(tzinfo=timeZone), - t.close_date.replace(tzinfo=timeZone) if t.close_date else None, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None) - for t in Trade.query.all()], - columns=columns) - - elif exportfilename: - - file = Path(exportfilename) - if file.exists(): - trades = load_backtest_data(file) - - return trades - def get_trading_env(args: Namespace): """ From 1c53aa5687c7b714b5bd7043a3e977f82e9f5ea2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 10:57:21 +0200 Subject: [PATCH 603/928] Add tests for load_trades --- freqtrade/tests/data/test_btanalysis.py | 28 ++++++++- freqtrade/tests/test_persistence.py | 84 +++++++++++++------------ 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index dd7cbe0d9..4a8babf1d 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,8 +1,11 @@ import pytest +from unittest.mock import MagicMock from pandas import DataFrame -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data, load_trades from freqtrade.data.history import make_testdata_path +from freqtrade.persistence import init, Trade +from freqtrade.tests.test_persistence import init_persistence, create_mock_trades def test_load_backtest_data(): @@ -19,3 +22,26 @@ def test_load_backtest_data(): with pytest.raises(ValueError, match=r"File .* does not exist\."): load_backtest_data(str("filename") + "nofile") + + +def test_load_trades_file(default_conf, fee, mocker): + # Real testing of load_backtest_data is done in test_load_backtest_data + lbt = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) + filename = make_testdata_path(None) / "backtest-result_test.json" + load_trades(db_url=None, exportfilename=filename) + assert lbt.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_load_trades_db(default_conf, fee, mocker): + + create_mock_trades(fee) + # remove init so it does not init again + init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) + + trades = load_trades(db_url=default_conf['db_url'], exportfilename=None) + assert init_mock.call_count == 1 + assert len(trades) == 3 + assert isinstance(trades, DataFrame) + assert "pair" in trades.columns + assert "open_time" in trades.columns diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index bb00fa8f4..381f04bd1 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -16,6 +16,50 @@ def init_persistence(default_conf): init(default_conf['db_url'], default_conf['dry_run']) +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='dry_run_buy_12345' + ) + Trade.session.add(trade) + + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + is_open=False, + open_order_id='dry_run_sell_12345' + ) + Trade.session.add(trade) + + # Simulate prod entry + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='prod_buy_12345' + ) + Trade.session.add(trade) + + def test_init_create_session(default_conf): # Check if init create a session init(default_conf['db_url'], default_conf['dry_run']) @@ -671,45 +715,7 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") def test_get_open(default_conf, fee): - # Simulate dry_run entries - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='dry_run_buy_12345' - ) - Trade.session.add(trade) - - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - is_open=False, - open_order_id='dry_run_sell_12345' - ) - Trade.session.add(trade) - - # Simulate prod entry - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='prod_buy_12345' - ) - Trade.session.add(trade) - + create_mock_trades(fee) assert len(Trade.get_open_trades()) == 2 From 1cd84157230c4416a106215460f7da8a0192cb28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 11:12:19 +0200 Subject: [PATCH 604/928] Move extract_trades_of_period to btanlaysis --- freqtrade/data/btanalysis.py | 10 +++++ freqtrade/tests/data/test_btanalysis.py | 53 ++++++++++++++++++++++--- scripts/plot_dataframe.py | 14 +------ 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index fb120e521..e1ddd1638 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -108,3 +108,13 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: trades = load_backtest_data(file) return trades + + +def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: + """ + Compare trades and backtested pair DataFrames to get trades performed on backtested period + :return: the DataFrame of a trades of period + """ + trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & + (trades['close_time'] <= dataframe.iloc[-1]['date'])] + return trades diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4a8babf1d..aa066557b 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,11 +1,18 @@ -import pytest from unittest.mock import MagicMock -from pandas import DataFrame -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data, load_trades -from freqtrade.data.history import make_testdata_path -from freqtrade.persistence import init, Trade -from freqtrade.tests.test_persistence import init_persistence, create_mock_trades +from arrow import Arrow +import pytest +from pandas import DataFrame, to_datetime + +from freqtrade.arguments import Arguments, TimeRange +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, + extract_trades_of_period, + load_backtest_data, load_trades) +from freqtrade.data.history import load_pair_history, make_testdata_path +from freqtrade.persistence import Trade, init +from freqtrade.strategy.interface import SellType +from freqtrade.tests.test_persistence import (create_mock_trades, + init_persistence) def test_load_backtest_data(): @@ -45,3 +52,37 @@ def test_load_trades_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns + + +def test_extract_trades_of_period(): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = load_pair_history(pair=pair, ticker_interval='1m', + datadir=None, timerange=timerange) + + # timerange = 2017-11-14 06:07 - 2017-11-14 22:58:00 + trades = DataFrame( + {'pair': [pair, pair, pair, pair], + 'profit_percent': [0.0, 0.1, -0.2, -0.5], + 'profit_abs': [0.0, 1, -2, -5], + 'open_time': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, + Arrow(2017, 11, 14, 9, 41, 0).datetime, + Arrow(2017, 11, 14, 14, 20, 0).datetime, + Arrow(2017, 11, 15, 3, 40, 0).datetime, + ], utc=True + ), + 'close_time': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime, + Arrow(2017, 11, 14, 10, 41, 0).datetime, + Arrow(2017, 11, 14, 15, 25, 0).datetime, + Arrow(2017, 11, 15, 3, 55, 0).datetime, + ], utc=True) + }) + trades1 = extract_trades_of_period(data, trades) + # First and last trade are dropped as they are out of range + assert len(trades1) == 2 + assert trades1.iloc[0].open_time == Arrow(2017, 11, 14, 9, 41, 0).datetime + assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime + assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime + assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime + diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 005148d3b..6d2e545ce 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -35,9 +35,10 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_trades -from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration +from freqtrade.plot.plotting import (extract_trades_of_period, generate_graph, + generate_plot_file) from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -119,17 +120,6 @@ def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: return dataframe -def extract_trades_of_period(dataframe, trades) -> pd.DataFrame: - """ - Compare trades and backtested pair DataFrames to get trades performed on backtested period - :return: the DataFrame of a trades of period - """ - # TODO: Document and move to btanalysis (?) - trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & - (trades['close_time'] <= dataframe.iloc[-1]['date'])] - return trades - - def analyse_and_plot_pairs(args: Namespace): """ From arguments provided in cli: From bf2c0390e7cc01020dbd10d2990a3606f75adaa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 12:54:27 +0200 Subject: [PATCH 605/928] Adjust some imports --- freqtrade/tests/conftest.py | 5 +++++ freqtrade/tests/data/test_btanalysis.py | 8 ++------ freqtrade/tests/test_persistence.py | 5 ----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..e956d89c4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -151,6 +151,11 @@ def patch_coinmarketcap(mocker) -> None: ) +@pytest.fixture(scope='function') +def init_persistence(default_conf): + persistence.init(default_conf['db_url'], default_conf['dry_run']) + + @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index aa066557b..6fa529394 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,15 +4,12 @@ from arrow import Arrow import pytest from pandas import DataFrame, to_datetime -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, extract_trades_of_period, load_backtest_data, load_trades) from freqtrade.data.history import load_pair_history, make_testdata_path -from freqtrade.persistence import Trade, init -from freqtrade.strategy.interface import SellType -from freqtrade.tests.test_persistence import (create_mock_trades, - init_persistence) +from freqtrade.tests.test_persistence import create_mock_trades def test_load_backtest_data(): @@ -85,4 +82,3 @@ def test_extract_trades_of_period(): assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime - diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 381f04bd1..32425ef7b 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -11,11 +11,6 @@ from freqtrade.persistence import Trade, clean_dry_run_db, init from freqtrade.tests.conftest import log_has -@pytest.fixture(scope='function') -def init_persistence(default_conf): - init(default_conf['db_url'], default_conf['dry_run']) - - def create_mock_trades(fee): """ Create some fake trades ... From 0300128cb82f4a109902ac78770345474a41793a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:35:15 +0200 Subject: [PATCH 606/928] Move plot-options to arguments.py --- freqtrade/arguments.py | 32 ++++++++++++++++++++++++++++++- freqtrade/tests/test_arguments.py | 14 ++++++++++++++ scripts/plot_dataframe.py | 30 +++-------------------------- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..fde372b63 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -55,7 +55,7 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if parsed_arg.config is None and not no_default_config: + if not no_default_config and parsed_arg.config is None: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg @@ -514,3 +514,33 @@ class Arguments(object): dest='erase', action='store_true' ) + + def plot_dataframe_options(self) -> None: + """ + Parses given arguments for plot_dataframe + """ + self.parser.add_argument( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. Separate ' + 'them with a comma. E.g: ema3,ema5 (default: %(default)s)', + type=str, + default='sma,ema3,ema5', + dest='indicators1', + ) + + self.parser.add_argument( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. Separate ' + 'them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', + type=str, + default='macd,macdsignal', + dest='indicators2', + ) + self.parser.add_argument( + '--plot-limit', + help='Specify tick limit for plotting - too high values cause huge files - ' + 'Default: %(default)s', + dest='plot_limit', + default=750, + type=int, + ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index afa42f287..d1b96a923 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -186,6 +186,20 @@ def test_download_data_options() -> None: assert args.exchange == 'binance' +def test_plot_dataframe_options() -> None: + args = [ + '--indicators1', 'sma10,sma100', + '--indicators2', 'macd,fastd,fastk', + '--plot-limit', '30', + ] + arguments = Arguments(args, '') + arguments.plot_dataframe_options() + args = arguments.parse_args(True) + assert args.indicators1 == "sma10,sma100" + assert args.indicators2 == "macd,fastd,fastk" + assert args.plot_limit == 30 + + def test_check_int_positive() -> None: assert Arguments.check_int_positive("3") == 3 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 6d2e545ce..d37d63c32 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -34,10 +34,10 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import load_trades, extract_trades_of_period from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (extract_trades_of_period, generate_graph, +from freqtrade.plot.plotting import (generate_graph, generate_plot_file) from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -171,31 +171,7 @@ def plot_parse_args(args: List[str]) -> Namespace: """ arguments = Arguments(args, 'Graph dataframe') arguments.scripts_options() - arguments.parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', - type=str, - default='sma,ema3,ema5', - dest='indicators1', - ) - - arguments.parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', - type=str, - default='macd,macdsignal', - dest='indicators2', - ) - arguments.parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', - dest='plot_limit', - default=750, - type=int, - ) + arguments.plot_dataframe_options() arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) arguments.backtesting_options(arguments.parser) From 3f04930f383272f588bd33d455b10f832cf16c18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:19:06 +0200 Subject: [PATCH 607/928] Require pairs argument --- freqtrade/arguments.py | 7 +++++++ freqtrade/tests/test_arguments.py | 17 +++++++++++++---- scripts/plot_dataframe.py | 5 +++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index fde372b63..cef38784d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -519,6 +519,13 @@ class Arguments(object): """ Parses given arguments for plot_dataframe """ + self.parser.add_argument( + '-p', '--pairs', + help='Show profits for only this pairs. Pairs are comma-separated.', + dest='pairs', + required=True, + default=None + ) self.parser.add_argument( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. Separate ' diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d1b96a923..d584a9e01 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -191,13 +191,22 @@ def test_plot_dataframe_options() -> None: '--indicators1', 'sma10,sma100', '--indicators2', 'macd,fastd,fastk', '--plot-limit', '30', + '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') arguments.plot_dataframe_options() - args = arguments.parse_args(True) - assert args.indicators1 == "sma10,sma100" - assert args.indicators2 == "macd,fastd,fastk" - assert args.plot_limit == 30 + pargs = arguments.parse_args(True) + assert pargs.indicators1 == "sma10,sma100" + assert pargs.indicators2 == "macd,fastd,fastk" + assert pargs.plot_limit == 30 + assert pargs.pairs == "UNITTEST/BTC" + + # Pop pairs argument + args = args[:-2] + arguments = Arguments(args, '') + arguments.plot_dataframe_options() + with pytest.raises(SystemExit, match=r'2'): + arguments.parse_args(True) def test_check_int_positive() -> None: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index d37d63c32..c44f2aa4b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -89,7 +89,7 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, ticker_interval=ticker_interval, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, - exchange=Exchange(_CONF), + exchange=exchange, live=live, ) @@ -132,6 +132,8 @@ def analyse_and_plot_pairs(args: Namespace): :return: None """ strategy, exchange, pairs = get_trading_env(args) + pairs = args.pairs.split(',') + # Set timerange to use timerange = Arguments.parse_timerange(args.timerange) ticker_interval = strategy.ticker_interval @@ -170,7 +172,6 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.scripts_options() arguments.plot_dataframe_options() arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) From 907c2f1e6b5a8ed53649640d09f62b5939d71236 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:31:24 +0200 Subject: [PATCH 608/928] Copy plot options to config --- freqtrade/configuration.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82d96313d..c63e99318 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -98,6 +98,9 @@ class Configuration(object): # Load Optimize configurations config = self._load_optimize_config(config) + # Add plotting options if available + config = self._load_plot_config(config) + # Set runmode if not self.runmode: # Handle real mode, infer dry/live from config @@ -332,6 +335,26 @@ class Configuration(object): return config + def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract information for sys.argv Plotting configuration + :return: configuration as dictionary + """ + + self._args_to_config(config, argname='pairs', + logstring='Using pairs {}') + + self._args_to_config(config, argname='indicators1', + logstring='Using indicators1: {}') + + self._args_to_config(config, argname='indicators2', + logstring='Using indicators2: {}') + + self._args_to_config(config, argname='plot_limit', + logstring='Limiting plot to: {}') + + return config + def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: """ Validate the configuration follow the Config Schema From 488bb971ffa25a6a277e4d494b098401d9c9d870 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 13:41:36 +0200 Subject: [PATCH 609/928] Get rid of global conf object --- scripts/plot_dataframe.py | 70 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index c44f2aa4b..e54a78124 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -35,47 +35,17 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_trades, extract_trades_of_period -from freqtrade.exchange import Exchange from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_graph, generate_plot_file) -from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) -_CONF: Dict[str, Any] = {} -def get_trading_env(args: Namespace): - """ - Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter - :return: Strategy - """ - global _CONF - - # Load the configuration - _CONF.update(setup_configuration(args, RunMode.BACKTEST)) - - pairs = args.pairs.split(',') - if pairs is None: - logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC') - exit() - - # Load the strategy - try: - strategy = StrategyResolver(_CONF).strategy - exchange = Exchange(_CONF) - except AttributeError: - logger.critical( - 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', - args.strategy - ) - exit() - - return [strategy, exchange, pairs] - - -def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, live: bool): +def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, + datadir: Path, refresh_pairs: bool, live: bool): """ Get tickers data for each pairs on live or local, option defined in args :return: dictionary of tickers. output format: {'pair': tickersdata} @@ -84,10 +54,10 @@ def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, ticker_interval = strategy.ticker_interval tickers = history.load_data( - datadir=Path(str(_CONF.get("datadir"))), + datadir=datadir, pairs=pairs, ticker_interval=ticker_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), + refresh_pairs=refresh_pairs, timerange=timerange, exchange=exchange, live=live, @@ -120,7 +90,7 @@ def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: return dataframe -def analyse_and_plot_pairs(args: Namespace): +def analyse_and_plot_pairs(config: Dict[str, Any]): """ From arguments provided in cli: -Initialise backtest env @@ -131,14 +101,20 @@ def analyse_and_plot_pairs(args: Namespace): -Generate plot files :return: None """ - strategy, exchange, pairs = get_trading_env(args) - pairs = args.pairs.split(',') + exchange_name = config.get('exchange', {}).get('name').title() + exchange = ExchangeResolver(exchange_name, config).exchange + + strategy = StrategyResolver(config).strategy + pairs = config["pairs"].split(',') # Set timerange to use - timerange = Arguments.parse_timerange(args.timerange) + timerange = Arguments.parse_timerange(config["timerange"]) ticker_interval = strategy.ticker_interval - tickers = get_tickers_data(strategy, exchange, pairs, timerange, args.live) + tickers = get_tickers_data(strategy, exchange, pairs, timerange, + datadir=Path(str(config.get("datadir"))), + refresh_pairs=config.get('refresh_pairs', False), + live=config.get("live", False)) pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 @@ -147,8 +123,8 @@ def analyse_and_plot_pairs(args: Namespace): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(db_url=args.db_url, - exportfilename=args.exportfilename) + trades = load_trades(db_url=config["db_url"], + exportfilename=config["exportfilename"]) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) @@ -156,8 +132,8 @@ def analyse_and_plot_pairs(args: Namespace): pair=pair, data=dataframe, trades=trades, - indicators1=args.indicators1.split(","), - indicators2=args.indicators2.split(",") + indicators1=config["indicators1"].split(","), + indicators2=config["indicators2"].split(",") ) generate_plot_file(fig, pair, ticker_interval) @@ -176,7 +152,11 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments.common_args_parser() arguments.optimizer_shared_options(arguments.parser) arguments.backtesting_options(arguments.parser) - return arguments.parse_args() + parsed_args = arguments.parse_args() + + # Load the configuration + config = setup_configuration(parsed_args, RunMode.BACKTEST) + return config def main(sysargv: List[str]) -> None: From 4b7dfc64c66afa33f2723be1214668b16f42f327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 14:03:55 +0200 Subject: [PATCH 610/928] Add test for generate_plot_file --- freqtrade/plot/plotting.py | 1 - freqtrade/tests/test_plotting.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b1e32c4fb..dcea3291f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,7 +1,6 @@ import logging from typing import List -import arrow import pandas as pd from pathlib import Path diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index e264ef6b3..b1dad5d5c 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -32,6 +32,7 @@ def generage_empty_figure(): vertical_spacing=0.0001, ) + def test_generate_row(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) @@ -155,3 +156,14 @@ def test_generate_graph_no_trades(default_conf, mocker): assert row_mock.call_count == 2 assert trades_mock.call_count == 1 + + +def test_generate_plot_file(mocker, caplog): + fig = generage_empty_figure() + plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) + generate_plot_file(fig, "UNITTEST/BTC", "5m") + + assert plot_mock.call_count == 1 + assert plot_mock.call_args[0][0] == fig + assert (plot_mock.call_args_list[0][1]['filename'] + == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") From fc3e3c468c3a79dacb05259f8b4c2cdeedfb3212 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:26:43 +0200 Subject: [PATCH 611/928] File existence is checked in load_backtest_data --- freqtrade/data/btanalysis.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e1ddd1638..4aecf8ecf 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -103,9 +103,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: elif exportfilename: - file = Path(exportfilename) - if file.exists(): - trades = load_backtest_data(file) + trades = load_backtest_data(Path(exportfilename)) return trades From 0eb109f8f7a01d8962f76cd89a111c9aa8ab3a7a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 19:53:48 +0200 Subject: [PATCH 612/928] Improve some tests --- freqtrade/arguments.py | 8 ++++---- freqtrade/tests/test_plotting.py | 23 +++++++++++++++++++++-- scripts/plot_dataframe.py | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index cef38784d..3f21709c9 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -528,8 +528,8 @@ class Arguments(object): ) self.parser.add_argument( '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a comma. E.g: ema3,ema5 (default: %(default)s)', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Separate them with a comma. E.g: ema3,ema5 (default: %(default)s)', type=str, default='sma,ema3,ema5', dest='indicators1', @@ -537,8 +537,8 @@ class Arguments(object): self.parser.add_argument( '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Separate them with a comma. E.g: macd,fastd,fastk (default: %(default)s)', type=str, default='macd,macdsignal', dest='indicators2', diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index b1dad5d5c..15ab698d8 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,8 +5,9 @@ from plotly import tools import plotly.graph_objs as go from copy import deepcopy -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import TimeRange from freqtrade.data import history +from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_graph, generate_plot_file, generate_row, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -71,8 +72,26 @@ def test_plot_trades(): # nothing happens when no trades are available fig = plot_trades(fig1, None) assert fig == fig1 + pair = "ADA/BTC" + filename = history.make_testdata_path(None) / "backtest-result_test.json" + trades = load_backtest_data(filename) + trades = trades.loc[trades['pair'] == pair] - # TODO: implement tests that do something + fig = plot_trades(fig, trades) + figure = fig1.layout.figure + + # Check buys - color, should be in first graph, ... + trade_buy = find_trace_in_fig_data(figure.data, "trade_buy") + assert isinstance(trade_buy, go.Scatter) + assert trade_buy.yaxis == 'y' + assert len(trades) == len(trade_buy.x) + assert trade_buy.marker.color == 'green' + + trade_sell = find_trace_in_fig_data(figure.data, "trade_sell") + assert isinstance(trade_sell, go.Scatter) + assert trade_sell.yaxis == 'y' + assert len(trades) == len(trade_sell.x) + assert trade_sell.marker.color == 'red' def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index e54a78124..54ce199f4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -141,7 +141,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): logger.info('End of ploting process %s plots generated', pair_counter) -def plot_parse_args(args: List[str]) -> Namespace: +def plot_parse_args(args: List[str]) -> Dict[str, Any]: """ Parse args passed to the script :param args: Cli arguments From 765eff23f018c6532fa169505d354f175f0f08b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jun 2019 20:14:31 +0200 Subject: [PATCH 613/928] Fix typo --- freqtrade/plot/plotting.py | 2 +- scripts/plot_dataframe.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index dcea3291f..94c0830bf 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -93,7 +93,7 @@ def generate_graph( ) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB - Volume will always be ploted in row2, so Row 1 and are to our disposal for custom indicators + Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators :param pair: Pair to Display on the graph :param data: OHLCV DataFrame containing indicators and buy/sell signals :param trades: All trades created diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 54ce199f4..eebe20be2 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -26,7 +26,6 @@ Example of usage: """ import logging import sys -from argparse import Namespace from pathlib import Path from typing import Any, Dict, List From 813c008af22d78a854c11f6dd04cda50e579b325 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 16 Jun 2019 21:37:43 +0300 Subject: [PATCH 614/928] setup_configuration() cleanup --- freqtrade/optimize/__init__.py | 10 +++------- freqtrade/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 475aaa82f..8b548eefe 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -5,8 +5,9 @@ from typing import Any, Dict from filelock import FileLock, Timeout from freqtrade import DependencyException, constants -from freqtrade.configuration import Configuration from freqtrade.state import RunMode +from freqtrade.utils import setup_utils_configuration + logger = logging.getLogger(__name__) @@ -17,12 +18,7 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: :param args: Cli args from Arguments() :return: Configuration """ - configuration = Configuration(args, method) - config = configuration.load_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' + config = setup_utils_configuration(args, method) if method == RunMode.BACKTEST: if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 324b54a4e..d550ef43c 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -10,15 +10,16 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: +def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: """ - Prepare the configuration for the Hyperopt module + Prepare the configuration for utils subcommands :param args: Cli args from Arguments() :return: Configuration """ configuration = Configuration(args, method) config = configuration.load_config() + config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials config['exchange']['key'] = '' config['exchange']['secret'] = '' From 195bf5a4ccdbfffb3c997e64332af7177f1ef9da Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 16 Jun 2019 21:37:59 +0300 Subject: [PATCH 615/928] tests adjusted --- freqtrade/tests/test_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index 7550efb23..a12b709d7 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,17 +1,18 @@ -from freqtrade.utils import setup_configuration, start_list_exchanges +from freqtrade.utils import setup_utils_configuration, start_list_exchanges from freqtrade.tests.conftest import get_args from freqtrade.state import RunMode import re -def test_setup_configuration(): +def test_setup_utils_configuration(): args = [ '--config', 'config.json.example', ] - config = setup_configuration(get_args(args), RunMode.OTHER) + config = setup_utils_configuration(get_args(args), RunMode.OTHER) assert "exchange" in config + assert config['exchange']['dry_run'] is True assert config['exchange']['key'] == '' assert config['exchange']['secret'] == '' From d217f32bbcde1b751ea45eceed79f50d267dfd71 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 17 Jun 2019 04:35:39 +0300 Subject: [PATCH 616/928] minor: fix typo in freqtradebot.py --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 471e9d218..e6eb74c21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -205,19 +205,19 @@ class FreqtradeBot(object): else: stake_amount = self.config['stake_amount'] - avaliable_amount = self.wallets.get_free(self.config['stake_currency']) + available_amount = self.wallets.get_free(self.config['stake_currency']) if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: logger.warning('Can\'t open a new trade: max number of trades is reached') return None - return avaliable_amount / (self.config['max_open_trades'] - open_trades) + return available_amount / (self.config['max_open_trades'] - open_trades) # Check if stake_amount is fulfilled - if avaliable_amount < stake_amount: + if available_amount < stake_amount: raise DependencyException( - f"Available balance({avaliable_amount} {self.config['stake_currency']}) is " + f"Available balance({available_amount} {self.config['stake_currency']}) is " f"lower than stake amount({stake_amount} {self.config['stake_currency']})" ) From 475e76b272593ab9bd96db00d8ca3173bf57d978 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 06:55:30 +0200 Subject: [PATCH 617/928] Add order_type to buy_notification --- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/rpc/test_rpc_telegram.py | 6 ++++-- freqtrade/tests/rpc/test_rpc_webhook.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 497c117ac..41d3ca0e9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -132,7 +132,7 @@ class Telegram(RPC): msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying {pair}\n" - "with limit `{limit:.8f}\n" + "at rate `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 46ef15f56..f1615d0d5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1188,6 +1188,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, + 'order_type': 'limit', 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', @@ -1195,7 +1196,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'with limit `0.00001099\n' \ + 'at rate `0.00001099\n' \ '(0.001000 BTC,0.000 USD)`' @@ -1339,6 +1340,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, + 'order_type': 'limit', 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', @@ -1346,7 +1348,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Bittrex:* Buying ETH/BTC\n' \ - 'with limit `0.00001099\n' \ + 'at rate `0.00001099\n' \ '(0.001000 BTC)`' diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index da7aec0a6..a9129529c 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -126,6 +126,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 0.005, + 'order_type': 'limit', 'stake_amount': 0.8, 'stake_amount_fiat': 500, 'stake_currency': 'BTC', From 557122921a876044aedccdf3ee54335d7e9b69c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 06:55:54 +0200 Subject: [PATCH 618/928] Add order_type to sell-notification --- freqtrade/freqtradebot.py | 7 ++++--- freqtrade/tests/rpc/test_rpc_telegram.py | 6 ++++++ freqtrade/tests/rpc/test_rpc_webhook.py | 1 + freqtrade/tests/test_freqtradebot.py | 5 +++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e6eb74c21..305a97ba6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -345,8 +345,8 @@ class FreqtradeBot(object): return False amount = stake_amount / buy_limit_requested - - order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], + order_type = self.strategy.order_types['buy'] + order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) order_id = order['id'] @@ -356,7 +356,6 @@ class FreqtradeBot(object): buy_limit_filled_price = buy_limit_requested if order_status == 'expired' or order_status == 'rejected': - order_type = self.strategy.order_types['buy'] order_tif = self.strategy.order_time_in_force['buy'] # return false if the order is not filled @@ -390,6 +389,7 @@ class FreqtradeBot(object): 'exchange': self.exchange.name.capitalize(), 'pair': pair_s, 'limit': buy_limit_filled_price, + 'order_type': order_type, 'stake_amount': stake_amount, 'stake_currency': stake_currency, 'fiat_currency': fiat_currency @@ -875,6 +875,7 @@ class FreqtradeBot(object): 'pair': trade.pair, 'gain': gain, 'limit': trade.close_rate_requested, + 'order_type': self.strategy.order_types['sell'], 'amount': trade.amount, 'open_rate': trade.open_rate, 'current_rate': current_rate, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index f1615d0d5..9bc2ba06e 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -756,6 +756,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, '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, @@ -810,6 +811,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, '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, @@ -855,6 +857,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker 'gain': 'loss', 'limit': 1.098e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.098e-05, 'profit_amount': -5.91e-06, @@ -1218,6 +1221,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'market', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, @@ -1243,6 +1247,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'market', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, @@ -1369,6 +1374,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'gain': 'loss', 'limit': 3.201e-05, 'amount': 1333.3333333333335, + 'order_type': 'limit', 'open_rate': 7.5e-05, 'current_rate': 3.201e-05, 'profit_amount': -0.05746268, diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index a9129529c..a2dcd9b31 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -74,6 +74,7 @@ def test_send_msg(default_conf, mocker): 'gain': "profit", 'limit': 0.005, 'amount': 0.8, + 'order_type': 'limit', 'open_rate': 0.004, 'current_rate': 0.005, 'profit_amount': 0.001, diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 6566e4036..ceb695423 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1994,6 +1994,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc '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, @@ -2040,6 +2041,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, '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, @@ -2094,6 +2096,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'gain': 'loss', 'limit': 1.08801e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -1.498e-05, @@ -2265,6 +2268,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, '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, @@ -2312,6 +2316,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, + 'order_type': 'market', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, From 06afb3f155420c85221998f7e3b41b8a609147cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 07:01:17 +0200 Subject: [PATCH 619/928] Don't use "limit" for sell-orders either --- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/rpc/test_rpc_telegram.py | 6 +++--- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 41d3ca0e9..3eb060074 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -144,7 +144,7 @@ class Telegram(RPC): msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) message = ("*{exchange}:* Selling {pair}\n" - "*Limit:* `{limit:.8f}`\n" + "*Rate:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 9bc2ba06e..b34e214af 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1232,7 +1232,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Limit:* `0.00003201`\n' + '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1257,7 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' - '*Limit:* `0.00003201`\n' + '*Rate:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1385,7 +1385,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] \ == '*Binance:* Selling KEY/ETH\n' \ - '*Limit:* `0.00003201`\n' \ + '*Rate:* `0.00003201`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index ceb695423..65225689b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2316,7 +2316,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, - 'order_type': 'market', + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, From 7cd36239a461b332bc53699c3a66a17a0ca1a968 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 07:03:33 +0200 Subject: [PATCH 620/928] UPdate documentation with new value --- docs/webhook-config.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 811b57f9b..112f8a77e 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -43,6 +43,7 @@ Possible parameters are: * `stake_amount` * `stake_currency` * `fiat_currency` +* `order_type` ### Webhooksell @@ -61,6 +62,7 @@ Possible parameters are: * `stake_currency` * `fiat_currency` * `sell_reason` +* `order_type` ### Webhookstatus From ba4890d303348f29af540bfb9dff05d6f9f52f65 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Jun 2019 14:34:45 +0200 Subject: [PATCH 621/928] Fix tests on windows --- freqtrade/tests/conftest.py | 3 ++- freqtrade/tests/strategy/test_strategy.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..7424d5ece 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -5,6 +5,7 @@ import re from copy import deepcopy from datetime import datetime from functools import reduce +from pathlib import Path from typing import List from unittest.mock import MagicMock, PropertyMock @@ -860,7 +861,7 @@ def tickers(): @pytest.fixture def result(): - with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: + with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file: return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True) # FIX: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index b96f9c79e..15d1c18ef 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -75,14 +75,10 @@ def test_load_strategy_byte64(result): def test_load_strategy_invalid_directory(result, caplog): resolver = StrategyResolver() - extra_dir = path.join('some', 'path') + extra_dir = Path.cwd() / 'some/path' resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) - assert ( - 'freqtrade.resolvers.strategy_resolver', - logging.WARNING, - 'Path "{}" does not exist'.format(extra_dir), - ) in caplog.record_tuples + assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) From 25755f6adfb2e4307ed478ff9fbf4ab0dd051a20 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:13 +0000 Subject: [PATCH 622/928] Update ccxt from 1.18.667 to 1.18.725 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 8b44e9d28..ed3f052a9 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.667 +ccxt==1.18.725 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.14.2 From 6973087d5b2dad8e74263eaeebf86cd17bf631a1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:14 +0000 Subject: [PATCH 623/928] Update pytest from 4.6.2 to 4.6.3 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 315033847..a8ba6f6c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.6.2 +pytest==4.6.3 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 0e7ea1dadad4d57eb355b0fa799e04b973596882 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Jun 2019 15:23:15 +0000 Subject: [PATCH 624/928] Update coveralls from 1.8.0 to 1.8.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a8ba6f6c0..21bb899b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,5 @@ pytest==4.6.3 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 -coveralls==1.8.0 +coveralls==1.8.1 mypy==0.701 From 6f950bbd66eb02a2a99df48dd791999a483679f8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 18 Jun 2019 01:46:30 +0300 Subject: [PATCH 625/928] json validator cosmetics --- freqtrade/configuration.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82d96313d..01bbb955d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -34,13 +34,17 @@ def set_loggers(log_level: int = 0) -> None: logging.getLogger('telegram').setLevel(logging.INFO) -def _extend_with_default(validator_class): - validate_properties = validator_class.VALIDATORS["properties"] +def _extend_validator(validator_class): + """ + Extended validator for the Freqtrade configuration JSON Schema. + Currently it only handles defaults for subschemas. + """ + validate_properties = validator_class.VALIDATORS['properties'] def set_defaults(validator, properties, instance, schema): for prop, subschema in properties.items(): - if "default" in subschema: - instance.setdefault(prop, subschema["default"]) + if 'default' in subschema: + instance.setdefault(prop, subschema['default']) for error in validate_properties( validator, properties, instance, schema, @@ -48,11 +52,11 @@ def _extend_with_default(validator_class): yield error return validators.extend( - validator_class, {"properties": set_defaults}, + validator_class, {'properties': set_defaults} ) -ValidatorWithDefaults = _extend_with_default(Draft4Validator) +FreqtradeValidator = _extend_validator(Draft4Validator) class Configuration(object): @@ -339,7 +343,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - ValidatorWithDefaults(constants.CONF_SCHEMA).validate(conf) + FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) return conf except ValidationError as exception: logger.critical( From 8c40a406b6830e13bbc76a96c51ca57fa5b17c10 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 01:53:38 +0300 Subject: [PATCH 626/928] arguments cleanup --- freqtrade/arguments.py | 174 +++++++++++++++--------------- freqtrade/tests/test_arguments.py | 4 +- scripts/download_backtest_data.py | 3 +- scripts/plot_dataframe.py | 35 ++---- scripts/plot_profit.py | 9 +- 5 files changed, 102 insertions(+), 123 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..5b65443de 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -33,7 +33,8 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: - self.common_args_parser() + self.common_options() + self.main_options() self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -60,62 +61,65 @@ class Arguments(object): return parsed_arg - def common_args_parser(self) -> None: + def common_options(self) -> None: """ - Parses given common arguments and returns them as a parsed object. + Parses arguments that are common for the main Freqtrade, all subcommands and scripts. """ - self.parser.add_argument( + parser = self.parser + + parser.add_argument( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', dest='loglevel', default=0, ) - self.parser.add_argument( + parser.add_argument( '--logfile', help='Log to the file specified', dest='logfile', - type=str, - metavar='FILE' + metavar='FILE', ) - self.parser.add_argument( + parser.add_argument( '--version', action='version', version=f'%(prog)s {__version__}' ) - self.parser.add_argument( + parser.add_argument( '-c', '--config', help='Specify configuration file (default: %(default)s). ' 'Multiple --config options may be used.', dest='config', action='append', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '-d', '--datadir', help='Path to backtest data.', dest='datadir', - default=None, - type=str, metavar='PATH', ) - self.parser.add_argument( + + def main_options(self) -> None: + """ + Parses arguments for the main Freqtrade. + """ + parser = self.parser + + parser.add_argument( '-s', '--strategy', help='Specify strategy class name (default: %(default)s).', dest='strategy', default='DefaultStrategy', - type=str, metavar='NAME', ) - self.parser.add_argument( + parser.add_argument( '--strategy-path', help='Specify additional strategy lookup path.', dest='strategy_path', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '--dynamic-whitelist', help='Dynamically generate and update whitelist' ' based on 24h BaseVolume (default: %(const)s).' @@ -126,52 +130,46 @@ class Arguments(object): metavar='INT', nargs='?', ) - self.parser.add_argument( + parser.add_argument( '--db-url', help='Override trades database URL, this is useful if dry_run is enabled' ' or in custom deployments (default: %(default)s).', dest='db_url', - type=str, metavar='PATH', ) - self.parser.add_argument( + parser.add_argument( '--sd-notify', help='Notify systemd service manager.', action='store_true', dest='sd_notify', ) - @staticmethod - def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: + def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: """ - Parses given common arguments for Backtesting, Edge and Hyperopt modules. + Parses arguments common for Backtesting, Edge and Hyperopt modules. :param parser: - :return: """ + parser = subparser or self.parser + parser.add_argument( '-i', '--ticker-interval', help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', dest='ticker_interval', - type=str, ) parser.add_argument( '--timerange', help='Specify what timerange of data to use.', - default=None, - type=str, dest='timerange', ) parser.add_argument( '--max_open_trades', help='Specify max_open_trades to use.', - default=None, type=int, dest='max_open_trades', ) parser.add_argument( '--stake_amount', help='Specify stake_amount.', - default=None, type=float, dest='stake_amount', ) @@ -184,11 +182,12 @@ class Arguments(object): dest='refresh_pairs', ) - @staticmethod - def backtesting_options(parser: argparse.ArgumentParser) -> None: + def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Backtesting module. """ + parser = subparser or self.parser + parser.add_argument( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', @@ -224,8 +223,6 @@ class Arguments(object): '--export', help='Export backtest results, argument are: trades. ' 'Example --export=trades', - type=str, - default=None, dest='export', ) parser.add_argument( @@ -234,37 +231,36 @@ class Arguments(object): requires --export to be set as well\ Example --export-filename=user_data/backtest_data/backtest_today.json\ (default: %(default)s)', - type=str, default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), dest='exportfilename', metavar='PATH', ) - @staticmethod - def edge_options(parser: argparse.ArgumentParser) -> None: + def edge_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Edge module. """ + parser = subparser or self.parser + parser.add_argument( '--stoplosses', help='Defines a range of stoploss against which edge will assess the strategy ' 'the format is "min,max,step" (without any space).' 'example: --stoplosses=-0.01,-0.1,-0.001', - type=str, dest='stoploss_range', ) - @staticmethod - def hyperopt_options(parser: argparse.ArgumentParser) -> None: + def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for Hyperopt module. """ + parser = subparser or self.parser + parser.add_argument( '--customhyperopt', help='Specify hyperopt class name (default: %(default)s).', dest='hyperopt', default=constants.DEFAULT_HYPEROPT, - type=str, metavar='NAME', ) parser.add_argument( @@ -321,7 +317,6 @@ class Arguments(object): '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', dest='hyperopt_random_state', - default=None, type=Arguments.check_int_positive, metavar='INT', ) @@ -335,11 +330,12 @@ class Arguments(object): metavar='INT', ) - @staticmethod - def list_exchanges_options(parser: argparse.ArgumentParser) -> None: + def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses given arguments for the list-exchanges command. """ + parser = subparser or self.parser + parser.add_argument( '-1', '--one-column', help='Print exchanges in one column', @@ -349,7 +345,7 @@ class Arguments(object): def _build_subcommands(self) -> None: """ - Builds and attaches all subcommands + Builds and attaches all subcommands. :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge @@ -360,19 +356,19 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.optimizer_shared_options(backtesting_cmd) + self.common_optimize_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.optimizer_shared_options(edge_cmd) + self.common_optimize_options(edge_cmd) self.edge_options(edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.optimizer_shared_options(hyperopt_cmd) + self.common_optimize_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) # Add list-exchanges subcommand @@ -437,69 +433,43 @@ class Arguments(object): ) return uint - def scripts_options(self) -> None: + def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: """ - Parses given arguments for scripts. + Parses arguments common for scripts. """ - self.parser.add_argument( + parser = subparser or self.parser + + parser.add_argument( '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', - default=None ) def download_data_options(self) -> None: """ - Parses given arguments for testdata download + Parses given arguments for testdata download script """ - self.parser.add_argument( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - dest='loglevel', - default=0, - ) - self.parser.add_argument( - '--logfile', - help='Log to the file specified', - dest='logfile', - type=str, - metavar='FILE', - ) - self.parser.add_argument( - '-c', '--config', - help='Specify configuration file (default: %(default)s). ' - 'Multiple --config options may be used.', - dest='config', - action='append', - type=str, - metavar='PATH', - ) - self.parser.add_argument( - '-d', '--datadir', - help='Path to backtest data.', - dest='datadir', - metavar='PATH', - ) - self.parser.add_argument( + parser = self.parser + + parser.add_argument( '--pairs-file', help='File containing a list of pairs to download.', dest='pairs_file', metavar='FILE', ) - self.parser.add_argument( + parser.add_argument( '--days', help='Download data for given number of days.', dest='days', type=Arguments.check_int_positive, metavar='INT', ) - self.parser.add_argument( + parser.add_argument( '--exchange', help='Exchange name (default: %(default)s). Only valid if no config is provided.', dest='exchange', ) - self.parser.add_argument( + parser.add_argument( '-t', '--timeframes', help='Specify which tickers to download. Space separated list. \ Default: %(default)s.', @@ -508,9 +478,39 @@ class Arguments(object): nargs='+', dest='timeframes', ) - self.parser.add_argument( + parser.add_argument( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', dest='erase', action='store_true' ) + + def plot_dataframe_options(self) -> None: + """ + Parses given arguments for plot dataframe script + """ + parser = self.parser + + parser.add_argument( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. Separate ' + 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', + default='sma,ema3,ema5', + dest='indicators1', + ) + + parser.add_argument( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. Separate ' + 'them with a coma. E.g: fastd,fastk (default: %(default)s)', + default='macd,macdsignal', + dest='indicators2', + ) + parser.add_argument( + '--plot-limit', + help='Specify tick limit for plotting - too high values cause huge files - ' + 'Default: %(default)s', + dest='plot_limit', + default=750, + type=int, + ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index afa42f287..7a5047507 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -47,9 +47,9 @@ def test_parse_args_verbose() -> None: assert args.loglevel == 1 -def test_scripts_options() -> None: +def test_common_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments.scripts_options() + arguments.common_scripts_options() args = arguments.get_parsed_arg() assert args.pairs == 'ETH/BTC' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 6263d0e2f..dd4627c14 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -20,7 +20,8 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' -arguments = Arguments(sys.argv[1:], 'download utility') +arguments = Arguments(sys.argv[1:], 'Download backtest data') +arguments.common_options() arguments.download_data_options() # Do not read the default config if config is not specified diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4f8ffb32b..19a4d3506 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -349,35 +349,12 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.scripts_options() - arguments.parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', - type=str, - default='sma,ema3,ema5', - dest='indicators1', - ) - - arguments.parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', - type=str, - default='macd,macdsignal', - dest='indicators2', - ) - arguments.parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', - dest='plot_limit', - default=750, - type=int, - ) - arguments.common_args_parser() - arguments.optimizer_shared_options(arguments.parser) - arguments.backtesting_options(arguments.parser) + arguments.common_options() + arguments.main_options() + arguments.common_optimize_options() + arguments.backtesting_options() + arguments.common_scripts_options() + arguments.plot_dataframe_options() return arguments.parse_args() diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 5f7d42c87..fd98c120c 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -206,10 +206,11 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.scripts_options() - arguments.common_args_parser() - arguments.optimizer_shared_options(arguments.parser) - arguments.backtesting_options(arguments.parser) + arguments.common_options() + arguments.main_options() + arguments.common_optimize_options() + arguments.backtesting_options() + arguments.common_scripts_options() return arguments.parse_args() From c6fed4e4931d1b9e4cbab813a1b1eb48cdc8c108 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 02:42:29 +0300 Subject: [PATCH 627/928] make flake happy --- freqtrade/arguments.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 5b65443de..01182f2df 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -493,16 +493,16 @@ class Arguments(object): parser.add_argument( '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Separate them with a coma. E.g: ema3,ema5 (default: %(default)s)', default='sma,ema3,ema5', dest='indicators1', ) parser.add_argument( '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Separate them with a coma. E.g: fastd,fastk (default: %(default)s)', default='macd,macdsignal', dest='indicators2', ) From 860e05636699bdd890fb4646418da7c1702a06bf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 19 Jun 2019 02:49:12 +0300 Subject: [PATCH 628/928] --datadir is now handled in arguments.common_options() --- freqtrade/tests/test_arguments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 7a5047507..89c910a28 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -178,6 +178,7 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') + arguments.common_options() arguments.download_data_options() args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' From 0866b5f29f37545e85dc642d595a0a96ee045eaf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 00:04:11 +0300 Subject: [PATCH 629/928] allow reading config from stdin --- docs/bot-usage.md | 3 ++- freqtrade/arguments.py | 5 +++-- freqtrade/configuration.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index cb98e1ea5..b215d7b7c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -26,7 +26,8 @@ optional arguments: --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple - --config options may be used. + --config options may be used. Can be set to '-' to + read config from stdin. -d PATH, --datadir PATH Path to backtest data. -s NAME, --strategy NAME diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 35d388432..0e92f8845 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -85,8 +85,9 @@ class Arguments(object): ) self.parser.add_argument( '-c', '--config', - help='Specify configuration file (default: %(default)s). ' - 'Multiple --config options may be used.', + help="Specify configuration file (default: %(default)s). " + "Multiple --config options may be used. " + "Can be set to '-' to read config from stdin.", dest='config', action='append', type=str, diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 01bbb955d..47b4bb8f7 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -79,6 +79,7 @@ class Configuration(object): # Now expecting a list of config filenames here, not a string for path in self.args.config: logger.info('Using config: %s ...', path) + # Merge config options, overwriting old values config = deep_merge_dicts(self._load_config_file(path), config) @@ -118,7 +119,8 @@ class Configuration(object): :return: configuration as dictionary """ try: - with open(path) as file: + # Read config from stdin if requested in the options + with open(path) if path != '-' else sys.stdin as file: conf = json.load(file) except FileNotFoundError: raise OperationalException( From a8efb1e1c8f52d290fa7c4cc1c616edfabd72195 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 03:26:02 +0300 Subject: [PATCH 630/928] test for #1948 added --- freqtrade/tests/strategy/test_interface.py | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index e384003dc..6409123e7 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -185,6 +185,39 @@ def test_min_roi_reached2(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) +def test_min_roi_reached3(default_conf, fee) -> None: + + # test for issue #1948 + min_roi = {20: 0.07, + 30: 0.05, + 55: 0.30, + } + strategy = DefaultStrategy(default_conf) + strategy.minimal_roi = min_roi + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + ) + + assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime) + assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime) + assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime) + assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime) + + # Should not trigger with 20% profit since after 55 minutes only 30% is active. + assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime) + assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) + + def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 144e053a4e1349d9037317c3a697e77983484926 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 20 Jun 2019 03:26:25 +0300 Subject: [PATCH 631/928] fix for #1948 --- freqtrade/strategy/interface.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 68e0a7b37..353ae64b4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -357,13 +357,13 @@ class IStrategy(ABC): # Check if time matches and current rate is above threshold trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - # Get highest entry in ROI dict where key >= trade-duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) + # Get highest entry in ROI dict where key <= trade-duration + roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) + if not roi_list: + return False + roi_entry = max(roi_list) threshold = self.minimal_roi[roi_entry] - if current_profit > threshold: - return True - - return False + return current_profit > threshold def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ From 911e71cd9b5c87c0c04930c8324966296ec06ad0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:30:05 +0200 Subject: [PATCH 632/928] remove redundant test-functions --- freqtrade/tests/conftest.py | 12 ++++++++ freqtrade/tests/test_freqtradebot.py | 41 +++------------------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 034cb5f8b..c857d33e3 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -110,11 +110,23 @@ def patch_freqtradebot(mocker, config) -> None: def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: FreqtradeBot + """ patch_freqtradebot(mocker, config) return FreqtradeBot(config) def get_patched_worker(mocker, config) -> Worker: + """ + This function patches _init_modules() to not call dependencies + :param mocker: a Mocker object to apply patches + :param config: Config to pass to the bot + :return: Worker + """ patch_freqtradebot(mocker, config) return Worker(args=None, config=config) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 65225689b..31c4a791a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -19,47 +19,14 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, - patch_exchange, patch_get_signal, +from freqtrade.tests.conftest import (get_patched_freqtradebot, + get_patched_worker, log_has, log_has_re, + patch_edge, patch_exchange, + patch_freqtradebot, patch_get_signal, patch_wallet) from freqtrade.worker import Worker -# Functions for recurrent object patching -def patch_freqtradebot(mocker, config) -> None: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: None - """ - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - patch_exchange(mocker) - - -def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: FreqtradeBot - """ - patch_freqtradebot(mocker, config) - return FreqtradeBot(config) - - -def get_patched_worker(mocker, config) -> Worker: - """ - This function patches _init_modules() to not call dependencies - :param mocker: a Mocker object to apply patches - :param config: Config to pass to the bot - :return: Worker - """ - patch_freqtradebot(mocker, config) - return Worker(args=None, config=config) - - def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests From dd379c4192771dd7d56a80be162862b911906986 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:32:46 +0200 Subject: [PATCH 633/928] Cancelling stoploss order should not kill the bot --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 305a97ba6..0c6ebdeff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -843,7 +843,10 @@ class FreqtradeBot(object): # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + try: + self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + except InvalidOrderException: + 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), From a8dcfc05c58d7d74a20f79122db4b079f1629e9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:36:39 +0200 Subject: [PATCH 634/928] Add test to verify InvalidOrder is handled correctly --- freqtrade/tests/test_freqtradebot.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 31c4a791a..5229eb2c6 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2075,6 +2075,35 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe } == last_msg +def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, markets, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + sellmock = MagicMock() + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets), + sell=sellmock + ) + + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + patch_get_signal(freqtrade) + freqtrade.create_trade() + + trade = Trade.query.first() + Trade.session = MagicMock() + + freqtrade.config['dry_run'] = False + trade.stoploss_order_id = "abcd" + + freqtrade.execute_sell(trade=trade, limit=1234, + sell_reason=SellType.STOP_LOSS) + assert sellmock.call_count == 1 + assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples) + + def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: From 63640518da73fa8722dc70d5b7f73c3db5e69305 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:56:58 +0200 Subject: [PATCH 635/928] Gracefully handle errosr when cancelling stoploss orders fixes #1933 --- freqtrade/freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0c6ebdeff..c601465d9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -691,13 +691,22 @@ class FreqtradeBot(object): # cancelling the current stoploss on exchange first logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' 'in order to add another one ...', order['id']) - if self.exchange.cancel_order(order['id'], trade.pair): + try: + self.exchange.cancel_order(order['id'], trade.pair) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {order['id']} " + f"for pair {trade.pair}") + + try: # creating the new one stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 )['id'] trade.stoploss_order_id = str(stoploss_order_id) + except DependencyException: + logger.exception(f"Could create trailing stoploss order " + f"for pair {trade.pair}.") def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: From 89ba649ddb7e053fae0350e98689d43f9ade004e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jun 2019 20:57:15 +0200 Subject: [PATCH 636/928] Test handling errors while trailing stop loss --- freqtrade/tests/test_freqtradebot.py | 77 ++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 5229eb2c6..87b344853 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -22,8 +22,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_freqtradebot, patch_get_signal, - patch_wallet) + patch_get_signal, patch_wallet) from freqtrade.worker import Worker @@ -1143,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, stop_price=0.00002344 * 0.95) +def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, + markets, limit_buy_order, + limit_sell_order) -> None: + # When trailing stoploss is set + stoploss_limit = MagicMock(return_value={'id': 13434334}) + patch_exchange(mocker) + + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + markets=PropertyMock(return_value=markets), + stoploss_limit=stoploss_limit + ) + + # enabling TSL + default_conf['trailing_stop'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = -0.05 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + patch_get_signal(freqtrade) + freqtrade.create_trade() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = "abcd" + trade.stop_loss = 0.2 + trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None) + + stoploss_order_hanging = { + 'id': "abcd", + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.1' + } + } + mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", + caplog.record_tuples) + + # Still try to create order + assert stoploss_limit.call_count == 1 + + # Fail creating stoploss order + caplog.clear() + cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) + mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert cancel_mock.call_count == 1 + assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*", + caplog.record_tuples) + + def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: @@ -2075,7 +2145,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe } == last_msg -def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, markets, caplog) -> None: +def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, + markets, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) sellmock = MagicMock() From f907a487c87c2d3ee1ddc30066d2cfdabacda48d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:06:54 +0200 Subject: [PATCH 637/928] make ticker_interval available to hyperopt functions --- docs/hyperopt.md | 7 ++++++- freqtrade/resolvers/hyperopt_resolver.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 79ea4771b..15b02b56f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -12,7 +12,7 @@ and still take a long time. ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at -an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py) +an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py) Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. @@ -71,6 +71,11 @@ Place the corresponding settings into the following methods The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. +#### Using ticker-interval as part of the Strategy + +The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`. +In the case of the linked sample-value this would be `SampleHyperOpts.ticker_interval`. + ## Solving a Mystery Let's say you are curious: should you use MACD crossings or lower Bollinger diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index e7683bc78..638148ee2 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -32,6 +32,9 @@ class HyperOptResolver(IResolver): hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) + # Assign ticker_interval to be used in hyperopt + self.hyperopt.__class__.ticker_interval = config.get('ticker_interval') + if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.") From 1a27ae8a817632a446d0a8b139d79055ff5120f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:07:39 +0200 Subject: [PATCH 638/928] Add tests to verify that ticker_interval is there --- freqtrade/tests/optimize/test_hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index baa5da545..c40baccbc 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -167,6 +167,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: "Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples) assert log_has("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples) + assert hasattr(x, "ticker_interval") def test_start(mocker, default_conf, caplog) -> None: From 7a0d86660e5d02658c0201203bcd503d14add826 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 07:10:30 +0200 Subject: [PATCH 639/928] Mypy type errors --- freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/resolvers/hyperopt_resolver.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 622de3015..08823ece0 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -20,6 +20,7 @@ class IHyperOpt(ABC): stoploss -> float: optimal stoploss designed for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy """ + ticker_interval: str @staticmethod @abstractmethod diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 638148ee2..9333bb09a 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -33,7 +33,7 @@ class HyperOptResolver(IResolver): self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt - self.hyperopt.__class__.ticker_interval = config.get('ticker_interval') + self.hyperopt.__class__.ticker_interval = str(config['ticker_interval']) if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " From a581ca66bf33e0e9fa4b3a36eea0b56804fd33df Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 19:31:18 +0200 Subject: [PATCH 640/928] Adapt test after merging develop --- freqtrade/arguments.py | 1 + freqtrade/tests/test_arguments.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 3075dd0fe..59123a14b 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -444,6 +444,7 @@ class Arguments(object): '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', + required=True ) def download_data_options(self) -> None: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 78ca9d055..4c2bf708d 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -195,6 +195,7 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') + arguments.common_scripts_options() arguments.plot_dataframe_options() pargs = arguments.parse_args(True) assert pargs.indicators1 == "sma10,sma100" @@ -205,6 +206,7 @@ def test_plot_dataframe_options() -> None: # Pop pairs argument args = args[:-2] arguments = Arguments(args, '') + arguments.common_scripts_options() arguments.plot_dataframe_options() with pytest.raises(SystemExit, match=r'2'): arguments.parse_args(True) From db17b20e26a563048838d98ca5c6cc4f35381fc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jun 2019 20:21:03 +0200 Subject: [PATCH 641/928] Don't require pairs but fall back to pair_whitelist instead --- freqtrade/arguments.py | 1 - freqtrade/tests/test_arguments.py | 8 -------- scripts/plot_dataframe.py | 5 ++++- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 59123a14b..3075dd0fe 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -444,7 +444,6 @@ class Arguments(object): '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', dest='pairs', - required=True ) def download_data_options(self) -> None: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 4c2bf708d..d9292bdb5 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -203,14 +203,6 @@ def test_plot_dataframe_options() -> None: assert pargs.plot_limit == 30 assert pargs.pairs == "UNITTEST/BTC" - # Pop pairs argument - args = args[:-2] - arguments = Arguments(args, '') - arguments.common_scripts_options() - arguments.plot_dataframe_options() - with pytest.raises(SystemExit, match=r'2'): - arguments.parse_args(True) - def test_check_int_positive() -> None: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a17076085..4aacc99dd 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -104,7 +104,10 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): exchange = ExchangeResolver(exchange_name, config).exchange strategy = StrategyResolver(config).strategy - pairs = config["pairs"].split(',') + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] # Set timerange to use timerange = Arguments.parse_timerange(config["timerange"]) From de38aea16439ab7e92c90da69ed6f0505224b371 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 15:45:20 +0200 Subject: [PATCH 642/928] Fix sequence of loading trades --- freqtrade/data/btanalysis.py | 38 +++++++++++++++++++------------- freqtrade/plot/plotting.py | 2 ++ freqtrade/tests/test_plotting.py | 3 ++- scripts/plot_dataframe.py | 5 +++-- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 4aecf8ecf..5efb10433 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -73,37 +73,45 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int return df_final[df_final['pair'] > max_open_trades] -def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: +def load_trades_from_db(db_url: str) -> pd.DataFrame: """ - Load trades, either from a DB (using dburl) or via a backtest export file. + Load trades from a DB (using dburl) :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) - :param exportfilename: Path to a file exported from backtesting :returns: Dataframe containing Trades """ - timeZone = pytz.UTC - trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + persistence.init(db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", + "open_rate", "close_rate", "duration"] - if db_url: - persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration"] - - for x in Trade.query.all(): - logger.info("date: {}".format(x.open_date)) + for x in Trade.query.all(): + logger.info("date: {}".format(x.open_date)) trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date.replace(tzinfo=timeZone), - t.close_date.replace(tzinfo=timeZone) if t.close_date else None, + t.open_date.replace(tzinfo=pytz.UTC), + t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.open_rate, t.close_rate, t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) for t in Trade.query.all()], columns=columns) - elif exportfilename: + return trades + +def load_trades(exportfilename: str = None, db_url: str = None) -> pd.DataFrame: + """ + Load trades, either from a DB (using dburl) or via a backtest export file. + :param exportfilename: Path to a file exported from backtesting + :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :returns: Dataframe containing Trades + """ + + trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) + if exportfilename: trades = load_backtest_data(Path(exportfilename)) + elif db_url: + trades = load_trades_from_db(db_url) return trades diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 94c0830bf..c058f7fb2 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -81,6 +81,8 @@ def plot_trades(fig, trades: pd.DataFrame): ) fig.append_trace(trade_buys, 1, 1) fig.append_trace(trade_sells, 1, 1) + else: + logger.warning("No trades found.") return fig diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 15ab698d8..ec81b93b8 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -67,11 +67,12 @@ def test_generate_row(default_conf, caplog): assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) -def test_plot_trades(): +def test_plot_trades(caplog): fig1 = generage_empty_figure() # nothing happens when no trades are available fig = plot_trades(fig1, None) assert fig == fig1 + assert log_has("No trades found.", caplog.record_tuples) pair = "ADA/BTC" filename = history.make_testdata_path(None) / "backtest-result_test.json" trades = load_backtest_data(filename) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4aacc99dd..99ba60d40 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -125,8 +125,9 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = load_trades(db_url=config["db_url"], - exportfilename=config["exportfilename"]) + trades = load_trades(exportfilename=config["exportfilename"], + db_url=config["db_url"], + ) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) From 8758218b09152f90286dba5fa0bfafa01441ea29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:18:22 +0200 Subject: [PATCH 643/928] Add data-analysis documentation --- docs/backtesting.md | 18 +----------------- docs/data-analysis.md | 44 +++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 docs/data-analysis.md diff --git a/docs/backtesting.md b/docs/backtesting.md index 5a25bc255..8d8ea8030 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -221,24 +221,8 @@ strategies, your configuration, and the crypto-currency you have set up. ### Further backtest-result analysis To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). -You can then load the trades to perform further analysis. +You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section. -A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. - -Freqtrade provides an easy to load the backtest results, which is `load_backtest_data` - and takes a path to the backtest-results file. - -``` python -from freqtrade.data.btanalysis import load_backtest_data -df = load_backtest_data("user_data/backtest-result.json") - -# Show value-counts per pair -df.groupby("pair")["sell_reason"].value_counts() - -``` - -This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable. - -If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it. ## Backtesting multiple strategies diff --git a/docs/data-analysis.md b/docs/data-analysis.md new file mode 100644 index 000000000..ef3a02355 --- /dev/null +++ b/docs/data-analysis.md @@ -0,0 +1,44 @@ +# Analyzing bot data + +After performing backtests, or after running the bot for some time, it will be interresting to analyze the results your bot generated. + +A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. + +The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. + +## Backtesting + +To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). +You can then load the trades to perform further analysis. + +A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. + +Freqtrade provides an easy to load the backtest results, which is `load_backtest_data` - and takes a path to the backtest-results file. + +``` python +from freqtrade.data.btanalysis import load_backtest_data +df = load_backtest_data("user_data/backtest-result.json") + +# Show value-counts per pair +df.groupby("pair")["sell_reason"].value_counts() + +``` + +This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable. + +If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it. + +## Live data + +To analyze the trades your bot generated, you can load them to a DataFrame as follwos: + +``` python +from freqtrade.data.btanalysis import load_trades_from_db + +df = load_trades_from_db("sqlite:///tradesv3.sqlite") + +df.groupby("pair")["sell_reason"].value_counts() + +``` + +Feel free to submit an issue or Pull Request if you would like to share ideas on how to best analyze the data. diff --git a/mkdocs.yml b/mkdocs.yml index 6b445ee3a..b5e759432 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ nav: - Plotting: plotting.md - Deprecated features: deprecated.md - FAQ: faq.md + - Data Analysis: data-analysis.md - SQL Cheatsheet: sql_cheatsheet.md - Sandbox testing: sandbox-testing.md - Contributors guide: developer.md From 3e61ada34a0e1d2b7545e640453991c52f6ff5d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:18:49 +0200 Subject: [PATCH 644/928] Be explicit in what is used, db or trades --- freqtrade/arguments.py | 7 +++++++ freqtrade/configuration.py | 3 ++- scripts/plot_dataframe.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 3075dd0fe..d623b7d7a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -515,3 +515,10 @@ class Arguments(object): default=750, type=int, ) + parser.add_argument( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) Default: %(default)s', + dest='trade_source', + default="file", + choices=["DB", "file"] + ) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index b2c35c977..d74b712c3 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -358,7 +358,8 @@ class Configuration(object): self._args_to_config(config, argname='plot_limit', logstring='Limiting plot to: {}') - + self._args_to_config(config, argname='trade_source', + logstring='Using trades from: {}') return config def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 99ba60d40..828373cb2 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -33,10 +33,10 @@ import pandas as pd from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades, extract_trades_of_period +from freqtrade.data.btanalysis import (extract_trades_of_period, + load_backtest_data, load_trades_from_db) from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (generate_graph, - generate_plot_file) +from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -124,10 +124,12 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): tickers = {} tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) + trades = None + if config["trade_source"] == "DB": + trades = load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + trades = load_backtest_data(Path(config["exportfilename"])) - trades = load_trades(exportfilename=config["exportfilename"], - db_url=config["db_url"], - ) trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) From 559d5ebd1deae0f4cf521d2d88077e0dbdb8c552 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:20:41 +0200 Subject: [PATCH 645/928] Remove combined load-method since it's confusing --- docs/plotting.md | 2 +- freqtrade/data/btanalysis.py | 43 ++++++++----------------- freqtrade/tests/data/test_btanalysis.py | 12 ++----- 3 files changed, 17 insertions(+), 40 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 6dc3d13b1..b8e041d61 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -58,7 +58,7 @@ Timerange doesn't work with live data. To plot trades stored in a database use `--db-url` argument: ``` bash -python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH +python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB ``` To plot trades from a backtesting result, use `--export-filename ` diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5efb10433..9d6d90cf3 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -82,36 +82,21 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration"] + "open_rate", "close_rate", "duration", "sell_reason", + "max_rate", "min_rate"] - for x in Trade.query.all(): - logger.info("date: {}".format(x.open_date)) - - trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date.replace(tzinfo=pytz.UTC), - t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None) - for t in Trade.query.all()], - columns=columns) - - return trades - - -def load_trades(exportfilename: str = None, db_url: str = None) -> pd.DataFrame: - """ - Load trades, either from a DB (using dburl) or via a backtest export file. - :param exportfilename: Path to a file exported from backtesting - :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) - :returns: Dataframe containing Trades - """ - - trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) - if exportfilename: - trades = load_backtest_data(Path(exportfilename)) - elif db_url: - trades = load_trades_from_db(db_url) + trades = pd.DataFrame([(t.pair, t.calc_profit(), + t.open_date.replace(tzinfo=pytz.UTC), + t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, + t.open_rate, t.close_rate, + t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None, + t.sell_reason, + t.max_rate, + t.min_rate, + ) + for t in Trade.query.all()], + columns=columns) return trades diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 6fa529394..1cb48393d 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -7,7 +7,7 @@ from pandas import DataFrame, to_datetime from freqtrade.arguments import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, extract_trades_of_period, - load_backtest_data, load_trades) + load_backtest_data, load_trades_from_db) from freqtrade.data.history import load_pair_history, make_testdata_path from freqtrade.tests.test_persistence import create_mock_trades @@ -28,14 +28,6 @@ def test_load_backtest_data(): load_backtest_data(str("filename") + "nofile") -def test_load_trades_file(default_conf, fee, mocker): - # Real testing of load_backtest_data is done in test_load_backtest_data - lbt = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) - filename = make_testdata_path(None) / "backtest-result_test.json" - load_trades(db_url=None, exportfilename=filename) - assert lbt.call_count == 1 - - @pytest.mark.usefixtures("init_persistence") def test_load_trades_db(default_conf, fee, mocker): @@ -43,7 +35,7 @@ def test_load_trades_db(default_conf, fee, mocker): # remove init so it does not init again init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) - trades = load_trades(db_url=default_conf['db_url'], exportfilename=None) + trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 assert len(trades) == 3 assert isinstance(trades, DataFrame) From cc56d0e0fc75bc7de7afdb140ae9618d71a0ee78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:40:33 +0200 Subject: [PATCH 646/928] Remove unneeded initialization --- freqtrade/arguments.py | 3 ++- scripts/plot_dataframe.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index d623b7d7a..f6fb2dedb 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -517,7 +517,8 @@ class Arguments(object): ) parser.add_argument( '--trade-source', - help='Specify the source for trades (Can be DB or file (backtest file)) Default: %(default)s', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', dest='trade_source', default="file", choices=["DB", "file"] diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 828373cb2..e5cc6ef89 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -124,7 +124,6 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): tickers = {} tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - trades = None if config["trade_source"] == "DB": trades = load_trades_from_db(config["db_url"]) elif config["trade_source"] == "file": From 026784efac660d2a891ef1e1ee99dfa17b861a16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:45:38 +0200 Subject: [PATCH 647/928] remove get_tickers_data from plot_dataframe --- scripts/plot_dataframe.py | 48 +++++++++------------------------------ 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 4aacc99dd..23e70a987 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,7 +31,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments from freqtrade.data import history from freqtrade.data.btanalysis import load_trades, extract_trades_of_period from freqtrade.optimize import setup_configuration @@ -43,38 +43,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def get_tickers_data(strategy, exchange, pairs: List[str], timerange: TimeRange, - datadir: Path, refresh_pairs: bool, live: bool): - """ - Get tickers data for each pairs on live or local, option defined in args - :return: dictionary of tickers. output format: {'pair': tickersdata} - """ - - ticker_interval = strategy.ticker_interval - - tickers = history.load_data( - datadir=datadir, - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=refresh_pairs, - timerange=timerange, - exchange=exchange, - live=live, - ) - - # No ticker found, impossible to download, len mismatch - for pair, data in tickers.copy().items(): - logger.debug("checking tickers data of pair: %s", pair) - logger.debug("data.empty: %s", data.empty) - logger.debug("len(data): %s", len(data)) - if data.empty: - del tickers[pair] - logger.info( - 'An issue occured while retreiving data of %s pair, please retry ' - 'using -l option for live or --refresh-pairs-cached', pair) - return tickers - - def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: """ Get tickers then Populate strategy indicators and signals, then return the full dataframe @@ -113,10 +81,16 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): timerange = Arguments.parse_timerange(config["timerange"]) ticker_interval = strategy.ticker_interval - tickers = get_tickers_data(strategy, exchange, pairs, timerange, - datadir=Path(str(config.get("datadir"))), - refresh_pairs=config.get('refresh_pairs', False), - live=config.get("live", False)) + tickers = history.load_data( + datadir=Path(str(config.get("datadir"))), + pairs=pairs, + ticker_interval=config['ticker_interval'], + refresh_pairs=config.get('refresh_pairs', False), + timerange=timerange, + exchange=exchange, + live=config.get("live", False), + ) + pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 From 4cbcb5f36f365bab31d6a16e12f2ceeb885201d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 16:52:14 +0200 Subject: [PATCH 648/928] Move .title to ExchangeResolver (it does not make sense to do this over and over again) --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/resolvers/exchange_resolver.py | 1 + freqtrade/tests/conftest.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 4 ++-- scripts/plot_dataframe.py | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 305a97ba6..480bb680f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -53,8 +53,7 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) - exchange_name = self.config.get('exchange', {}).get('name').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.wallets = Wallets(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cc78ad2b..8bdf66f92 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,8 +63,7 @@ class Backtesting(object): self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] - exchange_name = self.config.get('exchange', {}).get('name').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.fee = self.exchange.get_fee() if self.config.get('runmode') != RunMode.HYPEROPT: diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 8d1845c71..25a86dd0e 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -22,6 +22,7 @@ class ExchangeResolver(IResolver): Load the custom class from config parameter :param config: configuration dictionary """ + exchange_name = exchange_name.title() try: self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) except ImportError: diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index e956d89c4..3523b44c4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -60,7 +60,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang patch_exchange(mocker, api_mock, id) config["exchange"]["name"] = id try: - exchange = ExchangeResolver(id.title(), config).exchange + exchange = ExchangeResolver(id, config).exchange except ImportError: exchange = Exchange(config) return exchange diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index f0dc96626..48a8538a9 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -124,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog.record_tuples) caplog.clear() - exchange = ExchangeResolver('Kraken', default_conf).exchange + exchange = ExchangeResolver('kraken', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog.record_tuples) - exchange = ExchangeResolver('Binance', default_conf).exchange + exchange = ExchangeResolver('binance', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 23e70a987..3792233de 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -68,8 +68,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - exchange_name = config.get('exchange', {}).get('name').title() - exchange = ExchangeResolver(exchange_name, config).exchange + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange strategy = StrategyResolver(config).strategy if "pairs" in config: From 451d4a400e3b92236cb40eee10463691b7eff77e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 22 Jun 2019 23:51:29 +0300 Subject: [PATCH 649/928] fix help strings shown to the user --- freqtrade/arguments.py | 39 ++++++++++++++++++++------------------- freqtrade/constants.py | 2 ++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 3075dd0fe..aa1a3e6da 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -87,9 +87,9 @@ class Arguments(object): ) parser.add_argument( '-c', '--config', - help="Specify configuration file (default: %(default)s). " - "Multiple --config options may be used. " - "Can be set to '-' to read config from stdin.", + help=f'Specify configuration file (default: {constants.DEFAULT_CONFIG}). ' + f'Multiple --config options may be used. ' + f'Can be set to `-` to read config from stdin.', dest='config', action='append', metavar='PATH', @@ -122,9 +122,9 @@ class Arguments(object): ) parser.add_argument( '--dynamic-whitelist', - help='Dynamically generate and update whitelist' - ' based on 24h BaseVolume (default: %(const)s).' - ' DEPRECATED.', + help='Dynamically generate and update whitelist ' + 'based on 24h BaseVolume (default: %(const)s). ' + 'DEPRECATED.', dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, type=int, @@ -133,8 +133,8 @@ class Arguments(object): ) parser.add_argument( '--db-url', - help='Override trades database URL, this is useful if dry_run is enabled' - ' or in custom deployments (default: %(default)s).', + help=f'Override trades database URL, this is useful if dry_run is enabled ' + f'or in custom deployments (default: {constants.DEFAULT_DB_DRYRUN_URL}.', dest='db_url', metavar='PATH', ) @@ -228,10 +228,10 @@ class Arguments(object): ) parser.add_argument( '--export-filename', - help='Save backtest results to this filename \ - requires --export to be set as well\ - Example --export-filename=user_data/backtest_data/backtest_today.json\ - (default: %(default)s)', + help='Save backtest results to this filename ' + 'requires --export to be set as well. ' + 'Example --export-filename=user_data/backtest_data/backtest_today.json ' + '(default: %(default)s)', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), dest='exportfilename', metavar='PATH', @@ -246,8 +246,8 @@ class Arguments(object): parser.add_argument( '--stoplosses', help='Defines a range of stoploss against which edge will assess the strategy ' - 'the format is "min,max,step" (without any space).' - 'example: --stoplosses=-0.01,-0.1,-0.001', + 'the format is "min,max,step" (without any space). ' + 'Example: --stoplosses=-0.01,-0.1,-0.001', dest='stoploss_range', ) @@ -289,8 +289,8 @@ class Arguments(object): ) parser.add_argument( '-s', '--spaces', - help='Specify which parameters to hyperopt. Space separate list. \ - Default: %(default)s.', + help='Specify which parameters to hyperopt. Space separate list. ' + 'Default: %(default)s.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', @@ -467,13 +467,14 @@ class Arguments(object): ) parser.add_argument( '--exchange', - help='Exchange name (default: %(default)s). Only valid if no config is provided.', + help=f'Exchange name (default: {constants.DEFAULT_EXCHANGE}). ' + f'Only valid if no config is provided.', dest='exchange', ) parser.add_argument( '-t', '--timeframes', - help='Specify which tickers to download. Space separated list. \ - Default: %(default)s.', + help=f'Specify which tickers to download. Space separated list. ' + f'Default: {constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], nargs='+', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4772952fc..7a487fcc7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -4,6 +4,7 @@ bot constants """ DEFAULT_CONFIG = 'config.json' +DEFAULT_EXCHANGE = 'bittrex' DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec DEFAULT_TICKER_INTERVAL = 5 # min @@ -21,6 +22,7 @@ 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', From 7fbdf36c6481f22eef52e75941c898ce8a3f496b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 19:23:51 +0300 Subject: [PATCH 650/928] avoid code duplication while selecting min_roi entries --- freqtrade/optimize/backtesting.py | 17 +++++++--------- freqtrade/strategy/interface.py | 33 ++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cc78ad2b..e0660eacb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -253,22 +253,19 @@ class Backtesting(object): sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: - trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): # Set close_rate to stoploss closerate = trade.stop_loss elif sell.sell_type == (SellType.ROI): - # get next entry in min_roi > to trade duration - # Interface.py skips on trade_duration <= duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, - self.strategy.minimal_roi.keys()))) - roi = self.strategy.minimal_roi[roi_entry] - - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + closerate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + else: + closerate = sell_row.open else: closerate = sell_row.open diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 353ae64b4..7ec782b0b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -from typing import Dict, List, NamedTuple, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import warnings import arrow @@ -347,23 +347,32 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: + def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: """ - Based an earlier trade and current price and ROI configuration, decides whether bot should - sell. Requires current_profit to be in percent!! - :return True if bot should sell at current rate + Based on trade duration defines the ROI entry that may have been reached. + :param trade_dur: trade duration in minutes + :return: minimal ROI entry value or None if none proper ROI entry was found. """ - - # Check if time matches and current rate is above threshold - trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - # Get highest entry in ROI dict where key <= trade-duration roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: - return False + return None roi_entry = max(roi_list) - threshold = self.minimal_roi[roi_entry] - return current_profit > threshold + return self.minimal_roi[roi_entry] + + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: + """ + Based on trade duration, current price and ROI configuration, decides whether bot should + sell. Requires current_profit to be in percent!! + :return: True if bot should sell at current rate + """ + # Check if time matches and current rate is above threshold + trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + roi = self.min_roi_reached_entry(trade_dur) + if roi is None: + return False + else: + return current_profit > roi def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ From 3716c04ed4c0e77c3b0e4016928ea4a88ed2d76a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 20:34:53 +0300 Subject: [PATCH 651/928] fix help string for --db-url --- freqtrade/arguments.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index aa1a3e6da..d945c998e 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -133,8 +133,9 @@ class Arguments(object): ) parser.add_argument( '--db-url', - help=f'Override trades database URL, this is useful if dry_run is enabled ' - f'or in custom deployments (default: {constants.DEFAULT_DB_DRYRUN_URL}.', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', dest='db_url', metavar='PATH', ) From 7f018839f82a13f05dc52a291256c4a8db8b976b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 21:42:46 +0300 Subject: [PATCH 652/928] diverse cosmetics to options help strings --- freqtrade/arguments.py | 53 +++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index d945c998e..07b4bbb2d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -76,7 +76,7 @@ class Arguments(object): ) parser.add_argument( '--logfile', - help='Log to the file specified', + help='Log to the file specified.', dest='logfile', metavar='FILE', ) @@ -87,7 +87,7 @@ class Arguments(object): ) parser.add_argument( '-c', '--config', - help=f'Specify configuration file (default: {constants.DEFAULT_CONFIG}). ' + help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' f'Multiple --config options may be used. ' f'Can be set to `-` to read config from stdin.', dest='config', @@ -109,7 +109,7 @@ class Arguments(object): parser.add_argument( '-s', '--strategy', - help='Specify strategy class name (default: %(default)s).', + help='Specify strategy class name (default: `%(default)s`).', dest='strategy', default='DefaultStrategy', metavar='NAME', @@ -155,7 +155,7 @@ class Arguments(object): parser.add_argument( '-i', '--ticker-interval', - help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', dest='ticker_interval', ) parser.add_argument( @@ -213,26 +213,25 @@ class Arguments(object): ) parser.add_argument( '--strategy-list', - help='Provide a commaseparated list of strategies to backtest ' + help='Provide a comma-separated list of strategies to backtest. ' 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with --export trades, ' + 'or via command line. When using this together with `--export trades`, ' 'the strategy-name is injected into the filename ' - '(so backtest-data.json becomes backtest-data-DefaultStrategy.json', + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', nargs='+', dest='strategy_list', ) parser.add_argument( '--export', help='Export backtest results, argument are: trades. ' - 'Example --export=trades', + 'Example: `--export=trades`', dest='export', ) parser.add_argument( '--export-filename', - help='Save backtest results to this filename ' - 'requires --export to be set as well. ' - 'Example --export-filename=user_data/backtest_data/backtest_today.json ' - '(default: %(default)s)', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), dest='exportfilename', metavar='PATH', @@ -246,9 +245,9 @@ class Arguments(object): parser.add_argument( '--stoplosses', - help='Defines a range of stoploss against which edge will assess the strategy ' - 'the format is "min,max,step" (without any space). ' - 'Example: --stoplosses=-0.01,-0.1,-0.001', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', dest='stoploss_range', ) @@ -260,7 +259,7 @@ class Arguments(object): parser.add_argument( '--customhyperopt', - help='Specify hyperopt class name (default: %(default)s).', + help='Specify hyperopt class name (default: `%(default)s`).', dest='hyperopt', default=constants.DEFAULT_HYPEROPT, metavar='NAME', @@ -290,8 +289,8 @@ class Arguments(object): ) parser.add_argument( '-s', '--spaces', - help='Specify which parameters to hyperopt. Space separate list. ' - 'Default: %(default)s.', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', @@ -340,7 +339,7 @@ class Arguments(object): parser.add_argument( '-1', '--one-column', - help='Print exchanges in one column', + help='Print exchanges in one column.', action='store_true', dest='print_one_column', ) @@ -443,7 +442,7 @@ class Arguments(object): parser.add_argument( '-p', '--pairs', - help='Show profits for only this pairs. Pairs are comma-separated.', + help='Show profits for only these pairs. Pairs are comma-separated.', dest='pairs', ) @@ -468,14 +467,14 @@ class Arguments(object): ) parser.add_argument( '--exchange', - help=f'Exchange name (default: {constants.DEFAULT_EXCHANGE}). ' + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' f'Only valid if no config is provided.', dest='exchange', ) parser.add_argument( '-t', '--timeframes', - help=f'Specify which tickers to download. Space separated list. ' - f'Default: {constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}.', + help=f'Specify which tickers to download. Space-separated list. ' + f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], nargs='+', @@ -497,7 +496,7 @@ class Arguments(object): parser.add_argument( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' - 'Separate them with a coma. E.g: ema3,ema5 (default: %(default)s)', + 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', default='sma,ema3,ema5', dest='indicators1', ) @@ -505,14 +504,14 @@ class Arguments(object): parser.add_argument( '--indicators2', help='Set indicators from your strategy you want in the third row of the graph. ' - 'Separate them with a coma. E.g: fastd,fastk (default: %(default)s)', + 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', default='macd,macdsignal', dest='indicators2', ) parser.add_argument( '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', dest='plot_limit', default=750, type=int, From 5b84cb39ac0ec5589cef99ccc8cea4b66edbaa54 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sun, 23 Jun 2019 22:51:33 +0300 Subject: [PATCH 653/928] typo fixed --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index d215dc8d6..f0c536ade 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ This page explains how to prepare your environment for running the bot. Before running your bot in production you will need to setup few external API. In production mode, the bot will require valid Exchange API -credentials. We also reccomend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). +credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - [Setup your exchange account](#setup-your-exchange-account) From 116d8e853e40783a6f23a5ba576fc5f433d5a0ad Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 23 Jun 2019 23:10:37 +0300 Subject: [PATCH 654/928] typos in docstrings fixed --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/data/history.py | 2 +- freqtrade/strategy/interface.py | 4 ++-- scripts/rest_client.py | 32 ++++++++++++++++---------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 4aecf8ecf..f78ca3fa8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -23,7 +23,7 @@ def load_backtest_data(filename) -> pd.DataFrame: """ Load backtest data file. :param filename: pathlib.Path object, or string pointing to the file. - :return a dataframe with the analysis results + :return: a dataframe with the analysis results """ if isinstance(filename, str): filename = Path(filename) @@ -78,7 +78,7 @@ def load_trades(db_url: str = None, exportfilename: str = None) -> pd.DataFrame: Load trades, either from a DB (using dburl) or via a backtest export file. :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) :param exportfilename: Path to a file exported from backtesting - :returns: Dataframe containing Trades + :return: Dataframe containing Trades """ timeZone = pytz.UTC diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 67f942119..e9694b90f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -63,7 +63,7 @@ def load_tickerdata_file( timerange: Optional[TimeRange] = None) -> Optional[list]: """ Load a pair from file, either .json.gz or .json - :return tickerlist or None if unsuccesful + :return: tickerlist or None if unsuccesful """ filename = pair_data_filename(datadir, pair, ticker_interval) pairdata = misc.file_load_json(filename) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 68e0a7b37..949a88b91 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,7 +158,7 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it - :return DataFrame with ticker data and indicator data + :return: DataFrame with ticker data and indicator data """ pair = str(metadata.get('pair')) @@ -351,7 +351,7 @@ class IStrategy(ABC): """ Based an earlier trade and current price and ROI configuration, decides whether bot should sell. Requires current_profit to be in percent!! - :return True if bot should sell at current rate + :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 2261fba0b..a46b3ebfb 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -65,14 +65,14 @@ class FtRestClient(): def start(self): """ Start the bot if it's in stopped state. - :returns: json object + :return: json object """ return self._post("start") def stop(self): """ Stop the bot. Use start to restart - :returns: json object + :return: json object """ return self._post("stop") @@ -80,77 +80,77 @@ class FtRestClient(): """ Stop buying (but handle sells gracefully). use reload_conf to reset - :returns: json object + :return: json object """ return self._post("stopbuy") def reload_conf(self): """ Reload configuration - :returns: json object + :return: json object """ return self._post("reload_conf") def balance(self): """ Get the account balance - :returns: json object + :return: json object """ return self._get("balance") def count(self): """ Returns the amount of open trades - :returns: json object + :return: json object """ return self._get("count") def daily(self, days=None): """ Returns the amount of open trades - :returns: json object + :return: json object """ return self._get("daily", params={"timescale": days} if days else None) def edge(self): """ Returns information about edge - :returns: json object + :return: json object """ return self._get("edge") def profit(self): """ Returns the profit summary - :returns: json object + :return: json object """ return self._get("profit") def performance(self): """ Returns the performance of the different coins - :returns: json object + :return: json object """ return self._get("performance") def status(self): """ Get the status of open trades - :returns: json object + :return: json object """ return self._get("status") def version(self): """ Returns the version of the bot - :returns: json object containing the version + :return: json object containing the version """ return self._get("version") def whitelist(self): """ Show the current whitelist - :returns: json object + :return: json object """ return self._get("whitelist") @@ -158,7 +158,7 @@ class FtRestClient(): """ Show the current blacklist :param add: List of coins to add (example: "BNB/BTC") - :returns: json object + :return: json object """ if not args: return self._get("blacklist") @@ -170,7 +170,7 @@ class FtRestClient(): Buy an asset :param pair: Pair to buy (ETH/BTC) :param price: Optional - price to buy - :returns: json object of the trade + :return: json object of the trade """ data = {"pair": pair, "price": price @@ -181,7 +181,7 @@ class FtRestClient(): """ Force-sell a trade :param tradeid: Id of the trade (can be received via status command) - :returns: json object + :return: json object """ return self._post("forcesell", data={"tradeid": tradeid}) From c1ee5d69c9b1cb799d8eef7d841c84fb2ee211d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 07:09:54 +0200 Subject: [PATCH 655/928] Try to get travis cache to work correctly --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d5cd52df2..455f4f037 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,4 +56,4 @@ notifications: cache: pip: True directories: - - /usr/local/lib + - /usr/local/lib/ From 1b156e0f344aed2f9025d3460aea400ae8b9a61f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 07:10:24 +0200 Subject: [PATCH 656/928] Don't install python to a system, it's error-prone and may not work --- setup.sh | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/setup.sh b/setup.sh index 11df6820e..8b5531746 100755 --- a/setup.sh +++ b/setup.sh @@ -1,12 +1,21 @@ #!/usr/bin/env bash #encoding=utf8 +function check_installed_pip() { + ${PYTHON} -m pip > /dev/null + if [ $? -ne 0 ]; then + echo "pip not found. Please make sure that pip is available for ${PYTHON}." + exit 1 + fi +} + # Check which python version is installed function check_installed_python() { which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" PYTHON=python3.7 + check_installed_pip return fi @@ -14,6 +23,7 @@ function check_installed_python() { if [ $? -eq 0 ]; then echo "using Python 3.6" PYTHON=python3.6 + check_installed_pip return fi @@ -21,7 +31,6 @@ function check_installed_python() { echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" exit 1 fi - } function updateenv() { @@ -29,21 +38,21 @@ function updateenv() { echo "Updating your virtual env" echo "-------------------------" source .env/bin/activate - echo "pip3 install in-progress. Please wait..." + echo "pip install in-progress. Please wait..." # Install numpy first to have py_find_1st install clean - pip3 install --upgrade pip numpy - pip3 install --upgrade -r requirements.txt + ${PYTHON} -m pip install --upgrade pip numpy + ${PYTHON} -m pip install --upgrade -r requirements.txt read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - pip3 install --upgrade -r requirements-dev.txt + ${PYTHON} -m pip install --upgrade -r requirements-dev.txt else echo "Dev dependencies ignored." fi - pip3 install --quiet -e . - echo "pip3 install completed" + ${PYTHON} -m pip install -e . + echo "pip install completed" echo } @@ -74,16 +83,14 @@ function install_macos() { echo "-------------------------" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi - brew install python3 wget install_talib test_and_fix_python_on_mac } # Install bot Debian_ubuntu function install_debian() { - sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update - sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git + sudo apt-get install build-essential autoconf libtool pkg-config make wget git install_talib } @@ -244,7 +251,7 @@ echo " Installing dependencies for Plotting scripts ----------------------------------------- " -pip install plotly --upgrade +${PYTHON} -m pip install plotly --upgrade } function help() { From 11d39bb0d3f3a151ea41bba085f9b031d552ea09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 17:20:41 +0200 Subject: [PATCH 657/928] Improve wording --- docs/data-analysis.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index ef3a02355..6d440ffa6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,6 +1,6 @@ # Analyzing bot data -After performing backtests, or after running the bot for some time, it will be interresting to analyze the results your bot generated. +After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated. A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. @@ -11,9 +11,7 @@ The following helpers will help you loading the data into Pandas DataFrames, and To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). You can then load the trades to perform further analysis. -A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. - -Freqtrade provides an easy to load the backtest results, which is `load_backtest_data` - and takes a path to the backtest-results file. +Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter. ``` python from freqtrade.data.btanalysis import load_backtest_data @@ -24,13 +22,13 @@ df.groupby("pair")["sell_reason"].value_counts() ``` -This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable. +This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output very difficult to digest due to information overload. -If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it. +If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it. ## Live data -To analyze the trades your bot generated, you can load them to a DataFrame as follwos: +To analyze the trades your bot generated, you can load them to a DataFrame as follows: ``` python from freqtrade.data.btanalysis import load_trades_from_db @@ -41,4 +39,4 @@ df.groupby("pair")["sell_reason"].value_counts() ``` -Feel free to submit an issue or Pull Request if you would like to share ideas on how to best analyze the data. +Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. From 5a30f0462f00d7d9215c949ac1d5e935a8accbad Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Jun 2019 15:24:06 +0000 Subject: [PATCH 658/928] Update ccxt from 1.18.725 to 1.18.805 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index ed3f052a9..bc54c9a0a 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.725 +ccxt==1.18.805 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.14.2 From e8429bd2300e7f1cef2564f34a4da9134ab7391a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Jun 2019 15:24:07 +0000 Subject: [PATCH 659/928] Update sqlalchemy from 1.3.4 to 1.3.5 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index bc54c9a0a..a05bfd122 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.805 -SQLAlchemy==1.3.4 +SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 cachetools==3.1.1 From 90ada0649ccee4b3296b9806be1ca7a74eac4a53 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Jun 2019 15:24:08 +0000 Subject: [PATCH 660/928] Update wrapt from 1.11.1 to 1.11.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index a05bfd122..6913217ff 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -7,7 +7,7 @@ arrow==0.14.2 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore -wrapt==1.11.1 +wrapt==1.11.2 scikit-learn==0.21.2 joblib==0.13.2 jsonschema==3.0.1 From d6dbb21a343f6ad28249efb3e2fbaf769f16e4b8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Jun 2019 15:24:09 +0000 Subject: [PATCH 661/928] Update mypy from 0.701 to 0.710 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ce05b47f0..1232d4dd4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 coveralls==1.8.1 -mypy==0.701 +mypy==0.710 From e83f8941a19ba5282fdeed105c7475afbecd6f71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 19:20:42 +0200 Subject: [PATCH 662/928] Fix documentation grammar --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 6d440ffa6..1940fa3e6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -22,7 +22,7 @@ df.groupby("pair")["sell_reason"].value_counts() ``` -This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output very difficult to digest due to information overload. +This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it. From 56e62948730fa65086c7018fabd3030669042128 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 19:44:14 +0200 Subject: [PATCH 663/928] Version bump to 2019.6 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 340d5b515..a5ae200d4 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.18.5-dev' +__version__ = '2019.6' class DependencyException(Exception): From e1daf0273506ed8204210bf4241a0823b4911624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 19:46:39 +0200 Subject: [PATCH 664/928] UPdate version for develop --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index a5ae200d4..2fdcccea5 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.6' +__version__ = '2019.6-dev' class DependencyException(Exception): From 7166674d6c4ba0726ce534b5c97358b11092260a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:09:39 +0200 Subject: [PATCH 665/928] Move check_int_positive out of arguments class --- freqtrade/arguments.py | 31 ++++++++++++++++--------------- freqtrade/tests/test_arguments.py | 16 ++++++++-------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 1ec32d1f0..0d0896b2a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -10,6 +10,18 @@ import arrow from freqtrade import __version__, constants +def check_int_positive(value: str) -> int: + try: + uint = int(value) + if uint <= 0: + raise ValueError + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a positive integer value" + ) + return uint + + class TimeRange(NamedTuple): """ NamedTuple Defining timerange inputs. @@ -33,6 +45,7 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: + # Common options self.common_options() self.main_options() self._build_subcommands() @@ -318,7 +331,7 @@ class Arguments(object): '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', dest='hyperopt_random_state', - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) parser.add_argument( @@ -327,7 +340,7 @@ class Arguments(object): "optimization path (default: 1).", dest='hyperopt_min_trades', default=1, - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) @@ -422,18 +435,6 @@ class Arguments(object): return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) - @staticmethod - def check_int_positive(value: str) -> int: - try: - uint = int(value) - if uint <= 0: - raise ValueError - except ValueError: - raise argparse.ArgumentTypeError( - f"{value} is invalid for this parameter, should be a positive integer value" - ) - return uint - def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses arguments common for scripts. @@ -462,7 +463,7 @@ class Arguments(object): '--days', help='Download data for given number of days.', dest='days', - type=Arguments.check_int_positive, + type=check_int_positive, metavar='INT', ) parser.add_argument( diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d9292bdb5..20e63d7e7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments, TimeRange, check_int_positive # Parse common command-line-arguments. Used for all tools @@ -206,18 +206,18 @@ def test_plot_dataframe_options() -> None: def test_check_int_positive() -> None: - assert Arguments.check_int_positive("3") == 3 - assert Arguments.check_int_positive("1") == 1 - assert Arguments.check_int_positive("100") == 100 + assert check_int_positive("3") == 3 + assert check_int_positive("1") == 1 + assert check_int_positive("100") == 100 with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("-2") + check_int_positive("-2") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("0") + check_int_positive("0") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("3.5") + check_int_positive("3.5") with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive("DeadBeef") + check_int_positive("DeadBeef") From 7017e46ba10dc42e81884986d7725ccfb7d50393 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:10:35 +0200 Subject: [PATCH 666/928] Add dict with all possible cli arguments --- freqtrade/arguments.py | 343 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 0d0896b2a..f1624344a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -22,6 +22,342 @@ def check_int_positive(value: str) -> int: return uint +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line arguments +AVAILABLE_CLI_OPTIONS = { + # Common arguments + "loglevel": Arg( + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + dest='loglevel', + default=0, + ), + "logfile": Arg( + '--logfile', + help='Log to the file specified.', + dest='logfile', + metavar='FILE', + ), + + "version": Arg( + '--version', + action='version', + version=f'%(prog)s {__version__}' + ), + "config": Arg( + '-c', '--config', + help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' + f'Multiple --config options may be used. ' + f'Can be set to `-` to read config from stdin.', + dest='config', + action='append', + metavar='PATH',), + "datadir": Arg( + '-d', '--datadir', + help='Path to backtest data.', + dest='datadir', + metavar='PATH',), + # Main options + "strategy": Arg( + '-s', '--strategy', + help='Specify strategy class name (default: `%(default)s`).', + dest='strategy', + default='DefaultStrategy', + metavar='NAME', + ), + "strategy_path": Arg( + '--strategy-path', + help='Specify additional strategy lookup path.', + dest='strategy_path', + metavar='PATH', + ), + "dynamic_whitelist": Arg( + '--dynamic-whitelist', + help='Dynamically generate and update whitelist ' + 'based on 24h BaseVolume (default: %(const)s). ' + 'DEPRECATED.', + dest='dynamic_whitelist', + const=constants.DYNAMIC_WHITELIST, + type=int, + metavar='INT', + nargs='?', + ), + "db_url": Arg( + '--db-url', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', + dest='db_url', + metavar='PATH', + ), + "sd_notify": Arg( + '--sd-notify', + help='Notify systemd service manager.', + action='store_true', + dest='sd_notify', + ), + # Optimize common + "ticker_interval": Arg( + '-i', '--ticker-interval', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + dest='ticker_interval', + ), + "timerange": Arg( + '--timerange', + help='Specify what timerange of data to use.', + dest='timerange', + ), + "max_open_trades": Arg( + '--max_open_trades', + help='Specify max_open_trades to use.', + type=int, + dest='max_open_trades', + ), + "stake_amount": Arg( + '--stake_amount', + help='Specify stake_amount.', + type=float, + dest='stake_amount', + ), + "refresh_pairs": Arg( + '-r', '--refresh-pairs-cached', + help='Refresh the pairs files in tests/testdata with the latest data from the ' + 'exchange. Use it if you want to run your optimization commands with ' + 'up-to-date data.', + action='store_true', + dest='refresh_pairs', + ), + # backtesting + "position_stacking": Arg( + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking).', + action='store_true', + dest='position_stacking', + default=False + ), + "use_max_market_positions": Arg( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number).', + action='store_false', + dest='use_max_market_positions', + default=True + ), + "live": Arg( + '-l', '--live', + help='Use live data.', + action='store_true', + dest='live', + ), + "strategy_list": Arg( + '--strategy-list', + help='Provide a comma-separated list of strategies to backtest. ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line. When using this together with `--export trades`, ' + 'the strategy-name is injected into the filename ' + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', + nargs='+', + dest='strategy_list', + ), + "export": Arg( + '--export', + help='Export backtest results, argument are: trades. ' + 'Example: `--export=trades`', + dest='export', + ), + "exportfilename": Arg( + '--export-filename', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + default=os.path.join('user_data', 'backtest_data', + 'backtest-result.json'), + dest='exportfilename', + metavar='PATH', + ), + # Edge + "stoploss_range": Arg( + '--stoplosses', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', + dest='stoploss_range', + ), + # hyperopt + "hyperopt": Arg( + '--customhyperopt', + help='Specify hyperopt class name (default: `%(default)s`).', + dest='hyperopt', + default=constants.DEFAULT_HYPEROPT, + metavar='NAME', + ), + "epochs": Arg( + '-e', '--epochs', + help='Specify number of epochs (default: %(default)d).', + dest='epochs', + default=constants.HYPEROPT_EPOCH, + type=int, + metavar='INT', + ), + "spaces": Arg( + '-s', '--spaces', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + default='all', + nargs='+', + dest='spaces', + ), + "print_all": Arg( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + dest='print_all', + default=False + ), + "hyperopt_jobs": Arg( + '-j', '--job-workers', + help='The number of concurrently running jobs for hyperoptimization ' + '(hyperopt worker processes). ' + 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' + 'If 1 is given, no parallel computing code is used at all.', + dest='hyperopt_jobs', + default=-1, + type=int, + metavar='JOBS', + ), + "hyperopt_random_state": Arg( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + dest='hyperopt_random_state', + type=check_int_positive, + metavar='INT', + ), + "hyperopt_min_trades": Arg( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + dest='hyperopt_min_trades', + default=1, + type=check_int_positive, + metavar='INT', + ), + # List_exchange + "print_one_column": Arg( + '-1', '--one-column', + help='Print exchanges in one column.', + action='store_true', + dest='print_one_column', + ), + # script_options + "pairs": Arg( + '-p', '--pairs', + help='Show profits for only these pairs. Pairs are comma-separated.', + dest='pairs', + ), + # Download data + + "pairs_file": Arg( + '--pairs-file', + help='File containing a list of pairs to download.', + dest='pairs_file', + metavar='FILE', + ), + "days": Arg( + '--days', + help='Download data for given number of days.', + dest='days', + type=check_int_positive, + metavar='INT', + ), + "exchange": Arg( + '--exchange', + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' + f'Only valid if no config is provided.', + dest='exchange', + ), + "timeframes": Arg( + '-t', '--timeframes', + help=f'Specify which tickers to download. Space-separated list. ' + f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', + choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', + '6h', '8h', '12h', '1d', '3d', '1w'], + nargs='+', + dest='timeframes', + ), + "erase": Arg( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes.', + dest='erase', + action='store_true', + ), + # Plot_df_options + "indicators1": Arg( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', + default='sma,ema3,ema5', + dest='indicators1', + ), + "indicators2": Arg( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', + default='macd,macdsignal', + dest='indicators2', + ), + "plot_limit": Arg( + '--plot-limit', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', + dest='plot_limit', + default=750, + type=int, + ), + "trade_source": Arg( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', + dest='trade_source', + default="file", + choices=["DB", "file"] + ) +} + +ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] +ARGS_STRATEGY = ["strategy", "strategy_path"] + +ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] + +ARGS_COMMON_OPTIMIZE = ["loglevel", "ticker_interval", "timerange", + "max_open_trades", "stake_amount", "refresh_pairs"] + +ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", + "live", "strategy_list", "export", "exportfilename"] + +ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", + "use_max_market_positions", "print_all", "hyperopt_jobs", + "hyperopt_random_state", "hyperopt_min_trades"] + +ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] + + +ARGS_LIST_EXCHANGE = ["print_one_column"] + +ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] + +ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", + "export", "exportfilename", "trade_source"]) + + class TimeRange(NamedTuple): """ NamedTuple Defining timerange inputs. @@ -74,6 +410,13 @@ class Arguments(object): return parsed_arg + def build_args(self, optionlist, parser=None): + parser = parser or self.parser + + for val in optionlist: + opt = AVAILABLE_CLI_OPTIONS[val] + parser.add_argument(*opt.cli, **opt.kwargs) + def common_options(self) -> None: """ Parses arguments that are common for the main Freqtrade, all subcommands and scripts. From 7e82be53cde24affda9edee34fe7247d26beb6c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:14:24 +0200 Subject: [PATCH 667/928] Use build_args to build subcomand arguments --- freqtrade/arguments.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f1624344a..8eeb86468 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -381,9 +381,8 @@ class Arguments(object): self.parser = argparse.ArgumentParser(description=description) def _load_args(self) -> None: - # Common options - self.common_options() - self.main_options() + self.build_args(optionlist=ARGS_MAIN) + self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -713,20 +712,17 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.common_optimize_options(backtesting_cmd) - self.backtesting_options(backtesting_cmd) + self.build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.common_optimize_options(edge_cmd) - self.edge_options(edge_cmd) + self.build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.common_optimize_options(hyperopt_cmd) - self.hyperopt_options(hyperopt_cmd) + self.build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( @@ -734,7 +730,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.list_exchanges_options(list_exchanges_cmd) + self.build_args(optionlist=ARGS_LIST_EXCHANGE, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: From ee312ac2302a7ffc87cbd2c62c0e16639ae5ec14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:18:26 +0200 Subject: [PATCH 668/928] Use build_args for plot_dataframe script --- freqtrade/arguments.py | 4 ++-- scripts/plot_dataframe.py | 10 +++------- scripts/plot_profit.py | 6 +++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 8eeb86468..c5431a665 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -354,8 +354,8 @@ ARGS_LIST_EXCHANGE = ["print_one_column"] ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", - "export", "exportfilename", "trade_source"]) + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", + "export", "exportfilename", "timerange", "refresh_pairs", "live"]) class TimeRange(NamedTuple): diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7eaf0b337..7e81af925 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -31,7 +31,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.arguments import Arguments +from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) @@ -125,12 +125,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.common_options() - arguments.main_options() - arguments.common_optimize_options() - arguments.backtesting_options() - arguments.common_scripts_options() - arguments.plot_dataframe_options() + arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) + parsed_args = arguments.parse_args() # Load the configuration diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index fd98c120c..8bc9f2f9a 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,7 +24,7 @@ import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments +from freqtrade.arguments import Arguments, ARGS_PLOT_DATAFRAME from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.exchange import timeframe_to_seconds @@ -206,8 +206,8 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.common_options() - arguments.main_options() + arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) + arguments.common_optimize_options() arguments.backtesting_options() arguments.common_scripts_options() From 27798c1683f784f4cc2e1bde8a31ea5bdb7eb465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:18:54 +0200 Subject: [PATCH 669/928] Remove main_options --- freqtrade/arguments.py | 45 ------------------------------------------ 1 file changed, 45 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index c5431a665..69da90f42 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -456,51 +456,6 @@ class Arguments(object): metavar='PATH', ) - def main_options(self) -> None: - """ - Parses arguments for the main Freqtrade. - """ - parser = self.parser - - parser.add_argument( - '-s', '--strategy', - help='Specify strategy class name (default: `%(default)s`).', - dest='strategy', - default='DefaultStrategy', - metavar='NAME', - ) - parser.add_argument( - '--strategy-path', - help='Specify additional strategy lookup path.', - dest='strategy_path', - metavar='PATH', - ) - parser.add_argument( - '--dynamic-whitelist', - help='Dynamically generate and update whitelist ' - 'based on 24h BaseVolume (default: %(const)s). ' - 'DEPRECATED.', - dest='dynamic_whitelist', - const=constants.DYNAMIC_WHITELIST, - type=int, - metavar='INT', - nargs='?', - ) - parser.add_argument( - '--db-url', - help=f'Override trades database URL, this is useful in custom deployments ' - f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' - f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - dest='db_url', - metavar='PATH', - ) - parser.add_argument( - '--sd-notify', - help='Notify systemd service manager.', - action='store_true', - dest='sd_notify', - ) - def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: """ Parses arguments common for Backtesting, Edge and Hyperopt modules. From ba7a0dde06ea72666e757a68c90709ab75f03d06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:21:42 +0200 Subject: [PATCH 670/928] Use build_args for download script --- freqtrade/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 6 +++--- scripts/download_backtest_data.py | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 69da90f42..e4c810b23 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -351,7 +351,7 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGE = ["print_one_column"] -ARGS_DOWNLOADER = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] +ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 20e63d7e7..38383a346 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,7 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange, check_int_positive +from freqtrade.arguments import Arguments, TimeRange, check_int_positive, ARGS_DOWNLOADER # Parse common command-line-arguments. Used for all tools @@ -178,8 +178,8 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.common_options() - arguments.download_data_options() + arguments.build_args(ARGS_DOWNLOADER) + args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/folder' diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index dd4627c14..472b3ef4e 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -8,7 +8,7 @@ import sys from pathlib import Path from typing import Any, Dict, List -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.arguments import Arguments, TimeRange, ARGS_DOWNLOADER from freqtrade.configuration import Configuration from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange @@ -21,8 +21,7 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' arguments = Arguments(sys.argv[1:], 'Download backtest data') -arguments.common_options() -arguments.download_data_options() +arguments.build_args(ARGS_DOWNLOADER) # Do not read the default config if config is not specified # in the command line options explicitely From ca5093901b6e85e224809a00dc05c8967c2ca4ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:27:29 +0200 Subject: [PATCH 671/928] Use build_args for plot script --- freqtrade/arguments.py | 3 +++ scripts/plot_profit.py | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index e4c810b23..278f21c97 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -357,6 +357,9 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "refresh_pairs", "live"]) +ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "timerange", "export", "exportfilename"]) + class TimeRange(NamedTuple): """ diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 8bc9f2f9a..32bfae9cc 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -24,7 +24,7 @@ import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments, ARGS_PLOT_DATAFRAME +from freqtrade.arguments import Arguments, ARGS_PLOT_PROFIT from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.exchange import timeframe_to_seconds @@ -206,11 +206,7 @@ def plot_parse_args(args: List[str]) -> Namespace: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) - - arguments.common_optimize_options() - arguments.backtesting_options() - arguments.common_scripts_options() + arguments.build_args(optionlist=ARGS_PLOT_PROFIT) return arguments.parse_args() From b92c6cdf359076c564f5f4e2cfe748d510d4daa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jun 2019 20:32:54 +0200 Subject: [PATCH 672/928] Cleanup arguments and test_arguments --- freqtrade/arguments.py | 334 +----------------------------- freqtrade/tests/test_arguments.py | 10 +- 2 files changed, 8 insertions(+), 336 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 278f21c97..f020252f8 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -354,8 +354,9 @@ ARGS_LIST_EXCHANGE = ["print_one_column"] ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", - "export", "exportfilename", "timerange", "refresh_pairs", "live"]) + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", + "trade_source", "export", "exportfilename", "timerange", + "refresh_pairs", "live"]) ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ["pairs", "timerange", "export", "exportfilename"]) @@ -419,244 +420,6 @@ class Arguments(object): opt = AVAILABLE_CLI_OPTIONS[val] parser.add_argument(*opt.cli, **opt.kwargs) - def common_options(self) -> None: - """ - Parses arguments that are common for the main Freqtrade, all subcommands and scripts. - """ - parser = self.parser - - parser.add_argument( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - dest='loglevel', - default=0, - ) - parser.add_argument( - '--logfile', - help='Log to the file specified.', - dest='logfile', - metavar='FILE', - ) - parser.add_argument( - '--version', - action='version', - version=f'%(prog)s {__version__}' - ) - parser.add_argument( - '-c', '--config', - help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' - f'Multiple --config options may be used. ' - f'Can be set to `-` to read config from stdin.', - dest='config', - action='append', - metavar='PATH', - ) - parser.add_argument( - '-d', '--datadir', - help='Path to backtest data.', - dest='datadir', - metavar='PATH', - ) - - def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses arguments common for Backtesting, Edge and Hyperopt modules. - :param parser: - """ - parser = subparser or self.parser - - parser.add_argument( - '-i', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - dest='ticker_interval', - ) - parser.add_argument( - '--timerange', - help='Specify what timerange of data to use.', - dest='timerange', - ) - parser.add_argument( - '--max_open_trades', - help='Specify max_open_trades to use.', - type=int, - dest='max_open_trades', - ) - parser.add_argument( - '--stake_amount', - help='Specify stake_amount.', - type=float, - dest='stake_amount', - ) - parser.add_argument( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your optimization commands with ' - 'up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) - - def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses given arguments for Backtesting module. - """ - parser = subparser or self.parser - - parser.add_argument( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - dest='position_stacking', - default=False - ) - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-l', '--live', - help='Use live data.', - action='store_true', - dest='live', - ) - parser.add_argument( - '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' - 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with `--export trades`, ' - 'the strategy-name is injected into the filename ' - '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', - nargs='+', - dest='strategy_list', - ) - parser.add_argument( - '--export', - help='Export backtest results, argument are: trades. ' - 'Example: `--export=trades`', - dest='export', - ) - parser.add_argument( - '--export-filename', - help='Save backtest results to the file with this filename (default: `%(default)s`). ' - 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', - default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - dest='exportfilename', - metavar='PATH', - ) - - def edge_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses given arguments for Edge module. - """ - parser = subparser or self.parser - - parser.add_argument( - '--stoplosses', - help='Defines a range of stoploss values against which edge will assess the strategy. ' - 'The format is "min,max,step" (without any space). ' - 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - dest='stoploss_range', - ) - - def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses given arguments for Hyperopt module. - """ - parser = subparser or self.parser - - parser.add_argument( - '--customhyperopt', - help='Specify hyperopt class name (default: `%(default)s`).', - dest='hyperopt', - default=constants.DEFAULT_HYPEROPT, - metavar='NAME', - ) - parser.add_argument( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - dest='position_stacking', - default=False - ) - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-e', '--epochs', - help='Specify number of epochs (default: %(default)d).', - dest='epochs', - default=constants.HYPEROPT_EPOCH, - type=int, - metavar='INT', - ) - parser.add_argument( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space-separated list. ' - 'Default: `%(default)s`.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - default='all', - nargs='+', - dest='spaces', - ) - parser.add_argument( - '--print-all', - help='Print all results, not only the best ones.', - action='store_true', - dest='print_all', - default=False - ) - parser.add_argument( - '-j', '--job-workers', - help='The number of concurrently running jobs for hyperoptimization ' - '(hyperopt worker processes). ' - 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' - 'If 1 is given, no parallel computing code is used at all.', - dest='hyperopt_jobs', - default=-1, - type=int, - metavar='JOBS', - ) - parser.add_argument( - '--random-state', - help='Set random state to some positive integer for reproducible hyperopt results.', - dest='hyperopt_random_state', - type=check_int_positive, - metavar='INT', - ) - parser.add_argument( - '--min-trades', - help="Set minimal desired number of trades for evaluations in the hyperopt " - "optimization path (default: 1).", - dest='hyperopt_min_trades', - default=1, - type=check_int_positive, - metavar='INT', - ) - - def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses given arguments for the list-exchanges command. - """ - parser = subparser or self.parser - - parser.add_argument( - '-1', '--one-column', - help='Print exchanges in one column.', - action='store_true', - dest='print_one_column', - ) - def _build_subcommands(self) -> None: """ Builds and attaches all subcommands. @@ -731,94 +494,3 @@ class Arguments(object): stop = int(stops) return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) - - def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None: - """ - Parses arguments common for scripts. - """ - parser = subparser or self.parser - - parser.add_argument( - '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', - dest='pairs', - ) - - def download_data_options(self) -> None: - """ - Parses given arguments for testdata download script - """ - parser = self.parser - - parser.add_argument( - '--pairs-file', - help='File containing a list of pairs to download.', - dest='pairs_file', - metavar='FILE', - ) - parser.add_argument( - '--days', - help='Download data for given number of days.', - dest='days', - type=check_int_positive, - metavar='INT', - ) - parser.add_argument( - '--exchange', - help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' - f'Only valid if no config is provided.', - dest='exchange', - ) - parser.add_argument( - '-t', '--timeframes', - help=f'Specify which tickers to download. Space-separated list. ' - f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', - choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w'], - nargs='+', - dest='timeframes', - ) - parser.add_argument( - '--erase', - help='Clean all existing data for the selected exchange/pairs/timeframes.', - dest='erase', - action='store_true' - ) - - def plot_dataframe_options(self) -> None: - """ - Parses given arguments for plot dataframe script - """ - parser = self.parser - - parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. ' - 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', - default='sma,ema3,ema5', - dest='indicators1', - ) - - parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. ' - 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', - default='macd,macdsignal', - dest='indicators2', - ) - parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting. Notice: too high values cause huge files. ' - 'Default: %(default)s.', - dest='plot_limit', - default=750, - type=int, - ) - parser.add_argument( - '--trade-source', - help='Specify the source for trades (Can be DB or file (backtest file)) ' - 'Default: %(default)s', - dest='trade_source', - default="file", - choices=["DB", "file"] - ) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 38383a346..c9d2a2261 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,7 +3,8 @@ import argparse import pytest -from freqtrade.arguments import Arguments, TimeRange, check_int_positive, ARGS_DOWNLOADER +from freqtrade.arguments import (ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME, + Arguments, TimeRange, check_int_positive) # Parse common command-line-arguments. Used for all tools @@ -49,8 +50,8 @@ def test_parse_args_verbose() -> None: def test_common_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') - arguments.common_scripts_options() - args = arguments.get_parsed_arg() + arguments.build_args(ARGS_DOWNLOADER) + args = arguments.parse_args() assert args.pairs == 'ETH/BTC' @@ -195,8 +196,7 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') - arguments.common_scripts_options() - arguments.plot_dataframe_options() + arguments.build_args(ARGS_PLOT_DATAFRAME) pargs = arguments.parse_args(True) assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" From c106534663915bb04038dd83aff836c351cb31f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 20:13:40 +0200 Subject: [PATCH 673/928] Improve developer-document to include a note to keep both branches uptodate while creating a changelog. Cost me ~5 minutes doing the 2019.6 release... --- docs/developer.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index cf6b5d2cd..a5cf837e4 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -130,7 +130,7 @@ If the day shows the same day, then the last candle can be assumed as incomplete This part of the documentation is aimed at maintainers, and shows how to create a release. -### create release branch +### Create release branch ``` bash # make sure you're in develop branch @@ -144,7 +144,10 @@ git checkout -b new_release * Commit this part * push that branch to the remote and create a PR against the master branch -### create changelog from git commits +### Create changelog from git commits + +!!! Note + Make sure that both master and develop are up-todate!. ``` bash # Needs to be done before merging / pulling that branch. From 8e92fc62a39ba4fb60d018b980ab54bcf6774db2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 20:16:31 +0200 Subject: [PATCH 674/928] Use correct new versioning now --- docs/developer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index a5cf837e4..716148788 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -140,7 +140,7 @@ git checkout develop git checkout -b new_release ``` -* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) +* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for June 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -163,5 +163,5 @@ git log --oneline --no-decorate --no-merges master..develop ### After-release -* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). +* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). * Create a PR against develop to update that branch. From 353437bbd1f95fae76f489471f1058c2f5869ab8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Jun 2019 21:08:40 +0200 Subject: [PATCH 675/928] 07 is July!! --- docs/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 716148788..f58e0597d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -140,7 +140,7 @@ git checkout develop git checkout -b new_release ``` -* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for June 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. +* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch From 1d5c3f34ae357badcfdf196ad7571ac4a2835d1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jun 2019 19:59:57 +0200 Subject: [PATCH 676/928] Update qtpylib from source --- freqtrade/vendor/qtpylib/indicators.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 6edf626f0..b3b2ac533 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -213,8 +213,7 @@ def atr(bars, window=14, exp=False): else: res = rolling_mean(tr, window) - res = pd.Series(res) - return (res.shift(1) * (window - 1) + res) / window + return pd.Series(res) # --------------------------------------------- @@ -602,6 +601,14 @@ def pvt(bars): bars['close'].shift(1)) * bars['volume'] return trend.cumsum() + +def chopiness(bars, window=14): + atrsum = true_range(bars).rolling(window).sum() + highs = bars['high'].rolling(window).max() + lows = bars['low'].rolling(window).min() + return 100 * np.log10(atrsum / (highs - lows)) / np.log10(window) + + # ============================================= @@ -629,6 +636,7 @@ PandasObject.rsi = rsi PandasObject.stoch = stoch PandasObject.zscore = zscore PandasObject.pvt = pvt +PandasObject.chopiness = chopiness PandasObject.tdi = tdi PandasObject.true_range = true_range PandasObject.mid_price = mid_price From 6fc6eaf742823b9a2e53415a4220faa5f35589a3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 26 Jun 2019 22:23:16 +0300 Subject: [PATCH 677/928] minor: couple of typos fixed --- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/tests/strategy/test_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index b2743a417..47218bef4 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -132,7 +132,7 @@ class StrategyResolver(IResolver): abs_paths.insert(0, Path(extra_dir).resolve()) if ":" in strategy_name: - logger.info("loading base64 endocded strategy") + logger.info("loading base64 encoded strategy") strat = strategy_name.split(":") if len(strat) == 2: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 15d1c18ef..8971b041c 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -66,7 +66,7 @@ def test_load_strategy(result): assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_strategy_byte64(result): +def test_load_strategy_base64(result): with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) From 05d93cda16ffb9582e2c8e44ba2022a2b67adb4c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 01:03:38 +0300 Subject: [PATCH 678/928] fix #1963 --- freqtrade/freqtradebot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b6fc005dd..b3e0ef59d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,8 +478,11 @@ class FreqtradeBot(object): return order_amount # use fee from order-dict if possible - if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}): - if trade.pair.startswith(order['fee']['currency']): + if 'fee' in order and order['fee'] is not None and \ + (order['fee'].keys() >= {'currency', 'cost'}): + if order['fee']['currency'] is not None and \ + order['fee']['cost'] is not None and \ + trade.pair.startswith(order['fee']['currency']): new_amount = order_amount - order['fee']['cost'] logger.info("Applying fee on amount for %s (from %s to %s) from Order", trade, order['amount'], new_amount) @@ -496,9 +499,12 @@ class FreqtradeBot(object): fee_abs = 0 for exectrade in trades: amount += exectrade['amount'] - if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}): + if "fee" in exectrade and exectrade['fee'] is not None and \ + (exectrade['fee'].keys() >= {'currency', 'cost'}): # only applies if fee is in quote currency! - if trade.pair.startswith(exectrade['fee']['currency']): + if exectrade['fee']['currency'] is not None and \ + exectrade['fee']['cost'] is not None and \ + trade.pair.startswith(exectrade['fee']['currency']): fee_abs += exectrade['fee']['cost'] if amount != order_amount: From 3043a8d9c9b52310d8c590806b45c5f6a4ae6af0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:20:22 +0200 Subject: [PATCH 679/928] Be more explicit about what's missing --- docs/installation.md | 3 +++ setup.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index f0c536ade..d74280c9e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,6 +18,9 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. + ```bash git clone git@github.com:freqtrade/freqtrade.git cd freqtrade diff --git a/setup.sh b/setup.sh index 8b5531746..c99a98eb1 100755 --- a/setup.sh +++ b/setup.sh @@ -4,7 +4,7 @@ function check_installed_pip() { ${PYTHON} -m pip > /dev/null if [ $? -ne 0 ]; then - echo "pip not found. Please make sure that pip is available for ${PYTHON}." + echo "pip not found (called as '${PYTHON} -m pip'). Please make sure that pip is available for ${PYTHON}." exit 1 fi } From f04d49886bca1d56b0f149cd6a9c47e0a6a6de76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:29:18 +0200 Subject: [PATCH 680/928] Add test to verify behaviour if currency in fee-dict is None --- freqtrade/tests/test_freqtradebot.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 87b344853..89d3bba5f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2886,6 +2886,30 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo assert freqtrade.get_real_amount(trade, buy_order_fee) == amount +def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker): + + limit_buy_order = deepcopy(buy_order_fee) + limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} + trades_for_order[0]['fee']['currency'] = None + + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + amount = sum(x['amount'] for x in trades_for_order) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Amount does not change + assert freqtrade.get_real_amount(trade, limit_buy_order) == amount + + def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 From f8dd0b0cb318434e2479460b7177f11525ae9f70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 06:32:26 +0200 Subject: [PATCH 681/928] Use parenteses instead of \ seperators --- freqtrade/freqtradebot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b3e0ef59d..a62591b15 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,11 +478,11 @@ class FreqtradeBot(object): return order_amount # use fee from order-dict if possible - if 'fee' in order and order['fee'] is not None and \ - (order['fee'].keys() >= {'currency', 'cost'}): - if order['fee']['currency'] is not None and \ - order['fee']['cost'] is not None and \ - trade.pair.startswith(order['fee']['currency']): + if ('fee' in order and order['fee'] is not None and + (order['fee'].keys() >= {'currency', 'cost'})): + if (order['fee']['currency'] is not None and + order['fee']['cost'] is not None and + trade.pair.startswith(order['fee']['currency'])): new_amount = order_amount - order['fee']['cost'] logger.info("Applying fee on amount for %s (from %s to %s) from Order", trade, order['amount'], new_amount) @@ -499,12 +499,12 @@ class FreqtradeBot(object): fee_abs = 0 for exectrade in trades: amount += exectrade['amount'] - if "fee" in exectrade and exectrade['fee'] is not None and \ - (exectrade['fee'].keys() >= {'currency', 'cost'}): + if ("fee" in exectrade and exectrade['fee'] is not None and + (exectrade['fee'].keys() >= {'currency', 'cost'})): # only applies if fee is in quote currency! - if exectrade['fee']['currency'] is not None and \ - exectrade['fee']['cost'] is not None and \ - trade.pair.startswith(exectrade['fee']['currency']): + if (exectrade['fee']['currency'] is not None and + exectrade['fee']['cost'] is not None and + trade.pair.startswith(exectrade['fee']['currency'])): fee_abs += exectrade['fee']['cost'] if amount != order_amount: From 98681b78b4deeddc4ac1cc4bf91359cdc1fa5a6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 07:06:11 +0200 Subject: [PATCH 682/928] Show ifferent message for balance in dry-run --- freqtrade/rpc/rpc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 048ebec63..f5adffc65 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -298,7 +298,10 @@ class RPC(object): 'est_btc': est_btc, }) if total == 0.0: - raise RPCException('all balances are zero') + if self._freqtrade.config.get('dry_run', False): + raise RPCException('Running in Dry Run, balances are not available.') + else: + raise RPCException('All balances are zero.') symbol = fiat_display_currency value = self._fiat_converter.convert_amount(total, 'BTC', From 6643b83afe0f92170bb822ad530ca85595897d49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 07:06:35 +0200 Subject: [PATCH 683/928] Update tests to test both balance versions --- freqtrade/tests/rpc/test_rpc_telegram.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b34e214af..673536993 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -559,10 +559,32 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) + freqtradebot.config['dry_run'] = False telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert 'all balances are zero' in result + assert 'All balances are zero.' in result + + +def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) + result = msg_mock.call_args_list[0][0][0] + assert msg_mock.call_count == 1 + assert "Running in Dry Run, balances are not available." in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: From e5a8030dd7883d28797b8797f1b3207fa3623c1f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 16:42:10 +0300 Subject: [PATCH 684/928] comment added --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e0660eacb..d963b39c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -265,6 +265,7 @@ class Backtesting(object): closerate = - (trade.open_rate * roi + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) else: + # This should not be reached... closerate = sell_row.open else: closerate = sell_row.open From 16a9e6b72f54c64496afb4dfd5d4d00b6cdc8596 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jun 2019 19:51:04 +0200 Subject: [PATCH 685/928] Improve install documentation --- docs/installation.md | 56 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d74280c9e..544706f87 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,12 +4,22 @@ This page explains how to prepare your environment for running the bot. ## Prerequisite +### Requirements + +Click each one for install guide: + +* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [pip](https://pip.pypa.io/en/stable/installing/) +* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) + +### API keys + Before running your bot in production you will need to setup few external API. In production mode, the bot will require valid Exchange API credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). -- [Setup your exchange account](#setup-your-exchange-account) - ### Setup your exchange account You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. @@ -33,7 +43,7 @@ git checkout develop ## Easy Installation - Linux Script -If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. +If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. ```bash $ ./setup.sh @@ -48,7 +58,7 @@ usage: This script will install everything you need to run the bot: -* Mandatory software as: `Python3`, `ta-lib`, `wget` +* Mandatory software as: `ta-lib` * Setup your virtualenv * Configure your `config.json` file @@ -73,24 +83,16 @@ Config parameter is a `config.json` configurator. This script will ask you quest We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. -### Requirements - -Click each one for install guide: - -* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) -* [pip](https://pip.pypa.io/en/stable/installing/) -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. ### Linux - Ubuntu 16.04 -#### Install Python 3.6, Git, and wget +#### Install necessary dependencies ```bash -sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update -sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git +sudo apt-get install build-essential git ``` #### Raspberry Pi / Raspbian @@ -114,14 +116,6 @@ python3 -m pip install -r requirements-common.txt python3 -m pip install -e . ``` -### MacOS - -#### Install Python 3.6, git and wget - -```bash -brew install python3 git wget -``` - ### Common #### 1. Install TA-Lib @@ -162,7 +156,7 @@ git clone https://github.com/freqtrade/freqtrade.git ``` -Optionally checkout the stable/master branch: +Optionally checkout the master branch to get the latest stable release: ```bash git checkout master @@ -180,9 +174,9 @@ cp config.json.example config.json #### 5. Install python dependencies ``` bash -pip3 install --upgrade pip -pip3 install -r requirements.txt -pip3 install -e . +python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt +python3 -m pip install -e . ``` #### 6. Run the Bot @@ -190,7 +184,7 @@ pip3 install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3.6 freqtrade -c config.json +python3 freqtrade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. @@ -220,7 +214,7 @@ when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd as the watchdog. -!!! Note +!!! Note The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ @@ -240,8 +234,6 @@ If that is not available on your system, feel free to try the instructions below git clone https://github.com/freqtrade/freqtrade.git ``` -copy paste `config.json` to ``\path\freqtrade-develop\freqtrade` - #### Install ta-lib Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). From 21bf01a24c9569e21b2d38a9aca6b03e84be2652 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 27 Jun 2019 22:29:17 +0300 Subject: [PATCH 686/928] partial freqtradebot cleanup --- freqtrade/freqtradebot.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a62591b15..b103d73a7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -592,13 +592,13 @@ class FreqtradeBot(object): logger.info(' order book asks top %s: %0.8f', i, order_book_rate) sell_rate = order_book_rate - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -668,7 +668,7 @@ class FreqtradeBot(object): if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) - self.notify_sell(trade) + self._notify_sell(trade) return True # Finally we check if stoploss on exchange should be moved up because of trailing. @@ -713,13 +713,15 @@ class FreqtradeBot(object): logger.exception(f"Could create trailing stoploss order " f"for pair {trade.pair}.") - def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: - if self.edge: - stoploss = self.edge.stoploss(trade.pair) - should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss) - else: - should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell) + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + buy: bool, sell: bool) -> bool: + """ + Check and execute sell + """ + should_sell = self.strategy.should_sell( + trade, sell_rate, datetime.utcnow(), buy, sell, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + ) if should_sell.sell_flag: self.execute_sell(trade, sell_rate, should_sell.sell_type) @@ -873,9 +875,9 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() - self.notify_sell(trade) + self._notify_sell(trade) - def notify_sell(self, trade: Trade): + def _notify_sell(self, trade: Trade): """ Sends rpc notification when a sell occured. """ From 4f5e212f87aa9569f80704d74812486ba1598727 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 28 Jun 2019 01:01:51 +0300 Subject: [PATCH 687/928] fix #1978 --- freqtrade/configuration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index d74b712c3..82349700e 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -170,7 +170,7 @@ class Configuration(object): self._load_logging_config(config) # Support for sd_notify - if self.args.sd_notify: + if 'sd_notify' in self.args and self.args.sd_notify: config['internals'].update({'sd_notify': True}) # Add dynamic_whitelist if found @@ -186,7 +186,8 @@ class Configuration(object): '(not applicable with Backtesting and Hyperopt)' ) - if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL: + if ('db_url' in self.args and self.args.db_url and + self.args.db_url != constants.DEFAULT_DB_PROD_URL): config.update({'db_url': self.args.db_url}) logger.info('Parameter --db-url detected ...') From 0436811cf06f44a77f4164d5daa2521a72a99f5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jun 2019 06:47:40 +0200 Subject: [PATCH 688/928] Use mode OTHER, nto backtesting --- scripts/plot_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7e81af925..232fb7aad 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -130,7 +130,7 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: parsed_args = arguments.parse_args() # Load the configuration - config = setup_configuration(parsed_args, RunMode.BACKTEST) + config = setup_configuration(parsed_args, RunMode.OTHER) return config From 044be3b93e0c51c0826a0205219645ed2e4c32f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 16:57:04 +0200 Subject: [PATCH 689/928] Add create_cum_profit column --- freqtrade/arguments.py | 2 +- freqtrade/data/btanalysis.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f020252f8..cf2c52ed6 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -359,7 +359,7 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + "refresh_pairs", "live"]) ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + - ["pairs", "timerange", "export", "exportfilename"]) + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) class TimeRange(NamedTuple): diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5a0dee042..30fd5bc93 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -109,3 +109,15 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & (trades['close_time'] <= dataframe.iloc[-1]['date'])] return trades + + +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): + """ + Adds a column `col_name` with the cumulative profit for the given trades array. + """ + df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() + # Set first value to 0 + df.loc[df.iloc[0].name, col_name] = 0 + # FFill to get continuous + df[col_name] = df[col_name].ffill() + return df From e8796e009c47909fef84b4f677cd754b03f9ac6b Mon Sep 17 00:00:00 2001 From: AlexV Date: Sat, 29 Jun 2019 17:20:10 +0200 Subject: [PATCH 690/928] adding bitstamp to list of bad exchanges. --- 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 a65294091..b649a7b65 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -723,7 +723,7 @@ class Exchange(object): def is_exchange_bad(exchange: str) -> bool: - return exchange in ['bitmex'] + return exchange in ['bitmex', 'bitstamp'] def is_exchange_available(exchange: str, ccxt_module=None) -> bool: From edd3fc88256d8322abda7284d35165d3079021db Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 17:19:42 +0200 Subject: [PATCH 691/928] Add test for create_cum_profit --- freqtrade/data/btanalysis.py | 3 +++ freqtrade/tests/data/test_btanalysis.py | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 30fd5bc93..dae891423 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -114,6 +114,9 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): """ Adds a column `col_name` with the cumulative profit for the given trades array. + :param df: DataFrame with date index + :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :return: Returns df with one additional column, col_name, containing the cumulative profit. """ df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() # Set first value to 0 diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 1cb48393d..4eca73934 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from arrow import Arrow import pytest +from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.arguments import TimeRange -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, +from freqtrade.arguments import TimeRange, Arguments +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.data.history import load_pair_history, make_testdata_path @@ -74,3 +74,19 @@ def test_extract_trades_of_period(): assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime + + +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") + + df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=None, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 From 79b4e2dc8596bd0424e551e29b9f333f5d099439 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 17:23:03 +0200 Subject: [PATCH 692/928] Rename generate_graph to generate_candlestick_graph --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 14 +++++++------- scripts/plot_dataframe.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index c058f7fb2..ae9889975 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -86,7 +86,7 @@ def plot_trades(fig, trades: pd.DataFrame): return fig -def generate_graph( +def generate_candlestick_graph( pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index ec81b93b8..46462bd76 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -8,7 +8,7 @@ from copy import deepcopy from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data -from freqtrade.plot.plotting import (generate_graph, generate_plot_file, +from freqtrade.plot.plotting import (generate_candlestick_graph, generate_plot_file, generate_row, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -95,7 +95,7 @@ def test_plot_trades(caplog): assert trade_sell.marker.color == 'red' -def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): +def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', @@ -110,8 +110,8 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): indicators1 = [] indicators2 = [] - fig = generate_graph(pair=pair, data=data, trades=None, - indicators1=indicators1, indicators2=indicators2) + fig = generate_candlestick_graph(pair=pair, data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) assert isinstance(fig, go.Figure) assert fig.layout.title.text == pair figure = fig.layout.figure @@ -131,7 +131,7 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): assert log_has("No sell-signals found.", caplog.record_tuples) -def test_generate_graph_no_trades(default_conf, mocker): +def test_generate_candlestick_graph_no_trades(default_conf, mocker): row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', @@ -147,8 +147,8 @@ def test_generate_graph_no_trades(default_conf, mocker): indicators1 = [] indicators2 = [] - fig = generate_graph(pair=pair, data=data, trades=None, - indicators1=indicators1, indicators2=indicators2) + fig = generate_candlestick_graph(pair=pair, data=data, trades=None, + indicators1=indicators1, indicators2=indicators2) assert isinstance(fig, go.Figure) assert fig.layout.title.text == pair figure = fig.layout.figure diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 232fb7aad..80773b3b0 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -36,7 +36,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import generate_graph, generate_plot_file +from freqtrade.plot.plotting import generate_candlestick_graph, generate_plot_file from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -105,7 +105,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): trades = trades.loc[trades['pair'] == pair] trades = extract_trades_of_period(dataframe, trades) - fig = generate_graph( + fig = generate_candlestick_graph( pair=pair, data=dataframe, trades=trades, From 4506832925717d59b2d307a296996768efa3da8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:07:25 +0200 Subject: [PATCH 693/928] Update docstring --- scripts/plot_dataframe.py | 14 +------------- scripts/plot_profit.py | 10 +--------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 80773b3b0..701672f29 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -2,19 +2,7 @@ """ Script to display when the bot will buy on specific pair(s) -Mandatory Cli parameters: --p / --pairs: pair(s) to examine - -Option but recommended --s / --strategy: strategy to use - - -Optional Cli parameters --d / --datadir: path to pair(s) backtest data ---timerange: specify what timerange of data to use. --l / --live: Live, to download the latest ticker for the pair(s) --db / --db-url: Show trades stored in database - +Use `python plot_dataframe.py --help` to display the command line arguments Indicators recommended Row 1: sma, ema3, ema5, ema10, ema50 diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 32bfae9cc..01dc260d9 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -2,15 +2,7 @@ """ Script to display profits -Mandatory Cli parameters: --p / --pair: pair to examine - -Optional Cli parameters --c / --config: specify configuration file --s / --strategy: strategy to use --d / --datadir: path to pair backtest data ---timerange: specify what timerange of data to use ---export-filename: Specify where the backtest export is located. +Use `python plot_profit.py --help` to display the command line arguments """ import json import logging From e50eee59cf5c5450ff035eaabce0c025dd1580ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:30:31 +0200 Subject: [PATCH 694/928] Seperate plot-name generation and plotting --- freqtrade/plot/plotting.py | 19 ++++++++++++------- freqtrade/tests/test_plotting.py | 17 ++++++++++++----- scripts/plot_dataframe.py | 6 ++++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ae9889975..f8a010068 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -204,7 +204,16 @@ def generate_candlestick_graph( return fig -def generate_plot_file(fig, pair, ticker_interval) -> None: +def generate_plot_filename(pair, ticker_interval) -> str: + pair_name = pair.replace("/", "_") + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + + logger.info('Generate plot file for %s', pair) + + return file_name + + +def generate_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot @@ -212,12 +221,8 @@ def generate_plot_file(fig, pair, ticker_interval) -> None: :param ticker_interval: Used as part of the filename :return: None """ - logger.info('Generate plot file for %s', pair) - - pair_name = pair.replace("/", "_") - file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' Path("user_data/plots").mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), - auto_open=False) + plot(fig, filename=str(Path('user_data/plots').joinpath(filename)), + auto_open=auto_open) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 46462bd76..527534522 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -1,15 +1,17 @@ +from copy import deepcopy from unittest.mock import MagicMock -from plotly import tools import plotly.graph_objs as go -from copy import deepcopy +from plotly import tools from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data -from freqtrade.plot.plotting import (generate_candlestick_graph, generate_plot_file, - generate_row, plot_trades) +from freqtrade.plot.plotting import (generate_candlestick_graph, + generate_plot_file, + generate_plot_filename, generate_row, + plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -178,10 +180,15 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker): assert trades_mock.call_count == 1 +def test_generate_Plot_filename(): + fn = generate_plot_filename("UNITTEST/BTC", "5m") + assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html" + + def test_generate_plot_file(mocker, caplog): fig = generage_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) - generate_plot_file(fig, "UNITTEST/BTC", "5m") + generate_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 701672f29..d97c6f041 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,7 +24,9 @@ from freqtrade.data import history from freqtrade.data.btanalysis import (extract_trades_of_period, load_backtest_data, load_trades_from_db) from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import generate_candlestick_graph, generate_plot_file +from freqtrade.plot.plotting import (generate_candlestick_graph, + generate_plot_file, + generate_plot_filename) from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -101,7 +103,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): indicators2=config["indicators2"].split(",") ) - generate_plot_file(fig, pair, ticker_interval) + generate_plot_file(fig, generate_plot_filename(pair, ticker_interval)) logger.info('End of ploting process %s plots generated', pair_counter) From 4218d569de7dfa3e400d0f4f7c2dd4698c6961f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:41:22 +0200 Subject: [PATCH 695/928] Only read trades once --- scripts/plot_dataframe.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index d97c6f041..07079304e 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -80,6 +80,11 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): live=config.get("live", False), ) + if config["trade_source"] == "DB": + trades = load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + trades = load_backtest_data(Path(config["exportfilename"])) + pair_counter = 0 for pair, data in tickers.items(): pair_counter += 1 @@ -87,18 +92,14 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): tickers = {} tickers[pair] = data dataframe = generate_dataframe(strategy, tickers, pair) - if config["trade_source"] == "DB": - trades = load_trades_from_db(config["db_url"]) - elif config["trade_source"] == "file": - trades = load_backtest_data(Path(config["exportfilename"])) - trades = trades.loc[trades['pair'] == pair] - trades = extract_trades_of_period(dataframe, trades) + trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( pair=pair, data=dataframe, - trades=trades, + trades=trades_pair, indicators1=config["indicators1"].split(","), indicators2=config["indicators2"].split(",") ) From 8aa327cb8a331119a919d217e3ea29beadad08b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:50:31 +0200 Subject: [PATCH 696/928] Add load_trades abstraction (to load trades from either DB or file) --- freqtrade/data/btanalysis.py | 14 ++++++++++++++ freqtrade/tests/data/test_btanalysis.py | 25 +++++++++++++++++++++++-- scripts/plot_dataframe.py | 8 ++------ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index dae891423..f6f7e5541 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -101,6 +101,20 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades +def load_trades(config) -> pd.DataFrame: + """ + Based on configuration option "trade_source": + * loads data from DB (using `db_url`) + * loads data from backtestfile (`using exportfilename`) + """ + if config["trade_source"] == "DB": + return load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + return load_backtest_data(Path(config["exportfilename"])) + else: + return None + + def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: """ Compare trades and backtested pair DataFrames to get trades performed on backtested period diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4eca73934..01e5dc90d 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -4,10 +4,11 @@ import pytest from arrow import Arrow from pandas import DataFrame, to_datetime -from freqtrade.arguments import TimeRange, Arguments +from freqtrade.arguments import Arguments, TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, - load_backtest_data, load_trades_from_db) + load_backtest_data, load_trades, + load_trades_from_db) from freqtrade.data.history import load_pair_history, make_testdata_path from freqtrade.tests.test_persistence import create_mock_trades @@ -76,6 +77,26 @@ def test_extract_trades_of_period(): assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime +def test_load_trades(default_conf, mocker): + db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock()) + bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) + + default_conf['trade_source'] = "DB" + load_trades(default_conf) + + assert db_mock.call_count == 1 + assert bt_mock.call_count == 0 + + db_mock.reset_mock() + bt_mock.reset_mock() + default_conf['trade_source'] = "file" + default_conf['exportfilename'] = "testfile.json" + load_trades(default_conf) + + assert db_mock.call_count == 0 + assert bt_mock.call_count == 1 + + def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 07079304e..dfd31e9ab 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,8 +21,7 @@ import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data import history -from freqtrade.data.btanalysis import (extract_trades_of_period, - load_backtest_data, load_trades_from_db) +from freqtrade.data.btanalysis import extract_trades_of_period, load_trades from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_candlestick_graph, generate_plot_file, @@ -80,10 +79,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): live=config.get("live", False), ) - if config["trade_source"] == "DB": - trades = load_trades_from_db(config["db_url"]) - elif config["trade_source"] == "file": - trades = load_backtest_data(Path(config["exportfilename"])) + trades = load_trades(config) pair_counter = 0 for pair, data in tickers.items(): From c3db4ebbc3ed4fff2206d508633c88aabdec06c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jun 2019 20:52:33 +0200 Subject: [PATCH 697/928] Revise plot_profit to use pandas functions where possible --- scripts/plot_profit.py | 145 ++++++++++------------------------------- 1 file changed, 35 insertions(+), 110 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 01dc260d9..5bff9b2dd 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -4,64 +4,28 @@ Script to display profits Use `python plot_profit.py --help` to display the command line arguments """ -import json import logging import sys from argparse import Namespace from pathlib import Path -from typing import List, Optional +from typing import List -import numpy as np +import pandas as pd import plotly.graph_objs as go from plotly import tools from plotly.offline import plot -from freqtrade.arguments import Arguments, ARGS_PLOT_PROFIT +from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.configuration import Configuration from freqtrade.data import history -from freqtrade.exchange import timeframe_to_seconds -from freqtrade.misc import common_datearray +from freqtrade.data.btanalysis import create_cum_profit, load_trades +from freqtrade.plot.plotting import generate_plot_file from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode - logger = logging.getLogger(__name__) -# data:: [ pair, profit-%, enter, exit, time, duration] -# data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65] -def make_profit_array(data: List, px: int, min_date: int, - interval: str, - filter_pairs: Optional[List] = None) -> np.ndarray: - pg = np.zeros(px) - filter_pairs = filter_pairs or [] - # Go through the trades - # and make an total profit - # array - for trade in data: - pair = trade[0] - if filter_pairs and pair not in filter_pairs: - continue - profit = trade[1] - trade_sell_time = int(trade[3]) - - ix = define_index(min_date, trade_sell_time, interval) - if ix < px: - logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4]) - pg[ix] += profit - - # rewrite the pg array to go from - # total profits at each timeframe - # to accumulated profits - pa = 0 - for x in range(0, len(pg)): - p = pg[x] # Get current total percent - pa += p # Add to the accumulated percent - pg[x] = pa # write back to save memory - - return pg - - def plot_profit(args: Namespace) -> None: """ Plots the total profit for all pairs. @@ -70,34 +34,15 @@ def plot_profit(args: Namespace) -> None: in helping out to find a good algorithm. """ - # We need to use the same pairs, same ticker_interval - # and same timeperiod as used in backtesting - # to match the tickerdata against the profits-results + # We need to use the same pairs and the same ticker_interval + # as used in backtesting / trading + # to match the tickerdata against the results timerange = Arguments.parse_timerange(args.timerange) config = Configuration(args, RunMode.OTHER).get_config() # Init strategy - try: - strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy - - except AttributeError: - logger.critical( - 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', - config.get('strategy') - ) - exit(1) - - # Load the profits results - try: - filename = args.exportfilename - with open(filename) as file: - data = json.load(file) - except FileNotFoundError: - logger.critical( - 'File "backtest-result.json" not found. This script require backtesting ' - 'results to run.\nPlease run a backtesting with the parameter --export.') - exit(1) + strategy = StrategyResolver(config).strategy # Take pairs from the cli otherwise switch to the pair in the config file if args.pairs: @@ -106,6 +51,11 @@ def plot_profit(args: Namespace) -> None: else: filter_pairs = config['exchange']['pair_whitelist'] + # Load the profits results + trades = load_trades(config) + + trades = trades[trades['pair'].isin(filter_pairs)] + ticker_interval = strategy.ticker_interval pairs = config['exchange']['pair_whitelist'] @@ -120,49 +70,28 @@ def plot_profit(args: Namespace) -> None: refresh_pairs=False, timerange=timerange ) - dataframes = strategy.tickerdata_to_dataframe(tickers) - # NOTE: the dataframes are of unequal length, - # 'dates' is an merged date array of them all. - - dates = common_datearray(dataframes) - min_date = int(min(dates).timestamp()) - max_date = int(max(dates).timestamp()) - num_iterations = define_index(min_date, max_date, ticker_interval) + 1 - - # Make an average close price of all the pairs that was involved. + # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - # We are essentially saying: - # array <- sum dataframes[*]['close'] / num_items dataframes - # FIX: there should be some onliner numpy/panda for this - avgclose = np.zeros(num_iterations) - num = 0 - for pair, pair_data in dataframes.items(): - close = pair_data['close'] - maxprice = max(close) # Normalize price to [0,1] - logger.info('Pair %s has length %s' % (pair, len(close))) - for x in range(0, len(close)): - avgclose[x] += close[x] / maxprice - # avgclose += close - num += 1 - avgclose /= num - # make an profits-growth array - pg = make_profit_array(data, num_iterations, min_date, ticker_interval, filter_pairs) + # Combine close-values for all pairs, rename columns to "pair" + df_comb = pd.concat([tickers[pair].set_index('date').rename( + {'close': pair}, axis=1)[pair] for pair in tickers], axis=1) + df_comb['mean'] = df_comb.mean(axis=1) + + # Add combined cumulative profit + df_comb = create_cum_profit(df_comb, trades, 'cum_profit') - # # Plot the pairs average close prices, and total profit growth - # - avgclose = go.Scattergl( - x=dates, - y=avgclose, + x=df_comb.index, + y=df_comb['mean'], name='Avg close price', ) profit = go.Scattergl( - x=dates, - y=pg, + x=df_comb.index, + y=df_comb['cum_profit'], name='Profit', ) @@ -172,23 +101,19 @@ def plot_profit(args: Namespace) -> None: fig.append_trace(profit, 2, 1) for pair in pairs: - pg = make_profit_array(data, num_iterations, min_date, ticker_interval, [pair]) + profit_col = f'cum_profit_{pair}' + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + pair_profit = go.Scattergl( - x=dates, - y=pg, - name=pair, + x=df_comb.index, + y=df_comb[profit_col], + name=f"Profit {pair}", ) fig.append_trace(pair_profit, 3, 1) - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html'))) - - -def define_index(min_date: int, max_date: int, ticker_interval: str) -> int: - """ - Return the index of a specific date - """ - interval_seconds = timeframe_to_seconds(ticker_interval) - return int((max_date - min_date) / interval_seconds) + generate_plot_file(fig, + filename='freqtrade-profit-plot.html', + auto_open=True) def plot_parse_args(args: List[str]) -> Namespace: From 700bab7279bd37d97528123d60b43dfba8b9e882 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:28:34 +0200 Subject: [PATCH 698/928] Rename generate_plot_file to store_plot_file --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 4 ++-- scripts/plot_dataframe.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f8a010068..68d61bb92 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -213,7 +213,7 @@ def generate_plot_filename(pair, ticker_interval) -> str: return file_name -def generate_plot_file(fig, filename: str, auto_open: bool = False) -> None: +def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 527534522..0e93e8fad 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -9,7 +9,7 @@ from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_candlestick_graph, - generate_plot_file, + store_plot_file, generate_plot_filename, generate_row, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -188,7 +188,7 @@ def test_generate_Plot_filename(): def test_generate_plot_file(mocker, caplog): fig = generage_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) - generate_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") + store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html") assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index dfd31e9ab..a0f4ef778 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,7 +24,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import extract_trades_of_period, load_trades from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (generate_candlestick_graph, - generate_plot_file, + store_plot_file, generate_plot_filename) from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -67,7 +67,6 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): # Set timerange to use timerange = Arguments.parse_timerange(config["timerange"]) - ticker_interval = strategy.ticker_interval tickers = history.load_data( datadir=Path(str(config.get("datadir"))), @@ -100,7 +99,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): indicators2=config["indicators2"].split(",") ) - generate_plot_file(fig, generate_plot_filename(pair, ticker_interval)) + store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval'])) logger.info('End of ploting process %s plots generated', pair_counter) From c87d27048bbd8e5c53bec3d49f0336dec1953dfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:28:49 +0200 Subject: [PATCH 699/928] align plot_profit to plot_dataframe --- scripts/plot_profit.py | 69 +++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 5bff9b2dd..f28763077 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -6,27 +6,25 @@ Use `python plot_profit.py --help` to display the command line arguments """ import logging import sys -from argparse import Namespace from pathlib import Path -from typing import List +from typing import Any, Dict, List import pandas as pd import plotly.graph_objs as go from plotly import tools -from plotly.offline import plot from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_trades -from freqtrade.plot.plotting import generate_plot_file -from freqtrade.resolvers import StrategyResolver +from freqtrade.optimize import setup_configuration +from freqtrade.plot.plotting import store_plot_file +from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def plot_profit(args: Namespace) -> None: +def plot_profit(config: Dict[str, Any]) -> None: """ Plots the total profit for all pairs. Note, the profit calculation isn't realistic. @@ -34,42 +32,33 @@ def plot_profit(args: Namespace) -> None: in helping out to find a good algorithm. """ + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange + + # Take pairs from the cli otherwise switch to the pair in the config file + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] + # We need to use the same pairs and the same ticker_interval # as used in backtesting / trading # to match the tickerdata against the results - timerange = Arguments.parse_timerange(args.timerange) + timerange = Arguments.parse_timerange(config["timerange"]) - config = Configuration(args, RunMode.OTHER).get_config() - - # Init strategy - strategy = StrategyResolver(config).strategy - - # Take pairs from the cli otherwise switch to the pair in the config file - if args.pairs: - filter_pairs = args.pairs - filter_pairs = filter_pairs.split(',') - else: - filter_pairs = config['exchange']['pair_whitelist'] + tickers = history.load_data( + datadir=Path(str(config.get("datadir"))), + pairs=pairs, + ticker_interval=config['ticker_interval'], + refresh_pairs=config.get('refresh_pairs', False), + timerange=timerange, + exchange=exchange, + live=config.get("live", False), + ) # Load the profits results trades = load_trades(config) - trades = trades[trades['pair'].isin(filter_pairs)] - - ticker_interval = strategy.ticker_interval - pairs = config['exchange']['pair_whitelist'] - - if filter_pairs: - pairs = list(set(pairs) & set(filter_pairs)) - logger.info('Filter, keep pairs %s' % pairs) - - tickers = history.load_data( - datadir=Path(str(config.get('datadir'))), - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=False, - timerange=timerange - ) + trades = trades[trades['pair'].isin(pairs)] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend @@ -111,12 +100,12 @@ def plot_profit(args: Namespace) -> None: ) fig.append_trace(pair_profit, 3, 1) - generate_plot_file(fig, + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) -def plot_parse_args(args: List[str]) -> Namespace: +def plot_parse_args(args: List[str]) -> Dict[str, Any]: """ Parse args passed to the script :param args: Cli arguments @@ -125,7 +114,11 @@ def plot_parse_args(args: List[str]) -> Namespace: arguments = Arguments(args, 'Graph profits') arguments.build_args(optionlist=ARGS_PLOT_PROFIT) - return arguments.parse_args() + parsed_args = arguments.parse_args() + + # Load the configuration + config = setup_configuration(parsed_args, RunMode.OTHER) + return config def main(sysargv: List[str]) -> None: From 42ea0a19d2b79e319398569aa543c84ac46c3e92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:41:43 +0200 Subject: [PATCH 700/928] create FTPlots class to combine duplicate script code --- freqtrade/plot/plotting.py | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 68d61bb92..868ffbc31 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,8 +1,14 @@ import logging -from typing import List +from pathlib import Path +from typing import Any, Dict, List, Optional import pandas as pd -from pathlib import Path + +from freqtrade.arguments import Arguments +from frqtrade.exchange import Exchange +from freqtrade.data import history +from freqtrade.data.btanalysis import load_trades +from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -16,6 +22,38 @@ except ImportError: exit(1) +class FTPlots(): + + def __init__(self, config: Dict[str, Any]): + self._config = config + self.exchange: Optional[Exchange] = None + + if self._config.get("live", False) or self._config.get("refresh_pairs", False): + self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), + self._config).exchange + + self.strategy = StrategyResolver(self._config).strategy + if "pairs" in self._config: + self.pairs = self._config["pairs"].split(',') + else: + self.pairs = self._config["exchange"]["pair_whitelist"] + + # Set timerange to use + self.timerange = Arguments.parse_timerange(self._config["timerange"]) + + self.tickers = history.load_data( + datadir=Path(str(self._config.get("datadir"))), + pairs=self.pairs, + ticker_interval=self._config['ticker_interval'], + refresh_pairs=self._config.get('refresh_pairs', False), + timerange=self.timerange, + exchange=self.exchange, + live=self._config.get("live", False), + ) + + self.trades = load_trades(self._config) + + def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: """ Generator all the indicator selected by the user for a specific row From 88545d882ca7df3b52586b0058ad9bf94fc0fe2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:42:10 +0200 Subject: [PATCH 701/928] Use FTPlots class in plot-scripts --- scripts/plot_dataframe.py | 36 +++++------------------------- scripts/plot_profit.py | 46 +++++++-------------------------------- 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index a0f4ef778..431c6239c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -14,19 +14,16 @@ Example of usage: """ import logging import sys -from pathlib import Path from typing import Any, Dict, List import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments -from freqtrade.data import history -from freqtrade.data.btanalysis import extract_trades_of_period, load_trades +from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (generate_candlestick_graph, +from freqtrade.plot.plotting import (FTPlots, generate_candlestick_graph, store_plot_file, generate_plot_filename) -from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -57,38 +54,17 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange - - strategy = StrategyResolver(config).strategy - if "pairs" in config: - pairs = config["pairs"].split(',') - else: - pairs = config["exchange"]["pair_whitelist"] - - # Set timerange to use - timerange = Arguments.parse_timerange(config["timerange"]) - - tickers = history.load_data( - datadir=Path(str(config.get("datadir"))), - pairs=pairs, - ticker_interval=config['ticker_interval'], - refresh_pairs=config.get('refresh_pairs', False), - timerange=timerange, - exchange=exchange, - live=config.get("live", False), - ) - - trades = load_trades(config) + plot = FTPlots(config) pair_counter = 0 - for pair, data in tickers.items(): + for pair, data in plot.tickers.items(): pair_counter += 1 logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(strategy, tickers, pair) + dataframe = generate_dataframe(plot.strategy, tickers, pair) - trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = plot.trades.loc[plot.trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index f28763077..cd507100f 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -6,7 +6,6 @@ Use `python plot_profit.py --help` to display the command line arguments """ import logging import sys -from pathlib import Path from typing import Any, Dict, List import pandas as pd @@ -14,11 +13,9 @@ import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data import history -from freqtrade.data.btanalysis import create_cum_profit, load_trades +from freqtrade.data.btanalysis import create_cum_profit from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import store_plot_file -from freqtrade.resolvers import ExchangeResolver +from freqtrade.plot.plotting import FTPlots, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -31,41 +28,16 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ + plot = FTPlots(config) - exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange - - # Take pairs from the cli otherwise switch to the pair in the config file - if "pairs" in config: - pairs = config["pairs"].split(',') - else: - pairs = config["exchange"]["pair_whitelist"] - - # We need to use the same pairs and the same ticker_interval - # as used in backtesting / trading - # to match the tickerdata against the results - timerange = Arguments.parse_timerange(config["timerange"]) - - tickers = history.load_data( - datadir=Path(str(config.get("datadir"))), - pairs=pairs, - ticker_interval=config['ticker_interval'], - refresh_pairs=config.get('refresh_pairs', False), - timerange=timerange, - exchange=exchange, - live=config.get("live", False), - ) - - # Load the profits results - trades = load_trades(config) - - trades = trades[trades['pair'].isin(pairs)] + trades = plot.trades[plot.trades['pair'].isin(plot.pairs)] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend # Combine close-values for all pairs, rename columns to "pair" - df_comb = pd.concat([tickers[pair].set_index('date').rename( - {'close': pair}, axis=1)[pair] for pair in tickers], axis=1) + df_comb = pd.concat([plot.tickers[pair].set_index('date').rename( + {'close': pair}, axis=1)[pair] for pair in plot.tickers], axis=1) df_comb['mean'] = df_comb.mean(axis=1) # Add combined cumulative profit @@ -89,7 +61,7 @@ def plot_profit(config: Dict[str, Any]) -> None: fig.append_trace(avgclose, 1, 1) fig.append_trace(profit, 2, 1) - for pair in pairs: + for pair in plot.pairs: profit_col = f'cum_profit_{pair}' df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) @@ -100,9 +72,7 @@ def plot_profit(config: Dict[str, Any]) -> None: ) fig.append_trace(pair_profit, 3, 1) - store_plot_file(fig, - filename='freqtrade-profit-plot.html', - auto_open=True) + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From 0d5e94b147b74a201914b2558e69a5cf2470bd9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:44:50 +0200 Subject: [PATCH 702/928] Rename generate_row to add_indicators --- freqtrade/plot/plotting.py | 6 +++--- freqtrade/tests/test_plotting.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 868ffbc31..8ac4800bb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -54,7 +54,7 @@ class FTPlots(): self.trades = load_trades(self._config) -def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: +def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: """ Generator all the indicator selected by the user for a specific row :param fig: Plot figure to append to @@ -224,7 +224,7 @@ def generate_candlestick_graph( fig.append_trace(bb_upper, 1, 1) # Add indicators to main plot - fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) + fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) fig = plot_trades(fig, trades) @@ -237,7 +237,7 @@ def generate_candlestick_graph( fig.append_trace(volume, 2, 1) # Add indicators to seperate row - fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) + fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) return fig diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 0e93e8fad..e4d913f70 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -10,14 +10,14 @@ from freqtrade.data import history from freqtrade.data.btanalysis import load_backtest_data from freqtrade.plot.plotting import (generate_candlestick_graph, store_plot_file, - generate_plot_filename, generate_row, + generate_plot_filename, add_indicators, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re def fig_generating_mock(fig, *args, **kwargs): - """ Return Fig - used to mock generate_row and plot_trades""" + """ Return Fig - used to mock add_indicators and plot_trades""" return fig @@ -36,7 +36,7 @@ def generage_empty_figure(): ) -def test_generate_row(default_conf, caplog): +def test_add_indicators(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) @@ -51,20 +51,20 @@ def test_generate_row(default_conf, caplog): fig = generage_empty_figure() # Row 1 - fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) + fig1 = add_indicators(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) figure = fig1.layout.figure ema10 = find_trace_in_fig_data(figure.data, "ema10") assert isinstance(ema10, go.Scatter) assert ema10.yaxis == "y" - fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) + fig2 = add_indicators(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) figure = fig2.layout.figure macd = find_trace_in_fig_data(figure.data, "macd") assert isinstance(macd, go.Scatter) assert macd.yaxis == "y3" # No indicator found - fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) + fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) assert fig == fig3 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) @@ -98,7 +98,7 @@ def test_plot_trades(caplog): def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog): - row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) @@ -134,7 +134,7 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, c def test_generate_candlestick_graph_no_trades(default_conf, mocker): - row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', + row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', MagicMock(side_effect=fig_generating_mock)) From 348513c1514311894035642591d046d035c73df3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 09:47:07 +0200 Subject: [PATCH 703/928] Improve formatting of plotting.py --- freqtrade/plot/plotting.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 8ac4800bb..2fe15d320 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -84,7 +84,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools def plot_trades(fig, trades: pd.DataFrame): """ - Plot trades to "fig" + Add trades to "fig" """ # Trades can be empty if trades is not None and len(trades) > 0: @@ -124,13 +124,9 @@ def plot_trades(fig, trades: pd.DataFrame): return fig -def generate_candlestick_graph( - pair: str, - data: pd.DataFrame, - trades: pd.DataFrame = None, - indicators1: List[str] = [], - indicators2: List[str] = [], -) -> go.Figure: +def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, + indicators1: List[str] = [], + indicators2: List[str] = [],) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators @@ -243,6 +239,9 @@ def generate_candlestick_graph( def generate_plot_filename(pair, ticker_interval) -> str: + """ + Generate filenames per pair/ticker_interval to be used for storing plots + """ pair_name = pair.replace("/", "_") file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' From 6b387d320e594bd8c82bd91cca951a6a09dc3050 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:04:43 +0200 Subject: [PATCH 704/928] extract combine_tickers to btanalysis --- freqtrade/data/btanalysis.py | 19 ++++++++++++++++++- freqtrade/tests/data/test_btanalysis.py | 20 ++++++++++++++++++-- scripts/plot_profit.py | 6 ++---- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f6f7e5541..834d41263 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -3,6 +3,7 @@ Helpers when analyzing backtest data """ import logging from pathlib import Path +from typing import Dict import numpy as np import pandas as pd @@ -125,7 +126,23 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p return trades -def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str): +def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"): + """ + Combine multiple dataframes "column" + :param tickers: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: DataFrame with the column renamed to the dict key, and a column + named mean, containing the mean of all pairs. + """ + df_comb = pd.concat([tickers[pair].set_index('date').rename( + {column: pair}, axis=1)[pair] for pair in tickers], axis=1) + + df_comb['mean'] = df_comb.mean(axis=1) + + return df_comb + + +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 01e5dc90d..e8872f9a4 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -5,11 +5,14 @@ from arrow import Arrow from pandas import DataFrame, to_datetime from freqtrade.arguments import Arguments, TimeRange -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, + combine_tickers_with_mean, + create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades, load_trades_from_db) -from freqtrade.data.history import load_pair_history, make_testdata_path +from freqtrade.data.history import (load_data, load_pair_history, + make_testdata_path) from freqtrade.tests.test_persistence import create_mock_trades @@ -97,6 +100,19 @@ def test_load_trades(default_conf, mocker): assert bt_mock.call_count == 1 +def test_combine_tickers_with_mean(): + pairs = ["ETH/BTC", "XLM/BTC"] + tickers = load_data(datadir=None, + pairs=pairs, + ticker_interval='5m' + ) + df = combine_tickers_with_mean(tickers) + assert isinstance(df, DataFrame) + assert "ETH/BTC" in df.columns + assert "XLM/BTC" in df.columns + assert "mean" in df.columns + + def test_create_cum_profit(): filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index cd507100f..248eeb7b0 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -13,7 +13,7 @@ import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data.btanalysis import create_cum_profit +from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import FTPlots, store_plot_file from freqtrade.state import RunMode @@ -36,9 +36,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # this could be useful to gauge the overall market trend # Combine close-values for all pairs, rename columns to "pair" - df_comb = pd.concat([plot.tickers[pair].set_index('date').rename( - {'close': pair}, axis=1)[pair] for pair in plot.tickers], axis=1) - df_comb['mean'] = df_comb.mean(axis=1) + df_comb = combine_tickers_with_mean(plot.tickers, "close") # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit') From 0a184d380e0c0c045f7a4539da100fade9db51a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:14:33 +0200 Subject: [PATCH 705/928] create add_profit function --- freqtrade/plot/plotting.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2fe15d320..f8736fe74 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,9 +5,9 @@ from typing import Any, Dict, List, Optional import pandas as pd from freqtrade.arguments import Arguments -from frqtrade.exchange import Exchange +from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import load_trades, create_cum_profit from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -82,7 +82,27 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools return fig -def plot_trades(fig, trades: pd.DataFrame): +def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.make_subplots: + """ + Add profit-plot + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param column: Column to use for plot + :param name: Name to use + :return: fig with added profit plot + """ + profit = go.Scattergl( + x=data.index, + y=data[column], + name=name, + ) + fig.append_trace(profit, row, 1) + + return fig + + +def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: """ Add trades to "fig" """ From 5a11ffcad808ede0990c72356e8e19c627aca0e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:24:10 +0200 Subject: [PATCH 706/928] Add test for add_profit --- freqtrade/data/btanalysis.py | 2 -- freqtrade/tests/test_plotting.py | 26 +++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 834d41263..556666f4e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -112,8 +112,6 @@ def load_trades(config) -> pd.DataFrame: return load_trades_from_db(config["db_url"]) elif config["trade_source"] == "file": return load_backtest_data(Path(config["exportfilename"])) - else: - return None def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index e4d913f70..2a73ad24e 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,11 +5,11 @@ from unittest.mock import MagicMock import plotly.graph_objs as go from plotly import tools -from freqtrade.arguments import TimeRange +from freqtrade.arguments import TimeRange, Arguments from freqtrade.data import history -from freqtrade.data.btanalysis import load_backtest_data +from freqtrade.data.btanalysis import load_backtest_data, create_cum_profit from freqtrade.plot.plotting import (generate_candlestick_graph, - store_plot_file, + store_plot_file, add_profit, generate_plot_filename, add_indicators, plot_trades) from freqtrade.strategy.default_strategy import DefaultStrategy @@ -194,3 +194,23 @@ def test_generate_plot_file(mocker, caplog): assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") + + +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") + + df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=None, timerange=timerange) + fig = generage_empty_figure() + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits") + + fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') + figure = fig1.layout.figure + profits = find_trace_in_fig_data(figure.data, "Profits") + assert isinstance(profits, go.Scattergl) + assert profits.yaxis == "y2" From 0b517584aacc12d9c1b054b77c29a52633c9b3b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:25:48 +0200 Subject: [PATCH 707/928] Use add_profit in script --- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/test_plotting.py | 12 ++++++------ scripts/plot_profit.py | 18 +++--------------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f8736fe74..4c45c0375 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -7,7 +7,7 @@ import pandas as pd from freqtrade.arguments import Arguments from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades, create_cum_profit +from freqtrade.data.btanalysis import load_trades from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 2a73ad24e..fb2c52e1e 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -5,13 +5,13 @@ from unittest.mock import MagicMock import plotly.graph_objs as go from plotly import tools -from freqtrade.arguments import TimeRange, Arguments +from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import load_backtest_data, create_cum_profit -from freqtrade.plot.plotting import (generate_candlestick_graph, - store_plot_file, add_profit, - generate_plot_filename, add_indicators, - plot_trades) +from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data +from freqtrade.plot.plotting import (add_indicators, add_profit, + generate_candlestick_graph, + generate_plot_filename, plot_trades, + store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 248eeb7b0..ad135b5e6 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,14 +8,13 @@ import logging import sys from typing import Any, Dict, List -import pandas as pd import plotly.graph_objs as go from plotly import tools from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, store_plot_file +from freqtrade.plot.plotting import FTPlots, store_plot_file, add_profit from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -48,27 +47,16 @@ def plot_profit(config: Dict[str, Any]) -> None: name='Avg close price', ) - profit = go.Scattergl( - x=df_comb.index, - y=df_comb['cum_profit'], - name='Profit', - ) - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) fig.append_trace(avgclose, 1, 1) - fig.append_trace(profit, 2, 1) + fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') for pair in plot.pairs: profit_col = f'cum_profit_{pair}' df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) - pair_profit = go.Scattergl( - x=df_comb.index, - y=df_comb[profit_col], - name=f"Profit {pair}", - ) - fig.append_trace(pair_profit, 3, 1) + fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) From c7a4a16eec3778ccc6b8b2cdab66e0c3434ad242 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:31:36 +0200 Subject: [PATCH 708/928] Create generate_plot_graph --- freqtrade/plot/plotting.py | 35 +++++++++++++++++++++++++++++++++-- scripts/plot_profit.py | 33 ++------------------------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 4c45c0375..04e246371 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,9 +5,10 @@ from typing import Any, Dict, List, Optional import pandas as pd from freqtrade.arguments import Arguments -from freqtrade.exchange import Exchange from freqtrade.data import history -from freqtrade.data.btanalysis import load_trades +from freqtrade.data.btanalysis import (combine_tickers_with_mean, + create_cum_profit, load_trades) +from freqtrade.exchange import Exchange from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -28,6 +29,7 @@ class FTPlots(): self._config = config self.exchange: Optional[Exchange] = None + # Exchange is only needed when downloading data! if self._config.get("live", False) or self._config.get("refresh_pairs", False): self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), self._config).exchange @@ -258,6 +260,35 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra return fig +def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: pd.DataFrame = None, + ) -> go.Figure: + # Combine close-values for all pairs, rename columns to "pair" + df_comb = combine_tickers_with_mean(tickers, "close") + + # Add combined cumulative profit + df_comb = create_cum_profit(df_comb, trades, 'cum_profit') + + # Plot the pairs average close prices, and total profit growth + avgclose = go.Scattergl( + x=df_comb.index, + y=df_comb['mean'], + name='Avg close price', + ) + + fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + + fig.append_trace(avgclose, 1, 1) + fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') + + for pair in pairs: + profit_col = f'cum_profit_{pair}' + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + + fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") + + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + + def generate_plot_filename(pair, ticker_interval) -> str: """ Generate filenames per pair/ticker_interval to be used for storing plots diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index ad135b5e6..f1cf99828 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,13 +8,9 @@ import logging import sys from typing import Any, Dict, List -import plotly.graph_objs as go -from plotly import tools - from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments -from freqtrade.data.btanalysis import create_cum_profit, combine_tickers_with_mean from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, store_plot_file, add_profit +from freqtrade.plot.plotting import FTPlots, generate_profit_graph from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -33,32 +29,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - - # Combine close-values for all pairs, rename columns to "pair" - df_comb = combine_tickers_with_mean(plot.tickers, "close") - - # Add combined cumulative profit - df_comb = create_cum_profit(df_comb, trades, 'cum_profit') - - # Plot the pairs average close prices, and total profit growth - avgclose = go.Scattergl( - x=df_comb.index, - y=df_comb['mean'], - name='Avg close price', - ) - - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) - - fig.append_trace(avgclose, 1, 1) - fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') - - for pair in plot.pairs: - profit_col = f'cum_profit_{pair}' - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) - - fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") - - store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + generate_profit_graph(plot.pairs, plot.tickers, trades) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From 587d71efb586b336e5c7ed50feb83b5f1f70afbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 10:47:55 +0200 Subject: [PATCH 709/928] Test generate_profit_plot --- freqtrade/plot/plotting.py | 3 ++- freqtrade/tests/test_plotting.py | 34 +++++++++++++++++++++++++++++++- scripts/plot_profit.py | 5 +++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 04e246371..922f2847f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -276,6 +276,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: ) fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + fig['layout'].update(title="Profit plot") fig.append_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') @@ -286,7 +287,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") - store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) + return fig def generate_plot_filename(pair, ticker_interval) -> str: diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index fb2c52e1e..32e7dcd8a 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -10,7 +10,8 @@ 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, generate_candlestick_graph, - generate_plot_filename, plot_trades, + generate_plot_filename, + generate_profit_graph, plot_trades, store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -214,3 +215,34 @@ def test_add_profit(): profits = find_trace_in_fig_data(figure.data, "Profits") assert isinstance(profits, go.Scattergl) assert profits.yaxis == "y2" + + +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") + pairs = ["POWR/BTC", "XLM/BTC"] + + tickers = history.load_data(datadir=None, + pairs=pairs, + ticker_interval='5m', + timerange=timerange + ) + trades = trades[trades['pair'].isin(pairs)] + + fig = generate_profit_graph(pairs, tickers, trades) + assert isinstance(fig, go.Figure) + + assert fig.layout.title.text == "Profit plot" + figure = fig.layout.figure + assert len(figure.data) == 4 + + avgclose = find_trace_in_fig_data(figure.data, "Avg close price") + assert isinstance(avgclose, go.Scattergl) + + profit = find_trace_in_fig_data(figure.data, "Profit") + assert isinstance(profit, go.Scattergl) + + for pair in pairs: + profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") + assert isinstance(profit_pair, go.Scattergl) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index f1cf99828..c29b4d967 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, generate_profit_graph +from freqtrade.plot.plotting import FTPlots, generate_profit_graph, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -29,7 +29,8 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - generate_profit_graph(plot.pairs, plot.tickers, trades) + fig = generate_profit_graph(plot.pairs, plot.tickers, trades) + store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) def plot_parse_args(args: List[str]) -> Dict[str, Any]: From db59d39e2c40a96c54e2e1abe5179765a98d3883 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 11:06:51 +0200 Subject: [PATCH 710/928] Don't use class for plotting This will allow easy usage of the methods from jupter notebooks --- freqtrade/plot/plotting.py | 60 +++++++++++++++++++++----------------- scripts/plot_dataframe.py | 11 +++---- scripts/plot_profit.py | 11 +++---- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 922f2847f..079a098dc 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional import pandas as pd @@ -23,37 +23,43 @@ except ImportError: exit(1) -class FTPlots(): +def init_plotscript(config): + """ + Initialize objects needed for plotting + :return: Dict with tickers, trades, pairs and strategy + """ + exchange: Optional[Exchange] = None - def __init__(self, config: Dict[str, Any]): - self._config = config - self.exchange: Optional[Exchange] = None + # Exchange is only needed when downloading data! + if config.get("live", False) or config.get("refresh_pairs", False): + exchange = ExchangeResolver(config.get('exchange', {}).get('name'), + config).exchange - # Exchange is only needed when downloading data! - if self._config.get("live", False) or self._config.get("refresh_pairs", False): - self.exchange = ExchangeResolver(self._config.get('exchange', {}).get('name'), - self._config).exchange + strategy = StrategyResolver(config).strategy + if "pairs" in config: + pairs = config["pairs"].split(',') + else: + pairs = config["exchange"]["pair_whitelist"] - self.strategy = StrategyResolver(self._config).strategy - if "pairs" in self._config: - self.pairs = self._config["pairs"].split(',') - else: - self.pairs = self._config["exchange"]["pair_whitelist"] + # Set timerange to use + timerange = Arguments.parse_timerange(config["timerange"]) - # Set timerange to use - self.timerange = Arguments.parse_timerange(self._config["timerange"]) + tickers = history.load_data( + datadir=Path(str(config.get("datadir"))), + pairs=pairs, + ticker_interval=config['ticker_interval'], + refresh_pairs=config.get('refresh_pairs', False), + timerange=timerange, + exchange=exchange, + live=config.get("live", False), + ) - self.tickers = history.load_data( - datadir=Path(str(self._config.get("datadir"))), - pairs=self.pairs, - ticker_interval=self._config['ticker_interval'], - refresh_pairs=self._config.get('refresh_pairs', False), - timerange=self.timerange, - exchange=self.exchange, - live=self._config.get("live", False), - ) - - self.trades = load_trades(self._config) + trades = load_trades(config) + return {"tickers": tickers, + "trades": trades, + "pairs": pairs, + "strategy": strategy, + } def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 431c6239c..1e2d9f248 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,7 +21,7 @@ import pandas as pd from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import (FTPlots, generate_candlestick_graph, +from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, store_plot_file, generate_plot_filename) from freqtrade.state import RunMode @@ -54,17 +54,18 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): -Generate plot files :return: None """ - plot = FTPlots(config) + plot_elements = init_plotscript(config) + trades = plot_elements['trades'] pair_counter = 0 - for pair, data in plot.tickers.items(): + for pair, data in plot_elements["tickers"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(plot.strategy, tickers, pair) + dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair) - trades_pair = plot.trades.loc[plot.trades['pair'] == pair] + trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index c29b4d967..7442ef155 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments from freqtrade.optimize import setup_configuration -from freqtrade.plot.plotting import FTPlots, generate_profit_graph, store_plot_file +from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -23,13 +23,14 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ - plot = FTPlots(config) - - trades = plot.trades[plot.trades['pair'].isin(plot.pairs)] + plot_elements = init_plotscript(config) + trades = plot_elements['trades'] + # Filter trades to relevant pairs + trades = trades[trades['pair'].isin(plot_elements["pairs"])] # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - fig = generate_profit_graph(plot.pairs, plot.tickers, trades) + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades) store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True) From 44e050095859004c0c2a913027cc5eee01d71a08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 13:01:12 +0200 Subject: [PATCH 711/928] Test init_plotscript --- freqtrade/tests/test_plotting.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 32e7dcd8a..cef229d19 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -11,8 +11,8 @@ from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.plot.plotting import (add_indicators, add_profit, generate_candlestick_graph, generate_plot_filename, - generate_profit_graph, plot_trades, - store_plot_file) + generate_profit_graph, init_plotscript, + plot_trades, store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.tests.conftest import log_has, log_has_re @@ -37,6 +37,26 @@ def generage_empty_figure(): ) +def test_init_plotscript(default_conf, mocker): + default_conf['timerange'] = "20180110-20180112" + default_conf['trade_source'] = "file" + default_conf['ticker_interval'] = "5m" + default_conf["datadir"] = history.make_testdata_path(None) + default_conf['exportfilename'] = str( + history.make_testdata_path(None) / "backtest-result_test.json") + ret = init_plotscript(default_conf) + assert "tickers" in ret + assert "trades" in ret + assert "pairs" in ret + assert "strategy" in ret + + default_conf['pairs'] = "POWR/BTC,XLM/BTC" + ret = init_plotscript(default_conf) + assert "tickers" in ret + assert "POWR/BTC" in ret["tickers"] + assert "XLM/BTC" in ret["tickers"] + + def test_add_indicators(default_conf, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) From 59818af69ced404503ab543c4ce03752c6016082 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 13:15:41 +0200 Subject: [PATCH 712/928] Remove common_datearray function --- freqtrade/misc.py | 20 -------------------- freqtrade/plot/plotting.py | 4 ++-- freqtrade/tests/test_misc.py | 21 +++------------------ 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 460e20e91..05946e008 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -5,10 +5,8 @@ import gzip import logging import re from datetime import datetime -from typing import Dict import numpy as np -from pandas import DataFrame import rapidjson @@ -41,24 +39,6 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray: - """ - Return dates from Dataframe - :param dfs: Dict with format pair: pair_data - :return: List of dates - """ - alldates = {} - for pair, pair_data in dfs.items(): - dates = datesarray_to_datetimearray(pair_data['date']) - for date in dates: - alldates[date] = 1 - lst = [] - for date, _ in alldates.items(): - lst.append(date) - arr = np.array(lst) - return np.sort(arr, axis=0) - - def file_dump_json(filename, data, is_zip=False) -> None: """ Dump JSON data into a file diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 079a098dc..ccb932698 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -266,8 +266,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra return fig -def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: pd.DataFrame = None, - ) -> go.Figure: +def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], + trades: pd.DataFrame) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_tickers_with_mean(tickers, "close") diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 7a7b15cf2..1a6b2a92d 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -4,10 +4,9 @@ import datetime from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, - file_dump_json, file_load_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file, pair_data_filename -from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.data.history import pair_data_filename +from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, + file_load_json, format_ms_time, shorten_date) def test_shorten_date() -> None: @@ -32,20 +31,6 @@ def test_datesarray_to_datetimearray(ticker_history_list): assert date_len == 2 -def test_common_datearray(default_conf) -> None: - strategy = DefaultStrategy(default_conf) - tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') - tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", pair="UNITTEST/BTC", - fill_missing=True)} - dataframes = strategy.tickerdata_to_dataframe(tickerlist) - - dates = common_datearray(dataframes) - - assert dates.size == dataframes['UNITTEST/BTC']['date'].size - assert dates[0] == dataframes['UNITTEST/BTC']['date'][0] - assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1] - - def test_file_dump_json(mocker) -> None: file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) From 0c7d14fe50694edd916c550b1c1213990146f648 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 20:30:31 +0200 Subject: [PATCH 713/928] Check if timeframes is available and fail gracefully otherwise --- freqtrade/exchange/exchange.py | 7 +++++++ freqtrade/resolvers/exchange_resolver.py | 1 + 2 files changed, 8 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b649a7b65..cefee999c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,6 +269,13 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ + logger.warning("validate_timerames") + if not hasattr(self._api, "timeframes"): + # If timeframes is missing, the exchange probably has no fetchOHLCV method. + # Therefore we also show that. + raise OperationalException( + f"This exchange ({self.name}) does not have a `timeframes` attribute and " + f"is therefore not supported. fetchOHLCV: {self.exchange_has('fetchOHLCV')}") timeframes = self._api.timeframes if timeframe not in timeframes: raise OperationalException( diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 25a86dd0e..089f6306f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -28,6 +28,7 @@ class ExchangeResolver(IResolver): except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") + if not hasattr(self, "exchange"): self.exchange = Exchange(config) def _load_exchange( From 01904d3c1e47321140304faecb768a3709bc751c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jun 2019 20:30:47 +0200 Subject: [PATCH 714/928] Test not having timeframe available on exchange object --- freqtrade/tests/exchange/test_exchange.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 48a8538a9..b74882ad4 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -396,6 +396,23 @@ def test_validate_timeframes_failed(default_conf, mocker): Exchange(default_conf) +def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'This exchange (.*) does not have a `timeframes` attribute and*'): + Exchange(default_conf) + + def test_validate_timeframes_not_in_config(default_conf, mocker): del default_conf["ticker_interval"] api_mock = MagicMock() From 0d601fd1113615d1155986ffe65ba9b49067b68c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jul 2019 06:18:28 +0200 Subject: [PATCH 715/928] Remove logger message --- freqtrade/exchange/exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cefee999c..2350c752f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,7 +269,6 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ - logger.warning("validate_timerames") if not hasattr(self._api, "timeframes"): # If timeframes is missing, the exchange probably has no fetchOHLCV method. # Therefore we also show that. From 06ad04e5fa8f48adac1d1ae12caf5689464569f3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:30 +0000 Subject: [PATCH 716/928] Update ccxt from 1.18.805 to 1.18.860 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 6913217ff..409c979b5 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.805 +ccxt==1.18.860 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 1e4f459a2678e377e418480b5415d6c59c87b13b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:31 +0000 Subject: [PATCH 717/928] Update pytest from 4.6.3 to 5.0.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1232d4dd4..99697340d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.6.3 +pytest==5.0.0 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From c91add203d451b93d384af919f99d5d4edc408e8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jul 2019 18:28:32 +0000 Subject: [PATCH 718/928] Update mypy from 0.710 to 0.711 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 99697340d..c360bc85e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 coveralls==1.8.1 -mypy==0.710 +mypy==0.711 From 85ac217abc5303fb2be26a1dfd1ec2bc0e14d433 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jul 2019 20:33:27 +0200 Subject: [PATCH 719/928] Remove duplicate keyword from arguments --- freqtrade/arguments.py | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f020252f8..f699cb549 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -36,13 +36,11 @@ AVAILABLE_CLI_OPTIONS = { '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', - dest='loglevel', default=0, ), "logfile": Arg( '--logfile', help='Log to the file specified.', - dest='logfile', metavar='FILE', ), @@ -56,26 +54,22 @@ AVAILABLE_CLI_OPTIONS = { help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' f'Multiple --config options may be used. ' f'Can be set to `-` to read config from stdin.', - dest='config', action='append', metavar='PATH',), "datadir": Arg( '-d', '--datadir', help='Path to backtest data.', - dest='datadir', metavar='PATH',), # Main options "strategy": Arg( '-s', '--strategy', help='Specify strategy class name (default: `%(default)s`).', - dest='strategy', default='DefaultStrategy', metavar='NAME', ), "strategy_path": Arg( '--strategy-path', help='Specify additional strategy lookup path.', - dest='strategy_path', metavar='PATH', ), "dynamic_whitelist": Arg( @@ -83,7 +77,6 @@ AVAILABLE_CLI_OPTIONS = { help='Dynamically generate and update whitelist ' 'based on 24h BaseVolume (default: %(const)s). ' 'DEPRECATED.', - dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, type=int, metavar='INT', @@ -94,37 +87,31 @@ AVAILABLE_CLI_OPTIONS = { help=f'Override trades database URL, this is useful in custom deployments ' f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - dest='db_url', metavar='PATH', ), "sd_notify": Arg( '--sd-notify', help='Notify systemd service manager.', action='store_true', - dest='sd_notify', ), # Optimize common "ticker_interval": Arg( '-i', '--ticker-interval', help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - dest='ticker_interval', ), "timerange": Arg( '--timerange', help='Specify what timerange of data to use.', - dest='timerange', ), "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', type=int, - dest='max_open_trades', ), "stake_amount": Arg( '--stake_amount', help='Specify stake_amount.', type=float, - dest='stake_amount', ), "refresh_pairs": Arg( '-r', '--refresh-pairs-cached', @@ -132,14 +119,12 @@ AVAILABLE_CLI_OPTIONS = { 'exchange. Use it if you want to run your optimization commands with ' 'up-to-date data.', action='store_true', - dest='refresh_pairs', ), # backtesting "position_stacking": Arg( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', action='store_true', - dest='position_stacking', default=False ), "use_max_market_positions": Arg( @@ -147,14 +132,12 @@ AVAILABLE_CLI_OPTIONS = { help='Disable applying `max_open_trades` during backtest ' '(same as setting `max_open_trades` to a very high number).', action='store_false', - dest='use_max_market_positions', default=True ), "live": Arg( '-l', '--live', help='Use live data.', action='store_true', - dest='live', ), "strategy_list": Arg( '--strategy-list', @@ -164,13 +147,11 @@ AVAILABLE_CLI_OPTIONS = { 'the strategy-name is injected into the filename ' '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', nargs='+', - dest='strategy_list', ), "export": Arg( '--export', help='Export backtest results, argument are: trades. ' 'Example: `--export=trades`', - dest='export', ), "exportfilename": Arg( '--export-filename', @@ -179,7 +160,6 @@ AVAILABLE_CLI_OPTIONS = { 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - dest='exportfilename', metavar='PATH', ), # Edge @@ -188,20 +168,17 @@ AVAILABLE_CLI_OPTIONS = { help='Defines a range of stoploss values against which edge will assess the strategy. ' 'The format is "min,max,step" (without any space). ' 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - dest='stoploss_range', ), # hyperopt "hyperopt": Arg( '--customhyperopt', help='Specify hyperopt class name (default: `%(default)s`).', - dest='hyperopt', default=constants.DEFAULT_HYPEROPT, metavar='NAME', ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - dest='epochs', default=constants.HYPEROPT_EPOCH, type=int, metavar='INT', @@ -213,13 +190,11 @@ AVAILABLE_CLI_OPTIONS = { choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', - dest='spaces', ), "print_all": Arg( '--print-all', help='Print all results, not only the best ones.', action='store_true', - dest='print_all', default=False ), "hyperopt_jobs": Arg( @@ -228,7 +203,6 @@ AVAILABLE_CLI_OPTIONS = { '(hyperopt worker processes). ' 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' 'If 1 is given, no parallel computing code is used at all.', - dest='hyperopt_jobs', default=-1, type=int, metavar='JOBS', @@ -236,7 +210,6 @@ AVAILABLE_CLI_OPTIONS = { "hyperopt_random_state": Arg( '--random-state', help='Set random state to some positive integer for reproducible hyperopt results.', - dest='hyperopt_random_state', type=check_int_positive, metavar='INT', ), @@ -244,7 +217,6 @@ AVAILABLE_CLI_OPTIONS = { '--min-trades', help="Set minimal desired number of trades for evaluations in the hyperopt " "optimization path (default: 1).", - dest='hyperopt_min_trades', default=1, type=check_int_positive, metavar='INT', @@ -254,26 +226,22 @@ AVAILABLE_CLI_OPTIONS = { '-1', '--one-column', help='Print exchanges in one column.', action='store_true', - dest='print_one_column', ), # script_options "pairs": Arg( '-p', '--pairs', help='Show profits for only these pairs. Pairs are comma-separated.', - dest='pairs', ), # Download data "pairs_file": Arg( '--pairs-file', help='File containing a list of pairs to download.', - dest='pairs_file', metavar='FILE', ), "days": Arg( '--days', help='Download data for given number of days.', - dest='days', type=check_int_positive, metavar='INT', ), @@ -281,7 +249,6 @@ AVAILABLE_CLI_OPTIONS = { '--exchange', help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' f'Only valid if no config is provided.', - dest='exchange', ), "timeframes": Arg( '-t', '--timeframes', @@ -290,12 +257,10 @@ AVAILABLE_CLI_OPTIONS = { choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], nargs='+', - dest='timeframes', ), "erase": Arg( '--erase', help='Clean all existing data for the selected exchange/pairs/timeframes.', - dest='erase', action='store_true', ), # Plot_df_options @@ -304,20 +269,17 @@ AVAILABLE_CLI_OPTIONS = { help='Set indicators from your strategy you want in the first row of the graph. ' 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', default='sma,ema3,ema5', - dest='indicators1', ), "indicators2": Arg( '--indicators2', help='Set indicators from your strategy you want in the third row of the graph. ' 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', default='macd,macdsignal', - dest='indicators2', ), "plot_limit": Arg( '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - dest='plot_limit', default=750, type=int, ), @@ -325,7 +287,6 @@ AVAILABLE_CLI_OPTIONS = { '--trade-source', help='Specify the source for trades (Can be DB or file (backtest file)) ' 'Default: %(default)s', - dest='trade_source', default="file", choices=["DB", "file"] ) @@ -418,7 +379,7 @@ class Arguments(object): for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] - parser.add_argument(*opt.cli, **opt.kwargs) + parser.add_argument(*opt.cli, dest=val, **opt.kwargs) def _build_subcommands(self) -> None: """ From 91fb9d0113d719d4e6d884e93ec7bc15017b4ccc Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 05:02:44 +0300 Subject: [PATCH 720/928] fix #1995 --- freqtrade/exchange/exchange.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2350c752f..556ede13a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -85,6 +85,9 @@ class Exchange(object): it does basic validation whether the specified exchange and pairs are valid. :return: None """ + self._api: ccxt.Exchange = None + self._api_async: ccxt_async.Exchange = None + self._config.update(config) self._cached_ticker: Dict[str, Any] = {} @@ -117,9 +120,9 @@ class Exchange(object): self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] # Initialize ccxt objects - self._api: ccxt.Exchange = self._init_ccxt( + self._api = self._init_ccxt( exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) - self._api_async: ccxt_async.Exchange = self._init_ccxt( + self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) logger.info('Using Exchange "%s"', self.name) @@ -173,6 +176,8 @@ class Exchange(object): api = getattr(ccxt_module, name.lower())(ex_config) except (KeyError, AttributeError): raise OperationalException(f'Exchange {name} is not supported') + except ccxt.NotSupported as e: + raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") self.set_sandbox(api, exchange_config, name) From d41b8cc96ec56b372889ab5ee213311586cb8c47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 05:13:41 +0300 Subject: [PATCH 721/928] catch ccxt.BaseError --- 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 556ede13a..c3171b961 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -176,7 +176,7 @@ class Exchange(object): api = getattr(ccxt_module, name.lower())(ex_config) except (KeyError, AttributeError): raise OperationalException(f'Exchange {name} is not supported') - except ccxt.NotSupported as e: + except ccxt.BaseError as e: raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") self.set_sandbox(api, exchange_config, name) From b3644f7fa000e2be4daabceab47216175552a1ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 06:26:39 +0200 Subject: [PATCH 722/928] Fix typo in docstring --- freqtrade/data/btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 556666f4e..dcd544d00 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -106,7 +106,7 @@ def load_trades(config) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) - * loads data from backtestfile (`using exportfilename`) + * loads data from backtestfile (using `exportfilename`) """ if config["trade_source"] == "DB": return load_trades_from_db(config["db_url"]) From b80cef964e5461fd19a7329982d97402e2ca3333 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 3 Jul 2019 01:03:38 +0300 Subject: [PATCH 723/928] fix validate_timeframes(); test added --- freqtrade/exchange/exchange.py | 11 ++++++---- freqtrade/tests/exchange/test_exchange.py | 26 +++++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2350c752f..71055460b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -269,12 +269,15 @@ class Exchange(object): """ Checks if ticker interval from config is a supported timeframe on the exchange """ - if not hasattr(self._api, "timeframes"): - # If timeframes is missing, the exchange probably has no fetchOHLCV method. + if not hasattr(self._api, "timeframes") or self._api.timeframes is None: + # If timeframes attribute is missing (or is None), the exchange probably + # has no fetchOHLCV method. # Therefore we also show that. raise OperationalException( - f"This exchange ({self.name}) does not have a `timeframes` attribute and " - f"is therefore not supported. fetchOHLCV: {self.exchange_has('fetchOHLCV')}") + f"The ccxt library does not provide the list of timeframes " + f"for the exchange \"{self.name}\" and this exchange " + f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") + timeframes = self._api.timeframes if timeframe not in timeframes: raise OperationalException( diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index b74882ad4..1d4b0bb11 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -396,7 +396,7 @@ def test_validate_timeframes_failed(default_conf, mocker): Exchange(default_conf) -def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): +def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): default_conf["ticker_interval"] = "3m" api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') @@ -409,7 +409,29 @@ def test_validate_timeframes_emulated_ohlcv(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) with pytest.raises(OperationalException, - match=r'This exchange (.*) does not have a `timeframes` attribute and*'): + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + +def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', + MagicMock(return_value={'timeframes': None})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): Exchange(default_conf) From fcdbe846e51a9c76f86c97b84dec28680d857a71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:06:50 +0200 Subject: [PATCH 724/928] Fix #1981 - Detect reverted currency pairs --- freqtrade/exchange/exchange.py | 11 ++++++++++- freqtrade/rpc/rpc.py | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3171b961..536a707cd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -270,6 +270,15 @@ class Exchange(object): f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + def get_valid_pair_combination(self, paira, pairb) -> str: + """ + Get valid combination of paira and pairb by trying both combinations. + """ + for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: + if pair in self._api.markets and self._api.markets[pair].get('active'): + return pair + raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") + def validate_timeframes(self, timeframe: List[str]) -> None: """ Checks if ticker interval from config is a supported timeframe on the exchange @@ -501,7 +510,7 @@ class Exchange(object): def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: - if pair not in self._api.markets: + if pair not in self._api.markets or not self._api.markets[pair].get('active'): raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) try: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f5adffc65..f77e0eddb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -281,10 +281,11 @@ class RPC(object): rate = 1.0 else: try: - if coin in('USDT', 'USD', 'EUR'): - rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") + if pair.startswith("BTC"): + rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) else: - rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) + rate = self._freqtrade.get_sell_rate(pair, False) except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue From 1bcf2737fe871701516d15ba120037ba51f93b1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:07:26 +0200 Subject: [PATCH 725/928] Add tests for new behaviour --- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/rpc/test_rpc.py | 68 ++++++++++++++++++++++- freqtrade/tests/rpc/test_rpc_apiserver.py | 2 + freqtrade/tests/rpc/test_rpc_telegram.py | 2 + 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index b74882ad4..804cf1af5 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -910,7 +910,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): 'last': 0.0001, } api_mock.fetch_ticker = MagicMock(return_value=tick) - api_mock.markets = {'ETH/BTC': {}} + api_mock.markets = {'ETH/BTC': {'active': True}} exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker ticker = exchange.get_ticker(pair='ETH/BTC') diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 5a4b5d1b2..d273244b0 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -324,7 +324,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, assert prec_satoshi(stats['best_rate'], 6.2) -def test_rpc_balance_handle(default_conf, mocker): +def test_rpc_balance_handle_error(default_conf, mocker): mock_balance = { 'BTC': { 'free': 10.0, @@ -371,6 +371,72 @@ def test_rpc_balance_handle(default_conf, mocker): assert result['total'] == 12.0 +def test_rpc_balance_handle(default_conf, mocker): + mock_balance = { + 'BTC': { + 'free': 10.0, + 'total': 12.0, + 'used': 2.0, + }, + 'ETH': { + 'free': 1.0, + 'total': 5.0, + 'used': 4.0, + }, + 'PAX': { + 'free': 5.0, + 'total': 10.0, + 'used': 5.0, + } + } + + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.Market', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + ) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=mock_balance), + get_ticker=MagicMock( + side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}), + get_valid_pair_combination=MagicMock( + side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") + ) + + freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) + rpc = RPC(freqtradebot) + rpc._fiat_converter = CryptoToFiatConverter() + + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12.15) + assert prec_satoshi(result['value'], 182250) + assert 'USD' == result['symbol'] + assert result['currencies'] == [ + {'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }, + {'available': 1.0, + 'balance': 5.0, + 'currency': 'ETH', + 'est_btc': 0.05, + 'pending': 4.0 + }, + {'available': 5.0, + 'balance': 10.0, + 'currency': 'PAX', + 'est_btc': 0.1, + 'pending': 5.0} + ] + assert result['total'] == 12.15 + + def test_rpc_start(mocker, default_conf) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index b7721fd8e..bd420ada6 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -244,6 +244,8 @@ def test_api_balance(botclient, mocker, rpc_balance): } mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 673536993..1bee5bff3 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -518,6 +518,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") msg_mock = MagicMock() mocker.patch.multiple( From 40fe2d2c164bddd3ce6f17f84a2d636498636ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2019 20:20:12 +0200 Subject: [PATCH 726/928] Test get_valid_pair_combination --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 24 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 536a707cd..a41b51e90 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -275,7 +275,7 @@ class Exchange(object): Get valid combination of paira and pairb by trying both combinations. """ for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: - if pair in self._api.markets and self._api.markets[pair].get('active'): + if pair in self.markets and self.markets[pair].get('active'): return pair raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 804cf1af5..4b264db08 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1455,10 +1455,11 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock()) ex = Exchange(default_conf) assert ex._ft_has == Exchange._ft_has_default @@ -1479,3 +1480,18 @@ def test_merge_ft_has_dict(default_conf, mocker): assert ex._ft_has != Exchange._ft_has_default assert not ex._ft_has['stoploss_on_exchange'] assert ex._ft_has['DeadBeef'] == 20 + + +def test_get_valid_pair_combination(default_conf, mocker, markets): + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock(), + markets=PropertyMock(return_value=markets)) + ex = Exchange(default_conf) + + assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" + 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") From 5c6039fd8b6c70c32f712455aab0470ccfa46711 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 19:53:50 +0200 Subject: [PATCH 727/928] Fix #1997 - rename folder to dir --- freqtrade/configuration.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 8 ++++---- freqtrade/tests/optimize/test_edge_cli.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- freqtrade/tests/test_configuration.py | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 82349700e..2bbec4654 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -254,7 +254,7 @@ class Configuration(object): config.update({'datadir': self._create_datadir(config, self.args.datadir)}) else: config.update({'datadir': self._create_datadir(config, None)}) - logger.info('Using data folder: %s ...', config.get('datadir')) + logger.info('Using data directory: %s ...', config.get('datadir')) def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 28568f20c..498a04db5 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -183,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -235,7 +235,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -851,7 +851,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Live: Downloading data for all defined pairs ...', @@ -910,7 +910,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Live: Downloading data for all defined pairs ...', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 6b527543f..7cc41b095 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -32,7 +32,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -71,7 +71,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N assert 'datadir' in config assert config['runmode'] == RunMode.EDGE assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index c3d6d0076..e5f87b022 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -61,7 +61,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -111,7 +111,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert config['runmode'] == RunMode.HYPEROPT assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 38f17fbea..b34e75a28 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -306,7 +306,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -356,7 +356,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config @@ -418,7 +418,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config From 17800c8ca561d48e5ad0da6484d148d63c76e54f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 19:56:48 +0200 Subject: [PATCH 728/928] Remove folder references (it's directory!) --- config_full.json.example | 2 +- docs/backtesting.md | 12 ++++++------ docs/bot-usage.md | 6 +++--- docs/configuration.md | 2 +- docs/docker.md | 2 +- docs/hyperopt.md | 2 +- docs/strategy-customization.md | 9 ++++----- freqtrade/tests/test_arguments.py | 4 ++-- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index acecfb649..b6451859c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -123,5 +123,5 @@ "process_throttle_secs": 5 }, "strategy": "DefaultStrategy", - "strategy_path": "/some/folder/" + "strategy_path": "user_data/strategies/" } diff --git a/docs/backtesting.md b/docs/backtesting.md index 8d8ea8030..2a5163e73 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -13,7 +13,7 @@ Backtesting will use the crypto-currencies (pair) from your config file and load static tickers located in [/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). If the 5 min and 1 min ticker for the crypto-currencies to test is not -already in the `testdata` folder, backtesting will download them +already in the `testdata` directory, backtesting will download them automatically. Testdata files will not be updated until you specify it. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. @@ -65,7 +65,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ python3 freqtrade backtesting --export trades ``` -The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder. +The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. #### Exporting trades to file specifying a custom filename @@ -107,7 +107,7 @@ To download new set of backtesting ticker data, you can use a download script. If you are using Binance for example: -- create a folder `user_data/data/binance` and copy `pairs.json` in that folder. +- 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 @@ -123,9 +123,9 @@ 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 folder than the exchange specific default, use `--datadir user_data/data/some_directory`. +- 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 folder, use `--pairs-file some_other_dir/pairs.json`. +- 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. @@ -231,7 +231,7 @@ To backtest multiple strategies, a list of Strategies can be provided. This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this should give a nice runtime boost. -All listed Strategies need to be in the same folder. +All listed Strategies need to be in the same directory. ``` bash freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b215d7b7c..0e01ac0e5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -109,14 +109,14 @@ Learn more about strategy file in ### How to use **--strategy-path**? This parameter allows you to add an additional strategy lookup path, which gets -checked before the default locations (The passed path must be a folder!): +checked before the default locations (The passed path must be a directory!): ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? -This is very simple. Copy paste your strategy file into the folder +This is very simple. Copy paste your strategy file into the directory `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. ### How to use **--dynamic-whitelist**? diff --git a/docs/configuration.md b/docs/configuration.md index 9c3b20338..f46ff14a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,7 +67,7 @@ Mandatory Parameters are marked as **Required**. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `strategy` | DefaultStrategy | Defines Strategy class to use. -| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). +| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. diff --git a/docs/docker.md b/docs/docker.md index 939ab3f7d..615d31796 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -140,7 +140,7 @@ To run a restartable instance in the background (feel free to place your configu #### Move your config file and database -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. ```bash mkdir ~/.freqtrade diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 15b02b56f..a15fd575a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -34,7 +34,7 @@ Depending on the space you want to optimize, only some of the below are required ### 1. Install a Custom Hyperopt File -Put your hyperopt file into the folder`user_data/hyperopts`. +Put your hyperopt file into the directory `user_data/hyperopts`. Let assume you want a hyperopt file `awesome_hyperopt.py`: Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 57c646aed..800012a0f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -5,8 +5,7 @@ indicators. ## Install a custom strategy file -This is very simple. Copy paste your strategy file into the folder -`user_data/strategies`. +This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: @@ -22,7 +21,7 @@ python3 freqtrade --strategy AwesomeStrategy The bot includes a default strategy file. However, we recommend you to use your own file to not have to lose your parameters every time the default strategy file will be updated on Github. Put your custom strategy file -into the folder `user_data/strategies`. +into the directory `user_data/strategies`. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. `cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` @@ -398,10 +397,10 @@ The default buy strategy is located in the file ### Specify custom strategy location -If you want to use a strategy from a different folder you can pass `--strategy-path` +If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder +python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index c9d2a2261..8f0dec226 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -174,7 +174,7 @@ def test_parse_args_hyperopt_custom() -> None: def test_download_data_options() -> None: args = [ '--pairs-file', 'file_with_pairs', - '--datadir', 'datadir/folder', + '--datadir', 'datadir/directory', '--days', '30', '--exchange', 'binance' ] @@ -183,7 +183,7 @@ def test_download_data_options() -> None: args = arguments.parse_args() assert args.pairs_file == 'file_with_pairs' - assert args.datadir == 'datadir/folder' + assert args.datadir == 'datadir/directory' assert args.days == 30 assert args.exchange == 'binance' From ce2a5b28380a9c9757f1674ec2f71b1195406d29 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:31:48 +0300 Subject: [PATCH 729/928] move loggers setup out of configuration --- freqtrade/configuration.py | 34 ++++----------------------- freqtrade/main.py | 3 --- freqtrade/tests/test_configuration.py | 11 +++++---- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 2bbec4654..f8488c438 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -6,8 +6,7 @@ import logging import os import sys from argparse import Namespace -from logging.handlers import RotatingFileHandler -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, Optional from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match @@ -15,25 +14,14 @@ from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants from freqtrade.exchange import (is_exchange_bad, is_exchange_available, is_exchange_officially_supported, available_exchanges) +from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode + logger = logging.getLogger(__name__) -def set_loggers(log_level: int = 0) -> None: - """ - Set the logger level for Third party libs - :return: None - """ - - logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger('ccxt.base.exchange').setLevel( - logging.INFO if log_level <= 2 else logging.DEBUG) - logging.getLogger('telegram').setLevel(logging.INFO) - - def _extend_validator(validator_class): """ Extended validator for the Freqtrade configuration JSON Schema. @@ -143,24 +131,10 @@ class Configuration(object): else: config.update({'verbosity': 0}) - # Log to stdout, not stderr - log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] if 'logfile' in self.args and self.args.logfile: config.update({'logfile': self.args.logfile}) - # Allow setting this as either configuration or argument - if 'logfile' in config: - log_handlers.append(RotatingFileHandler(config['logfile'], - maxBytes=1024 * 1024, # 1Mb - backupCount=10)) - - logging.basicConfig( - level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=log_handlers - ) - set_loggers(config['verbosity']) - logger.info('Verbosity set to %s', config['verbosity']) + setup_logging(config) def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/freqtrade/main.py b/freqtrade/main.py index 6f073f5d4..f02159a0e 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -16,7 +16,6 @@ from typing import Any, List from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.configuration import set_loggers from freqtrade.worker import Worker @@ -32,8 +31,6 @@ def main(sysargv: List[str] = None) -> None: return_code: Any = 1 worker = None try: - set_loggers() - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index b34e75a28..c5e60be7f 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -12,8 +12,9 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration, set_loggers +from freqtrade.configuration import Configuration from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL +from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re @@ -524,7 +525,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf))) # Prevent setting loggers - mocker.patch('freqtrade.configuration.set_loggers', MagicMock) + mocker.patch('freqtrade.loggers._set_loggers', MagicMock) arglist = ['-vvv'] args = Arguments(arglist, '').get_parsed_arg() @@ -546,7 +547,7 @@ def test_set_loggers() -> None: previous_value2 = logging.getLogger('ccxt.base.exchange').level previous_value3 = logging.getLogger('telegram').level - set_loggers() + _set_loggers() value1 = logging.getLogger('requests').level assert previous_value1 is not value1 @@ -560,13 +561,13 @@ def test_set_loggers() -> None: assert previous_value3 is not value3 assert value3 is logging.INFO - set_loggers(log_level=2) + _set_loggers(verbosity=2) assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.INFO assert logging.getLogger('telegram').level is logging.INFO - set_loggers(log_level=3) + _set_loggers(verbosity=3) assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG From 8e272e5774f74c5125b29a9d2bb50579a8480cc8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:48:39 +0300 Subject: [PATCH 730/928] minor: cosmetics in arguments.py --- freqtrade/arguments.py | 47 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ebdaf7c6e..b2d905a5a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -43,11 +43,10 @@ AVAILABLE_CLI_OPTIONS = { help='Log to the file specified.', metavar='FILE', ), - "version": Arg( '--version', action='version', - version=f'%(prog)s {__version__}' + version=f'%(prog)s {__version__}', ), "config": Arg( '-c', '--config', @@ -55,17 +54,19 @@ AVAILABLE_CLI_OPTIONS = { f'Multiple --config options may be used. ' f'Can be set to `-` to read config from stdin.', action='append', - metavar='PATH',), + metavar='PATH', + ), "datadir": Arg( '-d', '--datadir', help='Path to backtest data.', - metavar='PATH',), + metavar='PATH', + ), # Main options "strategy": Arg( '-s', '--strategy', help='Specify strategy class name (default: `%(default)s`).', - default='DefaultStrategy', metavar='NAME', + default='DefaultStrategy', ), "strategy_path": Arg( '--strategy-path', @@ -125,14 +126,14 @@ AVAILABLE_CLI_OPTIONS = { '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', action='store_true', - default=False + default=False, ), "use_max_market_positions": Arg( '--dmmp', '--disable-max-market-positions', help='Disable applying `max_open_trades` during backtest ' '(same as setting `max_open_trades` to a very high number).', action='store_false', - default=True + default=True, ), "live": Arg( '-l', '--live', @@ -158,9 +159,9 @@ AVAILABLE_CLI_OPTIONS = { help='Save backtest results to the file with this filename (default: `%(default)s`). ' 'Requires `--export` to be set as well. ' 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + metavar='PATH', default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'), - metavar='PATH', ), # Edge "stoploss_range": Arg( @@ -169,33 +170,33 @@ AVAILABLE_CLI_OPTIONS = { 'The format is "min,max,step" (without any space). ' 'Example: `--stoplosses=-0.01,-0.1,-0.001`', ), - # hyperopt + # Hyperopt "hyperopt": Arg( '--customhyperopt', help='Specify hyperopt class name (default: `%(default)s`).', - default=constants.DEFAULT_HYPEROPT, metavar='NAME', + default=constants.DEFAULT_HYPEROPT, ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - default=constants.HYPEROPT_EPOCH, type=int, metavar='INT', + default=constants.HYPEROPT_EPOCH, ), "spaces": Arg( '-s', '--spaces', help='Specify which parameters to hyperopt. Space-separated list. ' 'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - default='all', nargs='+', + default='all', ), "print_all": Arg( '--print-all', help='Print all results, not only the best ones.', action='store_true', - default=False + default=False, ), "hyperopt_jobs": Arg( '-j', '--job-workers', @@ -203,9 +204,9 @@ AVAILABLE_CLI_OPTIONS = { '(hyperopt worker processes). ' 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' 'If 1 is given, no parallel computing code is used at all.', - default=-1, type=int, metavar='JOBS', + default=-1, ), "hyperopt_random_state": Arg( '--random-state', @@ -217,9 +218,9 @@ AVAILABLE_CLI_OPTIONS = { '--min-trades', help="Set minimal desired number of trades for evaluations in the hyperopt " "optimization path (default: 1).", - default=1, type=check_int_positive, metavar='INT', + default=1, ), # List_exchange "print_one_column": Arg( @@ -233,7 +234,6 @@ AVAILABLE_CLI_OPTIONS = { help='Show profits for only these pairs. Pairs are comma-separated.', ), # Download data - "pairs_file": Arg( '--pairs-file', help='File containing a list of pairs to download.', @@ -263,7 +263,7 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), - # Plot_df_options + # Plot dataframe options "indicators1": Arg( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' @@ -280,16 +280,17 @@ AVAILABLE_CLI_OPTIONS = { '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - default=750, type=int, + metavar='INT', + default=750, ), "trade_source": Arg( '--trade-source', help='Specify the source for trades (Can be DB or file (backtest file)) ' 'Default: %(default)s', + choices=["DB", "file"], default="file", - choices=["DB", "file"] - ) + ), } ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] @@ -309,8 +310,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] - -ARGS_LIST_EXCHANGE = ["print_one_column"] +ARGS_LIST_EXCHANGES = ["print_one_column"] ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] @@ -339,7 +339,6 @@ class Arguments(object): """ Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: Optional[List[str]], description: str) -> None: self.args = args self.parsed_arg: Optional[argparse.Namespace] = None @@ -412,7 +411,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.build_args(optionlist=ARGS_LIST_EXCHANGE, parser=list_exchanges_cmd) + self.build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: From d8f133aaf3bcdd902298db8d623cecbcdea3445e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 00:51:01 +0300 Subject: [PATCH 731/928] remove duplicated loglevel option --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index b2d905a5a..f64398443 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -298,7 +298,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] -ARGS_COMMON_OPTIMIZE = ["loglevel", "ticker_interval", "timerange", +ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "refresh_pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", From a65b5f8e0209cf38b70d558026db443652842cbb Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:10:41 +0300 Subject: [PATCH 732/928] make some more arguments positive integers --- freqtrade/arguments.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f64398443..2d3dc0793 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -107,7 +107,8 @@ AVAILABLE_CLI_OPTIONS = { "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', - type=int, + type=check_int_positive, + metavar='INT', ), "stake_amount": Arg( '--stake_amount', @@ -180,7 +181,7 @@ AVAILABLE_CLI_OPTIONS = { "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', - type=int, + type=check_int_positive, metavar='INT', default=constants.HYPEROPT_EPOCH, ), @@ -280,7 +281,7 @@ AVAILABLE_CLI_OPTIONS = { '--plot-limit', help='Specify tick limit for plotting. Notice: too high values cause huge files. ' 'Default: %(default)s.', - type=int, + type=check_int_positive, metavar='INT', default=750, ), From 082065cd5057c233619c7c13bf3a7eeaea4c96f8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:20:26 +0300 Subject: [PATCH 733/928] minor cosmetics in arguments.py --- freqtrade/arguments.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 2d3dc0793..8e77cbc0a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -1,7 +1,6 @@ """ This module contains the argument manager class """ - import argparse import os import re @@ -29,9 +28,9 @@ class Arg: self.kwargs = kwargs -# List of available command line arguments +# List of available command line options AVAILABLE_CLI_OPTIONS = { - # Common arguments + # Common options "loglevel": Arg( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', @@ -122,7 +121,7 @@ AVAILABLE_CLI_OPTIONS = { 'up-to-date data.', action='store_true', ), - # backtesting + # Backtesting "position_stacking": Arg( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', @@ -223,13 +222,13 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), - # List_exchange + # List exchanges "print_one_column": Arg( '-1', '--one-column', help='Print exchanges in one column.', action='store_true', ), - # script_options + # Script options "pairs": Arg( '-p', '--pairs', help='Show profits for only these pairs. Pairs are comma-separated.', @@ -264,7 +263,7 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), - # Plot dataframe options + # Plot dataframe "indicators1": Arg( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' @@ -294,7 +293,9 @@ AVAILABLE_CLI_OPTIONS = { ), } + ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] + ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] @@ -326,9 +327,9 @@ ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + class TimeRange(NamedTuple): """ - NamedTuple Defining timerange inputs. + NamedTuple defining timerange inputs. [start/stop]type defines if [start/stop]ts shall be used. - if *type is none, don't use corresponding startvalue. + if *type is None, don't use corresponding startvalue. """ starttype: Optional[str] = None stoptype: Optional[str] = None From 8114d790a5f3b887f85713e9b4201512aed43713 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 01:40:52 +0300 Subject: [PATCH 734/928] commit forgotten loggers.py --- freqtrade/loggers.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 freqtrade/loggers.py diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py new file mode 100644 index 000000000..56dcf2486 --- /dev/null +++ b/freqtrade/loggers.py @@ -0,0 +1,50 @@ +import logging +import sys + +from logging.handlers import RotatingFileHandler +from typing import Any, Dict, List + + +logger = logging.getLogger(__name__) + + +def _set_loggers(verbosity: int = 0) -> None: + """ + Set the logging level for Third party libs + :return: None + """ + + logging.getLogger('requests').setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger("urllib3").setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger('ccxt.base.exchange').setLevel( + logging.INFO if verbosity <= 2 else logging.DEBUG + ) + logging.getLogger('telegram').setLevel(logging.INFO) + + +def setup_logging(config: Dict[str, Any]) -> None: + """ + Process --loglevel, --logfile options + """ + # Log level + verbosity = config['verbosity'] + + # Log to stdout, not stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + + if config.get('logfile'): + log_handlers.append(RotatingFileHandler(config['logfile'], + maxBytes=1024 * 1024, # 1Mb + backupCount=10)) + + logging.basicConfig( + level=logging.INFO if verbosity < 1 else logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=log_handlers + ) + _set_loggers(verbosity) + logger.info('Verbosity set to %s', verbosity) From f89b2a18e072859702cb3fdd4cbd1b248189bae4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 02:42:03 +0300 Subject: [PATCH 735/928] fix loglevel in conftest -- it's actually the verbosity level --- freqtrade/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index eb2a8600f..19a7dc423 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -227,7 +227,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "loglevel": logging.DEBUG, + "loglevel": 3, } return configuration From 84d3868994d0dd5c7c5144bdd8ffcdd2ca947a34 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 02:53:13 +0300 Subject: [PATCH 736/928] rename loglevel --> verbosity, because it's not logging level --- freqtrade/arguments.py | 4 ++-- freqtrade/configuration.py | 6 +++--- freqtrade/loggers.py | 2 +- freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_arguments.py | 10 +++++----- freqtrade/tests/test_main.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 8e77cbc0a..cb6b30b05 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -31,7 +31,7 @@ class Arg: # List of available command line options AVAILABLE_CLI_OPTIONS = { # Common options - "loglevel": Arg( + "verbosity": Arg( '-v', '--verbose', help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', @@ -294,7 +294,7 @@ AVAILABLE_CLI_OPTIONS = { } -ARGS_COMMON = ["loglevel", "logfile", "version", "config", "datadir"] +ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] ARGS_STRATEGY = ["strategy", "strategy_path"] diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index f8488c438..8ad0fffe9 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -123,11 +123,11 @@ class Configuration(object): def _load_logging_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load logging configuration: - the --loglevel, --logfile options + the -v/--verbose, --logfile options """ # Log level - if 'loglevel' in self.args and self.args.loglevel: - config.update({'verbosity': self.args.loglevel}) + if 'verbosity' in self.args and self.args.verbosity: + config.update({'verbosity': self.args.verbosity}) else: config.update({'verbosity': 0}) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 56dcf2486..b94038a44 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -28,7 +28,7 @@ def _set_loggers(verbosity: int = 0) -> None: def setup_logging(config: Dict[str, Any]) -> None: """ - Process --loglevel, --logfile options + Process -v/--verbose, --logfile options """ # Log level verbosity = config['verbosity'] diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 19a7dc423..888135fa1 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -227,7 +227,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "loglevel": 3, + "verbosity": 3, } return configuration diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 8f0dec226..8186892aa 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -19,7 +19,7 @@ def test_parse_args_defaults() -> None: assert args.config == ['config.json'] assert args.strategy_path is None assert args.datadir is None - assert args.loglevel == 0 + assert args.verbosity == 0 def test_parse_args_config() -> None: @@ -42,10 +42,10 @@ def test_parse_args_db_url() -> None: def test_parse_args_verbose() -> None: args = Arguments(['-v'], '').get_parsed_arg() - assert args.loglevel == 1 + assert args.verbosity == 1 args = Arguments(['--verbose'], '').get_parsed_arg() - assert args.loglevel == 1 + assert args.verbosity == 1 def test_common_scripts_options() -> None: @@ -146,7 +146,7 @@ def test_parse_args_backtesting_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == ['test_conf.json'] assert call_args.live is True - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval == '1m' @@ -165,7 +165,7 @@ def test_parse_args_hyperopt_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == ['test_conf.json'] assert call_args.epochs == 20 - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'hyperopt' assert call_args.spaces == ['buy'] assert call_args.func is not None diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e6a2006f9..9c578099d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -27,7 +27,7 @@ def test_parse_args_backtesting(mocker) -> None: call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] assert call_args.live is False - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval is None @@ -41,7 +41,7 @@ def test_main_start_hyperopt(mocker) -> None: assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == ['config.json'] - assert call_args.loglevel == 0 + assert call_args.verbosity == 0 assert call_args.subparser == 'hyperopt' assert call_args.func is not None From 6c2415d32fba4dfb784f3e0c18652b9eb304fa80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jul 2019 06:36:35 +0200 Subject: [PATCH 737/928] Rename parameters from pair to curr --- 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 a41b51e90..8aacc0cee 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -270,14 +270,14 @@ class Exchange(object): f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') - def get_valid_pair_combination(self, paira, pairb) -> str: + def get_valid_pair_combination(self, curr_1, curr_2) -> str: """ - Get valid combination of paira and pairb by trying both combinations. + Get valid pair combination of curr_1 and curr_2 by trying both combinations. """ - for pair in [f"{paira}/{pairb}", f"{pairb}/{paira}"]: + for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {paira} and {pairb} to get a valid pair.") + raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: List[str]) -> None: """ From f7a2428deb15967c9a13bbc0018009ccdc6f906c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 10:13:00 +0300 Subject: [PATCH 738/928] max_open_trades may be -1 --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index cb6b30b05..cb45bcdf5 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -106,7 +106,7 @@ AVAILABLE_CLI_OPTIONS = { "max_open_trades": Arg( '--max_open_trades', help='Specify max_open_trades to use.', - type=check_int_positive, + type=int, metavar='INT', ), "stake_amount": Arg( From 15d2cbd6df240af079c3c667be91219b560596a4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 7 Jul 2019 10:17:01 +0300 Subject: [PATCH 739/928] loggers: wording improved --- freqtrade/loggers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index b94038a44..90b8905e5 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) def _set_loggers(verbosity: int = 0) -> None: """ - Set the logging level for Third party libs + Set the logging level for third party libraries :return: None """ From c4fb0fd6ca532902a09bd7dacf0f6935a462b8f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2019 17:00:34 +0200 Subject: [PATCH 740/928] Don't run the bot with python3 freqtrade * we can either use `python3 -m freqtrade ...` or `freqtrade ...` - and shorter should be better. --- docs/backtesting.md | 18 +++++++++--------- docs/bot-usage.md | 12 ++++++------ docs/deprecated.md | 4 ++-- docs/edge.md | 8 ++++---- docs/faq.md | 4 ++-- docs/hyperopt.md | 4 ++-- docs/installation.md | 2 +- docs/strategy-customization.md | 6 +++--- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2a5163e73..179bcee15 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -24,37 +24,37 @@ The backtesting is very easy with freqtrade. #### With 5 min tickers (Per default) ```bash -python3 freqtrade backtesting +freqtrade backtesting ``` #### With 1 min tickers ```bash -python3 freqtrade backtesting --ticker-interval 1m +freqtrade backtesting --ticker-interval 1m ``` #### Update cached pairs with the latest data ```bash -python3 freqtrade backtesting --refresh-pairs-cached +freqtrade backtesting --refresh-pairs-cached ``` #### With live data (do not alter your testdata files) ```bash -python3 freqtrade backtesting --live +freqtrade backtesting --live ``` #### Using a different on-disk ticker-data source ```bash -python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 +freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 ``` #### With a (custom) strategy file ```bash -python3 freqtrade -s TestStrategy backtesting +freqtrade -s TestStrategy backtesting ``` Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory @@ -62,7 +62,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_ #### Exporting trades to file ```bash -python3 freqtrade backtesting --export trades +freqtrade backtesting --export trades ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. @@ -70,7 +70,7 @@ The exported trades can be used for [further analysis](#further-backtest-result- #### Exporting trades to file specifying a custom filename ```bash -python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json +freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json ``` #### Running backtest with smaller testset @@ -81,7 +81,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 freqtrade backtesting --timerange=-200 +freqtrade backtesting --timerange=-200 ``` #### Advanced use of timerange diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 0e01ac0e5..85692ae14 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -49,7 +49,7 @@ The bot allows you to select which configuration file you want to use. Per default, the bot will load the file `./config.json` ```bash -python3 freqtrade -c path/far/far/away/config.json +freqtrade -c path/far/far/away/config.json ``` ### How to use multiple configuration files? @@ -65,13 +65,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua require them): ```bash -python3 freqtrade -c ./config.json +freqtrade -c ./config.json ``` and specify both configuration files when running in the normal Live Trade Mode: ```bash -python3 freqtrade -c ./config.json -c path/to/secrets/keys.config.json +freqtrade -c ./config.json -c path/to/secrets/keys.config.json ``` This could help you hide your private Exchange key and Exchange secrete on you local machine @@ -97,7 +97,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` If the bot does not find your strategy file, it will display in an error @@ -111,7 +111,7 @@ Learn more about strategy file in This parameter allows you to add an additional strategy lookup path, which gets checked before the default locations (The passed path must be a directory!): ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? @@ -138,7 +138,7 @@ using `--db-url`. This can also be used to specify a custom database in production mode. Example command: ```bash -python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite +freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` ## Backtesting commands diff --git a/docs/deprecated.md b/docs/deprecated.md index c218bd360..b63c8f823 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -13,14 +13,14 @@ on BaseVolume. This value can be changed when you run the script. Get the 20 currencies based on BaseVolume. ```bash -python3 freqtrade --dynamic-whitelist +freqtrade --dynamic-whitelist ``` **Customize the number of currencies to retrieve** Get the 30 currencies based on BaseVolume. ```bash -python3 freqtrade --dynamic-whitelist 30 +freqtrade --dynamic-whitelist 30 ``` **Exception** diff --git a/docs/edge.md b/docs/edge.md index b0e0b2d42..93c15d330 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -209,7 +209,7 @@ Edge will remove sudden pumps in a given market while going through historical d You can run Edge independently in order to see in details the result. Here is an example: ```bash -python3 freqtrade edge +freqtrade edge ``` An example of its output: @@ -235,19 +235,19 @@ An example of its output: ### Update cached pairs with the latest data ```bash -python3 freqtrade edge --refresh-pairs-cached +freqtrade edge --refresh-pairs-cached ``` ### Precising stoploss range ```bash -python3 freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step +freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step ``` ### Advanced use of timerange ```bash -python3 freqtrade edge --timerange=20181110-20181113 +freqtrade edge --timerange=20181110-20181113 ``` Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. diff --git a/docs/faq.md b/docs/faq.md index c551e3638..22cfcd616 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -47,13 +47,13 @@ compute. We recommend you to run it at least 10.000 epochs: ```bash -python3 freqtrade hyperopt -e 10000 +freqtrade hyperopt -e 10000 ``` or if you want intermediate result to see ```bash -for i in {1..100}; do python3 freqtrade hyperopt -e 100; done +for i in {1..100}; do freqtrade hyperopt -e 100; done ``` #### Why it is so long to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index a15fd575a..062751908 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -158,7 +158,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -python3 freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all +freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -184,7 +184,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 freqtrade hyperopt --timerange -200 +freqtrade hyperopt --timerange -200 ``` ### Running Hyperopt with Smaller Search Space diff --git a/docs/installation.md b/docs/installation.md index 544706f87..657273e2f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -184,7 +184,7 @@ python3 -m pip install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3 freqtrade -c config.json +freqtrade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 800012a0f..15f44955b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -13,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` ## Change your strategy @@ -40,7 +40,7 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us You can test it with the parameter: `--strategy TestStrategy` ```bash -python3 freqtrade --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) @@ -400,7 +400,7 @@ The default buy strategy is located in the file If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/directory +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas From 27cb1a41746d4f6eceefbdea8800014f524b59bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jul 2019 17:08:14 +0200 Subject: [PATCH 741/928] Add FAQ section explaining "module not found" errors --- docs/faq.md | 34 +++++++++++++++++++++++----------- setup.sh | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 22cfcd616..83576af4d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,14 +1,25 @@ # Freqtrade FAQ -### Freqtrade commons +## Freqtrade common issues -#### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### The bot does not start + +Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. + +This could have the following reasons: + +* The virtual environment is not active + * run `source .env/bin/activate` to activate the virtual environment +* The installation did not work correctly. + * Please check the [Installation documentation](installation.md). + +### I have waited 5 minutes, why hasn't the bot made any trades yet?! Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient! -#### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative?! I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -19,24 +30,24 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? +### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again. -#### I want to improve the bot with a new strategy +### I want to improve the bot with a new strategy That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -#### Is there a setting to only SELL the coins being held and not perform anymore BUYS? +### Is there a setting to only SELL the coins being held and not perform anymore BUYS? You can use the `/forcesell all` command from Telegram. -### Hyperopt module +## Hyperopt module -#### How many epoch do I need to get a good Hyperopt result? +### How many epoch do I need to get a good Hyperopt result? Per default Hyperopts without `-e` or `--epochs` parameter will only run 100 epochs, means 100 evals of your triggers, guards, ... Too few @@ -56,7 +67,7 @@ or if you want intermediate result to see for i in {1..100}; do freqtrade hyperopt -e 100; done ``` -#### Why it is so long to run hyperopt? +### Why it is so long to run hyperopt? Finding a great Hyperopt results takes time. @@ -74,13 +85,14 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space. -### Edge module +## Edge module -#### Edge implements interesting approach for controlling position size, is there any theory behind it? +### Edge implements interesting approach for controlling position size, is there any theory behind it? The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. You can find further info on expectancy, winrate, risk management and position size in the following sources: + - https://www.tradeciety.com/ultimate-math-guide-for-traders/ - http://www.vantharp.com/tharp-concepts/expectancy.asp - https://samuraitradingacademy.com/trading-expectancy/ diff --git a/setup.sh b/setup.sh index c99a98eb1..fe7110ef6 100755 --- a/setup.sh +++ b/setup.sh @@ -242,7 +242,7 @@ function install() { echo "-------------------------" echo "Run the bot !" echo "-------------------------" - echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade'." + echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade'." } function plot() { From 322227bf67bd8889d334f5d56c398bf85668422a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 9 Jul 2019 00:59:34 +0300 Subject: [PATCH 742/928] fix #2005 --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ccb932698..7904d42c9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -42,7 +42,7 @@ def init_plotscript(config): pairs = config["exchange"]["pair_whitelist"] # Set timerange to use - timerange = Arguments.parse_timerange(config["timerange"]) + timerange = Arguments.parse_timerange(config.get("timerange")) tickers = history.load_data( datadir=Path(str(config.get("datadir"))), From c474e2ac86bc6b1a5a448f11a76622e3ed59cd57 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jul 2019 01:45:02 +0300 Subject: [PATCH 743/928] fix #2008 --- freqtrade/optimize/backtesting.py | 3 +++ freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fd47ef9a..9abc68fec 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -319,6 +319,9 @@ class Backtesting(object): position_stacking: do we allow position stacking? (default: False) :return: DataFrame """ + # Arguments are long and noisy, so this is commented out. + # Uncomment if you need to debug the backtest() method. +# logger.debug(f"Start backtest, args: {args}") processed = args['processed'] stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..33f28bf6b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -195,7 +195,7 @@ class Hyperopt(Backtesting): { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'position_stacking': self.config.get('position_stacking', True), + 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, } From 6a431280196685fa10f2033a9021e4aa36409c06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jul 2019 08:49:42 +0200 Subject: [PATCH 744/928] Fix non-rendering docs --- docs/rest-api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 0508f83e4..afecc1d80 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -16,10 +16,10 @@ Sample configuration: }, ``` -!!! Danger: Security warning +!!! Danger Security warning By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger: Password selection +!!! Danger Password selection Please make sure to select a very strong, unique password to protect your bot from unauthorized access. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. @@ -62,7 +62,7 @@ docker run -d \ ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. ## Consuming the API From e993e010f4bfcf220c23813f287f6f2d19b0ddbe Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Jul 2019 23:02:57 +0300 Subject: [PATCH 745/928] Fix #2013 --- freqtrade/optimize/hyperopt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..0a51a8fb7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -288,9 +288,8 @@ class Hyperopt(Backtesting): (max_date - min_date).days ) - if self.has_space('buy') or self.has_space('sell'): - self.strategy.advise_indicators = \ - self.custom_hyperopt.populate_indicators # type: ignore + self.strategy.advise_indicators = \ + self.custom_hyperopt.populate_indicators # type: ignore preprocessed = self.strategy.tickerdata_to_dataframe(data) From 1bdffcc73bf5cb492766d282edf1fcfeafab6dd1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 11 Jul 2019 21:23:23 +0300 Subject: [PATCH 746/928] make configuration a sep. module, including arguments --- freqtrade/configuration/__init__.py | 8 ++ freqtrade/{ => configuration}/arguments.py | 1 + freqtrade/configuration/check_exchange.py | 48 ++++++++ .../{ => configuration}/configuration.py | 112 +++--------------- freqtrade/configuration/json_schema.py | 53 +++++++++ freqtrade/data/history.py | 2 +- freqtrade/edge/__init__.py | 3 +- freqtrade/main.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/edge_cli.py | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/plot/plotting.py | 2 +- freqtrade/tests/conftest.py | 5 +- freqtrade/tests/data/test_btanalysis.py | 2 +- freqtrade/tests/data/test_history.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 2 +- freqtrade/tests/strategy/test_interface.py | 2 +- freqtrade/tests/test_arguments.py | 5 +- freqtrade/tests/test_configuration.py | 38 +++--- freqtrade/tests/test_main.py | 2 +- freqtrade/tests/test_plotting.py | 2 +- scripts/download_backtest_data.py | 6 +- scripts/plot_dataframe.py | 2 +- scripts/plot_profit.py | 2 +- 24 files changed, 164 insertions(+), 143 deletions(-) create mode 100644 freqtrade/configuration/__init__.py rename freqtrade/{ => configuration}/arguments.py (99%) create mode 100644 freqtrade/configuration/check_exchange.py rename freqtrade/{ => configuration}/configuration.py (78%) create mode 100644 freqtrade/configuration/json_schema.py diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py new file mode 100644 index 000000000..2f74dc597 --- /dev/null +++ b/freqtrade/configuration/__init__.py @@ -0,0 +1,8 @@ +from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 + +from freqtrade.configuration.arguments import ( # noqa: F401 + ARGS_DOWNLOADER, + ARGS_PLOT_DATAFRAME, + ARGS_PLOT_PROFIT) + +from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/arguments.py b/freqtrade/configuration/arguments.py similarity index 99% rename from freqtrade/arguments.py rename to freqtrade/configuration/arguments.py index cb45bcdf5..3e940ae2a 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -5,6 +5,7 @@ import argparse import os import re from typing import List, NamedTuple, Optional + import arrow from freqtrade import __version__, constants diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py new file mode 100644 index 000000000..8dae06f7a --- /dev/null +++ b/freqtrade/configuration/check_exchange.py @@ -0,0 +1,48 @@ +import logging +from typing import Any, Dict + +from freqtrade import OperationalException +from freqtrade.exchange import (is_exchange_bad, is_exchange_available, + is_exchange_officially_supported, available_exchanges) + + +logger = logging.getLogger(__name__) + + +def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: + """ + Check if the exchange name in the config file is supported by Freqtrade + :param check_for_bad: if True, check the exchange against the list of known 'bad' + exchanges + :return: False if exchange is 'bad', i.e. is known to work with the bot with + critical issues or does not work at all, crashes, etc. True otherwise. + raises an exception if the exchange if not supported by ccxt + and thus is not known for the Freqtrade at all. + """ + logger.info("Checking exchange...") + + exchange = config.get('exchange', {}).get('name').lower() + if not is_exchange_available(exchange): + raise OperationalException( + f'Exchange "{exchange}" is not supported by ccxt ' + f'and therefore not available for the bot.\n' + f'The following exchanges are supported by ccxt: ' + f'{", ".join(available_exchanges())}' + ) + + if check_for_bad and is_exchange_bad(exchange): + logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' + f'Use it only for development and testing purposes.') + return False + + if is_exchange_officially_supported(exchange): + logger.info(f'Exchange "{exchange}" is officially supported ' + f'by the Freqtrade development team.') + else: + logger.warning(f'Exchange "{exchange}" is supported by ccxt ' + f'and therefore available for the bot but not officially supported ' + f'by the Freqtrade development team. ' + f'It may work flawlessly (please report back) or have serious issues. ' + f'Use it at your own discretion.') + + return True diff --git a/freqtrade/configuration.py b/freqtrade/configuration/configuration.py similarity index 78% rename from freqtrade/configuration.py rename to freqtrade/configuration/configuration.py index 8ad0fffe9..760c470cb 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -8,12 +8,9 @@ import sys from argparse import Namespace from typing import Any, Callable, Dict, Optional -from jsonschema import Draft4Validator, validators -from jsonschema.exceptions import ValidationError, best_match - from freqtrade import OperationalException, constants -from freqtrade.exchange import (is_exchange_bad, is_exchange_available, - is_exchange_officially_supported, available_exchanges) +from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -22,31 +19,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def _extend_validator(validator_class): - """ - Extended validator for the Freqtrade configuration JSON Schema. - Currently it only handles defaults for subschemas. - """ - validate_properties = validator_class.VALIDATORS['properties'] - - def set_defaults(validator, properties, instance, schema): - for prop, subschema in properties.items(): - if 'default' in subschema: - instance.setdefault(prop, subschema['default']) - - for error in validate_properties( - validator, properties, instance, schema, - ): - yield error - - return validators.extend( - validator_class, {'properties': set_defaults} - ) - - -FreqtradeValidator = _extend_validator(Draft4Validator) - - class Configuration(object): """ Class to read and init the bot configuration @@ -58,6 +30,16 @@ class Configuration(object): self.config: Optional[Dict[str, Any]] = None self.runmode = runmode + def get_config(self) -> Dict[str, Any]: + """ + Return the config. Use this method to get the bot config + :return: Dict: Bot config + """ + if self.config is None: + self.config = self.load_config() + + return self.config + def load_config(self) -> Dict[str, Any]: """ Extract information for sys.argv and load the bot configuration @@ -75,7 +57,7 @@ class Configuration(object): config['internals'] = {} logger.info('Validating configuration ...') - self._validate_config_schema(config) + validate_config_schema(config) self._validate_config_consistency(config) # Set strategy if not specified in config and or if it's non default @@ -185,7 +167,7 @@ class Configuration(object): logger.info(f'Using DB: "{config["db_url"]}"') # Check if the exchange set by the user is supported - self.check_exchange(config) + check_exchange(config) return config @@ -337,24 +319,6 @@ class Configuration(object): logstring='Using trades from: {}') return config - def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: - """ - Validate the configuration follow the Config Schema - :param conf: Config in JSON format - :return: Returns the config if valid, otherwise throw an exception - """ - try: - FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) - return conf - except ValidationError as exception: - logger.critical( - 'Invalid configuration. See config.json.example. Reason: %s', - exception - ) - raise ValidationError( - best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message - ) - def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: """ Validate the configuration consistency @@ -383,51 +347,3 @@ class Configuration(object): raise OperationalException( f'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive_offset in your config.') - - def get_config(self) -> Dict[str, Any]: - """ - Return the config. Use this method to get the bot config - :return: Dict: Bot config - """ - if self.config is None: - self.config = self.load_config() - - return self.config - - def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool: - """ - Check if the exchange name in the config file is supported by Freqtrade - :param check_for_bad: if True, check the exchange against the list of known 'bad' - exchanges - :return: False if exchange is 'bad', i.e. is known to work with the bot with - critical issues or does not work at all, crashes, etc. True otherwise. - raises an exception if the exchange if not supported by ccxt - and thus is not known for the Freqtrade at all. - """ - logger.info("Checking exchange...") - - exchange = config.get('exchange', {}).get('name').lower() - if not is_exchange_available(exchange): - raise OperationalException( - f'Exchange "{exchange}" is not supported by ccxt ' - f'and therefore not available for the bot.\n' - f'The following exchanges are supported by ccxt: ' - f'{", ".join(available_exchanges())}' - ) - - if check_for_bad and is_exchange_bad(exchange): - logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. ' - f'Use it only for development and testing purposes.') - return False - - if is_exchange_officially_supported(exchange): - logger.info(f'Exchange "{exchange}" is officially supported ' - f'by the Freqtrade development team.') - else: - logger.warning(f'Exchange "{exchange}" is supported by ccxt ' - f'and therefore available for the bot but not officially supported ' - f'by the Freqtrade development team. ' - f'It may work flawlessly (please report back) or have serious issues. ' - f'Use it at your own discretion.') - - return True diff --git a/freqtrade/configuration/json_schema.py b/freqtrade/configuration/json_schema.py new file mode 100644 index 000000000..4c6f4a4a0 --- /dev/null +++ b/freqtrade/configuration/json_schema.py @@ -0,0 +1,53 @@ +import logging +from typing import Any, Dict + +from jsonschema import Draft4Validator, validators +from jsonschema.exceptions import ValidationError, best_match + +from freqtrade import constants + + +logger = logging.getLogger(__name__) + + +def _extend_validator(validator_class): + """ + Extended validator for the Freqtrade configuration JSON Schema. + Currently it only handles defaults for subschemas. + """ + validate_properties = validator_class.VALIDATORS['properties'] + + def set_defaults(validator, properties, instance, schema): + for prop, subschema in properties.items(): + if 'default' in subschema: + instance.setdefault(prop, subschema['default']) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {'properties': set_defaults} + ) + + +FreqtradeValidator = _extend_validator(Draft4Validator) + + +def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate the configuration follow the Config Schema + :param conf: Config in JSON format + :return: Returns the config if valid, otherwise throw an exception + """ + try: + FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) + return conf + except ValidationError as e: + logger.critical( + f"Invalid configuration. See config.json.example. Reason: {e}" + ) + raise ValidationError( + best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message + ) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 2a0d9b15e..f600615df 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -16,7 +16,7 @@ import arrow from pandas import DataFrame from freqtrade import OperationalException, misc -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange, timeframe_to_minutes diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4bc8bb493..7085663d6 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -10,8 +10,7 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade import constants, OperationalException -from freqtrade.arguments import Arguments -from freqtrade.arguments import TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history from freqtrade.strategy.interface import SellType diff --git a/freqtrade/main.py b/freqtrade/main.py index f02159a0e..a96fd43c5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -15,7 +15,7 @@ from argparse import Namespace from typing import Any, List from freqtrade import OperationalException -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.worker import Worker diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fd47ef9a..0592790ba 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 tabulate import tabulate -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 231493e4d..8d1fa381b 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.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7fd9bf5d9..09f99ab7b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,7 +18,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7904d42c9..dde6f78f0 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.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, load_trades) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 888135fa1..be1139863 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -14,7 +14,7 @@ import pytest from telegram import Chat, Message, Update from freqtrade import constants, persistence -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -22,6 +22,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker + logging.getLogger('').setLevel(logging.INFO) @@ -39,7 +40,7 @@ def log_has_re(line, logs): False) -def get_args(args) -> List[str]: +def get_args(args): return Arguments(args, '').get_parsed_arg() diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index e8872f9a4..e80840009 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.arguments import Arguments, TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, combine_tickers_with_mean, create_cum_profit, diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 46bcf06c4..424333e99 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -12,7 +12,7 @@ import pytest from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 498a04db5..bea36c30c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -11,7 +11,7 @@ import pytest from arrow import Arrow from freqtrade import DependencyException, constants -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index e92cb7b1c..ee8c8ddd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import arrow from pandas import DataFrame -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.persistence import Trade diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 8186892aa..ff1b0e6b0 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -3,8 +3,9 @@ import argparse import pytest -from freqtrade.arguments import (ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME, - Arguments, TimeRange, check_int_positive) +from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import check_int_positive # Parse common command-line-arguments. Used for all tools diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index c5e60be7f..156635128 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -11,8 +11,9 @@ import pytest from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade.configuration import Arguments, Configuration +from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode @@ -32,24 +33,21 @@ def test_load_config_invalid_pair(default_conf) -> None: default_conf['exchange']['pair_whitelist'].append('ETH-BTC') with pytest.raises(ValidationError, match=r'.*does not match.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_missing_attributes(default_conf) -> None: default_conf.pop('exchange') with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: default_conf['stake_amount'] = 'fake' with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(default_conf) + validate_config_schema(default_conf) def test_load_config_file(default_conf, mocker, caplog) -> None: @@ -469,25 +467,23 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None: - configuration = Configuration(Namespace()) - # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'BITTREX'}) - assert configuration.check_exchange(default_conf) + assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", caplog.record_tuples) caplog.clear() # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) - assert configuration.check_exchange(default_conf) + assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", caplog.record_tuples) caplog.clear() # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'kraken'}) - assert configuration.check_exchange(default_conf) + assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " r"by the Freqtrade development team\. .*", caplog.record_tuples) @@ -495,7 +491,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) - assert not configuration.check_exchange(default_conf) + assert not check_exchange(default_conf) assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " r"Use it only for development and testing purposes\.", caplog.record_tuples) @@ -503,7 +499,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) - assert configuration.check_exchange(default_conf, False) + assert check_exchange(default_conf, False) assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " r"by the Freqtrade development team\. .*", caplog.record_tuples) @@ -511,14 +507,13 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an invalid exchange default_conf.get('exchange').update({'name': 'unknown_exchange'}) - configuration.config = default_conf with pytest.raises( OperationalException, match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' r'and therefore not available for the bot.*' ): - configuration.check_exchange(default_conf) + check_exchange(default_conf) def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: @@ -656,8 +651,7 @@ def test_load_config_default_exchange(all_conf) -> None: with pytest.raises(ValidationError, match=r'\'exchange\' is a required property'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) def test_load_config_default_exchange_name(all_conf) -> None: @@ -671,8 +665,7 @@ def test_load_config_default_exchange_name(all_conf) -> None: with pytest.raises(ValidationError, match=r'\'name\' is a required property'): - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) @pytest.mark.parametrize("keys", [("exchange", "sandbox", False), @@ -695,7 +688,6 @@ def test_load_config_default_subkeys(all_conf, keys) -> None: assert subkey not in all_conf[key] - configuration = Configuration(Namespace()) - configuration._validate_config_schema(all_conf) + validate_config_schema(all_conf) assert subkey in all_conf[key] assert all_conf[key][subkey] == keys[2] diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 9c578099d..2f778e5fa 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest from freqtrade import OperationalException -from freqtrade.arguments import Arguments +from freqtrade.configuration import Arguments from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cef229d19..cb1852223 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_objs as go from plotly import tools -from freqtrade.arguments import Arguments, TimeRange +from freqtrade.configuration import Arguments, 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, diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 472b3ef4e..60a9e2cbc 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -8,8 +8,10 @@ import sys from pathlib import Path from typing import Any, Dict, List -from freqtrade.arguments import Arguments, TimeRange, ARGS_DOWNLOADER +from freqtrade.configuration import Arguments, TimeRange +from freqtrade.configuration import ARGS_DOWNLOADER from freqtrade.configuration import Configuration +from freqtrade.configuration.check_exchange import check_exchange from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange from freqtrade.misc import deep_merge_dicts @@ -79,7 +81,7 @@ if args.config and args.exchange: "using exchange settings from the configuration file.") # Check if the exchange set by the user is supported -configuration.check_exchange(config) +check_exchange(config) configuration._load_datadir_config(config) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 1e2d9f248..f0bc30366 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -18,7 +18,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.arguments import ARGS_PLOT_DATAFRAME, Arguments +from freqtrade.configuration import Arguments, ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 7442ef155..632b7879a 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,7 +8,7 @@ import logging import sys from typing import Any, Dict, List -from freqtrade.arguments import ARGS_PLOT_PROFIT, Arguments +from freqtrade.configuration import Arguments, ARGS_PLOT_PROFIT from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode From 94e6fb89b327dbc7c114cdb6fef93a94150b76a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 00:39:42 +0300 Subject: [PATCH 747/928] tests happy --- freqtrade/tests/conftest.py | 14 ++++ freqtrade/tests/optimize/test_backtesting.py | 30 +++---- freqtrade/tests/optimize/test_edge_cli.py | 18 ++--- freqtrade/tests/optimize/test_hyperopt.py | 38 +++------ freqtrade/tests/test_configuration.py | 83 +++++++------------- freqtrade/tests/test_main.py | 33 ++------ 6 files changed, 76 insertions(+), 140 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index be1139863..9be610090 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -44,6 +44,20 @@ def get_args(args): return Arguments(args, '').get_parsed_arg() +def patched_configuration_open(mocker, config): + file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + read_data=json.dumps(config) + )) + return file_mock + + +def patched_configuration_load_config_file(mocker, config) -> None: + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._load_config_file', + lambda *args, **kwargs: config + ) + + def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index bea36c30c..d05d07a1b 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -22,7 +22,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_open) def trim_dictlist(dict_list, num): @@ -165,9 +166,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -205,10 +204,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -276,9 +273,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -295,9 +290,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -828,9 +822,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -880,9 +872,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): gen_strattable_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 7cc41b095..c81a8dce8 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -8,13 +8,12 @@ from freqtrade.edge import PairInfo from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_open) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -46,10 +45,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, edge_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -92,9 +89,8 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) + patched_configuration_open(mocker, edge_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e5f87b022..a83b4c0d0 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,7 +16,8 @@ from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_load_config_file, patched_configuration_open) @pytest.fixture(scope='function') @@ -44,9 +45,7 @@ def create_trials(mocker, hyperopt) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = [ '--config', 'config.json', @@ -82,10 +81,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -148,11 +145,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo def test_hyperoptresolver(mocker, default_conf, caplog) -> None: + patched_configuration_load_config_file(mocker, default_conf) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) hyperopts = DefaultHyperOpts delattr(hyperopts, 'populate_buy_trend') delattr(hyperopts, 'populate_sell_trend') @@ -172,10 +166,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -198,10 +189,7 @@ def test_start(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None: - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', @@ -226,10 +214,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: def test_start_failure(mocker, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -250,10 +235,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: def test_start_filelock(mocker, default_conf, caplog) -> None: start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 156635128..09ba6cd54 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -17,7 +17,7 @@ from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re +from freqtrade.tests.conftest import log_has, log_has_re, patched_configuration_open @pytest.fixture(scope="function") @@ -51,9 +51,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + file_mock = patched_configuration_open(mocker, default_conf) configuration = Configuration(Namespace()) validated_conf = configuration._load_config_file('somefile') @@ -63,9 +61,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -87,7 +83,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: config_files = [conf1, conf2] configsmock = MagicMock(side_effect=config_files) - mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock) + mocker.patch('freqtrade.configuration.configuration.Configuration._load_config_file', configsmock) arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] args = Arguments(arg_list, '').get_parsed_arg() @@ -107,9 +103,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -124,7 +118,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> def test_load_config_file_exception(mocker) -> None: mocker.patch( - 'freqtrade.configuration.open', + 'freqtrade.configuration.configuration.open', MagicMock(side_effect=FileNotFoundError('File not found')) ) configuration = Configuration(Namespace()) @@ -134,9 +128,7 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -148,9 +140,8 @@ def test_load_config(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -171,9 +162,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False conf["db_url"] = "sqlite:///path/to/db.sqlite" - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -189,9 +178,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = "sqlite:///path/to/db.sqlite" - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -207,9 +194,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False del conf["db_url"] - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -227,9 +212,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = DEFAULT_DB_PROD_URL - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) - )) + patched_configuration_open(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -247,9 +230,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: 'strategy': 'CustomStrategy', 'strategy_path': '/tmp/strategies', }) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -260,9 +241,8 @@ def test_load_custom_strategy(default_conf, mocker) -> None: def test_show_info(default_conf, mocker, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -285,9 +265,8 @@ def test_show_info(default_conf, mocker, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -325,10 +304,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_open(mocker, default_conf) + mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) arglist = [ '--config', 'config.json', @@ -391,9 +368,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non """ Test setup_configuration() function """ - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -441,9 +416,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) + arglist = [ 'hyperopt', '--epochs', '10', @@ -517,8 +491,8 @@ def test_check_exchange(default_conf, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf))) + patched_configuration_open(mocker, default_conf) + # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) arglist = ['-vvv'] @@ -570,8 +544,7 @@ def test_set_loggers() -> None: def test_set_logfile(default_conf, mocker): - mocker.patch('freqtrade.configuration.open', - mocker.mock_open(read_data=json.dumps(default_conf))) + patched_configuration_open(mocker, default_conf) arglist = [ '--logfile', 'test_file.log', @@ -588,9 +561,7 @@ def test_set_logfile(default_conf, mocker): def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_open(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 2f778e5fa..8e5a24f8e 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -10,7 +10,8 @@ from freqtrade.configuration import Arguments from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State -from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.tests.conftest import (log_has, patch_exchange, + patched_configuration_load_config_file) from freqtrade.worker import Worker @@ -50,10 +51,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -70,10 +68,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -93,10 +88,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -118,10 +110,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: State.RUNNING, OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) @@ -145,10 +134,7 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -159,10 +145,7 @@ def test_reconfigure(mocker, default_conf) -> None: # Renew mock to return modified data conf = deepcopy(default_conf) conf['stake_amount'] += 1 - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: conf - ) + patched_configuration_load_config_file(mocker, conf) worker._config = conf # reconfigure should return a new instance From 7e103e34f81c06e5702b4202e79cc5966d0dea4c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 01:41:09 +0300 Subject: [PATCH 748/928] flake happy --- freqtrade/tests/conftest.py | 1 - freqtrade/tests/optimize/test_backtesting.py | 8 +++++--- freqtrade/tests/optimize/test_edge_cli.py | 8 +++++--- freqtrade/tests/optimize/test_hyperopt.py | 9 ++++++--- freqtrade/tests/test_configuration.py | 11 ++++++++--- freqtrade/tests/test_main.py | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 9be610090..f57e58774 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -6,7 +6,6 @@ from copy import deepcopy from datetime import datetime from functools import reduce from pathlib import Path -from typing import List from unittest.mock import MagicMock, PropertyMock import arrow diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d05d07a1b..55405ea3d 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument -import json import math import random from unittest.mock import MagicMock @@ -23,7 +22,7 @@ from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_open) def trim_dictlist(dict_list, num): @@ -205,7 +204,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index c81a8dce8..470a831fb 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103, C0330 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -import json from unittest.mock import MagicMock from freqtrade.edge import PairInfo @@ -9,7 +8,7 @@ from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_open) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: @@ -46,7 +45,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: patched_configuration_open(mocker, edge_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a83b4c0d0..386893572 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -import json import os from datetime import datetime from unittest.mock import MagicMock @@ -17,7 +16,8 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file, patched_configuration_open) + patched_configuration_load_config_file, + patched_configuration_open) @pytest.fixture(scope='function') @@ -82,7 +82,10 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 09ba6cd54..d25d36526 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name -import json import logging from argparse import Namespace from copy import deepcopy @@ -83,7 +82,10 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: config_files = [conf1, conf2] configsmock = MagicMock(side_effect=config_files) - mocker.patch('freqtrade.configuration.configuration.Configuration._load_config_file', configsmock) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._load_config_file', + configsmock + ) arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] args = Arguments(arg_list, '').get_parsed_arg() @@ -305,7 +307,10 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) - mocker.patch('freqtrade.configuration.configuration.Configuration._create_datadir', lambda s, c, x: x) + mocker.patch( + 'freqtrade.configuration.configuration.Configuration._create_datadir', + lambda s, c, x: x + ) arglist = [ '--config', 'config.json', diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 8e5a24f8e..bcaad4d7d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -11,7 +11,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State from freqtrade.tests.conftest import (log_has, patch_exchange, - patched_configuration_load_config_file) + patched_configuration_load_config_file) from freqtrade.worker import Worker From bbfbd87a9fe86b371331cb496848beddec5da199 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 03:26:27 +0300 Subject: [PATCH 749/928] move create_datadir() to separate file --- freqtrade/configuration/configuration.py | 17 +++-------------- freqtrade/configuration/create_datadir.py | 18 ++++++++++++++++++ freqtrade/tests/optimize/test_backtesting.py | 4 ++-- freqtrade/tests/optimize/test_edge_cli.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- freqtrade/tests/test_configuration.py | 10 +++++----- 6 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 freqtrade/configuration/create_datadir.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 760c470cb..4959c2adc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -3,13 +3,13 @@ This module contains the configuration class """ import json import logging -import os import sys from argparse import Namespace from typing import Any, Callable, Dict, Optional from freqtrade import OperationalException, 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.loggers import setup_logging from freqtrade.misc import deep_merge_dicts @@ -171,17 +171,6 @@ class Configuration(object): return config - def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str: - if not datadir: - # set datadir - exchange_name = config.get('exchange', {}).get('name').lower() - datadir = os.path.join('user_data', 'data', exchange_name) - - if not os.path.isdir(datadir): - os.makedirs(datadir) - logger.info(f'Created data directory: {datadir}') - return datadir - def _args_to_config(self, config: Dict[str, Any], argname: str, logstring: str, logfun: Optional[Callable] = None) -> None: """ @@ -207,9 +196,9 @@ class Configuration(object): the --datadir option """ if 'datadir' in self.args and self.args.datadir: - config.update({'datadir': self._create_datadir(config, self.args.datadir)}) + config.update({'datadir': create_datadir(config, self.args.datadir)}) else: - config.update({'datadir': self._create_datadir(config, None)}) + config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/create_datadir.py new file mode 100644 index 000000000..ecb59bc84 --- /dev/null +++ b/freqtrade/configuration/create_datadir.py @@ -0,0 +1,18 @@ +import logging +import os +from typing import Any, Dict, Optional + + +logger = logging.getLogger(__name__) + + +def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: + if not datadir: + # set datadir + exchange_name = config.get('exchange', {}).get('name').lower() + datadir = os.path.join('user_data', 'data', exchange_name) + + if not os.path.isdir(datadir): + os.makedirs(datadir) + logger.info(f'Created data directory: {datadir}') + return datadir diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 55405ea3d..ee4d23d35 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -205,8 +205,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 470a831fb..582271af2 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -46,8 +46,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: patched_configuration_open(mocker, edge_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 386893572..7c00c3023 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -83,8 +83,8 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) args = [ diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d25d36526..f6d2deeaa 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -12,6 +12,7 @@ from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants from freqtrade.configuration import Arguments, Configuration 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.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers @@ -308,8 +309,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_open(mocker, default_conf) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._create_datadir', - lambda s, c, x: x + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x ) arglist = [ @@ -580,12 +581,11 @@ def test_validate_default_conf(default_conf) -> None: validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) -def test__create_datadir(mocker, default_conf, caplog) -> None: +def test_create_datadir(mocker, default_conf, caplog) -> None: mocker.patch('os.path.isdir', MagicMock(return_value=False)) md = MagicMock() mocker.patch('os.makedirs', md) - cfg = Configuration(Namespace()) - cfg._create_datadir(default_conf, '/foo/bar') + create_datadir(default_conf, '/foo/bar') assert md.call_args[0][0] == "/foo/bar" assert log_has('Created data directory: /foo/bar', caplog.record_tuples) From b499e74502d9b107b1fafb73a57f29fbabd7d105 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 12 Jul 2019 23:45:49 +0300 Subject: [PATCH 750/928] minor improvements to resolvers --- freqtrade/resolvers/exchange_resolver.py | 8 +++--- freqtrade/resolvers/hyperopt_resolver.py | 16 ++++++----- freqtrade/resolvers/iresolver.py | 11 ++++---- freqtrade/resolvers/pairlist_resolver.py | 18 ++++++------ freqtrade/resolvers/strategy_resolver.py | 21 ++++++++------ freqtrade/tests/pairlist/test_pairlist.py | 6 ++-- freqtrade/tests/strategy/test_strategy.py | 34 ++++++++++++----------- 7 files changed, 62 insertions(+), 52 deletions(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 089f6306f..5f1708ee6 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -45,13 +45,13 @@ class ExchangeResolver(IResolver): exchange = ex_class(kwargs['config']) if exchange: - logger.info("Using resolved exchange %s", exchange_name) + logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange except AttributeError: - # Pass and raise ImportError instead + # Pass and raise OperationalException instead pass raise ImportError( - "Impossible to load Exchange '{}'. This class does not exist" - " or contains Python code errors".format(exchange_name) + f"Impossible to load Exchange '{exchange_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 9333bb09a..30e097f3f 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,6 +7,7 @@ import logging from pathlib import Path from typing import Optional, Dict +from freqtrade import OperationalException from freqtrade.constants import DEFAULT_HYPEROPT from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.resolvers import IResolver @@ -63,15 +64,16 @@ class HyperOptResolver(IResolver): for _path in abs_paths: try: - hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, - object_name=hyperopt_name) + (hyperopt, module_path) = self._search_object(directory=_path, + object_type=IHyperOpt, + object_name=hyperopt_name) if hyperopt: - logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) + logger.info(f"Using resolved hyperopt {hyperopt_name} from '{module_path}'...") return hyperopt except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - "Impossible to load Hyperopt '{}'. This class does not exist" - " or contains Python code errors".format(hyperopt_name) + raise OperationalException( + f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 86b9a799b..1065abba7 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Optional, Type, Any +from typing import Any, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class IResolver(object): @staticmethod def _search_object(directory: Path, object_type, object_name: str, - kwargs: dict = {}) -> Optional[Any]: + kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path @@ -57,9 +57,10 @@ class IResolver(object): if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue + module_path = Path.resolve(directory.joinpath(entry)) obj = IResolver._get_valid_object( - object_type, Path.resolve(directory.joinpath(entry)), object_name + object_type, module_path, object_name ) if obj: - return obj(**kwargs) - return None + return (obj(**kwargs), module_path) + return (None, None) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 4306a9669..ac67ca496 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,6 +6,7 @@ This module load custom hyperopts import logging from pathlib import Path +from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -44,16 +45,17 @@ class PairListResolver(IResolver): for _path in abs_paths: try: - pairlist = self._search_object(directory=_path, object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) + (pairlist, module_path) = self._search_object(directory=_path, + object_type=IPairList, + object_name=pairlist_name, + kwargs=kwargs) if pairlist: - logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) + logger.info(f"Using resolved pairlist {pairlist_name} from '{module_path}'...") return pairlist except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - "Impossible to load Pairlist '{}'. This class does not exist" - " or contains Python code errors".format(pairlist_name) + raise OperationalException( + f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 47218bef4..4a5604db8 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,7 +11,7 @@ from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional -from freqtrade import constants +from freqtrade import constants, OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -149,10 +149,12 @@ class StrategyResolver(IResolver): for _path in abs_paths: try: - strategy = self._search_object(directory=_path, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) + (strategy, module_path) = self._search_object(directory=_path, + object_type=IStrategy, + object_name=strategy_name, + kwargs={'config': config}) if strategy: - logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) + logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") strategy._populate_fun_len = len( getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) @@ -161,11 +163,12 @@ class StrategyResolver(IResolver): return import_strategy(strategy, config=config) except TypeError as e: logger.warning( - f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}") + f"Impossible to load strategy '{strategy_name}' from {module_path}. " + f"Error: {e}") except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - raise ImportError( - f"Impossible to load Strategy '{strategy_name}'. This class does not exist" - " or contains Python code errors" + raise OperationalException( + f"Impossible to load Strategy '{strategy_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py index 38a8d78c7..e7439bb51 100644 --- a/freqtrade/tests/pairlist/test_pairlist.py +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -34,9 +34,9 @@ def whitelist_conf(default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - with pytest.raises(ImportError, - match=r"Impossible to load Pairlist 'NonexistingPairList'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Pairlist 'NonexistingPairList'. " + r"This class does not exist or contains Python code errors."): PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 8971b041c..904f84946 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -9,6 +9,7 @@ from unittest.mock import Mock import pytest from pandas import DataFrame +from freqtrade import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy @@ -44,21 +45,22 @@ def test_import_strategy(caplog): def test_search_strategy(): default_config = {} default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() - assert isinstance( - StrategyResolver._search_object( - directory=default_location, - object_type=IStrategy, - kwargs={'config': default_config}, - object_name='DefaultStrategy' - ), - IStrategy + + s, _ = StrategyResolver._search_object( + directory=default_location, + object_type=IStrategy, + kwargs={'config': default_config}, + object_name='DefaultStrategy' ) - assert StrategyResolver._search_object( + assert isinstance(s, IStrategy) + + s, _ = StrategyResolver._search_object( directory=default_location, object_type=IStrategy, kwargs={'config': default_config}, object_name='NotFoundStrategy' - ) is None + ) + assert s is None def test_load_strategy(result): @@ -85,18 +87,18 @@ def test_load_strategy_invalid_directory(result, caplog): def test_load_not_found_strategy(): strategy = StrategyResolver() - with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'NotFoundStrategy'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Strategy 'NotFoundStrategy'. " + r"This class does not exist or contains Python code errors."): strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) def test_load_staticmethod_importerror(mocker, caplog): mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( side_effect=TypeError("can't pickle staticmethod objects"))) - with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'DefaultStrategy'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Strategy 'DefaultStrategy'. " + r"This class does not exist or contains Python code errors."): StrategyResolver() assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) From 2e1269c4740d5d287866d382f70a30a91d6b5ec4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 13:30:57 +0200 Subject: [PATCH 751/928] Revert comment for Exception that's not changed --- freqtrade/resolvers/exchange_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 5f1708ee6..6fb12a65f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -48,7 +48,7 @@ class ExchangeResolver(IResolver): logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange except AttributeError: - # Pass and raise OperationalException instead + # Pass and raise ImportError instead pass raise ImportError( From 9887cb997ed18907995f60db8ee795d407a86ded Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:17:09 +0200 Subject: [PATCH 752/928] Check if Price is needed for market orders This is currently the case for: cex, coinex, cointiger, fcoin, fcoinjp, hadax, huobipro, huobiru, uex, --- freqtrade/exchange/exchange.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ec2832cbf..65f013a03 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -387,7 +387,9 @@ class Exchange(object): try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + needs_price = (ordertype != 'market' + or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) + rate = self.symbol_price_prec(pair, rate) if needs_price else None return self._api.create_order(pair, ordertype, side, amount, rate, params) @@ -395,12 +397,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} (total {rate * amount}).' f'Message: {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} (total {rate * amount}).' f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From 25822d17177e73f8b3346e14d9d21c038e93b0ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:18:30 +0200 Subject: [PATCH 753/928] Add empty options dict to all tests using create_order --- freqtrade/tests/exchange/test_exchange.py | 5 +++++ freqtrade/tests/exchange/test_kraken.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index bc1e7912c..9de605ffd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -552,6 +552,7 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -592,6 +593,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'market' time_in_force = 'gtc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -659,6 +661,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -719,6 +722,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -783,6 +787,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): 'foo': 'bar' } }) + api_mock.options = {} default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) diff --git a/freqtrade/tests/exchange/test_kraken.py b/freqtrade/tests/exchange/test_kraken.py index 8b81a08a9..8f476affb 100644 --- a/freqtrade/tests/exchange/test_kraken.py +++ b/freqtrade/tests/exchange/test_kraken.py @@ -11,6 +11,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'limit' time_in_force = 'ioc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -42,6 +43,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { From a8f3f2bc1a85d56b360dead4dfba36082afe62dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 14:23:23 +0200 Subject: [PATCH 754/928] Extend test to cover market orders with price too --- freqtrade/tests/exchange/test_exchange.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 9de605ffd..a4f1bca18 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -543,16 +543,17 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): ("buy"), ("sell") ]) -@pytest.mark.parametrize("ordertype,rate", [ - ("market", None), - ("limit", 200), - ("stop_loss_limit", 200) +@pytest.mark.parametrize("ordertype,rate,marketprice", [ + ("market", None, None), + ("market", 200, True), + ("limit", 200, None), + ("stop_loss_limit", 200, None) ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): +def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) - api_mock.options = {} + api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { From efbc7cccb18f8124c8ab9e4900c4fb6f80907ed8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 20:56:17 +0300 Subject: [PATCH 755/928] enable --dmmp for hyperopt --- freqtrade/optimize/hyperopt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 33f28bf6b..03bafd1ad 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -190,11 +190,21 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) + + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + max_open_trades = self.config['max_open_trades'] + else: + logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + max_open_trades = 0 + min_date, max_date = get_timeframe(processed) + results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, + 'max_open_trades': max_open_trades, 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, From 65f77306d3cb55d5e9365c606bfd88cf01318260 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 21:00:48 +0300 Subject: [PATCH 756/928] using logger.debug, info was too noisy --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03bafd1ad..6f113d0e0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -195,7 +195,7 @@ class Hyperopt(Backtesting): if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: - logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 min_date, max_date = get_timeframe(processed) From 4238ee090d4effc75ad14427f335cc254b5c5861 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:05:28 +0200 Subject: [PATCH 757/928] Cleanup some code after deepcode.ai suggestions --- freqtrade/data/dataprovider.py | 9 ++++++--- freqtrade/tests/strategy/test_strategy.py | 4 ++-- freqtrade/tests/test_talib.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 2852cbcb0..b87589df7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,7 +17,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -class DataProvider(object): +class DataProvider(): def __init__(self, config: dict, exchange: Exchange) -> None: self._config = config @@ -81,11 +81,14 @@ class DataProvider(object): # TODO: Implement me pass - def orderbook(self, pair: str, max: int): + def orderbook(self, pair: str, maximum: int): """ return latest orderbook data + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. """ - return self._exchange.get_order_book(pair, max) + return self._exchange.get_order_book(pair, maximum) @property def runmode(self) -> RunMode: diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 904f84946..9a2c950e5 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -69,8 +69,8 @@ def test_load_strategy(result): def test_load_strategy_base64(result): - with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: - encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") + with open("freqtrade/tests/strategy/test_strategy.py", "rb") as file: + encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) diff --git a/freqtrade/tests/test_talib.py b/freqtrade/tests/test_talib.py index 093c3023c..2c7f73eb1 100644 --- a/freqtrade/tests/test_talib.py +++ b/freqtrade/tests/test_talib.py @@ -13,4 +13,4 @@ def test_talib_bollingerbands_near_zero_values(): {'close': 0.00000014} ]) bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2) - assert (bollinger['upperband'][3] != bollinger['middleband'][3]) + assert bollinger['upperband'][3] != bollinger['middleband'][3] From dadf8adb3ef8897d56dd526b7dd1a2538fbddf35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:14:35 +0200 Subject: [PATCH 758/928] Replace filter usage --- freqtrade/rpc/telegram.py | 3 ++- freqtrade/tests/test_plotting.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3eb060074..fe4929780 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -217,7 +217,8 @@ class Telegram(RPC): "*Open Order:* `{open_order}`" if r['open_order'] else "" ] - messages.append("\n".join(filter(None, lines)).format(**r)) + # Filter empty lines using list-comprehension + messages.append("\n".join([l for l in lines if l]).format(**r)) for msg in messages: self._send_msg(msg, bot=bot) diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index cef229d19..565659d25 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -23,7 +23,7 @@ def fig_generating_mock(fig, *args, **kwargs): def find_trace_in_fig_data(data, search_string: str): - matches = filter(lambda x: x.name == search_string, data) + matches = (d for d in data if d.name == search_string) return next(matches) From e955b1ae09963d31219926c9334d2e4fabf7d863 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jul 2019 20:21:57 +0200 Subject: [PATCH 759/928] Use log_has_re instead of plain regex filters for log messages --- freqtrade/tests/test_freqtradebot.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 89d3bba5f..9a22a4f94 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2,7 +2,6 @@ # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments import logging -import re import time from copy import deepcopy from unittest.mock import MagicMock, PropertyMock @@ -1419,8 +1418,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No # Assert we call handle_trade() if trade is feasible for execution freqtrade.update_trade_state(trade) - regexp = re.compile('Found open order for.*') - assert filter(regexp.match, caplog.record_tuples) + assert log_has_re('Found open order for.*', caplog.record_tuples) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): @@ -1941,14 +1939,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - ) Trade.session.add(trade_buy) - regexp = re.compile( - 'Cannot query order for Trade(id=1, pair=ETH/BTC, amount=90.99181073, ' - 'open_rate=0.00001099, open_since=10 hours ago) due to Traceback (most ' - 'recent call last):\n.*' - ) freqtrade.check_handle_timedout() - assert filter(regexp.match, caplog.record_tuples) + assert log_has_re(r'Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, ' + r'open_rate=0.00001099, open_since=10 hours ago\) due to Traceback \(most ' + r'recent call last\):\n.*', caplog.record_tuples) def test_handle_timedout_limit_buy(mocker, default_conf) -> None: From 876cae280709935fa275ccf7a0a20e042596a2dd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jul 2019 22:20:26 +0300 Subject: [PATCH 760/928] docs adjusted to current default values; more detailed description of --eps and --dmmp added --- docs/hyperopt.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index a15fd575a..61e1f9fce 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -204,6 +204,27 @@ Legal values are: - `stoploss`: search for the best stoploss value - space-separated list of any of the above values for example `--spaces roi stoploss` +### Position stacking and disabling max market positions. + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previosly open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is same as setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. @@ -288,19 +309,11 @@ This would translate to the following ROI table: } ``` -### Validate backtest result +### Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. -To archive the same results (number of trades, ...) than during hyperopt, please use the command line flags `--disable-max-market-positions` and `--enable-position-stacking` for backtesting. -This configuration is the default in hyperopt for performance reasons. - -You can overwrite position stacking in the configuration by explicitly setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L191). - -Enabling the market-position for hyperopt is currently not possible. - -!!! Note - Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. ## Next Step From 9cae2900d4c301f51032f719c6f7ffee042bb1a6 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 01:44:25 +0300 Subject: [PATCH 761/928] get rid of patched_configuration_open() in tests --- freqtrade/tests/conftest.py | 7 ---- freqtrade/tests/optimize/test_backtesting.py | 14 +++---- freqtrade/tests/optimize/test_edge_cli.py | 8 ++-- freqtrade/tests/optimize/test_hyperopt.py | 7 ++-- freqtrade/tests/test_configuration.py | 43 +++++++++++--------- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index f57e58774..5862a2e89 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -43,13 +43,6 @@ def get_args(args): return Arguments(args, '').get_parsed_arg() -def patched_configuration_open(mocker, config): - file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( - read_data=json.dumps(config) - )) - return file_mock - - def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( 'freqtrade.configuration.configuration.Configuration._load_config_file', diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index ee4d23d35..9304871a8 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -22,7 +22,7 @@ from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_load_config_file) def trim_dictlist(dict_list, num): @@ -165,7 +165,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -203,7 +203,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -275,7 +275,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -292,7 +292,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -824,7 +824,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -874,7 +874,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): gen_strattable_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 582271af2..badaa5c45 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -8,11 +8,11 @@ from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_open) + patched_configuration_load_config_file) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -44,7 +44,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: - patched_configuration_open(mocker, edge_conf) + patched_configuration_load_config_file(mocker, edge_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -91,7 +91,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) - patched_configuration_open(mocker, edge_conf) + patched_configuration_load_config_file(mocker, edge_conf) args = [ '--config', 'config.json', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 7c00c3023..e8b4aa78d 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -16,8 +16,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file, - patched_configuration_open) + patched_configuration_load_config_file) @pytest.fixture(scope='function') @@ -45,7 +44,7 @@ def create_trials(mocker, hyperopt) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -81,7 +80,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index f6d2deeaa..4f3f4934d 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name - +import json import logging from argparse import Namespace from copy import deepcopy @@ -17,7 +17,8 @@ from freqtrade.configuration.json_schema import validate_config_schema from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, log_has_re, patched_configuration_open +from freqtrade.tests.conftest import (log_has, log_has_re, + patched_configuration_load_config_file) @pytest.fixture(scope="function") @@ -51,7 +52,9 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = patched_configuration_open(mocker, default_conf) + file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf) + )) configuration = Configuration(Namespace()) validated_conf = configuration._load_config_file('somefile') @@ -61,7 +64,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -106,7 +109,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -131,7 +134,7 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -143,7 +146,7 @@ def test_load_config(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--dynamic-whitelist', '10', @@ -165,7 +168,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False conf["db_url"] = "sqlite:///path/to/db.sqlite" - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -181,7 +184,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = "sqlite:///path/to/db.sqlite" - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -197,7 +200,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = False del conf["db_url"] - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -215,7 +218,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = DEFAULT_DB_PROD_URL - patched_configuration_open(mocker, conf) + patched_configuration_load_config_file(mocker, conf) arglist = [ '--strategy', 'TestStrategy', @@ -233,7 +236,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: 'strategy': 'CustomStrategy', 'strategy_path': '/tmp/strategies', }) - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) @@ -244,7 +247,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None: def test_show_info(default_conf, mocker, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--dynamic-whitelist', '10', @@ -268,7 +271,7 @@ def test_show_info(default_conf, mocker, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -307,7 +310,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', lambda c, x: x @@ -374,7 +377,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non """ Test setup_configuration() function """ - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--config', 'config.json', @@ -422,7 +425,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ 'hyperopt', @@ -497,7 +500,7 @@ def test_check_exchange(default_conf, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) @@ -550,7 +553,7 @@ def test_set_loggers() -> None: def test_set_logfile(default_conf, mocker): - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) arglist = [ '--logfile', 'test_file.log', @@ -567,7 +570,7 @@ def test_set_logfile(default_conf, mocker): def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True - patched_configuration_open(mocker, default_conf) + patched_configuration_load_config_file(mocker, default_conf) args = Arguments([], '').get_parsed_arg() configuration = Configuration(args) From 007703156bc2a7c025e665fcdfbe02bd4aa7fe56 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 01:55:35 +0300 Subject: [PATCH 762/928] do not export ARGS_* from configuration --- freqtrade/configuration/__init__.py | 6 ------ freqtrade/tests/test_arguments.py | 2 +- scripts/download_backtest_data.py | 2 +- scripts/plot_dataframe.py | 3 ++- scripts/plot_profit.py | 3 ++- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 2f74dc597..548b508a7 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,8 +1,2 @@ from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 - -from freqtrade.configuration.arguments import ( # noqa: F401 - ARGS_DOWNLOADER, - ARGS_PLOT_DATAFRAME, - ARGS_PLOT_PROFIT) - from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index ff1b0e6b0..9de960da3 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, TimeRange -from freqtrade.configuration import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME +from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME from freqtrade.configuration.arguments import check_int_positive diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 60a9e2cbc..9bf05941f 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -9,8 +9,8 @@ from pathlib import Path from typing import Any, Dict, List from freqtrade.configuration import Arguments, TimeRange -from freqtrade.configuration import ARGS_DOWNLOADER from freqtrade.configuration import Configuration +from freqtrade.configuration.arguments import ARGS_DOWNLOADER from freqtrade.configuration.check_exchange import check_exchange from freqtrade.data.history import download_pair_history from freqtrade.exchange import Exchange diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index f0bc30366..fc7e30173 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -18,7 +18,8 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.configuration import Arguments, ARGS_PLOT_DATAFRAME +from freqtrade.configuration import Arguments +from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph, diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 632b7879a..96536e1e5 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -8,7 +8,8 @@ import logging import sys from typing import Any, Dict, List -from freqtrade.configuration import Arguments, ARGS_PLOT_PROFIT +from freqtrade.configuration import Arguments +from freqtrade.configuration.arguments import ARGS_PLOT_PROFIT from freqtrade.optimize import setup_configuration from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file from freqtrade.state import RunMode From bbab5fef0c83fe44474db15992491d5931f6e7f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 06:27:43 +0200 Subject: [PATCH 763/928] Remove wrong import in legacy startup sript --- bin/freqtrade | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/freqtrade b/bin/freqtrade index b9e3a7008..25c94fe98 100755 --- a/bin/freqtrade +++ b/bin/freqtrade @@ -3,9 +3,7 @@ import sys import warnings -from freqtrade.main import main, set_loggers - -set_loggers() +from freqtrade.main import main warnings.warn( "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", From a3b7e1f774dad3f5f1448224288d1b294f91d32a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 06:59:20 +0200 Subject: [PATCH 764/928] Update wording in docs --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 61e1f9fce..f0b578814 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -216,7 +216,7 @@ some potential trades to be hidden (or masked) by previosly open trades. The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is same as setting `max_open_trades` to a very high +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high number). !!! Note From 107f00ff8f23f2721da9aa41d2a3fa9d71538832 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:17:15 +0200 Subject: [PATCH 765/928] Add hyperopt option to clean temporary pickle files --- freqtrade/configuration/arguments.py | 10 +++++++++- freqtrade/configuration/configuration.py | 2 ++ freqtrade/optimize/hyperopt.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..a3b2ca61f 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -223,6 +223,13 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), + "hyperopt_clean_state": Arg( + "--clean", + help="Remove temporary hyperopt files (should be used when the custom hyperopt file " + "was changed, or when changing the arguments for --min-trades or spaces.", + default=False, + action='store_true', + ), # List exchanges "print_one_column": Arg( '-1', '--one-column', @@ -309,7 +316,8 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", - "hyperopt_random_state", "hyperopt_min_trades"] + "hyperopt_random_state", "hyperopt_min_trades", + "hyperopt_clean_state"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4959c2adc..ab8d018d5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -284,6 +284,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') + self._args_to_config(config, argname='hyperopt_clean_state', + logstring='Removing hyperopt temp files') return config diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e3683a66c..577180e3b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -63,10 +63,22 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 + if self.config['hyperopt_clean_state']: + self.clean_hyperopt() # Previous evaluations self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + def clean_hyperopt(self): + """ + Remove hyperopt pickle files to restart hyperopt. + """ + for f in [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]: + p = Path(f) + if p.is_file(): + logger.info(f"Removing `{p}`.") + p.unlink() + def get_args(self, params): dimensions = self.hyperopt_space() # Ensure the number of dimensions match From b1b4048f97783a56ecb55de9b1483dec277b63af Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:27:34 +0200 Subject: [PATCH 766/928] Add test for hyperopt --- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_hyperopt.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 577180e3b..2bbfd3474 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -63,7 +63,7 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 - if self.config['hyperopt_clean_state']: + if self.config.get('hyperopt_clean_state'): self.clean_hyperopt() # Previous evaluations self.trials_file = TRIALSDATA_PICKLE diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e8b4aa78d..5eb1e02dc 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -11,7 +11,7 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE +from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE, TICKERDATA_PICKLE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -510,3 +510,20 @@ def test_generate_optimizer(mocker, default_conf) -> None: hyperopt = Hyperopt(default_conf) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected + + +def test_clean_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + hyp = Hyperopt(default_conf) + + hyp.clean_hyperopt() + assert unlinkmock.call_count == 2 + assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) From 2fedae60603073f7853106c3f8d048be09d3b9ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 20:28:55 +0200 Subject: [PATCH 767/928] Move unnecessary things out of generate_optimizer --- freqtrade/optimize/hyperopt.py | 31 ++++++++++++++--------- freqtrade/tests/optimize/test_hyperopt.py | 4 +++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2bbfd3474..c3550da52 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -69,6 +69,20 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + # Populate functions here (hasattr is slow so should not be run during "regular" operations) + if hasattr(self.custom_hyperopt, 'populate_buy_trend'): + self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore + + if hasattr(self.custom_hyperopt, 'populate_sell_trend'): + self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore + + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + self.max_open_trades = self.config['max_open_trades'] + else: + logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + self.max_open_trades = 0 + def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. @@ -184,39 +198,32 @@ class Hyperopt(Backtesting): return spaces def generate_optimizer(self, _params: Dict) -> Dict: + """ + Used Optimize function. Called once per epoch to optimize whatever is configured. + Keep this function as optimized as possible! + """ params = self.get_args(_params) if self.has_space('roi'): self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if self.has_space('sell'): self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - max_open_trades = self.config['max_open_trades'] - else: - logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 - min_date, max_date = get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'max_open_trades': max_open_trades, + 'max_open_trades': self.max_open_trades, 'position_stacking': self.config.get('position_stacking', False), 'start_date': min_date, 'end_date': max_date, diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 5eb1e02dc..39f83a0e0 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -371,6 +371,10 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 + assert hasattr(hyperopt, "advise_sell") + assert hasattr(hyperopt, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] def test_format_results(hyperopt): From 8096a1fb04f4993c49ed8d68a1fd57f670aae1ca Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 15 Jul 2019 22:17:57 +0300 Subject: [PATCH 768/928] minor: configuration cleanup --- freqtrade/configuration/configuration.py | 159 +++++++++++++---------- 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4959c2adc..d34434cb2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -40,49 +40,20 @@ class Configuration(object): return self.config - def load_config(self) -> Dict[str, Any]: + def _load_config_files(self) -> Dict[str, Any]: """ - Extract information for sys.argv and load the bot configuration - :return: Configuration dictionary + Iterate through the config files passed in the args, + loading all of them and merging their contents. """ config: Dict[str, Any] = {} - # Now expecting a list of config filenames here, not a string + + # We expect here a list of config filenames for path in self.args.config: logger.info('Using config: %s ...', path) # Merge config options, overwriting old values config = deep_merge_dicts(self._load_config_file(path), config) - if 'internals' not in config: - config['internals'] = {} - - logger.info('Validating configuration ...') - validate_config_schema(config) - self._validate_config_consistency(config) - - # Set strategy if not specified in config and or if it's non default - if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): - config.update({'strategy': self.args.strategy}) - - if self.args.strategy_path: - config.update({'strategy_path': self.args.strategy_path}) - - # Load Common configuration - config = self._load_common_config(config) - - # Load Optimize configurations - config = self._load_optimize_config(config) - - # Add plotting options if available - config = self._load_plot_config(config) - - # Set runmode - 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 - - config.update({'runmode': self.runmode}) - return config def _load_config_file(self, path: str) -> Dict[str, Any]: @@ -94,13 +65,50 @@ class Configuration(object): try: # Read config from stdin if requested in the options with open(path) if path != '-' else sys.stdin as file: - conf = json.load(file) + config = json.load(file) except FileNotFoundError: raise OperationalException( f'Config file "{path}" not found!' ' Please create a config file or check whether it exists.') - return conf + return config + + def _normalize_config(self, config: Dict[str, Any]) -> None: + """ + Make config more canonical -- i.e. for example add missing parts that we expect + to be normally in it... + """ + if 'internals' not in config: + config['internals'] = {} + + def load_config(self) -> Dict[str, Any]: + """ + Extract information for sys.argv and load the bot configuration + :return: Configuration dictionary + """ + # Load all configs + config: Dict[str, Any] = self._load_config_files() + + # Make resulting config more canonical + self._normalize_config(config) + + logger.info('Validating configuration ...') + validate_config_schema(config) + self._validate_config_consistency(config) + + # Load Common configuration + self._load_common_config(config) + + # Load Optimize configurations + self._load_optimize_config(config) + + # Add plotting options if available + self._load_plot_config(config) + + # Set runmode + self._load_runmode_config(config) + + return config def _load_logging_config(self, config: Dict[str, Any]) -> None: """ @@ -118,16 +126,22 @@ class Configuration(object): setup_logging(config) - def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_strategy_config(self, config: Dict[str, Any]) -> None: + + # Set strategy if not specified in config and or if it's non default + if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): + config.update({'strategy': self.args.strategy}) + + if self.args.strategy_path: + config.update({'strategy_path': self.args.strategy_path}) + + def _load_common_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load common configuration :return: configuration as dictionary """ self._load_logging_config(config) - - # Support for sd_notify - if 'sd_notify' in self.args and self.args.sd_notify: - config['internals'].update({'sd_notify': True}) + self._load_strategy_config(config) # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: @@ -157,6 +171,8 @@ class Configuration(object): config['db_url'] = constants.DEFAULT_DB_PROD_URL logger.info('Dry run is disabled') + logger.info(f'Using DB: "{config["db_url"]}"') + if config.get('forcebuy_enable', False): logger.warning('`forcebuy` RPC message enabled.') @@ -164,32 +180,13 @@ class Configuration(object): if config.get('max_open_trades') == -1: config['max_open_trades'] = float('inf') - logger.info(f'Using DB: "{config["db_url"]}"') + # Support for sd_notify + 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) - return config - - def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun: Optional[Callable] = None) -> None: - """ - :param config: Configuration dictionary - :param argname: Argumentname in self.args - will be copied to config dict. - :param logstring: Logging String - :param logfun: logfun is applied to the configuration entry before passing - that entry to the log string using .format(). - sample: logfun=len (prints the length of the found - configuration instead of the content) - """ - if argname in self.args and getattr(self.args, argname): - - config.update({argname: getattr(self.args, argname)}) - if logfun: - logger.info(logstring.format(logfun(config[argname]))) - else: - logger.info(logstring.format(config[argname])) - def _load_datadir_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load datadir configuration: @@ -201,12 +198,11 @@ class Configuration(object): config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) - def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_optimize_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load Optimize configuration :return: configuration as dictionary """ - # This will override the strategy configuration self._args_to_config(config, argname='ticker_interval', logstring='Parameter -i/--ticker-interval detected ... ' @@ -285,14 +281,11 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - return config - - def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + def _load_plot_config(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv Plotting configuration :return: configuration as dictionary """ - self._args_to_config(config, argname='pairs', logstring='Using pairs {}') @@ -306,7 +299,14 @@ class Configuration(object): logstring='Limiting plot to: {}') self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') - return config + + def _load_runmode_config(self, config: Dict[str, Any]) -> None: + 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}.") + + config.update({'runmode': self.runmode}) def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: """ @@ -336,3 +336,22 @@ class Configuration(object): 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) -> None: + """ + :param config: Configuration dictionary + :param argname: Argumentname in self.args - will be copied to config dict. + :param logstring: Logging String + :param logfun: logfun is applied to the configuration entry before passing + that entry to the log string using .format(). + sample: logfun=len (prints the length of the found + configuration instead of the content) + """ + if argname in self.args and getattr(self.args, argname): + + config.update({argname: getattr(self.args, argname)}) + if logfun: + logger.info(logstring.format(logfun(config[argname]))) + else: + logger.info(logstring.format(config[argname])) From 2a20423be6b10cf1e1c6d38c29ab48a919557007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:35:42 +0200 Subject: [PATCH 769/928] Allow loading custom hyperopt loss functions --- freqtrade/configuration/arguments.py | 10 +++++- freqtrade/configuration/configuration.py | 4 +++ freqtrade/optimize/default_hyperopt.py | 37 +++++++++++++++++++--- freqtrade/optimize/hyperopt.py | 32 ++++++++++--------- freqtrade/optimize/hyperopt_loss.py | 37 ++++++++++++++++++++++ user_data/hyperopts/sample_hyperopt.py | 39 +++++++++++++++++++++--- 6 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index a3b2ca61f..3e9629fbb 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -230,6 +230,14 @@ AVAILABLE_CLI_OPTIONS = { default=False, action='store_true', ), + "loss_function": Arg( + '--loss-function', + help='Define the loss-function to use for hyperopt.' + 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' + 'Default: `%(default)s`.', + choices=['legacy', 'custom'], + default='legacy', + ), # List exchanges "print_one_column": Arg( '-1', '--one-column', @@ -317,7 +325,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_clean_state"] + "hyperopt_clean_state", "loss_function"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ab8d018d5..a8a45653e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -284,9 +284,13 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') + self._args_to_config(config, argname='hyperopt_clean_state', logstring='Removing hyperopt temp files') + self._args_to_config(config, argname='loss_function', + logstring='Using loss function: {}') + return config def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 7f1cb2435..1c93fcc5d 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -1,16 +1,30 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from functools import reduce +from math import exp +from typing import Any, Callable, Dict, List +from datetime import datetime + import talib.abstract as ta from pandas import DataFrame -from typing import Dict, Any, Callable, List -from functools import reduce - from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class_name = 'DefaultHyperOpts' +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 class DefaultHyperOpts(IHyperOpt): @@ -19,6 +33,21 @@ class DefaultHyperOpts(IHyperOpt): You can override it with your own hyperopt """ + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result + @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c3550da52..ec1af345e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,7 +7,7 @@ This module contains the hyperopt logic import logging import os import sys -from math import exp + from operator import itemgetter from pathlib import Path from pprint import pprint @@ -22,6 +22,7 @@ from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy logger = logging.getLogger(__name__) @@ -69,6 +70,20 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + # Assign loss function + if self.config['loss_function'] == 'legacy': + self.calculate_loss = hyperopt_loss_legacy + elif (self.config['loss_function'] == 'custom' and + hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): + self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom + + # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. + # TODO: Maybe this should just stop hyperopt completely? + if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): + logger.warning("Could not load hyperopt configuration. " + "Falling back to legacy configuration.") + self.calculate_loss = hyperopt_loss_legacy + # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore @@ -160,16 +175,6 @@ class Hyperopt(Backtesting): print('.', end='') sys.stdout.flush() - def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / self.expected_max_profit) - duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) - result = trade_loss + profit_loss + duration_loss - return result - def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration @@ -231,9 +236,7 @@ class Hyperopt(Backtesting): ) result_explanation = self.format_results(results) - total_profit = results.profit_percent.sum() trade_count = len(results.index) - trade_duration = results.trade_duration.mean() # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -246,7 +249,8 @@ class Hyperopt(Backtesting): 'result': result_explanation, } - loss = self.calculate_loss(total_profit, trade_count, trade_duration) + loss = self.calculate_loss(results=results, trade_count=trade_count, + min_date=min_date.datetime, max_date=max_date.datetime) return { 'loss': loss, diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py new file mode 100644 index 000000000..d80febed5 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss.py @@ -0,0 +1,37 @@ +from math import exp +from pandas import DataFrame + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +def hyperopt_loss_legacy(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 7cb55378e..6428a1843 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -1,17 +1,31 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from functools import reduce +from math import exp +from typing import Any, Callable, Dict, List +from datetime import datetime + +import numpy as np# noqa F401 import talib.abstract as ta from pandas import DataFrame -from typing import Dict, Any, Callable, List -from functools import reduce - -import numpy from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class_name = 'SampleHyperOpts' +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 # This class is a sample. Feel free to customize it. @@ -28,6 +42,21 @@ class SampleHyperOpts(IHyperOpt): roi_space, generate_roi_table, stoploss_space """ + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result + @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) From 710443d2003f36c805dd65b56f6dd35dab5cd82d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:36:01 +0200 Subject: [PATCH 770/928] Add documentation for custom hyperopt --- docs/hyperopt.md | 54 +++++++++++++++++++++++++++++----- freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 401bfc3fe..cb344abf3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -31,6 +31,7 @@ Depending on the space you want to optimize, only some of the below are required * Optional but recommended * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +* Add custom loss-function `hyperopt_loss_custom` (Details below) ### 1. Install a Custom Hyperopt File @@ -150,6 +151,45 @@ The above setup expects to find ADX, RSI and Bollinger Bands in the populated in When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +### Using a custom loss function + +To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. +You then need to add the command line parameter `--loss custom` to your hyperopt call so this fuction is being used. + +A sample of this can be found below. + +``` python + @staticmethod + def hyperopt_loss_custom(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result +``` + +Currently, the arguments are: + +* `results`: DataFrame containing the result +* `trade_count`: Amount of trades (identical to `len(results)`) +* `min_date`: Start date of the hyperopting TimeFrame +* `min_date`: End date of the hyperopting TimeFrame + +This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. + +!!! Note + This function is called once per iteration - so please make sure to have this as optimized as possible to now slow hyperopt down unnecessarily. + +!!! Note + The last 2 arguments, `*args` and `**kwargs` are not strictly necessary but ensure compatibility for the future, so we can easily enable more parameters once we discover what could be usefull without breaking your custom hyperopt file. + ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. @@ -197,14 +237,14 @@ new buy strategy you have. Legal values are: -- `all`: optimize everything -- `buy`: just search for a new buy strategy -- `sell`: just search for a new sell strategy -- `roi`: just optimize the minimal profit table for your strategy -- `stoploss`: search for the best stoploss value -- space-separated list of any of the above values for example `--spaces roi stoploss` +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* space-separated list of any of the above values for example `--spaces roi stoploss` -### Position stacking and disabling max market positions. +### Position stacking and disabling max market positions In some situations, you may need to run Hyperopt (and Backtesting) with the `--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ec1af345e..8650c147f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,7 +75,7 @@ class Hyperopt(Backtesting): self.calculate_loss = hyperopt_loss_legacy elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): - self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom + self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. # TODO: Maybe this should just stop hyperopt completely? From e5170582de5b777fe0a6513723d4853ccb9141f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 21:54:41 +0200 Subject: [PATCH 771/928] Adapt tests to new loss-function method --- freqtrade/optimize/hyperopt.py | 6 +-- freqtrade/tests/optimize/test_hyperopt.py | 62 ++++++++++++++++------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8650c147f..cc9d9299c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,6 +18,7 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension +from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting @@ -71,18 +72,17 @@ class Hyperopt(Backtesting): self.trials: List = [] # Assign loss function - if self.config['loss_function'] == 'legacy': + if self.config.get('loss_function', 'legacy') == 'legacy': self.calculate_loss = hyperopt_loss_legacy elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - # TODO: Maybe this should just stop hyperopt completely? if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): logger.warning("Could not load hyperopt configuration. " "Falling back to legacy configuration.") - self.calculate_loss = hyperopt_loss_legacy + raise OperationalException("Could not load hyperopt loss function.") # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 39f83a0e0..889d8cb44 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -2,20 +2,24 @@ import os from datetime import datetime from unittest.mock import MagicMock -from filelock import Timeout import pandas as pd import pytest +from arrow import Arrow +from filelock import Timeout from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE, TICKERDATA_PICKLE from freqtrade.optimize import setup_configuration, start_hyperopt +from freqtrade.optimize.default_hyperopt import DefaultHyperOpts +from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, + Hyperopt) from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from freqtrade.strategy.interface import SellType +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) @@ -25,6 +29,21 @@ def hyperopt(default_conf, mocker): return Hyperopt(default_conf) +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + # Functions for recurrent object patching def create_trials(mocker, hyperopt) -> None: """ @@ -254,26 +273,33 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: - - correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) - under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) +def test_loss_calculation_prefer_correct_trade_count(hyperopt, hyperopt_results) -> None: + correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) + over = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades + 100) + under = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: - shorter = hyperopt.calculate_loss(1, 100, 20) - longer = hyperopt.calculate_loss(1, 100, 30) +def test_loss_calculation_prefer_shorter_trades(hyperopt, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb['trade_duration'][1] = 20 + + longer = hyperopt.calculate_loss(hyperopt_results, 100) + shorter = hyperopt.calculate_loss(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt) -> None: - correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) - under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) - assert over == correct +def test_loss_calculation_has_limited_profit(hyperopt, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) + over = hyperopt.calculate_loss(results_over, hyperopt.target_trades) + under = hyperopt.calculate_loss(results_under, hyperopt.target_trades) + assert over < correct assert under > correct @@ -472,7 +498,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: ) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', - MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) From 55e8092cbf8b5a025f90a08f786e854e321ea1eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 22:52:33 +0200 Subject: [PATCH 772/928] Add sharpe ratio as loss function --- docs/hyperopt.md | 14 ++++++++ freqtrade/configuration/arguments.py | 2 +- freqtrade/optimize/hyperopt.py | 4 ++- freqtrade/optimize/hyperopt_loss.py | 27 ++++++++++++++++ freqtrade/tests/optimize/test_hyperopt.py | 39 ++++++++++++++++------- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb344abf3..b341ec669 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,6 +151,20 @@ The above setup expects to find ADX, RSI and Bollinger Bands in the populated in When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a function, which get's closer to 0 for increasing values. + +By default, freqtrade uses a loss function we call `legacy` - since it's been with freqtrade since the beginning and optimizes for short trade duration. + +This can be configured by using the `--loss ` argument. +Possible options are: + +* `legacy` - The default option, optimizing for short trades and few losses. +* `sharpe` - using the sharpe-ratio to determine the quality of results +* `custom` - Custom defined loss-function [see next section](#using-a-custom-loss-function) + + ### Using a custom loss function To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e9629fbb..b6deb2451 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -235,7 +235,7 @@ AVAILABLE_CLI_OPTIONS = { help='Define the loss-function to use for hyperopt.' 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' 'Default: `%(default)s`.', - choices=['legacy', 'custom'], + choices=['legacy', 'sharpe', 'custom'], default='legacy', ), # List exchanges diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cc9d9299c..929debc86 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe logger = logging.getLogger(__name__) @@ -74,6 +74,8 @@ class Hyperopt(Backtesting): # Assign loss function if self.config.get('loss_function', 'legacy') == 'legacy': self.calculate_loss = hyperopt_loss_legacy + elif self.config.get('loss_function', 'sharpe') == 'sharpe': + self.calculate_loss = hyperopt_loss_sharpe elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py index d80febed5..20194ecb0 100644 --- a/freqtrade/optimize/hyperopt_loss.py +++ b/freqtrade/optimize/hyperopt_loss.py @@ -1,4 +1,7 @@ +from datetime import datetime from math import exp + +import numpy as np from pandas import DataFrame # Define some constants: @@ -35,3 +38,27 @@ def hyperopt_loss_legacy(results: DataFrame, trade_count: int, duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = trade_loss + profit_loss + duration_loss return result + + +def hyperopt_loss_sharpe(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + Using sharpe ratio calculation + """ + total_profit = results.profit_percent + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_yearly_return = total_profit.sum() / days_period + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + sharp_ratio = 1. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + + # Negate sharp-ratio so lower is better (??) + return -sharp_ratio diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 889d8cb44..88d7de39c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -273,32 +274,48 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt, hyperopt_results) -> None: - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades + 100) - under = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades - 100) +def test_loss_calculation_prefer_correct_trade_count(hyperopt_results) -> None: + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(hyperopt_results, 600 + 100) + under = hyperopt_loss_legacy(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb['trade_duration'][1] = 20 - longer = hyperopt.calculate_loss(hyperopt_results, 100) - shorter = hyperopt.calculate_loss(resultsb, 100) + longer = hyperopt_loss_legacy(hyperopt_results, 100) + shorter = hyperopt_loss_legacy(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(results_over, hyperopt.target_trades) - under = hyperopt.calculate_loss(results_under, hyperopt.target_trades) + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(results_over, 600) + under = hyperopt_loss_legacy(results_under, 600) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + correct = hyperopt_loss_sharpe(hyperopt_results, len( + hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hyperopt_loss_sharpe(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hyperopt_loss_sharpe(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) assert over < correct assert under > correct From 7be25313a5447b8000c51f07216bb2c29af44dc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 22:59:28 +0200 Subject: [PATCH 773/928] Add some mypy ignores --- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 929debc86..bf5b70e14 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -73,9 +73,9 @@ class Hyperopt(Backtesting): # Assign loss function if self.config.get('loss_function', 'legacy') == 'legacy': - self.calculate_loss = hyperopt_loss_legacy + self.calculate_loss = hyperopt_loss_legacy # type: ignore elif self.config.get('loss_function', 'sharpe') == 'sharpe': - self.calculate_loss = hyperopt_loss_sharpe + self.calculate_loss = hyperopt_loss_sharpe # type: ignore elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 88d7de39c..aae3405ad 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -302,7 +302,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: under = hyperopt_loss_legacy(results_under, 600) assert over < correct assert under > correct - + def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: results_over = hyperopt_results.copy() From 07a1c48e8c03a614afde98ba55df250d3a437c9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 23:14:07 +0200 Subject: [PATCH 774/928] Fix wrong intendation for custom-hyperopt check --- freqtrade/optimize/default_hyperopt.py | 17 ----------------- freqtrade/optimize/hyperopt.py | 10 +++++----- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 1c93fcc5d..aa3056fc0 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -1,9 +1,7 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from functools import reduce -from math import exp from typing import Any, Callable, Dict, List -from datetime import datetime import talib.abstract as ta from pandas import DataFrame @@ -33,21 +31,6 @@ class DefaultHyperOpts(IHyperOpt): You can override it with your own hyperopt """ - @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index bf5b70e14..e041554dc 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -80,11 +80,11 @@ class Hyperopt(Backtesting): hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore - # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): - logger.warning("Could not load hyperopt configuration. " - "Falling back to legacy configuration.") - raise OperationalException("Could not load hyperopt loss function.") + # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. + if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): + logger.warning("Could not load hyperopt configuration. " + "Falling back to legacy configuration.") + raise OperationalException("Could not load hyperopt loss function.") # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): From c4e55d78d5304ba79529373a4159be811667b5a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 05:41:39 +0200 Subject: [PATCH 775/928] reword documentation --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b341ec669..ab7217f2c 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -199,10 +199,10 @@ Currently, the arguments are: This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. !!! Note - This function is called once per iteration - so please make sure to have this as optimized as possible to now slow hyperopt down unnecessarily. + This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. !!! Note - The last 2 arguments, `*args` and `**kwargs` are not strictly necessary but ensure compatibility for the future, so we can easily enable more parameters once we discover what could be usefull without breaking your custom hyperopt file. + Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. ## Execute Hyperopt From 7d62bb8c530278f9b67c4b1e27ca26d00d340ef7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 05:50:27 +0200 Subject: [PATCH 776/928] Revert --clean argument to --continue --- freqtrade/configuration/arguments.py | 10 +++++----- freqtrade/configuration/configuration.py | 4 ++-- freqtrade/optimize/hyperopt.py | 8 ++++++-- freqtrade/tests/optimize/test_hyperopt.py | 21 +++++++++++++++++++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index b6deb2451..f72f785a0 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -223,10 +223,10 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), - "hyperopt_clean_state": Arg( - "--clean", - help="Remove temporary hyperopt files (should be used when the custom hyperopt file " - "was changed, or when changing the arguments for --min-trades or spaces.", + "hyperopt_continue": Arg( + "--continue", + help="Continue hyperopt from previous runs. " + "By default, temporary files will be removed and hyperopt will start from scratch.", default=False, action='store_true', ), @@ -325,7 +325,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_clean_state", "loss_function"] + "hyperopt_continue", "loss_function"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a8a45653e..cb8f77234 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,8 +285,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - self._args_to_config(config, argname='hyperopt_clean_state', - logstring='Removing hyperopt temp files') + self._args_to_config(config, argname='hyperopt_continue', + logstring='Hyperopt continue: {}') self._args_to_config(config, argname='loss_function', logstring='Using loss function: {}') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e041554dc..fece7d9d8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -65,8 +65,11 @@ class Hyperopt(Backtesting): # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 - if self.config.get('hyperopt_clean_state'): + if not self.config.get('hyperopt_continue'): self.clean_hyperopt() + else: + logger.info("Continuing on previous hyperopt results.") + # Previous evaluations self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] @@ -99,6 +102,7 @@ class Hyperopt(Backtesting): else: logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') self.max_open_trades = 0 + self.position_stacking = self.config.get('position_stacking', False), def clean_hyperopt(self): """ @@ -231,7 +235,7 @@ class Hyperopt(Backtesting): 'stake_amount': self.config['stake_amount'], 'processed': processed, 'max_open_trades': self.max_open_trades, - 'position_stacking': self.config.get('position_stacking', False), + 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, } diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index aae3405ad..794cba973 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -418,6 +418,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: assert hasattr(hyperopt, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") def test_format_results(hyperopt): @@ -569,8 +570,24 @@ def test_clean_hyperopt(mocker, default_conf, caplog): }) mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) - hyp = Hyperopt(default_conf) + Hyperopt(default_conf) - hyp.clean_hyperopt() assert unlinkmock.call_count == 2 assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) + + +def test_continue_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + 'hyperopt_continue': True + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + Hyperopt(default_conf) + + assert unlinkmock.call_count == 0 + assert log_has(f"Continuing on previous hyperopt results.", caplog.record_tuples) From d23179e25c5bb799f049a4602e99acc87993ba3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:27:23 +0200 Subject: [PATCH 777/928] Update hyperopt-loss to use resolver --- docs/hyperopt.md | 39 ++++++----- freqtrade/configuration/arguments.py | 15 ++--- freqtrade/constants.py | 1 + freqtrade/optimize/default_hyperopt.py | 14 ---- freqtrade/optimize/default_hyperopt_loss.py | 51 ++++++++++++++ freqtrade/optimize/hyperopt.py | 22 ++----- freqtrade/optimize/hyperopt_loss.py | 64 ------------------ freqtrade/optimize/hyperopt_loss_interface.py | 25 +++++++ freqtrade/resolvers/hyperopt_resolver.py | 66 ++++++++++++++++++- 9 files changed, 177 insertions(+), 120 deletions(-) create mode 100644 freqtrade/optimize/default_hyperopt_loss.py delete mode 100644 freqtrade/optimize/hyperopt_loss.py create mode 100644 freqtrade/optimize/hyperopt_loss_interface.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ab7217f2c..81ecc8ed3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -153,31 +153,40 @@ add it to the `populate_indicators()` method in `hyperopt.py`. ## Loss-functions -Each hyperparameter tuning requires a target. This is usually defined as a function, which get's closer to 0 for increasing values. +Each hyperparameter tuning requires a target. This is usually defined as a loss function, which get's closer to 0 for increasing values. -By default, freqtrade uses a loss function we call `legacy` - since it's been with freqtrade since the beginning and optimizes for short trade duration. - -This can be configured by using the `--loss ` argument. -Possible options are: - -* `legacy` - The default option, optimizing for short trades and few losses. -* `sharpe` - using the sharpe-ratio to determine the quality of results -* `custom` - Custom defined loss-function [see next section](#using-a-custom-loss-function) +FreqTrade uses a default loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. +A different version this can be used by using the `--hyperopt-loss ` argument. +This class should be in it's own file within the `user_data/hyperopts/` directory. ### Using a custom loss function -To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. -You then need to add the command line parameter `--loss custom` to your hyperopt call so this fuction is being used. +To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt class. +For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. -A sample of this can be found below. +A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. ``` python +TARGET_TRADES = 600 +EXPECTED_MAX_PROFIT = 3.0 +MAX_ACCEPTED_TRADE_DURATION = 300 + +class SuperDuperHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: """ - Objective function, returns smaller number for more optimal results + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index f72f785a0..1c1070507 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -230,13 +230,12 @@ AVAILABLE_CLI_OPTIONS = { default=False, action='store_true', ), - "loss_function": Arg( - '--loss-function', - help='Define the loss-function to use for hyperopt.' - 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' - 'Default: `%(default)s`.', - choices=['legacy', 'sharpe', 'custom'], - default='legacy', + "hyperopt_loss": Arg( + '--hyperopt-loss-class', + help='Specify hyperopt loss class name. Can generate completely different results, ' + 'since the target for optimization is different. (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, ), # List exchanges "print_one_column": Arg( @@ -325,7 +324,7 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_continue", "loss_function"] + "hyperopt_continue", "hyperopt_loss"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7a487fcc7..9b73adcfe 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -12,6 +12,7 @@ HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_HYPEROPT = 'DefaultHyperOpts' +DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index aa3056fc0..ad76ff786 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -10,20 +10,6 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - class DefaultHyperOpts(IHyperOpt): """ diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py new file mode 100644 index 000000000..32bcf4dba --- /dev/null +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -0,0 +1,51 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from math import exp + +from pandas import DataFrame + +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +class DefaultHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fece7d9d8..39a8f073a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -18,12 +18,10 @@ from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver logger = logging.getLogger(__name__) @@ -48,6 +46,9 @@ class Hyperopt(Backtesting): super().__init__(config) self.custom_hyperopt = HyperOptResolver(self.config).hyperopt + self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss + self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days self.target_trades = 600 @@ -74,21 +75,6 @@ class Hyperopt(Backtesting): self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] - # Assign loss function - if self.config.get('loss_function', 'legacy') == 'legacy': - self.calculate_loss = hyperopt_loss_legacy # type: ignore - elif self.config.get('loss_function', 'sharpe') == 'sharpe': - self.calculate_loss = hyperopt_loss_sharpe # type: ignore - elif (self.config['loss_function'] == 'custom' and - hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): - self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore - - # Implement fallback to avoid odd crashes when custom-hyperopt fails to load. - if not hasattr(self.custom_hyperopt, 'hyperopt_loss_custom'): - logger.warning("Could not load hyperopt configuration. " - "Falling back to legacy configuration.") - raise OperationalException("Could not load hyperopt loss function.") - # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py deleted file mode 100644 index 20194ecb0..000000000 --- a/freqtrade/optimize/hyperopt_loss.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime -from math import exp - -import numpy as np -from pandas import DataFrame - -# Define some constants: - -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - - -def hyperopt_loss_legacy(results: DataFrame, trade_count: int, - *args, **kwargs) -> float: - """ - Objective function, returns smaller number for better results - This is the legacy algorithm (used until now in freqtrade). - Weights are distributed as follows: - * 0.4 to trade duration - * 0.25: Avoiding trade loss - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - - -def hyperopt_loss_sharpe(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - Using sharpe ratio calculation - """ - total_profit = results.profit_percent - days_period = (max_date - min_date).days - - # adding slippage of 0.1% per trade - total_profit = total_profit - 0.0005 - expected_yearly_return = total_profit.sum() / days_period - - if (np.std(total_profit) != 0.): - sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) - else: - sharp_ratio = 1. - - # print(expected_yearly_return, np.std(total_profit), sharp_ratio) - - # Negate sharp-ratio so lower is better (??) - return -sharp_ratio diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py new file mode 100644 index 000000000..b11b6e661 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -0,0 +1,25 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from abc import ABC, abstractmethod +from datetime import datetime + +from pandas import DataFrame + + +class IHyperOptLoss(ABC): + """ + Interface for freqtrade hyperopts Loss functions. + Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) + """ + ticker_interval: str + + @staticmethod + @abstractmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + """ diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 30e097f3f..89f384254 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -8,8 +8,9 @@ from pathlib import Path from typing import Optional, Dict from freqtrade import OperationalException -from freqtrade.constants import DEFAULT_HYPEROPT +from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver logger = logging.getLogger(__name__) @@ -77,3 +78,66 @@ class HyperOptResolver(IResolver): f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " "or contains Python code errors." ) + + +class HyperOptLossResolver(IResolver): + """ + This class contains all the logic to load custom hyperopt loss class + """ + + __slots__ = ['hyperoptloss'] + + def __init__(self, config: Optional[Dict] = None) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + config = config or {} + + # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt + hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS + self.hyperoptloss = self._load_hyperoptloss( + hyperopt_name, extra_dir=config.get('hyperopt_path')) + + # Assign ticker_interval to be used in hyperopt + self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + + if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): + raise OperationalException( + f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") + + def _load_hyperoptloss( + self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss: + """ + Search and loads the specified hyperopt loss class. + :param hyper_loss_name: name of the module to import + :param extra_dir: additional directory to search for the given hyperopt + :return: HyperOptLoss instance or None + """ + current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() + + abs_paths = [ + current_path.parent.parent.joinpath('user_data/hyperopts'), + current_path, + ] + + if extra_dir: + # Add extra hyperopt directory on top of search paths + abs_paths.insert(0, Path(extra_dir)) + + for _path in abs_paths: + try: + (hyperoptloss, module_path) = self._search_object(directory=_path, + object_type=IHyperOptLoss, + object_name=hyper_loss_name) + if hyperoptloss: + logger.info( + f"Using resolved hyperopt {hyper_loss_name} from '{module_path}'...") + return hyperoptloss + except FileNotFoundError: + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + + raise OperationalException( + f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " + "or contains Python code errors." + ) From ec49b22af32d7ac6662f312dc332f6cbe2dff755 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:45:13 +0200 Subject: [PATCH 778/928] Add sharpe ratio hyperopt loss --- freqtrade/configuration/configuration.py | 2 +- freqtrade/optimize/default_hyperopt_loss.py | 5 ++- freqtrade/optimize/hyperopt_loss_sharpe.py | 42 ++++++++++++++++++++ freqtrade/tests/optimize/test_hyperopt.py | 44 +++++++++++---------- 4 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_sharpe.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cb8f77234..e0bcede7b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -288,7 +288,7 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_continue', logstring='Hyperopt continue: {}') - self._args_to_config(config, argname='loss_function', + self._args_to_config(config, argname='hyperopt_loss', logstring='Using loss function: {}') return config diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 32bcf4dba..60faa9f61 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -1,6 +1,7 @@ """ -IHyperOptLoss interface -This module defines the interface for the loss-function for hyperopts +DefaultHyperOptLoss +This module defines the default HyperoptLoss class which is being used for +Hyperoptimization. """ from math import exp diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py new file mode 100644 index 000000000..5a22a215f --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -0,0 +1,42 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from datetime import datetime + +from pandas import DataFrame +import numpy as np + +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss + + +class SharpeHyperOptLoss(IHyperOptLoss): + """ + Defines the a loss function for hyperopt. + This implementation uses the sharpe ratio calculation. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + Using sharpe ratio calculation + """ + total_profit = results.profit_percent + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_yearly_return = total_profit.sum() / days_period + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + # Define high (negative) sharpe ratio to be clear that this is NOT optimal. + sharp_ratio = 20. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + return -sharp_ratio diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 794cba973..408112594 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,8 +15,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_args, log_has, log_has_re, @@ -274,48 +273,53 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt_results) -> None: - correct = hyperopt_loss_legacy(hyperopt_results, 600) - over = hyperopt_loss_legacy(hyperopt_results, 600 + 100) - under = hyperopt_loss_legacy(hyperopt_results, 600 - 100) +def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb['trade_duration'][1] = 20 - longer = hyperopt_loss_legacy(hyperopt_results, 100) - shorter = hyperopt_loss_legacy(resultsb, 100) + hl = HyperOptLossResolver(default_conf).hyperoptloss + longer = hl.hyperopt_loss_function(hyperopt_results, 100) + shorter = hl.hyperopt_loss_function(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt_loss_legacy(hyperopt_results, 600) - over = hyperopt_loss_legacy(results_over, 600) - under = hyperopt_loss_legacy(results_under, 600) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(results_over, 600) + under = hl.hyperopt_loss_function(results_under, 600) assert over < correct assert under > correct -def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt_loss_sharpe(hyperopt_results, len( - hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hyperopt_loss_sharpe(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hyperopt_loss_sharpe(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) assert over < correct assert under > correct From 12679da5da530b6e7cfc099afe96e33773821792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:50:25 +0200 Subject: [PATCH 779/928] Add test for hyperoptresolver --- freqtrade/tests/optimize/test_hyperopt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 408112594..a588bab64 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -13,6 +13,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts +from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver @@ -185,6 +186,18 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: assert hasattr(x, "ticker_interval") +def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: + + hl = DefaultHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', + MagicMock(return_value=hl) + ) + x = HyperOptResolver(default_conf, ).hyperopt + assert hasattr(x, "populate_indicators") + assert hasattr(x, "ticker_interval") + + def test_start(mocker, default_conf, caplog) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) From 192d7ad735756f22e1c818f6d67fcab1b02b22d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 06:54:38 +0200 Subject: [PATCH 780/928] Add column description to hyperopt documentation --- docs/hyperopt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 81ecc8ed3..1fbd59dd4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -31,7 +31,6 @@ Depending on the space you want to optimize, only some of the below are required * Optional but recommended * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used -* Add custom loss-function `hyperopt_loss_custom` (Details below) ### 1. Install a Custom Hyperopt File @@ -200,7 +199,9 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Currently, the arguments are: -* `results`: DataFrame containing the result +* `results`: DataFrame containing the result + The following columns are available in results (corresponds to the output of backtesting): + `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From 1493771087974b0552f5de80a8db172e9c292e59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jul 2019 19:40:42 +0200 Subject: [PATCH 781/928] improve description --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1fbd59dd4..4108946c2 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -200,8 +200,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Currently, the arguments are: * `results`: DataFrame containing the result - The following columns are available in results (corresponds to the output of backtesting): - `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` + The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): + `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From be26ba8f8f7bca1696df1c30ec6e976352bf8969 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 16 Jul 2019 23:00:19 +0300 Subject: [PATCH 782/928] rename _load_*_config() methods to _process_*_options() --- freqtrade/configuration/configuration.py | 51 ++++++++++-------------- scripts/download_backtest_data.py | 4 +- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d34434cb2..213ecaeaf 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -94,23 +94,20 @@ class Configuration(object): logger.info('Validating configuration ...') validate_config_schema(config) + self._validate_config_consistency(config) - # Load Common configuration - self._load_common_config(config) + self._process_common_options(config) - # Load Optimize configurations - self._load_optimize_config(config) + self._process_optimize_options(config) - # Add plotting options if available - self._load_plot_config(config) + self._process_plot_options(config) - # Set runmode - self._load_runmode_config(config) + self._process_runmode(config) return config - def _load_logging_config(self, config: Dict[str, Any]) -> None: + def _process_logging_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load logging configuration: the -v/--verbose, --logfile options @@ -126,7 +123,7 @@ class Configuration(object): setup_logging(config) - def _load_strategy_config(self, config: Dict[str, Any]) -> None: + def _process_strategy_options(self, config: Dict[str, Any]) -> None: # Set strategy if not specified in config and or if it's non default if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): @@ -135,13 +132,10 @@ class Configuration(object): if self.args.strategy_path: config.update({'strategy_path': self.args.strategy_path}) - def _load_common_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load common configuration - :return: configuration as dictionary - """ - self._load_logging_config(config) - self._load_strategy_config(config) + def _process_common_options(self, config: Dict[str, Any]) -> None: + + self._process_logging_options(config) + self._process_strategy_options(config) # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: @@ -187,7 +181,7 @@ class Configuration(object): # Check if the exchange set by the user is supported check_exchange(config) - def _load_datadir_config(self, config: Dict[str, Any]) -> None: + def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load datadir configuration: the --datadir option @@ -198,11 +192,8 @@ class Configuration(object): config.update({'datadir': create_datadir(config, None)}) logger.info('Using data directory: %s ...', config.get('datadir')) - def _load_optimize_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load Optimize configuration - :return: configuration as dictionary - """ + def _process_optimize_options(self, config: Dict[str, Any]) -> None: + # This will override the strategy configuration self._args_to_config(config, argname='ticker_interval', logstring='Parameter -i/--ticker-interval detected ... ' @@ -232,7 +223,7 @@ class Configuration(object): self._args_to_config(config, argname='timerange', logstring='Parameter --timerange detected: {} ...') - self._load_datadir_config(config) + self._process_datadir_options(config) self._args_to_config(config, argname='refresh_pairs', logstring='Parameter -r/--refresh-pairs-cached detected ...') @@ -281,11 +272,8 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - def _load_plot_config(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv Plotting configuration - :return: configuration as dictionary - """ + def _process_plot_options(self, config: Dict[str, Any]) -> None: + self._args_to_config(config, argname='pairs', logstring='Using pairs {}') @@ -300,7 +288,8 @@ class Configuration(object): self._args_to_config(config, argname='trade_source', logstring='Using trades from: {}') - def _load_runmode_config(self, config: Dict[str, Any]) -> None: + def _process_runmode(self, config: Dict[str, Any]) -> None: + 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 @@ -314,11 +303,11 @@ class Configuration(object): :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 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 9bf05941f..2cee54757 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -74,7 +74,7 @@ else: } timeframes = args.timeframes or ['1m', '5m'] -configuration._load_logging_config(config) +configuration._process_logging_options(config) if args.config and args.exchange: logger.warning("The --exchange option is ignored, " @@ -83,7 +83,7 @@ if args.config and args.exchange: # Check if the exchange set by the user is supported check_exchange(config) -configuration._load_datadir_config(config) +configuration._process_datadir_options(config) dl_path = Path(config['datadir']) From 8ccfc0f31635da57045fcf02b012d17178d7de06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 06:24:40 +0200 Subject: [PATCH 783/928] Remove unused variables --- freqtrade/optimize/hyperopt.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 39a8f073a..3cc6efe12 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -49,23 +49,9 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - # set TARGET_TRADES to suit your number concurrent trades so its realistic - # to the number of days - self.target_trades = 600 self.total_tries = config.get('epochs', 0) self.current_best_loss = 100 - # max average trade duration in minutes - # if eval ends with higher value, we consider it a failed eval - self.max_accepted_trade_duration = 300 - - # This is assumed to be expected avg profit * expected trade count. - # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, - # self.expected_max_profit = 3.85 - # Check that the reported Σ% values do not exceed this! - # Note, this is ratio. 3.85 stated above means 385Σ%. - self.expected_max_profit = 3.0 - if not self.config.get('hyperopt_continue'): self.clean_hyperopt() else: From 0e500de1a0a7b71cb867cdd4514a9a8937660a72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 06:32:24 +0200 Subject: [PATCH 784/928] Add sample loss and improve docstring --- docs/hyperopt.md | 1 + freqtrade/optimize/default_hyperopt_loss.py | 3 ++- user_data/hyperopts/sample_hyperopt.py | 15 --------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 4108946c2..74d03b8ab 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -186,6 +186,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): Weights are distributed as follows: * 0.4 to trade duration * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 60faa9f61..58be44ab9 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -37,10 +37,11 @@ class DefaultHyperOptLoss(IHyperOptLoss): *args, **kwargs) -> float: """ Objective function, returns smaller number for better results - This is the legacy algorithm (used until now in freqtrade). + This is the Default algorithm Weights are distributed as follows: * 0.4 to trade duration * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ total_profit = results.profit_percent.sum() trade_duration = results.trade_duration.mean() diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 6428a1843..8650d0a98 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -42,21 +42,6 @@ class SampleHyperOpts(IHyperOpt): roi_space, generate_roi_table, stoploss_space """ - @staticmethod - def hyperopt_loss_custom(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() - - trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) - duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) - result = trade_loss + profit_loss + duration_loss - return result - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) From 639a4d5cf724101b918db0a098527efcaf55e88d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 07:14:27 +0200 Subject: [PATCH 785/928] Allow importing interface from hyperopt.py --- docs/hyperopt.md | 6 ++++-- freqtrade/optimize/default_hyperopt_loss.py | 2 +- freqtrade/optimize/hyperopt.py | 2 ++ freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- user_data/hyperopts/sample_hyperopt.py | 14 -------------- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 74d03b8ab..6be3d590f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -161,12 +161,14 @@ This class should be in it's own file within the `user_data/hyperopts/` director ### Using a custom loss function -To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt class. +To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. -A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. +A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) ``` python +from freqtrade.optimize.hyperopt import IHyperOptLoss + TARGET_TRADES = 600 EXPECTED_MAX_PROFIT = 3.0 MAX_ACCEPTED_TRADE_DURATION = 300 diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 58be44ab9..2879c4091 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -8,7 +8,7 @@ from math import exp from pandas import DataFrame -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt import IHyperOptLoss # Define some constants: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3cc6efe12..759ceffbe 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -21,6 +21,8 @@ from skopt.space import Dimension from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting +# Import IHyperOptLoss to allow users import from this file +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index 5a22a215f..be1a3d4b4 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -8,7 +8,7 @@ from datetime import datetime from pandas import DataFrame import numpy as np -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt import IHyperOptLoss class SharpeHyperOptLoss(IHyperOptLoss): diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 8650d0a98..a78906cf3 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -13,20 +13,6 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# set TARGET_TRADES to suit your number concurrent trades so its realistic -# to the number of days -TARGET_TRADES = 600 -# This is assumed to be expected avg profit * expected trade count. -# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. -EXPECTED_MAX_PROFIT = 3.0 - -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval -MAX_ACCEPTED_TRADE_DURATION = 300 - # This class is a sample. Feel free to customize it. class SampleHyperOpts(IHyperOpt): From b8704e12b7db7975d498a778bb519f4ad031ff29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:51:44 +0200 Subject: [PATCH 786/928] Add sample hyperopt loss file --- user_data/hyperopts/sample_hyperopt_loss.py | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 user_data/hyperopts/sample_hyperopt_loss.py diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/user_data/hyperopts/sample_hyperopt_loss.py new file mode 100644 index 000000000..d5102bef5 --- /dev/null +++ b/user_data/hyperopts/sample_hyperopt_loss.py @@ -0,0 +1,47 @@ +from math import exp +from datetime import datetime + +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + +# Define some constants: + +# set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# self.expected_max_profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# max average trade duration in minutes +# if eval ends with higher value, we consider it a failed eval +MAX_ACCEPTED_TRADE_DURATION = 300 + + +class SampleHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + This is intendet to give you some inspiration for your own loss function. + + The Function needs to return a number (float) - which becomes for better backtest results. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result From 49b95fe00865a6b80fb4edd974f468ed6fbbacda Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:52:17 +0200 Subject: [PATCH 787/928] use Path.cwd() instead of odd parent.parent.parent structure --- freqtrade/resolvers/hyperopt_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 89f384254..42e5ff31c 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -55,7 +55,7 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/hyperopts'), + Path.cwd().joinpath('user_data/hyperopts'), current_path, ] @@ -117,7 +117,7 @@ class HyperOptLossResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/hyperopts'), + Path.cwd().joinpath('user_data/hyperopts'), current_path, ] From 545ff6f9f104bec6b4c982c96a80a0c637d7d349 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 06:31:44 +0200 Subject: [PATCH 788/928] Fix typo --- user_data/hyperopts/sample_hyperopt_loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/user_data/hyperopts/sample_hyperopt_loss.py index d5102bef5..5a2fb72b6 100644 --- a/user_data/hyperopts/sample_hyperopt_loss.py +++ b/user_data/hyperopts/sample_hyperopt_loss.py @@ -25,7 +25,7 @@ MAX_ACCEPTED_TRADE_DURATION = 300 class SampleHyperOptLoss(IHyperOptLoss): """ Defines the default loss function for hyperopt - This is intendet to give you some inspiration for your own loss function. + This is intended to give you some inspiration for your own loss function. The Function needs to return a number (float) - which becomes for better backtest results. """ From 3e5abd18ca6e95fe387fc8f8368ce310800f6184 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 06:56:52 +0200 Subject: [PATCH 789/928] Randomize tests again this used to be enabled, but the plugin changed how it works > From v1.0.0 onwards, this plugin no longer randomises tests by default. --- .travis.yml | 3 +-- requirements-dev.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 455f4f037..33a45b280 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ addons: install: - cd build_helpers && ./install_ta-lib.sh; cd .. - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH -- pip install --upgrade pytest-random-order - pip install -r requirements-dev.txt - pip install -e . jobs: @@ -27,7 +26,7 @@ jobs: include: - stage: tests script: - - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ + - pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ # Allow failure for coveralls - coveralls || true name: pytest diff --git a/requirements-dev.txt b/requirements-dev.txt index c360bc85e..83fec3b8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,5 +9,6 @@ pytest==5.0.0 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 +pytest-random-order==1.0.4 coveralls==1.8.1 mypy==0.711 From 96564d0dad2504cf36683e558e3f7d29b2c5f016 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 10:45:47 +0300 Subject: [PATCH 790/928] remove deprecated --dynamic-whitelist option --- docs/bot-usage.md | 18 ++---------------- docs/configuration.md | 10 +++++----- docs/deprecated.md | 3 +++ docs/edge.md | 2 +- freqtrade/configuration/arguments.py | 12 +----------- freqtrade/configuration/configuration.py | 13 ------------- freqtrade/tests/test_arguments.py | 15 --------------- freqtrade/tests/test_configuration.py | 11 ----------- 8 files changed, 12 insertions(+), 72 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..48ae299cc 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -7,8 +7,8 @@ This page explains the different parameters of the bot and how to run it. ``` usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] - [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] [--sd-notify] + [-s NAME] [--strategy-path PATH] [--db-url PATH] + [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -34,9 +34,6 @@ optional arguments: Specify strategy class name (default: DefaultStrategy). --strategy-path PATH Specify additional strategy lookup path. - --dynamic-whitelist [INT] - Dynamically generate and update whitelist based on 24h - BaseVolume (default: 20). DEPRECATED. --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: None). @@ -119,17 +116,6 @@ freqtrade --strategy AwesomeStrategy --strategy-path /some/directory This is very simple. Copy paste your strategy file into the directory `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. -### How to use **--dynamic-whitelist**? - -!!! danger "DEPRECATED" - This command line option is deprecated. Please move your configurations using it -to the configurations that utilize the `StaticPairList` or `VolumePairList` methods set -in the configuration file -as outlined [here](configuration/#dynamic-pairlists) - -Description of this deprecated feature was moved to [here](deprecated.md). -Please no longer use it. - ### How to use **--db-url**? When you run the bot in Dry-run mode, per default no transactions are diff --git a/docs/configuration.md b/docs/configuration.md index f46ff14a7..84008be04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,8 +44,8 @@ Mandatory Parameters are marked as **Required**. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. -| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. -| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. +| `exchange.pair_whitelist` | [] | List of currency to use by the bot. +| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. @@ -53,8 +53,8 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists). -| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). +| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. +| `pairlist.config` | None | Additional configuration for dynamic pairlists. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. @@ -381,7 +381,7 @@ section of the configuration. * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * `VolumePairList` * Formerly available as `--dynamic-whitelist []`. This command line -option is deprecated and should no longer be used. +option was removed and can no longer be used. * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss diff --git a/docs/deprecated.md b/docs/deprecated.md index b63c8f823..c1582ce31 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,9 @@ and are no longer supported. Please avoid their usage in your configuration. ### The **--dynamic-whitelist** command line option +This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) +and in freqtrade 2019.7 (master branch). + Per default `--dynamic-whitelist` will retrieve the 20 currencies based on BaseVolume. This value can be changed when you run the script. diff --git a/docs/edge.md b/docs/edge.md index 93c15d330..9047758f4 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -3,7 +3,7 @@ This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. !!! Warning - Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option. + Edge positioning is not compatible with dynamic (volume-based) whitelist. !!! Note Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..e2e1c6d0d 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -73,16 +73,6 @@ AVAILABLE_CLI_OPTIONS = { help='Specify additional strategy lookup path.', metavar='PATH', ), - "dynamic_whitelist": Arg( - '--dynamic-whitelist', - help='Dynamically generate and update whitelist ' - 'based on 24h BaseVolume (default: %(const)s). ' - 'DEPRECATED.', - const=constants.DYNAMIC_WHITELIST, - type=int, - metavar='INT', - nargs='?', - ), "db_url": Arg( '--db-url', help=f'Override trades database URL, this is useful in custom deployments ' @@ -299,7 +289,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["dynamic_whitelist", "db_url", "sd_notify"] +ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "refresh_pairs"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 213ecaeaf..311323c36 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,19 +137,6 @@ class Configuration(object): self._process_logging_options(config) self._process_strategy_options(config) - # Add dynamic_whitelist if found - if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: - # Update to volumePairList (the previous default) - config['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': self.args.dynamic_whitelist} - } - logger.warning( - 'Parameter --dynamic-whitelist has been deprecated, ' - 'and will be completely replaced by the whitelist dict in the future. ' - 'For now: using dynamically generated whitelist based on VolumePairList. ' - '(not applicable with Backtesting and Hyperopt)' - ) - if ('db_url' in self.args and self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL): config.update({'db_url': self.args.db_url}) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 9de960da3..9f2d02f13 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -86,21 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None: Arguments(['--strategy-path'], '').get_parsed_arg() -def test_parse_args_dynamic_whitelist() -> None: - args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg() - assert args.dynamic_whitelist == 20 - - -def test_parse_args_dynamic_whitelist_10() -> None: - args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg() - assert args.dynamic_whitelist == 10 - - -def test_parse_args_dynamic_whitelist_invalid_values() -> None: - with pytest.raises(SystemExit, match=r'2'): - Arguments(['--dynamic-whitelist', 'abc'], '').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-') diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4f3f4934d..b8ea92e57 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -149,7 +149,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--strategy-path', '/some/path', '--db-url', 'sqlite:///someurl', @@ -158,8 +157,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList' - assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10 assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' @@ -250,7 +247,6 @@ def test_show_info(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--db-url', 'sqlite:///tmp/testdb', ] @@ -259,13 +255,6 @@ def test_show_info(default_conf, mocker, caplog) -> None: configuration = Configuration(args) configuration.get_config() - assert log_has( - 'Parameter --dynamic-whitelist has been deprecated, ' - 'and will be completely replaced by the whitelist dict in the future. ' - 'For now: using dynamically generated whitelist based on VolumePairList. ' - '(not applicable with Backtesting and Hyperopt)', - caplog.record_tuples - ) assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples) assert log_has('Dry run is enabled', caplog.record_tuples) From 50d2950e6bba7812bf51c15de1a56b3fe6c27da7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 12:12:34 +0300 Subject: [PATCH 791/928] add -V alias for --version --- docs/bot-usage.md | 2 +- freqtrade/configuration/arguments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..5426cd7c9 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -23,7 +23,7 @@ optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). --logfile FILE Log to the file specified - --version show program's version number and exit + -V, --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple --config options may be used. Can be set to '-' to diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e940ae2a..518a85dd8 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -44,7 +44,7 @@ AVAILABLE_CLI_OPTIONS = { metavar='FILE', ), "version": Arg( - '--version', + '-V', '--version', action='version', version=f'%(prog)s {__version__}', ), From 75a0998ed25ac2c30a109c0b9a43a59f41d42da4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 18:08:02 +0300 Subject: [PATCH 792/928] docs: restore link to #dynamic-pairlists. --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 84008be04..c2fe23160 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,8 +53,8 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. -| `pairlist.config` | None | Additional configuration for dynamic pairlists. +| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). +| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. From 43d5ec2d4a47f12456eba6bb3f527f46f46c19a9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 18:15:51 +0300 Subject: [PATCH 793/928] docs: removed historical excursus which can confuse new users --- docs/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c2fe23160..e70ce2bad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -380,8 +380,6 @@ section of the configuration. * `StaticPairList` * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * `VolumePairList` - * Formerly available as `--dynamic-whitelist []`. This command line -option was removed and can no longer be used. * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss From 8b4827ad85d695dd85a972f919d0505a82eb1587 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jul 2019 20:53:29 +0200 Subject: [PATCH 794/928] Convert create_datadir to Pathlib --- freqtrade/configuration/create_datadir.py | 12 +++++++----- freqtrade/resolvers/pairlist_resolver.py | 2 +- freqtrade/tests/test_configuration.py | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/create_datadir.py index ecb59bc84..acc3a29ca 100644 --- a/freqtrade/configuration/create_datadir.py +++ b/freqtrade/configuration/create_datadir.py @@ -1,18 +1,20 @@ import logging -import os from typing import Any, Dict, Optional +from pathlib import Path logger = logging.getLogger(__name__) def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: + + folder = Path(datadir) if datadir else Path('user_data/data') if not datadir: # set datadir exchange_name = config.get('exchange', {}).get('name').lower() - datadir = os.path.join('user_data', 'data', exchange_name) + folder = folder.joinpath(exchange_name) - if not os.path.isdir(datadir): - os.makedirs(datadir) + if not folder.is_dir(): + folder.mkdir(parents=True) logger.info(f'Created data directory: {datadir}') - return datadir + return str(folder) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index ac67ca496..651a7d33f 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -39,7 +39,7 @@ class PairListResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/pairlist'), + Path.cwd().joinpath('user_data/pairlist'), current_path, ] diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 4f3f4934d..0c5d99195 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -585,11 +585,11 @@ def test_validate_default_conf(default_conf) -> None: def test_create_datadir(mocker, default_conf, caplog) -> None: - mocker.patch('os.path.isdir', MagicMock(return_value=False)) - md = MagicMock() - mocker.patch('os.makedirs', md) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + create_datadir(default_conf, '/foo/bar') - assert md.call_args[0][0] == "/foo/bar" + assert md.call_args[1]['parents'] is True assert log_has('Created data directory: /foo/bar', caplog.record_tuples) From e01c0ab4d680d72a4141ce5a5e60462027a67f31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jul 2019 20:02:28 +0200 Subject: [PATCH 795/928] Improve doc wording --- docs/bot-usage.md | 40 +++++++++++++++++++--------- docs/hyperopt.md | 34 ++++++++++++----------- freqtrade/configuration/arguments.py | 3 ++- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 85692ae14..aef91189a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -213,19 +213,22 @@ to find optimal parameter values for your stategy. ``` usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades MAX_OPEN_TRADES] - [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] - [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] - [--print-all] [-j JOBS] + [--max_open_trades INT] + [--stake_amount STAKE_AMOUNT] [-r] + [--customhyperopt NAME] [--eps] [-e INT] + [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--dmmp] [--print-all] [-j JOBS] + [--random-state INT] [--min-trades INT] [--continue] + [--hyperopt-loss-class NAME] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (1m, 5m, 30m, 1h, 1d). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades MAX_OPEN_TRADES + --max_open_trades INT Specify max_open_trades to use. --stake_amount STAKE_AMOUNT Specify stake_amount. @@ -235,18 +238,18 @@ optional arguments: run your optimization commands with up-to-date data. --customhyperopt NAME Specify hyperopt class name (default: - DefaultHyperOpts). + `DefaultHyperOpts`). --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). + -e INT, --epochs INT Specify number of epochs (default: 100). + -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] + Specify which parameters to hyperopt. Space-separated + list. Default: `all`. --dmmp, --disable-max-market-positions Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). - -e INT, --epochs INT Specify number of epochs (default: 100). - -s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] - Specify which parameters to hyperopt. Space separate - list. Default: all. --print-all Print all results, not only the best ones. -j JOBS, --job-workers JOBS The number of concurrently running jobs for @@ -254,6 +257,19 @@ optional arguments: (default), all CPUs are used, for -2, all CPUs but one are used, etc. If 1 is given, no parallel computing code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --continue Continue hyperopt from previous runs. By default, + temporary files will be removed and hyperopt will + start from scratch. + --hyperopt-loss-class NAME + Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. (default: + `DefaultHyperOptLoss`). ``` ## Edge commands diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6be3d590f..5ff5310a3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -144,7 +144,7 @@ it will end with telling you which paramter combination produced the best profit The search for best parameters starts with a few random combinations and then uses a regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`. +that minimizes the value of the [loss function](#loss-functions). The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to @@ -152,17 +152,19 @@ add it to the `populate_indicators()` method in `hyperopt.py`. ## Loss-functions -Each hyperparameter tuning requires a target. This is usually defined as a loss function, which get's closer to 0 for increasing values. +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. -FreqTrade uses a default loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. +By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss ` argument. +A different version this can be used by using the `--hyperopt-loss-class ` argument. This class should be in it's own file within the `user_data/hyperopts/` directory. -### Using a custom loss function +Currently, the following loss-functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. -To use a custom loss Class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. -For the sample below, you then need to add the command line parameter `--hyperoptloss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. +### Creating and using a custom loss function + +To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. +For the sample below, you then need to add the command line parameter `--hyperopt-loss-class SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) @@ -209,7 +211,7 @@ Currently, the arguments are: * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame -This function needs to return a floating point number (`float`). The smaller that number, the better is the result. The parameters and balancing for this are up to you. +This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. !!! Note This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. @@ -220,7 +222,7 @@ This function needs to return a floating point number (`float`). The smaller tha ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins). +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. @@ -235,8 +237,11 @@ running at least several thousand evaluations. The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. +!!! Note + By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. + !!! Warning - When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. + When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. ### Execute Hyperopt with Different Ticker-Data Source @@ -246,12 +251,11 @@ use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset -Use the `--timerange` argument to change how much of the testset -you want to use. The last N ticks/timeframes will be used. -Example: +Use the `--timerange` argument to change how much of the testset you want to use. +To use one month of data, use the following parameter: ```bash -freqtrade hyperopt --timerange -200 +freqtrade hyperopt --timerange 20180401-20180501 ``` ### Running Hyperopt with Smaller Search Space @@ -319,7 +323,7 @@ method, what those values match to. So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: -``` +``` python (dataframe['rsi'] < 29.0) ``` diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 1c1070507..891bf7d93 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -232,7 +232,8 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_loss": Arg( '--hyperopt-loss-class', - help='Specify hyperopt loss class name. Can generate completely different results, ' + help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' + 'Different functions can generate completely different results, ' 'since the target for optimization is different. (default: `%(default)s`).', metavar='NAME', default=constants.DEFAULT_HYPEROPT_LOSS, From 7af24dc48670fe7c04448efd2d985f15c88c7cdd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 17:55:35 +0300 Subject: [PATCH 796/928] cleanup Arguments: name attrs and methods as non-public --- freqtrade/configuration/arguments.py | 29 ++++++++++++++-------------- freqtrade/tests/test_arguments.py | 14 ++++++-------- scripts/download_backtest_data.py | 8 ++++---- scripts/plot_dataframe.py | 5 ++--- scripts/plot_profit.py | 5 ++--- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 518a85dd8..6103d69ec 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -342,14 +342,15 @@ class Arguments(object): """ Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: Optional[List[str]], description: str) -> None: + def __init__(self, args: Optional[List[str]], description: str, + no_default_config: bool = False) -> None: self.args = args - self.parsed_arg: Optional[argparse.Namespace] = None + self._parsed_arg: Optional[argparse.Namespace] = None self.parser = argparse.ArgumentParser(description=description) + self._no_default_config = no_default_config def _load_args(self) -> None: - self.build_args(optionlist=ARGS_MAIN) - + self._build_args(optionlist=ARGS_MAIN) self._build_subcommands() def get_parsed_arg(self) -> argparse.Namespace: @@ -357,13 +358,13 @@ class Arguments(object): Return the list of arguments :return: List[str] List of arguments """ - if self.parsed_arg is None: + if self._parsed_arg is None: self._load_args() - self.parsed_arg = self.parse_args() + self._parsed_arg = self._parse_args() - return self.parsed_arg + return self._parsed_arg - def parse_args(self, no_default_config: bool = False) -> argparse.Namespace: + def _parse_args(self) -> argparse.Namespace: """ Parses given arguments and returns an argparse Namespace instance. """ @@ -371,12 +372,12 @@ class Arguments(object): # Workaround issue in argparse with action='append' and default value # (see https://bugs.python.org/issue16399) - if not no_default_config and parsed_arg.config is None: + if not self._no_default_config and parsed_arg.config is None: parsed_arg.config = [constants.DEFAULT_CONFIG] return parsed_arg - def build_args(self, optionlist, parser=None): + def _build_args(self, optionlist, parser=None): parser = parser or self.parser for val in optionlist: @@ -396,17 +397,17 @@ class Arguments(object): # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=start_backtesting) - self.build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) + self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=start_edge) - self.build_args(optionlist=ARGS_EDGE, parser=edge_cmd) + self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=start_hyperopt) - self.build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) + self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( @@ -414,7 +415,7 @@ class Arguments(object): help='Print available exchanges.' ) list_exchanges_cmd.set_defaults(func=start_list_exchanges) - self.build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) @staticmethod def parse_timerange(text: Optional[str]) -> TimeRange: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 9de960da3..2110913f7 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -51,8 +51,8 @@ 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() + arguments._build_args(ARGS_DOWNLOADER) + args = arguments._parse_args() assert args.pairs == 'ETH/BTC' @@ -180,9 +180,8 @@ def test_download_data_options() -> None: '--exchange', 'binance' ] arguments = Arguments(args, '') - arguments.build_args(ARGS_DOWNLOADER) - - args = arguments.parse_args() + arguments._build_args(ARGS_DOWNLOADER) + args = arguments._parse_args() assert args.pairs_file == 'file_with_pairs' assert args.datadir == 'datadir/directory' assert args.days == 30 @@ -197,8 +196,8 @@ def test_plot_dataframe_options() -> None: '-p', 'UNITTEST/BTC', ] arguments = Arguments(args, '') - arguments.build_args(ARGS_PLOT_DATAFRAME) - pargs = arguments.parse_args(True) + arguments._build_args(ARGS_PLOT_DATAFRAME) + pargs = arguments._parse_args() assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.plot_limit == 30 @@ -206,7 +205,6 @@ def test_plot_dataframe_options() -> None: def test_check_int_positive() -> None: - assert check_int_positive("3") == 3 assert check_int_positive("1") == 1 assert check_int_positive("100") == 100 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 2cee54757..ed96cec71 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -22,12 +22,12 @@ logger = logging.getLogger('download_backtest_data') DEFAULT_DL_PATH = 'user_data/data' -arguments = Arguments(sys.argv[1:], 'Download backtest data') -arguments.build_args(ARGS_DOWNLOADER) - # Do not read the default config if config is not specified # in the command line options explicitely -args = arguments.parse_args(no_default_config=True) +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' diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index fc7e30173..034a6f448 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -89,9 +89,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph dataframe') - arguments.build_args(optionlist=ARGS_PLOT_DATAFRAME) - - parsed_args = arguments.parse_args() + arguments._build_args(optionlist=ARGS_PLOT_DATAFRAME) + parsed_args = arguments._parse_args() # Load the configuration config = setup_configuration(parsed_args, RunMode.OTHER) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 96536e1e5..4290bca45 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -42,9 +42,8 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]: :return: args: Array with all arguments """ arguments = Arguments(args, 'Graph profits') - arguments.build_args(optionlist=ARGS_PLOT_PROFIT) - - parsed_args = arguments.parse_args() + arguments._build_args(optionlist=ARGS_PLOT_PROFIT) + parsed_args = arguments._parse_args() # Load the configuration config = setup_configuration(parsed_args, RunMode.OTHER) From 4a144d1c18367ec006dc8a1d35d54c98a028eebb Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 18 Jul 2019 22:43:36 +0300 Subject: [PATCH 797/928] docs: description for whitelist and blacklist fixed --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e70ce2bad..f8dbbbbbb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,8 +44,8 @@ Mandatory Parameters are marked as **Required**. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. -| `exchange.pair_whitelist` | [] | List of currency to use by the bot. -| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. +| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. From fa8904978b9b4bd27a4daba95d7136e7b0a31712 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jul 2019 06:31:49 +0200 Subject: [PATCH 798/928] Don't use --hyperopt-loss-class, but --hyperopt-loss instead --- docs/bot-usage.md | 4 ++-- docs/hyperopt.md | 8 ++++---- freqtrade/configuration/arguments.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index aef91189a..ff2e3279c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -219,7 +219,7 @@ usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] [--random-state INT] [--min-trades INT] [--continue] - [--hyperopt-loss-class NAME] + [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit @@ -264,7 +264,7 @@ optional arguments: --continue Continue hyperopt from previous runs. By default, temporary files will be removed and hyperopt will start from scratch. - --hyperopt-loss-class NAME + --hyperopt-loss NAME Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5ff5310a3..ef3d28188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -156,15 +156,15 @@ Each hyperparameter tuning requires a target. This is usually defined as a loss By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss-class ` argument. +A different version this can be used by using the `--hyperopt-loss ` argument. This class should be in it's own file within the `user_data/hyperopts/` directory. -Currently, the following loss-functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. +Currently, the following loss functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. ### Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. -For the sample below, you then need to add the command line parameter `--hyperopt-loss-class SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. +For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) @@ -252,7 +252,7 @@ use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset Use the `--timerange` argument to change how much of the testset you want to use. -To use one month of data, use the following parameter: +For example, to use one month of data, pass the following parameter to the hyperopt call: ```bash freqtrade hyperopt --timerange 20180401-20180501 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 891bf7d93..c9304c15a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -231,7 +231,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "hyperopt_loss": Arg( - '--hyperopt-loss-class', + '--hyperopt-loss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. (default: `%(default)s`).', From 89db5c6bab15352c334e79bf0cba2ab720cd9687 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 14:52:59 +0200 Subject: [PATCH 799/928] Extract strategy-specific stuff from search logic will allow extracting all to IResolver --- freqtrade/resolvers/strategy_resolver.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 4a5604db8..114115d8a 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -155,19 +155,23 @@ class StrategyResolver(IResolver): kwargs={'config': config}) if strategy: logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") - strategy._populate_fun_len = len( - getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - try: - return import_strategy(strategy, config=config) - except TypeError as e: - logger.warning( - f"Impossible to load strategy '{strategy_name}' from {module_path}. " - f"Error: {e}") + break + except FileNotFoundError: logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + if strategy: + strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + + try: + return import_strategy(strategy, config=config) + except TypeError as e: + logger.warning( + f"Impossible to load strategy '{strategy_name}'. " + f"Error: {e}") + raise OperationalException( f"Impossible to load Strategy '{strategy_name}'. This class does not exist " "or contains Python code errors." From b35efd96dc01eb0e1b436577c778b59688ed4d04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:03:12 +0200 Subject: [PATCH 800/928] Extract load_object from multiple paths to iResolver --- freqtrade/resolvers/hyperopt_resolver.py | 34 +++++++++--------------- freqtrade/resolvers/iresolver.py | 25 ++++++++++++++++- freqtrade/resolvers/pairlist_resolver.py | 18 +++++-------- freqtrade/resolvers/strategy_resolver.py | 17 +++--------- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 42e5ff31c..3f8d03fd2 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -63,17 +63,12 @@ class HyperOptResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - for _path in abs_paths: - try: - (hyperopt, module_path) = self._search_object(directory=_path, - object_type=IHyperOpt, - object_name=hyperopt_name) - if hyperopt: - logger.info(f"Using resolved hyperopt {hyperopt_name} from '{module_path}'...") - return hyperopt - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (hyperopt, module_path) = self._load_object(paths=abs_paths, + object_type=IHyperOpt, + object_name=hyperopt_name, + kwargs={}) + if hyperopt: + return hyperopt raise OperationalException( f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " "or contains Python code errors." @@ -125,17 +120,12 @@ class HyperOptLossResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - for _path in abs_paths: - try: - (hyperoptloss, module_path) = self._search_object(directory=_path, - object_type=IHyperOptLoss, - object_name=hyper_loss_name) - if hyperoptloss: - logger.info( - f"Using resolved hyperopt {hyper_loss_name} from '{module_path}'...") - return hyperoptloss - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + (hyperoptloss, module_path) = self._load_object(paths=abs_paths, + object_type=IHyperOptLoss, + object_name=hyper_loss_name, + kwargs={}) + if hyperoptloss: + return hyperoptloss raise OperationalException( f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 1065abba7..aafc4b0dd 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Any, Optional, Tuple, Type, Union +from typing import Any, List, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -64,3 +64,26 @@ class IResolver(object): if obj: return (obj(**kwargs), module_path) return (None, None) + + @staticmethod + def _load_object(paths: List[Path], object_type, object_name: str, + kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: + """ + Try to load object from path list. + """ + + for _path in paths: + try: + (module, module_path) = IResolver._search_object(directory=_path, + object_type=object_type, + object_name=object_name, + kwargs=kwargs) + if module: + logger.info( + f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " + f"from '{module_path}'...") + return (module, module_path) + except FileNotFoundError: + logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + + return (None, None) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 651a7d33f..a74ce1cf7 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -43,18 +43,12 @@ class PairListResolver(IResolver): current_path, ] - for _path in abs_paths: - try: - (pairlist, module_path) = self._search_object(directory=_path, - object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) - if pairlist: - logger.info(f"Using resolved pairlist {pairlist_name} from '{module_path}'...") - return pairlist - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (pairlist, module_path) = self._load_object(paths=abs_paths, + object_type=IPairList, + object_name=pairlist_name, + kwargs=kwargs) + if pairlist: + return pairlist raise OperationalException( f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " "or contains Python code errors." diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 114115d8a..ac053399e 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -147,19 +147,10 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - for _path in abs_paths: - try: - (strategy, module_path) = self._search_object(directory=_path, - object_type=IStrategy, - object_name=strategy_name, - kwargs={'config': config}) - if strategy: - logger.info(f"Using resolved strategy {strategy_name} from '{module_path}'...") - break - - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - + (strategy, module_path) = self._load_object(paths=abs_paths, + object_type=IStrategy, + object_name=strategy_name, + kwargs={'config': config}) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) From 88eb93da52421ffa74ab5c5a6ad16aceb90b72cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:16:19 +0200 Subject: [PATCH 801/928] Fix base64 strategy test to make sure strategy was loaded via base64 --- freqtrade/tests/strategy/test_strategy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 9a2c950e5..02a3769fc 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging +import tempfile import warnings from base64 import urlsafe_b64encode from os import path @@ -68,11 +69,15 @@ def test_load_strategy(result): assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) -def test_load_strategy_base64(result): - with open("freqtrade/tests/strategy/test_strategy.py", "rb") as file: +def test_load_strategy_base64(result, caplog): + with open("user_data/strategies/test_strategy.py", "rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + # Make sure strategy was loaded from base64 (using temp directory)!! + assert log_has_re(r"Using resolved strategy TestStrategy from '" + + tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", + caplog.record_tuples) def test_load_strategy_invalid_directory(result, caplog): From 08ca260e82473f3630ce4dacb8f49fbef839b275 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 15:25:48 +0200 Subject: [PATCH 802/928] Simplify return valuef rom _load_object --- freqtrade/resolvers/hyperopt_resolver.py | 12 ++++-------- freqtrade/resolvers/iresolver.py | 6 +++--- freqtrade/resolvers/pairlist_resolver.py | 6 ++---- freqtrade/resolvers/strategy_resolver.py | 6 ++---- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 3f8d03fd2..3f39bd41d 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -63,10 +63,8 @@ class HyperOptResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - (hyperopt, module_path) = self._load_object(paths=abs_paths, - object_type=IHyperOpt, - object_name=hyperopt_name, - kwargs={}) + hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, + object_name=hyperopt_name) if hyperopt: return hyperopt raise OperationalException( @@ -120,10 +118,8 @@ class HyperOptLossResolver(IResolver): # Add extra hyperopt directory on top of search paths abs_paths.insert(0, Path(extra_dir)) - (hyperoptloss, module_path) = self._load_object(paths=abs_paths, - object_type=IHyperOptLoss, - object_name=hyper_loss_name, - kwargs={}) + hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, + object_name=hyper_loss_name) if hyperoptloss: return hyperoptloss diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index aafc4b0dd..192a38beb 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -67,7 +67,7 @@ class IResolver(object): @staticmethod def _load_object(paths: List[Path], object_type, object_name: str, - kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: + kwargs: dict = {}) -> Union[Any, None]: """ Try to load object from path list. """ @@ -82,8 +82,8 @@ class IResolver(object): logger.info( f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " f"from '{module_path}'...") - return (module, module_path) + return module except FileNotFoundError: logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) - return (None, None) + return None diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index a74ce1cf7..3d95c0295 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -43,10 +43,8 @@ class PairListResolver(IResolver): current_path, ] - (pairlist, module_path) = self._load_object(paths=abs_paths, - object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) + pairlist = self._load_object(paths=abs_paths, object_type=IPairList, + object_name=pairlist_name, kwargs=kwargs) if pairlist: return pairlist raise OperationalException( diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index ac053399e..aa73327ff 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -147,10 +147,8 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - (strategy, module_path) = self._load_object(paths=abs_paths, - object_type=IStrategy, - object_name=strategy_name, - kwargs={'config': config}) + strategy = self._load_object(paths=abs_paths, object_type=IStrategy, + object_name=strategy_name, kwargs={'config': config}) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) From e6528be63d3204c1a7c16fe15cff517d820419bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 16:20:45 +0200 Subject: [PATCH 803/928] Config is not optional for hyperopt resolver --- freqtrade/resolvers/hyperopt_resolver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 3f39bd41d..944687ce7 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -23,12 +23,11 @@ class HyperOptResolver(IResolver): __slots__ = ['hyperopt'] - def __init__(self, config: Optional[Dict] = None) -> None: + def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter :param config: configuration dictionary or None """ - config = config or {} # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT From dcddfce5bcb297d197511659249fd8bff61056a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:21:50 +0200 Subject: [PATCH 804/928] Fix small mistakes --- freqtrade/resolvers/hyperopt_resolver.py | 2 +- freqtrade/resolvers/iresolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 944687ce7..74412e738 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -26,7 +26,7 @@ class HyperOptResolver(IResolver): def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter - :param config: configuration dictionary or None + :param config: configuration dictionary """ # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 192a38beb..9d0c97917 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -67,7 +67,7 @@ class IResolver(object): @staticmethod def _load_object(paths: List[Path], object_type, object_name: str, - kwargs: dict = {}) -> Union[Any, None]: + kwargs: dict = {}) -> Optional[Any]: """ Try to load object from path list. """ From 1fea6d394a6cee4227c678485da1776ea38858bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:31:50 +0200 Subject: [PATCH 805/928] Import DefaultStrategy from the correct file --- freqtrade/strategy/__init__.py | 2 -- freqtrade/tests/strategy/test_strategy.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index c62bfe5dc..19eacda42 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,8 +3,6 @@ import sys from copy import deepcopy from freqtrade.strategy.interface import IStrategy -# Import Default-Strategy to have hyperopt correctly resolve -from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 logger = logging.getLogger(__name__) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 02a3769fc..609cc58ff 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -45,7 +45,7 @@ def test_import_strategy(caplog): def test_search_strategy(): default_config = {} - default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() + default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve() s, _ = StrategyResolver._search_object( directory=default_location, From d2ad32eef8e450222d91ad680b5943c45002f314 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jul 2019 19:56:43 +0200 Subject: [PATCH 806/928] partially revert last commit(DefaultStrategy import IS needed). * don't run functions in travis in a way we don't support --- .travis.yml | 4 ++-- freqtrade/strategy/__init__.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 33a45b280..b44fef7a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,11 +32,11 @@ jobs: name: pytest - script: - cp config.json.example config.json - - python freqtrade --datadir freqtrade/tests/testdata backtesting + - freqtrade --datadir freqtrade/tests/testdata backtesting name: backtest - script: - cp config.json.example config.json - - python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 + - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - script: flake8 freqtrade scripts name: flake8 diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 19eacda42..c62bfe5dc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,6 +3,8 @@ import sys from copy import deepcopy from freqtrade.strategy.interface import IStrategy +# Import Default-Strategy to have hyperopt correctly resolve +from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 logger = logging.getLogger(__name__) From a213674a98bf9ae48ac4ef0c527e9f00340049d1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:05 +0000 Subject: [PATCH 807/928] Update pandas from 0.24.2 to 0.25.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52442fb19..fef8f06f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ -r requirements-common.txt numpy==1.16.4 -pandas==0.24.2 +pandas==0.25.0 scipy==1.3.0 From d6b6e59ab8d92a8f388ea556831e829821893a47 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:06 +0000 Subject: [PATCH 808/928] Update ccxt from 1.18.860 to 1.18.965 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 409c979b5..5f549e245 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.860 +ccxt==1.18.965 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 7add015a758a69a0434c36589c99c18dbe17b14a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:07 +0000 Subject: [PATCH 809/928] Update sqlalchemy from 1.3.5 to 1.3.6 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 5f549e245..4c8bab90d 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.965 -SQLAlchemy==1.3.5 +SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.2 cachetools==3.1.1 From 6c41ca4b8cb00984c111d6a987072d5cf3f21219 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:08 +0000 Subject: [PATCH 810/928] Update flask from 1.0.3 to 1.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4c8bab90d..3cf34ad61 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.2 sdnotify==0.3.2 # Api server -flask==1.0.3 +flask==1.1.1 From e0cd34c9e1d32e7017acb22fdac6bdfc763cb432 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:09 +0000 Subject: [PATCH 811/928] Update flake8 from 3.7.7 to 3.7.8 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 83fec3b8a..28fd19ce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -flake8==3.7.7 +flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==5.0.0 From bd0faaf70291b7f8e8b1fb4fe9d42197fd2102c3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:11 +0000 Subject: [PATCH 812/928] Update pytest from 5.0.0 to 5.0.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 28fd19ce4..e6f7bde1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==5.0.0 +pytest==5.0.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 76b9d781ee9b028b12e78409444b6f34ab0f1a41 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:12 +0000 Subject: [PATCH 813/928] Update mypy from 0.711 to 0.720 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6f7bde1c..946d63039 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,4 @@ pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-random-order==1.0.4 coveralls==1.8.1 -mypy==0.711 +mypy==0.720 From 44b2261c347eb4757e3a6976e340e7e23f8c34dd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 22 Jul 2019 15:23:13 +0000 Subject: [PATCH 814/928] Update plotly from 3.10.0 to 4.0.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index d4e4fc165..a6753fc3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.10.0 +plotly==4.0.0 From 04382d4b44c867034ff4c1dd21e49d432879baf3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 22 Jul 2019 19:37:34 +0300 Subject: [PATCH 815/928] add --hyperopt-path option --- docs/bot-usage.md | 5 ++++- freqtrade/configuration/arguments.py | 8 +++++++- freqtrade/configuration/configuration.py | 7 +++++-- freqtrade/resolvers/hyperopt_resolver.py | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c643ecace..8031a2072 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -201,7 +201,8 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--eps] [-e INT] + [--customhyperopt NAME] [--hyperopt-path PATH] + [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] [--random-state INT] [--min-trades INT] [--continue] @@ -225,6 +226,8 @@ optional arguments: --customhyperopt NAME Specify hyperopt class name (default: `DefaultHyperOpts`). + --hyperopt-path PATH Specify additional lookup path for Hyperopts and + Hyperopt Loss functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3bbdc4bc2..c70d1b4d2 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -168,6 +168,11 @@ AVAILABLE_CLI_OPTIONS = { metavar='NAME', default=constants.DEFAULT_HYPEROPT, ), + "hyperopt_path": Arg( + '--hyperopt-path', + help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + metavar='PATH', + ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', @@ -312,7 +317,8 @@ ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "live", "strategy_list", "export", "exportfilename"] -ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "position_stacking", "epochs", "spaces", +ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", + "position_stacking", "epochs", "spaces", "use_max_market_positions", "print_all", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_continue", "hyperopt_loss"] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 737eb0900..c97bd277a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -129,8 +129,8 @@ class Configuration(object): if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): config.update({'strategy': self.args.strategy}) - if self.args.strategy_path: - config.update({'strategy_path': self.args.strategy_path}) + self._args_to_config(config, argname='strategy_path', + logstring='Using additional Strategy lookup path: {}') def _process_common_options(self, config: Dict[str, Any]) -> None: @@ -239,6 +239,9 @@ class Configuration(object): self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt file {}') + self._args_to_config(config, argname='hyperopt_path', + logstring='Using additional Hyperopt lookup path: {}') + self._args_to_config(config, argname='epochs', logstring='Parameter --epochs detected ... ' 'Will run Hyperopt with for {} epochs ...' diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 74412e738..5027d7ddf 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -60,7 +60,7 @@ class HyperOptResolver(IResolver): if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, Path(extra_dir)) + abs_paths.insert(0, Path(extra_dir).resolve()) hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, object_name=hyperopt_name) @@ -115,7 +115,7 @@ class HyperOptLossResolver(IResolver): if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, Path(extra_dir)) + abs_paths.insert(0, Path(extra_dir).resolve()) hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, object_name=hyper_loss_name) From 482f5f7a26c95a2d215e0b98edb817b169317d96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Jul 2019 20:39:38 +0200 Subject: [PATCH 816/928] Update plotly dependencies (will break 3.x installations) --- freqtrade/plot/plotting.py | 36 ++++++++++++++++---------------- freqtrade/resolvers/iresolver.py | 2 +- freqtrade/tests/test_plotting.py | 6 +++--- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index dde6f78f0..5c9c6e457 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,9 +15,9 @@ logger = logging.getLogger(__name__) try: - from plotly import tools + from plotly.subplots import make_subplots from plotly.offline import plot - import plotly.graph_objs as go + import plotly.graph_objects as go except ImportError: logger.exception("Module plotly not found \n Please install using `pip install plotly`") exit(1) @@ -62,7 +62,7 @@ def init_plotscript(config): } -def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: +def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: """ Generator all the indicator selected by the user for a specific row :param fig: Plot figure to append to @@ -79,7 +79,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools mode='lines', name=indicator ) - fig.append_trace(scattergl, row, 1) + fig.add_trace(scattergl, row, 1) else: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not found ' @@ -90,7 +90,7 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> tools return fig -def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.make_subplots: +def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: """ Add profit-plot :param fig: Plot figure to append to @@ -105,12 +105,12 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> tools.ma y=data[column], name=name, ) - fig.append_trace(profit, row, 1) + fig.add_trace(profit, row, 1) return fig -def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: +def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ Add trades to "fig" """ @@ -145,8 +145,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> tools.make_subplots: color='red' ) ) - fig.append_trace(trade_buys, 1, 1) - fig.append_trace(trade_sells, 1, 1) + fig.add_trace(trade_buys, 1, 1) + fig.add_trace(trade_sells, 1, 1) else: logger.warning("No trades found.") return fig @@ -167,7 +167,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra """ # Define the graph - fig = tools.make_subplots( + fig = make_subplots( rows=3, cols=1, shared_xaxes=True, @@ -189,7 +189,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra close=data.close, name='Price' ) - fig.append_trace(candles, 1, 1) + fig.add_trace(candles, 1, 1) if 'buy' in data.columns: df_buy = data[data['buy'] == 1] @@ -206,7 +206,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra color='green', ) ) - fig.append_trace(buys, 1, 1) + fig.add_trace(buys, 1, 1) else: logger.warning("No buy-signals found.") @@ -225,7 +225,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra color='red', ) ) - fig.append_trace(sells, 1, 1) + fig.add_trace(sells, 1, 1) else: logger.warning("No sell-signals found.") @@ -244,8 +244,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fillcolor="rgba(0,176,246,0.2)", line={'color': 'rgba(255,255,255,0)'}, ) - fig.append_trace(bb_lower, 1, 1) - fig.append_trace(bb_upper, 1, 1) + fig.add_trace(bb_lower, 1, 1) + fig.add_trace(bb_upper, 1, 1) # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) @@ -258,7 +258,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra y=data['volume'], name='Volume' ) - fig.append_trace(volume, 2, 1) + fig.add_trace(volume, 2, 1) # Add indicators to seperate row fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) @@ -281,10 +281,10 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], name='Avg close price', ) - fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) fig['layout'].update(title="Profit plot") - fig.append_trace(avgclose, 1, 1) + fig.add_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') for pair in pairs: diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 9d0c97917..841c3cf43 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -84,6 +84,6 @@ class IResolver(object): f"from '{module_path}'...") return module except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.relative_to(Path.cwd())) + logger.warning('Path "%s" does not exist.', _path.resolve()) return None diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 0ebadf720..509bf7880 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -2,8 +2,8 @@ from copy import deepcopy from unittest.mock import MagicMock -import plotly.graph_objs as go -from plotly import tools +import plotly.graph_objects as go +from plotly.subplots import make_subplots from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history @@ -28,7 +28,7 @@ def find_trace_in_fig_data(data, search_string: str): def generage_empty_figure(): - return tools.make_subplots( + return make_subplots( rows=3, cols=1, shared_xaxes=True, From 60cf56e23582fd7a0e06db0ce080640670cef836 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Jul 2019 20:57:40 +0200 Subject: [PATCH 817/928] Adapt tests to always provide message for ccxt exceptions Changes introduced in https://github.com/ccxt/ccxt/pull/5470 --- freqtrade/tests/exchange/test_exchange.py | 54 +++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a4f1bca18..a5cdf0a82 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -33,13 +33,13 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -47,13 +47,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -256,13 +256,13 @@ def test__load_async_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples) + assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() @@ -305,7 +305,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError) + api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) default_conf['exchange']['markets_refresh_interval'] = 10 exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") @@ -634,25 +634,25 @@ def test_buy_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + 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, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) @@ -758,22 +758,22 @@ def test_sell_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + 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) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -846,7 +846,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): assert exchange.get_balance(currency='BTC') == 123.4 with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_balance(currency='BTC') @@ -919,7 +919,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): - api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() @@ -1101,7 +1101,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", (arrow.utcnow().timestamp - 2000) * 1000) @@ -1173,15 +1173,15 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): def test_get_order_book_exception(default_conf, mocker, exchange_name): api_mock = MagicMock() with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(TemporaryError): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) @@ -1294,7 +1294,7 @@ def test_cancel_order(default_conf, mocker, exchange_name): assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 with pytest.raises(InvalidOrderException): - api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == 1 @@ -1321,7 +1321,7 @@ def test_get_order(default_conf, mocker, exchange_name): assert exchange.get_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): - api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 @@ -1437,22 +1437,22 @@ def test_stoploss_limit_order(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) From 0c2c094db6233383079c54b42eae296daf4fb5d9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 23 Jul 2019 18:51:24 +0300 Subject: [PATCH 818/928] minor: add OnlyProfitHyperOptLoss --- freqtrade/optimize/default_hyperopt_loss.py | 11 +++--- .../optimize/hyperopt_loss_onlyprofit.py | 34 +++++++++++++++++++ freqtrade/optimize/hyperopt_loss_sharpe.py | 17 ++++++---- 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_onlyprofit.py diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 2879c4091..4ab9fbe44 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -3,27 +3,26 @@ DefaultHyperOptLoss This module defines the default HyperoptLoss class which is being used for Hyperoptimization. """ - from math import exp from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss -# Define some constants: -# set TARGET_TRADES to suit your number concurrent trades so its realistic +# Set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days TARGET_TRADES = 600 + # This is assumed to be expected avg profit * expected trade count. # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, -# self.expected_max_profit = 3.85 +# expected max profit = 3.85 # Check that the reported Σ% values do not exceed this! # Note, this is ratio. 3.85 stated above means 385Σ%. EXPECTED_MAX_PROFIT = 3.0 -# max average trade duration in minutes -# if eval ends with higher value, we consider it a failed eval +# Max average trade duration in minutes. +# If eval ends with higher value, we consider it a failed eval. MAX_ACCEPTED_TRADE_DURATION = 300 diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py new file mode 100644 index 000000000..4a1fabe35 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -0,0 +1,34 @@ +""" +OnlyProfitHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# expected max profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + + +class OnlyProfitHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation takes only profit into account. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results. + """ + total_profit = results.profit_percent.sum() + return max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index be1a3d4b4..f74b27744 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -1,8 +1,9 @@ """ -IHyperOptLoss interface -This module defines the interface for the loss-function for hyperopts -""" +SharpeHyperOptLoss +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" from datetime import datetime from pandas import DataFrame @@ -13,8 +14,9 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss class SharpeHyperOptLoss(IHyperOptLoss): """ - Defines the a loss function for hyperopt. - This implementation uses the sharpe ratio calculation. + Defines the loss function for hyperopt. + + This implementation uses the Sharpe Ratio calculation. """ @staticmethod @@ -22,8 +24,9 @@ class SharpeHyperOptLoss(IHyperOptLoss): min_date: datetime, max_date: datetime, *args, **kwargs) -> float: """ - Objective function, returns smaller number for more optimal results - Using sharpe ratio calculation + Objective function, returns smaller number for more optimal results. + + Uses Sharpe Ratio calculation. """ total_profit = results.profit_percent days_period = (max_date - min_date).days From cf6113068cdf1c9458a9491854df01e66306bf4b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Tue, 23 Jul 2019 22:52:42 -0500 Subject: [PATCH 819/928] Resolve issue #2042 Issue #2042 noted that the terminal output from `setup.sh` regarding an option use the bot was missing from the documentation. This has been added. --- docs/bot-usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c643ecace..b29b046e5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,6 +2,8 @@ This page explains the different parameters of the bot and how to run it. +Note: You can now use the bot by executing `source .env/bin/activate; freqtrade`. + ## Bot commands From a0cecc6c522700e0884474d1f456fa03a3a22e01 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jul 2019 06:29:50 +0200 Subject: [PATCH 820/928] Fix test after pandas 0.25.0 update --- freqtrade/data/btanalysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index dcd544d00..f2356c34b 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -67,7 +67,6 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int dates = pd.Series(pd.concat(dates).values, name='date') df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) - df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"}) df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() From e9b77298a7ccb289da21c7eff311d3119619e1e2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 08:17:41 +0300 Subject: [PATCH 821/928] max() removed --- freqtrade/optimize/hyperopt_loss_onlyprofit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py index 4a1fabe35..a1c50e727 100644 --- a/freqtrade/optimize/hyperopt_loss_onlyprofit.py +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -12,8 +12,12 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss # This is assumed to be expected avg profit * expected trade count. # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, # expected max profit = 3.85 -# Check that the reported Σ% values do not exceed this! -# Note, this is ratio. 3.85 stated above means 385Σ%. +# +# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%. +# +# In this implementation it's only used in calculation of the resulting value +# of the objective function as a normalization coefficient and does not +# represent any limit for profits as in the Freqtrade legacy default loss function. EXPECTED_MAX_PROFIT = 3.0 @@ -31,4 +35,4 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss): Objective function, returns smaller number for better results. """ total_profit = results.profit_percent.sum() - return max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + return 1 - total_profit / EXPECTED_MAX_PROFIT From 05be16e9e1dee7510bcf5ba1d5d9eadb53cca2a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 08:49:33 +0300 Subject: [PATCH 822/928] helpstring alignment fixed --- docs/bot-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8031a2072..4462b63c6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -201,7 +201,7 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [-r] - [--customhyperopt NAME] [--hyperopt-path PATH] + [--customhyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--dmmp] [--print-all] [-j JOBS] From f58668fd67f971cb6b1b0074363112becb7da1f7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 20:54:12 +0300 Subject: [PATCH 823/928] test added --- freqtrade/tests/optimize/test_hyperopt.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a588bab64..fc71200a5 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -337,6 +337,24 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N assert under > correct +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( From 05b185494650c1cab2a94f08b7fb7780de2b9db6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 19:56:59 +0200 Subject: [PATCH 824/928] Gracefully handle InvalidOrderException. --- freqtrade/freqtradebot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b103d73a7..d52165e0a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -524,7 +524,11 @@ class FreqtradeBot(object): if trade.open_order_id: # Update trade with order values logger.info('Found open order for %s', trade) - order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + try: + order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + except InvalidOrderException as exception: + logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) + return # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) @@ -749,7 +753,7 @@ class FreqtradeBot(object): if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException): + except (RequestException, DependencyException, InvalidOrderException): logger.info( 'Cannot query order for %s due to %s', trade, From e1b8ff798fb007ea75be49d69ce69cd2971d5d44 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:05:48 +0200 Subject: [PATCH 825/928] Add test to verify that get_order was successfully cought --- freqtrade/tests/test_freqtradebot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9a22a4f94..1a4c5159c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1460,6 +1460,22 @@ def test_update_trade_state_exception(mocker, default_conf, assert log_has('Could not update trade amount: ', caplog.record_tuples) +def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.get_order', + MagicMock(side_effect=InvalidOrderException)) + + trade = MagicMock() + trade.open_order_id = '123' + trade.open_fee = 0.001 + + # Test raise of OperationalException exception + grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) + freqtrade.update_trade_state(trade) + assert grm_mock.call_count == 0 + assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog.record_tuples) + + def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! From 4b8b2f7c5bcde38c234eca6f8d2b9f073ead3df8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:06:20 +0200 Subject: [PATCH 826/928] Use raise xxx from e to have a nicer traceback --- freqtrade/exchange/exchange.py | 62 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 65f013a03..a7c76e635 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -174,10 +174,10 @@ class Exchange(object): try: api = getattr(ccxt_module, name.lower())(ex_config) - except (KeyError, AttributeError): - raise OperationalException(f'Exchange {name} is not supported') + except (KeyError, AttributeError) as e: + raise OperationalException(f'Exchange {name} is not supported') from e except ccxt.BaseError as e: - raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") + raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e self.set_sandbox(api, exchange_config, name) @@ -398,17 +398,17 @@ class Exchange(object): 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'Message: {e}') + 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'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e def buy(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force) -> Dict: @@ -493,9 +493,9 @@ class Exchange(object): return balances except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_tickers(self) -> Dict: @@ -504,12 +504,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') + f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: @@ -528,9 +528,9 @@ class Exchange(object): return data except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') + f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e else: logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] @@ -651,12 +651,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical candlestick data.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. ' + f'Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e @retrier def cancel_order(self, order_id: str, pair: str) -> None: @@ -667,12 +667,12 @@ class Exchange(object): return self._api.cancel_order(order_id, pair) except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not cancel order. Message: {e}') + f'Could not cancel order. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order(self, order_id: str, pair: str) -> Dict: @@ -683,12 +683,12 @@ class Exchange(object): return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Tried to get an invalid order (id: {order_id}). Message: {e}') + f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order_book(self, pair: str, limit: int = 100) -> dict: @@ -704,12 +704,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order book due to {e.__class__.__name__}. Message: {e}') + f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: @@ -726,9 +726,9 @@ class Exchange(object): except ccxt.NetworkError as e: raise TemporaryError( - f'Could not get trades due to networking error. Message: {e}') + f'Could not get trades due to networking error. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, @@ -742,9 +742,9 @@ class Exchange(object): price=price, takerOrMaker=taker_or_maker)['rate'] except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') + f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e def is_exchange_bad(exchange: str) -> bool: From 10c69387fd7b86560082bcda2df92e48421a3304 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 25 Jul 2019 21:06:41 +0300 Subject: [PATCH 827/928] docs adjusted --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ef3d28188..2755cae2d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -156,10 +156,10 @@ Each hyperparameter tuning requires a target. This is usually defined as a loss By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. -A different version this can be used by using the `--hyperopt-loss ` argument. -This class should be in it's own file within the `user_data/hyperopts/` directory. +A different loss function can be specified by using the `--hyperopt-loss ` argument. +This class should be in its own file within the `user_data/hyperopts/` directory. -Currently, the following loss functions are builtin: `SharpeHyperOptLoss` and `DefaultHyperOptLoss`. +Currently, the following loss functions are builtin: `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration). ### Creating and using a custom loss function From 7ee971c3e3a0b6b3b2bfb87806949e455fbacc17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:35:20 +0200 Subject: [PATCH 828/928] Add simple method to add deprecations to cmd line options --- freqtrade/configuration/configuration.py | 10 +++++--- freqtrade/tests/test_configuration.py | 29 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index c97bd277a..17ad37d6a 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,6 +4,7 @@ This module contains the configuration class import json import logging import sys +import warnings from argparse import Namespace from typing import Any, Callable, Dict, Optional @@ -15,7 +16,6 @@ from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode - logger = logging.getLogger(__name__) @@ -187,7 +187,8 @@ class Configuration(object): 'Using ticker_interval: {} ...') self._args_to_config(config, argname='live', - logstring='Parameter -l/--live detected ...') + logstring='Parameter -l/--live detected ...', + deprecated_msg='--live will be removed soon.') self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') @@ -323,7 +324,8 @@ class Configuration(object): '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) -> None: + logstring: str, logfun: Optional[Callable] = None, + deprecated_msg: Optional[str] = None) -> None: """ :param config: Configuration dictionary :param argname: Argumentname in self.args - will be copied to config dict. @@ -340,3 +342,5 @@ class Configuration(object): logger.info(logstring.format(logfun(config[argname]))) else: logger.info(logstring.format(config[argname])) + if deprecated_msg: + warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d3567e943..56ff79625 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,10 +1,11 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name import json import logging +import warnings from argparse import Namespace from copy import deepcopy -from unittest.mock import MagicMock from pathlib import Path +from unittest.mock import MagicMock import pytest from jsonschema import Draft4Validator, ValidationError, validate @@ -62,6 +63,32 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: assert validated_conf.items() >= default_conf.items() +def test__args_to_config(caplog): + + arg_list = ['--strategy-path', 'TestTest'] + args = Arguments(arg_list, '').get_parsed_arg() + configuration = Configuration(args) + config = {} + with warnings.catch_warnings(record=True) as w: + # No warnings ... + configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") + assert len(w) == 0 + assert log_has("DeadBeef", caplog.record_tuples) + assert config['strategy_path'] == "TestTest" + + configuration = Configuration(args) + config = {} + with warnings.catch_warnings(record=True) as w: + # Deprecation warnings! + configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef", + deprecated_msg="Going away soon!") + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "DEPRECATED: Going away soon!" in str(w[-1].message) + assert log_has("DeadBeef", caplog.record_tuples) + assert config['strategy_path'] == "TestTest" + + def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 patched_configuration_load_config_file(mocker, default_conf) From 0c14176cd7a32abf76cf3448e2ec93c9e5f9f33a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:36:19 +0200 Subject: [PATCH 829/928] Deprecate --live --- docs/deprecated.md | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index c1582ce31..2bf655191 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,31 +4,16 @@ This page contains description of the command line arguments, configuration para and the bot features that were declared as DEPRECATED by the bot development team and are no longer supported. Please avoid their usage in your configuration. +### the `--live` command line option + +`--live` in the context of backtesting allows to download the latest tick data for backtesting. +Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated. + +This command was deprecated in `2019.6-dev` and will be removed after the next release. + +## Removed features + ### The **--dynamic-whitelist** command line option This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) and in freqtrade 2019.7 (master branch). - -Per default `--dynamic-whitelist` will retrieve the 20 currencies based -on BaseVolume. This value can be changed when you run the script. - -**By Default** -Get the 20 currencies based on BaseVolume. - -```bash -freqtrade --dynamic-whitelist -``` - -**Customize the number of currencies to retrieve** -Get the 30 currencies based on BaseVolume. - -```bash -freqtrade --dynamic-whitelist 30 -``` - -**Exception** -`--dynamic-whitelist` must be greater than 0. If you enter 0 or a -negative value (e.g -2), `--dynamic-whitelist` will use the default -value (20). - - From 3c3a902a69fbeecdf544a10b83babb8ce424230e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jul 2019 20:42:08 +0200 Subject: [PATCH 830/928] Move argument definitions to their own file --- freqtrade/configuration/arguments.py | 300 +----------------------- freqtrade/configuration/cli_options.py | 302 +++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 298 deletions(-) create mode 100644 freqtrade/configuration/cli_options.py diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c70d1b4d2..988f485dc 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -2,308 +2,12 @@ This module contains the argument manager class """ import argparse -import os import re from typing import List, NamedTuple, Optional import arrow -from freqtrade import __version__, constants - - -def check_int_positive(value: str) -> int: - try: - uint = int(value) - if uint <= 0: - raise ValueError - except ValueError: - raise argparse.ArgumentTypeError( - f"{value} is invalid for this parameter, should be a positive integer value" - ) - return uint - - -class Arg: - # Optional CLI arguments - def __init__(self, *args, **kwargs): - self.cli = args - self.kwargs = kwargs - - -# List of available command line options -AVAILABLE_CLI_OPTIONS = { - # Common options - "verbosity": Arg( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - default=0, - ), - "logfile": Arg( - '--logfile', - help='Log to the file specified.', - metavar='FILE', - ), - "version": Arg( - '-V', '--version', - action='version', - version=f'%(prog)s {__version__}', - ), - "config": Arg( - '-c', '--config', - help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' - f'Multiple --config options may be used. ' - f'Can be set to `-` to read config from stdin.', - action='append', - metavar='PATH', - ), - "datadir": Arg( - '-d', '--datadir', - help='Path to backtest data.', - metavar='PATH', - ), - # Main options - "strategy": Arg( - '-s', '--strategy', - help='Specify strategy class name (default: `%(default)s`).', - metavar='NAME', - default='DefaultStrategy', - ), - "strategy_path": Arg( - '--strategy-path', - help='Specify additional strategy lookup path.', - metavar='PATH', - ), - "db_url": Arg( - '--db-url', - help=f'Override trades database URL, this is useful in custom deployments ' - f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' - f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', - metavar='PATH', - ), - "sd_notify": Arg( - '--sd-notify', - help='Notify systemd service manager.', - action='store_true', - ), - # Optimize common - "ticker_interval": Arg( - '-i', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', - ), - "timerange": Arg( - '--timerange', - help='Specify what timerange of data to use.', - ), - "max_open_trades": Arg( - '--max_open_trades', - help='Specify max_open_trades to use.', - type=int, - metavar='INT', - ), - "stake_amount": Arg( - '--stake_amount', - help='Specify stake_amount.', - type=float, - ), - "refresh_pairs": Arg( - '-r', '--refresh-pairs-cached', - help='Refresh the pairs files in tests/testdata with the latest data from the ' - 'exchange. Use it if you want to run your optimization commands with ' - 'up-to-date data.', - action='store_true', - ), - # Backtesting - "position_stacking": Arg( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - default=False, - ), - "use_max_market_positions": Arg( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - default=True, - ), - "live": Arg( - '-l', '--live', - help='Use live data.', - action='store_true', - ), - "strategy_list": Arg( - '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' - 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with `--export trades`, ' - 'the strategy-name is injected into the filename ' - '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', - nargs='+', - ), - "export": Arg( - '--export', - help='Export backtest results, argument are: trades. ' - 'Example: `--export=trades`', - ), - "exportfilename": Arg( - '--export-filename', - help='Save backtest results to the file with this filename (default: `%(default)s`). ' - 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', - metavar='PATH', - default=os.path.join('user_data', 'backtest_data', - 'backtest-result.json'), - ), - # Edge - "stoploss_range": Arg( - '--stoplosses', - help='Defines a range of stoploss values against which edge will assess the strategy. ' - 'The format is "min,max,step" (without any space). ' - 'Example: `--stoplosses=-0.01,-0.1,-0.001`', - ), - # Hyperopt - "hyperopt": Arg( - '--customhyperopt', - help='Specify hyperopt class name (default: `%(default)s`).', - metavar='NAME', - default=constants.DEFAULT_HYPEROPT, - ), - "hyperopt_path": Arg( - '--hyperopt-path', - help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', - metavar='PATH', - ), - "epochs": Arg( - '-e', '--epochs', - help='Specify number of epochs (default: %(default)d).', - type=check_int_positive, - metavar='INT', - default=constants.HYPEROPT_EPOCH, - ), - "spaces": Arg( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space-separated list. ' - 'Default: `%(default)s`.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - nargs='+', - default='all', - ), - "print_all": Arg( - '--print-all', - help='Print all results, not only the best ones.', - action='store_true', - default=False, - ), - "hyperopt_jobs": Arg( - '-j', '--job-workers', - help='The number of concurrently running jobs for hyperoptimization ' - '(hyperopt worker processes). ' - 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' - 'If 1 is given, no parallel computing code is used at all.', - type=int, - metavar='JOBS', - default=-1, - ), - "hyperopt_random_state": Arg( - '--random-state', - help='Set random state to some positive integer for reproducible hyperopt results.', - type=check_int_positive, - metavar='INT', - ), - "hyperopt_min_trades": Arg( - '--min-trades', - help="Set minimal desired number of trades for evaluations in the hyperopt " - "optimization path (default: 1).", - type=check_int_positive, - metavar='INT', - default=1, - ), - "hyperopt_continue": Arg( - "--continue", - help="Continue hyperopt from previous runs. " - "By default, temporary files will be removed and hyperopt will start from scratch.", - default=False, - action='store_true', - ), - "hyperopt_loss": Arg( - '--hyperopt-loss', - help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' - 'Different functions can generate completely different results, ' - 'since the target for optimization is different. (default: `%(default)s`).', - metavar='NAME', - default=constants.DEFAULT_HYPEROPT_LOSS, - ), - # List exchanges - "print_one_column": Arg( - '-1', '--one-column', - help='Print exchanges in one column.', - action='store_true', - ), - # Script options - "pairs": Arg( - '-p', '--pairs', - help='Show profits for only these pairs. Pairs are comma-separated.', - ), - # Download data - "pairs_file": Arg( - '--pairs-file', - help='File containing a list of pairs to download.', - metavar='FILE', - ), - "days": Arg( - '--days', - help='Download data for given number of days.', - type=check_int_positive, - metavar='INT', - ), - "exchange": Arg( - '--exchange', - help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' - f'Only valid if no config is provided.', - ), - "timeframes": Arg( - '-t', '--timeframes', - help=f'Specify which tickers to download. Space-separated list. ' - f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', - choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w'], - nargs='+', - ), - "erase": Arg( - '--erase', - help='Clean all existing data for the selected exchange/pairs/timeframes.', - action='store_true', - ), - # Plot dataframe - "indicators1": Arg( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. ' - 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', - default='sma,ema3,ema5', - ), - "indicators2": Arg( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. ' - 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', - default='macd,macdsignal', - ), - "plot_limit": Arg( - '--plot-limit', - help='Specify tick limit for plotting. Notice: too high values cause huge files. ' - 'Default: %(default)s.', - type=check_int_positive, - metavar='INT', - default=750, - ), - "trade_source": Arg( - '--trade-source', - help='Specify the source for trades (Can be DB or file (backtest file)) ' - 'Default: %(default)s', - choices=["DB", "file"], - default="file", - ), -} - +from freqtrade.arguments.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade import constants ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py new file mode 100644 index 000000000..04fde2051 --- /dev/null +++ b/freqtrade/configuration/cli_options.py @@ -0,0 +1,302 @@ +""" +Definition of cli arguments used in arguments.py +""" +import argparse +import os + +from freqtrade import __version__, constants + + +def check_int_positive(value: str) -> int: + try: + uint = int(value) + if uint <= 0: + raise ValueError + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a positive integer value" + ) + return uint + + +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line options +AVAILABLE_CLI_OPTIONS = { + # Common options + "verbosity": Arg( + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + default=0, + ), + "logfile": Arg( + '--logfile', + help='Log to the file specified.', + metavar='FILE', + ), + "version": Arg( + '-V', '--version', + action='version', + version=f'%(prog)s {__version__}', + ), + "config": Arg( + '-c', '--config', + help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' + f'Multiple --config options may be used. ' + f'Can be set to `-` to read config from stdin.', + action='append', + metavar='PATH', + ), + "datadir": Arg( + '-d', '--datadir', + help='Path to backtest data.', + metavar='PATH', + ), + # Main options + "strategy": Arg( + '-s', '--strategy', + help='Specify strategy class name (default: `%(default)s`).', + metavar='NAME', + default='DefaultStrategy', + ), + "strategy_path": Arg( + '--strategy-path', + help='Specify additional strategy lookup path.', + metavar='PATH', + ), + "db_url": Arg( + '--db-url', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', + metavar='PATH', + ), + "sd_notify": Arg( + '--sd-notify', + help='Notify systemd service manager.', + action='store_true', + ), + # Optimize common + "ticker_interval": Arg( + '-i', '--ticker-interval', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + ), + "timerange": Arg( + '--timerange', + help='Specify what timerange of data to use.', + ), + "max_open_trades": Arg( + '--max_open_trades', + help='Specify max_open_trades to use.', + type=int, + metavar='INT', + ), + "stake_amount": Arg( + '--stake_amount', + help='Specify stake_amount.', + type=float, + ), + "refresh_pairs": Arg( + '-r', '--refresh-pairs-cached', + help='Refresh the pairs files in tests/testdata with the latest data from the ' + 'exchange. Use it if you want to run your optimization commands with ' + 'up-to-date data.', + action='store_true', + ), + # Backtesting + "position_stacking": Arg( + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking).', + action='store_true', + default=False, + ), + "use_max_market_positions": Arg( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number).', + action='store_false', + default=True, + ), + "live": Arg( + '-l', '--live', + help='Use live data.', + action='store_true', + ), + "strategy_list": Arg( + '--strategy-list', + help='Provide a comma-separated list of strategies to backtest. ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line. When using this together with `--export trades`, ' + 'the strategy-name is injected into the filename ' + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', + nargs='+', + ), + "export": Arg( + '--export', + help='Export backtest results, argument are: trades. ' + 'Example: `--export=trades`', + ), + "exportfilename": Arg( + '--export-filename', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + metavar='PATH', + default=os.path.join('user_data', 'backtest_data', + 'backtest-result.json'), + ), + # Edge + "stoploss_range": Arg( + '--stoplosses', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', + ), + # Hyperopt + "hyperopt": Arg( + '--customhyperopt', + help='Specify hyperopt class name (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT, + ), + "hyperopt_path": Arg( + '--hyperopt-path', + help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + metavar='PATH', + ), + "epochs": Arg( + '-e', '--epochs', + help='Specify number of epochs (default: %(default)d).', + type=check_int_positive, + metavar='INT', + default=constants.HYPEROPT_EPOCH, + ), + "spaces": Arg( + '-s', '--spaces', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + nargs='+', + default='all', + ), + "print_all": Arg( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + default=False, + ), + "hyperopt_jobs": Arg( + '-j', '--job-workers', + help='The number of concurrently running jobs for hyperoptimization ' + '(hyperopt worker processes). ' + 'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. ' + 'If 1 is given, no parallel computing code is used at all.', + type=int, + metavar='JOBS', + default=-1, + ), + "hyperopt_random_state": Arg( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + type=check_int_positive, + metavar='INT', + ), + "hyperopt_min_trades": Arg( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + type=check_int_positive, + metavar='INT', + default=1, + ), + "hyperopt_continue": Arg( + "--continue", + help="Continue hyperopt from previous runs. " + "By default, temporary files will be removed and hyperopt will start from scratch.", + default=False, + action='store_true', + ), + "hyperopt_loss": Arg( + '--hyperopt-loss', + help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' + 'Different functions can generate completely different results, ' + 'since the target for optimization is different. (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, + ), + # List exchanges + "print_one_column": Arg( + '-1', '--one-column', + help='Print exchanges in one column.', + action='store_true', + ), + # Script options + "pairs": Arg( + '-p', '--pairs', + help='Show profits for only these pairs. Pairs are comma-separated.', + ), + # Download data + "pairs_file": Arg( + '--pairs-file', + help='File containing a list of pairs to download.', + metavar='FILE', + ), + "days": Arg( + '--days', + help='Download data for given number of days.', + type=check_int_positive, + metavar='INT', + ), + "exchange": Arg( + '--exchange', + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' + f'Only valid if no config is provided.', + ), + "timeframes": Arg( + '-t', '--timeframes', + help=f'Specify which tickers to download. Space-separated list. ' + f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', + choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', + '6h', '8h', '12h', '1d', '3d', '1w'], + nargs='+', + ), + "erase": Arg( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes.', + action='store_true', + ), + # Plot dataframe + "indicators1": Arg( + '--indicators1', + help='Set indicators from your strategy you want in the first row of the graph. ' + 'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', + default='sma,ema3,ema5', + ), + "indicators2": Arg( + '--indicators2', + help='Set indicators from your strategy you want in the third row of the graph. ' + 'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', + default='macd,macdsignal', + ), + "plot_limit": Arg( + '--plot-limit', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', + type=check_int_positive, + metavar='INT', + default=750, + ), + "trade_source": Arg( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', + choices=["DB", "file"], + default="file", + ), +} From bf1c197a37ac73a4ba96797f8c3d5f7b01c648a0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:21:31 +0300 Subject: [PATCH 831/928] import errors fixed --- freqtrade/configuration/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 988f485dc..4f0c3d31b 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -6,7 +6,7 @@ import re from typing import List, NamedTuple, Optional import arrow -from freqtrade.arguments.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade import constants ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 3de71a621..bf744f72b 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -5,7 +5,7 @@ import pytest from freqtrade.configuration import Arguments, TimeRange from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME -from freqtrade.configuration.arguments import check_int_positive +from freqtrade.configuration.cli_options import check_int_positive # Parse common command-line-arguments. Used for all tools From 327e505273830f80280867bf6a9d572310b3e7b4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:57:51 +0300 Subject: [PATCH 832/928] non-working link to misc.py removed --- docs/bot-usage.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4462b63c6..9af3692f8 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -294,11 +294,6 @@ optional arguments: To understand edge and how to read the results, please read the [edge documentation](edge.md). -## A parameter missing in the configuration? - -All parameters for `main.py`, `backtesting`, `hyperopt` are referenced -in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) - ## Next step The optimal strategy of the bot will change with time depending of the market trends. The next step is to From 1ac4a7e11609e0ece9640dd60013730169751310 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:59:10 +0300 Subject: [PATCH 833/928] rendering for a Note fixed --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 657273e2f..74b3a3202 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -215,7 +215,7 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co as the watchdog. !!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ From 785a7a22bca50e645b831caf67815db1fbc09b1e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 04:02:34 +0300 Subject: [PATCH 834/928] output divider in logs between throttles --- freqtrade/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index c224b4ee5..db0dba0e8 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -128,6 +128,7 @@ class Worker(object): return result def _process(self) -> bool: + logger.debug("========================================") state_changed = False try: state_changed = self.freqtrade.process() From c2deb1db255e5a091daa38460212b8a593141ef4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 26 Jul 2019 02:05:20 +0300 Subject: [PATCH 835/928] eliminate warnings in pytest when testing handling of the deprecated strategy interfaces --- freqtrade/tests/strategy/test_strategy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 609cc58ff..df8c0f126 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -366,6 +366,7 @@ def test_strategy_override_use_sell_profit_only(caplog): ) in caplog.record_tuples +@pytest.mark.filterwarnings("ignore:deprecated") def test_deprecate_populate_indicators(result): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', @@ -398,6 +399,7 @@ def test_deprecate_populate_indicators(result): in str(w[-1].message) +@pytest.mark.filterwarnings("ignore:deprecated") def test_call_deprecated_function(result, monkeypatch): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', From bc299067aa2d8f42e9dbc2bb178083f19c09410d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 27 Jul 2019 23:24:06 +0300 Subject: [PATCH 836/928] get rid of pandas warning in pytest --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index fc71200a5..fad89e877 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -297,7 +297,7 @@ def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_resu def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: resultsb = hyperopt_results.copy() - resultsb['trade_duration'][1] = 20 + resultsb.loc[1, 'trade_duration'] = 20 hl = HyperOptLossResolver(default_conf).hyperoptloss longer = hl.hyperopt_loss_function(hyperopt_results, 100) From 08a3d2632859c07ceaf9be05dc668799be5dd73e Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 27 Jul 2019 18:35:21 -0500 Subject: [PATCH 837/928] Update bot-usage.md Update in response to feedback. --- docs/bot-usage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b29b046e5..bc0d0a87c 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,7 +2,8 @@ This page explains the different parameters of the bot and how to run it. -Note: You can now use the bot by executing `source .env/bin/activate; freqtrade`. +!Note: + If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. ## Bot commands From b691fb7f2dc04b9def11a56e6746381622d144aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jul 2019 15:19:17 +0200 Subject: [PATCH 838/928] Fix some hyperopt tests --- freqtrade/tests/optimize/test_hyperopt.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index fad89e877..065b4c41b 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -8,7 +8,7 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import DependencyException +from freqtrade import DependencyException, OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt @@ -186,6 +186,13 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: assert hasattr(x, "ticker_interval") +def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): + HyperOptResolver(default_conf, ).hyperopt + + def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: hl = DefaultHyperOptLoss @@ -193,9 +200,15 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - x = HyperOptResolver(default_conf, ).hyperopt - assert hasattr(x, "populate_indicators") - assert hasattr(x, "ticker_interval") + x = HyperOptLossResolver(default_conf, ).hyperoptloss + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver(default_conf, ).hyperopt def test_start(mocker, default_conf, caplog) -> None: From 02bfe2dad3d9ad8c3f1aeba2023c703a20f2e9e2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:06 +0000 Subject: [PATCH 839/928] Update numpy from 1.16.4 to 1.17.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52442fb19..be1495d08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Load common requirements -r requirements-common.txt -numpy==1.16.4 +numpy==1.17.0 pandas==0.24.2 scipy==1.3.0 From 5a6e20a6aaa3174623db908a6ab8007143f554a5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:07 +0000 Subject: [PATCH 840/928] Update pandas from 0.24.2 to 0.25.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be1495d08..6420c7879 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ -r requirements-common.txt numpy==1.17.0 -pandas==0.24.2 +pandas==0.25.0 scipy==1.3.0 From fe088dc8c32bfe2f1378b5a361e198bcd8fbd042 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:08 +0000 Subject: [PATCH 841/928] Update ccxt from 1.18.860 to 1.18.992 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 409c979b5..0d6c3f217 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.860 +ccxt==1.18.992 SQLAlchemy==1.3.5 python-telegram-bot==11.1.0 arrow==0.14.2 From 0fd91e4450e97acb05635aac63e62fe6311dc926 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:09 +0000 Subject: [PATCH 842/928] Update sqlalchemy from 1.3.5 to 1.3.6 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 0d6c3f217..a1cdbc6c0 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.992 -SQLAlchemy==1.3.5 +SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.2 cachetools==3.1.1 From 9f70ebecf16fd7afaa51ddfec57796d706cc7e82 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:10 +0000 Subject: [PATCH 843/928] Update arrow from 0.14.2 to 0.14.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index a1cdbc6c0..b79c700c8 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.992 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 -arrow==0.14.2 +arrow==0.14.3 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From a3620c60adbbed1de171bbbaf03174cf973d7cc4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:11 +0000 Subject: [PATCH 844/928] Update flask from 1.0.3 to 1.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b79c700c8..2e52b84ad 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.2 sdnotify==0.3.2 # Api server -flask==1.0.3 +flask==1.1.1 From ebca1e435745c2c7c72805c25d6629fe0cc0df92 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:12 +0000 Subject: [PATCH 845/928] Update flake8 from 3.7.7 to 3.7.8 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 83fec3b8a..28fd19ce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -flake8==3.7.7 +flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==5.0.0 From 0f632201e07c06f2f95cd48661857b79a00b488f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:14 +0000 Subject: [PATCH 846/928] Update pytest from 5.0.0 to 5.0.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 28fd19ce4..e6f7bde1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==5.0.0 +pytest==5.0.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From 3e95b7d8a51c2a12d074223eecc9ab90ecd2f913 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:15 +0000 Subject: [PATCH 847/928] Update mypy from 0.711 to 0.720 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6f7bde1c..946d63039 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,4 @@ pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-random-order==1.0.4 coveralls==1.8.1 -mypy==0.711 +mypy==0.720 From 5ba0aa8082b1c801f7cd99dca8356e32f25ff23d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Jul 2019 15:25:16 +0000 Subject: [PATCH 848/928] Update plotly from 3.10.0 to 4.0.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index d4e4fc165..a6753fc3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.10.0 +plotly==4.0.0 From e64509f1b42d95c4a86a6b6663dbf15c27bae6ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:27:50 +0200 Subject: [PATCH 849/928] Version bump to 2019.7 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2fdcccea5..10900bee9 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.6-dev' +__version__ = '2019.7' class DependencyException(Exception): From 7a97995d81d25f6f9928c82a31d8de57c36e6a8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:30:14 +0200 Subject: [PATCH 850/928] 2017.7-dev version bump --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 10900bee9..14f0bb819 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.7' +__version__ = '2019.7-dev' class DependencyException(Exception): From e14dd4974f0ebfeb07a302064cc2fc5cd615019d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:32:28 +0200 Subject: [PATCH 851/928] Improve release documentation --- docs/developer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index f58e0597d..f50625111 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -156,6 +156,8 @@ git log --oneline --no-decorate --no-merges master..develop ### Create github release / tag +Once the PR against master is merged (best right after merging): + * Use the button "Draft a new release" in the Github UI (subsection releases) * Use the version-number specified as tag. * Use "master" as reference (this step comes after the above PR is merged). From 8dd8addd3aeadfd652f65a3a141491e263dcd79b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:52:38 +0200 Subject: [PATCH 852/928] Sort requirements-dev file --- requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 946d63039..f54b38a57 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,13 +2,13 @@ -r requirements.txt -r requirements-plot.txt +coveralls==1.8.1 flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 +mypy==0.720 pytest==5.0.1 -pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 +pytest-mock==1.10.4 pytest-random-order==1.0.4 -coveralls==1.8.1 -mypy==0.720 From 7bea0007c7b6ef253a5e7903728ac556f5c2e0e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:53:26 +0200 Subject: [PATCH 853/928] Allow installing via submodules freqtrade can be installed using `pip install -e .[all]` to include all dependencies --- setup.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index ca2f81d1f..1ae395295 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,24 @@ if version_info.major == 3 and version_info.minor < 6 or \ from freqtrade import __version__ +# Requirements used for submodules +api = ['flask'] +plot = ['plotly>=4.0'] + +develop = [ + 'coveralls', + 'flake8', + 'flake8-type-annotations', + 'flake8-tidy-imports', + 'mypy', + 'pytest', + 'pytest-asyncio', + 'pytest-cov', + 'pytest-mock', + 'pytest-random-order', +] + +all_extra = api + plot + develop setup(name='freqtrade', version=__version__, @@ -20,26 +38,37 @@ setup(name='freqtrade', setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ - 'ccxt', + # from requirements-common.txt + 'ccxt>=1.18', 'SQLAlchemy', 'python-telegram-bot', 'arrow', + 'cachetools', 'requests', 'urllib3', 'wrapt', - 'pandas', 'scikit-learn', - 'scipy', 'joblib', 'jsonschema', 'TA-Lib', 'tabulate', - 'cachetools', 'coinmarketcap', 'scikit-optimize', + 'filelock', + 'py_find_1st', 'python-rapidjson', - 'py_find_1st' + 'sdnotify', + # from requirements.txt + 'numpy', + 'pandas', + 'scipy', ], + extras_require={ + 'api': api, + 'dev': develop, + 'plot': plot, + 'all': all_extra, + }, include_package_data=True, zip_safe=False, entry_points={ From f825e81d0eba06b58adc721715d7bbc545413ee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:54:35 +0200 Subject: [PATCH 854/928] developers need all dependencies! --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ae395295..202e3fd0d 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup(name='freqtrade', ], extras_require={ 'api': api, - 'dev': develop, + 'dev': all_extra, 'plot': plot, 'all': all_extra, }, From 59caff8fb1d49bf26b29cb3855354d0eed7d2aa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jul 2019 20:57:57 +0200 Subject: [PATCH 855/928] UPdate developer docs --- docs/developer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f58e0597d..2895db7e8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -12,8 +12,8 @@ Special fields for the documentation (like Note boxes, ...) can be found [here]( ## Developer setup -To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`. +To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. From 8f1f416a52b2381d6c2958a905eb4716dae1d037 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Jul 2019 11:47:28 +0300 Subject: [PATCH 856/928] hyperopt cleanup and output improvements --- freqtrade/optimize/hyperopt.py | 65 ++++++++++++++++------------------ 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 759ceffbe..842a111ca 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -51,7 +51,7 @@ class Hyperopt(Backtesting): self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - self.total_tries = config.get('epochs', 0) + self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 if not self.config.get('hyperopt_continue'): @@ -124,13 +124,12 @@ class Hyperopt(Backtesting): """ results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] - logger.info( - 'Best result:\n%s\nwith values:\n', - best_result['result'] - ) + + log_str = self.format_results_logstring(best_result) + print(f"\nBest result:\n{log_str}\nwith values:") pprint(best_result['params'], indent=4) if 'roi_t1' in best_result['params']: - logger.info('ROI table:') + print("ROI table:") pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) def log_results(self, results) -> None: @@ -139,22 +138,26 @@ class Hyperopt(Backtesting): """ print_all = self.config.get('print_all', False) if print_all or results['loss'] < self.current_best_loss: - # Output human-friendly index here (starting from 1) - current = results['current_tries'] + 1 - total = results['total_tries'] - res = results['result'] - loss = results['loss'] - self.current_best_loss = results['loss'] - log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' - log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}' + log_str = self.format_results_logstring(results) if print_all: - print(log_msg) + print(log_str) else: - print('\n' + log_msg) + print('\n' + log_str) else: print('.', end='') sys.stdout.flush() + def format_results_logstring(self, results) -> str: + # Output human-friendly index here (starting from 1) + current = results['current_epoch'] + 1 + total = self.total_epochs + res = results['results_explanation'] + loss = results['loss'] + self.current_best_loss = results['loss'] + log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' + log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' + return log_str + def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration @@ -214,7 +217,7 @@ class Hyperopt(Backtesting): 'end_date': max_date, } ) - result_explanation = self.format_results(results) + results_explanation = self.format_results(results) trade_count = len(results.index) @@ -226,7 +229,7 @@ class Hyperopt(Backtesting): return { 'loss': MAX_LOSS, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } loss = self.calculate_loss(results=results, trade_count=trade_count, @@ -235,12 +238,12 @@ class Hyperopt(Backtesting): return { 'loss': loss, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } def format_results(self, results: DataFrame) -> str: """ - Return the format result in a string + Return the formatted results explanation in a string """ trades = len(results.index) avg_profit = results.profit_percent.mean() * 100.0 @@ -323,25 +326,19 @@ class Hyperopt(Backtesting): with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info(f'Effective number of parallel workers used: {jobs}') - EVALS = max(self.total_tries // jobs, 1) + EVALS = max(self.total_epochs // jobs, 1) for i in range(EVALS): asked = opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked) - opt.tell(asked, [i['loss'] for i in f_val]) - - self.trials += f_val + opt.tell(asked, [v['loss'] for v in f_val]) for j in range(jobs): current = i * jobs + j - self.log_results({ - 'loss': f_val[j]['loss'], - 'current_tries': current, - 'initial_point': current < INITIAL_POINTS, - 'total_tries': self.total_tries, - 'result': f_val[j]['result'], - }) - logger.debug(f"Optimizer params: {f_val[j]['params']}") - for j in range(jobs): - logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") + val = f_val[j] + val['current_epoch'] = current + val['is_initial_point'] = current < INITIAL_POINTS + self.log_results(val) + self.trials.append(val) + logger.debug(f"Optimizer epoch evaluated: {val}") except KeyboardInterrupt: print('User interrupted..') From b976f24672fd8f0118d94a5890925fc404998abc Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 30 Jul 2019 11:47:46 +0300 Subject: [PATCH 857/928] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 065b4c41b..5d6128e7c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -370,13 +370,13 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 + hyperopt.total_epochs = 2 hyperopt.log_results( { 'loss': 1, - 'current_tries': 1, - 'total_tries': 2, - 'result': 'foo.', - 'initial_point': False + 'current_epoch': 1, + 'results_explanation': 'foo.', + 'is_initial_point': False } ) out, err = capsys.readouterr() @@ -433,7 +433,7 @@ def test_roi_table_generation(hyperopt) -> None: assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} -def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: +def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch( @@ -443,7 +443,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', - MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) ) patch_exchange(mocker) @@ -457,8 +457,11 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() + parallel.assert_called_once() - assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples) + + out, err = capsys.readouterr() + assert 'Best result:\n* 1/1: foo result Objective: 1.00000\nwith values:\n' in out assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 @@ -598,8 +601,8 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31Σ%). Avg duration 100.0 mins.', + 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + '( 2.31Σ%). Avg duration 100.0 mins.', 'params': optimizer_param } From 065ebd39ef9fae6c57f133bbf4c41ee46e3e6be1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 1 Aug 2019 23:57:26 +0300 Subject: [PATCH 858/928] cleanup in hyperopt --- freqtrade/optimize/hyperopt.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 842a111ca..479f8c70a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -78,6 +78,12 @@ class Hyperopt(Backtesting): self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False), + if self.has_space('sell'): + # Make sure experimental is enabled + if 'experimental' not in self.config: + self.config['experimental'] = {} + self.config['experimental']['use_sell_signal'] = True + def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. @@ -124,13 +130,14 @@ 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{log_str}\nwith values:") - pprint(best_result['params'], indent=4) - if 'roi_t1' in best_result['params']: + pprint(params, indent=4) + if self.has_space('roi'): print("ROI table:") - pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) + pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) def log_results(self, results) -> None: """ @@ -162,9 +169,7 @@ class Hyperopt(Backtesting): """ Tell if a space value is contained in the configuration """ - if space in self.config['spaces'] or 'all' in self.config['spaces']: - return True - return False + return any(s in self.config['spaces'] for s in [space, 'all']) def hyperopt_space(self) -> List[Dimension]: """ @@ -172,16 +177,16 @@ class Hyperopt(Backtesting): """ spaces: List[Dimension] = [] if self.has_space('buy'): + logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() if self.has_space('sell'): + logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - # Make sure experimental is enabled - if 'experimental' not in self.config: - self.config['experimental'] = {} - self.config['experimental']['use_sell_signal'] = True if self.has_space('roi'): + logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() if self.has_space('stoploss'): + logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces From 3ccfe88ad86d57bd3c16dab2c18b4ea2cd61db81 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 1 Aug 2019 23:57:50 +0300 Subject: [PATCH 859/928] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 5d6128e7c..e114dceaf 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.tests.conftest import (get_args, log_has, log_has_re, @pytest.fixture(scope='function') def hyperopt(default_conf, mocker): + default_conf.update({'spaces': ['all']}) patch_exchange(mocker) return Hyperopt(default_conf) @@ -455,6 +456,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() From 0413598d7bdd655e4d0d8ac88351d0b3b7aa5cf9 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Tue, 30 Jul 2019 21:04:05 -0400 Subject: [PATCH 860/928] adding environment.yml for conda builds --- environment.yml | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..8f43b6991 --- /dev/null +++ b/environment.yml @@ -0,0 +1,58 @@ +name: freqtrade +channels: + - defaults + - conda-forge +dependencies: + # Required for app + - python>=3.6 + - pip + - wheel + - numpy + - pandas + - scipy + - SQLAlchemy + - scikit-learn + - arrow + - requests + - urllib3 + - wrapt + - joblib + - jsonschema + - tabulate + - python-rapidjson + - filelock + - flask + - python-dotenv + - cachetools + - scikit-optimize + - python-telegram-bot + # Optional for plotting + - plotly + # Optional for development + - flake8 + - pytest + - pytest-mock + - pytest-asyncio + - pytest-cov + - coveralls + - mypy + # Useful for jupyter + - ipykernel + - isort + - yapf + - pip: + # Required for app + - cython + - coinmarketcap + - ccxt + - TA-Lib + - py_find_1st + - sdnotify + # Optional for develpment + - flake8-tidy-imports + - flake8-type-annotations + - pytest-random-order + - -e . + + + From fceb41115448b4e7c776412094491ffce32926ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Aug 2019 20:08:30 +0200 Subject: [PATCH 861/928] Create detailed section about strategy problem analysis --- docs/data-analysis.md | 119 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 1940fa3e6..76fedf475 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -6,6 +6,125 @@ A good way for this is using Jupyter (notebook or lab) - which provides an inter The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. +## Strategy development problem analysis + +Debugging a strategy (are there no buy signals, ...) can be very time-consuming. +FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. + +I recommend using Juptyer Notebooks for this analysis, since it offers a dynamic way to rerun certain parts. + +The following is a full code-snippet, which will be explained by both comments, and step by step below. + +```python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = '../xmatt/strategies' +# Location of the data +data_location = '../freqtrade/user_data/data/binance/' +# Only use one pair here +pair = "XRP_ETH" + +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) + + +# Load strategy - best done in a new cell +# Needs to be ran each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() + +``` + +### Explanation + +#### Imports and constant definition + +``` python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = 'user_data/strategies' +# Location of the data +data_location = 'user_data/data/binance' +# Only use one pair here +pair = "XRP_ETH" +``` + +This first section imports necessary modules, and defines some constants you'll probably need differently + +#### Load candles + +``` python +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) +``` + +This second section loads the historic data and prints the amount of candles in the data. + +#### Run strategy and analyze results + +Now, it's time to load and run your strategy. +For this, I recommend using a new cell in your notebook, since you'll want to repeat this until you're satisfied with your strategy. + +``` python +# Load strategy - best done in a new cell +# Needs to be ran each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() +``` + +The code snippet loads and analyzes the strategy, prints the number of buy signals. + +The last 2 lines serve to analyze the dataframe in detail. +This can be important if your strategy did not generate any buy signals. +Note that using `data.head()` would also work, however this is misleading since most indicators have some "startup" time at the start of a backtested dataframe. + +There can be many things wrong, some signs to look for are: + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + ## Backtesting To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). From 01cd30984b1e166ab59b446521350b74fa83eb12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 06:47:03 +0200 Subject: [PATCH 862/928] Improve wording --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 76fedf475..2c5cc8842 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -79,7 +79,7 @@ data_location = 'user_data/data/binance' pair = "XRP_ETH" ``` -This first section imports necessary modules, and defines some constants you'll probably need differently +This first section imports necessary modules, and defines some constants you'll probably need to adjust for your case. #### Load candles From 76d22bc743f5e697b87e4991e6233c2a99b5e057 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 09:37:59 +0200 Subject: [PATCH 863/928] Show correct valueerror message --- freqtrade/data/btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f2356c34b..5865d56a7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ def load_backtest_data(filename) -> pd.DataFrame: filename = Path(filename) if not filename.is_file(): - raise ValueError("File {filename} does not exist.") + raise ValueError(f"File {filename} does not exist.") with filename.open() as file: data = json_load(file) From 0b9b5f39936967ad1e014d06032df14c18d6d31f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 19:50:12 +0200 Subject: [PATCH 864/928] Improve document wording --- docs/data-analysis.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2c5cc8842..68e085ff3 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -11,7 +11,7 @@ The following helpers will help you loading the data into Pandas DataFrames, and Debugging a strategy (are there no buy signals, ...) can be very time-consuming. FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. -I recommend using Juptyer Notebooks for this analysis, since it offers a dynamic way to rerun certain parts. +It's recommendet using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. The following is a full code-snippet, which will be explained by both comments, and step by step below. @@ -33,6 +33,8 @@ data_location = '../freqtrade/user_data/data/binance/' # Only use one pair here pair = "XRP_ETH" +### End constants + # Load data bt_data = load_pair_history(datadir=Path(data_location), ticker_interval = ticker_interval, @@ -91,7 +93,8 @@ bt_data = load_pair_history(datadir=Path(data_location), print(len(bt_data)) ``` -This second section loads the historic data and prints the amount of candles in the data. +This second section loads the historic data and prints the amount of candles in the DataFrame. +You can also inspect this dataframe by using `bt_data.head()` or `bt_data.tail()`. #### Run strategy and analyze results @@ -114,7 +117,7 @@ data = df.set_index('date', drop=True) data.tail() ``` -The code snippet loads and analyzes the strategy, prints the number of buy signals. +The code snippet loads and analyzes the strategy, calculates and prints the number of buy signals. The last 2 lines serve to analyze the dataframe in detail. This can be important if your strategy did not generate any buy signals. From 32605fa10aea008c5332c7369d10df77e568db64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 19:52:56 +0200 Subject: [PATCH 865/928] small improvements --- docs/data-analysis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 68e085ff3..5099e1013 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -41,9 +41,9 @@ bt_data = load_pair_history(datadir=Path(data_location), pair=pair) print(len(bt_data)) - +### Start strategy reload # Load strategy - best done in a new cell -# Needs to be ran each time the strategy-file is changed. +# Rerun each time the strategy-file is changed. strategy = StrategyResolver({'strategy': strategyname, 'user_data_dir': Path.cwd(), 'strategy_path': location}).strategy From 3eb571f34c1ae12a33dd2dfc6f2f7a85f398898a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Aug 2019 20:04:18 +0200 Subject: [PATCH 866/928] recommended ... --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 5099e1013..2b6d6ed58 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -11,7 +11,7 @@ The following helpers will help you loading the data into Pandas DataFrames, and Debugging a strategy (are there no buy signals, ...) can be very time-consuming. FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. -It's recommendet using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. +It's recommended using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. The following is a full code-snippet, which will be explained by both comments, and step by step below. From aa8f44f68c428962485231628a38dc9820009280 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 2 Aug 2019 22:22:58 +0300 Subject: [PATCH 867/928] improvements to hyperopt output --- freqtrade/optimize/hyperopt.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 479f8c70a..b68b1ac4f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,7 +11,7 @@ import sys from operator import itemgetter from pathlib import Path from pprint import pprint -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame @@ -70,7 +70,7 @@ class Hyperopt(Backtesting): if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: @@ -134,10 +134,19 @@ class Hyperopt(Backtesting): log_str = self.format_results_logstring(best_result) print(f"\nBest result:\n{log_str}\nwith values:") - pprint(params, indent=4) + 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: """ @@ -171,21 +180,24 @@ class Hyperopt(Backtesting): """ return any(s in self.config['spaces'] for s in [space, 'all']) - def hyperopt_space(self) -> List[Dimension]: + def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the space to use during Hyperopt + Return the dimentions in the hyperoptimization space. + :param space: Defines hyperspace to return dimentions for. + If None, then the self.has_space() will be used to return dimentions + for all hyperspaces used. """ spaces: List[Dimension] = [] - if self.has_space('buy'): + if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if self.has_space('sell'): + if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - if self.has_space('roi'): + if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if self.has_space('stoploss'): + if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces From b152d1a7abd09774a6b3367b28a89bfeae4e4a47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 2 Aug 2019 22:23:48 +0300 Subject: [PATCH 868/928] docs agjusted, plus minor fixes --- docs/hyperopt.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2755cae2d..fbd00bcbd 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -303,8 +303,9 @@ Given the following result from hyperopt: ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 with values: +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, @@ -347,21 +348,14 @@ If you are optimizing ROI, you're result will look as follows and include a ROI ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 with values: +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, - 'adx-enabled': false, + 'adx-enabled': False, 'rsi-enabled': True, - 'trigger': 'bb_lower', - 'roi_t1': 40, - 'roi_t2': 57, - 'roi_t3': 21, - 'roi_p1': 0.03634636907306948, - 'roi_p2': 0.055237357937802885, - 'roi_p3': 0.015163796015548354, - 'stoploss': -0.37996664668703606 -} + 'trigger': 'bb_lower'} ROI table: { 0: 0.10674752302642071, 21: 0.09158372701087236, @@ -372,9 +366,9 @@ ROI table: This would translate to the following ROI table: ``` python - minimal_roi = { +minimal_roi = { "118": 0, - "78": 0.0363463, + "78": 0.0363, "21": 0.0915, "0": 0.106 } From cad7d9135a3c2a926993d43710dfb5fe2585858f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 09:24:27 +0300 Subject: [PATCH 869/928] tests: hide deprecation warning due to use of --live --- freqtrade/tests/optimize/test_backtesting.py | 3 +++ freqtrade/tests/test_configuration.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 9304871a8..37757743e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -202,6 +202,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( @@ -812,6 +813,7 @@ def test_backtest_record(default_conf, fee, mocker): assert dur > 0 +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_live(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -858,6 +860,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): assert log_has(line, caplog.record_tuples) +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 56ff79625..1e76297a6 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -325,6 +325,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'export' not in config +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( From 3b65c986eea10e68da391997831a4d1a769aff2c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 10:20:20 +0300 Subject: [PATCH 870/928] wordings fixed --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b68b1ac4f..8767ed9a7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -182,9 +182,9 @@ class Hyperopt(Backtesting): def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the dimentions in the hyperoptimization space. - :param space: Defines hyperspace to return dimentions for. - If None, then the self.has_space() will be used to return dimentions + Return the dimensions in the hyperoptimization space. + :param space: Defines hyperspace to return dimensions for. + If None, then the self.has_space() will be used to return dimensions for all hyperspaces used. """ spaces: List[Dimension] = [] From 13620df717bf73e38e3ac974dfca8d31cdd53178 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 11:05:05 +0300 Subject: [PATCH 871/928] 'with values:' line removed --- docs/hyperopt.md | 6 ++++-- freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fbd00bcbd..0b5d1a50e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -303,8 +303,9 @@ Given the following result from hyperopt: ``` Best result: + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -with values: + Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, @@ -348,8 +349,9 @@ If you are optimizing ROI, you're result will look as follows and include a ROI ``` Best result: + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -with values: + Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8767ed9a7..427b17cb8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -133,7 +133,7 @@ class Hyperopt(Backtesting): params = best_result['params'] log_str = self.format_results_logstring(best_result) - print(f"\nBest result:\n{log_str}\nwith values:") + 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')}, From e8b2ae0b85c1e311a87c037b3852fadb60d60dff Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 3 Aug 2019 11:19:36 +0300 Subject: [PATCH 872/928] tests adjusted --- freqtrade/tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index e114dceaf..e3b049c06 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -463,7 +463,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: parallel.assert_called_once() out, err = capsys.readouterr() - assert 'Best result:\n* 1/1: foo result Objective: 1.00000\nwith values:\n' in out + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 From bbd58e772eaa003aa18ce28c5fe2a15818f5d76f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:11:51 +0200 Subject: [PATCH 873/928] Warn when using restricted pairs As noted in https://github.com/ccxt/ccxt/issues/5624, there is currently no way to detect if a user is impacted by this or not prior to creating a order. --- freqtrade/exchange/exchange.py | 8 +++++++- freqtrade/tests/conftest.py | 18 +++++++++--------- freqtrade/tests/exchange/test_exchange.py | 21 +++++++++++++++++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a7c76e635..37bbb778e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -260,7 +260,7 @@ class Exchange(object): if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') - # return + return for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs @@ -269,6 +269,12 @@ class Exchange(object): raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + elif self.markets[pair].get('info', {}).get('IsRestricted', False): + # Warn users about restricted pairs in whitelist. + # We cannot determine reliably if Users are affected. + logger.warning(f"Pair {pair} is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove {pair} from your whitelist.") def get_valid_pair_combination(self, curr_1, curr_2) -> str: """ diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 5862a2e89..71ed23901 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -304,7 +304,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -329,7 +329,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -354,7 +354,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -379,7 +379,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -404,7 +404,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'NEO/BTC': { 'id': 'neobtc', @@ -429,7 +429,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BTT/BTC': { 'id': 'BTTBTC', @@ -457,7 +457,7 @@ def markets(): 'max': None } }, - 'info': "", + 'info': {}, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -479,7 +479,7 @@ def markets(): } }, 'active': True, - 'info': "" + 'info': {}, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -501,7 +501,7 @@ def markets(): 'max': None } }, - 'info': "" + 'info': {}, } } diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a5cdf0a82..67a4bbeeb 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -318,7 +318,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {} }) id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -332,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': 'inactive' + 'XRP/BTC': {'inactive'} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -361,6 +361,23 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.record_tuples) +def test_validate_pairs_restricted(default_conf, mocker, caplog): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}} + }) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + + Exchange(default_conf) + assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove XRP/BTC from your whitelist.", + caplog.record_tuples) + + def test_validate_timeframes(default_conf, mocker): default_conf["ticker_interval"] = "5m" api_mock = MagicMock() From ad55faafa899fefc828bddd2e08a7f4a4f52749d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:18:37 +0200 Subject: [PATCH 874/928] Fix odd test --- freqtrade/tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 67a4bbeeb..2f9e525dd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -332,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': {'inactive'} + 'XRP/BTC': {'inactive': True} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) From 8ab07e0451d1205eb4ebee2f2043f9df56cc2e13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 13:22:44 +0200 Subject: [PATCH 875/928] Add FAQ section about restricted markets --- docs/faq.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 83576af4d..fe6f66130 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -45,6 +45,16 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c You can use the `/forcesell all` command from Telegram. +### I get the message "RESTRICTED_MARKET" + +Currently known to happen for US Bittrex users. +Bittrex split it's exchange into an US and an International verssion. +The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. + +If you have restricted pairs in your whitelist, you'll get a Warning-message on startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will not impact you. +If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. + ## Hyperopt module ### How many epoch do I need to get a good Hyperopt result? From d59608f7649ba5fc352c7a12d89b9466cbd11b2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 17:19:37 +0200 Subject: [PATCH 876/928] adjust some documentation wordings --- docs/faq.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index fe6f66130..e8367608f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,11 +48,11 @@ You can use the `/forcesell all` command from Telegram. ### I get the message "RESTRICTED_MARKET" Currently known to happen for US Bittrex users. -Bittrex split it's exchange into an US and an International verssion. +Bittrex split it's exchange into US and International versions. The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. -If you have restricted pairs in your whitelist, you'll get a Warning-message on startup for each restricted pair. -If you're an "International" Customer on the Bittrex exchange, then this warning will not impact you. +If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you. If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. ## Hyperopt module From c4e30862eeda84a1aa7f4e587c6851bf05905b4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 19:55:54 +0200 Subject: [PATCH 877/928] load_trades_db should give as many columns as possible --- freqtrade/data/btanalysis.py | 21 ++++++++++++++++----- freqtrade/tests/data/test_btanalysis.py | 5 +++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5865d56a7..6568fe31e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: """ trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration", "sell_reason", - "max_rate", "min_rate"] - trades = pd.DataFrame([(t.pair, t.calc_profit(), + columns = ["pair", "open_time", "close_time", "profit", "profitperc", + "open_rate", "close_rate", "amount", "duration", "sell_reason", + "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", + "stake_amount", "max_rate", "min_rate", "id", "exchange", + "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] + + trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=pytz.UTC), t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, - t.open_rate, t.close_rate, + t.calc_profit(), t.calc_profit_percent(), + t.open_rate, t.close_rate, t.amount, t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None, t.sell_reason, + t.fee_open, t.fee_close, + t.open_rate_requested, + t.close_rate_requested, + t.stake_amount, t.max_rate, t.min_rate, + t.id, t.exchange, + t.stop_loss, t.initial_stop_loss, + t.strategy, t.ticker_interval ) for t in Trade.query.all()], columns=columns) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index e80840009..bad8db66f 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -45,6 +45,11 @@ def test_load_trades_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns + assert "profitperc" in trades.columns + + for col in BT_DATA_COLUMNS: + if col not in ['index', 'open_at_end']: + assert col in trades.columns def test_extract_trades_of_period(): From d51fd1a5d0bec672fcadd92fcc38cf15add751ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 19:56:41 +0200 Subject: [PATCH 878/928] fix typo --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index e8367608f..a441ffacd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,7 +48,7 @@ You can use the `/forcesell all` command from Telegram. ### I get the message "RESTRICTED_MARKET" Currently known to happen for US Bittrex users. -Bittrex split it's exchange into US and International versions. +Bittrex split its exchange into US and International versions. The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. From c6bd14378502aeccf4ee2b6af120ca90922e61be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Aug 2019 20:04:49 +0200 Subject: [PATCH 879/928] add Operating system to issue template --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 94d998310..ae5375f43 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue. ## Step 2: Describe your environment + * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) * Branch: Master | Develop From 52d92cba90cd3e9baa1c48da8e47c8ddb68a4fb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:20:31 +0200 Subject: [PATCH 880/928] Split analyze_ticker and _analyze_ticker_int --- freqtrade/strategy/interface.py | 22 +++++++++++++++++----- freqtrade/tests/strategy/test_interface.py | 20 ++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2a28bcd22..5bd183fa9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,6 +158,21 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame with ticker data and indicator data + """ + logger.debug("TA Analysis Launched") + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + return dataframe + + def _analyze_ticker_int(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Parses the given ticker history and returns a populated DataFrame + add several TA indicators and buy signal to it + Used internally, may skip analysis if `process_only_new_candles` is set. :return: DataFrame with ticker data and indicator data """ @@ -168,10 +183,7 @@ class IStrategy(ABC): if (not self.process_only_new_candles or self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): # Defs that only make change on new candle data. - logger.debug("TA Analysis Launched") - dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] else: logger.debug("Skipping TA Analysis for already analyzed candle") @@ -198,7 +210,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self.analyze_ticker(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_int(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index ee8c8ddd4..6bf3770c5 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={}) def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) @@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) @@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], @@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): oldtime = arrow.utcnow().shift(minutes=-16) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_int', return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -252,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.record_tuples) -def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: +def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -267,7 +267,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) assert 'high' in ret.columns assert 'low' in ret.columns assert 'close' in ret.columns @@ -280,7 +280,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.record_tuples) caplog.clear() - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 From 62262d0bb50a398c09e4d9f31f5f6ab9176f972c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:21:22 +0200 Subject: [PATCH 881/928] improve docstring of _analyze_ticker_int --- freqtrade/strategy/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5bd183fa9..48a4f96a2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -172,7 +172,9 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it - Used internally, may skip analysis if `process_only_new_candles` is set. + WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame with ticker data and indicator data """ From e4380b533bc4515cd6fc227690d7efb77b945d12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:25:46 +0200 Subject: [PATCH 882/928] Print plot filename so it can be easily opened --- freqtrade/plot/plotting.py | 5 +++-- freqtrade/tests/test_plotting.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5c9c6e457..d03d3ae53 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -318,6 +318,7 @@ def store_plot_file(fig, filename: str, auto_open: bool = False) -> None: """ Path("user_data/plots").mkdir(parents=True, exist_ok=True) - - plot(fig, filename=str(Path('user_data/plots').joinpath(filename)), + _filename = Path('user_data/plots').joinpath(filename) + plot(fig, filename=str(_filename), auto_open=auto_open) + logger.info(f"Stored plot as {_filename}") diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 509bf7880..f9da773fe 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -215,6 +215,8 @@ def test_generate_plot_file(mocker, caplog): assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") + assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html", + caplog.record_tuples) def test_add_profit(): From c5ccf44750d70cc670671ea50af19d710468b97e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:26:04 +0200 Subject: [PATCH 883/928] Remove generate_dataframe from plot_dataframe script --- scripts/plot_dataframe.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 034a6f448..6412c45a4 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -16,8 +16,6 @@ import logging import sys from typing import Any, Dict, List -import pandas as pd - from freqtrade.configuration import Arguments from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period @@ -30,20 +28,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: - """ - Get tickers then Populate strategy indicators and signals, then return the full dataframe - :return: the DataFrame of a pair - """ - - dataframes = strategy.tickerdata_to_dataframe(tickers) - dataframe = dataframes[pair] - dataframe = strategy.advise_buy(dataframe, {'pair': pair}) - dataframe = strategy.advise_sell(dataframe, {'pair': pair}) - - return dataframe - - def analyse_and_plot_pairs(config: Dict[str, Any]): """ From arguments provided in cli: @@ -57,6 +41,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): """ plot_elements = init_plotscript(config) trades = plot_elements['trades'] + strategy = plot_elements["strategy"] pair_counter = 0 for pair, data in plot_elements["tickers"].items(): @@ -64,7 +49,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair) + + dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) From 4d1ce8178c8abc55488932f07b20055c08497312 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 10:38:37 +0200 Subject: [PATCH 884/928] intend if to be clearer --- freqtrade/data/btanalysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6568fe31e..36d8aedbb 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -93,8 +93,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.calc_profit(), t.calc_profit_percent(), t.open_rate, t.close_rate, t.amount, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None, + (t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None), t.sell_reason, t.fee_open, t.fee_close, t.open_rate_requested, From 2af663dccbd4fe395984125778729d5c968df087 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Aug 2019 12:55:03 +0200 Subject: [PATCH 885/928] rename _analyze_ticker_int to _analyze_ticker_internal --- freqtrade/strategy/interface.py | 4 ++-- freqtrade/tests/strategy/test_interface.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 48a4f96a2..37aa97bb1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,7 +168,7 @@ class IStrategy(ABC): dataframe = self.advise_sell(dataframe, metadata) return dataframe - def _analyze_ticker_int(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it @@ -212,7 +212,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self._analyze_ticker_int(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 6bf3770c5..9f5ab70e3 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={}) def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) @@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) @@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], @@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): oldtime = arrow.utcnow().shift(minutes=-16) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch.object( - _STRATEGY, '_analyze_ticker_int', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -252,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.record_tuples) -def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> None: +def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -267,7 +267,7 @@ def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> Non strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) assert 'high' in ret.columns assert 'low' in ret.columns assert 'close' in ret.columns @@ -280,7 +280,7 @@ def test__analyze_ticker_int_skip_analyze(ticker_history, mocker, caplog) -> Non caplog.record_tuples) caplog.clear() - ret = strategy._analyze_ticker_int(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 From c6444a10a8463530a75dfaa8d4a2f94fee849e38 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 5 Aug 2019 17:54:53 +0300 Subject: [PATCH 886/928] move roi_space, stoploss_space, generate_roi_table to IHyperOpt --- docs/hyperopt.md | 56 ++++++++++++++++++++---- freqtrade/optimize/default_hyperopt.py | 38 +--------------- freqtrade/optimize/hyperopt_interface.py | 52 ++++++++++++++++------ user_data/hyperopts/sample_hyperopt.py | 55 +++++------------------ 4 files changed, 99 insertions(+), 102 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0b5d1a50e..0304c3188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -18,19 +18,24 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil ### Checklist on all tasks / possibilities in hyperopt -Depending on the space you want to optimize, only some of the below are required. +Depending on the space you want to optimize, only some of the below are required: * fill `populate_indicators` - probably a copy from your strategy * fill `buy_strategy_generator` - for buy signal optimization * fill `indicator_space` - for buy signal optimzation * fill `sell_strategy_generator` - for sell signal optimization * fill `sell_indicator_space` - for sell signal optimzation -* fill `roi_space` - for ROI optimization -* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries) -* fill `stoploss_space` - stoploss optimization -* Optional but recommended - * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used - * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Optional, but recommended: + +* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used +* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) ### 1. Install a Custom Hyperopt File @@ -345,7 +350,7 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Understand Hyperopt ROI results -If you are optimizing ROI, you're result will look as follows and include a ROI table. +If you are optimizing ROI, your result will look as follows and include a ROI table: ``` Best result: @@ -376,6 +381,41 @@ minimal_roi = { } ``` +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: + +| # | minutes | ROI percentage | +|---|---|---| +| 1 | always 0 | 0.03...0.31 | +| 2 | 10...40 | 0.02...0.11 | +| 3 | 20...100 | 0.01...0.04 | +| 4 | 30...220 | always 0 | + +This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges. + +Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values, your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.37996664668703606 +``` + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. + ### Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index ad76ff786..1d4c40916 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List import talib.abstract as ta from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -156,42 +156,6 @@ class DefaultHyperOpts(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - """ - return [ - Real(-0.5, -0.02, name='stoploss'), - ] - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of from strategy diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 08823ece0..f1f123653 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from typing import Dict, Any, Callable, List from pandas import DataFrame -from skopt.space import Dimension +from skopt.space import Dimension, Integer, Real class IHyperOpt(ABC): @@ -26,56 +26,80 @@ class IHyperOpt(ABC): @abstractmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Populate indicators that will be used in the Buy and Sell strategy - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() - :return: a Dataframe with all mandatory indicators for the strategies + Populate indicators that will be used in the Buy and Sell strategy. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe(). + :return: A Dataframe with all mandatory indicators for the strategies. """ @staticmethod @abstractmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a buy strategy generator + Create a buy strategy generator. """ @staticmethod @abstractmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a sell strategy generator + Create a sell strategy generator. """ @staticmethod @abstractmethod def indicator_space() -> List[Dimension]: """ - Create an indicator space + Create an indicator space. """ @staticmethod @abstractmethod def sell_indicator_space() -> List[Dimension]: """ - Create a sell indicator space + Create a sell indicator space. """ @staticmethod - @abstractmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ - Create an roi table + Create a ROI table. + + Generates the ROI table that will be used by Hyperopt. + You may override it in your custom Hyperopt class. """ + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table @staticmethod - @abstractmethod def stoploss_space() -> List[Dimension]: """ - Create a stoploss space + Create a stoploss space. + + Defines range of stoploss values to search. + You may override it in your custom Hyperopt class. """ + return [ + Real(-0.5, -0.02, name='stoploss'), + ] @staticmethod - @abstractmethod def roi_space() -> List[Dimension]: """ - Create a roi space + Create a ROI space. + + Defines values to search for each ROI steps. + You may override it in your custom Hyperopt class. """ + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + Real(0.01, 0.04, name='roi_p1'), + Real(0.01, 0.07, name='roi_p2'), + Real(0.01, 0.20, name='roi_p3'), + ] diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index a78906cf3..9a755e52e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -18,14 +18,19 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class SampleHyperOpts(IHyperOpt): """ This is a test hyperopt to inspire you. + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md - You can: - - Rename the class name (Do not forget to update class_name) - - Add any methods you want to build your hyperopt - - Add any lib you need to build your hyperopt - You must keep: - - the prototype for the methods: populate_indicators, indicator_space, buy_strategy_generator, - roi_space, generate_roi_table, stoploss_space + + You can: + - Rename the class name. + - Add any methods you want to build your hyperopt. + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The roi_space, generate_roi_table, stoploss_space methods were moved to the parent class, you may + override them here if you need it. """ @staticmethod @@ -167,42 +172,6 @@ class SampleHyperOpts(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - """ - return [ - Real(-0.5, -0.02, name='stoploss'), - ] - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of from strategy From 9914198a6c90bcd92b5702115c984ec66cb7faa5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:09 +0000 Subject: [PATCH 887/928] Update ccxt from 1.18.992 to 1.18.1008 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2e52b84ad..dcc3be754 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.992 +ccxt==1.18.1008 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.3 From 187d029d204adf2403d02d57781a94772d6b2891 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:10 +0000 Subject: [PATCH 888/928] Update arrow from 0.14.3 to 0.14.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index dcc3be754..675a28d6a 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.1008 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 -arrow==0.14.3 +arrow==0.14.4 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From 930c25f7f113f1354b056eea772af36cdcf988cb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:11 +0000 Subject: [PATCH 889/928] Update scikit-learn from 0.21.2 to 0.21.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 675a28d6a..f03a90927 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.2 -scikit-learn==0.21.2 +scikit-learn==0.21.3 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From 403f7668d5e300db1c3a7f26d65a781be3a3dcaf Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:16 +0000 Subject: [PATCH 890/928] Update jsonschema from 3.0.1 to 3.0.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f03a90927..b90c13cec 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -10,7 +10,7 @@ urllib3==1.24.2 # pyup: ignore wrapt==1.11.2 scikit-learn==0.21.3 joblib==0.13.2 -jsonschema==3.0.1 +jsonschema==3.0.2 TA-Lib==0.4.17 tabulate==0.8.3 coinmarketcap==5.0.3 From d71102c45ad89a62ec6423a53d6f092f628262cc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:17 +0000 Subject: [PATCH 891/928] Update py_find_1st from 1.1.3 to 1.1.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b90c13cec..7280dfc6e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -20,7 +20,7 @@ scikit-optimize==0.5.2 filelock==3.0.12 # find first, C search in arrays -py_find_1st==1.1.3 +py_find_1st==1.1.4 #Load ticker files 30% faster python-rapidjson==0.7.2 From 5e64d629a3c338ff9684c60bb7ec612c47a7a362 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 5 Aug 2019 15:26:19 +0000 Subject: [PATCH 892/928] Update coveralls from 1.8.1 to 1.8.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f54b38a57..03b37417e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt -r requirements-plot.txt -coveralls==1.8.1 +coveralls==1.8.2 flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 From 904381058c2394248238f2d9aed5c86c9f271ab1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 19:25:43 +0200 Subject: [PATCH 893/928] Add documentation for conda install --- docs/installation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 74b3a3202..589d3fe7f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -219,6 +219,17 @@ as the watchdog. ------ +## Using Conda + +Freqtrade can also be installed using Anaconda (or Miniconda). + +``` bash +conda env create -f environment.yml +``` + +!!! Note: + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. + ## Windows We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). From 3721610a6321f3ec423a4ccf3968b1b64214e260 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:06:42 +0200 Subject: [PATCH 894/928] Add new detailed trade-scenario tests covers cases raised in #1971 --- .../tests/optimize/test_backtest_detail.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 402e22391..bc58b5ae4 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -224,6 +224,53 @@ tc12 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) +# Test 13 - Buy and sell ROI on same candle +# Set stop-loss at 10% (should not apply), ROI at 1% +tc13 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.01, profit_perc=0.01, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + +# Test 14 - Buy and Stoploss on same candle +# Set stop-loss at 5%, ROI at 10% (should not apply) +tc14 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4600, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] +) + + +# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle +# Set stop-loss at 5%, ROI at 10% (should not apply) +tc15 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4900, 5100, 6172, 1, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trailing_stop=False, + trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] +) + TESTS = [ tc0, tc1, @@ -238,6 +285,9 @@ TESTS = [ tc10, tc11, tc12, + tc13, + tc14, + tc15, ] From bc2e920ae2bde9fc0483cc336402507a147dad84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:07:29 +0200 Subject: [PATCH 895/928] Adjust code to verify "current" candle for buy/sells --- freqtrade/optimize/backtesting.py | 4 +++- freqtrade/tests/optimize/test_backtesting.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429633f31..dcccd36a3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -373,7 +373,9 @@ class Backtesting(object): continue trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], + # since indexes has been incremented before, we need to go one step back to + # also check the buying candle for sell conditions. + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:], trade_count_lock, stake_amount, max_open_trades) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 37757743e..59679d1a2 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -618,8 +618,9 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: + # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 19], ['lower', 0], ['sine', 18]] + tests = [['raise', 19], ['lower', 0], ['sine', 35]] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} From c7d032975471072932ebfdaa169a10a2a7b4ec86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:19:19 +0200 Subject: [PATCH 896/928] Clean up comments of detail-backtests --- .../tests/optimize/test_backtest_detail.py | 86 ++++++++----------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index bc58b5ae4..87f567b4f 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,9 +14,8 @@ from freqtrade.tests.optimize import (BTContainer, BTrade, _get_frame_time_from_offset, tests_ticker_interval) -# Test 0 Sell signal sell +# Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% -# TC0: Sell signal in candle 3 tc0 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -29,9 +28,8 @@ tc0 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) -# Test 1 Minus 8% Close +# Test 1: Stop-Loss Triggered 1% loss # Test with Stop-loss at 1% -# TC1: Stop-Loss Triggered 1% loss tc1 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -45,9 +43,8 @@ tc1 = BTContainer(data=[ ) -# Test 2 Minus 4% Low, minus 1% close +# Test 2: Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% -# TC2: Stop-Loss Triggered 3% Loss tc2 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -61,12 +58,12 @@ tc2 = BTContainer(data=[ ) -# Test 3 Candle drops 4%, Recovers 1%. -# Entry Criteria Met -# Candle drops 20% -# Test with Stop-Loss at 2% -# TC3: Trade-A: Stop-Loss Triggered 2% Loss -# Trade-B: Stop-Loss Triggered 2% Loss +# Test 3: Multiple trades. +# Candle drops 4%, Recovers 1%. +# Entry Criteria Met +# Candle drops 20% +# Trade-A: Stop-Loss Triggered 2% Loss +# Trade-B: Stop-Loss Triggered 2% Loss tc3 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -81,10 +78,10 @@ tc3 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] ) -# Test 4 Minus 3% / recovery +15% +# Test 4: Minus 3% / recovery +15% # Candle Data for test 3 – Candle drops 3% Closed 15% up # Test with Stop-loss at 2% ROI 6% -# TC4: Stop-Loss Triggered 2% Loss +# Stop-Loss Triggered 2% Loss tc4 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -97,9 +94,8 @@ tc4 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 5 / Drops 0.5% Closes +20% -# Set stop-loss at 1% ROI 3% -# TC5: ROI triggers 3% Gain +# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain +# stop-loss: 1%, ROI: 3% tc5 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4980, 4987, 6172, 1, 0], @@ -112,9 +108,8 @@ tc5 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) -# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve -# Set stop-loss at 2% ROI at 5% -# TC6: Stop-Loss triggers 2% Loss +# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss +# stop-loss: 2% ROI: 5% tc6 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -127,9 +122,8 @@ tc6 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 7 - 6% Positive / 1% Negative / Close 1% Positve -# Set stop-loss at 2% ROI at 3% -# TC7: ROI Triggers 3% Gain +# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain +# stop-loss: 2% ROI: 3% tc7 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], @@ -143,9 +137,8 @@ tc7 = BTContainer(data=[ ) -# Test 8 - trailing_stop should raise so candle 3 causes a stoploss. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC8: Trailing stoploss - stoploss should be adjusted candle 2 +# Test 8: trailing_stop should raise so candle 3 causes a stoploss. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2 tc8 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -158,10 +151,8 @@ tc8 = BTContainer(data=[ ) -# Test 9 - trailing_stop should raise - high and low in same candle. -# Candle Data for test 9 -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC9: Trailing stoploss - stoploss should be adjusted candle 3 +# Test 9: trailing_stop should raise - high and low in same candle. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3 tc9 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -173,10 +164,9 @@ tc9 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) -# Test 10 - trailing_stop should raise so candle 3 causes a stoploss +# Test 10: trailing_stop should raise so candle 3 causes a stoploss # without applying trailing_stop_positive since stoploss_offset is at 10%. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC10: Trailing stoploss - stoploss should be adjusted candle 2 +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc10 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -190,10 +180,9 @@ tc10 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] ) -# Test 11 - trailing_stop should raise so candle 3 causes a stoploss +# Test 11: trailing_stop should raise so candle 3 causes a stoploss # applying a positive trailing stop of 3% since stop_positive_offset is reached. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC11: Trailing stoploss - stoploss should be adjusted candle 2, +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc11 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -207,10 +196,9 @@ tc11 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) -# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle +# Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle # applying a positive trailing stop of 3% since stop_positive_offset is reached. -# Set stop-loss at 10%, ROI at 10% (should not apply) -# TC12: Trailing stoploss - stoploss should be adjusted candle 2, +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc12 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -224,8 +212,8 @@ tc12 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 13 - Buy and sell ROI on same candle -# Set stop-loss at 10% (should not apply), ROI at 1% +# Test 13: Buy and sell ROI on same candle +# stop-loss: 10% (should not apply), ROI: 1% tc13 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -233,14 +221,12 @@ tc13 = BTContainer(data=[ [2, 5100, 5251, 4850, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], - stop_loss=-0.10, roi=0.01, profit_perc=0.01, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.10, roi=0.01, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] ) # Test 14 - Buy and Stoploss on same candle -# Set stop-loss at 5%, ROI at 10% (should not apply) +# stop-loss: 5%, ROI: 10% (should not apply) tc14 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -248,15 +234,13 @@ tc14 = BTContainer(data=[ [2, 5100, 5251, 4850, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], - stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.05, roi=0.10, profit_perc=-0.05, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] ) # Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle -# Set stop-loss at 5%, ROI at 10% (should not apply) +# stop-loss: 5%, ROI: 10% (should not apply) tc15 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -264,9 +248,7 @@ tc15 = BTContainer(data=[ [2, 5100, 5251, 4650, 5100, 6172, 0, 0], [3, 4850, 5050, 4850, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], - stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trailing_stop=False, - trailing_only_offset_is_reached=False, trailing_stop_positive_offset=0.05, - trailing_stop_positive=0.03, + stop_loss=-0.05, roi=0.01, profit_perc=-0.04, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] ) From 0376630f7ad156ec631d9da7ee03fa078022a922 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Aug 2019 20:25:20 +0200 Subject: [PATCH 897/928] Update urllib to latest version --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2e52b84ad..475031660 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -6,7 +6,7 @@ python-telegram-bot==11.1.0 arrow==0.14.3 cachetools==3.1.1 requests==2.22.0 -urllib3==1.24.2 # pyup: ignore +urllib3==1.25.3 wrapt==1.11.2 scikit-learn==0.21.2 joblib==0.13.2 From 988a0245c28ca402186cd99c6f428eafaa719026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2019 20:27:42 +0200 Subject: [PATCH 898/928] Update install-script to use parameter Use --prefix /usr/local for install-script too --- .travis.yml | 14 +++++--------- build_helpers/install_ta-lib.sh | 10 ++++++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index b44fef7a1..a452d245b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,15 +10,11 @@ services: env: global: - IMAGE_NAME=freqtradeorg/freqtrade -addons: - apt: - packages: - - libelf-dev - - libdw-dev - - binutils-dev install: -- cd build_helpers && ./install_ta-lib.sh; cd .. -- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH +- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. +- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH +- export TA_LIBRARY_PATH=${HOME}/dependencies/lib +- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include - pip install -r requirements-dev.txt - pip install -e . jobs: @@ -55,4 +51,4 @@ notifications: cache: pip: True directories: - - /usr/local/lib/ + - $HOME/dependencies diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 9fe341bba..cb86e5f64 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -1,8 +1,14 @@ -if [ ! -f "/usr/local/lib/libta_lib.a" ]; then +if [ -z "$1" ]; then + INSTALL_LOC=/usr/local +else + INSTALL_LOC=${1} +fi +echo "Installing to ${INSTALL_LOC}" +if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && ./configure \ + && ./configure --prefix=${INSTALL_LOC}/ \ && make \ && which sudo && sudo make install || make install \ && cd .. From 9d471f3c9a2260bfac1be01e4655fdf0226fe4f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:27:38 +0200 Subject: [PATCH 899/928] Fix documentation for strategy-list --- docs/backtesting.md | 10 +++++++++- docs/bot-usage.md | 2 +- freqtrade/configuration/cli_options.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 179bcee15..7e9f7ff53 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -57,7 +57,15 @@ freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 freqtrade -s TestStrategy backtesting ``` -Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory +Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory. + +#### Comparing multiple Strategies + +```bash +freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m +``` + +Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies. #### Exporting trades to file diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 8877ed010..0ca2f3cc5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -165,7 +165,7 @@ optional arguments: number). -l, --live Use live data. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] - Provide a commaseparated list of strategies to + Provide a space-separated list of strategies to backtest Please note that ticker-interval needs to be set either in config or via command line. When using this together with --export trades, the strategy-name diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 04fde2051..04554c386 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -130,7 +130,7 @@ AVAILABLE_CLI_OPTIONS = { ), "strategy_list": Arg( '--strategy-list', - help='Provide a comma-separated list of strategies to backtest. ' + help='Provide a space-separated list of strategies to backtest. ' 'Please note that ticker-interval needs to be set either in config ' 'or via command line. When using this together with `--export trades`, ' 'the strategy-name is injected into the filename ' From 7e91a0f4a85c0a6e66222e7a9245cf9a011eb755 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:45:44 +0200 Subject: [PATCH 900/928] Fail gracefully if ticker-interval is not set --- freqtrade/optimize/backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429633f31..43b44e376 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,8 +10,8 @@ from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame -from tabulate import tabulate +from freqtrade import OperationalException from freqtrade.configuration import Arguments from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider @@ -21,6 +21,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import IStrategy, SellType +from tabulate import tabulate logger = logging.getLogger(__name__) @@ -88,6 +89,9 @@ class Backtesting(object): Load strategy into backtesting """ self.strategy = strategy + if "ticker_interval" not in self.config: + raise OperationalException("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`") self.ticker_interval = self.config.get('ticker_interval') self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) From 81f773054de3e314908654a777c41f980eb2e8b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 06:56:08 +0200 Subject: [PATCH 901/928] Add test to verify ticker_inteval is set --- freqtrade/tests/optimize/test_backtesting.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 37757743e..1ae1969f4 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -9,7 +9,7 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import DependencyException, constants +from freqtrade import DependencyException, OperationalException, constants from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi @@ -21,7 +21,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) @@ -345,6 +346,23 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert not backtesting.strategy.order_types["stoploss_on_exchange"] +def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None: + """ + Check that stoploss_on_exchange is set to False while backtesting + since backtesting assumes a perfect stoploss anyway. + """ + patch_exchange(mocker) + del default_conf['ticker_interval'] + default_conf['strategy_list'] = ['DefaultStrategy', + 'TestStrategy'] + + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + with pytest.raises(OperationalException): + Backtesting(default_conf) + log_has("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`", caplog.record_tuples) + + def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) From a90ced1f381391a9a7b69d2098460ebd88c27e0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 20:09:09 +0200 Subject: [PATCH 902/928] Since arguments are in milliseconds integer throughout ccxt. Explained here: https://github.com/ccxt/ccxt/issues/5636 fixes #2093 --- freqtrade/exchange/exchange.py | 3 ++- freqtrade/tests/exchange/test_exchange.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 37bbb778e..657f382d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -725,7 +725,8 @@ class Exchange(object): return [] try: # Allow 5s offset to catch slight time offsets (discovered in #1185) - my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5) + # since needs to be int in milliseconds + my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 2f9e525dd..559a3bd50 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1391,6 +1391,13 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) assert len(orders) == 1 assert orders[0]['price'] == 165 + assert api_mock.fetch_my_trades.call_count == 1 + # since argument should be + assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) + assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' + # Same test twice, hardcoded number and doing the same calculation + assert api_mock.fetch_my_trades.call_args[0][1] == 1525471195000 + assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_trades_for_order', 'fetch_my_trades', From caf4580346ef654d58421c4dd02a1eefb58f0264 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Aug 2019 20:23:32 +0200 Subject: [PATCH 903/928] Use UTC Timezone for test --- freqtrade/tests/exchange/test_exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 559a3bd50..ebe5ad9df 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -2,7 +2,7 @@ # pragma pylint: disable=protected-access import copy import logging -from datetime import datetime +from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock @@ -11,8 +11,8 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade import (DependencyException, OperationalException, - TemporaryError, InvalidOrderException) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.exchange import API_RETRY_COUNT from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -1361,7 +1361,7 @@ def test_name(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name): order_id = 'ABCD-ABCD' - since = datetime(2018, 5, 5) + since = datetime(2018, 5, 5, tzinfo=timezone.utc) default_conf["dry_run"] = False mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) api_mock = MagicMock() @@ -1396,7 +1396,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' # Same test twice, hardcoded number and doing the same calculation - assert api_mock.fetch_my_trades.call_args[0][1] == 1525471195000 + assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, From 8418dfbaed20a4794addff924585e1c2149cfbf3 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Tue, 6 Aug 2019 22:35:14 -0400 Subject: [PATCH 904/928] edits for jupyter notebook example --- .gitignore | 4 +- docs/data-analysis.md | 168 ++++++------------ environment.yml | 1 + user_data/notebooks/.gitkeep | 0 user_data/notebooks/analysis_example.ipynb | 187 +++++++++++++++++++++ 5 files changed, 239 insertions(+), 121 deletions(-) create mode 100644 user_data/notebooks/.gitkeep create mode 100644 user_data/notebooks/analysis_example.ipynb diff --git a/.gitignore b/.gitignore index 9ed046c40..3a9df9852 100644 --- a/.gitignore +++ b/.gitignore @@ -81,7 +81,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -*.ipynb # pyenv .python-version @@ -93,3 +92,6 @@ target/ .pytest_cache/ .mypy_cache/ + +#exceptions +!user_data/noteboks/*example.ipynb diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2b6d6ed58..826c34747 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,164 +1,92 @@ # Analyzing bot data -After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated. +You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). -A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. +## Strategy debugging example -The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. +Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. -## Strategy development problem analysis - -Debugging a strategy (are there no buy signals, ...) can be very time-consuming. -FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. - -It's recommended using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. - -The following is a full code-snippet, which will be explained by both comments, and step by step below. +### Import requirements and define variables used in the script ```python -# Some necessary imports +# Imports from pathlib import Path - +import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver +from freqtrade.data.btanalysis import load_backtest_data +from freqtrade.data.btanalysis import load_trades_from_db + # Define some constants -ticker_interval = "5m" - +ticker_interval = "1m" # Name of the strategy class -strategyname = 'Awesomestrategy' +strategy_name = 'NewStrategy' +# Path to user data +user_data_dir = 'user_data' # Location of the strategy -strategy_location = '../xmatt/strategies' +strategy_location = os.path.join(user_data_dir, 'strategies') # Location of the data -data_location = '../freqtrade/user_data/data/binance/' +data_location = os.path.join(user_data_dir, 'data', 'binance') +# Pair to analyze # Only use one pair here -pair = "XRP_ETH" +pair = "BTC_USDT" +``` -### End constants +### Load exchange data -# Load data +```python +# Load data using values set above bt_data = load_pair_history(datadir=Path(data_location), - ticker_interval = ticker_interval, + ticker_interval=ticker_interval, pair=pair) -print(len(bt_data)) - -### Start strategy reload -# Load strategy - best done in a new cell -# Rerun each time the strategy-file is changed. -strategy = StrategyResolver({'strategy': strategyname, - 'user_data_dir': Path.cwd(), - 'strategy_path': location}).strategy - -# Run strategy (just like in backtesting) -df = strategy.analyze_ticker(bt_data, {'pair': pair}) -print(f"Generated {df['buy'].sum()} buy signals") - -# Reindex data to be "nicer" and show data -data = df.set_index('date', drop=True) -data.tail() +# Confirm success +print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_location}") ``` -### Explanation +### Load and run strategy -#### Imports and constant definition +* Rerun each time the strategy file is changed +* Display the trade details. Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. -``` python -# Some necessary imports -from pathlib import Path - -from freqtrade.data.history import load_pair_history -from freqtrade.resolvers import StrategyResolver -# Define some constants -ticker_interval = "5m" - -# Name of the strategy class -strategyname = 'Awesomestrategy' -# Location of the strategy -strategy_location = 'user_data/strategies' -# Location of the data -data_location = 'user_data/data/binance' -# Only use one pair here -pair = "XRP_ETH" -``` - -This first section imports necessary modules, and defines some constants you'll probably need to adjust for your case. - -#### Load candles - -``` python -# Load data -bt_data = load_pair_history(datadir=Path(data_location), - ticker_interval = ticker_interval, - pair=pair) -print(len(bt_data)) -``` - -This second section loads the historic data and prints the amount of candles in the DataFrame. -You can also inspect this dataframe by using `bt_data.head()` or `bt_data.tail()`. - -#### Run strategy and analyze results - -Now, it's time to load and run your strategy. -For this, I recommend using a new cell in your notebook, since you'll want to repeat this until you're satisfied with your strategy. - -``` python -# Load strategy - best done in a new cell -# Needs to be ran each time the strategy-file is changed. -strategy = StrategyResolver({'strategy': strategyname, - 'user_data_dir': Path.cwd(), - 'strategy_path': location}).strategy - -# Run strategy (just like in backtesting) -df = strategy.analyze_ticker(bt_data, {'pair': pair}) -print(f"Generated {df['buy'].sum()} buy signals") - -# Reindex data to be "nicer" and show data -data = df.set_index('date', drop=True) -data.tail() -``` - -The code snippet loads and analyzes the strategy, calculates and prints the number of buy signals. - -The last 2 lines serve to analyze the dataframe in detail. -This can be important if your strategy did not generate any buy signals. -Note that using `data.head()` would also work, however this is misleading since most indicators have some "startup" time at the start of a backtested dataframe. - -There can be many things wrong, some signs to look for are: +Some possible problems: * Columns with NaN values at the end of the dataframe * Columns used in `crossed*()` functions with completely different units -## Backtesting +```python +# Load strategy using values set above +strategy = StrategyResolver({'strategy': strategy_name, + 'user_data_dir': user_data_dir, + 'strategy_path': strategy_location}).strategy -To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). -You can then load the trades to perform further analysis. +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) -Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter. +# Report results +print(f"Generated {df['buy'].sum()} buy signals") +data = df.set_index('date', drop=True) +data.tail() +``` -``` python -from freqtrade.data.btanalysis import load_backtest_data -df = load_backtest_data("user_data/backtest-result.json") +### Load backtest results into a pandas dataframe + +```python +# Load backtest results +df = load_backtest_data("user_data/backtest_data/backtest-result.json") # Show value-counts per pair df.groupby("pair")["sell_reason"].value_counts() - ``` -This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. - -If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it. - -## Live data - -To analyze the trades your bot generated, you can load them to a DataFrame as follows: +### Load live trading results into a pandas dataframe ``` python -from freqtrade.data.btanalysis import load_trades_from_db - +# Fetch trades from database df = load_trades_from_db("sqlite:///tradesv3.sqlite") +# Display results df.groupby("pair")["sell_reason"].value_counts() - ``` Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/environment.yml b/environment.yml index 8f43b6991..cd3350fd5 100644 --- a/environment.yml +++ b/environment.yml @@ -37,6 +37,7 @@ dependencies: - coveralls - mypy # Useful for jupyter + - jupyter - ipykernel - isort - yapf diff --git a/user_data/notebooks/.gitkeep b/user_data/notebooks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb new file mode 100644 index 000000000..f9c9b27bc --- /dev/null +++ b/user_data/notebooks/analysis_example.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy debugging example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change directory\n", + "# Define all paths relative to the project root shown in the cell output\n", + "import os\n", + "try:\n", + "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", + "\tprint(os.getcwd())\n", + "except:\n", + "\tpass\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import requirements and define variables used in the script" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "from pathlib import Path\n", + "import os\n", + "from freqtrade.data.history import load_pair_history\n", + "from freqtrade.resolvers import StrategyResolver\n", + "from freqtrade.data.btanalysis import load_backtest_data\n", + "from freqtrade.data.btanalysis import load_trades_from_db\n", + "\n", + "# Define some constants\n", + "ticker_interval = \"1m\"\n", + "# Name of the strategy class\n", + "strategy_name = 'NewStrategy'\n", + "# Path to user data\n", + "user_data_dir = 'user_data'\n", + "# Location of the strategy\n", + "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "# Location of the data\n", + "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "# Pair to analyze \n", + "# Only use one pair here\n", + "pair = \"BTC_USDT\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load exchange data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load data using values set above\n", + "bt_data = load_pair_history(datadir=Path(data_location),\n", + " ticker_interval=ticker_interval,\n", + " pair=pair)\n", + "\n", + "# Confirm success\n", + "print(\"Loaded \" + str(len(bt_data)) + f\" rows of data for {pair} from {data_location}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load and run strategy \n", + "\n", + "* Rerun each time the strategy file is changed\n", + "* Display the trade details. Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", + "\n", + "Some possible problems:\n", + "\n", + "* Columns with NaN values at the end of the dataframe\n", + "* Columns used in `crossed*()` functions with completely different units" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load strategy using values set above\n", + "strategy = StrategyResolver({'strategy': strategy_name,\n", + " 'user_data_dir': user_data_dir,\n", + " 'strategy_path': strategy_location}).strategy\n", + "\n", + "# Run strategy (just like in backtesting)\n", + "df = strategy.analyze_ticker(bt_data, {'pair': pair})\n", + "\n", + "# Report results\n", + "print(f\"Generated {df['buy'].sum()} buy signals\")\n", + "data = df.set_index('date', drop=True)\n", + "data.tail()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load backtest results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtest results\n", + "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", + "\n", + "# Show value-counts per pair\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load live trading results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch trades from database\n", + "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", + "\n", + "# Display results\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From dfce20203436e01cf666b89f56a16e6e95f7e1a1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:15 +0000 Subject: [PATCH 905/928] Update mkdocs-material from 3.1.0 to 4.4.0 --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c4d8c2cae..ce76d52e5 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1 +1 @@ -mkdocs-material==3.1.0 \ No newline at end of file +mkdocs-material==4.4.0 \ No newline at end of file From 33bc8a24049836d9dadafec80f9212f964f84c03 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:19 +0000 Subject: [PATCH 906/928] Update ccxt from 1.18.1008 to 1.18.1021 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e5ef27a38..1413539c3 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.1008 +ccxt==1.18.1021 SQLAlchemy==1.3.6 python-telegram-bot==11.1.0 arrow==0.14.4 From 5864968ce91882fa45a311c447ac4bc4c9f446d4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 7 Aug 2019 07:02:25 +0000 Subject: [PATCH 907/928] Update plotly from 4.0.0 to 4.1.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index a6753fc3f..f10bfac3f 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.0.0 +plotly==4.1.0 From 7d02580a2bb359ea1c793c658dbbe857cf0a5a0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:03:03 +0200 Subject: [PATCH 908/928] setup.sh script shall fail if venv initialization fails --- setup.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.sh b/setup.sh index fe7110ef6..908e77767 100755 --- a/setup.sh +++ b/setup.sh @@ -39,15 +39,13 @@ function updateenv() { echo "-------------------------" source .env/bin/activate echo "pip install in-progress. Please wait..." - # Install numpy first to have py_find_1st install clean - ${PYTHON} -m pip install --upgrade pip numpy - ${PYTHON} -m pip install --upgrade -r requirements.txt - + ${PYTHON} -m pip install --upgrade pip read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then ${PYTHON} -m pip install --upgrade -r requirements-dev.txt else + ${PYTHON} -m pip install --upgrade -r requirements.txt echo "Dev dependencies ignored." fi @@ -90,7 +88,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install build-essential autoconf libtool pkg-config make wget git + sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git install_talib } @@ -129,6 +127,10 @@ function reset() { echo ${PYTHON} -m venv .env + if [ $? -ne 0 ]; then + echo "Could not create virtual environment. Leaving now" + exit 1 + fi updateenv } From cc4900f66cc8cc6298fe6b3dde250ed67e783b0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:19:16 +0200 Subject: [PATCH 909/928] Doublecheck if virtualenv IS present --- setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.sh b/setup.sh index 908e77767..2ca6e4460 100755 --- a/setup.sh +++ b/setup.sh @@ -37,6 +37,10 @@ function updateenv() { echo "-------------------------" echo "Updating your virtual env" echo "-------------------------" + if [ ! -f .env/bin/activate ]; then + echo "Something went wrong, no virtual environment found." + exit 1 + fi source .env/bin/activate echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip From 757538f1147a605f1f246821abdbfee0ac50bb9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:35:52 +0200 Subject: [PATCH 910/928] Run ldconfig to add /usr/local/lib to path --- setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.sh b/setup.sh index 2ca6e4460..9bdafec0d 100755 --- a/setup.sh +++ b/setup.sh @@ -72,6 +72,10 @@ function install_talib() { ./configure --prefix=/usr/local make sudo make install + if [ -x "$(command -v apt-get)" ]; then + echo "Updating library path using ldconfig" + sudo ldconfig + fi cd .. && rm -rf ./ta-lib/ cd .. } From 831e708897edf62f2950f97aa67d1ac89b2cdfdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:44:47 +0200 Subject: [PATCH 911/928] Detect virtualenv and quit in that case --- setup.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.sh b/setup.sh index 9bdafec0d..d936e20f1 100755 --- a/setup.sh +++ b/setup.sh @@ -11,6 +11,12 @@ function check_installed_pip() { # Check which python version is installed function check_installed_python() { + if [ -n "${VIRTUAL_ENV}" ]; then + echo "Please deactivate your virtual environment before running setup.sh." + echo "You can do this by running 'deactivate'." + exit 2 + fi + which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" From 7a47d81b7b75f42480bf8af7b4e2f440ae6fe67a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Aug 2019 21:45:58 +0200 Subject: [PATCH 912/928] Ensure git reset --hard is realy desired --- setup.sh | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/setup.sh b/setup.sh index d936e20f1..c4b6e074a 100755 --- a/setup.sh +++ b/setup.sh @@ -117,28 +117,33 @@ function reset() { echo "----------------------------" echo "Reseting branch and virtual env" echo "----------------------------" + if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] then - if [ -d ".env" ]; then - echo "- Delete your previous virtual env" - rm -rf .env - fi - git fetch -a + read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]]; then - if [ "1" == $(git branch -vv |grep -c "* develop") ] - then - echo "- Hard resetting of 'develop' branch." - git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* master") ] - then - echo "- Hard resetting of 'master' branch." - git reset --hard origin/master + git fetch -a + + if [ "1" == $(git branch -vv |grep -c "* develop") ] + then + echo "- Hard resetting of 'develop' branch." + git reset --hard origin/develop + elif [ "1" == $(git branch -vv |grep -c "* master") ] + then + echo "- Hard resetting of 'master' branch." + git reset --hard origin/master + fi fi else echo "Reset ignored because you are not on 'master' or 'develop'." fi + if [ -d ".env" ]; then + echo "- Delete your previous virtual env" + rm -rf .env + fi echo ${PYTHON} -m venv .env if [ $? -ne 0 ]; then From 9df1c23c71639bd95a171b861ac604ae58d769a4 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Wed, 7 Aug 2019 19:48:55 -0400 Subject: [PATCH 913/928] changed Path, added jupyter --- setup.py | 2 ++ user_data/notebooks/analysis_example.ipynb | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 202e3fd0d..b46101c0f 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,8 @@ setup(name='freqtrade', 'dev': all_extra, 'plot': plot, 'all': all_extra, + 'jupyter': [], + }, include_package_data=True, zip_safe=False, diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index f9c9b27bc..298269c7a 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -20,7 +20,7 @@ "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", "\tprint(os.getcwd())\n", "except:\n", - "\tpass\n" + "\tpass" ] }, { @@ -38,7 +38,6 @@ "source": [ "# Imports\n", "from pathlib import Path\n", - "import os\n", "from freqtrade.data.history import load_pair_history\n", "from freqtrade.resolvers import StrategyResolver\n", "from freqtrade.data.btanalysis import load_backtest_data\n", @@ -51,9 +50,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "strategy_location = Path(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "data_location = Path(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" From 2bc67b4a96876cefd55efc6ded6363e94968c670 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Wed, 7 Aug 2019 20:47:37 -0400 Subject: [PATCH 914/928] missed a call of os.path. removed it. --- user_data/notebooks/analysis_example.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index 298269c7a..d14575e97 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -16,8 +16,9 @@ "# Change directory\n", "# Define all paths relative to the project root shown in the cell output\n", "import os\n", + "from pathlib import Path\n", "try:\n", - "\tos.chdir(os.path.join(os.getcwd(), '../..'))\n", + "\tos.chdir(Path(os.getcwd(), '../..'))\n", "\tprint(os.getcwd())\n", "except:\n", "\tpass" From 0d4a2c6c3af13d614cca871661fd2a4daeab4f64 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 8 Aug 2019 22:45:37 +0300 Subject: [PATCH 915/928] advanced sample hyperopt added; changes to helpstrings --- freqtrade/optimize/default_hyperopt.py | 3 +- user_data/hyperopts/sample_hyperopt.py | 16 +- .../hyperopts/sample_hyperopt_advanced.py | 261 ++++++++++++++++++ 3 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 user_data/hyperopts/sample_hyperopt_advanced.py diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 1d4c40916..e05dfc95c 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -13,10 +13,9 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt class DefaultHyperOpts(IHyperOpt): """ - Default hyperopt provided by freqtrade bot. + Default hyperopt provided by the Freqtrade bot. You can override it with your own hyperopt """ - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 9a755e52e..1a3823afa 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -14,25 +14,27 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -# This class is a sample. Feel free to customize it. class SampleHyperOpts(IHyperOpt): """ - This is a test hyperopt to inspire you. + This is a sample hyperopt to inspire you. + Feel free to customize it. More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md - You can: - - Rename the class name. + You should: + - Rename the class name to some unique name. - Add any methods you want to build your hyperopt. - Add any lib you need to build your hyperopt. You must keep: - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. - The roi_space, generate_roi_table, stoploss_space methods were moved to the parent class, you may - override them here if you need it. + The roi_space, generate_roi_table, stoploss_space methods are no longer required to be + copied in every custom hyperopt. However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + Sample implementation of these methods can be found in + https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py """ - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py new file mode 100644 index 000000000..00062a58d --- /dev/null +++ b/user_data/hyperopts/sample_hyperopt_advanced.py @@ -0,0 +1,261 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from functools import reduce +from math import exp +from typing import Any, Callable, Dict, List +from datetime import datetime + +import numpy as np# noqa F401 +import talib.abstract as ta +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +class AdvancedSampleHyperOpts(IHyperOpt): + """ + This is a sample hyperopt to inspire you. + Feel free to customize it. + + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md + + You should: + - Rename the class name to some unique name. + - Add any methods you want to build your hyperopt. + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The roi_space, generate_roi_table, stoploss_space methods are no longer required to be + copied in every custom hyperopt. However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + + This sample illustrates how to override these methods. + """ + @staticmethod + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['adx'] = ta.ADX(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['mfi'] = ta.MFI(dataframe) + dataframe['rsi'] = ta.RSI(dataframe) + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe['sar'] = ta.SAR(dataframe) + return dataframe + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by hyperopt + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use + """ + conditions = [] + # GUARDS AND TRENDS + if 'mfi-enabled' in params and params['mfi-enabled']: + conditions.append(dataframe['mfi'] < params['mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + conditions.append(dataframe['fastd'] < params['fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend + + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + + @staticmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the sell strategy parameters to be used by hyperopt + """ + def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Sell strategy Hyperopt will build and use + """ + # print(params) + conditions = [] + # GUARDS AND TRENDS + if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + if 'sell-adx-enabled' in params and params['sell-adx-enabled']: + conditions.append(dataframe['adx'] < params['sell-adx-value']) + if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + + # TRIGGERS + if 'sell-trigger' in params: + if params['sell-trigger'] == 'sell-bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['sell-trigger'] == 'sell-sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + + return dataframe + + return populate_sell_trend + + @staticmethod + def sell_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching sell strategy parameters + """ + return [ + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + Categorical([True, False], name='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') + ] + + @staticmethod + def generate_roi_table(params: Dict) -> Dict[int, float]: + """ + Generate the ROI table that will be used by Hyperopt + + This implementation generates the default legacy Freqtrade ROI tables. + + Change it if you need different number of steps in the generated + ROI tables or other structure of the ROI tables. + + Please keep it aligned with parameters in the 'roi' optimization + hyperspace defined by the roi_space method. + """ + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table + + @staticmethod + def roi_space() -> List[Dimension]: + """ + Values to search for each ROI steps + + Override it if you need some different ranges for the parameters in the + 'roi' optimization hyperspace. + + Please keep it aligned with the implementation of the + generate_roi_table method. + """ + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + Real(0.01, 0.04, name='roi_p1'), + Real(0.01, 0.07, name='roi_p2'), + Real(0.01, 0.20, name='roi_p3'), + ] + + @staticmethod + def stoploss_space() -> List[Dimension]: + """ + Stoploss Value to search + + Override it if you need some different range for the parameter in the + 'stoploss' optimization hyperspace. + """ + return [ + Real(-0.5, -0.02, name='stoploss'), + ] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of from strategy + must align to populate_indicators in this file + Only used when --spaces does not include buy + """ + dataframe.loc[ + ( + (dataframe['close'] < dataframe['bb_lowerband']) & + (dataframe['mfi'] < 16) & + (dataframe['adx'] > 25) & + (dataframe['rsi'] < 21) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of from strategy + must align to populate_indicators in this file + Only used when --spaces does not include sell + """ + dataframe.loc[ + ( + (qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) & + (dataframe['fastd'] > 54) + ), + 'sell'] = 1 + return dataframe From 8ad5afd3a1057ec4d118aef9ce21eb9d08851603 Mon Sep 17 00:00:00 2001 From: Cedric Schmeits Date: Thu, 8 Aug 2019 22:10:51 +0200 Subject: [PATCH 916/928] As -sharp_ratio is returned the value should be nagative. This leads in a high positive result of the loss function, as it is a minimal optimizer --- freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index f74b27744..5631a75de 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -39,7 +39,7 @@ class SharpeHyperOptLoss(IHyperOptLoss): sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) else: # Define high (negative) sharpe ratio to be clear that this is NOT optimal. - sharp_ratio = 20. + sharp_ratio = -20. # print(expected_yearly_return, np.std(total_profit), sharp_ratio) return -sharp_ratio From ccf3c6987435827af3649d19909fe78a0a3f4f25 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Thu, 8 Aug 2019 22:09:15 -0400 Subject: [PATCH 917/928] edits to clarify backtesting analysis --- docs/data-analysis.md | 70 +++++--- setup.py | 12 +- user_data/notebooks/analysis_example.ipynb | 195 +++++++++++++-------- 3 files changed, 176 insertions(+), 101 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 826c34747..3b07d77a6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -2,11 +2,33 @@ You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). +## Example snippets + +### Load backtest results into a pandas dataframe + +```python +# Load backtest results +df = load_backtest_data("user_data/backtest_data/backtest-result.json") + +# Show value-counts per pair +df.groupby("pair")["sell_reason"].value_counts() +``` + +### Load live trading results into a pandas dataframe + +``` python +# Fetch trades from database +df = load_trades_from_db("sqlite:///tradesv3.sqlite") + +# Display results +df.groupby("pair")["sell_reason"].value_counts() +``` + ## Strategy debugging example Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. -### Import requirements and define variables used in the script +### Import requirements and define variables used in analyses ```python # Imports @@ -47,12 +69,6 @@ print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_loca ### Load and run strategy * Rerun each time the strategy file is changed -* Display the trade details. Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. - -Some possible problems: - -* Columns with NaN values at the end of the dataframe -* Columns used in `crossed*()` functions with completely different units ```python # Load strategy using values set above @@ -60,33 +76,31 @@ strategy = StrategyResolver({'strategy': strategy_name, 'user_data_dir': user_data_dir, 'strategy_path': strategy_location}).strategy -# Run strategy (just like in backtesting) +# Generate buy/sell signals using strategy df = strategy.analyze_ticker(bt_data, {'pair': pair}) +``` +### Display the trade details + +* Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. + +#### Some possible problems + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + +#### Comparison with full backtest + +having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting. + +Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). +The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. + +```python # Report results print(f"Generated {df['buy'].sum()} buy signals") data = df.set_index('date', drop=True) data.tail() ``` -### Load backtest results into a pandas dataframe - -```python -# Load backtest results -df = load_backtest_data("user_data/backtest_data/backtest-result.json") - -# Show value-counts per pair -df.groupby("pair")["sell_reason"].value_counts() -``` - -### Load live trading results into a pandas dataframe - -``` python -# Fetch trades from database -df = load_trades_from_db("sqlite:///tradesv3.sqlite") - -# Display results -df.groupby("pair")["sell_reason"].value_counts() -``` - Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/setup.py b/setup.py index b46101c0f..6ac7a117f 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,15 @@ develop = [ 'pytest-random-order', ] -all_extra = api + plot + develop +jupyter = [ + 'jupyter', + 'nbstripout', + 'ipykernel', + 'isort', + 'yapf', + ] + +all_extra = api + plot + develop + jupyter setup(name='freqtrade', version=__version__, @@ -68,7 +76,7 @@ setup(name='freqtrade', 'dev': all_extra, 'plot': plot, 'all': all_extra, - 'jupyter': [], + 'jupyter': jupyter, }, include_package_data=True, diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index d14575e97..b3a61bc51 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -4,31 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Strategy debugging example" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Change directory\n", - "# Define all paths relative to the project root shown in the cell output\n", - "import os\n", - "from pathlib import Path\n", - "try:\n", - "\tos.chdir(Path(os.getcwd(), '../..'))\n", - "\tprint(os.getcwd())\n", - "except:\n", - "\tpass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Import requirements and define variables used in the script" + "# Analyzing bot data\n", + "\n", + "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation)." ] }, { @@ -39,11 +17,97 @@ "source": [ "# Imports\n", "from pathlib import Path\n", + "import os\n", "from freqtrade.data.history import load_pair_history\n", "from freqtrade.resolvers import StrategyResolver\n", "from freqtrade.data.btanalysis import load_backtest_data\n", - "from freqtrade.data.btanalysis import load_trades_from_db\n", + "from freqtrade.data.btanalysis import load_trades_from_db" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change directory\n", + "# Define all paths relative to the project root shown in the cell output\n", + "try:\n", + "\tos.chdir(Path(Path.cwd(), '../..'))\n", + "\tprint(Path.cwd())\n", + "except:\n", + "\tpass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example snippets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load backtest results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtest results\n", + "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", "\n", + "# Show value-counts per pair\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load live trading results into a pandas dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch trades from database\n", + "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", + "\n", + "# Display results\n", + "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Strategy debugging example\n", + "\n", + "Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import requirements and define variables used in analyses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Define some constants\n", "ticker_interval = \"1m\"\n", "# Name of the strategy class\n", @@ -51,9 +115,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = Path(user_data_dir, 'strategies')\n", + "strategy_location = os.path.join(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = Path(user_data_dir, 'data', 'binance')\n", + "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" @@ -85,15 +149,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Load and run strategy \n", - "\n", - "* Rerun each time the strategy file is changed\n", - "* Display the trade details. Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", - "\n", - "Some possible problems:\n", - "\n", - "* Columns with NaN values at the end of the dataframe\n", - "* Columns used in `crossed*()` functions with completely different units" + "### Load and run strategy\n", + "* Rerun each time the strategy file is changed" ] }, { @@ -107,53 +164,49 @@ " 'user_data_dir': user_data_dir,\n", " 'strategy_path': strategy_location}).strategy\n", "\n", - "# Run strategy (just like in backtesting)\n", - "df = strategy.analyze_ticker(bt_data, {'pair': pair})\n", + "# Generate buy/sell signals using strategy\n", + "df = strategy.analyze_ticker(bt_data, {'pair': pair})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display the trade details\n", + "* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", "\n", + "#### Some possible problems\n", + "\n", + "* Columns with NaN values at the end of the dataframe\n", + "* Columns used in `crossed*()` functions with completely different units\n", + "\n", + "#### Comparison with full backtest\n", + "\n", + "having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n", + "\n", + "Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29).\n", + "The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Report results\n", "print(f\"Generated {df['buy'].sum()} buy signals\")\n", "data = df.set_index('date', drop=True)\n", "data.tail()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load backtest results into a pandas dataframe" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Load backtest results\n", - "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n", - "\n", - "# Show value-counts per pair\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load live trading results into a pandas dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Fetch trades from database\n", - "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", - "\n", - "# Display results\n", - "df.groupby(\"pair\")[\"sell_reason\"].value_counts()" + "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." ] } ], From 15cf5ac2d74a05cf83df56a4f9a9978e6e5f78bf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 9 Aug 2019 09:31:30 +0300 Subject: [PATCH 918/928] docs improved --- docs/hyperopt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0304c3188..6ef68d82f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -350,7 +350,7 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Understand Hyperopt ROI results -If you are optimizing ROI, your result will look as follows and include a ROI table: +If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table: ``` Best result: @@ -396,7 +396,7 @@ Override the `generate_roi_table()` and `roi_space()` methods and implement your ### Understand Hyperopt Stoploss results -If you are optimizing stoploss values, your result will look as follows and include stoploss: +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss: ``` Best result: From 51d59e673b277c5bd0fa4bb343b99feea8c6453c Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:36:53 -0400 Subject: [PATCH 919/928] fixed another instance of Path in docs and nb --- docs/data-analysis.md | 4 ++-- user_data/notebooks/analysis_example.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 3b07d77a6..f79d1674e 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -46,9 +46,9 @@ strategy_name = 'NewStrategy' # Path to user data user_data_dir = 'user_data' # Location of the strategy -strategy_location = os.path.join(user_data_dir, 'strategies') +strategy_location = Path(user_data_dir, 'strategies') # Location of the data -data_location = os.path.join(user_data_dir, 'data', 'binance') +data_location = Path(user_data_dir, 'data', 'binance') # Pair to analyze # Only use one pair here pair = "BTC_USDT" diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index b3a61bc51..b8bcdd7bf 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -115,9 +115,9 @@ "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", - "strategy_location = os.path.join(user_data_dir, 'strategies')\n", + "strategy_location = Path(user_data_dir, 'strategies')\n", "# Location of the data\n", - "data_location = os.path.join(user_data_dir, 'data', 'binance')\n", + "data_location = Path(user_data_dir, 'data', 'binance')\n", "# Pair to analyze \n", "# Only use one pair here\n", "pair = \"BTC_USDT\"" From 247d7475e17cff5f18e3dc8e31667cbeac44e3a9 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:41:05 -0400 Subject: [PATCH 920/928] fixes to example notebook. --- user_data/notebooks/analysis_example.ipynb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index b8bcdd7bf..a92855add 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -33,10 +33,10 @@ "# Change directory\n", "# Define all paths relative to the project root shown in the cell output\n", "try:\n", - "\tos.chdir(Path(Path.cwd(), '../..'))\n", - "\tprint(Path.cwd())\n", + " os.chdir(Path(Path.cwd(), '../..'))\n", + " print(Path.cwd())\n", "except:\n", - "\tpass" + " pass" ] }, { @@ -109,7 +109,7 @@ "outputs": [], "source": [ "# Define some constants\n", - "ticker_interval = \"1m\"\n", + "ticker_interval = \"5m\"\n", "# Name of the strategy class\n", "strategy_name = 'NewStrategy'\n", "# Path to user data\n", @@ -201,10 +201,8 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." ] From 3cc772c8e9eead8a11cc7208a84bdaca6f580240 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 11:53:29 -0400 Subject: [PATCH 921/928] added reminders --- docs/data-analysis.md | 2 ++ user_data/notebooks/analysis_example.ipynb | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index f79d1674e..2e2ebc3c6 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -2,6 +2,8 @@ You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). +*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* + ## Example snippets ### Load backtest results into a pandas dataframe diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index a92855add..30e9e1a97 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -6,7 +6,12 @@ "source": [ "# Analyzing bot data\n", "\n", - "You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation)." + "You can analyze the results of backtests and trading history easily using Jupyter notebooks. \n", + "**Copy this file so your changes don't get clobbered with the next freqtrade update!** \n", + "For usage instructions, see [jupyter.org](https://jupyter.org/documentation). \n", + "*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n", + "\n", + "\n" ] }, { From dd35ba5e812589ec1da130681df2fb38ba87cd26 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 17:06:19 -0400 Subject: [PATCH 922/928] added imports to doc code blocks. --- docs/data-analysis.md | 9 +++++---- user_data/notebooks/analysis_example.ipynb | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 2e2ebc3c6..5db9e6c3b 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -9,6 +9,7 @@ You can analyze the results of backtests and trading history easily using Jupyte ### Load backtest results into a pandas dataframe ```python +from freqtrade.data.btanalysis import load_backtest_data # Load backtest results df = load_backtest_data("user_data/backtest_data/backtest-result.json") @@ -19,6 +20,8 @@ df.groupby("pair")["sell_reason"].value_counts() ### Load live trading results into a pandas dataframe ``` python +from freqtrade.data.btanalysis import load_trades_from_db + # Fetch trades from database df = load_trades_from_db("sqlite:///tradesv3.sqlite") @@ -38,13 +41,11 @@ from pathlib import Path import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver -from freqtrade.data.btanalysis import load_backtest_data -from freqtrade.data.btanalysis import load_trades_from_db # Define some constants -ticker_interval = "1m" +ticker_interval = "5m" # Name of the strategy class -strategy_name = 'NewStrategy' +strategy_name = 'AwesomeStrategy' # Path to user data user_data_dir = 'user_data' # Location of the strategy diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb index 30e9e1a97..f5e2c12d7 100644 --- a/user_data/notebooks/analysis_example.ipynb +++ b/user_data/notebooks/analysis_example.ipynb @@ -116,7 +116,7 @@ "# Define some constants\n", "ticker_interval = \"5m\"\n", "# Name of the strategy class\n", - "strategy_name = 'NewStrategy'\n", + "strategy_name = 'AwesomeStrategy'\n", "# Path to user data\n", "user_data_dir = 'user_data'\n", "# Location of the strategy\n", From 8eb39178ea2668af09a272128e3823d30910d059 Mon Sep 17 00:00:00 2001 From: Jonathan Raviotta Date: Fri, 9 Aug 2019 17:24:17 -0400 Subject: [PATCH 923/928] code block instructions. removed extra packages --- docs/data-analysis.md | 5 ++++- setup.py | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 5db9e6c3b..66bb50694 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -42,6 +42,9 @@ import os from freqtrade.data.history import load_pair_history from freqtrade.resolvers import StrategyResolver +# You can override strategy settings as demonstrated below. +# Customize these according to your needs. + # Define some constants ticker_interval = "5m" # Name of the strategy class @@ -66,7 +69,7 @@ bt_data = load_pair_history(datadir=Path(data_location), pair=pair) # Confirm success -print("Loaded " + str(len(bt_data)) + f" rows of data for {pair} from {data_location}") +print(f"Loaded {len(bt_data)} rows of data for {pair} from {data_location}") ``` ### Load and run strategy diff --git a/setup.py b/setup.py index 6ac7a117f..41e1b8f45 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,6 @@ jupyter = [ 'jupyter', 'nbstripout', 'ipykernel', - 'isort', - 'yapf', ] all_extra = api + plot + develop + jupyter From 50c9679e23d9d2fe219d7cc09e5256ffebc66b4b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 14:24:14 +0300 Subject: [PATCH 924/928] move load_config_file() to separate module --- freqtrade/configuration/configuration.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 17ad37d6a..e564c79ce 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -1,9 +1,7 @@ """ This module contains the configuration class """ -import json import logging -import sys import warnings from argparse import Namespace from typing import Any, Callable, Dict, Optional @@ -12,6 +10,7 @@ from freqtrade import OperationalException, 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.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts from freqtrade.state import RunMode @@ -52,24 +51,7 @@ class Configuration(object): logger.info('Using config: %s ...', path) # Merge config options, overwriting old values - config = deep_merge_dicts(self._load_config_file(path), config) - - return config - - def _load_config_file(self, path: str) -> Dict[str, Any]: - """ - Loads a config file from the given path - :param path: path as str - :return: configuration as dictionary - """ - try: - # Read config from stdin if requested in the options - with open(path) if path != '-' else sys.stdin as file: - config = json.load(file) - except FileNotFoundError: - raise OperationalException( - f'Config file "{path}" not found!' - ' Please create a config file or check whether it exists.') + config = deep_merge_dicts(load_config_file(path), config) return config From ad6a249832e2c0429fb534fa648607a705ee0f7d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 15:14:37 +0300 Subject: [PATCH 925/928] download_backtest_data.py adjusted --- scripts/download_backtest_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index ed96cec71..580592294 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -12,6 +12,7 @@ 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 @@ -42,7 +43,7 @@ if args.config: for path in args.config: logger.info(f"Using config: {path}...") # Merge config options, overwriting old values - config = deep_merge_dicts(configuration._load_config_file(path), config) + config = deep_merge_dicts(load_config_file(path), config) config['stake_currency'] = '' # Ensure we do not use Exchange credentials From 28d8fc871a14418a44521f2635809ad66dbbff3f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 15:15:09 +0300 Subject: [PATCH 926/928] tests adjusted --- freqtrade/configuration/load_config.py | 30 ++++++++++++++++++++++++++ freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_configuration.py | 14 ++++++------ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 freqtrade/configuration/load_config.py diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py new file mode 100644 index 000000000..25504144f --- /dev/null +++ b/freqtrade/configuration/load_config.py @@ -0,0 +1,30 @@ +""" +This module contain functions to load the configuration file +""" +import json +import logging +import sys +from typing import Any, Dict + +from freqtrade import OperationalException + + +logger = logging.getLogger(__name__) + + +def load_config_file(path: str) -> Dict[str, Any]: + """ + Loads a config file from the given path + :param path: path as str + :return: configuration as dictionary + """ + try: + # Read config from stdin if requested in the options + with open(path) if path != '-' else sys.stdin as file: + config = json.load(file) + except FileNotFoundError: + raise OperationalException( + f'Config file "{path}" not found!' + ' Please create a config file or check whether it exists.') + + return config diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 71ed23901..88257bcae 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -45,7 +45,7 @@ def get_args(args): def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( - 'freqtrade.configuration.configuration.Configuration._load_config_file', + 'freqtrade.configuration.load_config.load_config_file', lambda *args, **kwargs: config ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 1e76297a6..d4c8704be 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -15,6 +15,7 @@ from freqtrade.configuration import Arguments, Configuration 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.load_config import load_config_file from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers from freqtrade.state import RunMode @@ -26,8 +27,7 @@ from freqtrade.tests.conftest import (log_has, log_has_re, def all_conf(): config_file = Path(__file__).parents[2] / "config_full.json.example" print(config_file) - configuration = Configuration(Namespace()) - conf = configuration._load_config_file(str(config_file)) + conf = load_config_file(str(config_file)) return conf @@ -53,12 +53,11 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: - file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open( + file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - configuration = Configuration(Namespace()) - validated_conf = configuration._load_config_file('somefile') + validated_conf = load_config_file('somefile') assert file_mock.call_count == 1 assert validated_conf.items() >= default_conf.items() @@ -114,7 +113,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: configsmock = MagicMock(side_effect=config_files) mocker.patch( - 'freqtrade.configuration.configuration.Configuration._load_config_file', + 'freqtrade.configuration.load_config.load_config_file', configsmock ) @@ -154,10 +153,9 @@ def test_load_config_file_exception(mocker) -> None: 'freqtrade.configuration.configuration.open', MagicMock(side_effect=FileNotFoundError('File not found')) ) - configuration = Configuration(Namespace()) with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'): - configuration._load_config_file('somefile') + load_config_file('somefile') def test_load_config(default_conf, mocker) -> None: From ab092fc77f291e1b5de9356729bf692803d59ca3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Aug 2019 15:45:41 +0200 Subject: [PATCH 927/928] Reinstate comment on backesting data --- docs/data-analysis.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 66bb50694..ecd94445b 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -17,6 +17,8 @@ df = load_backtest_data("user_data/backtest_data/backtest-result.json") df.groupby("pair")["sell_reason"].value_counts() ``` +This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. + ### Load live trading results into a pandas dataframe ``` python From 48d83768787a7222b30b04b3d40289df143f24ab Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 10 Aug 2019 18:47:58 +0300 Subject: [PATCH 928/928] tests fixed --- freqtrade/tests/conftest.py | 2 +- freqtrade/tests/test_configuration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 88257bcae..4b9bf6cd8 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -45,7 +45,7 @@ def get_args(args): def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( - 'freqtrade.configuration.load_config.load_config_file', + 'freqtrade.configuration.configuration.load_config_file', lambda *args, **kwargs: config ) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index d4c8704be..e325a0de2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -113,7 +113,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: configsmock = MagicMock(side_effect=config_files) mocker.patch( - 'freqtrade.configuration.load_config.load_config_file', + 'freqtrade.configuration.configuration.load_config_file', configsmock )