From 7166a474ae9b58259a25c356d8e16f01213b63d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Mar 2019 19:54:16 +0100 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 8afce7e65105c07364991b4ab48ee718e10530eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Mar 2019 16:26:38 +0100 Subject: [PATCH 14/18] 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 7307084dfd5c0ec955278e285d464203d71a13bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Mar 2019 16:21:58 +0100 Subject: [PATCH 15/18] 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 16/18] 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 17/18] 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 18/18] 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